donglora-protocol 1.1.0

DongLoRa wire protocol types and COBS framing — shared between firmware and host crates
Documentation
//! 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,
}