Skip to main content

donglora_protocol/
errors.rs

1//! Error codes carried in `ERR` frames (`PROTOCOL.md §7`).
2//!
3//! Codes are u16 little-endian on the wire. The containing frame's tag
4//! distinguishes context: non-zero tag is a synchronous error tied to a
5//! specific command; tag `0x0000` is an asynchronous fault not tied to
6//! any command. The two namespaces share one u16 space:
7//!
8//! - `0x0000..=0x00FF` — synchronous-typical codes
9//! - `0x0100..=0x01FF` — asynchronous-typical codes
10//! - `0x0200..=0xFFFF` — reserved for future extensions
11//!
12//! Any code MAY appear in either context when it fits semantically; the
13//! split is convention, not enforcement. `ERADIO` for instance is listed
14//! in the async table but is emitted synchronously when a hardware fault
15//! aborts a specific command.
16
17/// Wire-level error code.
18///
19/// Parsing an unknown code yields `ErrorCode::Unknown(raw)` so hosts can
20/// round-trip frames they don't fully understand (useful when a device
21/// reports a newer minor-version error code).
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[cfg_attr(feature = "defmt", derive(defmt::Format))]
24pub enum ErrorCode {
25    /// `0x0001` — a parameter value is out of range or invalid.
26    EParam,
27    /// `0x0002` — payload length is wrong for the command or modulation.
28    ELength,
29    /// `0x0003` — command requires `CONFIGURED`; device is `UNCONFIGURED`.
30    ENotConfigured,
31    /// `0x0004` — requested modulation is not supported by this chip.
32    EModulation,
33    /// `0x0005` — unknown command type byte.
34    EUnknownCmd,
35    /// `0x0006` — transient: TX queue is full. Retry after a `TX_DONE`.
36    EBusy,
37
38    /// `0x0101` — radio SPI error or unexpected hardware state.
39    ERadio,
40    /// `0x0102` — inbound frame had bad CRC, bad COBS, or wrong length.
41    EFrame,
42    /// `0x0103` — firmware encountered an unexpected internal condition.
43    EInternal,
44
45    /// Any code not assigned in v1.0. Preserves the raw wire value so
46    /// parsers don't drop information they don't understand.
47    Unknown(u16),
48}
49
50impl ErrorCode {
51    /// Wire-form u16.
52    pub const fn as_u16(self) -> u16 {
53        match self {
54            Self::EParam => 0x0001,
55            Self::ELength => 0x0002,
56            Self::ENotConfigured => 0x0003,
57            Self::EModulation => 0x0004,
58            Self::EUnknownCmd => 0x0005,
59            Self::EBusy => 0x0006,
60            Self::ERadio => 0x0101,
61            Self::EFrame => 0x0102,
62            Self::EInternal => 0x0103,
63            Self::Unknown(raw) => raw,
64        }
65    }
66
67    /// Parse a wire u16. Assigned codes return their named variant;
68    /// anything else becomes `Unknown(raw)`.
69    pub const fn from_u16(v: u16) -> Self {
70        match v {
71            0x0001 => Self::EParam,
72            0x0002 => Self::ELength,
73            0x0003 => Self::ENotConfigured,
74            0x0004 => Self::EModulation,
75            0x0005 => Self::EUnknownCmd,
76            0x0006 => Self::EBusy,
77            0x0101 => Self::ERadio,
78            0x0102 => Self::EFrame,
79            0x0103 => Self::EInternal,
80            other => Self::Unknown(other),
81        }
82    }
83
84    /// Whether this code lives in the asynchronous-typical range
85    /// (`0x0100..=0x01FF`). Does not imply the containing frame's tag;
86    /// that's a separate wire-level fact.
87    pub const fn is_async_range(self) -> bool {
88        let v = self.as_u16();
89        v >= 0x0100 && v <= 0x01FF
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn canonical_sync_values() {
99        assert_eq!(ErrorCode::EParam.as_u16(), 0x0001);
100        assert_eq!(ErrorCode::ELength.as_u16(), 0x0002);
101        assert_eq!(ErrorCode::ENotConfigured.as_u16(), 0x0003);
102        assert_eq!(ErrorCode::EModulation.as_u16(), 0x0004);
103        assert_eq!(ErrorCode::EUnknownCmd.as_u16(), 0x0005);
104        assert_eq!(ErrorCode::EBusy.as_u16(), 0x0006);
105    }
106
107    #[test]
108    fn canonical_async_values() {
109        assert_eq!(ErrorCode::ERadio.as_u16(), 0x0101);
110        assert_eq!(ErrorCode::EFrame.as_u16(), 0x0102);
111        assert_eq!(ErrorCode::EInternal.as_u16(), 0x0103);
112    }
113
114    #[test]
115    fn assigned_roundtrip() {
116        let all = [
117            ErrorCode::EParam,
118            ErrorCode::ELength,
119            ErrorCode::ENotConfigured,
120            ErrorCode::EModulation,
121            ErrorCode::EUnknownCmd,
122            ErrorCode::EBusy,
123            ErrorCode::ERadio,
124            ErrorCode::EFrame,
125            ErrorCode::EInternal,
126        ];
127        for code in all {
128            assert_eq!(ErrorCode::from_u16(code.as_u16()), code);
129        }
130    }
131
132    #[test]
133    fn unknown_preserves_raw() {
134        let unusual = [0x0000u16, 0x0007, 0x0100, 0x0200, 0xFFFF];
135        for raw in unusual {
136            let c = ErrorCode::from_u16(raw);
137            assert_eq!(c.as_u16(), raw);
138            assert!(matches!(c, ErrorCode::Unknown(_)));
139        }
140    }
141
142    #[test]
143    fn is_async_range_boundaries() {
144        assert!(!ErrorCode::EParam.is_async_range());
145        assert!(!ErrorCode::EBusy.is_async_range());
146        assert!(ErrorCode::ERadio.is_async_range());
147        assert!(ErrorCode::EInternal.is_async_range());
148        // 0x00FF and 0x0200 are outside the async band.
149        assert!(!ErrorCode::Unknown(0x00FF).is_async_range());
150        assert!(!ErrorCode::Unknown(0x0200).is_async_range());
151        assert!(ErrorCode::Unknown(0x01FF).is_async_range());
152    }
153}