The daemon and GUI communicate over UDP using a binary frame format inspired by MAVLink v1. All multi-byte integers are big-endian on the wire.
| Port | Direction | Content |
|---|---|---|
| 5555 | GUI → daemon | Commands |
| 5556 | daemon → GUI | Telemetry |
| 5557 | daemon → GUI | Video (separate framing — see below) |
Default addresses: GUI connects to 192.168.50.3. Override in daemon/daemon.toml under [transport].
┌──────┬─────┬─────┬──────────┬────────────────┬────────────┐
│ STX │ len │ seq │ msg_id │ payload │ crc16 │
│ 0xFE │ 1 B │ 1 B │ 2 B │ len bytes │ 2 B │
└──────┴─────┴─────┴──────────┴────────────────┴────────────┘
| Field | Size | Description |
|---|---|---|
| STX | 1 byte | Start-of-frame marker, always 0xFE |
| len | 1 byte | Payload length in bytes (max 255) |
| seq | 1 byte | Sequence counter, wraps at 255 |
| msg_id | 2 bytes BE | Message type identifier |
| payload | len bytes | JSON-encoded UTF-8 string |
| crc16 | 2 bytes BE | CRC-16-CCITT over [len, seq, msg_id_lo, msg_id_hi] + payload |
CRC algorithm: CRC-16-CCITT, polynomial 0x1021, init 0xFFFF. msg_id is big-endian on the wire but the CRC input uses little-endian byte order for msg_id (lo byte first) — matches MAVLink v1. STX is not included in the CRC.
Implementation: daemon/core/link_protocol.py (Python) and godot/core/LinkProtocol.gd (GDScript).
| msg_id | Name | Direction | Payload fields |
|---|---|---|---|
| 0 | heartbeat | both | {} |
| 1 | ESTOP | GUI → daemon | {} — fast path, retry until SAFETY_ACK |
| 2 | REARM | GUI → daemon | {} — fast path, retry until SAFETY_ACK |
| 3 | SAFETY_ACK | daemon → GUI | {"action": "estop"|"rearm"} |
| 4 | battery | daemon → GUI | {"charge_pct": float, "voltage": float, "current": float, "estimated_runtime_s": float} |
| 5 | odometry | daemon → GUI | {"x": float, "y": float, "heading": float} (metres / radians, SE2) |
| 6 | lease_status | daemon → GUI | {"lease_owner": str, "estop_active": bool} |
| 7 | velocity_command | GUI → daemon | see Velocity command fields |
| 8 | video | daemon → GUI | separate port / framing — see Video |
| 9 | example_plugin | daemon → GUI | {"count": int, "timestamp": float} — example only |
| 10 | dock | GUI → daemon | {} — dock to configured station |
| 11 | dock_stations | daemon → GUI | {"ranges": [[id_start, id_end], ...]} |
| 12 | undock | GUI → daemon | {} |
| 13 | camera_select | GUI → daemon | {"source": str} — camera name or device index |
| 14 | camera_list | daemon → GUI | {"spot_sources": [str, ...], "device_indices": [int, ...]} |
| 15 | dock_status | daemon → GUI | {"state": "idle"|"searching"|"docking"|"docked"|"undocking"|"failed", "station_id": int|null} |
| 16 | waypoint_command | GUI → daemon | see Waypoint command fields |
| 17 | waypoint_status | daemon → GUI | {"state": "idle"|"navigating", "current": str|null, "waypoints": [str, ...]} |
msg_ids 0–3 are fast-pathed in the daemon receive loop before bus dispatch. msg_ids 18+ are available for new handlers.
Sent as msg_id = 7. All fields are optional; omitted fields default to 0 / false.
| Field | Type | Description |
|---|---|---|
vx |
float | Forward velocity (m/s) |
vy |
float | Lateral velocity (m/s) |
vrot |
float | Rotational velocity (rad/s) |
sit_stand |
bool | true = stand, false = sit (rising-edge on daemon) |
abort_requested |
bool | Safe power off (not ESTOP — use msg_id 1 for ESTOP) |
pitch_delta |
float | Body pitch increment this tick (rad), clamped ±0.5 |
roll_delta |
float | Body roll increment this tick (rad), clamped ±0.5 |
yaw_delta |
float | Body yaw increment this tick (rad), clamped ±1.5 |
height_delta |
float | Body height increment this tick (m), clamped −0.10 to +0.15 |
Walk mode activates when vx, vy, or vrot are present. Stand mode activates when only pose delta fields are present. Each velocity command expires 500 ms after issue — Spot stops if no new command arrives.
Sent as msg_id = 16. The action field is always required.
| Action | Additional fields | Description |
|---|---|---|
"save" |
name: str |
Snapshot current pose as a named waypoint |
"go" |
name: str |
Navigate to named waypoint |
"go_sequence" |
— | Navigate to all waypoints in saved order |
"stop" |
— | Abort current navigation |
"delete" |
name: str |
Remove named waypoint |
Waypoints are stored on the Jetson at ~/.spot-waypoints.json.
Video uses a separate UDP port (5557) with its own fragmentation header. It does not use the command/telemetry frame format above.
┌──────────┬──────────┬─────────┬─────────────┬──────────────┐
│ codec_id │ frame_id │ frag_id │ total_frags │ data │
│ 1 B │ 2 B │ 1 B │ 1 B │ remainder │
└──────────┴──────────┴─────────┴─────────────┴──────────────┘
| Field | Description |
|---|---|
codec_id |
1 = H.264, 2 = H.265 |
frame_id |
2-byte frame counter; wraps at 65535 |
frag_id |
0-based fragment index within this frame |
total_frags |
Total fragments for this frame |
data |
Raw compressed video bytes for this fragment |
Reassemble by collecting all fragments with the same frame_id until frag_id == total_frags - 1.