Medius - Native APIFrame Format

Frame format

The one packet shape

Every message, both directions, is a frame with one fixed shape.

[SOF 0xA5][TYPE u8][SEQ u8][LEN u16 LE][PAYLOAD ≤512][CRC16 u16 LE]
FieldBytesNotes
SOF1start-of-frame marker, always 0xA5
TYPE1the opcode
SEQ1per-frame sequence number
LEN2payload byte count, little-endian
PAYLOAD0-512the command's data; empty for argument-free commands
CRC162checksum over the frame body

Max payload 512 bytes, so a frame is at most 519. Every multi-byte number is little-endian: the 16-bit value 100 is the bytes 64 00.

Sequence numbers

Matching a reply to its request

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

CommandRole of SEQ
Ordinary commandsOnly lets you spot a dropped frame.
QUERYThe box copies your SEQ onto the RESP, so with several requests outstanding the matching SEQ pairs each reply with its request.

Opcodes

The TYPE byte

The opcodes run from 0x01 to 0x16. An unrecognised opcode is ignored harmlessly.

OpcodeNameDirectionPayloadReply
0x01MOVEPC→box4 or 6 bytesnone
0x02reserved---
0x03INJECTPC→box4 bytesnone
0x04RESETPC→box0 bytesnone
0x05QUERYPC→box1 byteRESP
0x06RESPbox→PCvariesnone
0x07REBOOTPC→box1 bytenone
0x08LOGbox→PCvariesnone
0x09LEDPC→box3 bytesnone
0x0ALOCKPC→box5 bytesnone
0x0BCATCHPC→box6 bytesnone
0x0CMOTION_EVENTbox→PC11 bytesnone
0x0Dreserved---
0x0Ereserved---
0x0FUSAGE_EVENTbox→PCvariesnone
0x10reserved---
0x11OPTIONPC→boxvariesnone
0x12CLIP_APPENDPC→boxvariesnone
0x13CLIP_CTRLPC→box1 bytenone
0x14CLIP_SETPC→box2 bytesnone
0x15CLIP_TRIGGERPC→box6 bytesnone
0x16TRAFFIC_EVENTbox→PCvariesnone

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

Checksum & integrity

Rejecting 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: a MOVE frame

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

  • Opcode 0x01.
  • Payload is the motion byte (00 = cursor), the two 16-bit values dx and dy (64 00, 00 00), then the flags byte (00).
  • LEN is 06 00.
+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| A5     | 01     | 00     | 06 00  | 00     | 64 00  | 00 00  | 00     | lo hi  |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| SOF    | TYPE   | SEQ    | LEN    | motion | dx     | dy     | flags  | CRC16  |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+

The CRC bytes are the little-endian crc16_ccitt of 01 00 06 00 00 64 00 00 00 00. Compute them rather than copying a literal.