Replies: 11 comments 3 replies
|
Thank you for the detailed proposal. This fits the direction of the project, and Wi-Fi OTA was already on the roadmap, so most of what you describe is additive rather than a rework. I would be glad to take you up on the offer to prototype it. Below I have tried to settle the two open decisions you raised so there is a clear boundary to build against. DirectionI agree with the overall shape: opt-in, administrator-triggered updates; a one-time USB migration to an OTA-capable partition table; direct-to-slot streaming with board, version, digest, and signature verification; and ESP-IDF rollback on failed boot. The existing pieces (firmware version in the heartbeat, the update-available check against Download origin and server boundaryI would keep your preferred model:
Cloudflare R2 stays exactly where it is. The only addition on the store side is an app-only OTA image alongside the current factory image, plus the metadata. Trust modelI would treat signing as required rather than optional. HTTPS and a digest cover the transport, but a signature is what protects against a compromised origin or tampered metadata. The plan I would fix on:
Nothing would ship over OTA without this in place. Scope: generic server, per-board enablementThe server side (the pending-update record, the signed-metadata endpoint, the device-card state machine) is board-agnostic, so I would build it generically and enable OTA per board only after that board's flash size and rollback path have been verified on hardware. E1004 would be the first verified target. That gives both the E1004-focused hardware verification and the generic framework, in the layers where each belongs. Partition migrationI agree with the one-time USB migration rather than rewriting a live partition table over the air. The one item to confirm first is the actual flash chip on real E1004 hardware; the 1.27 MB image against an 8 MB target leaves room for two application slots, but the chip itself should be confirmed before the feature is enabled for that board. State reportingThe device can report its OTA state and progress in the normal status heartbeat, and the command descriptor can ride the existing Suggested phasing
Your safety constraints (opt-in by default, kind-matched images only, battery gating with a preference for external power, NVS preservation, and keeping the Web Flasher and factory image as the recovery path) are exactly right, and I would build all of them in from the start. Next stepThe release pipeline and signing side depend on access I hold (the R2 account, the CI secrets, and the release private key), so I would keep that in-house: the app-only image, the signed-metadata endpoint, and generating and holding the keypair. I would publish the verification public key for the firmware to embed. The firmware side is the natural fit for you, and it is where most of the engineering sits: the OTA-capable partition layout, the device OTA client, on-device image and signature verification against the embedded public key, and the rollback path. Concretely, I would suggest:
I can start on Phase 1 so there is real signed metadata for you to verify against early. Does the firmware side suit you? |
|
Thank you, the split works for me, and the firmware boundary is yours. The pipeline, signing keys, and R2 hosting will stay on my side. I will begin Phase 1 tomorrow so that real signed metadata and the published verification key are available for you to build against as early as possible. I am happy with all three points, with one adjustment on where the descriptor rides. 1. Signed payload shapeAgreed as proposed. The response will carry:
Verification is over the raw decoded bytes before parsing, so neither side reproduces canonical JSON. The decoded manifest will contain:
Monocypher pinned to RFC 8032 plus project-specific vectors is a sound choice given the mbedTLS PureEdDSA gap. With Phase 1 I will publish the verification key and a set of signed fixtures (valid, wrong key, truncated, digest mismatch) so you can build and test against them before the live pipeline exists. 2. OTA descriptor placementMy preference is to place the descriptor on the always-200 /status response rather than /frame, and I would like that to be the contract. The reason is fleet coverage. /frame is the image channel, and every device kind polls it expecting either image bytes or a bodyless 304/204. The E1004 firmware would correctly read a 200 JSON envelope there, but the Raspberry Pi, Pico, and CircuitPython clients already in the field poll the same endpoint and would attempt to decode the JSON as an image. /status is already an always-200 JSON channel that every client tolerates, so an additional ota object is ignored by clients that do not understand it and acted upon by those that do. To bound this cleanly I would gate delivery on a capability flag: firmware advertises OTA support at register/status, declaring the descriptor schema version it speaks, and the server emits a descriptor only to a device that has advertised support. This keeps /frame unchanged, makes OTA opt-in per device kind, and lets me add non-ESP32 updaters later without renegotiating the transport. The existing 304 and 204 behaviour on /frame stays exactly as it is today. Descriptor shape on /status:
3. Rollback confirmationAgreed, and local checks only. The new app is marked valid once the running partition is correct, NVS is readable, and Wi-Fi initialisation and association succeed; the later heartbeat reports completed as telemetry rather than as the acceptance gate. Requiring a successful server heartbeat before marking the app valid could roll back healthy firmware during an unrelated server outage. If we later decide server reachability should be part of the gate, we will define it as a bounded-retry option, but the default stays local. Pipeline-side planI will keep Phase 1 to:
I will start tomorrow and post here once the verification key and fixtures are published, so you can build the firmware against them while the live pipeline comes together. |
|
Phase 1 groundwork is on
Test verification key (test only; the production key is published separately): The signed-bytes format above is frozen, so you can build the verifier and the RFC 8032 plus project-specific vectors against these fixtures now. Still to come as the next slice: the Separately, a heads-up on the firmware tree so we are not both in it at once: I am going to roll #123 (a short stay-awake window after a button press, so rapid next/previous presses do not each pay a fresh wake and Wi-Fi cycle) into the firmware myself today, ahead of the OTA phases. I will flag here once it is in. |
|
The Capability field (the TBD from earlier): a device advertises OTA support with an { "...heartbeat fields...": "...", "ota": { "schema": 1 } }Delivery: the server hands back the signed descriptor on the always-200 { "...status fields...": "...", "ota": { "payload": "...", "signature": "..." } }
Staging (server side, for testing): sign then stage, held in python -m app.ota.sign --key-id test-ed25519-1 --device-kind esp32_client \
--fw-version 1.4.0 --image app.bin --image-url https://host/app.bin \
--key tests/fixtures/ota/test_signing_key.hex > d.json
python -m app.ota.stage --data-root data --device-id <id> --descriptor d.jsonThat uses the published test key, so a firmware build carrying the test public key can verify a real descriptor delivered over the wire, with any reachable Still on my side: the production key, R2 image hosting, and OTA state reporting. None of those block you building and verifying the descriptor path against the test key now. |
|
Production signing is live and verified end to end. Signing + hosting. A Cloudflare Worker signs descriptors with an Ed25519 key held only in a Worker secret and serves the images from R2, co-located so the image it signs is the image it hosts. Its output is byte-identical to the Python signer, so a descriptor from either path verifies the same way. I confirmed the full chain against the real key: a Production key to embed. key_id Please have the firmware trust a keyed set One heads-up on the image fetch. Cloudflare's edge returned 403 to a bare/automated user-agent (a The |
|
Server-side rollout is wired now (edge v0.157.0), so the full path from a build to a device exists on my end: sign (Worker, prod-1 key) → host on R2 → stage, and now a deliberate per-kind rollout on top. An operator sets a signed build as a kind's release, offers it to a canary device or two first, then promotes it to the rest, with pause and a firmware-version gate so an already-updated device isn't re-offered. Nothing about the wire contract changed. Delivery is still: the device advertises One thing that turned out to be a non-issue: per-board targeting. Each hardware-catalog board ( So the outstanding piece is the device side you own: advertise the capability, read the descriptor from |
|
A quick firmware-side progress update: I have completed the capacity-independent groundwork locally, starting from the current firmware Implemented
Safety boundary
Verification completed
I am away from the E1004 at the moment, so the remaining hardware-dependent gate is still intentionally open. Once I am back, I will read the real flash ID/capacity over USB without erasing anything, then define the A/B layout. After that the next slice is direct R2 streaming with a normal User-Agent, inactive-slot writes, exact size/SHA-256 checks, boot validation/rollback, and OTA state telemetry. We can then use your prod-signed canary path for the first end-to-end run. I will continue from there when I am back later and post another progress update here. |
|
The verify-only path lines up with the frozen contract, so this is the right shape for the phase. A few confirmations, and a couple of things for when you're back at the E1004. Contract alignmentEverything here matches
Nothing on my side has moved since v0.156.0, so what you built against is current. Safety postureAgreed, keep it exactly as is: Firmware tree / #123You're clear here: #123 (post-button stay-awake) is on The R2 download (streaming slice)You've got the normal-User-Agent note. One extra data point: the release CI now probes RollbackConfirmed, local checks only: running partition correct, NVS readable, Wi-Fi init and association, then mark valid; the heartbeat's Canary, whenever you're readyMy end of the chain is live: Worker signing with The test matrix (RFC 8032 vector, the four fixtures, and the malformed/edge cases) is the coverage I wanted before pointing this at hardware, so I'm comfortable going straight to a canary once the apply slice lands. |
|
The E1004 firmware apply path and the one-time hardware migration are now ready for a real canary. Hardware result
I backed up the partition table, NVS, and the full used flash range before writing anything. The one-time USB migration then succeeded without erase-all. A semantic NVS readback showed all 59 pre-existing keys unchanged, including Wi-Fi credentials, Tesserae server identity, token, and device id. The only additions were the normal Wi-Fi bssid and channel cache entries created after reconnecting. The migrated device booted from ota_0, rejoined Wi-Fi, retained its registration, received a frame 304, posted battery and SHT4x telemetry successfully, and returned to its server-driven deep sleep interval. Firmware apply pathThe implementation is pushed here: 25711cc. It now includes:
The RFC 8032 and published fixture tests pass, and a clean E1004 build succeeds at about 1.24 MB. I also rebuilt and flashed the final capability-enabled baseline to the migrated unit; it is online and posting status successfully. Ready for canaryThe remaining hardware work is the real Wi-Fi transition from ota_0 to ota_1, followed by interrupted download, bad size/digest, and failed-first-boot rollback tests. I have deliberately not invented an OTA telemetry request shape beyond the frozen descriptor contract; the firmware currently emits the stable reason codes in its serial logs, and I can add heartbeat state fields once that exact server-side shape is published. The device side is ready for a single prod-1-signed E1004 canary whenever convenient. I will keep USB serial attached so we can capture the download, slot switch, first-boot confirmation, and any edge/WAF behavior end to end. |
|
OTA status: validated end to end, now fleet-wide. Rolling today's updates into one. Server side (edge 0.177.0 -> 0.180.0)
Release + a bug caught
End-to-end hardware validation
Fleet-wide (1.6.4)
Remaining caveatThe device fetches the image from R2 directly, and Cloudflare's bot rules 403'd bare / automated user-agents on I've cleaned up my earlier scattered comments from today into this single update. Apologies for the notification spam. |
|
I have now exercised the new Firmware page on the real E1004 and found one small state-model/UI issue after a successful update. Reproduction
The row then showed:
This survives a page reload, so it is not browser caching. The persisted state explains it: There is no repeated-update risk: Root causeIn update_available = bool(available) and (not fw or is_newer(str(available), fw))
queued = bool(v["is_canary"])The template checks I do not think the heartbeat path needs to delete the device from Suggested fixDerive the effective queue from the imported release version (the version the server can really offer), rather than from membership alone or from the possibly newer online fw = str(v["fw_version"] or "")
update_available = bool(available) and (not fw or is_newer(str(available), fw))
release_pending = bool(rel_fw) and (
not fw or is_newer(str(rel_fw), fw)
)
queued = bool(v["is_canary"] and release_pending)
rows.append(
{
**v,
# ...
"queued": queued,
"queued_fw": rel_fw if queued else None,
"available_fw": available,
# ...
"can_queue": bool(
v["capable"] and update_available and not queued and offerable
),
"can_withdraw": queued,
}
)Then render the actual queued release: {% elif d.queued %}
<span class="pill is-accent">
queued{% if d.queued_fw %} {{ fw(d.queued_fw) }}{% endif %}
</span>This should give the desired outcomes:
Suggested regression coverage would assert those four view-model cases, plus that the underlying canary membership remains available to the Fleet rollout model. The actual E1004 update itself was clean; this is just the post-update admin-page state. Happy to put this into a small PR if that would be useful. |
Uh oh!
There was an error while loading. Please reload this page.
Motivation
Would you be open to adding opt-in Wi-Fi firmware updates for Tesserae ESP32 panels?
For wall-mounted panels such as the reTerminal E1004, every firmware update currently requires physically connecting USB. That is manageable during development, but becomes inconvenient once a panel is installed in its normal location.
The project already has much of the supporting infrastructure:
The missing pieces are an OTA-capable partition layout, an explicit update command, image verification, and safe rollback.
Suggested shape
One final USB migration
Existing firmware uses one factory application partition with no OTA slots. I would not suggest rewriting that partition table remotely.
Instead, an OTA migration release could be flashed over USB once. It would install an OTA-capable partition table with two application slots. All later releases could then be installed over Wi-Fi.
The current E1004 v1.4.0 merged factory image is around 1.27 MB, while the board configuration conservatively targets devices with at least 8 MB of flash, so two suitably sized application slots appear practical. The real flash size should still be confirmed on hardware before enabling the feature for that target.
Explicit update from the device card
I think this should be user-triggered rather than silent automatic updating:
The UI could report states such as pending, downloading, verifying, rebooting, completed, and rolled back.
Safety constraints
Release pipeline
The current release pipeline publishes a complete factory image for WebSerial flashing. OTA would additionally need an app-only image plus version, board kind, size, SHA-256, and signature metadata. The factory artifact should remain unchanged for first installation and recovery.
Architecture question
The OTA image could either be downloaded directly from the existing public firmware store, with Tesserae only returning signed metadata, or proxied/cached by the local Tesserae server for installations where panels do not have direct internet access.
My initial preference would be:
Would this fit the direction of the project? If so, would you prefer this to begin as an E1004-focused hardware-verified prototype, or as a generic ESP32 OTA framework enabled only for verified board targets?
I would be happy to help prototype it once the desired trust model and server/device boundary are agreed.
All reactions