1use crate::{InfoParseError, MAX_MCU_UID_LEN, MAX_RADIO_UID_LEN};
9
10pub mod cap {
16 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 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 pub const MULTI_CLIENT: u64 = 1 << 32;
42}
43
44#[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 pub const MIN_WIRE_SIZE: usize = 37;
86
87 pub fn chip_id(&self) -> Option<crate::RadioChipId> {
90 crate::RadioChipId::from_u16(self.radio_chip_id)
91 }
92
93 pub fn supports(&self, mask: u64) -> bool {
95 self.capability_bitmap & mask != 0
96 }
97
98 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 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 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 supported_sf_bitmap: 0x1FE0,
212 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 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, 0x02, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0xFF, 0x03, 0xFF, 0x00, 0x40, 0x00, 0x10, 0x00, 0x80, 0xD1, 0xF0, 0x08, 0x00, 0x70, 0x38, 0x39, 0xF7, 0x16, 0x08, 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x00, ];
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 let truncated = n - 4;
321 assert!(Info::decode(&buf[..truncated]).is_err());
322 }
323}