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. Пастка повільного каталогу

MicroPython VFS не індексує вміст каталогів так, як це робить файлова система десктопа. 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>/ – знімки зображень або відео, зроблені застосунком. Та сама структура дерева, та сама причина.

Така розмітка коштує застосунку приблизно двадцять рядків коду і рятує його від режимів відмов, які виводять камеру з ладу через місяці після розгортання.