Medius - Native APIUpdate

Update

Replace either chip's firmware over this port

UPDATE writes new firmware to either chip while the box is running: a session per chip, the image in chunks, and one commit at the end. No ROM download mode, no BOOT button, no cable move.

   PC --CH343, framed 4 Mbaud--> DEVICE chip --> its own spare slot
                                     |
                                     +--UART1, 5 Mbaud--> HOST chip --> its own spare slot

The host chip has no serial port of its own and no wire to one: only GPIO1 and GPIO2 connect the two chips. Its image is relayed chunk for chunk and never buffered on the way.

SLOTS

Both chips carry two app slots and boot whichever the bootloader selects.

PartitionOffsetSizeHolds
nvs0x90000x6000Box name, options, and learned baselines.
phy_init0xF0000x1000PHY calibration.
ota_00x100000xF0000One app slot.
ota_10x1000000xF0000The other app slot.
otadata0x1F00000x2000Which slot boots, and its state.

otadata sits above the slots so nvs keeps its offset: the box name, the options and the learned baselines survive the one flash that installs this layout. A box that has never had it answers NOSLOT; see Flashing.

UPDATE

One firmware session on one chip

UPDATE carries five ops against one target chip. Every frame leads with the op and the chip it addresses. Opcode 0x17.

UPDATE 0x17 · payload 2..508 bytes

Returns UPDATE_RESP

PAYLOAD
OffsetFieldTypeNotes
0opu80=BEGIN 1=DATA 2=END 3=ABORT 4=ACTIVATE
1targetu80=device chip, 1=host chip; ignored by ACTIVATE
2..bodyvariesper op, below
ORDER
  BEGIN --> DATA --> DATA --> ... --> END --> (staged, inert)
    |                                          |
    +-- ABORT ---------------------------------+--> ACTIVATE --> reboot
EFFECT

A target can be staged, left alone, and activated later, and both chips may be staged before a single ACTIVATE commits them together. Read what each chip is running with QUERY(FIRMWARE). Each op below carries its own example. Library binding: update_firmware.

BEGIN

Erase the spare slot and open a session
UPDATE op 0 · [target u8][size u32][sha256 u8[32]]
PAYLOAD
OffsetFieldTypeNotes
2sizeu32total image bytes, little-endian; over 983040 is refused with TOOBIG
6sha256u8[32]digest of the whole image, checked at END
EFFECT

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 it erases the entire target slot before answering READY with the credit in arg. Library binding: stage_firmware.

The erase happens here, up front, and not lazily as bytes arrive. A 64 KB block erase disables the cache for tens of milliseconds, which no receive buffer on either wire can absorb mid-stream.

EXAMPLE

Open a 364784-byte device-chip image (size is 0x000590F0):

+--------+--------+--------+--------+--------+--------+-------------+----------+--------+
| A5     | 17     | 00     | 26 00  | 00     | 00     | F0 90 05 00 | 32 bytes | lo hi  |
+--------+--------+--------+--------+--------+--------+-------------+----------+--------+
| SOF    | TYPE   | SEQ    | LEN    | op     | target | size        | sha256   | CRC16  |
+--------+--------+--------+--------+--------+--------+-------------+----------+--------+

DATA

One chunk, in order
UPDATE op 1 · [target u8][seq u16][bytes 1..504]
PAYLOAD
OffsetFieldTypeNotes
2sequ16chunk index, little-endian; the byte offset is seq * 504
4..bytesu8[]1 to 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.

FLOW CONTROL

READY's arg says how many chunks the box will take before it must answer. Read it rather than assuming: the device chip asks for 16, the host chip for 6, because a relayed chunk waits in the inter-chip link's receive ring while the chip behind it writes the one before to flash. The window is a correctness requirement, not a throughput knob.

  PC   |-- credit chunks --|                       |-- credit chunks --|
  box                       |-- write, ACK next --|                     |-- ACK next --|
                            ^
                            cache is off here; nothing may be in flight
QuantityValue
Flash page write0.3 to 0.7 ms, both cores stalled.
UART0 RX FIFO128 bytes, which is 320 us at 4 Mbaud.
Credit window16 chunks to the device chip (8064 bytes), 6 to the host chip.
Inter-chip link ring4096 bytes, which is what caps the relayed window.

A sender that ignores the credit it was given overruns whichever hop is smaller, and the lost chunk leaves the window one short: nothing acknowledges it, and the session dies on the idle timer with the sender still waiting. Library binding: stage_firmware.

RESENDS

A chunk one behind the box's next expected seq is accepted and answered without being rewritten: that is what a lost acknowledgement looks like. Anything older is a SEQGAP and drops the session.

EXAMPLE

Chunk 3 of a device-chip image, a full 504 bytes of payload:

+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| A5     | 17     | 2A     | FC 01  | 01     | 00     | 03 00  | ...    | lo hi  |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| SOF    | TYPE   | SEQ    | LEN    | op     | target | seq    | bytes  | CRC16  |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+

END

Verify the digest and stop there
UPDATE op 2 · [target u8]
EFFECT

Checks the digest and the image header, and stops. It answers STAGED with the byte count, or BADSHA if the image is not what BEGIN promised. Library binding: stage_firmware.

A staged image is not bootable. END deliberately does not set the boot partition: that takes effect on the next boot of any kind, so an image staged and never activated would go live on the next power cut. Committing is ACTIVATE's job.

TIMEOUT

Ten seconds with no traffic ends update mode by itself, so a client that disappears cannot leave the clone down. A staged image survives that: it is inert, and discarding it would throw away a transfer that already completed.

EXAMPLE

Close the device-chip session:

