//! 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, }