//! `GET_INFO` response payload (`PROTOCOL.md §6.2`). //! //! Carries the device's identity, firmware version, radio type, //! capability bitmaps, queue depths, frequency range, TX-power range, //! and optional MCU / radio unique identifiers. All fields are stable //! for the lifetime of the session; hosts MAY cache the whole struct. use crate::{InfoParseError, MAX_MCU_UID_LEN, MAX_RADIO_UID_LEN}; /// Capability bitmap bit positions (`PROTOCOL.md §9`). /// /// Hosts should consult these before attempting `SET_CONFIG` with a /// particular modulation. Undefined bits MUST be zero in v1.0 and MUST /// be ignored by hosts that don't understand them. pub mod cap { // Modulations (bits 0–15) // // Bit 0 is written as a bare `1` (not `1 << 0`) because `1 << 0` and // `1 >> 0` both evaluate to 1 — the two-operator form produces an // equivalent mutation target that no test can distinguish. Every // other bit keeps the `1 << N` form for clarity. pub const LORA: u64 = 1; pub const FSK: u64 = 1 << 1; pub const GFSK: u64 = 1 << 2; pub const LR_FHSS: u64 = 1 << 3; pub const FLRC: u64 = 1 << 4; pub const MSK: u64 = 1 << 5; pub const GMSK: u64 = 1 << 6; pub const BLE_COMPATIBLE: u64 = 1 << 7; // Radio features (bits 16–31) pub const CAD_BEFORE_TX: u64 = 1 << 16; pub const IQ_INVERSION: u64 = 1 << 17; pub const RANGING: u64 = 1 << 18; pub const GNSS_SCAN: u64 = 1 << 19; pub const WIFI_MAC_SCAN: u64 = 1 << 20; pub const SPECTRAL_SCAN: u64 = 1 << 21; pub const FULL_DUPLEX: u64 = 1 << 22; // Protocol features (bits 32–47) pub const MULTI_CLIENT: u64 = 1 << 32; } /// `GET_INFO` response payload. /// /// `mcu_uid` and `radio_uid` are fixed-capacity arrays (`MAX_MCU_UID_LEN` /// and `MAX_RADIO_UID_LEN` respectively). Only the first `*_len` bytes /// are meaningful on the wire; the rest are zero-padded and ignored. /// /// `radio_chip_id` is stored as the raw u16. Use `chip_id()` to project /// it into the `RadioChipId` enum when convenient; unassigned values /// stay representable so a newer device advertising a chip this codec /// hasn't heard of still round-trips cleanly. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Info { pub proto_major: u8, pub proto_minor: u8, pub fw_major: u8, pub fw_minor: u8, pub fw_patch: u8, pub radio_chip_id: u16, pub capability_bitmap: u64, pub supported_sf_bitmap: u16, pub supported_bw_bitmap: u16, pub max_payload_bytes: u16, pub rx_queue_capacity: u16, pub tx_queue_capacity: u16, pub freq_min_hz: u32, pub freq_max_hz: u32, pub tx_power_min_dbm: i8, pub tx_power_max_dbm: i8, pub mcu_uid_len: u8, pub mcu_uid: [u8; MAX_MCU_UID_LEN], pub radio_uid_len: u8, pub radio_uid: [u8; MAX_RADIO_UID_LEN], } impl Info { /// Size of the fixed-layout prefix (through `radio_uid_len`'s byte). /// The spec locates `radio_uid_len` at offset `36 + mcu_uid_len`; the /// 37 bytes of fixed fields before `mcu_uid` plus 1 for the /// `radio_uid_len` byte gives the absolute minimum wire size (with /// `mcu_uid_len = 0` and `radio_uid_len = 0`). pub const MIN_WIRE_SIZE: usize = 37; /// Project `radio_chip_id` into the enum. Returns `None` for /// unassigned values so callers can decide how to handle them. pub fn chip_id(&self) -> Option { crate::RadioChipId::from_u16(self.radio_chip_id) } /// True if the device advertises a given capability bit. pub fn supports(&self, mask: u64) -> bool { self.capability_bitmap & mask != 0 } /// Encode into `buf`. Returns the number of bytes written /// (`37 + mcu_uid_len + radio_uid_len`). pub fn encode(&self, buf: &mut [u8]) -> Result { let mcu_n = self.mcu_uid_len as usize; let radio_n = self.radio_uid_len as usize; if mcu_n > MAX_MCU_UID_LEN || radio_n > MAX_RADIO_UID_LEN { return Err(InfoParseError::InvalidField); } let total = Self::MIN_WIRE_SIZE + mcu_n + radio_n; if buf.len() < total { return Err(InfoParseError::BufferTooSmall); } buf[0] = self.proto_major; buf[1] = self.proto_minor; buf[2] = self.fw_major; buf[3] = self.fw_minor; buf[4] = self.fw_patch; buf[5..7].copy_from_slice(&self.radio_chip_id.to_le_bytes()); buf[7..15].copy_from_slice(&self.capability_bitmap.to_le_bytes()); buf[15..17].copy_from_slice(&self.supported_sf_bitmap.to_le_bytes()); buf[17..19].copy_from_slice(&self.supported_bw_bitmap.to_le_bytes()); buf[19..21].copy_from_slice(&self.max_payload_bytes.to_le_bytes()); buf[21..23].copy_from_slice(&self.rx_queue_capacity.to_le_bytes()); buf[23..25].copy_from_slice(&self.tx_queue_capacity.to_le_bytes()); buf[25..29].copy_from_slice(&self.freq_min_hz.to_le_bytes()); buf[29..33].copy_from_slice(&self.freq_max_hz.to_le_bytes()); buf[33] = self.tx_power_min_dbm as u8; buf[34] = self.tx_power_max_dbm as u8; buf[35] = self.mcu_uid_len; buf[36..36 + mcu_n].copy_from_slice(&self.mcu_uid[..mcu_n]); let radio_len_idx = 36 + mcu_n; buf[radio_len_idx] = self.radio_uid_len; let radio_start = radio_len_idx + 1; buf[radio_start..radio_start + radio_n].copy_from_slice(&self.radio_uid[..radio_n]); Ok(total) } /// Decode from `buf`. The full slice must be the `GET_INFO` payload. pub fn decode(buf: &[u8]) -> Result { if buf.len() < Self::MIN_WIRE_SIZE { return Err(InfoParseError::TooShort); } let mcu_uid_len = buf[35]; if mcu_uid_len as usize > MAX_MCU_UID_LEN { return Err(InfoParseError::InvalidField); } let mcu_n = mcu_uid_len as usize; let radio_len_idx = 36 + mcu_n; if buf.len() < radio_len_idx + 1 { return Err(InfoParseError::TooShort); } let radio_uid_len = buf[radio_len_idx]; if radio_uid_len as usize > MAX_RADIO_UID_LEN { return Err(InfoParseError::InvalidField); } let radio_n = radio_uid_len as usize; let expected_total = Self::MIN_WIRE_SIZE + mcu_n + radio_n; if buf.len() < expected_total { return Err(InfoParseError::TooShort); } // Minor-version extensions may append fields; trailing bytes are // allowed. But we at least require the declared UIDs to fit. let mut mcu_uid = [0u8; MAX_MCU_UID_LEN]; mcu_uid[..mcu_n].copy_from_slice(&buf[36..36 + mcu_n]); let radio_start = radio_len_idx + 1; let mut radio_uid = [0u8; MAX_RADIO_UID_LEN]; radio_uid[..radio_n].copy_from_slice(&buf[radio_start..radio_start + radio_n]); Ok(Self { proto_major: buf[0], proto_minor: buf[1], fw_major: buf[2], fw_minor: buf[3], fw_patch: buf[4], radio_chip_id: u16::from_le_bytes([buf[5], buf[6]]), capability_bitmap: u64::from_le_bytes([ buf[7], buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], ]), supported_sf_bitmap: u16::from_le_bytes([buf[15], buf[16]]), supported_bw_bitmap: u16::from_le_bytes([buf[17], buf[18]]), max_payload_bytes: u16::from_le_bytes([buf[19], buf[20]]), rx_queue_capacity: u16::from_le_bytes([buf[21], buf[22]]), tx_queue_capacity: u16::from_le_bytes([buf[23], buf[24]]), freq_min_hz: u32::from_le_bytes([buf[25], buf[26], buf[27], buf[28]]), freq_max_hz: u32::from_le_bytes([buf[29], buf[30], buf[31], buf[32]]), tx_power_min_dbm: buf[33] as i8, tx_power_max_dbm: buf[34] as i8, mcu_uid_len, mcu_uid, radio_uid_len, radio_uid, }) } } #[cfg(test)] #[allow(clippy::panic, clippy::unwrap_used)] mod tests { use super::*; use crate::RadioChipId; fn sample() -> Info { let mut mcu = [0u8; MAX_MCU_UID_LEN]; mcu[..8].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67]); Info { proto_major: 1, proto_minor: 0, fw_major: 0, fw_minor: 1, fw_patch: 0, radio_chip_id: RadioChipId::Sx1262.as_u16(), capability_bitmap: cap::LORA | cap::FSK | cap::CAD_BEFORE_TX, // Bits 5..12 = SF5..SF12 supported_sf_bitmap: 0x1FE0, // Bits 0..9 = all sub-GHz BW enum values supported_bw_bitmap: 0x03FF, max_payload_bytes: 255, rx_queue_capacity: 64, tx_queue_capacity: 16, freq_min_hz: 150_000_000, freq_max_hz: 960_000_000, tx_power_min_dbm: -9, tx_power_max_dbm: 22, mcu_uid_len: 8, mcu_uid: mcu, radio_uid_len: 0, radio_uid: [0u8; MAX_RADIO_UID_LEN], } } #[test] fn appendix_c22_encode_matches_spec() { // PROTOCOL.md §C.2.2 — SX1262 board description. The spec's // pre-COBS payload (after stripping the 3-byte response header: // type 0x80, tag 0x0002) is: // // 01 00 00 01 00 02 00 03 00 01 00 00 00 00 00 00 // E0 1F FF 03 FF 00 40 00 10 00 80 D1 F0 08 00 70 // 38 39 F7 16 08 DE AD BE EF 01 23 45 67 00 // // 45 bytes: 37 fixed + 8 mcu_uid + 0 radio_uid. let mut buf = [0u8; 128]; let n = sample().encode(&mut buf).unwrap(); assert_eq!(n, 45); let expected: [u8; 45] = [ 0x01, 0x00, 0x00, 0x01, 0x00, // proto + fw 0x02, 0x00, // radio_chip_id = 0x0002 (SX1262) 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // capability_bitmap 0xE0, 0x1F, // supported_sf_bitmap = 0x1FE0 0xFF, 0x03, // supported_bw_bitmap = 0x03FF 0xFF, 0x00, // max_payload_bytes = 255 0x40, 0x00, // rx_queue_capacity = 64 0x10, 0x00, // tx_queue_capacity = 16 0x80, 0xD1, 0xF0, 0x08, // freq_min_hz = 150_000_000 0x00, 0x70, 0x38, 0x39, // freq_max_hz = 960_000_000 0xF7, // tx_power_min_dbm = -9 0x16, // tx_power_max_dbm = 22 0x08, // mcu_uid_len = 8 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67, // mcu_uid 0x00, // radio_uid_len = 0 ]; assert_eq!(&buf[..n], &expected); } #[test] fn roundtrip() { let info = sample(); let mut buf = [0u8; 128]; let n = info.encode(&mut buf).unwrap(); let decoded = Info::decode(&buf[..n]).unwrap(); assert_eq!(decoded, info); } #[test] fn chip_id_projection() { let info = sample(); assert_eq!(info.chip_id(), Some(RadioChipId::Sx1262)); let unknown = Info { radio_chip_id: 0xFFFF, ..info }; assert_eq!(unknown.chip_id(), None); } #[test] fn supports_bits() { let info = sample(); assert!(info.supports(cap::LORA)); assert!(info.supports(cap::FSK)); assert!(info.supports(cap::CAD_BEFORE_TX)); assert!(!info.supports(cap::MULTI_CLIENT)); assert!(!info.supports(cap::LR_FHSS)); } #[test] fn rejects_oversized_uids() { let mut info = sample(); info.mcu_uid_len = (MAX_MCU_UID_LEN + 1) as u8; let mut buf = [0u8; 128]; assert!(info.encode(&mut buf).is_err()); info.mcu_uid_len = 0; info.radio_uid_len = (MAX_RADIO_UID_LEN + 1) as u8; assert!(info.encode(&mut buf).is_err()); } #[test] fn rejects_short_buffer_on_decode() { assert!(matches!( Info::decode(&[0u8; 10]), Err(InfoParseError::TooShort) )); } #[test] fn rejects_declared_uid_overrun() { let mut buf = [0u8; 128]; let mut info = sample(); info.mcu_uid_len = 16; let n = info.encode(&mut buf).unwrap(); // Shorten the wire beyond what mcu_uid_len claims. let truncated = n - 4; assert!(Info::decode(&buf[..truncated]).is_err()); } }