# USB protocol

> Binary protocol of the motion MCU over USB-C and UART — framing, CRC, request and response, events, telemetry and the simulator.

The motion MCU speaks one binary protocol to every host: a PC over USB-C, and the network module of the device over an internal UART. Messages are Protocol Buffers defined in `upan.proto`, wrapped in small frames with a version byte and a CRC. Every request gets exactly one response with the same sequence number; events and telemetry arrive on their own. Use this page if you write your own host or library. For a ready tool, use `upan-cli` or the Python library; for network clients, use the [Network API](/docs/network-api) instead.

> **Note:** The protocol is version 0 of the schema and the firmware is in development. Frame format and the rules below are fixed for protocol version 1 of the frame (`ver = 1`); messages may still gain fields. Every message, field and enum is listed in the [API reference](/docs/api).

## Transports

| Transport | Where | Reported as |
|---|---|---|
| USB CDC (virtual serial port) | USB-C on the device, to a PC | `TRANSPORT_USB` |
| UART | between the network module and the motion MCU, inside the device | `TRANSPORT_UART_MODULE` |

Both carry the same frames and messages, so a host behaves the same over a cable and over Wi-Fi. `InfoGet` returns [`Info.transport`](/docs/api#Transport): the channel the request came in on.

Some commands depend on the transport: [`HardConfigSet`](/docs/api#HardConfigSet) is accepted only over USB (see [Configuration](/docs/configuration)). Over the UART it is refused with `ERROR_TRANSPORT_NOT_ALLOWED`.

## Framing

```text
COBS( ver | protobuf | crc16 ) 0x00
```

| Part | Size | Content |
|---|---|---|
| `ver` | 1 byte | frame version, currently `1` |
| `protobuf` | up to 400 bytes | one `HostMessage` (host → device) or `DeviceMessage` (device → host) |
| `crc16` | 2 bytes | CRC over `ver` + `protobuf`, **little-endian** (low byte first) |
| delimiter | 1 byte | `0x00` after the COBS-encoded block |

COBS removes all zero bytes from the frame, so `0x00` marks the end of a frame and a receiver can resynchronise after any line error by waiting for the next zero.

### CRC

CRC-16/CCITT-FALSE:

| Parameter | Value |
|---|---|
| Polynomial | `0x1021` |
| Initial value | `0xFFFF` |
| Reflect input / output | no |
| Final XOR | none |
| Check value (`"123456789"`) | `0x29B1` |

```python
def crc16_ccitt_false(data: bytes) -> int:
    crc = 0xFFFF
    for b in data:
        crc ^= b << 8
        for _ in range(8):
            crc = ((crc << 1) ^ 0x1021) if crc & 0x8000 else (crc << 1)
            crc &= 0xFFFF
    return crc

assert crc16_ccitt_false(b"123456789") == 0x29B1
```

### Invalid frames

| Received | Device reaction |
|---|---|
| empty frame, bad COBS, wrong `ver`, CRC mismatch | dropped silently |
| valid frame, protobuf does not decode or the message body is empty | response `ERROR_MALFORMED`; `seq` is what could be read, otherwise 0 |
| unknown command for this firmware | response `ERROR_UNSUPPORTED` |

## Requests and responses

The host sends [`HostMessage`](/docs/api#HostMessage): a `seq` and exactly one command in the `body` oneof. The device sends [`DeviceMessage`](/docs/api#DeviceMessage): a response, telemetry, an event or a heartbeat.

- The host increments `seq` for each request. `seq = 0` is reserved.
- Every `HostMessage` gets exactly one [`Response`](/docs/api#Response) with the same `seq`.
- `error = ERROR_NONE` means accepted (ACK); any other code means refused (NACK) — see [`ErrorCode`](/docs/api#ErrorCode).
- A response to a motion command means the command was accepted, not that the motion is finished. Completion comes as an event (`EVENT_MOVE_DONE`, `EVENT_HOME_DONE`, `EVENT_PROGRAM_DONE`).
- `*Get` commands return their data in the response (`info`, `soft_config`, `hard_config`).
- A `MoveLinear` across a module stack is answered once every axis has confirmed it is ready (up to about 100 ms).

A refusal tells the host what to change:

| Field | Filled for |
|---|---|
| `limit_violation` | `ERROR_LIMIT_EXCEEDED`: which limit, the requested value and the nearest allowed value, in base units |
| `program_instruction_index` | program errors: which instruction |
| `axis` | multi-axis moves: which module of the stack refused (0 = Core) |

For a refused `MoveRel`, the allowed value is a **distance**, not a position.

### Commands

| Group | Messages |
|---|---|
| Motion | `MoveAbs`, `MoveRel`, `Jog`, `Stop`, `EStop`, `Home`, `SetPosition`, `MoveLinear` |
| Programs | `ProgramBegin`, `ProgramChunk`, `ProgramCommit`, `ProgramRun`, `ProgramPause`, `ProgramResume`, `ProgramAbort` |
| Configuration | `SoftConfigGet`, `SoftConfigSet`, `HardConfigGet`, `HardConfigSet` |
| I/O | `OutputSet` |
| Service | `InfoGet`, `TelemetryConfig`, `FaultClear`, `Heartbeat`, `BootloaderEnter` |

Values are integers in base units: µm, µm/s, µm/s² on a linear axis, millidegrees on a rotary axis. A velocity or acceleration of 0 means "use the SoftConfig default". See [Configuration](/docs/configuration#base-units).

### Program upload

A program is uploaded in pieces: `ProgramBegin` (instruction count and CRC-32) → `ProgramChunk`… → `ProgramCommit`.

- The CRC-32 is the zlib CRC-32 (`"123456789"` → `0xCBF43926`) over the canonical protobuf encoding of each [`Instruction`](/docs/api#Instruction), in order.
- Chunks must arrive in order; `offset` is the number of instructions already accepted.
- A chunk holds up to 16 instructions and must fit one frame (400 bytes of protobuf). A multi-axis step can take about 90 bytes, so split by size, not only by count.
- A program holds up to 256 instructions and 6 KB encoded.

The checks on Commit and Run are described in [Programs](/docs/programs#rules-checked-by-the-device).

## Telemetry and events

**Telemetry** ([`Telemetry`](/docs/api#Telemetry)) is sent periodically: state, homed flag, position, velocity, target, encoder position and following error, inputs and outputs, limit and e-stop inputs, motor current, StallGuard value, supply voltage, driver flags, and the current program instruction and repeat. Set the rate with `TelemetryConfig.rate_hz` (0 = off). The device caps it at 100 Hz, separately for each transport.

**Events** ([`Event`](/docs/api#Event)) are sent asynchronously: move, home or program done, program aborted, limit hit, stall, following error, fault, e-stop pressed or released, input changed, jog timeout, presence window open. An event carries the position at that moment, an error code for faults and aborts, a `detail` (for example an input number or an instruction index) and the stack axis it concerns.

| Message | Goes to |
|---|---|
| `Response` | only the transport the request came from |
| `Event` | all transports (USB and UART) |
| `Telemetry` | each transport at its own configured rate |

## Link safety

- **Jog** runs only while the host keeps it alive. The host repeats `Jog` (or sends `Heartbeat`) more often than `timeout_ms`; otherwise the device decelerates on its own and sends `EVENT_JOG_TIMEOUT`. `timeout_ms` is capped by `SoftConfig.jog_timeout_max_ms`.
- A **loaded program keeps running** when the connection drops. After reconnecting, read the state from telemetry.
- The hardware stop input works independently of the protocol. `EStop` is a software stop in addition to it.

## Simulator

The simulator is the real motion firmware core compiled for a PC with simulated hardware. It runs in real time and exchanges the same frames over standard input and output. `upan-cli` starts it with `--port sim`:

```powershell
upan-cli -p sim status
```

```powershell
upan-cli -p sim --sim-arg=--homed move-to 20mm -v 5mm/s
```

Each `upan-cli` call starts a fresh simulator; state is not kept between calls. Options passed with `--sim-arg`:

| Option | Simulates |
|---|---|
| `--homed` | start homed at position 0 |
| `--presence` | presence button pressed (allows `config set --hard`) |
| `--supply-mv 5000` | USB power only: motion is refused with `ERROR_NO_MOTOR_POWER` |
| `--lim-min-at N`, `--lim-max-at N` | a limit switch at position N (in steps) |
| `--estop` | e-stop pressed |

For a longer session, keep one simulator open from Python:

```python
from upan.client import Device, EV
from upan.transport import SimTransport
from upan.v1 import upan_pb2 as pb

with Device(SimTransport(["--homed"])) as dev:
    dev.request(move_abs=pb.MoveAbs(target=5000))  # 5 mm in µm
    print(dev.wait_event({EV.EVENT_MOVE_DONE}, timeout=5))
```

> **Note:** How the host library, `upan-cli` and the simulator are distributed to users is not final yet.

## Firmware update

`BootloaderEnter` exists in the schema, but the current firmware answers it with `ERROR_UNSUPPORTED`. Signed updates and how signatures are checked are not final yet.
