14.2.2.1. Zamrażanie skryptów w oprogramowaniu układowym

Moduł zamrożony (frozen) to plik .py skompilowany do kodu bajtowego i wkompilowany w obraz oprogramowania układowego na etapie budowania. Środowisko uruchomieniowe importuje zamrożony moduł bezpośrednio z pamięci flash, w ogóle nie sięgając do systemu plików na dysku. W przypadku gotowego produktu jest to właściwe miejsce dla kodu aplikacji: użytkownik końcowy nie ma czego usuwać, żaden nieaktualny plik .py na karcie SD niczego nie nadpisze, a kamera przy każdym uruchomieniu wykonuje ten sam kod, niezależnie od tego, co (jeśli cokolwiek) znajduje się na jej dyskach.

Ta strona omawia sekwencję rozruchu, którą wykonuje kamera, a następnie sposób, w jaki manifest.py oraz dyrektywa freeze wkompilowują aplikację w build.

14.2.2.1.1. Sekwencja rozruchu

Co i kiedy uruchamia się w kamerze wychodzącej z resetu:

  • Bootloader. Po włączeniu zasilania następuje krótkie okno DFU, którego IDE używa do wysyłania aktualizacji oprogramowania układowego. Okno zamyka się po kilku sekundach, a bootloader przekazuje sterowanie do MicroPython. Działający skrypt może na żądanie ponownie wejść w to okno, wywołując machine.bootloader().

  • Inicjalizacja zamrożonego systemu plików. Zanim uruchomi się jakikolwiek kod aplikacji, środowisko uruchomieniowe uruchamia systemy plików. Wewnętrzna pamięć flash jest montowana w /flash (i formatowana na pusto, jeśli nic tam nie ma). Jeśli obecna jest karta SD oraz na wewnętrznej pamięci flash nie istnieje plik znacznikowy o nazwie SKIPSD, karta SD jest montowana w /sdcard. ROMFS, gdy build go zawiera, jest montowany automatycznie w /rom. Katalog roboczy jest ustawiany na katalog rozruchowy (/sdcard, jeśli karta została zamontowana, w przeciwnym razie /flash), a sys.path jest wypełniana wpisami /flash, /flash/lib, /sdcard, /sdcard/lib, /rom oraz /rom/lib. Konfiguracją rezydującą w pamięci flash zajmuje się zamrożony moduł o nazwie _boot.py – infrastruktura portu i płytki, a nie punkt zaczepienia dla aplikacji. Aplikacje nie modyfikują _boot.py; robi to build. Umieszczenie pliku SKIPSD w pamięci flash z poziomu IDE to wspierany sposób, aby kamera uruchamiała się z wewnętrznej pamięci flash zamiast z karty SD.

  • Konfiguracja przed REPL. boot.py uruchamia się przy każdym miękkim resecie – zimnym rozruchu, Ctrl-D z REPL, zakończeniu działania skryptu oraz odzyskiwaniu po zadziałaniu watchdoga – zanim REPL stanie się osiągalny. Jego zadaniem jest przygotowanie środowiska, w którym działa reszta systemu: rodzaj konfiguracji, którą REPL, aplikacja oraz wszelkie narzędzia odzyskiwania muszą mieć przygotowaną, aby działać. Nie jest to miejsce, w którym żyje sama aplikacja. main.py jest punktem wejścia aplikacji.

  • Pętla główna. main.py to pętla główna aplikacji. Uruchamia się raz przy zimnym rozruchu, bezpośrednio po boot.py. Nie jest uruchamiana ponownie przy kolejnych miękkich resetach – kamera przechodzi wtedy do REPL. Ta asymetria ma znaczenie podczas tworzenia oprogramowania (Ctrl-D przechodzi do REPL bez ponownego uruchamiania pętli, dzięki czemu programista może zbadać stan), ale nie w produkcji: kamera w terenie doświadcza włączenia zasilania, zadziałania watchdoga oraz twardych resetów, które wszystkie są resetami sprzętowymi ponownie wchodzącymi na ścieżkę zimnego rozruchu i ponownie uruchamiającymi main.py.

14.2.2.1.2. Zamrażanie w oprogramowaniu układowym

Zestaw zamrożonych modułów płytki jest deklarowany w pliku boards/<TARGET>/manifest.py w drzewie oprogramowania układowego. Manifest to niewielki plik Python, który wywołuje kilka dyrektyw:

  • freeze("$(OMV_LIB_DIR)/", "foo.py") – wkompilowuje pojedynczy foo.py w build.

  • package("mylib", base_path="...") – wkompilowuje wieloplikowy pakiet Python, zachowując jego układ katalogów pod podaną ścieżką bazową.

  • include("...") – dołącza inny plik manifestu. Manifesty płytek używają tego do współdzielenia wspólnych zestawów modułów.

  • require("logging") – dołącza nazwany moduł micropython-lib z repozytorium nadrzędnego wg nazwy.

