14.3.1. Lokitus¶
Toimitettu tuote ei voi raportoida kotiin print()-funktiolla. print() kirjoittaa USB:n vakiotulosteeseen, joka on olemassa vain silloin, kun kamera on kehittäjän työpöydällä pääte auki. Kentällä mikään ei lue sitä; jokainen rivi katoaa. logging-kirjasto on korvaaja – tasosuodatin, sovelluksen valitsema kohde ja muoto, joka kertoo mitä tapahtui ja milloin.
Kameran logging-moduuli on karsittu portti CPythonin vastaavasta – sama ajatusmalli, pienempi pinta-ala ja muutama tuotantoasetusten kannalta merkittävä ero.
14.3.1.1. Ajatusmalli¶
Lokitus rakentuu neljästä osasta. Jokaisella osalla on yksi tehtävä; juuri tämä erottelu mahdollistaa sen, että yksi loggeri voi haarautua useaan kohteeseen, joista kullakin on oma muotonsa ja tasonsa:
Loggeri on se, jota sovellus kutsuu. Koodi sanoo
log.info("frame %d", n); loggeri on objekti, johon kutsu osuu. Loggerit haetaan nimellä funktiollalogging.getLogger().Käsittelijä (Handler) päättää, minne tietue menee.
StreamHandlerkirjoittaa virtaan (oletuksenasys.stderr);FileHandlerlisää tiedot levyllä olevaan tiedostoon. Loggerilla voi olla mikä tahansa määrä käsittelijöitä.Muista
Kamerassa
sys.stdoutjasys.stderron kytketty samaan USB CDC -putkeen – kummankin kirjoitukset näkyvät samassa päätteessä, jonka kehittäjä on avannut USB:n yli. Käsittelijä, joka kirjoittaa kohteeseensys.stderr, on käytännössä käsittelijä, joka kirjoittaa samaan paikkaan kuinprint(). Käsittelijän abstraktio antaa silti kohdekohtaisen suodatuksen ja muotoilun; se ei vain anna fyysisesti erillistä kanavaa.Muotoilija (Formatter) päättää, miten tietue muunnetaan tekstiksi. Se ottaa tietueen ja palauttaa kirjoitettavan rivin. Yksi muotomerkkijono muotoilijaa kohti; yksi muotoilija käsittelijää kohti.
Tasosuodatin sijaitsee jokaisella loggerilla ja jokaisella käsittelijällä. Tietueet kantavat tasoa (
DEBUG/INFO/WARNING/ERROR/CRITICAL). Vain suodatintason saavuttavat tai sen ylittävät tietueet pääsevät läpi.
Tämä erottelu on tärkeää, koska tyypillisessä tuotantoasetuksessa on useampi kuin yksi kohde: SD-kortilla oleva tiedosto, joka säilyttää kaiken aina tasolle DEBUG saakka jälkikäteistä analyysiä varten, ja USB:hen menevä virta, joka nostaa esiin vain tason WARNING ja sitä vakavammat, jotta kameraan kytketty kehittäjä näkee kohokohdat hukkumatta yksityiskohtiin. Sama koodi, kaksi kohdetta, kaksi suodatinta.
14.3.1.2. Tasot ja mitä kukin niistä tarkoittaa¶
Viisi tasoa muodostavat järjestetyn asteikon. Tietueet kantavat tasoa, jotta kunkin käsittelijän suodatin voi pudottaa ne, joista se ei välitä.
DEBUG– jäljitys, kehyskohtaiset laskurit, sisäisen tilan vedokset. Alin taso; volyymi on suuri.INFO– normaalit toiminnalliset tapahtumat. Wi-Fi yhdistetty, malli ladattu, vahtikoira käynnistetty, uusi lokitiedosto vaihdettu käyttöön.WARNING– jotain odottamatonta, mutta sovellus käsitteli sen. Pudotettu kehys, uudelleen yritetty verkkopyyntö.ERROR– toiminto epäonnistui eikä sovellus voinut saattaa sitä loppuun. Mallitiedosto puuttuu, SD-kortin kirjoitus evätty.CRITICAL– sovellus ei voi jatkaa lainkaan. Muisti loppu, pakollinen liitos (mount) puuttuu.
Yksi tärkeä oletus, joka kannattaa muistaa: kameran logging-moduuli käynnistää jokaisen loggerin tasolla WARNING. Tasoilla DEBUG ja INFO olevat tietueet pudotetaan hiljaisesti, ellei Logger.setLevel()-metodia kutsuta – yleensä osana alla olevaa basicConfig()-kutsua. Yleinen ensimmäinen oire siitä, että lokitusasetus ”ei toimi”, on se, että sovellus lähetti tasolla INFO ja oletussuodatin söi tietueen.
Muista
Taso on ainoa suodatin, jonka kameran logging tarjoaa. Filter-objekteja rikkaampia tietuekohtaisia sääntöjä varten ei ole; jos tietueen taso pääsee läpi, se lähetetään.
14.3.1.3. basicConfig: pikaopas¶
logging.basicConfig() määrittää juuriloggerin yhdellä kutsulla. Kaksi muotoa esiintyy useimmin:
Kehitysasetus, kaikki USB:n stderr-virtaan tasolla INFO
import logging
logging.basicConfig(level=logging.INFO)
Tuotantoasetus, kaikki SD-kortilla olevaan tiedostoon aikaleimatulla muodolla:
import logging
logging.basicConfig(
filename='/sdcard/logs/app.log',
level=logging.INFO,
format='%(asctime)s %(levelname)s %(name)s: %(message)s',
)
Anna joko filename= FileHandler-luokkaa varten tai stream= StreamHandler-luokkaa varten; nämä kaksi ovat toisensa poissulkevia basicConfig()-funktiossa.
Muotomerkkijono on %(field)s-tyylinen malli. Kentät, joita kameran muotoilija tukee:
%(asctime)s– aikaleima muotoiltuna funktiostatime.localtime(). Oletusmuoto on%Y-%m-%d %H:%M:%S; annadatefmt=ohittaaksesi sen.%(levelname)s–DEBUG/INFO/WARNING/ERROR/CRITICAL.%(name)s– loggerin nimi (katso seuraava osio).%(message)s– tietueen muotoiltu viesti.%(msecs)d– tietueen aikaleiman millisekuntiosa.
Oletusmuoto, jos mitään ei anneta, on %(levelname)s:%(name)s:%(message)s – mikä sopii kehitysasetukseen ja on riittämätön kenttälokille, jossa aikaleima on se, mikä tekee tiedostosta hyödyllisen viikkoja myöhemmin.
basicConfig() ei tee mitään seuraavilla kutsuilla, ellei force=True anneta. Määritä kerran käynnistyksessä; älä kutsu sitä uudelleen ”kohteiden vaihtamiseksi” kesken ajon.
Muista
Kameran logging-moduulissa ei ole funktioita dictConfig() eikä fileConfig(). Konfigurointi on aina ohjelmallista – yksi setup_logging()-apufunktio, jota kutsutaan kerran tiedostosta main.py, on vakiintunut tapa.
14.3.1.4. Nimetyt loggerit moduulia kohti¶
Sovelluskoodin ei tulisi kutsua moduulitason pikakomentoja (logging.info(), logging.warning() ja niin edelleen). Ne kaikki ohjautuvat juuriloggerin kautta, ja syntyvät lokitietueet kantavat nimeä root – hyödytön sen kertomiseen, mistä tietue tuli.
Vakiintunut tapa on yksi loggeri moduulia kohti, nimettynä moduulin mukaan:
# in app/detector.py
import logging
log = logging.getLogger(__name__)
def detect(frame):
log.info("detect on %dx%d frame", frame.width(), frame.height())
Jokainen tietue kantaa tällöin nimeä app.detector kentässä %(name)s, ja lokirivi kertoo, kuka sen lähetti.
Kameran logging eroaa CPythonista yhdellä tärkeällä tavalla: loggerien nimiavaruus on litteä. getLogger('app') ja getLogger('app.detector') ovat itsenäisiä loggereita ilman vanhempi/lapsi-suhdetta – tason asettaminen loggerille app ei välity loggerille app.detector. Mekanismi, joka toimii: nimetty loggeri, jolla ei ole omia käsittelijöitä, lainaa juuriloggerin käsittelijät ja tason. Näin yksi basicConfig()-kutsu juurelle määrittää jokaisen muualla sovelluksessa olevan getLogger()-kutsun.
14.3.1.5. Laiska %-argumenttien muotoilu¶
Kirjoita:
log.info("processed %d frames in %d ms", count, dt)
Älä:
log.info(f"processed {count} frames in {dt} ms")
%-argumenttimuoto antaa loggerin interpoloida argumentit sen jälkeen, kun tasosuodatin on päättänyt, lähetetäänkö tietue. Pois suodatettu DEBUG-kutsu kuumassa silmukassa ei maksa mitään muotomerkkijonostaan. F-merkkijono evaluoidaan ensin, joka kerta, ennen kuin kutsu edes saavuttaa loggerin.
CPythonin extra=-avainsanaa rakenteisia kenttiä varten ei tueta kamerassa; anna arvot viestin argumentteina sen sijaan.
14.3.1.6. Poikkeusten lokitus¶
Lohkon except sisällä Logger.exception() kirjaa viestin tasolla ERROR ja liittää nykyisen poikkeuksen jäljityksen (traceback) tietueeseen:
try:
frame = csi0.snapshot()
process(frame)
except Exception:
log.exception("frame loop iteration failed")
Jäljitys kaapataan funktiolla sys.print_exception(), joka antaa poikkeuslokille sen monirivisen Traceback (most recent call last): -lohkon. Tämä on oikea työkalu ylimmän tason poikkeustenkäsittelyyn – ota kiinni, kirjaa ja jatka.
14.3.1.7. Useita käsittelijöitä¶
Alussa mainittu tuotantojako – kaikki tiedostoon tasolla DEBUG, kohokohdat stderr-virtaan tasolla WARNING – on kaksi samaan loggeriin liitettyä käsittelijää, joista kummallakin on oma tasonsa ja muotoilijansa:
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)
Juuriloggerin taso on ensimmäinen suodatin, johon jokainen tietue osuu. Aseta se alimmalle tasolle, jonka mikä tahansa käsittelijä haluaa nähdä – tässä DEBUG – jotta loggeri itse ei näännytä kumpaakaan käsittelijää. Käsittelijäkohtaiset tasot päättävät sitten, mitkä tietueet todella lähetetään mihinkin kohteeseen.
14.3.1.8. Lokitiedostojen kierrätys¶
Kameran logging-moduulissa ei ole luokkia RotatingFileHandler eikä TimedRotatingFileHandler. Kierrätys on sovelluksen tehtävä.
Kuvio on pitää nykyinen FileHandler tunnetussa paikassa, vaihtaa se uuteen, kun kierrätyskriteeri laukeaa, ja antaa päivätyn polun tarjota luontevan tiedostorajan. Tunneittaista kierrätystä polkuun /sdcard/logs/<year>/<month>/<day>/<hour>.log varten:
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
Kutsu rotate_if_needed() kerran pääsilmukan kierrosta kohti; polun tarkistus on halpa ja vaihto tapahtuu vain tunnin rajalla. Hakemistopuun on oltava olemassa, ennen kuin FileHandler voi avata tiedoston.
14.3.1.9. Tyhjennys virtaherkissä käyttöönotoissa¶
FileHandler-luokan kirjoitukset kulkevat taustalla olevan tiedosto-objektin Python-puskuroinnin kautta. Virtakatkos kirjoituksen ja tyhjennyksen välillä menettää viimeiset tietueet. Akkukäyttöisissä tai pistoke-irti-vedettävissä käyttöönotoissa kutsu flush() käsittelijän virtaan kriittisten tietueiden jälkeen tai ajastimella.
Pieni apufunktio, joka tyhjentää jokaisen juuriloggeriin liitetyn käsittelijän:
import logging
def flush_handlers():
for handler in logging.getLogger().handlers:
if hasattr(handler, 'stream'):
handler.stream.flush()
Kutsu flush_handlers() heti sellaisen tietueen jälkeen, jota sovelluksella ei ole varaa menettää:
log.critical("memory low: restarting")
flush_handlers()
Taustaturvan vuoksi kutsu sitä pääsilmukasta sellaisella tahdilla, joka tasapainottaa lokin tuoreuden flash-muistin kulumista vastaan – kerran sekunnissa on yleensä runsaasti. Logger.critical() ei itsessään laukaise tyhjennystä.
14.3.1.10. Käynnistyksen diagnostiikka¶
Kenttäloki ilman kontekstia on lähes hyödytön. Ensimmäisten tietueiden jokaisella kylmäkäynnistyksellä tulisi tunnistaa mikä kamera on kyseessä, mikä koonti on käynnissä ja miten kamera päätyi tähän käynnistykseen. Kolme laitteella olevaa lähdettä kattavat yhdessä kaiken tämän:
omv– OpenMV-laiteohjelmiston versio.os.uname()– MicroPython-versio, levyn nimi + MCU sekä git-tunniste ja koontipäivä lähteestä, josta laiteohjelmisto koottiin.machine– MCU:n yksilöllinen piitunniste (silicon ID) ja nollauksen syy, joka laukaisi tämän käynnistyksen.os.listdir()jokaista liitospistettä vastaan – tiedostojärjestelmät, jotka todella nousivat käyttöön.
Apufunktio, joka vetää jokaisen näistä lokin ensimmäisiin tietueisiin:
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)
Tyypillinen loki avautuu jotakuinkin näin:
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
Kahdeksan rivin päässä jokaisen lokitiedoston alusta operaattori tietää fyysisen yksikön, laiteohjelmiston sukulinjan, miksi kamera käynnistyi ja mikä tallennustila nousi käyttöön. unique id on MCU:n tehtaalla ohjelmoitu piisarjanumero; se on sama uudelleenflashauksien ja SD-korttien vaihtojen yli. build on git-tunniste ja päivämäärä siitä laiteohjelmistopuusta, josta kuva koottiin – ainoa kenttä, joka sanoo ”tämä on täsmälleen se binääri, joka toimitettiin tähän yksikköön tällä hetkellä.”
14.3.1.11. Kokonaisuuden kokoaminen¶
Täydellinen tuotantolokitusasetus, jaoteltuna apufunktioon, jota main.py kutsuu kerran käynnistyksessä:
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)
Sitten tiedoston main.py alussa:
from app.logging_setup import setup_logging, log_boot_diagnostics
setup_logging('/sdcard/logs/app.log')
log_boot_diagnostics()
Jokainen muu moduuli sovelluksessa tekee vain:
import logging
log = logging.getLogger(__name__)
ja saa määritetyn tulosteen ilmaiseksi – tiedosto täydellä yksityiskohdalla, virta varoituksilla, nimetyt tietueet, aikaleimattu muotoilija ja dokumentoitu käynnistys jokaisella kylmäkäynnistyksellä.