# 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
| 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
| `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
| `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
```text
`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 }` to `struct Bar { x: u8 }` **changes** the ID.
- Reordering fields inside `Foo` does **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](https://github.com/tarotene/telepath/issues/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
| 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
```rust
use telepath_wire::cmd_id::{derive_cmd_id, fnv1a_16};
// Derive a cmd_id from a function signature.
const CMD_PING: u16 = derive_cmd_id("ping", "()", "u32");
// Raw FNV-1a 16-bit hash.
const H: u16 = fnv1a_16(b"hello");
```
## `framing` module
```rust
use telepath_wire::framing::{cobs_encode, cobs_decode, FrameAccumulator, MAX_FRAME_SIZE};
// Encode
let mut frame = [0u8; MAX_FRAME_SIZE];
let n = cobs_encode(data, &mut frame)?; // includes 0x00 delimiter
// Decode
let mut decoded = [0u8; MAX_FRAME_SIZE];
let m = cobs_decode(&frame[..n - 1], &mut decoded)?;
// Stream accumulation
let mut acc = FrameAccumulator::<512>::new();
for byte in stream {
if acc.feed(byte) { // returns true on 0x00 delimiter
let raw = acc.frame(); // Some(&[u8]) or None on overflow
acc.reset();
}
}
```
## AppError payload format
When `Response.status == ResponseStatus::AppError`, `Response.payload`
contains a postcard-serialized [`AppErrorPayload`]:
| `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.
```rust
use telepath_wire::{AppErrorPayload, encode_app_error, decode_app_error};
// Encode (e.g. on the server, into a stack buffer)
let err = AppErrorPayload { code: 42, message: "sensor not ready" };
let mut buf = [0u8; 64];
let n = encode_app_error(&err, &mut buf).expect("encode failed");
// Decode (e.g. on the host, borrowing from the response payload)
let decoded = decode_app_error(&buf[..n]).expect("decode failed");
assert_eq!(decoded.code, 42);
assert_eq!(decoded.message, "sensor not ready");
```
## Build
```
cargo build -p telepath-wire
cargo test -p telepath-wire
```