14.3.2. Сторожевой таймер

Аппаратный сторожевой таймер – это основа, на которой держится любой другой выбор в области защиты. Это крошечный независимый таймер, который сбрасывает процессор, когда ему слишком долго не сообщали обратного. Скрипт, который застрял на ненадёжном датчике, сетевой вызов, который блокируется дольше своего таймаута, аллокатор памяти, застрявший в углу кучи, исключение, ускользнувшее из цикла – ни одно из них не останавливает сторожевой таймер. Таймер отсчитывает независимо, и камера перезагружается.

Для выпускаемого продукта сторожевой таймер не является опциональным. Без него любой из вышеперечисленных режимов отказа оставляет камеру мёртвой до тех пор, пока кто-то не заметит и не выключит и снова не включит питание. С ним камера сама возвращается к работе, и единственное свидетельство отказа – одна строка в журнале.

См. также

Страница сторожевой таймер в главе об оборудовании рассказывает, что такое сторожевой таймер на аппаратном уровне, и об основах API machine.WDT. Эта страница рассказывает о том, что меняется для производственного развёртывания.

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 – ничто из этого не должно срабатывать на сторожевом таймере. Реальное зависание (бесконечный цикл, заблокированный вызов ввода-вывода) должно. Слишком короткие таймауты превращают сторожевой таймер в источник ложных сбросов; слишком длинные таймауты позволяют камере оставаться неотзывчивой в течение минут, прежде чем сработает восстановление.

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 stdout, который никто не читает, а любое состояние в памяти, которое хранило приложение, теряется.

Оборачивание основного цикла в 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 – это то, что говорит полевой команде, действительно ли сработало восстановление, или камера просто нормально перезапитывалась.

Строка причины сброса – это то, что делает работу сторожевого таймера видимой в журнале. Журнал, полный сбросов 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() – это работа приложения за одну итерацию; остальная часть этого каркаса не меняется между продуктами. Защита – это один сторожевой таймер, одна обёртка и одна зарегистрированная загрузка при каждом холодном запуске – не так много кода, и разница между камерой, которая восстанавливается сама, и той, которая требует выезда сервиса.