14.2.2.1. Skripte in die Firmware einfrieren

Ein eingefrorenes (frozen) Modul ist eine .py-Datei, die zu Bytecode kompiliert und zur Build-Zeit in das Firmware-Image eingebunden wird. Die Laufzeitumgebung importiert ein eingefrorenes Modul direkt aus dem Flash, ohne jemals auf das Dateisystem zuzugreifen. Für ein ausgeliefertes Produkt ist dies der richtige Ort für den Anwendungscode: Der Endnutzer kann nichts löschen, eine veraltete .py auf der SD-Karte kann nichts überschreiben, und die Kamera führt bei jedem Start denselben Code aus, unabhängig davon, was (wenn überhaupt) sich auf ihren Laufwerken befindet.

Diese Seite behandelt die Startsequenz, die die Kamera durchläuft, und anschließend, wie manifest.py und die freeze-Direktive eine Anwendung in den Build einbacken.

14.2.2.1.1. Die Startsequenz

Was läuft auf einer Kamera nach einem Reset, und wann:

  • Der Bootloader. Beim Einschalten wird ein kurzes DFU-Fenster geöffnet, das die IDE zum Aufspielen von Firmware-Updates nutzt. Das Fenster schließt sich nach einigen Sekunden, und der Bootloader übergibt an MicroPython. Ein laufendes Skript kann dieses Fenster bei Bedarf erneut betreten, indem es machine.bootloader() aufruft.

  • Initialisierung des eingefrorenen Dateisystems. Bevor irgendein Anwendungscode läuft, bringt die Laufzeitumgebung die Dateisysteme hoch. Der interne Flash wird unter /flash eingehängt (und leer formatiert, falls dort nichts vorhanden ist). Ist eine SD-Karte vorhanden und existiert eine Markierungsdatei namens SKIPSD nicht im internen Flash, so wird die SD-Karte unter /sdcard eingehängt. ROMFS wird, wenn der Build es enthält, automatisch unter /rom eingehängt. Das Arbeitsverzeichnis wird auf das Boot-Verzeichnis gesetzt (/sdcard, falls die Karte eingehängt wurde, andernfalls /flash), und sys.path wird mit /flash, /flash/lib, /sdcard, /sdcard/lib, /rom und /rom/lib befüllt. Die im Flash residente Einrichtung wird von einem eingefrorenen Modul namens _boot.py übernommen – Port- und Board-Infrastruktur, kein Anwendungs-Hook. Anwendungen passen _boot.py nicht an; das tut der Build. Eine SKIPSD-Datei über die IDE in den Flash zu legen ist der unterstützte Weg, die Kamera vom internen Flash statt von der SD-Karte booten zu lassen.

  • Einrichtung vor der REPL. boot.py läuft bei jedem Soft-Reset – Kaltstart, Ctrl-D aus der REPL, Beenden des laufenden Skripts und Watchdog-Wiederherstellung – bevor die REPL erreichbar wird. Seine Aufgabe ist es, die Umgebung vorzubereiten, in der der Rest des Systems läuft: jene Art von Einrichtung, die die REPL, die Anwendung und jegliches Wiederherstellungswerkzeug zum Funktionieren benötigen. Hier liegt nicht die Anwendung selbst. main.py ist der Einstiegspunkt der Anwendung.

  • Hauptschleife. main.py ist die Hauptschleife der Anwendung. Sie läuft beim Kaltstart einmal, unmittelbar nach boot.py. Bei nachfolgenden Soft-Resets wird sie nicht erneut ausgeführt – die Kamera fällt stattdessen in die REPL zurück. Diese Asymmetrie ist für die Entwicklung wichtig (ein Ctrl-D fällt in die REPL zurück, ohne die Schleife erneut zu starten, sodass die Entwicklerin den Zustand untersuchen kann), aber nicht für die Produktion: Eine im Feld installierte Kamera erlebt Einschalt-, Watchdog- und Hard-Resets, die allesamt Hardware-Resets sind, die den Kaltstart-Pfad erneut betreten und main.py wieder ausführen.

14.2.2.1.2. In die Firmware einfrieren

Der Satz eingefrorener Module eines Boards wird in boards/<TARGET>/manifest.py im Firmware-Baum deklariert. Das Manifest ist eine kleine Python-Datei, die eine Handvoll Direktiven aufruft:

  • freeze("$(OMV_LIB_DIR)/", "foo.py") – bäckt eine einzelne foo.py in den Build ein.

  • package("mylib", base_path="...") – bäckt ein aus mehreren Dateien bestehendes Python-Paket ein und behält dessen Verzeichnisstruktur unter dem angegebenen Basispfad bei.

  • include("...") – zieht eine weitere Manifest-Datei hinein. Die Board-Manifeste nutzen dies, um gemeinsame Modulsätze zu teilen.

  • require("logging") – zieht ein benanntes vorgelagertes micropython-lib-Modul über seinen Namen hinein.

