Skip to main content

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}