Binary Protocol
Low-level frame format and CRC algorithm. Located in pepper_c1/protocol.py. The Python implementation is a faithful port of the reference C code in c_example/.
Frame Structure
Every message — both commands sent by the host and responses sent by the reader — uses the same binary frame format:
Byte 00xF5
STX
STX
Byte 1LEN_L
length low
length low
Byte 2LEN_H
length high
length high
Byte 3LEN_L^FF
XOR check
XOR check
Byte 4LEN_H^FF
XOR check
XOR check
Bytes 5…5+LEN-3PAYLOAD
command data
command data
Last 2 bytesCRC_L, CRC_H
CCITT-16 LE
CCITT-16 LE
| Field | Bytes | Description |
|---|---|---|
| STX | 1 | Start-of-frame marker, always 0xF5. |
| LEN_L | 1 | Low byte of LEN (little-endian). |
| LEN_H | 1 | High byte of LEN (little-endian). |
| LEN_L ^ 0xFF | 1 | XOR integrity check for LEN_L. |
| LEN_H ^ 0xFF | 1 | XOR integrity check for LEN_H. |
| PAYLOAD | LEN−2 | Command/response data (see below). |
| CRC_L, CRC_H | 2 | CCITT-16 CRC over PAYLOAD only, little-endian. |
LEN definition: LEN =
len(PAYLOAD) + 2 — it counts the 2 CRC bytes but not the 5 header bytes.Payload Format
Command payload (host → reader)
[ CMD_BYTE ] [ DATA_BYTES... ]
The first byte is the command opcode from the Command enum. Remaining bytes are command-specific parameters.
Response payload (reader → host)
[ RESP_BYTE ] [ ECHO_CMD_BYTE ] [ RESPONSE_DATA... ]
| RESP_BYTE | Meaning |
|---|---|
| 0x00 (ACK) | Command succeeded. RESPONSE_DATA contains the result. |
| 0xFF (ERROR) | Command failed. Payload contains the failing command byte and a 16-bit error code. |
| 0xFE (ASYNC) | Asynchronous event (e.g. tag detected during polling). Stored in PepperC1.async_events. |
CRC Algorithm
The CRC is computed using the CCITT-16 algorithm with initial value 0xFFFF, applied to the PAYLOAD bytes only (not the header or CRC bytes themselves). The result is transmitted little-endian (low byte first).
# Python implementation (from protocol.py) def calc_crc(data: bytes) -> int: crc = 0xFFFF for byte in data: temp = ((crc >> 8) ^ byte) & 0xFF crc = _CCITT_TABLE[temp] ^ ((crc << 8) & 0xFFFF) return crc
Functions
calc_crc
(data)
protocol
Compute CCITT-16 CRC (initial value 0xFFFF) over
data.| Parameter | Type | Description |
|---|---|---|
| data | bytes | Data bytes to checksum. |
Returnsint — 16-bit CRC value.
from pepper_c1.protocol import calc_crc crc = calc_crc(b'\x03\x00') # CRC of GET_UID command with index 0 print(f"0x{crc:04X}")
build_frame
(payload)
protocol
Wrap a payload in a complete binary protocol frame (STX + length + XOR check + payload + CRC).
| Parameter | Type | Description |
|---|---|---|
| payload | bytes | Command or response payload bytes. |
Returnsbytes — complete frame bytes ready to transmit.
from pepper_c1.protocol import build_frame from pepper_c1.commands import Command # Build a GET_VERSION command frame frame = build_frame(bytes([Command.GET_VERSION])) transport.write(frame)
parse_frame
(raw)
protocol
Validate a complete raw frame and return the payload (without CRC). The caller must pass a complete frame starting with
0xF5.| Parameter | Type | Description |
|---|---|---|
| raw | bytes | Complete raw frame bytes starting with STX (0xF5). |
Returnsbytes — payload bytes (without header or CRC).
RaisesProtocolError — on any validation failure (bad STX, XOR, length, or CRC mismatch).
FrameParser
protocol
Stateful incremental frame parser — mirrors the C implementation exactly. Use when bytes arrive in chunks (e.g. from a serial port or socket).
Methods
| Method | Description |
|---|---|
| feed(data: bytes) | Feed a chunk of bytes into the parser. Returns a list of complete payload byte strings. Invalid frames are silently discarded; the parser re-synchronises on the next STX byte. |
| reset() | Reset the parser state. Called automatically on instantiation. |
Internal States
| State | Description |
|---|---|
| WAIT4STX | Scanning for 0xF5 start byte. |
| WAIT4LEN | Collecting 4 length+XOR bytes. |
| RECEIVING | Accumulating payload+CRC bytes. |
from pepper_c1.protocol import FrameParser parser = FrameParser() # Feed incoming bytes in chunks chunk1 = transport._read_chunk(256) chunk2 = transport._read_chunk(256) completed = parser.feed(chunk1 + chunk2) for payload in completed: print(f"Got payload: {payload.hex()}")
Example: Manual Frame Round-Trip
from pepper_c1.protocol import build_frame, parse_frame, calc_crc from pepper_c1.commands import Command # Build GET_VERSION command payload = bytes([Command.GET_VERSION]) frame = build_frame(payload) print(f"Frame: {frame.hex(' ')}") # e.g.: f5 03 00 fc ff 0b f5 7d # Simulate receiving the frame back and parsing it received_payload = parse_frame(frame) print(f"Payload: {received_payload.hex()}") # e.g.: 0b (Command.GET_VERSION)