14.2.2.1. Congelare gli script nel firmware

Un modulo congelato (frozen) è un file .py compilato in bytecode e collegato all’immagine del firmware al momento della build. Il runtime importa un modulo congelato direttamente dalla flash, senza mai consultare il filesystem su disco. Per un prodotto da distribuire questo è il posto giusto per il codice dell’applicazione: niente che l’utente finale possa cancellare, niente che un vecchio file .py sulla scheda SD possa sovrascrivere, e la cam esegue lo stesso codice a ogni avvio indipendentemente da cosa (se c’è qualcosa) è presente sulle sue unità.

Questa pagina illustra la sequenza di avvio seguita dalla cam, e poi come manifest.py e la direttiva freeze integrano un’applicazione nella build.

14.2.2.1.1. La sequenza di avvio

Cosa viene eseguito, e quando, su una cam che esce dal reset:

  • Il bootloader. L’accensione apre una breve finestra DFU che l’IDE usa per inviare gli aggiornamenti del firmware. La finestra si chiude dopo alcuni secondi e il bootloader passa il controllo a MicroPython. Uno script in esecuzione può rientrare in questa finestra su richiesta chiamando machine.bootloader().

  • Inizializzazione del filesystem congelato. Prima che venga eseguito qualsiasi codice dell’applicazione, il runtime avvia i filesystem. La flash interna viene montata su /flash (e formattata vuota se non c’è nulla). Se è presente una scheda SD e un file marcatore chiamato SKIPSD non esiste sulla flash interna, la scheda SD viene montata su /sdcard. La ROMFS, quando la build la include, viene montata automaticamente su /rom. La directory di lavoro è impostata sulla directory di avvio (/sdcard se la scheda è montata, /flash altrimenti), e sys.path viene popolato con /flash, /flash/lib, /sdcard, /sdcard/lib, /rom e /rom/lib. La configurazione residente in flash è gestita da un modulo congelato chiamato _boot.py – infrastruttura del port e della board, non un hook applicativo. Le applicazioni non personalizzano _boot.py; lo fa la build. Inserire un file SKIPSD nella flash dall’IDE è il modo supportato per far avviare la cam dalla flash interna anziché dalla scheda SD.

  • Configurazione pre-REPL. boot.py viene eseguito a ogni soft reset – avvio a freddo, Ctrl-D dal REPL, ritorno dello script in esecuzione e recupero dal watchdog – prima che il REPL diventi raggiungibile. Il suo compito è preparare l’ambiente in cui gira il resto del sistema: il tipo di configurazione che il REPL, l’applicazione e qualsiasi strumento di recupero devono avere predisposto per funzionare. Non è il posto in cui risiede l’applicazione vera e propria. main.py è il punto di ingresso dell’applicazione.

  • Loop principale. main.py è il loop principale dell’applicazione. Viene eseguito una volta all’avvio a freddo, subito dopo boot.py. Non viene rieseguito ai successivi soft reset – la cam passa invece al REPL. Questa asimmetria è importante per lo sviluppo (un Ctrl-D passa al REPL senza rieseguire il loop, così lo sviluppatore può ispezionare lo stato) ma non per la produzione: una cam sul campo subisce accensioni, watchdog e hard reset, che sono tutti reset hardware che rientrano nel percorso di avvio a freddo ed eseguono nuovamente main.py.

14.2.2.1.2. Congelare nel firmware

L’insieme dei moduli congelati di una board è dichiarato in boards/<TARGET>/manifest.py nell’albero del firmware. Il manifest è un piccolo file Python che richiama una manciata di direttive:

  • freeze("$(OMV_LIB_DIR)/", "foo.py") – integra un singolo foo.py nella build.

  • package("mylib", base_path="...") – integra un pacchetto Python composto da più file, preservandone la struttura delle directory sotto il base path indicato.

  • include("...") – include un altro file manifest. I manifest delle board lo usano per condividere insiemi comuni di moduli.

  • require("logging") – include per nome un modulo micropython-lib upstream.

Un manifest applicativo minimale aggiunge una riga freeze per ogni script di primo livello e una riga package per ogni pacchetto da cui dipende l’applicazione.

14.2.2.1.2.1. Dove risiede il sorgente

Il sorgente dell’applicazione risiede sotto scripts/libraries/ nell’albero del firmware, accanto ai moduli che la build congela già. La variabile del manifest $(OMV_LIB_DIR) si espande in quel percorso, così le voci del manifest restano brevi. Modificare il manifest è già un’operazione interna all’albero, quindi mantenere il sorgente nell’albero evita di dover gestire un repository di progetto separato durante la risoluzione dei percorsi.

Una struttura tipica per un’applicazione che distribuisce un singolo main.py più un pacchetto di supporto:

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

E nel boards/<TARGET>/manifest.py della board, una riga freeze per lo script e una riga package per il pacchetto:

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

Gli script a file singolo – main.py qui, ma la stessa regola vale per boot.py o qualsiasi helper autonomo – usano freeze. I pacchetti multi-file usano package. Aggiungere un altro script è una riga freeze in più; aggiungere un altro pacchetto è una riga package in più.

14.2.2.1.2.2. Build e flashing

Una volta predisposto il manifest, compila il firmware esattamente come descrive il capitolo sul firmware

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

L’output finisce in build/<TARGET>/bin/

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

Eseguire il flashing del .bin e dell”.img tramite l’IDE produce una cam la cui applicazione fa parte della build.

La sequenza di avvio descritta sopra è ciò che rende efficace l’integrazione: il runtime risolve boot.py e main.py alle copie congelate prima ancora di controllare il filesystem, così una cam distribuita esegue il codice della build anche se la scheda SD contiene un vecchio boot.py lasciato dallo sviluppo.

14.2.2.1.2.3. Ordine di ricerca

La semantica di sovrascrittura è diversa per il percorso di esecuzione di boot.py / main.py e per le normali istruzioni import. Sapere quale sia quale è importante sia per la produzione che per lo sviluppo:

  • Per boot.py e main.py: il runtime cerca prima una copia congelata, poi il filesystem. Un boot.py congelato non può essere sovrascritto inserendone uno sulla scheda SD – chi possiede la cam non può cambiare il punto di ingresso senza riflashare.

  • Per import foo: il runtime cerca prima in sys.path – che copre /flash, /sdcard, /rom e le loro sottodirectory lib – poi i moduli congelati. Un foo.py con lo stesso nome su flash o SD sovrascrive effettivamente un foo congelato. Questa è la comodità per lo sviluppo: inserisci un modulo corretto sulla scheda, esegui un soft reset, vedi la modifica senza riflashare.

Un prodotto distribuito che voglia sopprimere il comportamento di sovrascrittura del congelato da parte del filesystem per gli import può svuotare sys.path nelle prime righe di boot.py

import sys

sys.path.clear()

Con sys.path vuoto, tutti gli import si risolvono solo dai moduli congelati; niente su flash, SD o ROMFS può oscurarli.

14.2.2.1.2.4. Il problema degli asset

Il congelamento è ottimo per il codice. Non è ottimo per i grandi asset binari: file di modelli di machine learning, tabelle di etichette, configurazioni JSON, template di immagini. Incorporarli come literal Python gonfia il sorgente, rallenta la ricompilazione e spreca il contenitore di bytecode su dati che l’interprete leggerà comunque grezzi. La pagina Costruire un’immagine ROMFS illustra il filesystem flash di sola lettura che colma questa lacuna.