<!-- Source: https://medius.k4tech.net/native/commands/update -->
# Update

_Replace either chip's firmware over this port_

[`UPDATE`](/native/commands/update.md#update) writes new firmware to either chip while the box is running: a [session](/native/commands/update.md#begin) per chip, the image in [chunks](/native/commands/update.md#data), and one [commit](/native/commands/update.md#activate) 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.

| 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, 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](/native/flashing.md).

## 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](/native/frame.md#opcodes) `0x17`.

```text
UPDATE 0x17 · payload 2..508 bytes
```

_Returns UPDATE_RESP_

#### PAYLOAD

| 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 |

#### 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`](/native/commands/update.md#activate) commits them together. Read what each chip is running with [`QUERY(FIRMWARE)`](/native/commands/requests.md#firmware). Each op below carries its own example. Library binding: [`update_firmware`](/library/update.md#update-firmware).

## BEGIN

_Erase the spare slot and open a session_

```text
UPDATE op 0 · [target u8][size u32][sha256 u8[32]]
```

#### PAYLOAD

| Offset | Field | Type | Notes |
| --- | --- | --- | --- |
| 2 | `size` | `u32` | total image bytes, little-endian; over `983040` is refused with `TOOBIG` |
| 6 | `sha256` | `u8[32]` | digest of the whole image, checked at `END` |

#### EFFECT

Puts the whole box in update mode: injection and [clip](/native/commands/clip.md) 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`](/library/update.md#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_

```text
UPDATE op 1 · [target u8][seq u16][bytes 1..504]
```

#### PAYLOAD

| Offset | Field | Type | Notes |
| --- | --- | --- | --- |
| 2 | `seq` | `u16` | chunk index, little-endian; the byte offset is `seq * 504` |
| 4.. | `bytes` | `u8[]` | 1 to 504 image bytes |

504 is what the [frame](/native/frame.md) 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
```

| Quantity | Value |
| --- | --- |
| Flash page write | 0.3 to 0.7 ms, both cores stalled. |
| UART0 RX FIFO | 128 bytes, which is 320 us at 4 Mbaud. |
| Credit window | 16 chunks to the device chip (8064 bytes), 6 to the host chip. |
| Inter-chip link ring | 4096 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`](/library/update.md#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_

```text
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`](/library/update.md#stage-firmware).

> **Note**
>
> 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`](/native/commands/update.md#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_

```text
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`](/library/update.md#abort-update).

#### DURING AN ACTIVATE

Once [`ACTIVATE`](/native/commands/update.md#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.

> **Warning**
>
> 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_

```text
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`](/library/update.md#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.

> **Warning**
>
> 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`](/native/commands/update.md#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`](/native/commands/update.md#update) frame, and one per acknowledged window of [`DATA`](/native/commands/update.md#data). [Opcode](/native/frame.md#opcodes) `0x18`.

```text
UPDATE_RESP 0x18 · payload 7 bytes
```

_Reply_

#### PAYLOAD

| Offset | Field | Type | Notes |
| --- | --- | --- | --- |
| 0 | `op` | `u8` | echoes the op being answered |
| 1 | `target` | `u8` | echoes the target; `ACTIVATE` always answers `0` |
| 2 | `status` | `u8` | below |
| 3 | `arg` | `u32` | per status, little-endian |

> **Note**
>
> `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

| Status | Value | `arg` | Meaning |
| --- | --- | --- | --- |
| `OK` | `0x00` | `0` | Accepted, nothing else to say. |
| `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; the box is on the 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 is not what the digest promised. |
| `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 already staged is still staged and still bootable. |

Every refusal reaches the library as [`Error::Update`](/library/types/errors.md).

#### 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)`](/native/commands/requests.md#firmware).

```
  boot new slot --> pending-verify --> chip confirms itself --> valid
                          |
                          +-- panics, or never confirms --> bootloader picks the old slot
```

| Chip | What confirms it | Grace |
| --- | --- | --- |
| Device | Ten seconds of a running main loop. | 30 s |
| Host | A 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.

> **Warning**
>
> 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`.
