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.
252 lines
9.2 KiB
Rust
252 lines
9.2 KiB
Rust
//! 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,
|
||
}
|