Minimalny manifest aplikacji dodaje jedną linię freeze na każdy skrypt najwyższego poziomu oraz jedną linię package na każdy pakiet, od którego zależy aplikacja.

14.2.2.1.2.1. Gdzie znajduje się kod źródłowy

Kod źródłowy aplikacji znajduje się w scripts/libraries/ w drzewie oprogramowania układowego, obok modułów, które build już zamraża. Zmienna manifestu $(OMV_LIB_DIR) rozwija się do tej ścieżki, dzięki czemu wpisy w manifeście pozostają krótkie. Edycja manifestu jest i tak operacją wewnątrz drzewa, więc trzymanie kodu źródłowego w drzewie pozwala uniknąć żonglowania osobnym repozytorium projektu przy rozwiązywaniu ścieżek.

Typowy układ dla aplikacji dostarczającej pojedynczy main.py wraz z towarzyszącym pakietem:

scripts/libraries/
    main.py
    my_lib/
        __init__.py
        helpers.py

Natomiast w pliku boards/<TARGET>/manifest.py płytki: jedna linia freeze dla skryptu i jedna linia package dla pakietu:

freeze("$(OMV_LIB_DIR)/", "main.py")
package("my_lib", base_path="$(OMV_LIB_DIR)/my_lib")

Skrypty jednoplikowe – tutaj main.py, ale ta sama zasada dotyczy boot.py lub dowolnego samodzielnego pomocnika – używają freeze. Wieloplikowe pakiety używają package. Dodanie kolejnego skryptu to jedna dodatkowa linia freeze; dodanie kolejnego pakietu to jedna dodatkowa linia package.

14.2.2.1.2.2. Budowanie i wgrywanie

Gdy manifest jest na miejscu, zbuduj oprogramowanie układowe dokładnie tak, jak opisuje rozdział o oprogramowaniu układowym

make -j$(nproc) -C lib/micropython/mpy-cross   # once, builds the cross-compiler
make -j$(nproc) TARGET=<TARGET>                # builds the firmware

Wynik trafia do build/<TARGET>/bin/

build/<TARGET>/bin/
    firmware.bin     # flash through the IDE
    romfs0.img       # flash through the IDE in a separate step

Wgranie plików .bin i .img przez IDE daje kamerę, której aplikacja jest częścią builda.

Powyższa sekwencja rozruchu sprawia, że wkompilowanie jest skuteczne: środowisko uruchomieniowe rozwiązuje boot.py i main.py do zamrożonych kopii, zanim w ogóle sprawdzi system plików, więc dostarczona kamera wykonuje kod z builda nawet wtedy, gdy karta SD zawiera nieaktualny boot.py pozostawiony z czasów tworzenia oprogramowania.

14.2.2.1.2.3. Kolejność wyszukiwania

Semantyka nadpisywania jest różna dla ścieżki wykonania boot.py / main.py oraz dla zwykłych instrukcji import. Wiedza o tym, co jest czym, ma znaczenie zarówno dla produkcji, jak i dla tworzenia oprogramowania:

  • Dla boot.py i main.py: środowisko uruchomieniowe najpierw szuka kopii zamrożonej, a dopiero potem w systemie plików. Zamrożonego boot.py nie da się nadpisać, umieszczając go na karcie SD – ktokolwiek trzyma kamerę, nie może zmienić punktu wejścia bez ponownego wgrania oprogramowania.

  • Dla import foo: środowisko uruchomieniowe najpierw przeszukuje sys.path – co obejmuje /flash, /sdcard, /rom oraz ich podkatalogi lib – a potem moduły zamrożone. Plik foo.py o tej samej nazwie na pamięci flash lub karcie SD faktycznie nadpisuje zamrożony foo. To jest udogodnienie przy tworzeniu oprogramowania: umieść poprawiony moduł na karcie, wykonaj miękki reset i zobacz zmianę bez ponownego wgrywania oprogramowania.

Gotowy produkt, który chce wyłączyć zachowanie nadpisywania zamrożonych modułów przez system plików dla importów, może wyczyścić sys.path na wczesnym etapie boot.py

import sys

sys.path.clear()

Przy pustym sys.path wszystkie importy są rozwiązywane wyłącznie z modułów zamrożonych; nic na pamięci flash, karcie SD ani w ROMFS nie może ich przesłonić.

14.2.2.1.2.4. Problem zasobów

Zamrażanie świetnie sprawdza się w przypadku kodu. Nie sprawdza się natomiast w przypadku dużych zasobów binarnych: plików modeli uczenia maszynowego, tablic etykiet, konfiguracji JSON, szablonów obrazów. Osadzanie ich jako literałów Python rozdyma kod źródłowy, wydłuża rekompilację i marnuje kontener kodu bajtowego na dane, które interpreter i tak zamierza odczytać w surowej postaci. Strona Budowanie obrazu ROMFS omawia tylko-do-odczytu system plików w pamięci flash, który wypełnia tę lukę.