14.3.3. Bestandssysteemhygiëne

Flash- en SD-opslag op een uitgeleverde cam raken vol met bestanden die geen enkele operator handmatig gaat opruimen. Twee beslissingen over die opslag blijven gedurende de hele levensduur bij het product: welk medium welk soort gegevens bevat, en hoe de mappen zijn gestructureerd zodat bestandsbewerkingen blijven werken naarmate de applicatie records opbouwt.

14.3.3.1. Waar dingen naartoe gaan

Code en assets reizen mee in de frozen modules en ROMFS die de build bij uitlevering vastlegt. Applicatie-status – alles wat de applicatie tijdens runtime schrijft, alles wat groeit, alles wat verandert tussen opstartbeurten – moet ergens anders leven. De cam stelt daarvoor twee beschrijfbare media beschikbaar:

  • Intern flashgeheugen op /flash: een klein beschrijfbaar bestandssysteem dat wordt aangekoppeld voordat enige applicatiecode draait. De juiste plek voor kleine records met vaste grootte die herstarts overleven: configuratie die de applicatie tijdens runtime bijwerkt, de laatst bekende kalibratie, een doorlopende teller, een markeerbestand van één regel dat zegt “deze cam is geprovisioneerd.” Beperkt aantal schrijfcycli – modern intern flashgeheugen verdraagt duizenden tot tienduizenden schrijfacties per sector, geen miljoenen, dus schrijfacties moeten zeldzaam zijn, niet per frame.

  • SD-kaart op /sdcard: een groter beschrijfbaar bestandssysteem dat wordt aangekoppeld wanneer er een kaart aanwezig is. De juiste plek voor omvangrijke variabele bestanden: afbeeldings- en video-opnamen, logbestanden, gegevens voor het fijnafstellen van modellen, alles wat kan groeien tot megabytes of gigabytes. Hogere schrijfcapaciteit dan intern flashgeheugen maar nog steeds eindig; verwijderbaar, vervangbaar, en het medium dat het meest waarschijnlijk verdwijnt terwijl de applicatie midden in een schrijfactie zit.

Het juiste antwoord op waar iets te schrijven is bijna altijd “flash voor kleine vaste records, SD voor al het andere.” De twee zijn niet uitwisselbaar: een applicatie die haar doorlopende logbestand naar /flash krabbelt, slijt de schrijfduurzaamheid van de flash op in een implementatie die op SD prima zou zijn geweest.

14.3.3.2. Behandel beide als kunnen-falen

/flash en /sdcard kunnen beide falen. De SD-kaart kan worden uitgeworpen, de flash kan beschadigd raken door een stroomonderbreking midden in een schrijfactie, beide kunnen vol raken, en elke bewerking op beide kan een OSError opleveren om redenen die de applicatie in het veld niet de kans krijgt te diagnosticeren.

Twee patronen zorgen ervoor dat de applicatie dat overleeft:

  • Verpak aankoppelingen en bewerkingen in try-blokken. Elke open(), os.listdir(), os.rename() tegen paden met gebruikersgegevens kan mislukken. Vang OSError op, log het, en val terug op een gedefinieerd alternatief – schrijf naar /flash als /sdcard weg is, sla de bewerking over als geen van beide beschikbaar is.

  • Atomaire schrijfacties voor bestanden die een stroomonderbreking moeten overleven. Schrijf naar een tijdelijk pad, sluit de handle, en doe dan os.rename() over de live naam heen. Ofwel is de hernoeming geslaagd en is het bestand de nieuwe versie, ofwel is dat niet gebeurd en is het bestand de oude versie. Er is geen derde toestand waarin het bestand halverwege geschreven is:

    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)
    

    Het patroon werkt op zowel flash als SD. Het werkt niet voor bestanden die groot genoeg zijn dat het tmp-bestand de vrije ruimte van het bestandssysteem opgebruikt; reserveer het voor kleine records.

14.3.3.3. De valkuil van de trage map

De MicroPython VFS indexeert de inhoud van mappen niet op de manier waarop een desktopbestandssysteem dat doet. os.listdir() en os.stat() lopen de onderliggende bestandstabel lineair door. Een map met honderd bestanden is prima; een map met tienduizend bestanden is onbruikbaar traag, waarbij elke os.listdir() seconden duurt en elke open() onderweg tegen de tabel controleert.

Applicaties die logs of opnamen naar schijf schrijven, lopen hier het snelst tegenaan. Een naïef /sdcard/logs/<timestamp>.log-schema dat elke minuut één nieuw bestand opent, vult de logs/-map met een half miljoen bestanden in een jaar implementatie. Ruim daarvoor begint de applicatie haar framesnelheid te missen omdat elke bestandsopening langer duurt dan een frame-interval.

Het juiste patroon is om bestanden te verdelen over een boom van gedateerde submappen, zodat geen enkele map ooit meer dan een paar honderd items bevat:

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

Een jaar van logging met één bestand per uur is nu verspreid over 365 dagmappen, elk met maximaal 24 bestanden; os.listdir() tegen een willekeurige map blijft goedkoop, en de frame-lus van de applicatie loopt niet vast op bestandsbewerkingen naarmate de implementatie veroudert.

Hetzelfde principe geldt voor afbeeldingsopnamen, sensorsporen, of al het andere waarvoor de applicatie een bestand per gebeurtenis schrijft. Als de gebeurtenissnelheid hoog is, wil de boom dieper zijn (jaar/maand/dag/uur, of jaar/maand/dag/uur/minuut) zodat elke bladmap klein blijft. Als de gebeurtenissnelheid laag is, is een jaar/maand-boom genoeg.

14.3.3.4. Paden per apparaat

In een vloot van meer dan één cam moeten logbestanden aangeven van welke fysieke eenheid ze afkomstig zijn. machine.unique_id() retourneert een hardware-identificatie die in de fabriek in de cam is gebakken; het is dezelfde waarde over herstarts heen, over firmware-updates heen, en over SD-kaartwisselingen heen. Verwerk het in het logpad of in de logrecords en een operator die naar een stapel SD-kaarten of een gecentraliseerd log kijkt, kan zien welke welke is:

import binascii
import machine

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

LOG_ROOT = '/sdcard/logs/' + UNIT_ID

Gecombineerd met het patroon van gedateerde submappen wordt de indeling /sdcard/logs/<unit-id>/2026/06/09/14.log – één uur aan records van één eenheid, in een map die ondiep genoeg is om te doorlopen, op een pad dat de eenheid op het bestandssysteem zelf benoemt.

14.3.3.5. Alles samenbrengen

De beschrijfbare opslag van een uitgeleverde cam ziet er ongeveer zo uit:

  • /flash – configuratie, kalibratie, een provisioning-markering. Zelden geschreven, vaak gelezen. Atomaire hernoemingspatroon voor elk bestand waarvan het verlies de volgende opstart zou breken.

  • /sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log – het operationele log. Continu geschreven, geroteerd door het pad, nooit geschreven via een map met duizenden naastliggende items.

  • /sdcard/captures/<unit-id>/<year>/<month>/<day>/ – afbeeldings- of video-opnamen die de applicatie maakt. Dezelfde boomvorm, dezelfde reden.

Die indeling kost de applicatie ongeveer twintig regels code en behoedt haar voor de faalmodi die de cam maanden na de start van een implementatie platleggen.