14.2.2.1. Scripts in de firmware bevriezen

Een frozen module is een .py-bestand dat tijdens het bouwen naar bytecode wordt gecompileerd en in de firmware-image wordt gekoppeld. De runtime importeert een frozen module rechtstreeks vanuit het flashgeheugen, zonder ooit naar het bestandssysteem op de schijf te kijken. Voor een verzonden product is dit de juiste plek voor de applicatiecode: er is niets voor de eindgebruiker om te verwijderen, niets waarmee een verouderd .py-bestand op de SD-kaart de boel kan overschrijven, en de cam draait bij elke boot dezelfde code, ongeacht wat (indien iets) er op de schijven staat.

Deze pagina behandelt de opstartvolgorde die de cam doorloopt, en vervolgens hoe manifest.py en de freeze-directive een applicatie in de build inbakken.

14.2.2.1.1. De opstartvolgorde

Wat er draait, en wanneer, op een cam die uit reset komt:

  • De bootloader. Bij het inschakelen wordt een kort DFU-venster geopend dat de IDE gebruikt om firmware-updates te pushen. Het venster sluit na enkele seconden en de bootloader draagt over aan MicroPython. Een draaiend script kan dit venster op verzoek opnieuw openen door machine.bootloader() aan te roepen.

  • Initialisatie van het frozen bestandssysteem. Voordat er applicatiecode draait, brengt de runtime de bestandssystemen omhoog. Het interne flashgeheugen wordt gekoppeld op /flash (en leeg geformatteerd als er niets staat). Als er een SD-kaart aanwezig is en een markeringsbestand genaamd SKIPSD niet op het interne flashgeheugen bestaat, wordt de SD-kaart gekoppeld op /sdcard. ROMFS wordt, wanneer de build het bevat, automatisch gekoppeld op /rom. De werkmap wordt ingesteld op de bootmap (/sdcard als de kaart gekoppeld is, anders /flash), en sys.path wordt gevuld met /flash, /flash/lib, /sdcard, /sdcard/lib, /rom en /rom/lib. De in flash residerende setup wordt afgehandeld door een frozen module genaamd _boot.py – port- en board-infrastructuur, geen applicatiehaak. Applicaties passen _boot.py niet aan; de build doet dat. Een SKIPSD-bestand vanuit de IDE op het flashgeheugen plaatsen is de ondersteunde manier om de cam vanaf het interne flashgeheugen te laten booten in plaats van vanaf de SD-kaart.

  • Pre-REPL-setup. boot.py draait bij elke soft reset – koude boot, Ctrl-D vanuit de REPL, het terugkeren van het draaiende script en watchdog-herstel – voordat de REPL bereikbaar wordt. De taak ervan is om de omgeving voor te bereiden waarin de rest van het systeem draait: het soort setup dat de REPL, de applicatie en eventuele hersteltools allemaal nodig hebben om te functioneren. Het is niet de plek waar de applicatie zelf woont. main.py is het startpunt van de applicatie.

  • Hoofdlus. main.py is de hoofdlus van de applicatie. Draait eenmaal bij koude boot, direct na boot.py. Wordt niet opnieuw uitgevoerd bij volgende soft resets – de cam valt in plaats daarvan terug naar de REPL. Die asymmetrie is van belang voor ontwikkeling (een Ctrl-D valt terug naar de REPL zonder de lus opnieuw uit te voeren, zodat de ontwikkelaar de status kan inspecteren) maar niet voor productie: een uitgerolde cam ziet inschakeling, watchdog en harde resets, die allemaal hardware-resets zijn die het koude-bootpad opnieuw betreden en main.py opnieuw uitvoeren.

14.2.2.1.2. In de firmware bevriezen

De verzameling frozen modules van een board wordt gedeclareerd in boards/<TARGET>/manifest.py in de firmware-boom. Het manifest is een klein Python-bestand dat een handvol directives aanroept:

  • freeze("$(OMV_LIB_DIR)/", "foo.py") – bakt een enkele foo.py in de build in.

  • package("mylib", base_path="...") – bakt een Python-pakket met meerdere bestanden in, met behoud van de mapindeling onder het opgegeven basispad.

  • include("...") – haalt een ander manifestbestand binnen. De board-manifesten gebruiken dit om gemeenschappelijke modulesets te delen.

  • require("logging") – haalt een benoemde upstream micropython-lib-module op naam binnen.

