16.2.2.1. Freezing scripts into the firmware¶
A frozen module is a .py file compiled to bytecode and
linked into the firmware image at build time. The runtime
imports a frozen module straight from flash, without ever
looking at the on-disk filesystem. For a shipped product this
is the right place for the application code: nothing for the
end user to delete, nothing for a stale .py on the SD card
to override, and the cam runs the same code every boot
regardless of what (if anything) is on its drives.
This page covers the startup sequence the cam follows, then how
manifest.py and the freeze directive bake an application
into the build.
16.2.2.1.1. The startup sequence¶
What runs, and when, on a cam coming out of reset:
The bootloader. Power-up enters a short DFU window the IDE uses to push firmware updates. The window closes after a few seconds and the bootloader hands off to MicroPython. A running script can re-enter this window on demand by calling
machine.bootloader().Frozen filesystem init. Before any application code runs, the runtime brings the filesystems up. Internal flash is mounted at
/flash(and formatted blank if there is nothing there). If an SD card is present and a marker file calledSKIPSDdoes not exist on internal flash, the SD card is mounted at/sdcard. ROMFS, when the build includes it, is mounted automatically at/rom. The working directory is set to the boot directory (/sdcardif the card mounted,/flashotherwise), andsys.pathis populated with/flash,/flash/lib,/sdcard,/sdcard/lib,/rom, and/rom/lib. The flash-resident setup is handled by a frozen module called_boot.py– port and board infrastructure, not an application hook. Applications do not customise_boot.py; the build does. Dropping aSKIPSDfile onto flash from the IDE is the supported way to make the cam boot from internal flash instead of the SD card.Pre-REPL setup.
boot.pyruns on every soft reset – cold boot,Ctrl-Dfrom the REPL, the running script returning, and watchdog recovery – before the REPL becomes reachable. Its job is to prepare the environment the rest of the system runs in: the kind of setup the REPL, the application, and any recovery tooling all need in place to function. It is not where the application itself lives.main.pyis the application’s entry point.Main loop.
main.pyis the application’s main loop. Runs once on cold boot, immediately afterboot.py. Not re-run on subsequent soft resets – the cam drops to the REPL instead. That asymmetry matters for development (a Ctrl-D drops to the REPL without re-running the loop, so the developer can inspect state) but not for production: a fielded cam sees power-on, watchdog, and hard resets, which are all hardware resets that re-enter the cold-boot path and runmain.pyagain.
16.2.2.1.2. Freezing into the firmware¶
A board’s frozen-module set is declared in
boards/<TARGET>/manifest.py in the firmware tree. The
manifest is a small Python file that calls a handful of
directives:
freeze("$(OMV_LIB_DIR)/", "foo.py")– bakes a singlefoo.pyinto the build.package("mylib", base_path="...")– bakes a multi-file Python package, preserving its directory layout under the given base path.include("...")– pulls in another manifest file. The board manifests use this to share common module sets.require("logging")– pulls in a named upstreammicropython-libmodule by name.
A minimal application manifest adds one freeze line per
top-level script and a package line per package the
application depends on.
16.2.2.1.2.1. Where the source lives¶
Application source lives under scripts/libraries/ in
the firmware tree, alongside the modules the build already
freezes. The manifest variable $(OMV_LIB_DIR) expands to
that path, so manifest entries stay short. Editing the
manifest is already an in-tree operation, so keeping the
source in-tree avoids juggling a separate project repo on
the path resolution.
A typical layout for an application that ships a single
main.py plus a supporting package:
scripts/libraries/
main.py
my_lib/
__init__.py
helpers.py
And in the board’s boards/<TARGET>/manifest.py, one
freeze line for the script and one package line for
the package:
freeze("$(OMV_LIB_DIR)/", "main.py")
package("my_lib", base_path="$(OMV_LIB_DIR)/my_lib")
Single-file scripts – main.py here, but the same rule
applies to boot.py or any standalone helper – use
freeze. Multi-file packages use package. Adding
another script is one more freeze line; adding another
package is one more package line.
16.2.2.1.2.2. Building and flashing¶
Once the manifest is in place, build the firmware exactly as the firmware chapter describes:
make -j$(nproc) -C lib/micropython/mpy-cross # once, builds the cross-compiler
make -j$(nproc) TARGET=<TARGET> # builds the firmware
The output lands in build/<TARGET>/bin/:
build/<TARGET>/bin/
firmware.bin # flash through the IDE
romfs0.img # flash through the IDE in a separate step
Flashing the .bin and .img through the IDE lands a
cam whose application is part of the build.
The startup sequence above is what makes the bake-in
effective: the runtime resolves boot.py and main.py to
the frozen copies before it ever checks the filesystem, so a
shipped cam runs the build’s code even if the SD card holds a
stale boot.py left from development.
16.2.2.1.2.3. Lookup order¶
The override semantics are different for the boot.py /
main.py execution path and for ordinary import
statements. Knowing which is which matters for both production
and development:
For
boot.pyandmain.py: the runtime looks for a frozen copy first, then the filesystem. A frozenboot.pycannot be overridden by dropping one onto the SD card – whoever holds the cam cannot change the entry point without reflashing.For
import foo: the runtime searchessys.pathfirst – which covers/flash,/sdcard,/rom, and theirlibsubdirectories – then frozen modules. A same-namefoo.pyon flash or SD does override a frozenfoo. This is the development affordance: drop a fixed module on the card, soft-reset, see the change without reflashing.
A shipped product that wants to suppress the
filesystem-overrides-frozen behaviour for imports can clear
sys.path early in boot.py:
import sys
sys.path.clear()
With sys.path empty, all imports resolve from the frozen
modules only; nothing on flash, SD, or ROMFS can shadow
them.
16.2.2.1.2.4. The asset problem¶
Freezing is great for code. It is not great for large binary assets: machine-learning model files, label tables, JSON configuration, image templates. Embedding those as Python literals balloons the source, recompiles slowly, and wastes the bytecode container on data the interpreter is just going to read raw anyway. The Building a ROMFS image page covers the read-only flash filesystem that fills this gap.