2021年9月22日 星期三

ESP32 CAMRA 二維碼(QR Code)辨識之門鎖控制 05-狀態控制工作流程

網頁最後修改時間:2021/09/22

經過前面幾篇網頁的介紹,ESP32 CAMRA 二維碼(QR Code)辨識之門鎖控制(下面簡稱 ESP32QRDoorLock)進入到最後的一篇。本篇將整合所有的部分,利用狀態轉移的概念方式,管理整個門鎖動作的工作流程。

內容有:


【(00)簡介】

本篇會建立一個負責管理門鎖狀態的工作任務,來管理狀態之間的轉換工作。

工作任務裡各狀態之間的轉換,取決於無線網路連線情形、二維碼辨識結果和門鎖開關位置,經由這些狀態決定 LED、電磁鎖的動作。

x04_ESP32CamQRCodeReader.ino 為基礎,首先在 lsInterrupt 區域的上方插入狀態控制工作任務的控制代碼(handle)的全局變數。

94
95
/*================== TASK:  DEVICESTATECONTROL =================*/
TaskHandle_t xDeviceStateControlTaskHandle;

FASTLED 區域的下方,宣告和定義一個列舉型別的全局變數 xDeviceState,裡面有八個預先定義的裝置狀態,初始值設定為 STARTUP

126
127
128
129
130
131
132
133
134
135
136
137
138
/*================== STATE =================*/
enum device_state_t{
    STARTUP = 0,         // 開機模式或等同於 OFF
    STANDBY_WO_WIFI,     // 與待機模式同,但 WIFI 沒有連線到無線分享器
                         // ,也就是只啟用 QRCode 功能
    STANDBY,             // 待機模式: 門鎖住,等待辨識二維碼
                         // ,也就是啟用 ORCode 和 WiFi 功能
    QRCODEMATCH,         // 二維碼辨識比對匹配
    QRCODEUNMATCH,       // 二維碼辨識比對不符
    QRCODEINVALID,       // 無效二維碼
    UNLOCK,              // 門已開啟,等待關門且取修二維碼辨識的動作
    LOCK,                // 門已關閉,開啟二維碼辨識功能
} xDeviceState = STARTUP;

狀態轉換需要的全局變數宣告和定義好了之後,下面就可以開始撰寫這部分的的程式。


【(01)LED blink () 程式碼變更】

此系列前面幾篇所使用的 blink () 功能簡單,能設定閃爍次數和顏色已很足夠。但對於 ESP32QRDoorLock 來說,blink () 不只需要可設定閃爍次數和顏色的功能,還要可設定延遲的時間。如此,LED 的操作才會簡單、變化才會多樣。

修改之後的 blink () 函式,除了支援上面的所說的功能之外,還提供三種不同的閃爍方式:

  • blink_count < 0:不管延遲時間,顯示指定的顏色直到下一個 blink () 被執行,通常用來關閉 LED;
  • blink_count = 0:保持恆亮,直到延遲時間逾時再關閉;
  • blink_count > 0:閃爍指定的次數,亮滅間隔一個延遲時間;

 169 
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
/**========================================================================
 **                           blink()
 *?  系統運行狀態的指示
 *@param blink_count const int8_t  
 *@param delay       const TickType_t 
 *@param color       CRGB 
 *========================================================================**/
void blink (const int8_t blink_count,   // 閃爍次數
            const TickType_t delay,     // 延遲時間 (ms)
            const CRGB color)           // WS2812 顏色
{
    uint8_t state = LOW;

    if (blink_count < 0) {              // -1, OFF
        ws2812[0] = color;
        FastLED.show();
    } else if (blink_count == 0 ) {     // 0, 保持恆亮 delay (ms)
        ws2812[0] = color;
        FastLED.show();
        vTaskDelay (pdMS_TO_TICKS(delay));
        ws2812[0] = BLACK;
        FastLED.show();
    } else {                            // >= 1
        for (int x = 0; x < (blink_count << 1); ++x) {
            if (state ^= HIGH) {
                ws2812[0] = color;
            } else {
                ws2812[0] = BLACK;
            }
            FastLED.show();
            vTaskDelay (pdMS_TO_TICKS(delay));
        }
    }
}

在這裡修改的 blink (),主要用於門鎖狀態控制的工作任務中。


【(02)門鎖狀態控制】

下面是 ESP32QRDoorLock 狀態轉移示意圖。依列舉型別所定義的,有九個狀態。工作流程由無線網路狀態(xWifiReconnectState)、二維碼辨識結果(xOnQrCodeState)、門鎖開關狀態和中斷旗標(bIntTrigger)作為轉換的判斷依據。

下面說說這整個狀態轉移工作流程,同時這也是 deviceStateControlTask 撰寫的依據。

開機啟動後的 xOnQrCodeState 預設值是 STARTUP,以 xWifiReconnectState 為主要判斷依據,再根據門鎖開關狀態,決定下一個轉換的狀態是 STANDBY_NO_WIFIUNLOCK 或 STANDBY 的哪一個?或還是留在當前的狀態中。

