Update
UPDATE writes new firmware to either chip while the box runs, over the control port with no BOOT button: a session per chip, the image in chunks, and one commit at the end.
PC --CH343, framed 6 Mbaud--> DEVICE chip --> its spare slot
|
+--UART1, 20 Mbaud-> HOST chip --> its spare slotThe host chip has no serial port. Only GPIO1 and GPIO2 connect the two chips. Its image is relayed chunk for chunk, never buffered.
Both chips carry two app slots and boot whichever the bootloader selects.
| Partition | Offset | Size | Holds |
|---|---|---|---|
nvs | 0x9000 | 0x6000 | Box name, options, and learned baselines. |
phy_init | 0xF000 | 0x1000 | PHY calibration. |
ota_0 | 0x10000 | 0xF0000 | One app slot. |
ota_1 | 0x100000 | 0xF0000 | The other app slot. |
otadata | 0x1F0000 | 0x2000 | Which slot boots, and its state. |
otadata sits above the slots, so nvs keeps its offset. The box name, options and learned baselines survive the one flash that installs this layout. A box without the layout replies NOSLOT.
UPDATEOne firmware session on one chip
UPDATE carries five ops against one target chip. Every frame leads with the op and the chip. Opcode 0x17.
UPDATE 0x17 · payload 2..508 bytes
Reply
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | op | u8 | 0=BEGIN 1=DATA 2=END 3=ABORT 4=ACTIVATE |
| 1 | target | u8 | 0=device chip, 1=host chip; ignored by ACTIVATE |
| 2.. | body | varies | per op, below |
BEGIN --> DATA --> DATA --> ... --> END --> (staged, inert)
| |
+-- ABORT ---------------------------------+--> ACTIVATE --> rebootA target can be staged and activated later. Both chips may be staged before one ACTIVATE commits them together. QUERY(FIRMWARE) reads what each chip runs. Library binding: update_firmware.
BEGINErase the spare slot and open a session
UPDATE op 0 · [target u8][size u32][sha256 u8[32]]
| Offset | Field | Type | Notes |
|---|---|---|---|
| 2 | size | u32 | total image bytes, little-endian; >983040 is refused with TOOBIG |
| 6 | sha256 | u8[32] | digest of the whole image, checked at END |
Puts the whole box in update mode: injection and clip playback stop, the host chip stops polling the real device, and the clone disconnects from the game PC.
Then the box erases the whole target slot and replies READY with the credit in arg. Library binding: stage_firmware.
The erase runs up front. A 64 KB block erase disables the cache for 50-100 ms, longer than any receive buffer on either link can absorb mid-stream.
Open a 364784-byte device-chip image (size is 0x000590F0):
DATAOne chunk, in order
UPDATE op 1 · [target u8][seq u16][bytes 1..504]
| Offset | Field | Type | Notes |
|---|---|---|---|
| 2 | seq | u16 | chunk index, little-endian; the byte offset is seq * 504 |
| 4.. | bytes | u8[] | 1-504 image bytes |
504 is what the frame has left: 512 less the op, the target and a two-byte index, rounded down to a multiple of four so every flash write is aligned.
READY's arg is how many chunks the box takes before it acknowledges.
Read it. The device chip asks for 16 and the host chip for 6, because a relayed chunk waits in the inter-chip link's receive ring while the host chip writes the previous chunk to flash.
The window is a correctness requirement.
PC |-- credit chunks --| |-- credit chunks --|
box |-- write, ACK next --| |-- ACK next --|
^
cache is off here; nothing may be in flight| Quantity | Value |
|---|---|
| Flash page write | 0.3-0.7 ms, both cores stalled. |
| UART0 RX FIFO | 128 bytes, 213 µs at 6 Mbaud. |
| Credit window | 16 chunks to the device chip (8064 bytes), 6 to the host chip. |
| Inter-chip link ring | 8192 bytes. Caps the relayed window. |
A sender ignoring its credit overruns the smaller hop. The lost chunk leaves the window one short, so nothing acknowledges the window. The session ends on the idle timer with the sender still waiting. Library binding: stage_firmware.
A chunk one behind the next expected seq (a lost acknowledgement) is acknowledged without being rewritten. Anything older is a SEQGAP and drops the session.
Chunk 3 of a device-chip image, a full 504 bytes of payload:
ENDVerify the digest and stop
UPDATE op 2 · [target u8]
Checks the digest and image header, then replies STAGED with the byte count, or BADSHA if the image isn't what BEGIN promised. Library binding: stage_firmware.
A staged image isn't bootable. END doesn't set the boot partition, since that would take effect on any next boot, a power cut included. ACTIVATE commits.
Ten seconds with no traffic ends update mode, so a vanished client can't leave the clone down. A staged image survives the timeout: it is inert, and its transfer already completed.
Close the device-chip session:
ABORTDiscard the transfer
UPDATE op 3 · [target u8]
Drops whatever is staged or in flight for that target and replies OK. Leaving update mode doesn't reboot: the host chip re-sends its descriptor snapshot and the clone is rebuilt as on a re-attach. Library binding: abort_update.
Once ACTIVATE is accepted, the box replies BUSY to new commands. The one exception is ABORT while the box waits on the host chip: the box abandons the wait and replies TIMEOUT to the ACTIVATE.
Past that wait, the boot partition is written, so nothing is left to abort. Cancelling the reboot would leave the box running one image with the loader pointed at the other.
Abandoning an activate disarms both chips, whichever target the frame names.
One ACTIVATE commits everything staged, so abandoning it abandons everything staged. A leftover image would let a later ACTIVATE commit it alone and put the chips on different versions. Outside an activate, ABORT is per-target.
Discard whatever the host chip is holding:
ACTIVATECommit every staged image and boot it
UPDATE op 4 · [target u8] (ignored)
Points each staged chip at its new slot and reboots into it, host chip first. The device chip is the only transport, so it goes last. Library binding: activate_firmware.
ACTIVATE
|
+--> host commits, reboots
| |
| +--> back on the link at the new slot
| |
+--------------------+--> device commits, replies OK, rebootsOnly staged targets are committed. A failure before ACTIVATE leaves both chips on their running slots, so it costs only a retry.
The reply is OK once both chips are committed, NOSTAGE if nothing was staged, or TIMEOUT if the host chip never came back on the new slot. The reply always names target 0, so match it on the op alone.
A refused commit leaves the image staged. The host chip goes first, so if it refuses, the device chip never commits.
Both chips are still on their running slots, but the device image stays staged and armed: the next ACTIVATE would commit it alone and put the chips on different versions.
After a failed ACTIVATE, either retry the whole update, or first send ABORT for each staged target (as the dashboard and both reference clients do).
Commit everything staged:
UPDATE_RESP Reply to one op
One reply per UPDATE frame, and one per acknowledged window of DATA. Opcode 0x18.
UPDATE_RESP 0x18 · payload 7 bytes
Reply
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | op | u8 | echoes the op |
| 1 | target | u8 | echoes the target; always 0 for ACTIVATE |
| 2 | status | u8 | below |
| 3 | arg | u32 | per status, little-endian |
SEQ echoes the command frame, except for DATA acknowledgements, which carry a rolling SEQ because one covers a whole window. Correlate on the op, not SEQ.
| Name | Value | arg | Meaning |
|---|---|---|---|
OK | 0x00 | 0 | Accepted. |
READY | 0x01 | credit | Slot erased. This many chunks before an acknowledgement. |
ACK | 0x02 | next seq | The window landed. Send from this index. |
STAGED | 0x03 | bytes written | Verified and inert until ACTIVATE. |
BUSY | 0x10 | 0 | A session is already open on this target. |
NOSLOT | 0x11 | 0 | No second app slot (single-app layout). |
TOOBIG | 0x12 | slot size | The declared size does not fit, or a chunk overran it. |
SEQGAP | 0x13 | expected seq | A chunk out of order. The session is dropped. |
WRITEFAIL | 0x14 | error code | The flash write failed. |
BADSHA | 0x15 | 0 | The image does not match the digest. |
BADIMAGE | 0x16 | error code | The bytes are not a bootable image. |
LINKDOWN | 0x17 | 0 | The host chip was addressed and the inter-chip link is down. |
TIMEOUT | 0x18 | 0 | The host chip did not come back on the new image. |
NOSTAGE | 0x19 | 0 | ACTIVATE with nothing staged. |
BADSTATE | 0x1A | expected op | An op out of order, or a malformed body. |
PROBATION | 0x1B | 0 | The running image has not confirmed itself yet. Wait and retry. |
UNTOUCHED | 0x1C | 0 | Refused before the slot was touched, so anything staged stays staged and bootable. |
Every refusal reaches the library as Error::Update.
READY for a device-chip BEGIN, credit 16:
RollbackReverts an image that won't run
A freshly booted image is on probation until it confirms itself. The bootloader reverts an image that never does. QUERY(FIRMWARE) reports each chip's booted slot and image state.
boot new slot --> pending-verify --> chip confirms itself --> valid
|
+-- panics, or never confirms --> bootloader picks the old slot| Chip | Confirmed by | Grace |
|---|---|---|
| Device | 10 s of a running main loop. | 30 s |
| Host | A completed clock exchange over the link, only. | 40 s |
The host chip's image arrives over the link, its only way back. A host image that runs but can't use the link must therefore revert by itself.
Measured on hardware: an image that panics in its entry point is back on the old slot in 0.8 s. An image built to ignore the link reverted and restored the link unaided.
Rollback covers an image that won't run, not one that runs wrongly. While a chip is on probation the box refuses another update with PROBATION.
Stable update path Fixed from protocol 5 on
A client that can update one box can update every later one. From protocol 5 (v3.2.0) on, no protocol changes these:
| Part | Where |
|---|---|
| Frame: SOF, header, CRC, the 512-byte payload limit | Frame layout |
| Control rate: 6 Mbaud, and 4 Mbaud on firmware before v3.4.0 | Serial link |
QUERY(VERSION), its reply's first ten data bytes (proto_ver, version, MAC) and the name after them, and the hello | VERSION, Ready hello |
QUERY(FIRMWARE) and the 17 bytes of its reply; anything new follows them | FIRMWARE |
UPDATE and UPDATE_RESP: ops, targets, bodies, the 504-byte chunk, the credit and how it is read, every status and its arg, update mode, staging and ACTIVATE | This page |
A status may be added. A client reads an unknown status as a refusal. The dashboard connects to any box on protocol 5 or later, and can update but not control a box on another protocol.