14.3.2. 看门狗

硬件看门狗是其他所有加固选择所依托的基础。它是一个微小的独立定时器,当处理器在过长时间内没有被告知一切正常时,它就会复位处理器。卡在不稳定传感器上的脚本、阻塞超时的网络调用、卡在堆某个角落里的内存分配器、逃出循环的异常——这些都无法阻止看门狗。无论如何,定时器都会持续倒计时,摄像头随之重启。

对于已发售的产品而言,看门狗并非可有可无。没有它,上述任何一种故障模式都会让摄像头陷入死机,直到有人发现并重新上电。有了它,摄像头会自行恢复运行,而故障留下的唯一证据就是日志中的一行记录。

参见

硬件章节的 看门狗定时器 页面介绍了看门狗在硬件层面是什么,以及 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 调用)则应当触发。超时过短会让看门狗成为误复位的来源;超时过长则会让摄像头无响应地停滞数分钟才触发恢复。

14.3.2.2. 喂狗

wdt.feed() 会重置倒计时。在主循环的每次迭代中调用它一次,放在循环体的顶部,这样喂狗就会在任何可能死机的工作之前无条件执行:

while True:
    wdt.feed()
    frame = csi0.snapshot()
    process(frame)

14.3.2.3. 应对异常

看门狗处理的是死机。异常则是另一种故障模式。未处理的异常会冒泡到脚本的顶层,main.py 退出,摄像头跌落到 REPL。随后看门狗会在其超时后触发,因为没有任何东西从 REPL 中喂它,摄像头复位,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)可以让 KeyboardInterruptSystemExit 继续正常工作,这正是通过 USB 连接的开发者所希望的。

这种模式是活跃性(liveness)的软件部分:看门狗捕获死机,包装器捕获崩溃,而日志记录下两者各自捕获到的内容。

14.3.2.4. 了解一次启动发生的原因

每一次软复位和每一次看门狗复位最终都会表现为一次全新的启动。启动时诊断辅助函数会在每次冷启动时记录 machine.reset_cause()reset cause 这一行正是用来告诉现场恢复是否真正触发,还是摄像头只是正常地重新上电。

复位原因这一行正是让看门狗的工作在日志中可见的关键。一份充满 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() 是应用程序每次迭代的工作内容;这套脚手架的其余部分在不同产品之间不会改变。加固就是一个看门狗、一个包装器,以及每次冷启动时记录的一条启动日志——代码量不大,却造就了能自行恢复的摄像头与需要上门维修的摄像头之间的差别。