14.3.3. Filsystemshygien

Flash- och SD-lagring på en levererad kamera fylls upp med filer som ingen operatör kommer att rensa bort för hand. Två beslut om den lagringen följer produkten genom hela dess livstid: vilken yta som rymmer vilken sorts data, och hur katalogerna struktureras så att filoperationer fortsätter fungera när applikationen samlar på sig poster.

14.3.3.1. Var saker hamnar

Kod och resurser ligger i de frysta modulerna och den ROMFS som bygget förbinder sig till vid leveranstillfället. Applikationens tillstånd – allt applikationen skriver under körning, allt som växer, allt som ändras mellan uppstarter – måste finnas någon annanstans. Kameran exponerar två skrivbara ytor för detta:

  • Internt flashminne/flash: ett litet skrivbart filsystem som monteras innan någon applikationskod körs. Rätt plats för små poster av fast storlek som överlever omstarter: konfiguration som applikationen uppdaterar under körning, senast kända kalibrering, en rullande räknare, en enradig markeringsfil som säger ”denna kamera har provisionerats.” Begränsat antal skrivcykler – modernt internt flashminne tål tusentals till tiotusentals skrivningar per sektor, inte miljontals, så skrivningar behöver vara sällsynta, inte per bildruta.

  • SD-kort/sdcard: ett större skrivbart filsystem som monteras när ett kort finns på plats. Rätt plats för skrymmande variabla filer: bild- och videoinspelningar, loggfiler, data för finjustering av modeller, allt som kan växa till megabyte eller gigabyte. Högre skrivkapacitet än internt flashminne men ändå ändlig; flyttbart, utbytbart, och den yta som mest sannolikt försvinner när applikationen är mitt i en skrivning.

Det rätta svaret på var man ska skriva något är nästan alltid ”flash för små fasta poster, SD för allt annat.” De två är inte utbytbara: en applikation som klottrar ner sin rullande loggfil till /flash kommer att slita ut flashminnets skrivuthållighet i en driftsättning som hade gått bra på SD.

14.3.3.2. Behandla båda som möjliga felkällor

/flash och /sdcard kan båda fallera. SD-kortet kan matas ut, flashminnet kan korrumperas av ett strömavbrott mitt i en skrivning, endera kan få slut på utrymme, och vilken operation som helst på endera kan utlösa OSError av skäl som applikationen inte kommer att få chans att diagnostisera ute i fält.

Två mönster gör att applikationen överlever det:

  • Omslut monteringar och operationer i try-block. Varje open(), os.listdir(), os.rename() mot sökvägar med användardata kan potentiellt fallera. Fånga OSError, logga det, och fall tillbaka på ett definierat alternativ – skriv till /flash om /sdcard är borta, hoppa över operationen om ingendera är tillgänglig.

  • Atomära skrivningar för filer som måste överleva ett strömavbrott. Skriv till en tillfällig sökväg, stäng handtaget, och kör sedan os.rename() över det aktiva namnet. Antingen lyckades omdöpningen och filen är den nya versionen, eller så gjorde den inte det och filen är den gamla versionen. Det finns inget tredje tillstånd där filen är halvskriven:

    import os
    
    def write_config_atomic(path, contents):
        tmp = path + '.tmp'
        with open(tmp, 'w') as f:
            f.write(contents)
            f.flush()
        os.rename(tmp, path)
    

    Mönstret fungerar på både flash och SD. Det fungerar inte för filer som är tillräckligt stora för att tmp-filen ska förbruka filsystemets fria utrymme; reservera det för små poster.

14.3.3.3. Fällan med långsamma kataloger

MicroPythons VFS indexerar inte kataloginnehåll på det sätt som ett skrivbordsfilsystem gör. os.listdir() och os.stat() går igenom den underliggande filtabellen linjärt. En katalog med hundra filer går bra; en katalog med tiotusen filer är obrukbart långsam, där varje os.listdir() tar sekunder och varje open() kontrolleras mot tabellen på vägen igenom.

Applikationer som skriver loggar eller inspelningar till disk stöter på detta snabbast. Ett naivt /sdcard/logs/<timestamp>.log-schema som öppnar en ny fil per minut fyller katalogen logs/ med en halv miljon filer på ett år av driftsättning. Långt innan dess börjar applikationen missa sin bildfrekvens eftersom varje filöppning tar längre tid än ett bildruteintervall.

Det rätta mönstret är att fördela filer över ett träd av daterade underkataloger så att ingen enskild katalog någonsin rymmer mer än ett par hundra poster:

import os
import time

LOG_ROOT = '/sdcard/logs'

def log_path(now=None):
    if now is None:
        now = time.localtime()
    year, month, day, hour = now[0], now[1], now[2], now[3]
    directory = '{}/{:04d}/{:02d}/{:02d}'.format(
        LOG_ROOT, year, month, day)
    _makedirs(directory)
    return '{}/{:02d}.log'.format(directory, hour)

def _makedirs(path):
    # os.makedirs equivalent -- create each level if missing
    parts = path.split('/')
    for i in range(2, len(parts) + 1):
        sub = '/'.join(parts[:i])
        try:
            os.mkdir(sub)
        except OSError:
            pass

Ett år av loggning med en fil per timme är nu utspridd över 365 dagkataloger, var och en med som mest 24 filer; os.listdir() mot vilken som helst enskild katalog förblir billig, och applikationens bildruteloop fastnar inte på filoperationer när driftsättningen åldras.

Samma princip gäller bildinspelningar, sensorspår, eller vad som helst annat som applikationen skriver en fil per händelse för. Om händelsefrekvensen är hög bör trädet vara djupare (år/månad/dag/timme, eller år/månad/dag/timme/minut) så att varje lövkatalog förblir liten. Om händelsefrekvensen är låg räcker ett år/månad-träd.

14.3.3.4. Sökvägar per enhet

I en flotta med fler än en kamera behöver loggfiler identifiera vilken fysisk enhet de kom från. machine.unique_id() returnerar en hårdvaruidentifierare som bakats in i kameran på fabriken; det är samma värde över omstarter, över firmware-uppdateringar och över byten av SD-kort. Bädda in det i loggsökvägen eller i loggposterna, så kan en operatör som tittar på en hög med SD-kort eller en centraliserad logg avgöra vilken som är vilken:

import binascii
import machine

UNIT_ID = binascii.hexlify(machine.unique_id()).decode()

LOG_ROOT = '/sdcard/logs/' + UNIT_ID

Kombinerat med mönstret med daterade underkataloger blir layouten /sdcard/logs/<unit-id>/2026/06/09/14.log – en enhets timme av poster, i en katalog som är tillräckligt grund för att gå igenom, på en sökväg som namnger enheten på själva filsystemet.

14.3.3.5. Att knyta ihop det

En levererad kameras skrivbara lagring ser ungefär ut så här:

  • /flash – konfiguration, kalibrering, en provisioneringsmarkering. Skrivs sällan, läses ofta. Atomärt omdöpningsmönster för varje fil vars förlust skulle bryta nästa uppstart.

  • /sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log – driftloggen. Skrivs kontinuerligt, roteras av sökvägen, skrivs aldrig genom en katalog med tusentals syskon.

  • /sdcard/captures/<unit-id>/<year>/<month>/<day>/ – bild- eller videoinspelningar som applikationen gör. Samma trädform, samma skäl.

Den layouten kostar applikationen ungefär tjugo rader kod och besparar den de felmoder som tar ner kameran månader in i en driftsättning.