14.3.3. Higiene do sistema de arquivos

O armazenamento em flash e em cartão SD de uma câmera em produção se enche de arquivos que nenhum operador vai limpar manualmente. Duas decisões sobre esse armazenamento acompanham o produto por toda a sua vida útil: qual superfície guarda qual tipo de dado e como os diretórios são estruturados para que as operações de arquivo continuem funcionando à medida que a aplicação acumula registros.

14.3.3.1. Onde cada coisa vai

O código e os assets ficam nos módulos congelados (frozen) e no ROMFS que a build define no momento do envio. O estado da aplicação – tudo o que a aplicação grava em tempo de execução, tudo o que cresce, tudo o que muda entre inicializações – precisa ficar em outro lugar. A câmera expõe duas superfícies graváveis para isso:

  • Flash interna em /flash: um pequeno sistema de arquivos gravável montado antes de qualquer código da aplicação rodar. O lugar certo para pequenos registros de tamanho fixo que sobrevivem a reinicializações: configuração que a aplicação atualiza em tempo de execução, a última calibração conhecida, um contador acumulativo, um arquivo marcador de uma linha dizendo “esta câmera foi provisionada”. Ciclos de gravação limitados – a flash interna moderna tolera de milhares a dezenas de milhares de gravações por setor, não milhões, então as gravações precisam ser infrequentes, não a cada quadro.

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

A resposta certa para onde gravar algo é quase sempre “flash para pequenos registros fixos, SD para todo o resto”. As duas não são intercambiáveis: uma aplicação que escreve seu arquivo de log acumulativo em /flash vai esgotar a resistência de gravação da flash em uma implantação que teria funcionado bem no SD.

14.3.3.2. Trate ambos como passíveis de falha

/flash e /sdcard podem falhar. O cartão SD pode ser ejetado, a flash pode ser corrompida por uma queda de energia no meio de uma gravação, ambos podem ficar sem espaço, e qualquer operação em qualquer um deles pode levantar OSError por razões que a aplicação não terá a chance de diagnosticar em campo.

Dois padrões fazem a aplicação sobreviver a isso:

  • Envolva montagens e operações em blocos try. Cada open(), os.listdir(), os.rename() em caminhos de dados do usuário pode falhar. Capture OSError, registre-o em log e recorra a uma alternativa definida – grave em /flash se /sdcard sumiu, pule a operação se nenhum dos dois estiver disponível.

  • Gravações atômicas para arquivos que precisam sobreviver a uma queda de energia. Grave em um caminho temporário, feche o handle e então faça os.rename() sobre o nome ativo. Ou o rename teve sucesso e o arquivo é a nova versão, ou não teve e o arquivo é a versão antiga. Não há um terceiro estado em que o arquivo fique gravado pela metade:

    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 quanto no SD. Ele não funciona para arquivos grandes o suficiente para que o arquivo tmp consuma todo o espaço livre do sistema de arquivos; reserve-o para registros pequenos.

14.3.3.3. A armadilha do diretório lento

O VFS do MicroPython não indexa o conteúdo dos diretórios da forma que um sistema de arquivos de desktop faz. os.listdir() e os.stat() percorrem a tabela de arquivos subjacente linearmente. Um diretório com cem arquivos não é problema; um diretório com dez mil arquivos é inutilizavelmente lento, com cada os.listdir() levando segundos e cada open() verificando a tabela ao longo do caminho.

Aplicações que gravam logs ou capturas em disco atingem isso mais rápido. Um esquema ingênuo de /sdcard/logs/<timestamp>.log que abre um novo arquivo por minuto enche o diretório logs/ com meio milhão de arquivos em um ano de implantação. Bem antes disso, a aplicação começa a perder sua taxa de quadros porque cada abertura de arquivo está levando mais tempo que um intervalo de quadro.

O padrão certo é dividir os arquivos em uma árvore de subdiretórios datados, de modo que nenhum diretório individual contenha mais 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 log de um-arquivo-por-hora agora está distribuído por 365 diretórios de dia, cada um contendo no máximo 24 arquivos; os.listdir() em qualquer um dos diretórios permanece barato, e o laço de quadros da aplicação não trava em operações de arquivo conforme a implantação envelhece.

O mesmo princípio se aplica a capturas de imagem, traços de sensor ou qualquer outra coisa para a qual a aplicação grave um arquivo por evento. Se a taxa de eventos for alta, a árvore deve ser mais profunda (ano/mês/dia/hora, ou ano/mês/dia/hora/minuto) para que cada diretório folha permaneça pequeno. Se a taxa de eventos for baixa, uma árvore ano/mês é suficiente.

14.3.3.4. Caminhos por dispositivo

Em uma frota de mais de uma câmera, os arquivos de log precisam identificar de qual unidade física vieram. machine.unique_id() retorna um identificador de hardware gravado na câmera na fábrica; é o mesmo valor entre reinicializações, entre atualizações de firmware e entre trocas de cartão SD. Incorpore-o no caminho do log ou nos registros do log, e um operador olhando para uma pilha de cartões SD ou um log centralizado consegue saber 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 subdiretórios datados, o layout passa a ser /sdcard/logs/<unit-id>/2026/06/09/14.log – uma hora de registros de uma unidade, em um diretório raso o suficiente para percorrer, em um caminho que nomeia a unidade no próprio sistema de arquivos.

14.3.3.5. Juntando tudo

O armazenamento gravável de uma câmera em produção se parece, aproximadamente, com isto:

  • /flash – configuração, calibração, um marcador de provisionamento. Gravado raramente, lido com frequência. Padrão de rename atômico para qualquer arquivo cuja perda quebraria a próxima inicialização.

  • /sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log – o log operacional. Gravado continuamente, rotacionado pelo caminho, nunca gravado através de um diretório com milhares de irmãos.

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

Esse layout custa à aplicação cerca de vinte linhas de código e a salva dos modos de falha que derrubam a câmera meses após o início de uma implantação.