Skip to main content

donglora_protocol/
info.rs

1//! `GET_INFO` response payload (`PROTOCOL.md §6.2`).
2//!
3//! Carries the device's identity, firmware version, radio type,
4//! capability bitmaps, queue depths, frequency range, TX-power range,
5//! and optional MCU / radio unique identifiers. All fields are stable
6//! for the lifetime of the session; hosts MAY cache the whole struct.
7
8use crate::{InfoParseError, MAX_MCU_UID_LEN, MAX_RADIO_UID_LEN};
9
10/// Capability bitmap bit positions (`PROTOCOL.md §9`).
11///
12/// Hosts should consult these before attempting `SET_CONFIG` with a
13/// particular modulation. Undefined bits MUST be zero in v1.0 and MUST
14/// be ignored by hosts that don't understand them.
15pub mod cap {
16    // Modulations (bits 0–15)
17    //
18    // Bit 0 is written as a bare `1` (not `1 << 0`) because `1 << 0` and
19    // `1 >> 0` both evaluate to 1 — the two-operator form produces an
20    // equivalent mutation target that no test can distinguish. Every
21    // other bit keeps the `1 << N` form for clarity.
22    pub const LORA: u64 = 1;
23    pub const FSK: u64 = 1 << 1;
24    pub const GFSK: u64 = 1 << 2;
25    pub const LR_FHSS: u64 = 1 << 3;
26    pub const FLRC: u64 = 1 << 4;
27    pub const MSK: u64 = 1 << 5;
28    pub const GMSK: u64 = 1 << 6;
29    pub const BLE_COMPATIBLE: u64 = 1 << 7;
30
31    // Radio features (bits 16–31)
32    pub const CAD_BEFORE_TX: u64 = 1 << 16;
33    pub const IQ_INVERSION: u64 = 1 << 17;
34    pub const RANGING: u64 = 1 << 18;
35    pub const GNSS_SCAN: u64 = 1 << 19;
36    pub const WIFI_MAC_SCAN: u64 = 1 << 20;
37    pub const SPECTRAL_SCAN: u64 = 1 << 21;
38    pub const FULL_DUPLEX: u64 = 1 << 22;
39
40    // Protocol features (bits 32–47)
41    pub const MULTI_CLIENT: u64 = 1 << 32;
42}
43
44/// `GET_INFO` response payload.
45///
46/// `mcu_uid` and `radio_uid` are fixed-capacity arrays (`MAX_MCU_UID_LEN`
47/// and `MAX_RADIO_UID_LEN` respectively). Only the first `*_len` bytes
48/// are meaningful on the wire; the rest are zero-padded and ignored.
49///
50/// `radio_chip_id` is stored as the raw u16. Use `chip_id()` to project
51/// it into the `RadioChipId` enum when convenient; unassigned values
52/// stay representable so a newer device advertising a chip this codec
53/// hasn't heard of still round-trips cleanly.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[cfg_attr(feature = "defmt", derive(defmt::Format))]
56pub struct Info {
57    pub proto_major: u8,
58    pub proto_minor: u8,
59    pub fw_major: u8,
60    pub fw_minor: u8,
61    pub fw_patch: u8,
62    pub radio_chip_id: u16,
63    pub capability_bitmap: u64,
64    pub supported_sf_bitmap: u16,
65    pub supported_bw_bitmap: u16,
66    pub max_payload_bytes: u16,
67    pub rx_queue_capacity: u16,
68    pub tx_queue_capacity: u16,
69    pub freq_min_hz: u32,
70    pub freq_max_hz: u32,
71    pub tx_power_min_dbm: i8,
72    pub tx_power_max_dbm: i8,
73    pub mcu_uid_len: u8,
74    pub mcu_uid: [u8; MAX_MCU_UID_LEN],
75    pub radio_uid_len: u8,
76    pub radio_uid: [u8; MAX_RADIO_UID_LEN],
77}
78
79impl Info {
80    /// Size of the fixed-layout prefix (through `radio_uid_len`'s byte).
81    /// The spec locates `radio_uid_len` at offset `36 + mcu_uid_len`; the
82    /// 37 bytes of fixed fields before `mcu_uid` plus 1 for the
83    /// `radio_uid_len` byte gives the absolute minimum wire size (with
84    /// `mcu_uid_len = 0` and `radio_uid_len = 0`).
85    pub const MIN_WIRE_SIZE: usize = 37;
86
87    /// Project `radio_chip_id` into the enum. Returns `None` for
88    /// unassigned values so callers can decide how to handle them.
89    pub fn chip_id(&self) -> Option<crate::RadioChipId> {
90        crate::RadioChipId::from_u16(self.radio_chip_id)
91    }
92
93    /// True if the device advertises a given capability bit.
94    pub fn supports(&self, mask: u64) -> bool {
95        self.capability_bitmap & mask != 0
96    }
97
98    /// Encode into `buf`. Returns the number of bytes written
99    /// (`37 + mcu_uid_len + radio_uid_len`).
100    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, InfoParseError> {
101        let mcu_n = self.mcu_uid_len as usize;
102        let radio_n = self.radio_uid_len as usize;
103        if mcu_n > MAX_MCU_UID_LEN || radio_n > MAX_RADIO_UID_LEN {
104            return Err(InfoParseError::InvalidField);
105        }
106        let total = Self::MIN_WIRE_SIZE + mcu_n + radio_n;
107        if buf.len() < total {
108            return Err(InfoParseError::BufferTooSmall);
109        }
110        buf[0] = self.proto_major;
111        buf[1] = self.proto_minor;
112        buf[2] = self.fw_major;
113        buf[3] = self.fw_minor;
114        buf[4] = self.fw_patch;
115        buf[5..7].copy_from_slice(&self.radio_chip_id.to_le_bytes());
116        buf[7..15].copy_from_slice(&self.capability_bitmap.to_le_bytes());
117        buf[15..17].copy_from_slice(&self.supported_sf_bitmap.to_le_bytes());
118        buf[17..19].copy_from_slice(&self.supported_bw_bitmap.to_le_bytes());
119        buf[19..21].copy_from_slice(&self.max_payload_bytes.to_le_bytes());
120        buf[21..23].copy_from_slice(&self.rx_queue_capacity.to_le_bytes());
121        buf[23..25].copy_from_slice(&self.tx_queue_capacity.to_le_bytes());
122        buf[25..29].copy_from_slice(&self.freq_min_hz.to_le_bytes());
123        buf[29..33].copy_from_slice(&self.freq_max_hz.to_le_bytes());
124        buf[33] = self.tx_power_min_dbm as u8;
125        buf[34] = self.tx_power_max_dbm as u8;
126        buf[35] = self.mcu_uid_len;
127        buf[36..36 + mcu_n].copy_from_slice(&self.mcu_uid[..mcu_n]);
128        let radio_len_idx = 36 + mcu_n;
129        buf[radio_len_idx] = self.radio_uid_len;
130        let radio_start = radio_len_idx + 1;
131        buf[radio_start..radio_start + radio_n].copy_from_slice(&self.radio_uid[..radio_n]);
132        Ok(total)
133    }
134
135    /// Decode from `buf`. The full slice must be the `GET_INFO` payload.
136    pub fn decode(buf: &[u8]) -> Result<Self, InfoParseError> {
137        if buf.len() < Self::MIN_WIRE_SIZE {
138            return Err(InfoParseError::TooShort);
139        }
140        let mcu_uid_len = buf[35];
141        if mcu_uid_len as usize > MAX_MCU_UID_LEN {
142            return Err(InfoParseError::InvalidField);
143        }
144        let mcu_n = mcu_uid_len as usize;
145        let radio_len_idx = 36 + mcu_n;
146        if buf.len() < radio_len_idx + 1 {
147            return Err(InfoParseError::TooShort);
148        }
149        let radio_uid_len = buf[radio_len_idx];
150        if radio_uid_len as usize > MAX_RADIO_UID_LEN {
151            return Err(InfoParseError::InvalidField);
152        }
153        let radio_n = radio_uid_len as usize;
154        let expected_total = Self::MIN_WIRE_SIZE + mcu_n + radio_n;
155        if buf.len() < expected_total {
156            return Err(InfoParseError::TooShort);
157        }
158        // Minor-version extensions may append fields; trailing bytes are
159        // allowed. But we at least require the declared UIDs to fit.
160        let mut mcu_uid = [0u8; MAX_MCU_UID_LEN];
161        mcu_uid[..mcu_n].copy_from_slice(&buf[36..36 + mcu_n]);
162        let radio_start = radio_len_idx + 1;
163        let mut radio_uid = [0u8; MAX_RADIO_UID_LEN];
164        radio_uid[..radio_n].copy_from_slice(&buf[radio_start..radio_start + radio_n]);
165
166        Ok(Self {
167            proto_major: buf[0],
168            proto_minor: buf[1],
169            fw_major: buf[2],
170            fw_minor: buf[3],
171            fw_patch: buf[4],
172            radio_chip_id: u16::from_le_bytes([buf[5], buf[6]]),
173            capability_bitmap: u64::from_le_bytes([
174                buf[7], buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14],
175            ]),
176            supported_sf_bitmap: u16::from_le_bytes([buf[15], buf[16]]),
177            supported_bw_bitmap: u16::from_le_bytes([buf[17], buf[18]]),
178            max_payload_bytes: u16::from_le_bytes([buf[19], buf[20]]),
179            rx_queue_capacity: u16::from_le_bytes([buf[21], buf[22]]),
180            tx_queue_capacity: u16::from_le_bytes([buf[23], buf[24]]),
181            freq_min_hz: u32::from_le_bytes([buf[25], buf[26], buf[27], buf[28]]),
182            freq_max_hz: u32::from_le_bytes([buf[29], buf[30], buf[31], buf[32]]),
183            tx_power_min_dbm: buf[33] as i8,
184            tx_power_max_dbm: buf[34] as i8,
185            mcu_uid_len,
186            mcu_uid,
187            radio_uid_len,
188            radio_uid,
189        })
190    }
191}
192
193#[cfg(test)]
194#[allow(clippy::panic, clippy::unwrap_used)]
195mod tests {
196    use super::*;
197    use crate::RadioChipId;
198
199    fn sample() -> Info {
200        let mut mcu = [0u8; MAX_MCU_UID_LEN];
201        mcu[..8].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67]);
202        Info {
203            proto_major: 1,
204            proto_minor: 0,
205            fw_major: 0,
206            fw_minor: 1,
207            fw_patch: 0,
208            radio_chip_id: RadioChipId::Sx1262.as_u16(),
209            capability_bitmap: cap::LORA | cap::FSK | cap::CAD_BEFORE_TX,
210            // Bits 5..12 = SF5..SF12
211            supported_sf_bitmap: 0x1FE0,
212            // Bits 0..9 = all sub-GHz BW enum values
213            supported_bw_bitmap: 0x03FF,
214            max_payload_bytes: 255,
215            rx_queue_capacity: 64,
216            tx_queue_capacity: 16,
217            freq_min_hz: 150_000_000,
218            freq_max_hz: 960_000_000,
219            tx_power_min_dbm: -9,
220            tx_power_max_dbm: 22,
221            mcu_uid_len: 8,
222            mcu_uid: mcu,
223            radio_uid_len: 0,
224            radio_uid: [0u8; MAX_RADIO_UID_LEN],
225        }
226    }
227
228    #[test]
229    fn appendix_c22_encode_matches_spec() {
230        // PROTOCOL.md §C.2.2 — SX1262 board description. The spec's
231        // pre-COBS payload (after stripping the 3-byte response header:
232        // type 0x80, tag 0x0002) is:
233        //
234        //   01 00 00 01 00 02 00 03 00 01 00 00 00 00 00 00
235        //   E0 1F FF 03 FF 00 40 00 10 00 80 D1 F0 08 00 70
236        //   38 39 F7 16 08 DE AD BE EF 01 23 45 67 00
237        //
238        // 45 bytes: 37 fixed + 8 mcu_uid + 0 radio_uid.
239        let mut buf = [0u8; 128];
240        let n = sample().encode(&mut buf).unwrap();
241        assert_eq!(n, 45);
242        let expected: [u8; 45] = [
243            0x01, 0x00, 0x00, 0x01, 0x00, // proto + fw
244            0x02, 0x00, // radio_chip_id = 0x0002 (SX1262)
245            0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // capability_bitmap
246            0xE0, 0x1F, // supported_sf_bitmap = 0x1FE0
247            0xFF, 0x03, // supported_bw_bitmap = 0x03FF
248            0xFF, 0x00, // max_payload_bytes = 255
249            0x40, 0x00, // rx_queue_capacity = 64
250            0x10, 0x00, // tx_queue_capacity = 16
251            0x80, 0xD1, 0xF0, 0x08, // freq_min_hz = 150_000_000
252            0x00, 0x70, 0x38, 0x39, // freq_max_hz = 960_000_000
253            0xF7, // tx_power_min_dbm = -9
254            0x16, // tx_power_max_dbm = 22
255            0x08, // mcu_uid_len = 8
256            0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67, // mcu_uid
257            0x00, // radio_uid_len = 0
258        ];
259        assert_eq!(&buf[..n], &expected);
260    }
261
262    #[test]
263    fn roundtrip() {
264        let info = sample();
265        let mut buf = [0u8; 128];
266        let n = info.encode(&mut buf).unwrap();
267        let decoded = Info::decode(&buf[..n]).unwrap();
268        assert_eq!(decoded, info);
269    }
270
271    #[test]
272    fn chip_id_projection() {
273        let info = sample();
274        assert_eq!(info.chip_id(), Some(RadioChipId::Sx1262));
275
276        let unknown = Info {
277            radio_chip_id: 0xFFFF,
278            ..info
279        };
280        assert_eq!(unknown.chip_id(), None);
281    }
282
283    #[test]
284    fn supports_bits() {
285        let info = sample();
286        assert!(info.supports(cap::LORA));
287        assert!(info.supports(cap::FSK));
288        assert!(info.supports(cap::CAD_BEFORE_TX));
289        assert!(!info.supports(cap::MULTI_CLIENT));
290        assert!(!info.supports(cap::LR_FHSS));
291    }
292
293    #[test]
294    fn rejects_oversized_uids() {
295        let mut info = sample();
296        info.mcu_uid_len = (MAX_MCU_UID_LEN + 1) as u8;
297        let mut buf = [0u8; 128];
298        assert!(info.encode(&mut buf).is_err());
299
300        info.mcu_uid_len = 0;
301        info.radio_uid_len = (MAX_RADIO_UID_LEN + 1) as u8;
302        assert!(info.encode(&mut buf).is_err());
303    }
304
305    #[test]
306    fn rejects_short_buffer_on_decode() {
307        assert!(matches!(
308            Info::decode(&[0u8; 10]),
309            Err(InfoParseError::TooShort)
310        ));
311    }
312
313    #[test]
314    fn rejects_declared_uid_overrun() {
315        let mut buf = [0u8; 128];
316        let mut info = sample();
317        info.mcu_uid_len = 16;
318        let n = info.encode(&mut buf).unwrap();
319        // Shorten the wire beyond what mcu_uid_len claims.
320        let truncated = n - 4;
321        assert!(Info::decode(&buf[..truncated]).is_err());
322    }
323}