use crate::{InfoParseError, MAX_MCU_UID_LEN, MAX_RADIO_UID_LEN};
pub mod cap {
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;
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;
pub const MULTI_CLIENT: u64 = 1 << 32;
}
#[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 {
pub const MIN_WIRE_SIZE: usize = 37;
pub fn chip_id(&self) -> Option<crate::RadioChipId> {
crate::RadioChipId::from_u16(self.radio_chip_id)
}
pub fn supports(&self, mask: u64) -> bool {
self.capability_bitmap & mask != 0
}
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, InfoParseError> {
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)
}
pub fn decode(buf: &[u8]) -> Result<Self, InfoParseError> {
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);
}
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,
supported_sf_bitmap: 0x1FE0,
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() {
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, 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, ];
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();
let truncated = n - 4;
assert!(Info::decode(&buf[..truncated]).is_err());
}
}