IMU UDP Protocol and Timing Model

This page is the English public documentation for the root PROTOCOL.md. It defines the current binary UDP attitude packets and the timing fields used by the host-side motion pipeline. All multi-byte packet fields use little-endian encoding.

UDP Channels

Channel

Port

Direction

Purpose

Data

4211

device to host

Binary attitude packets and text status

Discovery

4212

device broadcast

VQF_DISC device beacon

Discovery beacons use:

VQF_DISC,node=<MAC suffix>,id=<optional device ID>,ip=<IP>,tcp=4210,udp=4211,ver=<firmware version>

The device also announces _vqf-imu._tcp by mDNS, with host name vqf-imu-<node_id>.local.

Rotation v1

type = 0x01, exactly 12 bytes:

Offset

Type

Field

0

uint8

type

1

uint8

flags (bit0=REST, bit1=REST_SKIP)

2

uint16

seq

4

int16[4]

quaternion w, x, y, z in Q15

Python layout: "<BBH4h". Decode each Q15 value as value / 32767.0. seq is only meaningful within one device. It cannot order samples from different IMUs.

Rotation v2

type = 0x03, exactly 20 bytes:

Offset

Type

Field

0

uint8

type

1

uint8

flags

2

uint16

seq

4

uint64

sensor_time_us

12

int16[4]

quaternion w, x, y, z in Q15

Python layout: "<BBHQ4h".

sensor_time_us is the device monotonic microsecond clock since boot. The firmware must capture it at IMU sampling or VQF attitude update time, never at UDP send time. It is not Unix time and is not comparable across devices before clock synchronization.

The current firmware emits v2 0x03. The host remains compatible with legacy v1 0x01 firmware, but v1 nodes cannot participate in common-timeline solving.

Text Status Packet

type = 0x02, at least 4 bytes:

Offset

Type

Field

0

uint8

type

1

uint8

Reserved flags

2

uint16

Packet sequence number

4

UTF-8 bytes

Null-terminated text payload

The device uses this packet for low-rate messages such as $CAL,..., $PWR,... and $DIAG,....

Host Timing Fields

Field

Meaning

host_received_ns

Host monotonic time recorded immediately after recvfrom() returns.

mapped_sample_time_ns

Host-monotonic sample time after a valid clock mapping, otherwise null.

frame_id

Future monotonically increasing common mocap frame identifier.

frame_time_ns

Future common target time used to solve one body frame.

solved_time_ns

Future host monotonic time when body solving completes.

time_sync_valid

True after at least four valid samples spanning at least three seconds.

time_sync_error_us

RMS residual of the accepted linear clock fit, otherwise null.

max_sample_age_ms

Age of the oldest valid sample in a solved common frame, otherwise null.

Wall-clock Unix time is for logs and UI only. Real-time ordering, liveness, and packet-rate calculations use the host monotonic clock.

TCP Commands

The host opens a TCP connection to port 4210 and sends newline-terminated text commands:

Command

Purpose

$CMD,CAL_START

Start magnetometer calibration collection

$CMD,CAL_STOP

Stop calibration collection

$CMD,CAL_ERASE

Erase stored calibration parameters

$CMD,OFF

Request power off

$CMD,SET_ID,<id>

Store a custom device ID

$CMD,GET_ID

Query the custom device ID

$CMD,SYNC,<nonce>

Clock-sync request; TCP replies directly with $SYNC,<nonce>,<t1_us>,<t2_us>

TCP Clock Synchronization

The host synchronizes only nodes that emit v2 attitude packets. Every 10 seconds, it runs four immediate exchanges per node:

  1. The host records t0_host_ns before sending $CMD,SYNC,<nonce>.

  2. The device TCP receive task records t1_device_us after receiving the full line and t2_device_us immediately before replying.

  3. The host records t3_host_ns after receiving $SYNC,<nonce>,<t1_us>,<t2_us>.

  4. The host fits host_monotonic_ns ~= scale * sensor_time_us + offset.

Samples with RTT above 50 ms, or materially worse than the current best RTT, are discarded. offset handles different boot times; scale compensates for oscillator error and long-term drift. Before the mapping is valid, the system shows each node’s latest pose but does not treat nodes as sampled at one instant.

Common Mocap Timeline

Body solving never simply combines the most recently received attitude from each node. For synchronized v2 nodes, the host keeps a short pose history and selects:

t_target = min(latest_sample_time[node]) - safety_margin

The default safety_margin is 20 ms. Every required role (chest, upper arm, forearm, and palm) must provide samples before and after t_target with a gap no greater than 80 ms. The host uses shortest-path SLERP to obtain each attitude at that common instant. Missing, unsynchronized, insufficient, or stale data invalidates the common frame; old and new poses are never silently mixed.

In MocapState, frame_time_ns is t_target; node_samples retains the bracketing sequence numbers, times, interpolation fraction, and sample age. time_sync_error_us is the largest mapping error among nodes used in the frame, while max_sample_age_ms is the oldest leading-sample age.

OTA

The OTA HTTP service listens on port 8080:

Request

Purpose

POST /update

Upload a full firmware binary with Content-Type: application/octet-stream

GET /info

Query OTA/device information

Use a raw binary request body. Multipart form uploads are not supported.