Sequence numbersReply matching

SEQ is a one-byte, caller-set counter per frame, typically incrementing and wrapping at 255.

CommandRole
Ordinary commandsDropped-frame detection only.
QUERYCopied onto the RESP, pairing each reply with its request when several are outstanding.
TRANSFERCopied onto the TRANSFER_RESP.
UPDATEAn UPDATE_RESP matches its request by the echoed op. DATA acknowledgements carry a rolling SEQ, since one covers a whole window of chunks.

OpcodesTYPE byte

An unrecognised opcode is ignored.

OpcodeNameDirectionPayloadReply
0x01MOVEPC→box4 or 6 bytesnone
0x02reserved---
0x03INJECTPC→box4 bytesnone
0x04RESETPC→box1 bytenone
0x05QUERYPC→box1 or 2 bytesRESP
0x06RESPbox→PCvariesnone
0x07REBOOTPC→box1 bytenone
0x08LOGbox→PCvariesnone
0x09LEDPC→box3 bytesnone
0x0ALOCKPC→box6 bytesnone
0x0BCATCHPC→box6 bytesnone
0x0CMOTION_EVENTbox→PC13 bytesnone
0x0Dreserved---
0x0Ereserved---
0x0FUSAGE_EVENTbox→PCvariesnone
0x10reserved---
0x11OPTIONPC→boxvariesnone
0x12CLIP_APPENDPC→boxvariesnone
0x13CLIP_CTRLPC→box1 bytenone
0x14CLIP_SETPC→box2 bytesnone
0x15CLIP_TRIGGERPC→box6-40 bytesnone
0x16TRAFFIC_EVENTbox→PCvariesnone
0x17UPDATEPC→box2-508 bytesUPDATE_RESP
0x18UPDATE_RESPbox→PC7 bytesnone
0x19RAWPC→box2-512 bytesnone
0x1ATRANSFERPC→box9-512 bytesTRANSFER_RESP
0x1BTRANSFER_RESPbox→PC2-506 bytesnone
0x1CREWRITEPC→box9-510 bytesnone
0x1DPATCHPC→box1-510 bytesnone
0x1ETRANSFORMPC→box8 bytesnone

Retired numbers are never reused. 0x02 was WHEEL, now MOVE (motion-tagged). 0x0D and 0x0E were KEY and CONSUMER, now INJECT (class-tagged). 0x10 was CONS_EVENT, now the class-tagged USAGE_EVENT.

ChecksumRejecting corrupted frames

The last two bytes are a CRC16-CCITT checksum over TYPE | SEQ | LEN | PAYLOAD, stored little-endian. On a mismatch the box drops the frame with no reply and resyncs at the next 0xA5.

ParameterValue
Polynomial0x1021
Initial value0xFFFF
Bit reflectionNone
Final XORNone
def crc16_ccitt(data):
    crc = 0xFFFF
    for b in data:
        crc ^= b << 8
        for _ in range(8):
            crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
    return crc

def encode_frame(type, seq, payload):
    body = bytes([type, seq]) + len(payload).to_bytes(2, "little") + payload
    crc = crc16_ccitt(body)
    return bytes([0xA5]) + body + crc.to_bytes(2, "little")

Example MOVE frame

A cursor MOVE of dx = 100, dy = 0.

  • Opcode 0x01.
  • Payload: motion (00 = cursor), 16-bit dx and dy (64 00, 00 00), then flags (00).
  • LEN is 06 00.
A5SOF
01TYPE
00SEQ
06 00LEN
00motion
64 00dx
00 00dy
00flags
lo hiCRC16

The CRC is the little-endian crc16_ccitt of 01 00 06 00 00 64 00 00 00 00. Compute it; don't copy a literal.