14.3.3. Igiene del filesystem#

La memoria flash e SD di una cam in produzione si riempiono di file che nessun operatore andrà a ripulire a mano. Due decisioni riguardanti questa memoria accompagnano il prodotto per tutta la sua vita: quale supporto contiene quale tipo di dati e come sono strutturate le directory affinché le operazioni sui file continuino a funzionare man mano che l’applicazione accumula record.

14.3.3.1. Dove vanno le cose#

Il codice e gli asset risiedono nei moduli congelati e nel ROMFS che la build fissa al momento della spedizione. Lo stato dell’applicazione – tutto ciò che l’applicazione scrive a runtime, tutto ciò che cresce, tutto ciò che cambia tra un avvio e l’altro – deve risiedere da qualche altra parte. La cam espone due supporti scrivibili a questo scopo:

  • Flash interna in /flash: un piccolo filesystem scrivibile montato prima che venga eseguito qualsiasi codice applicativo. Il posto giusto per piccoli record di dimensione fissa che sopravvivono ai riavvii: la configurazione che l’applicazione aggiorna a runtime, l’ultima calibrazione nota, un contatore progressivo, un file marcatore di una riga che dice «questa cam è stata provisionata.» Cicli di scrittura limitati – la flash interna moderna tollera da migliaia a decine di migliaia di scritture per settore, non milioni, quindi le scritture devono essere poco frequenti, non per ogni frame.

  • Scheda SD in /sdcard: un filesystem scrivibile più grande montato quando è presente una scheda. Il posto giusto per file voluminosi e di dimensione variabile: acquisizioni di immagini e video, file di log, dati per il fine-tuning del modello, qualsiasi cosa che possa crescere fino a megabyte o gigabyte. Maggiore capacità di scrittura rispetto alla flash interna ma comunque finita; rimovibile, sostituibile, e il supporto che più probabilmente sparisce mentre l’applicazione è nel mezzo di una scrittura.

La risposta giusta su dove scrivere qualcosa è quasi sempre «flash per i piccoli record fissi, SD per tutto il resto.» I due non sono intercambiabili: un’applicazione che scarabocchia il proprio file di log progressivo su /flash esaurirà la resistenza alle scritture della flash in un’installazione in cui sarebbe andato tutto bene su SD.

14.3.3.2. Tratta entrambi come soggetti a guasti#

/flash e /sdcard possono entrambi guastarsi. La scheda SD può essere espulsa, la flash può danneggiarsi a causa di un’interruzione di corrente nel mezzo di una scrittura, entrambe possono esaurire lo spazio, e qualsiasi operazione su una delle due può sollevare un OSError per motivi che l’applicazione non avrà la possibilità di diagnosticare sul campo.

Due pattern fanno sì che l’applicazione sopravviva a tutto questo:

  • Avvolgi i mount e le operazioni in blocchi try. Ogni open(), os.listdir(), os.rename() su percorsi di dati utente è potenzialmente soggetto a guasti. Cattura OSError, registralo nel log e ricorri a un’alternativa definita – scrivi su /flash se /sdcard non c’è, salta l’operazione se nessuno dei due è disponibile.

  • Scritture atomiche per i file che devono sopravvivere a un’interruzione di corrente. Scrivi su un percorso temporaneo, chiudi l’handle, poi usa os.rename() sul nome attivo. O il rename è riuscito e il file è la nuova versione, oppure non è riuscito e il file è la vecchia versione. Non esiste un terzo stato in cui il file è scritto a metà:

    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)
    

    Il pattern funziona sia su flash che su SD. Non funziona per file abbastanza grandi da far sì che il file temporaneo consumi lo spazio libero del filesystem; riservalo ai piccoli record.

14.3.3.3. La trappola della directory lenta#

Il VFS di MicroPython non indicizza il contenuto delle directory come fa un filesystem desktop. os.listdir() e os.stat() scorrono linearmente la tabella dei file sottostante. Una directory con un centinaio di file va bene; una directory con diecimila file è inutilizzabilmente lenta, con ogni os.listdir() che richiede secondi e ogni open() che deve verificare la tabella durante il suo percorso.

Le applicazioni che scrivono log o acquisizioni su disco incontrano questo problema più rapidamente. Uno schema ingenuo come /sdcard/logs/<timestamp>.log che apre un nuovo file al minuto riempie la directory logs/ con mezzo milione di file in un anno di installazione. Molto prima di allora l’applicazione inizia a mancare il proprio frame rate perché ogni apertura di file richiede più tempo di un intervallo tra frame.

Il pattern giusto è distribuire i file su un albero di sottodirectory datate in modo che nessuna singola directory contenga mai più di qualche centinaio di voci:

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 anno di logging con un file all’ora è ora distribuito su 365 directory giornaliere, ciascuna contenente al massimo 24 file; os.listdir() su una qualsiasi singola directory rimane economico, e il ciclo dei frame dell’applicazione non si blocca sulle operazioni sui file man mano che l’installazione invecchia.

Lo stesso principio si applica alle acquisizioni di immagini, alle tracce dei sensori, o a qualsiasi altra cosa per cui l’applicazione scrive un file per evento. Se la frequenza degli eventi è alta, l’albero deve essere più profondo (anno/mese/giorno/ora, oppure anno/mese/giorno/ora/minuto) in modo che ogni directory foglia rimanga piccola. Se la frequenza degli eventi è bassa, un albero anno/mese è sufficiente.

14.3.3.4. Percorsi per dispositivo#

In un parco di più di una cam, i file di log devono identificare da quale unità fisica provengono. machine.unique_id() restituisce un identificatore hardware impresso nella cam in fabbrica; è lo stesso valore tra un riavvio e l’altro, tra gli aggiornamenti del firmware, e tra le sostituzioni della scheda SD. Inseriscilo nel percorso del log o nei record del log e un operatore che guarda una pila di schede SD o un log centralizzato può capire quale sia quale:

import binascii
import machine

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

LOG_ROOT = '/sdcard/logs/' + UNIT_ID

Combinato con il pattern delle sottodirectory datate, il layout diventa /sdcard/logs/<unit-id>/2026/06/09/14.log – un’ora di record di una singola unità, in una directory abbastanza superficiale da poter essere scorsa, su un percorso che identifica l’unità sul filesystem stesso.

14.3.3.5. Mettere tutto insieme#

La memoria scrivibile di una cam in produzione assomiglia all’incirca a questa:

  • /flash – configurazione, calibrazione, un marcatore di provisioning. Scritta raramente, letta spesso. Pattern di rename atomico per ogni file la cui perdita comprometterebbe il prossimo avvio.

  • /sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log – il log operativo. Scritto continuamente, ruotato dal percorso, mai scritto attraverso una directory con migliaia di elementi simili.

  • /sdcard/captures/<unit-id>/<year>/<month>/<day>/ – le acquisizioni di immagini o video che l’applicazione effettua. Stessa forma dell’albero, stesso motivo.

Quel layout costa all’applicazione circa venti righe di codice e la salva dai modi di fallimento che mettono fuori uso la cam mesi dopo l’inizio di un’installazione.