Ein minimales Anwendungsmanifest fügt eine freeze-Zeile pro Top-Level-Skript und eine package-Zeile pro Paket hinzu, von dem die Anwendung abhängt.

14.2.2.1.2.1. Wo der Quellcode liegt

Der Anwendungsquellcode liegt unter scripts/libraries/ im Firmware-Baum, neben den Modulen, die der Build bereits einfriert. Die Manifest-Variable $(OMV_LIB_DIR) expandiert zu diesem Pfad, sodass Manifest-Einträge kurz bleiben. Das Bearbeiten des Manifests ist ohnehin eine Operation innerhalb des Baums, sodass das Halten des Quellcodes im Baum es erspart, ein separates Projekt-Repository im Pfad zu jonglieren.

Ein typisches Layout für eine Anwendung, die eine einzelne main.py plus ein unterstützendes Paket ausliefert:

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

Und im boards/<TARGET>/manifest.py des Boards eine freeze-Zeile für das Skript und eine package-Zeile für das Paket:

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

Einzeldatei-Skripte – hier main.py, aber dieselbe Regel gilt für boot.py oder jeden eigenständigen Helfer – verwenden freeze. Pakete aus mehreren Dateien verwenden package. Ein weiteres Skript hinzuzufügen ist eine weitere freeze-Zeile; ein weiteres Paket hinzuzufügen ist eine weitere package-Zeile.

14.2.2.1.2.2. Bauen und Flashen

Sobald das Manifest vorhanden ist, bauen Sie die Firmware genau so, wie es das Firmware-Kapitel beschreibt:

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

Die Ausgabe landet in build/<TARGET>/bin/:

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

Das Flashen der .bin und .img über die IDE ergibt eine Kamera, deren Anwendung Teil des Builds ist.

Die obige Startsequenz ist es, die das Einbacken wirksam macht: Die Laufzeitumgebung löst boot.py und main.py zu den eingefrorenen Kopien auf, bevor sie überhaupt das Dateisystem prüft, sodass eine ausgelieferte Kamera den Code des Builds ausführt, selbst wenn die SD-Karte eine veraltete boot.py aus der Entwicklung enthält.

14.2.2.1.2.3. Reihenfolge der Suche

Die Override-Semantik ist unterschiedlich für den Ausführungspfad von boot.py / main.py und für gewöhnliche import-Anweisungen. Zu wissen, was was ist, ist sowohl für die Produktion als auch für die Entwicklung wichtig:

  • Für boot.py und main.py: Die Laufzeitumgebung sucht zuerst nach einer eingefrorenen Kopie, dann im Dateisystem. Eine eingefrorene boot.py lässt sich nicht überschreiben, indem man eine auf die SD-Karte legt – wer die Kamera in Händen hält, kann den Einstiegspunkt nicht ändern, ohne neu zu flashen.

  • Für import foo: Die Laufzeitumgebung durchsucht zuerst sys.path – der /flash, /sdcard, /rom und deren lib-Unterverzeichnisse abdeckt – dann die eingefrorenen Module. Eine gleichnamige foo.py im Flash oder auf der SD-Karte überschreibt durchaus ein eingefrorenes foo. Dies ist die Entwicklungs-Annehmlichkeit: ein korrigiertes Modul auf die Karte legen, soft-reseten, die Änderung sehen, ohne neu zu flashen.

Ein ausgeliefertes Produkt, das das Verhalten „Dateisystem überschreibt eingefroren“ für Importe unterdrücken möchte, kann sys.path früh in boot.py leeren:

import sys

sys.path.clear()

Ist sys.path leer, werden alle Importe ausschließlich aus den eingefrorenen Modulen aufgelöst; nichts im Flash, auf der SD-Karte oder in ROMFS kann sie verdecken.

14.2.2.1.2.4. Das Asset-Problem

Einfrieren ist großartig für Code. Es ist nicht großartig für große Binär-Assets: Modelldateien für maschinelles Lernen, Label-Tabellen, JSON-Konfiguration, Bildvorlagen. Diese als Python-Literale einzubetten bläht den Quellcode auf, kompiliert langsam neu und verschwendet den Bytecode-Container für Daten, die der Interpreter ohnehin nur roh lesen wird. Die Seite Ein ROMFS-Image erstellen behandelt das schreibgeschützte Flash-Dateisystem, das diese Lücke füllt.