Board v2 · Firmware in developmentDraft
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 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.
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: the channel the request came in on.
Some commands depend on the transport: HardConfigSet is accepted only over USB (see Configuration). Over the UART it is refused with ERROR_TRANSPORT_NOT_ALLOWED.
Framing
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 |
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: a seq and exactly one command in the body oneof. The device sends DeviceMessage: a response, telemetry, an event or a heartbeat.
- The host increments
seqfor each request.seq = 0is reserved. - Every
HostMessagegets exactly oneResponsewith the sameseq. error = ERROR_NONEmeans accepted (ACK); any other code means refused (NACK) — seeErrorCode.- 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). *Getcommands return their data in the response (info,soft_config,hard_config).- A
MoveLinearacross 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.
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 eachInstruction, in order. - Chunks must arrive in order;
offsetis 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.
Telemetry and events
Telemetry (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) 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 sendsHeartbeat) more often thantimeout_ms; otherwise the device decelerates on its own and sendsEVENT_JOG_TIMEOUT.timeout_msis capped bySoftConfig.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.
EStopis 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:
upan-cli -p sim status
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:
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-cliand 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.