14.3.1. Logování¶
Hotový produkt se nemůže hlásit domů pomocí print(). print() zapisuje na USB stdout, který existuje pouze tehdy, když kamera leží na stole vývojáře s otevřeným terminálem. V terénu jej nikdo nečte; každý řádek skončí nepovšimnut. Náhradou je knihovna logging – filtr úrovní, cíl podle volby aplikace a formát, který říká, co se stalo a kdy.
Modul logging na kameře je odlehčený port z CPythonu – stejný mentální model, menší rozsah a několik rozdílů, které mají pro produkční nastavení význam.
14.3.1.1. Mentální model¶
Logování se skládá ze čtyř částí. Každá část má jeden úkol; právě toto oddělení umožňuje, aby jeden logger rozváděl výstup do více cílů, každý s vlastním formátem a úrovní:
Logger je to, co aplikace volá. Kód říká
log.info("frame %d", n); logger je objekt, na kterém toto volání skončí. Loggery se vyhledávají podle názvu pomocílogging.getLogger().Handler rozhoduje, kam záznam putuje.
StreamHandlerzapisuje do streamu (ve výchozím stavusys.stderr);FileHandlerpřipojuje záznamy do souboru na disku. Logger může mít libovolný počet handlerů.Poznámka
Na kameře jsou
sys.stdoutasys.stderrzapojeny do téže USB CDC roury – zápisy do kteréhokoli z nich se objeví ve stejném terminálu, který má vývojář otevřený přes USB. Handler, který zapisuje dosys.stderr, je v praxi handler, který zapisuje na stejné místo jakoprint(). Abstrakce handleru vám přesto poskytuje filtrování a formátování pro každý cíl zvlášť; jen vám nedává fyzicky oddělený kanál.Formatter rozhoduje, jak se záznam vykreslí do textu. Vezme záznam a vrátí řádek, který se zapíše. Jeden formátovací řetězec na formatter; jeden formatter na handler.
Filtr úrovní sedí na každém loggeru a každém handleru. Záznamy nesou úroveň (
DEBUG/INFO/WARNING/ERROR/CRITICAL). Projdou pouze záznamy na úrovni filtru nebo vyšší.
Toto oddělení má význam, protože typické produkční nastavení má více než jeden cíl: soubor na SD kartě, který uchovává vše až po DEBUG pro pozdější analýzu, a stream na USB, který vynáší na povrch jen WARNING a horší, aby vývojář připojený ke kameře viděl ty nejdůležitější věci, aniž by se utopil v detailech. Stejný kód, dva cíle, dva filtry.
14.3.1.2. Úrovně a co každá z nich znamená¶
Pět úrovní tvoří uspořádanou škálu. Záznamy nesou úroveň, aby filtr na každém handleru mohl zahodit ty, o které se nestará.
DEBUG– trasování, čítače na snímek, výpisy vnitřního stavu. Nejnižší úroveň; objem je vysoký.INFO– běžné provozní události. Wi-Fi připojena, načten model, spuštěn watchdog, založen nový log soubor po rotaci.WARNING– něco neočekávaného, ale aplikace to zvládla. Zahozený snímek, opakovaný síťový požadavek.ERROR– operace selhala a aplikace ji nedokázala dokončit. Chybějící soubor s modelem, odmítnutý zápis na SD kartu.CRITICAL– aplikace vůbec nemůže pokračovat. Došla paměť, chybí povinné připojení.
Jeden důležitý výchozí stav, který je třeba si zapamatovat: modul logging na kameře spouští každý logger na úrovni WARNING. Záznamy na úrovni DEBUG a INFO jsou tiše zahazovány, dokud není zavolána Logger.setLevel() – obvykle jako součást volání basicConfig() níže. Častým prvním příznakem nastavení logování, které „nefunguje“, je to, že aplikace odeslala záznam na úrovni INFO a výchozí filtr jej spolkl.
Poznámka
Úroveň je jediný filtr, který logging na kameře nabízí. Neexistují žádné objekty Filter pro bohatší pravidla na úrovni jednotlivých záznamů; pokud úroveň záznamu projde, je vydán.
14.3.1.3. basicConfig: rychlý start¶
logging.basicConfig() nakonfiguruje kořenový logger jediným voláním. Nejčastěji se objevují dvě podoby:
Vývojové nastavení, vše na USB stderr na úrovni INFO
import logging
logging.basicConfig(level=logging.INFO)
Produkční nastavení, vše do souboru na SD kartě s formátem opatřeným časovým razítkem:
import logging
logging.basicConfig(
filename='/sdcard/logs/app.log',
level=logging.INFO,
format='%(asctime)s %(levelname)s %(name)s: %(message)s',
)
Předejte buď filename= pro FileHandler, nebo stream= pro StreamHandler; tyto dvě možnosti se v basicConfig() vzájemně vylučují.
Formátovací řetězec je šablona ve stylu %(field)s. Pole, která formatter na kameře podporuje:
%(asctime)s– časové razítko zformátované ztime.localtime(). Výchozí formát je%Y-%m-%d %H:%M:%S; pro přepsání předejtedatefmt=.%(levelname)s–DEBUG/INFO/WARNING/ERROR/CRITICAL.%(name)s– název loggeru (viz následující oddíl).%(message)s– zformátovaná zpráva záznamu.%(msecs)d– milisekundová část časového razítka záznamu.
Výchozí formát, pokud není žádný zadán, je %(levelname)s:%(name)s:%(message)s – což je v pořádku pro vývojové nastavení a nedostatečné pro terénní log, kde je to právě časové razítko, co dělá soubor užitečným i o týdny později.
basicConfig() při dalších voláních nedělá nic, pokud není předáno force=True. Nakonfigurujte jednou při startu; nevolejte ji znovu kvůli „přepnutí cílů“ během běhu.
Poznámka
logging na kameře nemá dictConfig() ani fileConfig(). Konfigurace je vždy programová – konvencí je jeden pomocník setup_logging() volaný jednou z main.py.
14.3.1.4. Pojmenované loggery pro každý modul¶
Kód aplikace by neměl volat zkratky na úrovni modulu (logging.info(), logging.warning() a tak dále). Ty všechny ústí přes kořenový logger a výsledné log záznamy nesou název root – k ničemu, když chcete zjistit, odkud záznam pochází.
Konvencí je jeden logger na modul, pojmenovaný podle modulu:
# in app/detector.py
import logging
log = logging.getLogger(__name__)
def detect(frame):
log.info("detect on %dx%d frame", frame.width(), frame.height())
Každý záznam pak nese app.detector v %(name)s a log řádek říká, kdo jej vydal.
logging na kameře se od CPythonu liší v jednom důležitém ohledu: jmenný prostor loggerů je plochý. getLogger('app') a getLogger('app.detector') jsou nezávislé loggery bez vztahu rodič / potomek – nastavení úrovně na app se nepropaguje na app.detector. Mechanismus, který funguje: pojmenovaný logger bez vlastních handlerů si vypůjčí handlery a úroveň kořenového loggeru. Tak jediné volání basicConfig() na kořenovém loggeru nastaví každé volání getLogger() jinde v aplikaci.
14.3.1.5. Líné formátování argumentů přes %¶
Pište:
log.info("processed %d frames in %d ms", count, dt)
Ne:
log.info(f"processed {count} frames in {dt} ms")
Forma s argumenty přes % umožňuje loggeru interpolovat argumenty až poté, co filtr úrovní rozhodne, zda záznam vydat. Odfiltrované volání DEBUG v horké smyčce nestojí za svůj formátovací řetězec nic. F-string se vyhodnocuje jako první, pokaždé, ještě než se volání vůbec dostane k loggeru.
Klíčové slovo CPythonu extra= pro strukturovaná pole není na kameře podporováno; předejte hodnoty místo toho jako argumenty zprávy.
14.3.1.6. Logování výjimek¶
Uvnitř bloku except Logger.exception() zaloguje zprávu na úrovni ERROR a zároveň připojí k záznamu traceback aktuální výjimky:
try:
frame = csi0.snapshot()
process(frame)
except Exception:
log.exception("frame loop iteration failed")
Traceback je zachycen prostřednictvím sys.print_exception(), což je to, co dává logu výjimky jeho víceřádkový blok Traceback (most recent call last):. Toto je správný nástroj pro zpracování výjimek na nejvyšší úrovni – zachytit, zalogovat a pokračovat dál.
14.3.1.7. Více handlerů¶
Produkční rozdělení zmíněné na začátku – vše do souboru na úrovni DEBUG, to nejdůležitější na stderr na úrovni WARNING – jsou dva handlery připojené ke stejnému loggeru, každý s vlastní úrovní a formatterem:
import logging
fmt = '%(asctime)s %(levelname)s %(name)s: %(message)s'
file_handler = logging.FileHandler('/sdcard/logs/app.log')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(fmt))
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.WARNING)
stream_handler.setFormatter(logging.Formatter(fmt))
root = logging.getLogger()
root.setLevel(logging.DEBUG) # admit everything to the filters
root.addHandler(file_handler)
root.addHandler(stream_handler)
Úroveň kořenového loggeru je první filtr, na který každý záznam narazí. Nastavte ji na nejnižší úroveň, kterou chce vidět kterýkoli handler – zde DEBUG – aby žádný handler nebyl ochuzen samotným loggerem. Úrovně jednotlivých handlerů pak rozhodnou, které záznamy se skutečně vydají do kterého cíle.
14.3.1.8. Rotace log souborů¶
logging na kameře nemá RotatingFileHandler ani TimedRotatingFileHandler. Rotace je úkolem aplikace.
Vzorem je udržovat aktuální FileHandler na známém místě, vyměnit jej za nový, když se spustí kritérium pro přepnutí, a nechat datovanou cestu poskytnout přirozené rozhraní mezi soubory. Pro hodinové přepínání do /sdcard/logs/<year>/<month>/<day>/<hour>.log
import logging
import time
_LOG_FMT = '%(asctime)s %(levelname)s %(name)s: %(message)s'
_current_path = None
_current_handler = None
def _hourly_path(now):
return '/sdcard/logs/{:04d}/{:02d}/{:02d}/{:02d}.log'.format(
now[0], now[1], now[2], now[3])
def rotate_if_needed():
global _current_path, _current_handler
path = _hourly_path(time.localtime())
if path == _current_path:
return
root = logging.getLogger()
if _current_handler is not None:
root.removeHandler(_current_handler)
_current_handler.close()
_current_handler = logging.FileHandler(path)
_current_handler.setFormatter(logging.Formatter(_LOG_FMT))
root.addHandler(_current_handler)
_current_path = path
Volejte rotate_if_needed() jednou za každou iteraci hlavní smyčky; kontrola cesty je levná a výměna proběhne pouze na hranici hodiny. Stromová struktura adresářů musí existovat dříve, než FileHandler může soubor otevřít.
14.3.1.9. Vyprazdňování bufferu u nasazení citlivých na výpadek napájení¶
Zápisy FileHandler procházejí přes Pythonovo bufferování podkladového souborového objektu. Výpadek napájení mezi zápisem a vyprázdněním bufferu ztratí poslední záznamy. U nasazení napájených z baterie nebo s rizikem vytržení napájení volejte flush() na streamu handleru po kritických záznamech, nebo na časovač.
Malý pomocník, který vyprázdní buffer každého handleru připojeného ke kořenovému loggeru:
import logging
def flush_handlers():
for handler in logging.getLogger().handlers:
if hasattr(handler, 'stream'):
handler.stream.flush()
Volejte flush_handlers() hned po záznamu, který si aplikace nemůže dovolit ztratit:
log.critical("memory low: restarting")
flush_handlers()
Pro bezpečnost na pozadí jej volejte z hlavní smyčky v takovém rytmu, který vyvažuje čerstvost logu proti opotřebení flash paměti – jednou za sekundu obvykle bohatě stačí. Logger.critical() sama o sobě vyprázdnění bufferu nespustí.
14.3.1.10. Diagnostika při startu¶
Terénní log bez kontextu je téměř k ničemu. První záznamy při každém studeném startu by měly identifikovat, která kamera to je, jaká verze běží a jak se kamera k tomuto startu dostala. Tři zdroje na zařízení dohromady pokrývají všechno:
omv– verze firmwaru OpenMV.os.uname()– verze MicroPythonu, název desky + MCU a git tag a datum sestavení zdroje, ze kterého byl firmware sestaven.machine– jedinečné křemíkové ID MCU a příčina resetu, která tento start spustila.os.listdir()proti každému přípojnému bodu – souborové systémy, které skutečně naběhly.
Pomocník, který natáhne každý z těchto údajů do prvních záznamů logu:
import binascii
import logging
import machine
import omv
import os
log = logging.getLogger(__name__)
_RESET_NAMES = {
machine.PWRON_RESET: "power-on",
machine.HARD_RESET: "hard reset",
machine.WDT_RESET: "watchdog timeout",
machine.DEEPSLEEP_RESET: "wake from deep sleep",
machine.SOFT_RESET: "soft reset",
}
def log_boot_diagnostics():
uname = os.uname()
log.info("machine: %s", uname.machine)
log.info("unique id: %s",
binascii.hexlify(machine.unique_id()).decode())
log.info("firmware: openmv %s, micropython %s",
omv.version_string(), uname.release)
log.info("build: %s", uname.version)
log.info("reset cause: %s",
_RESET_NAMES.get(machine.reset_cause(), "unknown"))
for mount in ('/flash', '/sdcard', '/rom'):
try:
os.listdir(mount)
log.info("mount %s: ok", mount)
except OSError as e:
log.warning("mount %s: %s", mount, e)
Typický log začíná zhruba takto:
INFO machine: OPENMV4 with STM32H743
INFO unique id: 002C00543235501020373835
INFO firmware: openmv 5.0.0, micropython 1.28.0
INFO build: v1.28.0-101-gabc1234 on 2026-06-09
INFO reset cause: watchdog timeout
INFO mount /flash: ok
INFO mount /sdcard: ok
INFO mount /rom: ok
Osm řádků na začátku každého log souboru, a operátor zná fyzický kus, původ firmwaru, proč se kamera nabootovala a které úložiště naběhlo. unique id je z výroby naprogramované křemíkové sériové číslo MCU; je stejné napříč přeflashováními i napříč výměnami SD karty. build je git tag a datum stromu firmwaru, ze kterého byl image sestaven – jediné pole, které říká „toto je přesně ten binární soubor, který byl dodán do tohoto kusu v tomto okamžiku“.
14.3.1.11. Poskládání dohromady¶
Kompletní produkční nastavení logování, vyčleněné do pomocníka, kterého main.py zavolá jednou při startu:
import logging
_LOG_FMT = '%(asctime)s %(levelname)s %(name)s: %(message)s'
def setup_logging(log_path):
fh = logging.FileHandler(log_path)
fh.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter(_LOG_FMT))
sh = logging.StreamHandler()
sh.setLevel(logging.WARNING)
sh.setFormatter(logging.Formatter(_LOG_FMT))
root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.addHandler(fh)
root.addHandler(sh)
Pak na začátku main.py
from app.logging_setup import setup_logging, log_boot_diagnostics
setup_logging('/sdcard/logs/app.log')
log_boot_diagnostics()
Každý další modul v aplikaci pak jen udělá:
import logging
log = logging.getLogger(__name__)
a zdarma získá nakonfigurovaný výstup – soubor s úplnými detaily, stream s varováními, pojmenované záznamy, formatter s časovými razítky a zdokumentovaný start při každém studeném startu.