若由 STARTUP 進入到 STANDBY_NO_WIFI 狀態,表示無線網路超過 15 秒還未能正常連線。進入到此狀態後(LED 每秒閃爍一次黃色),每隔 10 秒還會再做一次連線狀態檢查。若在 Watchdog 計時器逾時重置之前連線,則會跳回到 STARTUP,否則就只能等待被重置。

若由 STARTUP 進入到 UNLOCK 狀態,表示在前一個狀態做檢查的時候,門是開啟的。這時會開啟中斷服務函式,然後一直等待,直到門被關閉(門鎖開關檢測到關閉;LED 每 800ms 閃爍一次紅色,共三次),才會轉換到下一個 LOCK 的狀態。LOCK 狀態的動作很簡單,單純切換回 STARTUP 狀態,並輸出門關閉的訊息。

如果無線網路連線以及門鎖開關狀態都正常,那麼就會從 STARTUP 直接切換到 STANDBY 的狀態(LED 每秒亮滅一次藍色)。在這個狀態中,主要會檢查兩個切換到下一個狀態的條件:門鎖開關的位置和二維碼辨識的結果。

在 STANDBY 的狀態中,首先檢查的是門鎖開關是否在關閉的位置。如果不是,則切換到 UNLOCK 狀態(LED 每 200ms 閃爍一次紅色,共兩次)等待門被關閉;若門已關閉,則根據二維碼辨識 xOnQrCodeState 的三種結果(MATCHUNMATCHINVALID),切換到相對應的狀態。

在 STANDBY 的狀態中,若二維碼辨識結果為 MATCH,則切換狀態至 QRCODEMATCH。首先執行開鎖動作,接著 LED 綠色亮兩秒關閉,最後檢查門鎖狀態。若門判斷已開啟,則切換至 UNLOCK 狀態;若門判斷未開,則表示開鎖的動作未完成或是出現錯誤,LED 每 400ms 閃爍一次紅色(共三次),再切換狀態至 STARTUP

在 STANDBY 的狀態中,若二維碼辨識結果為 UNMATCH,則切換狀態至 QRCODEUNMATCH。LED 常亮紅色 1500ms,再切換狀態至 STARTUP

在 STANDBY 的狀態中,若二維碼辨識結果為 INVALID,則切換狀態至 QRCODEINVALID。LED 每 500ms 閃爍一次橙色(共二次),再切換狀態至 STARTUP

'將上面所說的狀態轉換的概念,轉換為如下的程式碼。

375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
/**========================================================================
 **                           systemStateControlTask()
 *?  系統狀態控制的任務
 *@param pvParameters void*  
 *========================================================================**/ 
