Skip to content

Serial protocols

Two sketches speak to a host, and they use different protocols: LowLevelServer takes commands over a binary protocol, while RLControl runs autonomously and only reports.

The Nano acts as a low-level server: the host reads state, decides an action, and sends it back. This is the transport used for real-rig fine-tuning and for any computer-side controller.

Commands are a single byte, some followed by a little-endian IEEE-754 float (4 bytes, AVR native order).

ByteCommandPayloadReply
0x01CMD_READY—echoes 0x01
0x02CMD_GET_STATE—20-byte state packet (below)
0x03CMD_SET_ACCELfloat angular acceleration, rad/s²—
0x04CMD_ENGAGE_MOTOR——
0x05CMD_DISENGAGE_MOTOR——
0x06CMD_TARE_PENDULUM—re-zeroes the pendulum angle to the current reading
0x07CMD_SET_TARGETfloat absolute motor position, rad—
0x08CMD_ZERO_MOTOR—re-zeroes the motor step counter to the arm’s current position; echoes 0x08

CMD_GET_STATE replies with five little-endian values, 20 bytes total:

OffsetTypeField
0uint32timestamp, microseconds
4floatmotor position, rad
8floatpendulum position, rad
12floatmotor velocity, rad/s
16floatpendulum velocity, rad/s

Two details matter if you are writing a client:

  • The timestamp is the sample time, not the reply time. It is taken from the buffer entry the positions came from, so (t, pos, vel) is self-consistent and can be time-aligned without inheriting up to a sample of bias.
  • Velocities are finite differences over a window, not instantaneous derivatives — (newest − oldest)/Δt across the firmware’s 8 ms sampling window. The simulation reproduces this exactly, including the quantisation, because the resulting noise is something the policy trains against.
  • One frame everywhere: values are the firmware’s own step counter and encoder accumulator, unmodified — the same frame the policies are trained in. Neither side of the wire flips signs.

Past the ±125° soft limit, the firmware ignores the host’s acceleration command and applies a fixed opposing brake. This is not the same as commanding zero acceleration: with allow_reverse enabled, zero means hold current speed, so a motor travelling outbound at 5 rad/s would coast into the hard stop if the host went quiet. The brake decelerates regardless of host liveness.

A dropped byte is handled by bailing out: CMD_SET_ACCEL reads its 4 payload bytes with a 5 ms timeout, and a short read aborts the command rather than consuming the next one. The following command re-syncs the parser.

Always call ready-check after opening the port and wait for the echo. Opening a serial port resets the Nano, so anything sent before the sketch is running is lost.

The standalone RL sketch is fully autonomous and needs no host, but it accepts a few single-character commands at 500,000 baud:

CommandEffect
Ptoggle CSV telemetry
Eengage the motor (re-arm after a hard-limit trip)
Ddisengage the motor
Mprint AS5600 magnet diagnostics

This is what analyze_onboard.py captures to score a deployment:

ColumnFieldNote
1t_usmicroseconds
2motor_pos_rad×1000
3phi_rad×1000, 0 = hanging
4action×1000, the raw policy output
5statecontroller state machine
6freq_hzcheck this first — an off-rate loop invalidates the numbers
7overrunsticks that missed their deadline
8latency_ussample→command latency this tick
9latency_max_usworst seen since boot

Columns 8 and 9 measure the delay between the sensor sample the policy read and the moment the resulting command reached the stepper — the quantity the simulation models as obs_staleness_s. analyze_onboard.py reports their mean, p95 and max alongside the nominal the sim assumes, so a real rig can be checked against its model rather than trusted. See transport delay.

Older captures have seven columns; the analysis script treats the latency fields as optional and skips that line when they are absent.

The sketch prints [boot] policy(hanging/upright) = …, which you can compare against the PyTorch student on the same reference inputs if you suspect a weight export or PROGMEM bug.

Because opening the port resets the board, hang the pendulum still before launching a capture — the resting pose becomes the encoder zero.