14.3.3. Higiene do sistema de ficheiros

O armazenamento flash e em cartão SD de uma câmara enviada para produção enche-se de ficheiros que nenhum operador vai limpar manualmente. Duas decisões sobre esse armazenamento acompanham o produto durante toda a sua vida útil: que superfície alberga que tipo de dados e como as pastas estão estruturadas para que as operações de ficheiros continuem a funcionar à medida que a aplicação acumula registos.

14.3.3.1. Onde as coisas ficam

O código e os recursos residem nos módulos frozen e no ROMFS que a compilação confirma no momento do envio. O estado da aplicação – tudo o que a aplicação escreve em tempo de execução, tudo o que cresce, tudo o que muda entre arranques – tem de ficar noutro lugar. A câmara expõe duas superfícies graváveis para isso:

  • Flash interno em /flash: um pequeno sistema de ficheiros gravável montado antes de qualquer código de aplicação ser executado. O lugar certo para registos pequenos de tamanho fixo que sobrevivem a reinicializações: configuração que a aplicação atualiza em tempo de execução, última calibração conhecida, um contador incremental, um ficheiro marcador de uma linha a dizer «esta câmara foi provisionada.» Ciclos de escrita limitados – a flash interna moderna tolera milhares a dezenas de milhares de escritas por setor, não milhões, pelo que as escritas devem ser pouco frequentes, não por fotograma.

  • Cartão SD em /sdcard: um sistema de ficheiros gravável de maior dimensão, montado quando um cartão está presente. O lugar certo para ficheiros variáveis volumosos: capturas de imagem e vídeo, ficheiros de registo, dados de ajuste fino de modelos, tudo o que possa crescer para megabytes ou gigabytes. Maior capacidade de escrita do que a flash interna, mas ainda assim finita; amovível, substituível, e a superfície com maior probabilidade de desaparecer quando a aplicação está a meio de uma escrita.

A resposta certa para onde escrever algo é quase sempre «flash para registos pequenos e fixos, SD para todo o resto.» As duas não são intercambiáveis: uma aplicação que escreve o seu ficheiro de registo incremental em /flash irá esgotar a endurance de escrita da flash numa implantação que teria funcionado bem no SD.

14.3.3.2. Tratar ambas como suscetíveis de falha

/flash e /sdcard podem ambas falhar. O cartão SD pode ser ejetado, a flash pode ser corrompida por uma perda de alimentação no meio de uma escrita, qualquer uma delas pode ficar sem espaço, e qualquer operação em qualquer uma delas pode lançar OSError por razões que a aplicação não terá oportunidade de diagnosticar no campo.

Dois padrões permitem que a aplicação sobreviva a isso:

  • Envolver montagens e operações em blocos try. Cada open(), os.listdir(), os.rename() contra caminhos de dados do utilizador pode falhar. Apanhe OSError, registe-o e recorra a uma alternativa definida – escreva para /flash se /sdcard não estiver disponível, ignore a operação se nenhuma estiver disponível.

  • Escritas atómicas para ficheiros que devem sobreviver a uma perda de alimentação. Escreva para um caminho temporário, feche o identificador e depois execute os.rename() sobre o nome em uso. Ou o rename foi bem-sucedido e o ficheiro é a nova versão, ou não foi e o ficheiro é a versão antiga. Não existe um terceiro estado em que o ficheiro está meio escrito:

    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)
    

    O padrão funciona tanto na flash como no SD. Não funciona para ficheiros suficientemente grandes para que o ficheiro temporário ocupe o espaço livre do sistema de ficheiros; reserve-o para registos pequenos.

14.3.3.3. A armadilha da pasta lenta

O VFS do MicroPython não indexa o conteúdo das pastas da forma como um sistema de ficheiros de secretária faz. os.listdir() e os.stat() percorrem a tabela de ficheiros subjacente linearmente. Uma pasta com cem ficheiros está bem; uma pasta com dez mil ficheiros é inutilizavelmente lenta, com cada os.listdir() a demorar segundos e cada open() a verificar na tabela ao longo do caminho.

As aplicações que escrevem registos ou capturas em disco atingem este limite mais rapidamente. Um esquema ingénuo /sdcard/logs/<timestamp>.log que abre um novo ficheiro por minuto preenche a pasta logs/ com meio milhão de ficheiros num ano de implantação. Muito antes disso, a aplicação começa a perder a sua taxa de fotogramas porque cada abertura de ficheiro demora mais do que um intervalo de fotograma.

O padrão correto é dividir os ficheiros por uma árvore de subpastas com data para que nenhuma pasta individual contenha mais do que algumas centenas de entradas:

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

Um ano de registos com um ficheiro por hora está agora distribuído por 365 pastas diárias, cada uma contendo no máximo 24 ficheiros; os.listdir() contra qualquer pasta individual mantém-se eficiente, e o ciclo de fotogramas da aplicação não para em operações de ficheiros à medida que a implantação avança.

O mesmo princípio aplica-se a capturas de imagens, rastreios de sensor, ou qualquer outra coisa que a aplicação escreva num ficheiro por evento. Se a taxa de eventos for alta, a árvore precisa de ser mais profunda (ano/mês/dia/hora, ou ano/mês/dia/hora/minuto) para que cada pasta folha se mantenha pequena. Se a taxa de eventos for baixa, uma árvore ano/mês é suficiente.

14.3.3.4. Caminhos por dispositivo

Numa frota com mais de uma câmara, os ficheiros de registo precisam de identificar de que unidade física vieram. machine.unique_id() devolve um identificador de hardware incorporado na câmara na fábrica; é o mesmo valor em todas as reinicializações, em todas as atualizações de firmware e em todas as trocas de cartão SD. Incorpore-o no caminho de registo ou nos registros e um operador a olhar para uma pilha de cartões SD ou um registo centralizado pode dizer qual é qual:

import binascii
import machine

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

LOG_ROOT = '/sdcard/logs/' + UNIT_ID

Combinado com o padrão de subpastas com data, o esquema torna-se /sdcard/logs/<unit-id>/2026/06/09/14.log – uma hora de registos de uma unidade, numa pasta suficientemente superficial para percorrer, num caminho que identifica a unidade no próprio sistema de ficheiros.

14.3.3.5. Tudo junto

O armazenamento gravável de uma câmara enviada tem aproximadamente este aspeto:

  • /flash – configuração, calibração, um marcador de provisionamento. Escrito raramente, lido frequentemente. Padrão de atomic-rename para qualquer ficheiro cuja perda quebraria o próximo arranque.

  • /sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log – o registo operacional. Escrito continuamente, rotacionado pelo caminho, nunca escrito através de uma pasta com milhares de irmãos.

  • /sdcard/captures/<unit-id>/<year>/<month>/<day>/ – capturas de imagem ou vídeo que a aplicação faz. Mesma estrutura de árvore, mesma razão.

Esse esquema custa à aplicação cerca de vinte linhas de código e poupa-a dos modos de falha que derrubam a câmara meses depois do início de uma implantação.