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
Byte 1LEN_L
length low
Byte 2LEN_H
length high
Byte 3LEN_L^FF
XOR check
Byte 4LEN_H^FF
XOR check
Bytes 5…5+LEN-3PAYLOAD
command data
Last 2 bytesCRC_L, CRC_H
CCITT-16 LE
FieldBytesDescription
STX1Start-of-frame marker, always 0xF5.
LEN_L1Low byte of LEN (little-endian).
LEN_H1High byte of LEN (little-endian).
LEN_L ^ 0xFF1XOR integrity check for LEN_L.
LEN_H ^ 0xFF1XOR integrity check for LEN_H.
PAYLOADLEN−2Command/response data (see below).
CRC_L, CRC_H2CCITT-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_BYTEMeaning
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.
ParameterTypeDescription
databytesData 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).
ParameterTypeDescription
payloadbytesCommand 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.
ParameterTypeDescription
rawbytesComplete 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

MethodDescription
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

StateDescription
WAIT4STXScanning for 0xF5 start byte.
WAIT4LENCollecting 4 length+XOR bytes.
RECEIVINGAccumulating 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)