What Commander talks to: an EZ-B v4.x/2 controller, firmware v2017.01.05.00
on the unit this was verified against. Four listeners:
| port | what | notes |
|---|---|---|
| 23 | binary command protocol | one client only — ARC must be closed |
| 24 | camera stream | EZIMG + u32-LE length + JPEG |
| 80 | web config UI | |
| 8080 | plaintext CLI | coexists with a client on 23; useful for health checks |
The board's address is 192.168.1.1 in AP mode; once it joins your LAN, DHCP
decides. Set COMMANDER_ROBOT_HOST (or pass --host) to wherever it lands.
The board has no authentication on any port — which is why Commander
refuses to bind beyond loopback without a token (see safety.md).
Contiguous 24-port ranges that abut exactly — each ends where the next begins, which is what validates the values:
0x27–0x3E set servo speed
0x64–0x7B set digital LOW
0x7C–0x93 set digital HIGH
0x94–0xAB get digital
0xAC–0xC3 set servo position (degrees 0 = release)
0xC4–0xDB get ADC
0x01 release all servos
0x04 0x02 battery voltage (u16 LE × 0.003862434)
0x55 handshake
Handshake reply confirmed as 0x2A (decimal 42) against a real board on
2026-07-29, matching Synthiam's published protocol. The competing 2 in
ARC_Firmware_Src.json and ARC's log (0x02) is a different field — the
FirmwareId, reported later in a session — not the handshake reply. connect()
still records what arrives and warns rather than asserting, so a firmware change
surfaces as a log line instead of a hard failure.
16 servos. Names from Synthiam's build docs. Travel comes from ARC's own runtime limits where the factory Connection init script sets them (D1, D6, D9, D14, D18 — ARC clamps to these at runtime), and from observed positions across all 127 factory keyframes everywhere else.
| port | joint | travel | port | joint | travel |
|---|---|---|---|---|---|
| D0 | head pan | 16–172 | D8 | right forearm | 1–180 |
| D1 | head tilt | 70–173 | D9 | right gripper | 30–90 |
| D2 | right shoulder | 1–180 | D12 | left hip | 1–180 |
| D3 | left shoulder | 1–180 | D13 | left knee | 1–180 |
| D4 | left elbow | 1–180 | D14 | left ankle | 60–120 |
| D5 | left forearm | 1–180 | D16 | right hip | 1–180 |
| D6 | left gripper | 30–90 | D17 | right knee | 1–180 |
| D7 | right elbow | 1–180 | D18 | right ankle | 60–120 |
Travel is not taken from the project's ServoDescriptor entries: on the
verification unit all 16 read Minimum 90 / Maximum 90 (unconfigured
defaults), which would reject every command. test_factory_data.py guards
against that regression.
Port 24, independent of the command channel — no handshake, no contention with port 23, and no servo current, so it works on a partly charged pack.
./.venv/Scripts/python.exe scripts/camera_dump.py --count 10
./.venv/Scripts/python.exe scripts/camera_dump.py --probe # if the stream is silentVerified on hardware 2026-07-29: 320x240 RGB JPEG, ~13 fps, ~10 KB per frame.
The wire format is "EZIMG" + uint32-LE length + payload, but the length is the
transport size, not the image size. The firmware pads every frame with zeros up
to a 1 KB boundary and counts the padding in the prefix — observed frames
declared 11264 (11 x 1024) and 12288 (12 x 1024) bytes while their JPEG data
ended at 11049 and 11327.
So the payload is a JPEG followed by zeros, and the trailing bytes must be
trimmed at the FF D9 EOI marker before handing anything to a decoder. The
parser does this and reports padding_total in its stats. Only zero padding is
stripped: non-zero bytes after EOI are kept, since silently discarding real data
would hide a genuine format change.
The parser also resyncs by scanning for the next magic rather than trusting
alignment (joining a live stream mid-frame is normal), treats the length prefix
as authoritative so payload bytes spelling EZIMG do not split a frame, and
rejects implausible lengths rather than trying to allocate them.
data/actions.json (schema v1) is Commander's own format, exported from ARC's
project once via scripts/export_actions.py. ARC is not required after
that. The factory JD project yields 126 keyframes (127 ARC frames minus
the PAUSE pseudo-frame) and 39 actions.