Skip to content

Firmware updates over any link, resumable (ADR-0016) - #147

Open
longzhi wants to merge 4 commits into
mainfrom
claude/ota-transfer
Open

longzhi wants to merge 4 commits into
mainfrom
claude/ota-transfer

Conversation

@longzhi

@longzhi longzhi commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Third step of #138: the chunked, resumable transfer from ADR-0016, used first for firmware.

What changes

  • Firmware (firmware-rs/core/src/transfer.rs, ota.rs): xfer.begin/chunk/end/cancel. The box writes the OTA slot it isn't running from, erasing one 4 KB sector ahead of the writes, acks each chunk (≤ 3072 B) with the next offset it expects, keeps progress in memory so a repeated xfer.begin for the same image resumes, over any link. On xfer.end it reads the slot back, checks SHA-256, then selects the slot in otadata (state NEW, in the sector that keeps the running entry) and restarts. Rollback is ADR-0013's. New hello lines: TRANSFER 1, FIRMWARE ROLLED BACK.
  • Daemon (daemon/src/transfer.rs): window of 4 chunks; on a drop or a link move it waits up to 3 min and resumes with xfer.begin. POST /v1/device/firmware takes this path whenever the box reports TRANSFER 1 (cable too), the ROM flasher otherwise. Success = the box reports its build again without FIRMWARE ROLLED BACK. /v1/status adds device.transfer and device.rolled_back.
  • Line limit 1024 → 4608 (box and protocol crate), sealed LAN lines 8192; only chunks come near it, only to TRANSFER 1 firmware.
  • Apps (Mac and Linux): no new buttons, the existing Update / Flash from file… go through the daemon. After a failed update, the hold-K0-and-replug hint shows only for ROM-flashed firmware. No new strings.
  • Docs: docs/protocol.md "Firmware over the link", docs/app.md update flow, docs/relay-protocol.md.

Not in this PR (deviations from ADR-0016)

  • The on-screen UPDATING n% overlay and cancelling with a long K2. Follow-up.
  • Character packs over xfer.* (kind: character answers unsupported); they still use voice.*.

Checked

  • cargo test --workspace, clippy (no new warnings), swift build + SelfTest, check-localization.py, tools/build-firmware.sh (app 2,820,896 B).
  • Unit tests: slot selection and rollback detection in otadata, resume after a drop, bad image / CRC / SHA never touch otadata, a full chunk fits the line limit, daemon resumes where the box got to after a dropped link.
  • Simulator end to end over the local network (Noise session): the real 2.8 MB image went over in under 2 s and the box answered xfer.done after the SHA-256 check.

On a box

With dragon's go-ahead (App quit, this branch's daemon):

Path 2.8 MB image Result
USB (native port) 38 s, ~74 KB/s restarted into the other slot, done
Local network 115 s, ~25 KB/s done
relay-staging, daemon killed at 30% and restarted ~4 min, ~12 KB/s resumed at 31%, done
An image that resets before confirming itself bootloader went back, box says FIRMWARE ROLLED BACK, daemon reports the update failed

The rollback test found a bug, fixed in this branch: over USB the failing image prints its build before it confirms itself, so the daemon reported success. It now waits 8 s and reads the box's answer after that.

Too slow over the network: the ADR expected under a minute. To look at next, before merging or as a follow-up: the box reads the LAN in 256-byte pieces through a 16-deep channel drained once per main-loop pass, and erases 4 KB sectors one at a time (~690 erases for 2.8 MB, ~30 s on their own, which also caps USB); over the Relay the window of 4 chunks is small for its round trip.

The box is back on main's firmware with the previous App running.

Refs #138

🤖 Generated with Claude Code

longzhi and others added 2 commits October 13, 2026 17:45
xfer.begin/chunk/end/cancel over the VibeBuddy Protocol, so the same
transfer runs over USB, the local network and the Relay (inside the
Noise session off the cable). The box writes the OTA slot it isn't
running from, erasing one sector ahead of the writes, acknowledges each
chunk of up to 3072 bytes with the next offset it expects, and keeps
the progress in memory: an xfer.begin for the same image carries on
from there, over any way of carrying the link. On xfer.end it reads the
slot back and checks the SHA-256 before it writes otadata, then
restarts; the new image boots once and confirms itself, or the
bootloader goes back (ADR-0013).

ota.rs learns which slot is running, how to select the other one (in
the sector that keeps the running entry, so there is something to go
back to), and whether the last update was rolled back. The box answers
hello with TRANSFER 1, and FIRMWARE ROLLED BACK after a rollback.

Lines from a computer may now be 4608 bytes, so a chunk fits one; a
sealed line on the local network 8192.

Refs #138

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daemon sends the app image as xfer.chunk lines of 3072 bytes, four
ahead of the box's acknowledgements, to any box that reports TRANSFER 1,
on the cable as well as over the local network and the Relay. When the
link drops or moves it waits up to three minutes for it to come back and
asks again with xfer.begin; the box answers with where it got to. After
xfer.done it waits for the box to report its build again, and reports a
failure if the box says FIRMWARE ROLLED BACK. Older firmware keeps the
ROM flasher, and still needs the cable.

The protocol's line limit goes from 1024 to 4608 bytes so a chunk fits
one line; only chunks come near it, and only firmware that reports
TRANSFER 1 gets them.

/v1/status reports device.transfer and device.rolled_back. The Mac and
Linux apps keep their Update and Flash from file... buttons as they are;
after a failed update they show the hold-K0-and-replug advice only for
firmware that updates through the ROM.

Docs: Firmware over the link in docs/protocol.md, the update flow in
docs/app.md; resuming large writes is no longer undecided in
docs/relay-protocol.md.

Refs #138

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 13, 2026 10:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

longzhi and others added 2 commits October 13, 2026 18:29
On a box over USB, an image that failed before confirming itself had
already printed its build, so the daemon reported the update done while
the bootloader was going back. The daemon now waits 8 s after the new
build and reads the box's answer after that: a rollback restarts it
within about 2 s and says FIRMWARE ROLLED BACK. Over the network the
box is only reached once it has confirmed itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a box a 2.8 MB image took 38 s over USB, 115 s over the local network
and about 4 minutes through the Relay. The box erased one 4 KB sector
per chunk, about 690 erases; it read the network 256 bytes at a time,
each piece an event for the main loop; and four chunks in flight were
too few for the Relay's round trip.

The box now erases what the image needs before it answers xfer.begin,
one 64 KB block per main-loop pass, and only writes while chunks arrive.
That also keeps erases, which stop interrupts, away from bytes arriving
on the UART bridge. It reads the network 1 KB at a time. The daemon
keeps up to 16 chunks in flight and sends at most 40 a second, under
the Relay's 50 messages a second, and gives xfer.ready 60 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants