telepath-wire
Shared wire protocol types for the Telepath RPC system. Used by both
telepath-server (MCU side) and telepath-client (PC side).
#![no_std] — no heap allocation. All types are stack-friendly and
borrow from the receive buffer for zero-copy deserialization.
Protocol overview
| Aspect | Specification |
|---|---|
| Framing (Host→Target) | COBS; 0x00 delimiter |
| Framing (Target→Host) | rzCOBS; 0x00 delimiter |
| Serialization | postcard (little-endian, varint-compressed) |
| Packet types | Request (0x01) / Response (0x02) |
| Error signaling | ResponseStatus field inside Response |
| Discovery CmdID | 0x0000 — reserved (CDP) |
Key types
| Item | Description |
|---|---|
Request<'a> |
RPC call from host to target; args borrows from the receive buffer |
Response<'a> |
RPC reply from target to host; payload borrows from the receive buffer |
PacketType |
Wire discriminant: Request / Response |
ResponseStatus |
Ok / AppError / SystemError |
WireError |
PayloadTooLarge / SerdeError(postcard::Error) / UnknownPacketType / FramingError |
Constants
| Constant | Value | Description |
|---|---|---|
MAX_PAYLOAD_SIZE |
256 | Maximum payload bytes (both sides enforce this) |
framing::MAX_FRAME_SIZE |
512 | Maximum COBS frame bytes including delimiter |
CMD_ID_DISCOVERY |
0x0000 |
Reserved CmdID for Command Discovery Protocol |
Command ID derivation
Command IDs are 16-bit values that identify a command on the wire. Each ID is derived deterministically from the command's textual signature at build time, so firmware and host always agree without a runtime registry sync.
Algorithm
FNV-1a 32-bit (offset basis 0x811c9dc5, prime 0x01000193), XOR-folded to
16 bits: result = (hash >> 16) ^ (hash as u16).
XOR-fold is preferred over truncation because it preserves avalanche across the full input range and reduces low-bit bias inherent in multiplicative hashes.
Pre-image
name || 0x1F || args_type || 0x1F || ret_type (UTF-8 bytes)
0x1F (ASCII Unit Separator) cannot appear in Rust identifiers or type paths,
so it is collision-free as a field delimiter.
Type-name caveat
args_type and ret_type are the textual Rust type names as extracted by the
#[command] proc-macro (syn-derived token strings). This is a textual
canonicalization, not a true postcard schema digest:
- Renaming
struct Foo { x: u8 }tostruct Bar { x: u8 }changes the ID. - Reordering fields inside
Foodoes not change the ID.
Migration to a real postcard-schema fingerprint is planned once
postcard >= 1.2 is adopted in this workspace
(see issue #3).
0x0000 reservation
CMD_ID_DISCOVERY (0x0000) is reserved for the Command Discovery Protocol.
If the raw hash collides with it, derive_cmd_id loops over descending salt
bytes (0xFF, 0xFE, …) until the result is non-zero — guaranteeing that
CMD_ID_DISCOVERY is never returned.
Collision risk
| Commands (N) | P(at least one collision) |
|---|---|
| 32 | ~0.8% |
| 64 | ~3.1% |
| 128 | ~12% |
| 256 | ~38% |
| 1024 | ~99% |
(Birthday-paradox approximation: P ≈ 1 − e^(−N²/131072).)
Keep one device's command count ≤ 64 for a comfortable collision margin.
The reserved 0x0000 ID is avoided by rehashing the preimage with a 0xFF salt byte when the raw hash equals 0x0000; the discovery ID is never emitted by user commands.
Cross-command duplicate ID detection is enforced at build time: two #[command] functions in the same crate that hash to the same ID produce a compile_error!, while cross-crate collisions are caught as a linker "multiple definition" error before the firmware is flashed.
Usage
use ;
// Derive a cmd_id from a function signature.
const CMD_PING: u16 = derive_cmd_id;
// Raw FNV-1a 16-bit hash.
const H: u16 = fnv1a_16;
framing module
use ;
// Encode
let mut frame = ;
let n = cobs_encode?; // includes 0x00 delimiter
// Decode
let mut decoded = ;
let m = cobs_decode?;
// Stream accumulation
let mut acc = new;
for byte in stream
AppError payload format
When Response.status == ResponseStatus::AppError, Response.payload
contains a postcard-serialized [AppErrorPayload]:
| Field | Type | Wire encoding |
|---|---|---|
code |
u16 |
postcard varint (1–3 bytes) |
message |
&str |
postcard varint(len) + UTF-8 bytes |
Use the encode_app_error / decode_app_error helpers; both are no_std /
no-alloc and borrow the message slice from the receive buffer.
The code namespace is application-defined. Reserve code = 0 as a
catch-all "unspecified application error" when no finer classification is
available.
use ;
// Encode (e.g. on the server, into a stack buffer)
let err = AppErrorPayload ;
let mut buf = ;
let n = encode_app_error.expect;
// Decode (e.g. on the host, borrowing from the response payload)
let decoded = decode_app_error.expect;
assert_eq!;
assert_eq!;
Build
cargo build -p telepath-wire
cargo test -p telepath-wire