Documentation Architecture

Firmware

Embedded software, device logic, peripheral drivers, and the firmware update mechanism.

Stack

The firmware is written in MicroPython and runs on the Pico 2 W. It is a single main.py entry point plus a lib/ directory of modules. Firmware is pushed to the device with mpremote fs cp - no compile step in the current phase.

Boot paths

The firmware has two distinct boot paths. Which one runs is decided by whether /config.json on flash contains Wi-Fi credentials.

Unprovisioned boot

main.py → provisioning.enter_provisioning_mode() immediately
        → no OLED / I2C init until after provisioning
        → reboot after successful provisioning

Hardware init is deliberately skipped on first boot. I²C initialisation delayed the serial listener, which caused the PWA to time out waiting for the ready beacon. Unprovisioned devices enter the provisioning loop immediately, before any peripheral is touched.

Provisioned boot

main.py → KnownHardware() → Wi-Fi → DNS :53 → HTTP :8080 → main loop

Modules

Module Purpose
provisioning.pyUSB WebSerial JSON loop: ready beacon, identify, challenge, scan, router_info, provision
ecdsa.pyPure-MicroPython ECDSA P-256 signing with DER encoding. ~150 lines, ~3s per signature on RP2350
otp_keys.pyDevice key storage. OTP primary via custom C module, flash file fallback. Atomic burn with read-back verify
dns_monitor.pyUDP 53 server. Forwards to 1.1.1.1, logs queries, classifies flagged-domain / new-connection / normal
http_server.pyHTTP API on port 8080. CORS *. Routes: health, stats, devices, audit, debug, token, allowlist, device rename
devices.pyDeviceTracker. Tracks devices by source IP
names_store.pyPersistent device names at /names.json. Max 32 characters per name
dns_diag.pyNetwork diagnostic tool. T1–T4 tests for DNS issues
ups.pyPico UPS B + INA219 battery monitor. Voltage, current, power, charge percentage
ssd1306.pyOLED display driver

DNS monitoring

Heron listens on UDP 53 and forwards every query transparently to 1.1.1.1 / 8.8.8.8. Queries are logged with source IP, domain, and timestamp. The classifier in dns_monitor.py tags each query as one of:

The flagged field is true for both flagged-domain and new-connection classes. The log is cached to /dnslog.json on flash and is clearable from the dashboard.

Key storage

Device keys live in the RP2350's one-time-programmable memory. The 254-byte payload uses a fixed layout:

Field Offset Length Notes
Private key032ECDSA P-256, raw big-endian
Public key3265Uncompressed point (0x04 ‖ X ‖ Y)
Serial9788-byte hex identifier
Certificate105148DER-encoded, null-padded, trimmed on read
Magic byte25310x01 = keys committed

If these offsets ever move, every burned device is bricked. The test suite locks them.

Burn is atomic. Data fields are written first, read back and verified, then the magic byte is written last. If any byte does not match, the magic is never set and the device can be re-attempted - OTP bits only go 0→1, so retrying a partial write sets the same bits again.

ECDSA signing

The signer is pure Python - no C extension. It uses the Pico's true random number generator for per-signature k, does point multiplication via double-and-add on the Cortex-M33, and applies low-S normalisation for canonical signatures. Signing takes roughly three seconds on the RP2350. This is a one-time operation during provisioning, so the latency is acceptable and avoids a custom MicroPython build with a C crypto module.

Firmware updates

Updates are local by design. There is no cloud, no server, no auto-download. A signed .heronpkg bundle - a single file the user can copy by email, USB stick, or any channel - is applied to a plugged-in device.

The bundle carries a monotonic version number, a minimum-source-version gate, and per-file SHA-256 hashes. The signature is verified against a dedicated update public key burned into the device at manufacturing - independent from the attestation root key, so each can rotate without dragging the other. Files are staged to .new paths, verified on device, then renamed atomically. A power cut during the apply leaves the old firmware intact.

Phase 2 secure boot - signed UF2s, frozen modules, SECURE_BOOT_ENABLE on production devices - is deferred until volume production. The current updater verifies integrity at apply time, not boot time.

Hard rules