rpi-loader-ota 0.3.1

Over-the-air update bundle format for bare-metal Raspberry Pi firmware — one container, packed on a host and validated on the device
Documentation

rpi-loader-ota

CI crates.io

The over-the-air update bundle a bare-metal Raspberry Pi installs on itself: one container, packed on a host and validated on the device.

A bundle is a list of (path, role, bytes) under one checksum, so a board can reject a damaged transfer before it writes anything to its card. Each entry says where it lands, so an update can replace anything on a boot partition: a kernel, a website, a settings file, a certificate, config.txt, the Raspberry Pi firmware itself — or any subset of those. A bundle carrying no kernel is how a website is updated without rewriting an image that has not changed.

This is no_std, and it is not Pi-specific beyond the shape of the problem — anything that boots a raw image from a FAT partition and can receive a few megabytes over some transport can use it.

Why a crate

Because the encoder and the decoder are the same code. A wire format described in two places is a format that has already drifted: the two implementations this one was collected from agreed on every byte and disagreed on what they enforced, each accepting bundles the other rejected.

The host end is the rpi-loader CLI, which packs bundles and can upload one to a running board. The device end is this crate, linked into the firmware.

The container

Version 2 of the format, little-endian throughout:

magic   : 4 bytes             per-application; see `Format`
version : u8      = 2
reserved: u8      = 0
count   : u16                 number of entries
entries : count x {
            role    : u8      file, kernel, firmware or config
            path_len: u8
            path    : [u8; path_len]
            size    : u32
            data    : [u8; size]
          }
crc32   : u32                 IEEE CRC-32 over every preceding byte

The magic is the application's own, and is what stops a bundle built for one board installing on another — nothing else in the container says which board it belongs to.

Bundle::parse allocates nothing. It validates in place and borrows, so a device holds one buffer — the upload as it arrived — and nothing else. The checksum is verified before any entry is walked, because a corrupt transfer produces plausible-looking lengths and parsing those first yields a misleading error.

The same rules run in both directions, so a packer cannot build what a device would reject: no absolute paths, no .. or empty components, no backslashes, no duplicate destinations, at most one kernel and one config.txt, and a start*.elf only alongside its matching fixup*.dat — the two are released together and a mismatched pair does not boot.

Roles, and the order they install in

A role says what an entry is, and the installer decides what that means. Naming the boot image by role rather than by filename or position is what lets a device choose its own destination.

Role What it is
File anything the application owns
Kernel the boot image
Firmware bootcode.bin, start*.elf, fixup*.dat
Config config.txt

apply writes them in role order rather than packed order, and the crate imposes it rather than trusting the bundle: files, then firmware, then config.txt, then the kernel. The kernel is last because with one boot image that write is the commit — everything that could fail has to have failed already. config.txt precedes it because losing that file falls back to loading kernel7.img, so it cannot on its own stop a board booting.

One entry has no safety net, and it is worth knowing which: bootcode.bin is loaded by the ROM by name, so there is no slot to write it to and no config line to point elsewhere. A failed write there means the card comes out. It is ~50 KB, it changes almost never, and on the Pi 4 it is in SPI EEPROM and not on the card at all.

Packing one

The host end is the rpi-loader CLI, which reads a bundle.toml describing what the card holds and emits the container above. See that crate for the manifest format; it is not repeated here, because one description of a format in two places is the thing this crate exists to prevent.

Features

Feature Pulls in
The container: encode, decode, checksum alloc (default) nothing
Installing one onto a FAT volume apply resident-fat

The split keeps the host CLI from depending on a filesystem implementation in order to write a file on a host, and keeps a device that only installs bundles from compiling an encoder it never calls.

What your application still supplies

The transport and the reboot. How a bundle arrives — an HTTP route, a job handed to another core, a command over serial — is application shaped, and a crate that took it would be choosing your web framework.

The measurement. What an update costs the card is usually the number worth having, and collecting it here would mean depending on an async runtime for a clock and on whatever is counting device commands. The install reports its progress; the caller decides what to time.

License

MIT OR Apache-2.0, at your option.