14.3.1. Naplózás¶
Egy leszállított termék nem tud hazatelefonálni a print() segítségével. A print() az USB stdout-ra ír, ami csak akkor létezik, ha a kamera egy fejlesztő munkapadján van, nyitott terminállal. A terepen semmi sem olvassa; minden sor a padlóra esik. A logging könyvtár ennek a helyettesítője – egy szintszűrő, egy az alkalmazás által választott célhely, és egy formátum, amely megmondja, mi és mikor történt.
A kamerán lévő logging modul a CPython megfelelőjének lecsupaszított átirata – ugyanaz a gondolati modell, kisebb felület, néhány eltéréssel, amelyek a éles üzemű beállításnál számítanak.
14.3.1.1. A gondolati modell¶
A naplózás négy darabból épül fel. Mindegyik darabnak egyetlen feladata van; ez a szétválasztás teszi lehetővé, hogy egyetlen logger több célhelyre elágazzon, mindegyik a saját formátumával és szintjével:
A Logger az, amit az alkalmazás meghív. A kód azt mondja:
log.info("frame %d", n); a logger az az objektum, amelyre ez a hívás érkezik. A loggereket név alapján keressük meg alogging.getLogger()segítségével.A Handler dönti el, hogy hová kerül egy rekord. Egy
StreamHandleregy adatfolyamra ír (alapértelmezetten asys.stderr-re); egyFileHandleregy lemezen lévő fájlhoz fűz hozzá. Egy loggernek tetszőleges számú handlere lehet.Megjegyzés
A kamerán a
sys.stdoutés asys.stderrugyanahhoz az USB CDC csatornához van bekötve – a bármelyikre történő írás ugyanazon a terminálon jelenik meg, amelyet a fejlesztő USB-n keresztül nyitva tart. Egysys.stderr-re író handler a gyakorlatban olyan handler, amely ugyanoda ír, ahová aprint(). A handler absztrakció továbbra is biztosít célhelyenkénti szűrést és formázást; csak épp nem ad fizikailag külön csatornát.A Formatter dönti el, hogy hogyan jelenik meg egy rekord szövegként. Fog egy rekordot, és visszaadja a kiírandó sort. Egy formátumsztring formatterenként; egy formatter handlerenként.
A szintszűrő minden loggeren és minden handleren ott ül. A rekordok egy szintet hordoznak (
DEBUG/INFO/WARNING/ERROR/CRITICAL). Csak a szűrő szintjén vagy afölött lévő rekordok jutnak át.
Ez a szétválasztás azért számít, mert egy tipikus éles üzemű beállításnak egynél több célhelye van: egy fájl az SD-kártyán, amely mindent megtart egészen a DEBUG szintig az utólagos elemzéshez, és egy adatfolyam az USB felé, amely csak a WARNING szintet és a rosszabbat hozza felszínre, hogy a kamerához csatlakozott fejlesztő lássa a lényeget anélkül, hogy belefulladna a részletekbe. Ugyanaz a kód, két célhely, két szűrő.
14.3.1.2. Szintek és hogy mit jelent mindegyik¶
Az öt szint egy rendezett skála. A rekordok egy szintet hordoznak, hogy az egyes handlereken lévő szűrő eldobhassa azokat, amelyek nem érdeklik.
DEBUG– nyomkövetés, képkockánkénti számlálók, belső állapot kiírások. A legalacsonyabb szint; nagy mennyiségű.INFO– normál működési események. A Wi-Fi csatlakozott, egy modell betöltődött, a watchdog elindult, egy új naplófájl került forgatásra.WARNING– valami váratlan, de az alkalmazás kezelte. Egy eldobott képkocka, egy újrapróbált hálózati kérés.ERROR– egy művelet meghiúsult, és az alkalmazás nem tudta befejezni. Egy hiányzó modellfájl, egy elutasított SD-kártya írás.CRITICAL– az alkalmazás egyáltalán nem tud folytatódni. Elfogyott a memória, hiányzik egy kötelező csatlakoztatás.
Egy fontos alapértelmezés, amelyet érdemes megjegyezni: a kamera logging modulja minden loggert WARNING szinten indít. A DEBUG és INFO szintű rekordok csendben eldobódnak, hacsak nem hívják meg a Logger.setLevel()-t – általában az alábbi basicConfig() hívás részeként. Egy „nem működő” naplózási beállítás gyakori első tünete, hogy az alkalmazás INFO szinten bocsátott ki, és az alapértelmezett szűrő megette a rekordot.
Megjegyzés
A szint az egyetlen szűrő, amelyet a kamera logging modulja kínál. Nincsenek Filter objektumok a részletesebb rekordonkénti szabályokhoz; ha egy rekord szintje átjut, akkor kibocsátódik.
14.3.1.3. basicConfig: a gyorsindítás¶
A logging.basicConfig() egyetlen hívással konfigurálja a gyökér loggert. Két alak fordul elő leggyakrabban:
Egy fejlesztői beállítás, minden az USB stderr-re INFO szinten:
import logging
logging.basicConfig(level=logging.INFO)
Egy éles üzemű beállítás, minden egy SD-kártyán lévő fájlba, időbélyeggel ellátott formátummal:
import logging
logging.basicConfig(
filename='/sdcard/logs/app.log',
level=logging.INFO,
format='%(asctime)s %(levelname)s %(name)s: %(message)s',
)
Adj át vagy filename=-t egy FileHandler-hez, vagy stream=-et egy StreamHandler-hez; a kettő kölcsönösen kizárja egymást a basicConfig()-ban.
A formátumsztring egy %(field)s-stílusú sablon. A kamera formattere a következő mezőket támogatja:
%(asctime)s– atime.localtime()-ból formázott időbélyeg. Az alapértelmezett formátum%Y-%m-%d %H:%M:%S; a felülírásához add át adatefmt=-et.%(levelname)s–DEBUG/INFO/WARNING/ERROR/CRITICAL.%(name)s– a logger neve (lásd a következő szakaszt).%(message)s– a rekord formázott üzenete.%(msecs)d– a rekord időbélyegének ezredmásodperc-törtrésze.
Az alapértelmezett formátum, ha nincs megadva, a %(levelname)s:%(name)s:%(message)s – ami a fejlesztői beállításhoz megfelelő, de elégtelen egy terepi naplóhoz, ahol épp az időbélyeg teszi a fájlt hetekkel később is hasznossá.
A basicConfig() a további hívásoknál üresjárat, hacsak nem adják át a force=True-t. Konfiguráld egyszer az indításkor; ne hívd meg újra, hogy futás közben „célhelyet válts”.
Megjegyzés
A kamera logging moduljában nincs dictConfig() vagy fileConfig(). A konfiguráció mindig programozott – egyetlen setup_logging() segédfüggvény, amelyet egyszer hívnak meg a main.py-ból, ez a bevett gyakorlat.
14.3.1.4. Modulonként elnevezett loggerek¶
Az alkalmazás kódjának nem szabad a modulszintű rövidítéseket hívnia (logging.info(), logging.warning() és így tovább). Ezek mind a gyökér loggeren keresztül haladnak, és az ebből származó naplórekordok a root nevet hordozzák – haszontalan annak megállapítására, hogy honnan jött a rekord.
A bevett gyakorlat modulonként egy logger, a modulról elnevezve:
# in app/detector.py
import logging
log = logging.getLogger(__name__)
def detect(frame):
log.info("detect on %dx%d frame", frame.width(), frame.height())
Ekkor minden rekord az app.detector-t hordozza a %(name)s-ben, és a naplósor megmondja, ki bocsátotta ki.
A kamera logging modulja egy fontos szempontból eltér a CPythontól: a logger névtér lapos. A getLogger('app') és a getLogger('app.detector') független loggerek, szülő/gyermek kapcsolat nélkül – egy szint beállítása az app-on nem terjed át az app.detector-ra. A mechanizmus, ami működik: egy saját handlerekkel nem rendelkező, elnevezett logger kölcsönveszi a gyökér logger handlereit és szintjét. Így állít be egyetlen, a gyökéren meghívott basicConfig() hívás minden más, az alkalmazás bármely pontján lévő getLogger() hívást.
14.3.1.5. Lusta %-argumentumos formázás¶
Írd ezt:
log.info("processed %d frames in %d ms", count, dt)
Ne ezt:
log.info(f"processed {count} frames in {dt} ms")
A %-argumentumos forma lehetővé teszi, hogy a logger az argumentumokat azután helyettesítse be, hogy a szintszűrő eldöntötte, kibocsátja-e a rekordot. Egy kiszűrt DEBUG hívás egy gyakran futó hurokban semmibe sem kerül a formátumsztringjéért. Egy f-string elsőként, minden alkalommal kiértékelődik, még mielőtt a hívás egyáltalán elérné a loggert.
A CPython extra= kulcsszava strukturált mezőkhöz nem támogatott a kamerán; helyette add át az értékeket üzenetargumentumként.
14.3.1.6. Kivételek naplózása¶
Egy except blokkon belül a Logger.exception() ERROR szinten naplózza az üzenetet, és hozzáfűzi az aktuális kivétel veremkövetését a rekordhoz:
try:
frame = csi0.snapshot()
process(frame)
except Exception:
log.exception("frame loop iteration failed")
A veremkövetést a sys.print_exception() rögzíti, ami egy kivételnaplónak a többsoros Traceback (most recent call last): blokkját adja. Ez a megfelelő eszköz a legfelső szintű kivételkezeléshez – elkapni, naplózni és továbbmenni.
14.3.1.7. Több handler¶
A fentebb említett éles üzemű felosztás – minden egy fájlba DEBUG szinten, a lényeg a stderr-re WARNING szinten – két handler, amely ugyanahhoz a loggerhez van csatolva, mindegyik a saját szintjével és formatterével:
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)
A gyökér logger szintje az első szűrő, amelybe minden rekord beleütközik. Állítsd a legalacsonyabb szintre, amelyet bármelyik handler látni akar – itt DEBUG-ra –, hogy egyik handlert se éheztesse ki maga a logger. A handlerenkénti szintek aztán eldöntik, mely rekordok kerülnek ténylegesen kibocsátásra mely célhelyre.
14.3.1.8. Naplófájlok forgatása¶
A kamera logging moduljában nincs RotatingFileHandler vagy TimedRotatingFileHandler. A forgatás az alkalmazás feladata.
A minta az, hogy az aktuális FileHandler-t egy ismert helyen tartjuk, egy újra cseréljük, amikor a forgatási feltétel teljesül, és hagyjuk, hogy egy dátumozott útvonal biztosítsa a természetes fájlhatárt. Óránkénti forgatáshoz a /sdcard/logs/<year>/<month>/<day>/<hour>.log útvonalra:
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
Hívd meg a rotate_if_needed()-et a fő hurok minden iterációjában egyszer; az útvonal-ellenőrzés olcsó, és a csere csak az órahatáron történik meg. A könyvtárfának léteznie kell, mielőtt a FileHandler meg tudja nyitni a fájlt.
14.3.1.9. Ürítés energiaérzékeny telepítéseknél¶
A FileHandler írásai a mögöttes fájlobjektum Python-pufferelésén mennek keresztül. Egy áramkimaradás egy írás és egy ürítés között elveszíti a végén lévő rekordokat. Akkumulátoros vagy húzd-ki-a-dugót telepítéseknél hívd meg a flush()-t a handler adatfolyamán a kritikus rekordok után, vagy időzítve.
Egy kis segédfüggvény, amely üríti a gyökér loggerhez csatolt összes handlert:
import logging
def flush_handlers():
for handler in logging.getLogger().handlers:
if hasattr(handler, 'stream'):
handler.stream.flush()
Hívd meg a flush_handlers()-t közvetlenül egy olyan rekord után, amelyet az alkalmazás nem engedhet meg magának elveszíteni:
log.critical("memory low: restarting")
flush_handlers()
Háttérbeli biztonság érdekében hívd meg a fő hurokból olyan ütemben, amely egyensúlyt teremt a naplófrissesség és a flash memória kopása között – másodpercenként egyszer általában bőven elég. A Logger.critical() önmagában nem vált ki ürítést.
14.3.1.10. Indítási diagnosztika¶
Egy terepi napló kontextus nélkül szinte haszontalan. Minden hidegindításnál az első rekordoknak azonosítaniuk kell, hogy melyik kamera, milyen build fut, és hogyan jutott el a kamera ehhez az indításhoz. Három eszközfedélzeti forrás együtt mindezt lefedi:
omv– az OpenMV firmware verziója.os.uname()– a MicroPython verziója, a lapka neve + MCU, valamint a git címke és annak a forrásnak a buildelési dátuma, amelyből a firmware épült.machine– az MCU egyedi szilícium-azonosítója és az ezt az indítást kiváltó visszaállítási ok.os.listdir()minden csatlakozási ponton – a fájlrendszerek, amelyek ténylegesen elindultak.
Egy segédfüggvény, amely mindegyiket behúzza a napló első rekordjaiba:
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)
Egy tipikus napló valami ilyesmivel nyit:
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
Minden naplófájl nyolc sorára az üzemeltető ismeri a fizikai egységet, a firmware leszármazási vonalát, hogy miért indult el a kamera, és melyik tároló jött fel. Az unique id az MCU gyárilag beprogramozott szilícium-sorozatszáma; ez azonos az újraflashelések és az SD-kártya cserék között. A build annak a firmware-fának a git címkéje és dátuma, amelyből a kép épült – az az egyetlen mező, amely azt mondja: „pontosan ez az a bináris, amely ehhez az egységhez ezen az időponton kiszállításra került.”
14.3.1.11. Összerakva¶
Egy teljes éles üzemű naplózási beállítás, egy segédfüggvénybe szervezve, amelyet a main.py egyszer hív meg az indításkor:
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)
Majd a main.py tetején:
from app.logging_setup import setup_logging, log_boot_diagnostics
setup_logging('/sdcard/logs/app.log')
log_boot_diagnostics()
Az alkalmazás bármely más moduljának csak ennyit kell tennie:
import logging
log = logging.getLogger(__name__)
és ingyen megkapja a konfigurált kimenetet – fájlt teljes részletességgel, adatfolyamot figyelmeztetésekkel, elnevezett rekordokat, időbélyeggel ellátott formattert, és egy dokumentált indítást minden hidegindításnál.