14.3.2. El watchdog

El watchdog de hardware es la base sobre la que se asienta cualquier otra decisión de blindaje. Es un pequeño temporizador independiente que reinicia el procesador cuando no se le ha indicado lo contrario durante demasiado tiempo. Un script que se queda atascado en un sensor inestable, una llamada de red que se bloquea más allá de su tiempo de espera, un asignador de memoria atascado en un rincón del montículo, una excepción que escapó del bucle: ninguno de ellos detiene el watchdog. El temporizador sigue contando hacia atrás de todos modos, y la cámara se reinicia.

Para un producto que se envía, un watchdog no es opcional. Sin él, cualquiera de los modos de fallo anteriores deja la cámara muerta hasta que alguien lo nota y la apaga y enciende. Con él, la cámara vuelve a arrancar por sí sola y la única prueba del fallo es una línea en el registro.

Ver también

La página de la temporizador watchdog del capítulo de hardware explica qué es un watchdog a nivel de hardware y los fundamentos de la API machine.WDT. Esta página cubre lo que cambia para un despliegue en producción.

14.3.2.1. Iniciar el watchdog

machine.WDT es la API. Está respaldada por hardware: una vez construido, el temporizador se ejecuta hasta el siguiente reinicio. No hay stop(), ni deinit(), ni escape con Ctrl-C. Esa es la idea.

Una configuración típica al principio de main.py, justo antes del bucle que protege:

from machine import WDT

wdt = WDT(timeout=10_000)              # milliseconds

main.py es el lugar adecuado para el watchdog porque es donde vive el bucle. Un reinicio del watchdog es un reinicio de hardware, por lo que la ruta de arranque en frío se vuelve a ejecutar y main.py vuelve a entrar en el bucle por sí mismo: la recuperación funciona sin necesidad de nada en boot.py. Iniciar el watchdog en boot.py significa, en cambio, que cada reinicio en caliente (un Ctrl-D de un desarrollador, por ejemplo) le entrega a la aplicación un temporizador de hardware que no tiene forma de detener, lo cual es una molestia en el banco de pruebas y una trampa en el código de configuración de producción que se ejecuta antes de que el bucle esté listo.

Elige el tiempo de espera para que sea de 2 a 3 veces más largo que el peor tiempo de iteración observado del bucle principal. La fluctuación de la velocidad de fotogramas, una lectura lenta de un sensor frío, un breve corte del Wi-Fi: ninguno de esos debería disparar el watchdog. Un bloqueo real (un bucle infinito, una llamada de E/S bloqueada) sí debería hacerlo. Los tiempos de espera demasiado cortos convierten el watchdog en una fuente de reinicios falsos; los tiempos de espera demasiado largos dejan la cámara sin responder durante minutos antes de que se active la recuperación.

14.3.2.2. Alimentarlo

wdt.feed() reinicia la cuenta atrás. Llámalo una vez por iteración del bucle principal, al principio del cuerpo del bucle para que la alimentación se produzca incondicionalmente antes de cualquier trabajo que pueda quedarse colgado:

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

14.3.2.3. Sobrevivir a las excepciones

El watchdog se encarga de los bloqueos. Las excepciones son un modo de fallo distinto. Una excepción no controlada sube hasta el nivel superior del script, main.py finaliza y la cámara cae al REPL. El watchdog se dispara entonces tras su tiempo de espera porque nada lo alimenta desde el REPL, la cámara se reinicia y main.py se ejecuta de nuevo, de modo que la recuperación sí funciona, pero el campo paga un tiempo de espera completo más un reinicio por cada fallo, el rastreo va a la salida estándar de USB que nadie lee, y se pierde cualquier estado en memoria que la aplicación estuviera conservando.

Envolver el bucle principal en un try / except de nivel superior convierte un fallo en un evento registrado que la aplicación supera, sin pagar el coste de un reinicio:

import logging

log = logging.getLogger(__name__)

while True:
    wdt.feed()
    try:
        frame = csi0.snapshot()
        process(frame)
    except Exception:
        log.exception("frame loop iteration failed")

Capturar Exception (no BaseException) mantiene en funcionamiento KeyboardInterrupt y SystemExit, que es lo que quiere un desarrollador conectado por USB.

Este patrón es la mitad de software de la vivacidad: el watchdog captura los bloqueos, el envoltorio captura los fallos y el registro anota lo que cualquiera de ellos capturó.

14.3.2.4. Saber por qué se produjo un arranque

Cada reinicio en caliente y cada reinicio del watchdog acaban apareciendo como un nuevo arranque. El ayudante de diagnóstico de tiempo de arranque registra machine.reset_cause() en cada arranque en frío; la línea reset cause es lo que le indica al campo si la recuperación se activó realmente frente a la cámara simplemente apagándose y encendiéndose de forma normal.

La línea de causa del reinicio es lo que hace visible en el registro el trabajo del watchdog. Un registro lleno de reinicios por watchdog timeout indica que la aplicación ha estado quedándose colgada y que el watchdog la ha estado recuperando. Un registro sin ellos indica que el watchdog no ha tenido que dispararse, lo que normalmente significa que la aplicación está sana, pero también puede significar que el tiempo de espera está configurado demasiado largo para captar los bloqueos que realmente están ocurriendo.

14.3.2.5. Un punto de partida completo

Un main.py que reúne el watchdog, la configuración del registro, el diagnóstico de tiempo de arranque y el envoltorio tiene este aspecto:

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() es el trabajo por iteración de la aplicación; el resto de este andamiaje no cambia entre productos. El blindaje es un watchdog, un envoltorio y un arranque registrado en cada arranque en frío: no es mucho código, y es la diferencia entre una cámara que se recupera por sí sola y una que necesita una llamada de servicio.