Medius - Native APIFrame Format

Frame format

The one packet shape

Every message, both directions, is a frame with one fixed shape. The whole protocol is frames; each command is the same machinery with a different opcode and payload.

[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 helps you spot a dropped frame.
QUERYThe box copies your SEQ onto the RESP, so with several requests outstanding the matching SEQ tells you which reply answers which.

Opcodes

The TYPE byte

The opcodes run from 0x01 to 0x15. Four values are reserved, retired by the unified-input collapse. An unrecognised opcode is ignored harmlessly, which keeps newer and older firmware compatible.

OpcodeNameDirectionPayloadReply
0x01MOVEPC→box3 or 5 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→box1 bytenone
0x0CMOTION_EVENTbox→PC6 bytesnone
0x0Dreserved---
0x0Ereserved---
0x0FUSAGE_EVENTbox→PCvariesnone
0x10reserved---
0x11OPTIONPC→boxvariesnone
0x12CLIP_APPENDPC→boxvariesnone
0x13CLIP_CTRLPC→box1 bytenone
0x14CLIP_SETPC→box2 bytesnone
0x15CLIP_TRIGGERPC→box6 bytesnone

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 (everything but SOF and the checksum itself), stored little-endian. On a mismatch the box silently drops the frame and resyncs at the next 0xA5, so corrupted frames are never acted on.

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) then the two 16-bit values dx and dy (64 00, 00 00).
  • LEN is 05 00.
+--------+--------+--------+--------+--------+--------+--------+--------+
| A5     | 01     | 00     | 05 00  | 00     | 64 00  | 00 00  | lo hi  |
+--------+--------+--------+--------+--------+--------+--------+--------+
| SOF    | TYPE   | SEQ    | LEN    | motion | dx     | dy     | CRC16  |
+--------+--------+--------+--------+--------+--------+--------+--------+

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