14.3.3. ファイルシステムの衛生管理

出荷されたカメラ上のフラッシュおよびSDストレージは、運用担当者が手作業で消すことのないファイルでいっぱいになっていきます。このストレージに関する2つの決定は、製品の寿命を通じて付きまといます。すなわち、どの記録領域がどの種類のデータを保持するか、そしてアプリケーションがレコードを蓄積していっても、ファイル操作が機能し続けるようにディレクトリをどう構成するかです。

14.3.3.1. どこに何を置くか

コードとアセットは、ビルドが出荷時にコミットするフリーズモジュールとROMFSに収まります。アプリケーションの状態、つまりアプリケーションが実行時に書き込むもの、増大するもの、起動ごとに変化するものは、別の場所に存在しなければなりません。カメラはそのために2つの書き込み可能な記録領域を公開しています。

  • /flash にある内蔵フラッシュ: アプリケーションコードが実行される前にマウントされる、小さな書き込み可能なファイルシステムです。再起動を生き延びる小さな固定サイズのレコードに適した場所です。アプリケーションが実行時に更新する設定、最後に判明したキャリブレーション、ローリングカウンター、「このカメラはプロビジョニング済み」と記す1行のマーカーファイルなどです。書き込み回数には制限があります。最新の内蔵フラッシュはセクターあたり数千から数万回の書き込みに耐えますが、数百万回ではありません。そのため書き込みは、フレームごとではなく、頻度を抑える必要があります。

  • /sdcard にあるSDカード: カードが挿入されているときにマウントされる、より大きな書き込み可能なファイルシステムです。かさばる可変サイズのファイルに適した場所です。画像や動画のキャプチャ、ログファイル、モデルのファインチューニングデータなど、メガバイトやギガバイトに達しうるものすべてです。内蔵フラッシュより書き込み容量は大きいものの、依然として有限です。取り外し可能で交換でき、アプリケーションが書き込みの最中に最も消える可能性の高い記録領域です。

何をどこに書き込むかの正解は、ほぼ常に「小さな固定レコードはフラッシュへ、それ以外はすべてSDへ」です。両者は交換可能ではありません。ローリングログファイルを /flash に書き殴るアプリケーションは、SDなら問題のなかったデプロイにおいて、フラッシュの書き込み耐久性を使い果たしてしまいます。

14.3.3.2. 両方を「故障しうるもの」として扱う

/flash/sdcard も、どちらも故障しうるものです。SDカードは取り出される可能性があり、フラッシュは書き込み中の電源喪失で破損する可能性があり、いずれも空き容量を使い果たす可能性があり、どちらに対する操作も、アプリケーションが現地で診断する機会を得られない理由で OSError を送出する可能性があります。

2つのパターンによって、アプリケーションはそれを生き延びます。

  • マウントと操作をtryブロックで包む。 ユーザーデータのパスに対するすべての open()os.listdir()os.rename() は、失敗する可能性があります。OSError をキャッチしてログに記録し、定められた代替手段にフォールバックしてください。/sdcard が失われていれば /flash に書き込み、どちらも利用できなければその操作をスキップします。

  • 電源喪失を生き延びなければならないファイルにはアトミックな書き込みを。 一時パスに書き込んでハンドルを閉じ、その後 os.rename() で本来の名前に上書きします。リネームが成功してファイルが新バージョンになるか、成功せずファイルが旧バージョンのままかのいずれかです。ファイルが書きかけになる第3の状態はありません:

    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)
    

    このパターンはフラッシュとSDの両方で機能します。一時ファイルがファイルシステムの空き容量を使い果たすほど大きいファイルでは機能しません。小さなレコードのために取っておいてください。

14.3.3.3. 遅いディレクトリの罠

MicroPython のVFSは、デスクトップのファイルシステムのようにディレクトリの内容をインデックス化しません。os.listdir()os.stat() は、基盤となるファイルテーブルを線形に走査します。100個のファイルがあるディレクトリは問題ありませんが、1万個のファイルがあるディレクトリは使い物にならないほど遅く、すべての os.listdir() に数秒かかり、すべての open() が処理の過程でテーブルを照合することになります。

ログやキャプチャをディスクに書き込むアプリケーションは、これに最も早く直面します。1分ごとに新しいファイルを1つ開く素朴な /sdcard/logs/<timestamp>.log 方式は、1年のデプロイで logs/ ディレクトリを50万個のファイルで満たします。それよりはるか以前に、すべてのファイルオープンが1フレーム間隔より長くかかるようになり、アプリケーションはフレームレートを取りこぼし始めます。

正しいパターンは、日付ごとのサブディレクトリのツリーにファイルを分散させ、単一のディレクトリが数百エントリを超えて保持することがないようにすることです:

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

1時間に1ファイルのログの1年分は、いまや365個の日ディレクトリに分散し、それぞれが最大でも24個のファイルしか含みません。どの単一ディレクトリに対する os.listdir() も低コストのままであり、デプロイが古くなってもアプリケーションのフレームループがファイル操作で停滞することはありません。

同じ原則は、画像キャプチャ、センサーのトレース、その他アプリケーションがイベントごとにファイルを書き込むものすべてに当てはまります。イベント発生率が高い場合、各リーフディレクトリを小さく保つために、ツリーはより深くしたくなります(year/month/day/hour、あるいは year/month/day/hour/minute)。イベント発生率が低い場合は、year/month のツリーで十分です。

14.3.3.4. デバイスごとのパス

1台を超えるカメラのフリートでは、ログファイルがどの物理ユニットから来たかを識別できる必要があります。machine.unique_id() は、工場でカメラに焼き付けられたハードウェア識別子を返します。これは再起動をまたいでも、ファームウェア更新をまたいでも、SDカードの交換をまたいでも同じ値です。これをログのパスやログレコードに埋め込んでおけば、SDカードの山や集約されたログを見る運用担当者は、どれがどれかを見分けられます:

import binascii
import machine

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

LOG_ROOT = '/sdcard/logs/' + UNIT_ID

日付ごとのサブディレクトリのパターンと組み合わせると、レイアウトは /sdcard/logs/<unit-id>/2026/06/09/14.log になります。あるユニットの1時間分のレコードが、走査できるほど浅いディレクトリに収まり、ファイルシステム自体の上でユニットを名指しするパスに置かれます。

14.3.3.5. まとめ

出荷されたカメラの書き込み可能なストレージは、おおよそ次のようになります。

  • /flash -- 設定、キャリブレーション、プロビジョニングマーカー。書き込みはまれで、読み込みは頻繁です。失われると次の起動が壊れるようなファイルには、アトミックリネームのパターンを使います。

  • /sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log -- 運用ログ。継続的に書き込まれ、パスによってローテーションされ、数千の兄弟を持つディレクトリを介して書き込まれることは決してありません。

  • /sdcard/captures/<unit-id>/<year>/<month>/<day>/ -- アプリケーションが行う画像や動画のキャプチャ。同じツリー形状、同じ理由です。

このレイアウトは、アプリケーションに約20行のコードのコストをかける代わりに、デプロイから数か月後にカメラを停止させる障害モードからアプリケーションを救います。