donglora-protocol 1.1.0

DongLoRa wire protocol types and COBS framing — shared between firmware and host crates
Documentation
//! Error codes carried in `ERR` frames (`PROTOCOL.md §7`).
//!
//! Codes are u16 little-endian on the wire. The containing frame's tag
//! distinguishes context: non-zero tag is a synchronous error tied to a
//! specific command; tag `0x0000` is an asynchronous fault not tied to
//! any command. The two namespaces share one u16 space:
//!
//! - `0x0000..=0x00FF` — synchronous-typical codes
//! - `0x0100..=0x01FF` — asynchronous-typical codes
//! - `0x0200..=0xFFFF` — reserved for future extensions
//!
//! Any code MAY appear in either context when it fits semantically; the
//! split is convention, not enforcement. `ERADIO` for instance is listed
//! in the async table but is emitted synchronously when a hardware fault
//! aborts a specific command.

/// Wire-level error code.
///
/// Parsing an unknown code yields `ErrorCode::Unknown(raw)` so hosts can
/// round-trip frames they don't fully understand (useful when a device
/// reports a newer minor-version error code).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum ErrorCode {
    /// `0x0001` — a parameter value is out of range or invalid.
    EParam,
    /// `0x0002` — payload length is wrong for the command or modulation.
    ELength,
    /// `0x0003` — command requires `CONFIGURED`; device is `UNCONFIGURED`.
    ENotConfigured,
    /// `0x0004` — requested modulation is not supported by this chip.
    EModulation,
    /// `0x0005` — unknown command type byte.
    EUnknownCmd,
    /// `0x0006` — transient: TX queue is full. Retry after a `TX_DONE`.
    EBusy,

    /// `0x0101` — radio SPI error or unexpected hardware state.
    ERadio,
    /// `0x0102` — inbound frame had bad CRC, bad COBS, or wrong length.
    EFrame,
    /// `0x0103` — firmware encountered an unexpected internal condition.
    EInternal,

    /// Any code not assigned in v1.0. Preserves the raw wire value so
    /// parsers don't drop information they don't understand.
    Unknown(u16),
}

impl ErrorCode {
    /// Wire-form u16.
    pub const fn as_u16(self) -> u16 {
        match self {
            Self::EParam => 0x0001,
            Self::ELength => 0x0002,
            Self::ENotConfigured => 0x0003,
            Self::EModulation => 0x0004,
            Self::EUnknownCmd => 0x0005,
            Self::EBusy => 0x0006,
            Self::ERadio => 0x0101,
            Self::EFrame => 0x0102,
            Self::EInternal => 0x0103,
            Self::Unknown(raw) => raw,
        }
    }

    /// Parse a wire u16. Assigned codes return their named variant;
    /// anything else becomes `Unknown(raw)`.
    pub const fn from_u16(v: u16) -> Self {
        match v {
            0x0001 => Self::EParam,
            0x0002 => Self::ELength,
            0x0003 => Self::ENotConfigured,
            0x0004 => Self::EModulation,
            0x0005 => Self::EUnknownCmd,
            0x0006 => Self::EBusy,
            0x0101 => Self::ERadio,
            0x0102 => Self::EFrame,
            0x0103 => Self::EInternal,
            other => Self::Unknown(other),
        }
    }

    /// Whether this code lives in the asynchronous-typical range
    /// (`0x0100..=0x01FF`). Does not imply the containing frame's tag;
    /// that's a separate wire-level fact.
    pub const fn is_async_range(self) -> bool {
        let v = self.as_u16();
        v >= 0x0100 && v <= 0x01FF
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn canonical_sync_values() {
        assert_eq!(ErrorCode::EParam.as_u16(), 0x0001);
        assert_eq!(ErrorCode::ELength.as_u16(), 0x0002);
        assert_eq!(ErrorCode::ENotConfigured.as_u16(), 0x0003);
        assert_eq!(ErrorCode::EModulation.as_u16(), 0x0004);
        assert_eq!(ErrorCode::EUnknownCmd.as_u16(), 0x0005);
        assert_eq!(ErrorCode::EBusy.as_u16(), 0x0006);
    }

    #[test]
    fn canonical_async_values() {
        assert_eq!(ErrorCode::ERadio.as_u16(), 0x0101);
        assert_eq!(ErrorCode::EFrame.as_u16(), 0x0102);
        assert_eq!(ErrorCode::EInternal.as_u16(), 0x0103);
    }

    #[test]
    fn assigned_roundtrip() {
        let all = [
            ErrorCode::EParam,
            ErrorCode::ELength,
            ErrorCode::ENotConfigured,
            ErrorCode::EModulation,
            ErrorCode::EUnknownCmd,
            ErrorCode::EBusy,
            ErrorCode::ERadio,
            ErrorCode::EFrame,
            ErrorCode::EInternal,
        ];
        for code in all {
            assert_eq!(ErrorCode::from_u16(code.as_u16()), code);
        }
    }

    #[test]
    fn unknown_preserves_raw() {
        let unusual = [0x0000u16, 0x0007, 0x0100, 0x0200, 0xFFFF];
        for raw in unusual {
            let c = ErrorCode::from_u16(raw);
            assert_eq!(c.as_u16(), raw);
            assert!(matches!(c, ErrorCode::Unknown(_)));
        }
    }

    #[test]
    fn is_async_range_boundaries() {
        assert!(!ErrorCode::EParam.is_async_range());
        assert!(!ErrorCode::EBusy.is_async_range());
        assert!(ErrorCode::ERadio.is_async_range());
        assert!(ErrorCode::EInternal.is_async_range());
        // 0x00FF and 0x0200 are outside the async band.
        assert!(!ErrorCode::Unknown(0x00FF).is_async_range());
        assert!(!ErrorCode::Unknown(0x0200).is_async_range());
        assert!(ErrorCode::Unknown(0x01FF).is_async_range());
    }
}