+--------+--------+--------+--------+--------+--------+--------+
| A5     | 17     | 3C     | 02 00  | 02     | 00     | lo hi  |
+--------+--------+--------+--------+--------+--------+--------+
| SOF    | TYPE   | SEQ    | LEN    | op     | target | CRC16  |
+--------+--------+--------+--------+--------+--------+--------+

ABORT

Throw the transfer away
UPDATE op 3 · [target u8]
EFFECT

Drops whatever is staged or in flight for that target and answers OK. Leaving update mode does not reboot: the host chip re-sends its descriptor snapshot and the clone is rebuilt the same way a re-attach rebuilds it. Library binding: abort_update.

DURING AN ACTIVATE

Once ACTIVATE is accepted the box answers new commands with BUSY, and this is the one exception: while the box is waiting on the host chip it abandons that wait and answers the ACTIVATE with TIMEOUT. Past that point the boot partition is already written, so there is nothing left to abort, and 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; leaving one behind would let a later ACTIVATE commit that one alone and put the two chips on different versions. Outside an activate, ABORT is per-target as usual.

EXAMPLE

Discard whatever the host chip is holding:

+--------+--------+--------+--------+--------+--------+--------+
| A5     | 17     | 41     | 02 00  | 03     | 01     | lo hi  |
+--------+--------+--------+--------+--------+--------+--------+
| SOF    | TYPE   | SEQ    | LEN    | op     | target | CRC16  |
+--------+--------+--------+--------+--------+--------+--------+

ACTIVATE

Commit every staged image and boot it
UPDATE op 4 · [target u8] (ignored)
EFFECT

Points each staged chip at its new slot and reboots into it, host chip first. The device chip is the only transport, so it is the last one down. Library binding: activate_firmware.

  ACTIVATE
     |
     +--> host commits, reboots
     |         |
     |         +--> back on the link at the new slot
     |                    |
     +--------------------+--> device commits, replies OK, reboots

If only one target is staged, only that one is committed. Anything that fails before ACTIVATE leaves both chips on their running slots untouched, so a failed transfer costs a retry and nothing else. 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; it 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 leave the two chips on different versions. After a failed ACTIVATE, either retry the whole update or send ABORT for each staged target first. The dashboard and both reference clients do the latter for you.

EXAMPLE

Commit everything staged:

+--------+--------+--------+--------+--------+--------+--------+
| A5     | 17     | 55     | 02 00  | 04     | 00     | lo hi  |
+--------+--------+--------+--------+--------+--------+--------+
| SOF    | TYPE   | SEQ    | LEN    | op     | target | CRC16  |
+--------+--------+--------+--------+--------+--------+--------+

UPDATE_RESP

The answer to one op

One reply per UPDATE frame, and one per acknowledged window of DATA. Opcode 0x18.

UPDATE_RESP 0x18 · payload 7 bytes

Reply

PAYLOAD
OffsetFieldTypeNotes
0opu8echoes the op being answered
1targetu8echoes the target; ACTIVATE always answers 0
2statusu8below
3argu32per status, little-endian

SEQ echoes the command frame, except for DATA acknowledgements, which carry a rolling SEQ because one answers a whole window. Correlate on the op alone, not on SEQ.

STATUS
StatusValueargMeaning
OK0x000Accepted, nothing else to say.
READY0x01creditSlot erased; this many chunks before an acknowledgement.
ACK0x02next seqThe window landed; send from this index.
STAGED0x03bytes writtenVerified and inert until ACTIVATE.
BUSY0x100A session is already open on this target.
NOSLOT0x110No second app slot; the box is on the single-app layout.
TOOBIG0x12slot sizeThe declared size does not fit, or a chunk overran it.
SEQGAP0x13expected seqA chunk out of order; the session is dropped.
WRITEFAIL0x14error codeThe flash write failed.
BADSHA0x150The image is not what the digest promised.
BADIMAGE0x16error codeThe bytes are not a bootable image.
LINKDOWN0x170The host chip was addressed and the inter-chip link is down.
TIMEOUT0x180The host chip did not come back on the new image.
NOSTAGE0x190ACTIVATE with nothing staged.
BADSTATE0x1Aexpected opAn op out of order, or a malformed body.
PROBATION0x1B0The running image has not confirmed itself yet; wait and retry.
UNTOUCHED0x1C0Refused before the slot was touched, so anything already staged is still staged and still bootable.

Every refusal reaches the library as Error::Update.

EXAMPLE

READY for a device-chip BEGIN, credit 16:

+--------+--------+--------+--------+--------+--------+--------+-------------+--------+
| A5     | 18     | 00     | 07 00  | 00     | 00     | 01     | 10 00 00 00 | lo hi  |
+--------+--------+--------+--------+--------+--------+--------+-------------+--------+
| SOF    | TYPE   | SEQ    | LEN    | op     | target | status | arg         | CRC16  |
+--------+--------+--------+--------+--------+--------+--------+-------------+--------+

Rollback

An image that will not run does not stick

A freshly booted image is on probation until it proves itself, and the bootloader reverts one that never does. Which slot each chip booted, and the state of that image, is in QUERY(FIRMWARE).

  boot new slot --> pending-verify --> chip confirms itself --> valid
                          |
                          +-- panics, or never confirms --> bootloader picks the old slot
ChipWhat confirms itGrace
DeviceTen seconds of a running main loop.30 s
HostA completed clock exchange over the link.15 s

Measured on hardware: an image that panics in its entry point is back on the old slot in 0.8 s, and one that boots but never confirms is reverted by its own grace timer.

Rollback covers an image that will not run. It does not cover one that runs and is wrong. While a chip is on probation the box refuses to open another update and answers PROBATION.