14.3.3. Higiena systemu plików¶
Pamięć flash i karta SD w wysłanej kamerze zapełniają się plikami, których żaden operator nie będzie ręcznie usuwał. Dwie decyzje dotyczące tej pamięci pozostają z produktem przez cały okres jego użytkowania: która powierzchnia przechowuje jaki rodzaj danych oraz jak zorganizowane są katalogi, aby operacje na plikach nadal działały w miarę gromadzenia rekordów przez aplikację.
14.3.3.1. Gdzie co trafia¶
Kod i zasoby znajdują się w zamrożonych modułach oraz ROMFS, które kompilacja zatwierdza w momencie wysyłki. Stan aplikacji – wszystko, co aplikacja zapisuje w czasie działania, wszystko, co rośnie, wszystko, co zmienia się między uruchomieniami – musi przebywać gdzie indziej. Kamera udostępnia do tego dwie zapisywalne powierzchnie:
Wewnętrzna pamięć flash pod
/flash: niewielki zapisywalny system plików montowany przed uruchomieniem jakiegokolwiek kodu aplikacji. Właściwe miejsce na małe rekordy o stałym rozmiarze, które przetrwają ponowne uruchomienia: konfigurację, którą aplikacja aktualizuje w czasie działania, ostatnio znaną kalibrację, narastający licznik, jednowierszowy plik znacznika mówiący „ta kamera została przygotowana”. Ograniczona liczba cykli zapisu – nowoczesna wewnętrzna pamięć flash znosi od tysięcy do dziesiątek tysięcy zapisów na sektor, a nie miliony, więc zapisy muszą być rzadkie, a nie wykonywane na każdą ramkę.Karta SD pod
/sdcard: większy zapisywalny system plików montowany, gdy karta jest obecna. Właściwe miejsce na obszerne pliki o zmiennym rozmiarze: przechwycone obrazy i wideo, pliki dziennika, dane do dostrajania modelu, wszystko, co może urosnąć do megabajtów lub gigabajtów. Większa pojemność zapisu niż wewnętrzna pamięć flash, ale wciąż skończona; wymienna, łatwa do zastąpienia i powierzchnia najbardziej narażona na zniknięcie w trakcie zapisu przez aplikację.
Właściwa odpowiedź na pytanie gdzie coś zapisać to niemal zawsze „flash dla małych rekordów o stałym rozmiarze, SD dla wszystkiego innego”. Te dwie powierzchnie nie są wymienne: aplikacja, która zapisuje swój narastający plik dziennika do /flash, wyczerpie wytrzymałość zapisu pamięci flash we wdrożeniu, które na karcie SD przebiegłoby bez problemu.
14.3.3.2. Traktuj obie jako mogące zawieść¶
Zarówno /flash, jak i /sdcard mogą zawieść. Karta SD może zostać wysunięta, pamięć flash może ulec uszkodzeniu wskutek utraty zasilania w trakcie zapisu, w obu może zabraknąć miejsca, a każda operacja na którejkolwiek z nich może zgłosić OSError z powodów, których aplikacja nie będzie miała szansy zdiagnozować w terenie.
Dwa wzorce sprawiają, że aplikacja przetrwa taką sytuację:
Opakowuj montowanie i operacje w bloki try. Każde
open(),os.listdir(),os.rename()wykonywane na ścieżkach z danymi użytkownika może się nie powieść. PrzechwyćOSError, zapisz to w dzienniku i przejdź do zdefiniowanej alternatywy – zapisz do/flash, jeśli/sdcardzniknęło, lub pomiń operację, jeśli żadna z powierzchni nie jest dostępna.Zapisy atomowe dla plików, które muszą przetrwać utratę zasilania. Zapisz do tymczasowej ścieżki, zamknij uchwyt, a następnie wykonaj
os.rename()na docelową nazwę. Albo zmiana nazwy się powiodła i plik jest nową wersją, albo nie i plik jest starą wersją. Nie ma trzeciego stanu, w którym plik jest zapisany do połowy: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)
Wzorzec ten działa zarówno na pamięci flash, jak i na karcie SD. Nie sprawdza się on w przypadku plików na tyle dużych, że plik tymczasowy zajmuje całą wolną przestrzeń systemu plików; zarezerwuj go dla małych rekordów.
14.3.3.3. Pułapka wolnego katalogu¶
VFS MicroPython nie indeksuje zawartości katalogów tak, jak robi to system plików na komputerze stacjonarnym. os.listdir() i os.stat() przeszukują leżącą u podstaw tablicę plików liniowo. Katalog ze stu plikami jest w porządku; katalog z dziesięcioma tysiącami plików jest nieznośnie wolny, gdzie każde os.listdir() trwa sekundy, a każde open() sprawdza tablicę po drodze.
Aplikacje, które zapisują dzienniki lub przechwycone obrazy na dysk, napotykają ten problem najszybciej. Naiwny schemat /sdcard/logs/<timestamp>.log, który otwiera jeden nowy plik na minutę, zapełnia katalog logs/ pół milionem plików w ciągu roku wdrożenia. Na długo przedtem aplikacja zaczyna nie nadążać z liczbą klatek, ponieważ każde otwarcie pliku trwa dłużej niż interwał ramki.
Właściwym wzorcem jest rozdzielenie plików w drzewie datowanych podkatalogów, tak aby żaden pojedynczy katalog nigdy nie zawierał więcej niż kilkaset wpisów:
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
Roczne rejestrowanie po jednym pliku na godzinę jest teraz rozłożone na 365 katalogów dni, z których każdy zawiera co najwyżej 24 pliki; os.listdir() wykonywane na dowolnym katalogu pozostaje tanie, a pętla ramek aplikacji nie zacina się na operacjach plikowych w miarę starzenia się wdrożenia.
Ta sama zasada dotyczy przechwytywanych obrazów, śladów sensora i wszystkiego innego, dla czego aplikacja zapisuje plik na zdarzenie. Jeśli częstotliwość zdarzeń jest wysoka, drzewo powinno być głębsze (rok/miesiąc/dzień/godzina lub rok/miesiąc/dzień/godzina/minuta), aby każdy katalog liścia pozostawał mały. Jeśli częstotliwość zdarzeń jest niska, wystarczy drzewo rok/miesiąc.
14.3.3.4. Ścieżki per urządzenie¶
We flocie liczącej więcej niż jedną kamerę pliki dziennika muszą identyfikować, z której fizycznej jednostki pochodzą. machine.unique_id() zwraca identyfikator sprzętowy wpisany w kamerę fabrycznie; jest to ta sama wartość po ponownych uruchomieniach, po aktualizacjach oprogramowania układowego i po wymianach karty SD. Umieść go w ścieżce dziennika lub w rekordach dziennika, a operator patrzący na stos kart SD lub na scentralizowany dziennik będzie w stanie rozpoznać, która jest która:
import binascii
import machine
UNIT_ID = binascii.hexlify(machine.unique_id()).decode()
LOG_ROOT = '/sdcard/logs/' + UNIT_ID
W połączeniu ze wzorcem datowanych podkatalogów układ przyjmuje postać /sdcard/logs/<unit-id>/2026/06/09/14.log – godzina rekordów jednej jednostki, w katalogu na tyle płytkim, by go przejść, na ścieżce, która nazywa jednostkę w samym systemie plików.
14.3.3.5. Łącząc wszystko w całość¶
Zapisywalna pamięć wysłanej kamery wygląda mniej więcej tak:
/flash– konfiguracja, kalibracja, znacznik przygotowania. Zapisywana rzadko, odczytywana często. Wzorzec atomowej zmiany nazwy dla każdego pliku, którego utrata uniemożliwiłaby kolejne uruchomienie./sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log– dziennik operacyjny. Zapisywany w sposób ciągły, rotowany przez ścieżkę, nigdy nie zapisywany przez katalog z tysiącami sąsiadów./sdcard/captures/<unit-id>/<year>/<month>/<day>/– obrazy lub wideo przechwytywane przez aplikację. Ten sam kształt drzewa, ten sam powód.
Taki układ kosztuje aplikację około dwudziestu wierszy kodu i chroni ją przed trybami awarii, które wyłączają kamerę po miesiącach wdrożenia.