14.3.2. O watchdog

O watchdog de hardware é a base sobre a qual assenta todas as outras escolhas de proteção. É um pequeno temporizador independente que reinicia o processador quando não foi informado em contrário por demasiado tempo. Um script que bloqueia num sensor instável, uma chamada de rede que bloqueia após o seu timeout, um alocador de memória preso num canto do heap, uma exceção que escapou ao loop – nenhum deles para o watchdog. O temporizador conta decrescentemente independentemente, e a câmara reinicia.

Para um produto enviado, um watchdog não é opcional. Sem ele, qualquer um dos modos de falha acima deixa a câmara parada até que alguém repare e a ligue de novo. Com ele, a câmara recupera por si própria e a única evidência da falha é uma linha no registo.

Veja também

A página do temporizador watchdog no capítulo de hardware aborda o que é um watchdog ao nível do hardware e as noções básicas da API machine.WDT. Esta página aborda o que muda numa implementação em produção.

14.3.2.1. Iniciar o watchdog

machine.WDT é a API. É suportada por hardware: uma vez construído, o temporizador corre até ao próximo reinício. Não existe stop(), nem deinit(), nem escape por Ctrl-C. Esse é o objetivo.

Uma configuração típica no início de main.py, imediatamente antes do loop que protege:

from machine import WDT

wdt = WDT(timeout=10_000)              # milliseconds

main.py é o local correto para o watchdog porque é aí que o loop reside. Um reinício por watchdog é um reinício de hardware, pelo que o caminho de arranque a frio é re-executado e main.py re-entra no loop autonomamente – a recuperação funciona sem qualquer ligação em boot.py. Iniciar o watchdog em boot.py significa que cada reinício suave (um Ctrl-D de um programador, por exemplo) entrega à aplicação um temporizador de hardware que não tem forma de parar, o que é um incómodo no banco de trabalho e uma armadilha no código de configuração de produção que corre antes de o loop estar pronto.

Escolha o timeout para ser 2 a 3 vezes mais longo do que o pior tempo de iteração observado do loop principal. Jitter na taxa de fotogramas, uma leitura lenta de sensor num sensor frio, uma breve falha de Wi-Fi – nenhum destes deve disparar o watchdog. Um bloqueio real (um loop infinito, uma chamada de E/S bloqueada) deve. Timeouts demasiado curtos tornam o watchdog numa fonte de reinícios falsos; timeouts demasiado longos deixam a câmara sem resposta durante minutos antes de a recuperação disparar.

14.3.2.2. Alimentá-lo

wdt.feed() reinicia a contagem decrescente. Chame-o uma vez por iteração do loop principal, no início do corpo do loop para que a alimentação ocorra incondicionalmente antes de qualquer trabalho que possa bloquear:

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

14.3.2.3. Sobreviver a exceções

O watchdog trata dos bloqueios. As exceções são um modo de falha diferente. Uma exceção não tratada sobe ao nível superior do script, main.py termina e a câmara cai para o REPL. O watchdog dispara então após o seu timeout porque nada o está a alimentar a partir do REPL, a câmara reinicia e main.py corre novamente – portanto a recuperação funciona, mas o campo paga um timeout completo mais reinício por cada falha, o traceback vai para o stdout USB que nada lê e qualquer estado em memória que a aplicação estava a manter desaparece.

Envolver o loop principal numa instrução try / except de nível superior transforma uma falha num evento registado pelo qual a aplicação continua, sem pagar o custo de um reinício:

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 (não BaseException) mantém KeyboardInterrupt e SystemExit a funcionar, que é o que um programador ligado por USB pretende.

Este padrão é a metade de software da vivacidade: o watchdog captura os bloqueios, o invólucro captura as falhas e o registo regista o que qualquer um deles capturou.

14.3.2.4. Saber por que aconteceu um arranque

Cada reinício suave e cada reinício por watchdog acabam por aparecer como um arranque a frio. O auxiliar de diagnóstico no arranque regista machine.reset_cause() em cada arranque a frio; a linha reset cause é o que indica ao campo se a recuperação realmente disparou em comparação com a câmara a ciclar normalmente.

A linha da causa do reinício é o que torna o trabalho do watchdog visível no registo. Um registo cheio de reinícios watchdog timeout diz que a aplicação tem estado a bloquear e o watchdog tem estado a recuperá-la. Um registo sem eles diz que o watchdog não teve de disparar – o que normalmente significa que a aplicação está saudável, mas pode também significar que o timeout está demasiado longo para capturar os bloqueios que estão realmente a acontecer.

14.3.2.5. Um arranque completo

Um main.py que reúne watchdog, configuração de registo, diagnóstico no arranque e o invólucro tem este aspeto:

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() é o trabalho por iteração da aplicação; o resto deste andaime não muda entre produtos. A proteção é um watchdog, um invólucro e um arranque registado em cada arranque a frio – não muito código, e a diferença entre uma câmara que recupera autonomamente e uma que precisa de uma chamada de serviço.