Een minimaal applicatiemanifest voegt één freeze-regel toe per top-level script en één package-regel per pakket waarvan de applicatie afhankelijk is.

14.2.2.1.2.1. Waar de broncode staat

De applicatiebroncode staat onder scripts/libraries/ in de firmware-boom, naast de modules die de build al bevriest. De manifestvariabele $(OMV_LIB_DIR) breidt uit naar dat pad, zodat manifestregels kort blijven. Het bewerken van het manifest is al een operatie binnen de boom, dus de broncode in de boom houden voorkomt dat je een aparte projectrepo in de padresolutie moet jongleren.

Een typische indeling voor een applicatie die een enkele main.py plus een ondersteunend pakket verzendt:

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

En in de boards/<TARGET>/manifest.py van het board, één freeze-regel voor het script en één package-regel voor het pakket:

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

Enkele-bestandsscripts – hier main.py, maar dezelfde regel geldt voor boot.py of elke zelfstandige helper – gebruiken freeze. Pakketten met meerdere bestanden gebruiken package. Nog een script toevoegen is één extra freeze-regel; nog een pakket toevoegen is één extra package-regel.

14.2.2.1.2.2. Bouwen en flashen

Zodra het manifest op zijn plaats staat, bouw je de firmware precies zoals het firmware-hoofdstuk beschrijft:

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

De uitvoer komt terecht in build/<TARGET>/bin/

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

Het flashen van de .bin en .img via de IDE levert een cam op waarvan de applicatie deel uitmaakt van de build.

De bovenstaande opstartvolgorde is wat het inbakken effectief maakt: de runtime resolveert boot.py en main.py naar de frozen kopieën voordat hij überhaupt het bestandssysteem controleert, zodat een verzonden cam de code van de build draait, zelfs als de SD-kaart een verouderde boot.py bevat die nog van de ontwikkeling is overgebleven.

14.2.2.1.2.3. Opzoekvolgorde

De overschrijfsemantiek is verschillend voor het uitvoeringspad van boot.py / main.py en voor gewone import-statements. Weten welk pad welk is, is van belang voor zowel productie als ontwikkeling:

  • Voor boot.py en main.py: de runtime zoekt eerst naar een frozen kopie, daarna naar het bestandssysteem. Een frozen boot.py kan niet worden overschreven door er een op de SD-kaart te plaatsen – wie de cam in handen heeft, kan het startpunt niet wijzigen zonder opnieuw te flashen.

  • Voor import foo: de runtime doorzoekt eerst sys.path – dat dekt /flash, /sdcard, /rom en hun lib-submappen – en daarna de frozen modules. Een foo.py met dezelfde naam op flash of SD overschrijft wel een frozen foo. Dit is de ontwikkelfaciliteit: plaats een gecorrigeerde module op de kaart, doe een soft reset en zie de wijziging zonder opnieuw te flashen.

Een verzonden product dat het gedrag van bestandssysteem-overschrijft-frozen voor imports wil onderdrukken, kan sys.path vroeg in boot.py leegmaken:

import sys

sys.path.clear()

Met een lege sys.path worden alle imports alleen vanuit de frozen modules geresolveerd; niets op flash, SD of ROMFS kan ze overschaduwen.

14.2.2.1.2.4. Het assetprobleem

Bevriezen is geweldig voor code. Het is niet geweldig voor grote binaire assets: machine-learning-modelbestanden, labeltabellen, JSON-configuratie, afbeeldingssjablonen. Deze inbedden als Python-literals doet de broncode opzwellen, hercompileert traag en verspilt de bytecode-container aan data die de interpreter toch gewoon rauw gaat lezen. De Een ROMFS-image bouwen-pagina behandelt het alleen-lezen flash-bestandssysteem dat dit gat opvult.