14.3.3. Igiena sistemului de fișiere

Memoria flash și stocarea pe card SD ale unei camere livrate se umplu cu fișiere pe care niciun operator nu le va curăța manual. Două decizii referitoare la această stocare rămân cu produsul pe tot parcursul vieții sale: care suport deține ce tip de date și cum sunt structurate directoarele astfel încât operațiile cu fișiere să continue să funcționeze pe măsură ce aplicația acumulează înregistrări.

14.3.3.1. Unde ajung lucrurile

Codul și resursele sunt incluse în modulele înghețate și în ROMFS pe care le fixează compilarea la momentul livrării. Starea aplicației – tot ce scrie aplicația în timpul execuției, tot ce crește, tot ce se schimbă între porniri – trebuie să rezide în altă parte. Camera expune două suprafețe care pot fi scrise pentru aceasta:

  • Memoria flash internă la /flash: un sistem de fișiere mic, care poate fi scris, montat înainte ca orice cod de aplicație să ruleze. Locul potrivit pentru înregistrări mici, de dimensiune fixă, care supraviețuiesc repornirilor: configurația pe care aplicația o actualizează în timpul execuției, ultima calibrare cunoscută, un contor incremental, un fișier marker pe o singură linie care spune „această cameră a fost provizionată”. Cicluri de scriere limitate – memoria flash internă modernă tolerează mii până la zeci de mii de scrieri per sector, nu milioane, așa că scrierile trebuie să fie rare, nu per cadru.

  • Cardul SD la /sdcard: un sistem de fișiere mai mare, care poate fi scris, montat atunci când este prezent un card. Locul potrivit pentru fișiere voluminoase și variabile: capturi de imagine și video, fișiere de jurnal, date de reglare fină a modelelor, orice care ar putea crește până la megaocteți sau gigaocteți. Capacitate de scriere mai mare decât memoria flash internă, dar tot finită; detașabil, înlocuibil și suprafața cea mai predispusă să dispară când aplicația este în mijlocul unei scrieri.

Răspunsul corect pentru unde să scrii ceva este aproape întotdeauna „flash pentru înregistrări mici și fixe, SD pentru tot restul”. Cele două nu sunt interschimbabile: o aplicație care își scrie fișierul de jurnal incremental în /flash va epuiza rezistența la scriere a memoriei flash într-o implementare care ar fi fost în regulă pe SD.

14.3.3.2. Tratează-le pe ambele ca fiind susceptibile de eșec

/flash și /sdcard pot eșua amândouă. Cardul SD poate fi scos, memoria flash poate fi coruptă de o pierdere de alimentare în mijlocul unei scrieri, oricare dintre ele poate rămâne fără spațiu, iar orice operație asupra oricăreia poate ridica OSError din motive pe care aplicația nu va avea ocazia să le diagnosticheze pe teren.

Două tipare fac aplicația să supraviețuiască acestui lucru:

  • Învelește montările și operațiile în blocuri try. Fiecare open(), os.listdir(), os.rename() asupra căilor cu date de utilizator poate eșua. Prinde OSError, înregistrează-l în jurnal și revino la o alternativă definită – scrie în /flash dacă /sdcard a dispărut, sari peste operație dacă niciuna nu este disponibilă.

  • Scrieri atomice pentru fișierele care trebuie să supraviețuiască unei pierderi de alimentare. Scrie într-o cale temporară, închide descriptorul, apoi folosește os.rename() peste numele activ. Fie redenumirea a reușit și fișierul este noua versiune, fie nu a reușit și fișierul este versiunea veche. Nu există o a treia stare în care fișierul este scris pe jumătate:

    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)
    

    Tiparul funcționează atât pe flash, cât și pe SD. Nu funcționează pentru fișierele suficient de mari încât fișierul tmp să consume tot spațiul liber al sistemului de fișiere; rezervă-l pentru înregistrări mici.

14.3.3.3. Capcana directorului lent

VFS-ul MicroPython nu indexează conținutul directoarelor așa cum o face un sistem de fișiere desktop. os.listdir() și os.stat() parcurg liniar tabelul de fișiere subiacent. Un director cu o sută de fișiere este în regulă; un director cu zece mii de fișiere este inutilizabil de lent, fiecare os.listdir() durând secunde și fiecare open() verificând tabelul pe parcurs.

Aplicațiile care scriu jurnale sau capturi pe disc întâmpină acest lucru cel mai rapid. O schemă naivă /sdcard/logs/<timestamp>.log care deschide câte un fișier nou pe minut umple directorul logs/ cu o jumătate de milion de fișiere într-un an de implementare. Cu mult înainte de asta, aplicația începe să rateze rata de cadre deoarece fiecare deschidere de fișier durează mai mult decât un interval de cadru.

Tiparul corect este să împarți fișierele într-un arbore de subdirectoare datate astfel încât niciun director să nu dețină vreodată mai mult de câteva sute de intrări:

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

Un an de jurnalizare cu câte un fișier pe oră este acum răspândit pe 365 de directoare de zi, fiecare conținând cel mult 24 de fișiere; os.listdir() asupra oricărui director rămâne ieftin, iar bucla de cadre a aplicației nu se blochează pe operații cu fișiere pe măsură ce implementarea îmbătrânește.

Același principiu se aplică pentru capturile de imagine, urmele de senzori sau orice altceva pentru care aplicația scrie un fișier per eveniment. Dacă rata evenimentelor este ridicată, arborele trebuie să fie mai adânc (an/lună/zi/oră sau an/lună/zi/oră/minut) astfel încât fiecare director-frunză să rămână mic. Dacă rata evenimentelor este scăzută, un arbore an/lună este suficient.

14.3.3.4. Căi per dispozitiv

Într-un parc de mai mult de o cameră, fișierele de jurnal trebuie să identifice de la ce unitate fizică au provenit. machine.unique_id() returnează un identificator hardware înscris în cameră la fabrică; este aceeași valoare la reporniri, la actualizările de firmware și la schimburile de carduri SD. Inserează-l în calea jurnalului sau în înregistrările de jurnal și un operator care se uită la o grămadă de carduri SD sau la un jurnal centralizat poate spune care este care:

import binascii
import machine

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

LOG_ROOT = '/sdcard/logs/' + UNIT_ID

Combinată cu tiparul de subdirectoare datate, structura devine /sdcard/logs/<unit-id>/2026/06/09/14.log – o oră de înregistrări a unei unități, într-un director suficient de superficial pentru a fi parcurs, pe o cale care numește unitatea chiar pe sistemul de fișiere.

14.3.3.5. Punând totul cap la cap

Stocarea care poate fi scrisă a unei camere livrate arată aproximativ astfel:

  • /flash – configurație, calibrare, un marker de provizionare. Scris rar, citit des. Tiparul de redenumire atomică pentru orice fișier a cărui pierdere ar strica următoarea pornire.

  • /sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log – jurnalul operațional. Scris continuu, rotit prin cale, niciodată scris printr-un director cu mii de frați.

  • /sdcard/captures/<unit-id>/<year>/<month>/<day>/ – capturile de imagine sau video pe care le face aplicația. Aceeași formă de arbore, același motiv.

Această structură costă aplicația aproximativ douăzeci de linii de cod și o scutește de modurile de eșec care scot camera din funcțiune după luni de implementare.