donglora_protocol/lib.rs
1//! DongLoRa Protocol v2 — wire types, framing, and codecs.
2//!
3//! This crate is the normative Rust implementation of the protocol
4//! defined in `PROTOCOL.md`. It is `no_std` and alloc-free: every
5//! container is fixed-capacity (`heapless::Vec`) and every fallible
6//! operation returns a typed error — no panics, no unwraps, no
7//! `unsafe`.
8//!
9//! # Layout
10//!
11//! - [`frame`] — wire-level COBS + CRC framing. `encode_frame` turns a
12//! `(type_id, tag, payload)` tuple into a complete wire-ready byte
13//! slice; [`FrameDecoder`] accumulates inbound bytes and emits
14//! [`FrameResult`] values as frames arrive.
15//! - [`commands`] — host-to-device command enum and `type_id` constants.
16//! - [`events`] — device-to-host message enum (`OK` / `ERR` / `RX` /
17//! `TX_DONE`) and its sub-shapes.
18//! - [`modulation`] — per-modulation parameter structs for `SET_CONFIG`
19//! (LoRa, FSK/GFSK, LR-FHSS, FLRC).
20//! - [`info`] — `GET_INFO` response payload, including capability bits.
21//! - [`errors`] — wire-level error codes (`ErrorCode`).
22//! - [`chip_id`] — `RadioChipId` enum.
23//! - [`crc`] — CRC-16/CCITT-FALSE implementation.
24//!
25//! # Typical encode
26//!
27//! ```no_run
28//! use donglora_protocol::{encode_frame, Command, commands::TYPE_PING, MAX_WIRE_FRAME};
29//!
30//! let cmd = Command::Ping;
31//! let mut payload_buf = [0u8; 4];
32//! let payload_len = cmd.encode_payload(&mut payload_buf).unwrap();
33//!
34//! let mut wire = [0u8; MAX_WIRE_FRAME];
35//! let wire_len = encode_frame(cmd.type_id(), 1, &payload_buf[..payload_len], &mut wire).unwrap();
36//! // wire[..wire_len] is ready to transmit.
37//! # let _ = (TYPE_PING, wire_len);
38//! ```
39//!
40//! # Typical decode
41//!
42//! ```no_run
43//! use donglora_protocol::{Command, FrameDecoder, FrameResult};
44//!
45//! let mut decoder = FrameDecoder::new();
46//! let incoming_bytes: &[u8] = &[];
47//! decoder.feed(incoming_bytes, |res| match res {
48//! FrameResult::Ok { type_id, tag, payload } => {
49//! match Command::parse(type_id, payload) {
50//! Ok(cmd) => { /* dispatch cmd with tag */ }
51//! Err(_e) => { /* respond ERR(EUNKNOWN_CMD) with echoed tag */ }
52//! }
53//! }
54//! FrameResult::Err(_e) => { /* emit async ERR(EFRAME) */ }
55//! });
56//! ```
57
58#![no_std]
59
60pub mod chip_id;
61pub mod commands;
62pub mod crc;
63pub mod errors;
64pub mod events;
65pub mod frame;
66pub mod info;
67pub mod modulation;
68
69// ── Public re-exports ───────────────────────────────────────────────
70
71pub use chip_id::RadioChipId;
72pub use commands::{Command, TxFlags};
73pub use errors::ErrorCode;
74pub use events::{
75 DeviceMessage, OkPayload, Owner, RxOrigin, RxPayload, SetConfigResult, SetConfigResultCode,
76 TxDonePayload, TxResult,
77};
78pub use frame::{FrameDecoder, FrameResult, encode_frame};
79pub use info::{Info, cap};
80pub use modulation::{
81 FlrcBitrate, FlrcBt, FlrcCodingRate, FlrcConfig, FlrcPreambleLen, FskConfig, LoRaBandwidth,
82 LoRaCodingRate, LoRaConfig, LoRaHeaderMode, LrFhssBandwidth, LrFhssCodingRate, LrFhssConfig,
83 LrFhssGrid, Modulation, ModulationId,
84};
85
86// ── Top-level size constants ────────────────────────────────────────
87
88/// Maximum over-the-air packet payload (bytes). The spec allows this to
89/// be chip-dependent via `GET_INFO.max_payload_bytes`; 255 is the
90/// ceiling for all currently-supported Semtech silicon.
91pub const MAX_OTA_PAYLOAD: usize = 255;
92
93/// Maximum payload-field size in any DongLoRa Protocol frame. This is the OTA limit
94/// plus the 20-byte `RX` metadata prefix: type + tag + this + crc gives
95/// the overall frame budget.
96pub const MAX_PAYLOAD_FIELD: usize = MAX_OTA_PAYLOAD + 20;
97
98/// Byte count of the pre-CRC, pre-COBS frame header: `type(1) + tag(2)`.
99pub const FRAME_HEADER_SIZE: usize = 3;
100
101/// Byte count of the frame trailer (CRC16).
102pub const FRAME_TRAILER_SIZE: usize = 2;
103
104/// Maximum pre-COBS frame size: header + max payload + trailer.
105pub const MAX_PRE_COBS_FRAME: usize = FRAME_HEADER_SIZE + MAX_PAYLOAD_FIELD + FRAME_TRAILER_SIZE;
106
107/// COBS encoding overhead upper bound: `ceil(n/254) + 1` bytes added
108/// around a run of n bytes. For `MAX_PRE_COBS_FRAME = 280` this is 3.
109pub const MAX_COBS_OVERHEAD: usize = 3;
110
111/// Maximum on-wire frame size: pre-COBS size + COBS overhead + 1 for the
112/// trailing `0x00` sentinel. Buffer sizes of this value (or more)
113/// guarantee no encode-side overflow across any valid frame.
114pub const MAX_WIRE_FRAME: usize = MAX_PRE_COBS_FRAME + MAX_COBS_OVERHEAD + 1;
115
116/// Maximum FSK/GFSK sync-word length (`PROTOCOL.md §10.2`).
117pub const MAX_SYNC_WORD_LEN: usize = 8;
118
119/// Maximum `GET_INFO.mcu_uid` length (`PROTOCOL.md §6.2`).
120pub const MAX_MCU_UID_LEN: usize = 32;
121
122/// Maximum `GET_INFO.radio_uid` length (`PROTOCOL.md §6.2`).
123pub const MAX_RADIO_UID_LEN: usize = 16;
124
125/// Maximum `OK` payload size across all shapes. `Info` is the worst
126/// case: `GET_INFO`'s 37-byte fixed prefix plus both maximum UIDs
127/// (32 + 16).
128pub const MAX_OK_PAYLOAD: usize = 37 + MAX_MCU_UID_LEN + MAX_RADIO_UID_LEN;
129
130/// Maximum `OK` payload size for `SET_CONFIG`: `result(1) + owner(1) +
131/// modulation_id(1) + max params(24 for FSK with 8-byte sync word)`.
132pub const MAX_SETCONFIG_OK_PAYLOAD: usize = 2 + 1 + 24;
133
134// ── Error types ─────────────────────────────────────────────────────
135
136/// Errors returned from `encode_frame`.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138#[cfg_attr(feature = "defmt", derive(defmt::Format))]
139pub enum FrameEncodeError {
140 /// Payload exceeds `MAX_PAYLOAD_FIELD`.
141 PayloadTooLarge,
142 /// Output buffer too small for the encoded frame.
143 BufferTooSmall,
144 /// COBS encoder rejected the input (should be unreachable given
145 /// size checks, but surfaced as its own variant for completeness).
146 CobsEncode,
147}
148
149/// Errors returned from the frame decoder.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151#[cfg_attr(feature = "defmt", derive(defmt::Format))]
152pub enum FrameDecodeError {
153 /// COBS decode failed.
154 Cobs,
155 /// CRC check failed.
156 Crc,
157 /// Decoded frame shorter than the minimum `type + tag + crc`.
158 TooShort,
159}
160
161/// Errors from encoding a `Command` payload.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163#[cfg_attr(feature = "defmt", derive(defmt::Format))]
164pub enum CommandEncodeError {
165 /// Output buffer too small.
166 BufferTooSmall,
167 /// TX with zero-byte payload (spec rejects with `ERR(ELENGTH)`).
168 EmptyTxPayload,
169 /// TX payload exceeds `MAX_OTA_PAYLOAD`.
170 PayloadTooLarge,
171 /// FSK sync_word_len > `MAX_SYNC_WORD_LEN`.
172 SyncWordTooLong,
173}
174
175/// Errors from parsing a `Command` payload.
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177#[cfg_attr(feature = "defmt", derive(defmt::Format))]
178pub enum CommandParseError {
179 /// `type_id` is not a defined command.
180 UnknownType,
181 /// Payload length is wrong for the command or modulation.
182 WrongLength,
183 /// An enum field carries a value not in the defined table.
184 InvalidField,
185 /// A reserved bit (`TX` flag bits 1–7) was set.
186 ReservedBitSet,
187 /// `SET_CONFIG`'s `modulation_id` is not assigned.
188 UnknownModulation,
189}
190
191/// Errors from encoding a device-message payload.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193#[cfg_attr(feature = "defmt", derive(defmt::Format))]
194pub enum DeviceMessageEncodeError {
195 BufferTooSmall,
196 /// Payload exceeds an enum's fixed size budget.
197 PayloadTooLarge,
198 InvalidField,
199 SyncWordTooLong,
200}
201
202/// Errors from parsing a device-message payload.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204#[cfg_attr(feature = "defmt", derive(defmt::Format))]
205pub enum DeviceMessageParseError {
206 UnknownType,
207 /// Payload length wrong for the message shape.
208 WrongLength,
209 /// Payload shorter than the fixed prefix required by the message.
210 TooShort,
211 /// An enum field carries a value not in the defined table.
212 InvalidField,
213 /// Parser needs the originating command's `type_id` for an `OK`
214 /// frame but none was supplied.
215 MissingContext,
216 /// `OK` frame for an unknown originating command type, or unknown
217 /// `modulation_id` inside a `SET_CONFIG` result.
218 UnknownContext,
219}
220
221/// Errors from modulation param encoding.
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223#[cfg_attr(feature = "defmt", derive(defmt::Format))]
224pub enum ModulationEncodeError {
225 BufferTooSmall,
226 SyncWordTooLong,
227}
228
229/// Errors from modulation param parsing.
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231#[cfg_attr(feature = "defmt", derive(defmt::Format))]
232pub enum ModulationParseError {
233 /// Declared length did not match the expected total for this
234 /// modulation shape.
235 WrongLength { expected: usize, actual: usize },
236 /// Payload shorter than the modulation's fixed prefix.
237 TooShort,
238 /// An enum field or reserved byte carried an invalid value.
239 InvalidField,
240 /// `modulation_id` byte does not match any defined modulation.
241 UnknownModulation,
242}
243
244/// Errors from `Info` encode/decode.
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246#[cfg_attr(feature = "defmt", derive(defmt::Format))]
247pub enum InfoParseError {
248 TooShort,
249 BufferTooSmall,
250 InvalidField,
251}