12.3. The packet format#
Every byte that crosses the wire between the cam and the host is part of a packet. A packet starts with a 10-byte header, runs a variable-length payload, and ends with a 4-byte trailing CRC. No other bytes appear on the wire – once the host has seen the 2-byte sync word, the next bytes are a header in this exact sequence.
12.3.1. The header#
Ten bytes, packed without padding. Each field:
sync– the 16-bit word0xD5AAin little-endian order. Byte 0 on the wire is0xAA, byte 1 is0xD5. A receiver scanning bytes can find the start of a packet by searching for the pairAA D5; anything before it is treated as garbage. The choice of value is deliberate:0xAAand0xD5rarely appear in printable text, and the pair is unlikely to occur by accident in the middle of a payload.seq– one byte. A counter that increments by one for each packet sent on a given direction. The receiver checks that the next packet’s sequence number is the expected one; if not, the reliability layer asks for a retransmit.chan– one byte. The channel ID this packet belongs to. Channels 0..31 are usable; the built-instdin,stdout,stream, and (optionally)profilechannels take fixed IDs the cam reserves.flags– one byte. A bit-field telling the receiver how to interpret the packet:bit 0
ACK– this packet is an acknowledgement of a previous one.bit 1
NAK– this packet rejects a previous one.bit 2
RTX– this packet is a retransmit.bit 3
ACK_REQ– the sender wants this packet acknowledged.bit 4
FRAGMENT– more fragments follow this one in a larger message.bit 5
EVENT– this packet carries a channel event rather than data.bits 6 and 7 are reserved.
opcode– one byte. The command or response code. The protocol library reserves opcode ranges by purpose:0x00..0x0F– protocol commands (SYNC, GET_CAPS, SET_CAPS, STATS, VERSION).0x10..0x1F– system commands (RESET, BOOT, INFO, EVENT, MEMORY).0x20..0x2F– channel commands (LIST, POLL, LOCK, UNLOCK, SHAPE, SIZE, READ, WRITE, IOCTL, EVENT).
len– two bytes, little-endian. The number of payload bytes that follow the header. A length of zero is legal – many acknowledgements and small commands carry no payload.crc– two bytes. A CRC-16 over the previous eight header bytes. A receiver that gets a header with a bad CRC drops the whole packet without even looking at the payload.
12.3.2. The payload#
Zero or more bytes, treated as opaque by the framing layer. What’s in the payload depends on the opcode: for a CHANNEL_READ reply it’s the actual channel data; for a GET_CAPS reply it’s a small fixed structure; for a channel write it’s whatever the host sent.
The maximum payload size depends on the cam’s protocol buffer size (refer to the per-board table in protocol.init()). Messages longer than the cap are split into fragments with the FRAGMENT flag set on all but the last.
12.3.3. The trailing CRC#
Four bytes, a CRC-32 over the payload. Catches corruption that the header CRC can’t see, particularly on long payloads where a single-bit error mid-frame would otherwise slip through.
Splitting the integrity check across two CRCs is deliberate. The header CRC protects the framing fields themselves – particularly the payload length. Without a separate header CRC, a single bit flip in the length byte would cause the receiver to read the wrong number of bytes for the payload and desync from the byte stream entirely; with one, a damaged header is rejected outright and the receiver re-scans for the next sync word. The payload CRC then protects the message body as a separate concern, so a bit flip in the data is reported as a corrupt payload rather than mistaken for a framing error.
The format is small enough to walk through byte by byte, and the fact that every packet has the same layout – sync, then header, then payload, then CRC – means a hand-rolled parser fits in a screen of code. That’s why a tiny host implementation in C, Python, or Rust is a weekend project; the protocol library is the maintained Python version on each side.