14.3.3. Гигиена файловой системы¶
Флеш-память и SD-накопитель на поставляемой камере заполняются файлами, которые ни один оператор не будет вычищать вручную. Два решения относительно этого хранилища сопровождают продукт на протяжении всего срока его службы: какая поверхность хранит какой тип данных и как организованы каталоги, чтобы файловые операции продолжали работать по мере накопления записей приложением.
14.3.3.1. Что и где хранится¶
Код и ресурсы размещаются в замороженных модулях и ROMFS, которые сборка фиксирует на момент поставки. Состояние приложения – всё, что приложение записывает во время выполнения, всё, что растёт, всё, что меняется между загрузками – должно находиться где-то ещё. Камера предоставляет для этого две записываемые поверхности:
Внутренняя флеш-память в
/flash: небольшая записываемая файловая система, монтируемая до запуска любого кода приложения. Подходящее место для небольших записей фиксированного размера, переживающих перезагрузки: конфигурация, которую приложение обновляет во время выполнения, последняя известная калибровка, циклический счётчик, однострочный файл-маркер, сообщающий «эта камера была инициализирована». Ограниченное число циклов записи – современная внутренняя флеш-память выдерживает тысячи и десятки тысяч записей на сектор, а не миллионы, поэтому записи должны быть нечастыми, а не на каждый кадр.SD-карта в
/sdcard: более крупная записываемая файловая система, монтируемая при наличии карты. Подходящее место для объёмных файлов переменного размера: захваты изображений и видео, файлы журналов, данные для дообучения модели, всё, что может вырасти до мегабайтов или гигабайтов. Более высокая ёмкость записи, чем у внутренней флеш-памяти, но всё же конечная; съёмная, заменяемая и наиболее вероятно исчезающая поверхность, когда приложение находится в середине записи.
Правильный ответ на вопрос куда что-то записывать почти всегда таков: «флеш для небольших фиксированных записей, SD для всего остального». Эти две поверхности не взаимозаменяемы: приложение, которое пишет свой циклический файл журнала в /flash, исчерпает ресурс записи флеш-памяти в развёртывании, которое прекрасно работало бы на SD.
14.3.3.2. Считайте обе поверхности способными отказать¶
/flash и /sdcard могут обе отказать. SD-карту можно извлечь, флеш-память может быть повреждена при потере питания в середине записи, на любой из них может закончиться место, и любая операция с любой из них может вызвать OSError по причинам, которые приложение не получит возможности диагностировать в полевых условиях.
Два паттерна позволяют приложению пережить это:
Заключайте монтирование и операции в блоки try. Каждый
open(),os.listdir(),os.rename()для путей с пользовательскими данными потенциально может завершиться неудачей. ПерехватывайтеOSError, записывайте в журнал и переходите к заданной альтернативе – пишите в/flash, если/sdcardотсутствует, пропускайте операцию, если ни одна из поверхностей недоступна.Атомарная запись для файлов, которые должны пережить потерю питания. Запишите во временный путь, закройте дескриптор, затем выполните
os.rename()поверх рабочего имени. Либо переименование удалось и файл является новой версией, либо нет и файл является старой версией. Третьего состояния, в котором файл записан наполовину, не существует: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. Ловушка медленного каталога¶
VFS в MicroPython не индексирует содержимое каталогов так, как это делает настольная файловая система. os.listdir() и os.stat() обходят базовую таблицу файлов линейно. Каталог с сотней файлов работает нормально; каталог с десятью тысячами файлов становится неприемлемо медленным, при этом каждый os.listdir() занимает секунды, а каждый open() проверяется по таблице по ходу выполнения.
Приложения, которые пишут журналы или захваты на диск, сталкиваются с этим быстрее всего. Наивная схема /sdcard/logs/<timestamp>.log, открывающая один новый файл в минуту, заполняет каталог logs/ полумиллионом файлов за год развёртывания. Задолго до этого приложение начинает не успевать за своей частотой кадров, потому что каждое открытие файла занимает больше времени, чем интервал между кадрами.
Правильный паттерн – распределять файлы по дереву датированных подкаталогов, чтобы ни один отдельный каталог никогда не содержал более нескольких сотен записей:
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
Год ведения журнала с одним файлом в час теперь распределён по 365 каталогам-дням, каждый из которых содержит не более 24 файлов; os.listdir() для любого отдельного каталога остаётся дешёвым, и цикл кадров приложения не зависает на файловых операциях по мере старения развёртывания.
Тот же принцип применим к захватам изображений, трассировкам датчика или всему остальному, для чего приложение пишет файл на каждое событие. Если частота событий высока, дерево должно быть глубже (год/месяц/день/час или год/месяц/день/час/минута), чтобы каждый конечный каталог оставался небольшим. Если частота событий низкая, достаточно дерева год/месяц.
14.3.3.4. Пути для отдельных устройств¶
В парке из более чем одной камеры файлы журналов должны идентифицировать, с какого физического устройства они получены. 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 – час записей одного устройства, в каталоге, достаточно мелком для обхода, на пути, который называет устройство в самой файловой системе.
14.3.3.5. Собирая всё вместе¶
Записываемое хранилище поставляемой камеры выглядит примерно так:
/flash– конфигурация, калибровка, маркер инициализации. Записывается редко, читается часто. Паттерн атомарного переименования для любого файла, потеря которого нарушила бы следующую загрузку./sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log– эксплуатационный журнал. Записывается непрерывно, ротируется по пути, никогда не пишется через каталог с тысячами соседних записей./sdcard/captures/<unit-id>/<year>/<month>/<day>/– захваты изображений или видео, которые делает приложение. Та же форма дерева, та же причина.
Эта структура обходится приложению примерно в двадцать строк кода и спасает его от режимов отказа, которые выводят камеру из строя спустя месяцы после начала развёртывания.