Skip to content

Latest commit

 

History

History
101 lines (80 loc) · 4.36 KB

File metadata and controls

101 lines (80 loc) · 4.36 KB

The EZ-B board and its protocol

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).

Opcode table

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.

Joint map

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.

Camera

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 silent

Verified on hardware 2026-07-29: 320x240 RGB JPEG, ~13 fps, ~10 KB per frame.

The padding gotcha

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.

Motion library format

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.