void deviceStateControlTask (void *pvParameters) {

    while (true) {
        switch (xDeviceState) {
            case STARTUP:
            {
                //* 這裡分別使用 xOnQrCodeState, xWifiReconnectState 和
                //  無線網路連線狀態做判斷,最後得出 xDeviceState 的結果。
                static int t = 0;
                if (xWifiReconnectState == CONNECTING) {
                    vTaskDelay (pdMS_TO_TICKS(200));
                    if (t++ >= 75) {
                        t = 0;
                        //* 每 200ms 亮滅一次,連續三次,橙色
                        blink (3, 100, ORANGE);
                        if (dDoorState == CLOSE) {
                            xDeviceState = STANDBY_WO_WIFI;
                            Serial.println (F("device state change to STANDBY_WO_WIFI"));
                        } else {
                            xDeviceState = UNLOCK;
                            Serial.println (F("\nDOOR OPENED"));
                            Serial.println (F("device state change to UNLOCK"));
                        }
                    }
                } else if (xWifiReconnectState == CONNECTED || 
                            xWifiReconnectState == RUNNING) {
                    t = 0;
                    //* 連線成功,每 200ms 亮滅一次,連續五次,紫色 
                    blink (5, 100, VIOLET);
                    if (dDoorState == CLOSE) {
                        xDeviceState = STANDBY;
                        Serial.println (F("device state change to STANDBY"));
                    } else {
                        xDeviceState = UNLOCK;
                        Serial.println (F("\nDOOR OPENED"));
                        Serial.println (F("device state change to UNLOCK"));
                    }
                } else if (xWifiReconnectState == DISCONNECTED) {
                    blink (-1, 0, BLACK);
                    Serial.println (F("detect wifi disconnected"));
                } else {}
            }
                break;
            case STANDBY_WO_WIFI: 
            {
                static int t = 0;
                //* 每秒亮滅一次, 黃色
                blink (1, 500, YELLOW);
                //* 如果跳到這個狀態的同時無線網路連線上了,就要跳回起頭重新檢查一下無線網路狀態
                if (t > 9 &&
                    (xWifiReconnectState != CONNECTED || xWifiReconnectState != RUNNING) ) {
                    t = 0;
                    xDeviceState = STARTUP;
                    Serial.println (F("device state change to STARTUP"));
                } else {
                    t++;
                }
            }
                break;
            case STANDBY:
                //* 每秒亮滅一次, 藍色
                blink (1, 500, BLUE);
                //* 檢測門鎖狀態 
                if (dDoorState == OPEN) {
                    xDeviceState = UNLOCK;
                    Serial.println (F("Error detected: door open in STANBY"));
                    blink (2, 200, RED);
                    Serial.println (F("device state change to UNLOCK"));
                    break;
                }
                //* 檢測是否符合狀態轉移的條件
                if (xOnQrCodeState == MATCH) {
                    xDeviceState = QRCODEMATCH;
                    Serial.println (F("device state change to QRCODEMATCH"));
                    xOnQrCodeState = WORKING;
                } else if (xOnQrCodeState == UNMATCH) {
                    xDeviceState = QRCODEUNMATCH;
                    Serial.println (F("device state change to QRCODEUNMATCH"));
                    xOnQrCodeState = WORKING;
                } else if (xOnQrCodeState == INVALID) {
                    xDeviceState = QRCODEINVALID;
                    Serial.println (F("device state change to QRCODEINVALID"));
                    xOnQrCodeState = WORKING;
                } else {}
                break;
            case QRCODEMATCH:
                doorControl (false);
                blink (0, 2000, GREEN);
                if (dDoorState == OPEN) {
                    xDeviceState = UNLOCK;
                    Serial.println (F("device state change to UNLOCK"));
                }
                else {
                    //* 每 400ms 閃爍一次,共三次,紅色。        
                    blink (3, 200, RED);
                    Serial.println(F("ERROR: The door can not be opened!"));
                    xDeviceState = STARTUP;
                    Serial.println (F("device state change to STARTUP"));
                }
                break;
            case QRCODEUNMATCH:
            {
                //* 常亮兩秒, 紅色 
                blink (0, 1500, RED);
                xDeviceState = STARTUP;
                Serial.println (F("device state change to STARTUP"));
            }
                break;
            case QRCODEINVALID:
            {
                //* 每 500ms 閃爍一次,共兩次,橙色。
                blink (2, 250, ORANGE);
                xDeviceState = STARTUP;
                Serial.println (F("device state change to STARTUP"));
            }
                break;
            case UNLOCK:
                // 偵測關門的中斷
                attachInterrupt (digitalPinToInterrupt(LIMITSWITCH), lsInterrupt, FALLING);
                Serial.println (F("waiting for door close..."));
                while (dDoorState == OPEN || bIntTrigger == false);
                //* 每 800ms 閃爍一次,共三次,紅色。  
                blink (3, 400, RED);
                bIntTrigger = false;
                xDeviceState = LOCK;
                Serial.println (F("done!\ndevice state change to LOCK"));
                break;
            case LOCK:
            {
                xDeviceState = STARTUP;
                Serial.println (F("done!\ndevice state change to STARTUP"));
            }
                break;
            default:
                Serial.println (F("deviceStateControlTask: wrong case"));
                break;        
        }
        vTaskDelay (pdMS_TO_TICKS(100));
    }
}

最後一個動作。將上面的 deviceStateControlTask,加入到 setup () 中做為工作任務。

558
559
560
561
562
563
564
565
566
    /*======= SYSTEMSTATECONTROLTASK =======*/
    //* Core 0, Priority 1
     xTaskCreatePinnedToCore (
        deviceStateControlTask, "deviceStateControlTask",
        usStackDepth, NULL,     // usStackDepth 請自行測試該值大小
        1,                                   // Priotity 1
        &xDeviceStateControlTaskHandle, 0    // Core 0
    );
    delay (250);


【(03)參考程式碼】

把上面的程式碼插入到 x04_ESP32CamQRCodeReader.ino 中,就完成整個程式的撰寫。

編譯、上傳成功後,就會如下圖輸出類似的開機訊息。

二維碼辨識成功後,狀態切換工作流程所輸出的訊息。

此篇所提供的 ESP32QRDoorLock 參考程式碼,基本上已能與展示影片提供相同的操作(二維碼辨識和狀態切換),不過需要先自行補上 GPIO 的號碼,以及為 wifiReconnectTaskdeviceStateControlTask 這兩個工作任務提供合適的 usStackDepth 堆疊大小後才行。

完整版本的動作輸出,看 "新產品上市-ESP32-CAM 燒錄與接腳擴充二合一底板介紹、使用方法和實作展示" 網頁最下方的展示影片。


【(04)結論】

到這篇,已經完成 ESP32-CAM 二維碼辨識之門鎖控制一系列的全部網頁撰寫,若完成硬體配線後在測試過程中有任何問題,歡迎在下面留言詢問。


.

.


<<部落格相關文章>>


    .

    .

    沒有留言:

    張貼留言

    留言屬名為"Unknown"或"不明"的用戶,大多這樣的留言都會直接被刪除掉,不會得到任何回覆!

    發問問題,請描述清楚你(妳)的問題,別人回答前不會想去 "猜" 問題是什麼?

    不知道怎麼發問,請看 [公告] 部落格提問須知 - 如何問問題 !