Files
protocol-rs/src/lib.rs
T
swaits 4cccb65124 feat!: protocol v2 wire format and module restructure
The wire protocol is re-specified as DongLoRa Protocol v2 (see PROTOCOL.md). Every type_id, tag-correlation rule, and field layout is incompatible with 0.1.0.

* Add PROTOCOL.md as the normative spec for the v2 wire format.

* Replace framing.rs with a streaming FrameDecoder that emits (type_id, tag, payload) triples on each COBS-delimited, CRC-checked frame.

* Split the single-file lib.rs into dedicated modules: frame, commands, events, errors, modulation, info, crc, chip_id. Re-exports preserve convenience imports at the crate root.

* Replace RadioConfig / Bandwidth with a Modulation enum covering LoRa, FSK/GFSK, LR-FHSS, and FLRC, each with its own typed parameter struct.

* Add TxFlags, RxOrigin, SetConfigResult, OkPayload to model v2 response shapes, and RadioChipId for GET_INFO.

* Rewrite fuzz corpus: replace the 4 cobs_* / command_roundtrip targets with decoder_feed, frame_roundtrip, lora_config_roundtrip, and parse_device_message; drop parse_radio_config.

* Expand proptest and mutation-killer coverage for the new wire shape.

* Rewrite canonical-byte test vectors against the v2 spec.
2026-04-22 09:21:11 -06:00

252 lines
9.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! DongLoRa Protocol v2 — wire types, framing, and codecs.
//!
//! This crate is the normative Rust implementation of the protocol
//! defined in `PROTOCOL.md`. It is `no_std` and alloc-free: every
//! container is fixed-capacity (`heapless::Vec`) and every fallible
//! operation returns a typed error — no panics, no unwraps, no
//! `unsafe`.
//!
//! # Layout
//!
//! - [`frame`] — wire-level COBS + CRC framing. `encode_frame` turns a
//! `(type_id, tag, payload)` tuple into a complete wire-ready byte
//! slice; [`FrameDecoder`] accumulates inbound bytes and emits
//! [`FrameResult`] values as frames arrive.
//! - [`commands`] — host-to-device command enum and `type_id` constants.
//! - [`events`] — device-to-host message enum (`OK` / `ERR` / `RX` /
//! `TX_DONE`) and its sub-shapes.
//! - [`modulation`] — per-modulation parameter structs for `SET_CONFIG`
//! (LoRa, FSK/GFSK, LR-FHSS, FLRC).
//! - [`info`] — `GET_INFO` response payload, including capability bits.
//! - [`errors`] — wire-level error codes (`ErrorCode`).
//! - [`chip_id`] — `RadioChipId` enum.
//! - [`crc`] — CRC-16/CCITT-FALSE implementation.
//!
//! # Typical encode
//!
//! ```no_run
//! use donglora_protocol::{encode_frame, Command, commands::TYPE_PING, MAX_WIRE_FRAME};
//!
//! let cmd = Command::Ping;
//! let mut payload_buf = [0u8; 4];
//! let payload_len = cmd.encode_payload(&mut payload_buf).unwrap();
//!
//! let mut wire = [0u8; MAX_WIRE_FRAME];
//! let wire_len = encode_frame(cmd.type_id(), 1, &payload_buf[..payload_len], &mut wire).unwrap();
//! // wire[..wire_len] is ready to transmit.
//! # let _ = (TYPE_PING, wire_len);
//! ```
//!
//! # Typical decode
//!
//! ```no_run
//! use donglora_protocol::{Command, FrameDecoder, FrameResult};
//!
//! let mut decoder = FrameDecoder::new();
//! let incoming_bytes: &[u8] = &[];
//! decoder.feed(incoming_bytes, |res| match res {
//! FrameResult::Ok { type_id, tag, payload } => {
//! match Command::parse(type_id, payload) {
//! Ok(cmd) => { /* dispatch cmd with tag */ }
//! Err(_e) => { /* respond ERR(EUNKNOWN_CMD) with echoed tag */ }
//! }
//! }
//! FrameResult::Err(_e) => { /* emit async ERR(EFRAME) */ }
//! });
//! ```
#![no_std]
pub mod chip_id;
pub mod commands;
pub mod crc;
pub mod errors;
pub mod events;
pub mod frame;
pub mod info;
pub mod modulation;
// ── Public re-exports ───────────────────────────────────────────────
pub use chip_id::RadioChipId;
pub use commands::{Command, TxFlags};
pub use errors::ErrorCode;
pub use events::{
DeviceMessage, OkPayload, Owner, RxOrigin, RxPayload, SetConfigResult, SetConfigResultCode,
TxDonePayload, TxResult,
};
pub use frame::{FrameDecoder, FrameResult, encode_frame};
pub use info::{Info, cap};
pub use modulation::{
FlrcBitrate, FlrcBt, FlrcCodingRate, FlrcConfig, FlrcPreambleLen, FskConfig, LoRaBandwidth,
LoRaCodingRate, LoRaConfig, LoRaHeaderMode, LrFhssBandwidth, LrFhssCodingRate, LrFhssConfig,
LrFhssGrid, Modulation, ModulationId,
};
// ── Top-level size constants ────────────────────────────────────────
/// Maximum over-the-air packet payload (bytes). The spec allows this to
/// be chip-dependent via `GET_INFO.max_payload_bytes`; 255 is the
/// ceiling for all currently-supported Semtech silicon.
pub const MAX_OTA_PAYLOAD: usize = 255;
/// Maximum payload-field size in any DongLoRa Protocol frame. This is the OTA limit
/// plus the 20-byte `RX` metadata prefix: type + tag + this + crc gives
/// the overall frame budget.
pub const MAX_PAYLOAD_FIELD: usize = MAX_OTA_PAYLOAD + 20;
/// Byte count of the pre-CRC, pre-COBS frame header: `type(1) + tag(2)`.
pub const FRAME_HEADER_SIZE: usize = 3;
/// Byte count of the frame trailer (CRC16).
pub const FRAME_TRAILER_SIZE: usize = 2;
/// Maximum pre-COBS frame size: header + max payload + trailer.
pub const MAX_PRE_COBS_FRAME: usize = FRAME_HEADER_SIZE + MAX_PAYLOAD_FIELD + FRAME_TRAILER_SIZE;
/// COBS encoding overhead upper bound: `ceil(n/254) + 1` bytes added
/// around a run of n bytes. For `MAX_PRE_COBS_FRAME = 280` this is 3.
pub const MAX_COBS_OVERHEAD: usize = 3;
/// Maximum on-wire frame size: pre-COBS size + COBS overhead + 1 for the
/// trailing `0x00` sentinel. Buffer sizes of this value (or more)
/// guarantee no encode-side overflow across any valid frame.
pub const MAX_WIRE_FRAME: usize = MAX_PRE_COBS_FRAME + MAX_COBS_OVERHEAD + 1;
/// Maximum FSK/GFSK sync-word length (`PROTOCOL.md §10.2`).
pub const MAX_SYNC_WORD_LEN: usize = 8;
/// Maximum `GET_INFO.mcu_uid` length (`PROTOCOL.md §6.2`).
pub const MAX_MCU_UID_LEN: usize = 32;
/// Maximum `GET_INFO.radio_uid` length (`PROTOCOL.md §6.2`).
pub const MAX_RADIO_UID_LEN: usize = 16;
/// Maximum `OK` payload size across all shapes. `Info` is the worst
/// case: `GET_INFO`'s 37-byte fixed prefix plus both maximum UIDs
/// (32 + 16).
pub const MAX_OK_PAYLOAD: usize = 37 + MAX_MCU_UID_LEN + MAX_RADIO_UID_LEN;
/// Maximum `OK` payload size for `SET_CONFIG`: `result(1) + owner(1) +
/// modulation_id(1) + max params(24 for FSK with 8-byte sync word)`.
pub const MAX_SETCONFIG_OK_PAYLOAD: usize = 2 + 1 + 24;
// ── Error types ─────────────────────────────────────────────────────
/// Errors returned from `encode_frame`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum FrameEncodeError {
/// Payload exceeds `MAX_PAYLOAD_FIELD`.
PayloadTooLarge,
/// Output buffer too small for the encoded frame.
BufferTooSmall,
/// COBS encoder rejected the input (should be unreachable given
/// size checks, but surfaced as its own variant for completeness).
CobsEncode,
}
/// Errors returned from the frame decoder.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum FrameDecodeError {
/// COBS decode failed.
Cobs,
/// CRC check failed.
Crc,
/// Decoded frame shorter than the minimum `type + tag + crc`.
TooShort,
}
/// Errors from encoding a `Command` payload.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum CommandEncodeError {
/// Output buffer too small.
BufferTooSmall,
/// TX with zero-byte payload (spec rejects with `ERR(ELENGTH)`).
EmptyTxPayload,
/// TX payload exceeds `MAX_OTA_PAYLOAD`.
PayloadTooLarge,
/// FSK sync_word_len > `MAX_SYNC_WORD_LEN`.
SyncWordTooLong,
}
/// Errors from parsing a `Command` payload.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum CommandParseError {
/// `type_id` is not a defined command.
UnknownType,
/// Payload length is wrong for the command or modulation.
WrongLength,
/// An enum field carries a value not in the defined table.
InvalidField,
/// A reserved bit (`TX` flag bits 1–7) was set.
ReservedBitSet,
/// `SET_CONFIG`'s `modulation_id` is not assigned.
UnknownModulation,
}
/// Errors from encoding a device-message payload.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum DeviceMessageEncodeError {
BufferTooSmall,
/// Payload exceeds an enum's fixed size budget.
PayloadTooLarge,
InvalidField,
SyncWordTooLong,
}
/// Errors from parsing a device-message payload.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum DeviceMessageParseError {
UnknownType,
/// Payload length wrong for the message shape.
WrongLength,
/// Payload shorter than the fixed prefix required by the message.
TooShort,
/// An enum field carries a value not in the defined table.
InvalidField,
/// Parser needs the originating command's `type_id` for an `OK`
/// frame but none was supplied.
MissingContext,
/// `OK` frame for an unknown originating command type, or unknown
/// `modulation_id` inside a `SET_CONFIG` result.
UnknownContext,
}
/// Errors from modulation param encoding.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum ModulationEncodeError {
BufferTooSmall,
SyncWordTooLong,
}
/// Errors from modulation param parsing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum ModulationParseError {
/// Declared length did not match the expected total for this
/// modulation shape.
WrongLength { expected: usize, actual: usize },
/// Payload shorter than the modulation's fixed prefix.
TooShort,
/// An enum field or reserved byte carried an invalid value.
InvalidField,
/// `modulation_id` byte does not match any defined modulation.
UnknownModulation,
}
/// Errors from `Info` encode/decode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum InfoParseError {
TooShort,
BufferTooSmall,
InvalidField,
}