14.3.2. 看門狗¶
硬體看門狗是其他每一項強化選擇所立足的底層基礎。它是一個微小且獨立的計時器,當處理器太久沒有另行通知時,它就會將其重置。一個卡在不穩定感測器上的指令碼、一個阻塞超過逾時限制的網路呼叫、一個卡在堆積某個角落的記憶體配置器、一個逃出迴圈的例外——這些都無法讓看門狗停下來。計時器無論如何都會倒數,而 cam 會重新開機。
對於出貨的產品而言,看門狗並非選配。沒有它,上述任何一種失效模式都會讓 cam 死掉,直到有人察覺並重新開關電源。有了它,cam 會自行恢復運作,而失效留下的唯一證據就是記錄檔中的一行。
請參閱
硬體章節的 看門狗計時器 頁面,說明了看門狗在硬體層面是什麼,以及 machine.WDT API 的基礎。本頁說明的是在量產部署中有哪些改變。
14.3.2.1. 啟動看門狗¶
machine.WDT 就是這個 API。它由硬體支撐:一旦建構完成,計時器就會持續運行直到下一次重置。沒有 stop()、沒有 deinit()、也沒有 Ctrl-C 可以逃脫。這正是重點所在。
在 main.py 頂端的典型設定,緊接在它所保護的迴圈之前:
from machine import WDT
wdt = WDT(timeout=10_000) # milliseconds
main.py 是看門狗的合適安身之處,因為迴圈就住在那裡。看門狗重置是一次硬體重置,所以冷開機路徑會重新執行,main.py 會自行重新進入迴圈——恢復機制無需在 boot.py 中做任何接線就能運作。改在 boot.py 中啟動看門狗,意味著每一次軟重置(例如開發者按下的 Ctrl-D)都會交給應用程式一個它無法停止的硬體計時器,這在工作檯上是個惱人之事,而在迴圈尚未就緒前就執行的量產設定程式碼中則是個陷阱。
將逾時設定為主迴圈最差觀測迭代時間的 2 到 3 倍。影格率抖動、冷感測器上一次緩慢的感測器讀取、短暫的 Wi-Fi 卡頓——這些都不該觸發看門狗。真正的當機(無窮迴圈、被阻塞的 I/O 呼叫)則應該觸發。逾時設得太短會讓看門狗變成誤重置的來源;設得太長則會讓 cam 在恢復機制啟動前枯坐數分鐘無回應。
14.3.2.2. 餵食看門狗¶
wdt.feed() 會重置倒數計時。在主迴圈的每一次迭代中呼叫它一次,放在迴圈本體的頂端,這樣餵食動作就會在任何可能當機的工作之前無條件發生:
while True:
wdt.feed()
frame = csi0.snapshot()
process(frame)
14.3.2.3. 在例外中存活¶
看門狗處理的是當機。例外則是另一種失效模式。未處理的例外會向上冒泡到指令碼的最頂層,main.py 結束,cam 掉回 REPL。看門狗接著會在其逾時後觸發,因為 REPL 中沒有任何東西在餵食它,cam 重置,main.py 再次執行——所以恢復機制確實有效,但現場每次當機都要付出一整段逾時加上重新開機的代價,回溯訊息送往沒有人讀取的 USB 標準輸出,而應用程式所保留的任何記憶體內狀態都消失了。
將主迴圈包在一個最頂層的 try / except 中,能把當機變成一個應用程式可繼續執行下去、被記錄下來的事件,而無需付出重置的代價:
import logging
log = logging.getLogger(__name__)
while True:
wdt.feed()
try:
frame = csi0.snapshot()
process(frame)
except Exception:
log.exception("frame loop iteration failed")
捕捉 Exception(而非 BaseException)能讓 KeyboardInterrupt 與 SystemExit 維持運作,這正是透過 USB 連接的開發者所想要的。
這個模式是存活性的軟體那一半:看門狗捕捉當機,包裹器捕捉崩潰,記錄檔記下兩者中任一所捕捉到的事件。
14.3.2.4. 知道開機為何發生¶
每一次軟重置與每一次看門狗重置,最終都會以一次全新的開機呈現出來。開機時診斷輔助工具會在每次冷啟動時記錄 machine.reset_cause();reset cause 這一行正是告訴現場恢復機制究竟是真的啟動了,還是 cam 只是正常地重新開關電源。
重置原因這一行,正是讓看門狗的工作在記錄檔中可見的東西。一份滿是 watchdog timeout 重置的記錄檔,說明應用程式一直在當機,而看門狗一直在恢復它。一份沒有這些的記錄檔,說明看門狗未曾需要觸發——這通常意味著應用程式很健康,但也可能意味著逾時設得太長,以致無法捕捉到實際正在發生的當機。
14.3.2.5. 一個完整的起步範本¶
一個把看門狗、記錄設定、開機時診斷與包裹器整合在一起的 main.py 看起來像這樣:
import logging
from machine import WDT
from app.logging_setup import setup_logging, log_boot_diagnostics
setup_logging('/sdcard/logs/app.log')
log_boot_diagnostics()
log = logging.getLogger(__name__)
wdt = WDT(timeout=10_000)
while True:
wdt.feed()
try:
step()
except Exception:
log.exception("loop iteration failed")
step() 是應用程式每次迭代的工作;這個鷹架的其餘部分在不同產品之間不會改變。強化就是一個看門狗、一個包裹器,以及每次冷啟動時記錄一筆開機——程式碼不多,卻是一個能自行恢復的 cam 與一個需要叫服務的 cam 之間的差別。