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 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")

BaseException 이 아니라 Exception 을 잡으면 KeyboardInterruptSystemExit 가 계속 동작하는데, 이는 USB로 연결된 개발자가 원하는 바입니다.

이 패턴은 라이브니스의 소프트웨어 절반입니다. 워치독은 멈춤을 잡고, 래퍼는 크래시를 잡으며, 로그는 둘 중 무엇이 무엇을 잡았는지를 기록합니다.

14.3.2.4. 부팅이 일어난 이유 알기

모든 소프트 리셋과 모든 워치독 리셋은 결국 새로운 부팅으로 나타납니다. 부트 타임 진단 헬퍼는 모든 콜드 스타트마다 machine.reset_cause() 를 로그에 남깁니다. reset cause 줄은 복구가 실제로 발동했는지, 아니면 cam이 그냥 정상적으로 전원을 껐다 켰는지를 현장에 알려주는 것입니다.

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() 은 애플리케이션의 반복당 작업입니다. 이 스캐폴드의 나머지는 제품마다 바뀌지 않습니다. 하드닝은 하나의 워치독, 하나의 래퍼, 그리고 모든 콜드 스타트마다 기록되는 부팅입니다. 많은 코드는 아니지만, 스스로 복구하는 cam과 서비스 호출이 필요한 cam의 차이를 만듭니다.