//! Vendor-neutral Qualcomm Atheros MIMO CSI transport and deterministic simulator. //! This is not a Qualcomm firmware ABI; see ADR-268/269. use serde::{Deserialize, Serialize}; use thiserror::Error; pub const QUALCOMM_CSI_MAGIC: u32 = 0x3153_4351; // "QCS1" little endian pub const QUALCOMM_CSI_VERSION: u8 = 1; pub const QUALCOMM_CSI_HEADER_LEN: usize = 72; pub const QUALCOMM_CSI_CRC_LEN: usize = 4; pub const QUALCOMM_CSI_MAX_FRAME_LEN: usize = 65_507; pub const QUALCOMM_CSI_MAX_ELEMENTS: usize = 16_384; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u8)] pub enum ReportKind { Csi = 1, Capabilities = 2, } impl TryFrom for ReportKind { type Error = CsiParseError; fn try_from(value: u8) -> Result { match value { 1 => Ok(Self::Csi), 2 => Ok(Self::Capabilities), _ => Err(CsiParseError::UnknownReportKind(value)), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u16)] pub enum ChipsetProfile { Qca9300 = 1, Qcn9074 = 2, Qcn9274 = 3, } impl TryFrom for ChipsetProfile { type Error = CsiParseError; fn try_from(value: u16) -> Result { match value { 1 => Ok(Self::Qca9300), 2 => Ok(Self::Qcn9074), 3 => Ok(Self::Qcn9274), _ => Err(CsiParseError::UnknownChipset(value)), } } } impl ChipsetProfile { pub fn name(self) -> &'static str { match self { Self::Qca9300 => "qca9300", Self::Qcn9074 => "qcn9074", Self::Qcn9274 => "qcn9274", } } pub fn max_chains(self) -> u8 { match self { Self::Qca9300 => 3, Self::Qcn9074 | Self::Qcn9274 => 4, } } pub fn max_bandwidth_mhz(self) -> u16 { match self { Self::Qca9300 => 40, Self::Qcn9074 | Self::Qcn9274 => 160, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u8)] pub enum ElementFormat { ComplexI16 = 1, ComplexF32 = 2, Bytes = 3, } impl TryFrom for ElementFormat { type Error = CsiParseError; fn try_from(value: u8) -> Result { match value { 1 => Ok(Self::ComplexI16), 2 => Ok(Self::ComplexF32), 3 => Ok(Self::Bytes), _ => Err(CsiParseError::UnknownElementFormat(value)), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u8)] pub enum PpduType { Ht = 1, Vht = 2, HeSu = 3, HeMu = 4, Eht = 5, } impl TryFrom for PpduType { type Error = CsiParseError; fn try_from(value: u8) -> Result { match value { 1 => Ok(Self::Ht), 2 => Ok(Self::Vht), 3 => Ok(Self::HeSu), 4 => Ok(Self::HeMu), 5 => Ok(Self::Eht), _ => Err(CsiParseError::UnknownPpduType(value)), } } } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct CsiFlags(pub u16); impl CsiFlags { pub const CALIBRATED: u16 = 1 << 0; pub const SATURATED: u16 = 1 << 1; pub const TIME_SYNCHRONIZED: u16 = 1 << 2; pub const DROPPED_PREDECESSOR: u16 = 1 << 3; pub const SYNTHETIC: u16 = 1 << 15; pub fn contains(self, flag: u16) -> bool { self.0 & flag != 0 } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum CsiPayload { ComplexI16 { rssi_dbm: Vec, values: Vec<[i16; 2]>, }, ComplexF32 { rssi_dbm: Vec, values: Vec<[f32; 2]>, }, Bytes(Vec), } impl CsiPayload { pub fn len(&self) -> usize { match self { Self::ComplexI16 { values, .. } => values.len(), Self::ComplexF32 { values, .. } => values.len(), Self::Bytes(values) => values.len(), } } pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn rssi_dbm(&self) -> &[i8] { match self { Self::ComplexI16 { rssi_dbm, .. } | Self::ComplexF32 { rssi_dbm, .. } => rssi_dbm, Self::Bytes(_) => &[], } } fn format(&self) -> ElementFormat { match self { Self::ComplexI16 { .. } => ElementFormat::ComplexI16, Self::ComplexF32 { .. } => ElementFormat::ComplexF32, Self::Bytes(_) => ElementFormat::Bytes, } } fn encoded_len(&self) -> usize { match self { Self::ComplexI16 { rssi_dbm, values } => rssi_dbm.len() + values.len() * 4, Self::ComplexF32 { rssi_dbm, values } => rssi_dbm.len() + values.len() * 8, Self::Bytes(values) => values.len(), } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CsiFrame { pub report_kind: ReportKind, pub sequence: u32, pub timestamp_us: u64, pub device_id: u64, pub chipset: ChipsetProfile, pub bandwidth_mhz: u16, pub center_freq_khz: u32, pub flags: CsiFlags, pub tx_count: u8, pub rx_count: u8, pub ppdu_type: PpduType, pub subcarrier_count: u16, pub noise_floor_dbm: i8, pub scale: f32, pub subcarrier_spacing_hz: f32, pub calibration_id: u32, pub payload: CsiPayload, } impl CsiFrame { pub fn to_bytes(&self) -> Result, CsiParseError> { self.validate()?; let payload_len = self.payload.encoded_len(); let frame_len = QUALCOMM_CSI_HEADER_LEN .checked_add(payload_len) .and_then(|n| n.checked_add(QUALCOMM_CSI_CRC_LEN)) .ok_or(CsiParseError::LengthOverflow)?; if frame_len > QUALCOMM_CSI_MAX_FRAME_LEN { return Err(CsiParseError::FrameTooLarge(frame_len)); } let mut out = Vec::with_capacity(frame_len); out.extend_from_slice(&QUALCOMM_CSI_MAGIC.to_le_bytes()); out.push(QUALCOMM_CSI_VERSION); out.push(self.report_kind as u8); out.extend_from_slice(&(QUALCOMM_CSI_HEADER_LEN as u16).to_le_bytes()); out.extend_from_slice(&(frame_len as u32).to_le_bytes()); out.extend_from_slice(&self.sequence.to_le_bytes()); out.extend_from_slice(&self.timestamp_us.to_le_bytes()); out.extend_from_slice(&self.device_id.to_le_bytes()); out.extend_from_slice(&(self.chipset as u16).to_le_bytes()); out.extend_from_slice(&self.bandwidth_mhz.to_le_bytes()); out.extend_from_slice(&self.center_freq_khz.to_le_bytes()); out.extend_from_slice(&self.flags.0.to_le_bytes()); out.push(self.tx_count); out.push(self.rx_count); out.push(self.payload.format() as u8); out.push(self.ppdu_type as u8); out.extend_from_slice(&self.subcarrier_count.to_le_bytes()); out.push(self.payload.rssi_dbm().len() as u8); out.push(self.noise_floor_dbm as u8); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&self.scale.to_le_bytes()); out.extend_from_slice(&self.subcarrier_spacing_hz.to_le_bytes()); out.extend_from_slice(&self.calibration_id.to_le_bytes()); out.extend_from_slice(&(payload_len as u32).to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); debug_assert_eq!(out.len(), QUALCOMM_CSI_HEADER_LEN); match &self.payload { CsiPayload::ComplexI16 { rssi_dbm, values } => { out.extend(rssi_dbm.iter().map(|v| *v as u8)); for [i, q] in values { out.extend_from_slice(&i.to_le_bytes()); out.extend_from_slice(&q.to_le_bytes()); } } CsiPayload::ComplexF32 { rssi_dbm, values } => { out.extend(rssi_dbm.iter().map(|v| *v as u8)); for [i, q] in values { out.extend_from_slice(&i.to_le_bytes()); out.extend_from_slice(&q.to_le_bytes()); } } CsiPayload::Bytes(values) => out.extend_from_slice(values), } out.extend_from_slice(&crc32_ieee(&out).to_le_bytes()); Ok(out) } pub fn from_bytes(input: &[u8]) -> Result<(Self, usize), CsiParseError> { if input.len() < QUALCOMM_CSI_HEADER_LEN { return Err(CsiParseError::InsufficientData { needed: QUALCOMM_CSI_HEADER_LEN, got: input.len(), }); } let magic = u32_at(input, 0); if magic != QUALCOMM_CSI_MAGIC { return Err(CsiParseError::InvalidMagic(magic)); } if input[4] != QUALCOMM_CSI_VERSION { return Err(CsiParseError::UnsupportedVersion(input[4])); } let report_kind = ReportKind::try_from(input[5])?; let header_len = u16_at(input, 6) as usize; if header_len != QUALCOMM_CSI_HEADER_LEN { return Err(CsiParseError::InvalidHeaderLength(header_len)); } let frame_len = u32_at(input, 8) as usize; if frame_len > QUALCOMM_CSI_MAX_FRAME_LEN { return Err(CsiParseError::FrameTooLarge(frame_len)); } if frame_len < header_len + QUALCOMM_CSI_CRC_LEN { return Err(CsiParseError::InvalidFrameLength(frame_len)); } if input.len() < frame_len { return Err(CsiParseError::InsufficientData { needed: frame_len, got: input.len(), }); } let expected_crc = u32_at(input, frame_len - 4); let actual_crc = crc32_ieee(&input[..frame_len - 4]); if expected_crc != actual_crc { return Err(CsiParseError::CrcMismatch { expected: expected_crc, actual: actual_crc, }); } let chipset = ChipsetProfile::try_from(u16_at(input, 32))?; let format = ElementFormat::try_from(input[44])?; let ppdu_type = PpduType::try_from(input[45])?; let tx_count = input[42]; let rx_count = input[43]; let subcarrier_count = u16_at(input, 46); let rssi_count = input[48] as usize; let payload_len = u32_at(input, 64) as usize; if header_len + payload_len + 4 != frame_len { return Err(CsiParseError::PayloadLengthMismatch); } let payload_bytes = &input[header_len..header_len + payload_len]; let elements = (tx_count as usize) .checked_mul(rx_count as usize) .and_then(|n| n.checked_mul(subcarrier_count as usize)) .ok_or(CsiParseError::LengthOverflow)?; let payload = match format { ElementFormat::Bytes => CsiPayload::Bytes(payload_bytes.to_vec()), ElementFormat::ComplexI16 => { if rssi_count > payload_bytes.len() || payload_bytes.len() - rssi_count != elements * 4 { return Err(CsiParseError::PayloadLengthMismatch); } let rssi_dbm = payload_bytes[..rssi_count] .iter() .map(|v| *v as i8) .collect(); let values = payload_bytes[rssi_count..] .chunks_exact(4) .map(|b| { [ i16::from_le_bytes([b[0], b[1]]), i16::from_le_bytes([b[2], b[3]]), ] }) .collect(); CsiPayload::ComplexI16 { rssi_dbm, values } } ElementFormat::ComplexF32 => { if rssi_count > payload_bytes.len() || payload_bytes.len() - rssi_count != elements * 8 { return Err(CsiParseError::PayloadLengthMismatch); } let rssi_dbm = payload_bytes[..rssi_count] .iter() .map(|v| *v as i8) .collect(); let mut values = Vec::with_capacity(elements); for b in payload_bytes[rssi_count..].chunks_exact(8) { let i = f32::from_le_bytes(b[0..4].try_into().unwrap()); let q = f32::from_le_bytes(b[4..8].try_into().unwrap()); if !i.is_finite() || !q.is_finite() { return Err(CsiParseError::NonFiniteValue); } values.push([i, q]); } CsiPayload::ComplexF32 { rssi_dbm, values } } }; let frame = Self { report_kind, sequence: u32_at(input, 12), timestamp_us: u64_at(input, 16), device_id: u64_at(input, 24), chipset, bandwidth_mhz: u16_at(input, 34), center_freq_khz: u32_at(input, 36), flags: CsiFlags(u16_at(input, 40)), tx_count, rx_count, ppdu_type, subcarrier_count, noise_floor_dbm: input[49] as i8, scale: f32_at(input, 52), subcarrier_spacing_hz: f32_at(input, 56), calibration_id: u32_at(input, 60), payload, }; frame.validate()?; Ok((frame, frame_len)) } fn validate(&self) -> Result<(), CsiParseError> { if !matches!(self.bandwidth_mhz, 20 | 40 | 80 | 160) || self.bandwidth_mhz > self.chipset.max_bandwidth_mhz() { return Err(CsiParseError::InvalidBandwidth(self.bandwidth_mhz)); } if self.tx_count == 0 || self.rx_count == 0 || self.tx_count > self.chipset.max_chains() || self.rx_count > self.chipset.max_chains() { return Err(CsiParseError::InvalidDimensions); } if !self.scale.is_finite() || self.scale <= 0.0 || !self.subcarrier_spacing_hz.is_finite() || self.subcarrier_spacing_hz <= 0.0 { return Err(CsiParseError::NonFiniteValue); } match (&self.report_kind, &self.payload) { (ReportKind::Csi, CsiPayload::ComplexI16 { rssi_dbm, values }) => { self.validate_csi(rssi_dbm, values.len()) } (ReportKind::Csi, CsiPayload::ComplexF32 { rssi_dbm, values }) => { if !values.iter().flatten().all(|v| v.is_finite()) { return Err(CsiParseError::NonFiniteValue); } self.validate_csi(rssi_dbm, values.len()) } (ReportKind::Capabilities, CsiPayload::Bytes(v)) if !v.is_empty() => Ok(()), _ => Err(CsiParseError::PayloadTypeMismatch), } } fn validate_csi(&self, rssi: &[i8], values: usize) -> Result<(), CsiParseError> { let expected = self.tx_count as usize * self.rx_count as usize * self.subcarrier_count as usize; if expected == 0 || expected > QUALCOMM_CSI_MAX_ELEMENTS { return Err(CsiParseError::InvalidDimensions); } if values != expected || rssi.len() != self.rx_count as usize { return Err(CsiParseError::PayloadLengthMismatch); } Ok(()) } } #[derive(Debug, Error, PartialEq)] pub enum CsiParseError { #[error("insufficient data: needed {needed}, got {got}")] InsufficientData { needed: usize, got: usize }, #[error("invalid magic {0:#010x}")] InvalidMagic(u32), #[error("unsupported version {0}")] UnsupportedVersion(u8), #[error("unknown report kind {0}")] UnknownReportKind(u8), #[error("unknown chipset profile {0}")] UnknownChipset(u16), #[error("unknown element format {0}")] UnknownElementFormat(u8), #[error("unknown PPDU type {0}")] UnknownPpduType(u8), #[error("invalid header length {0}")] InvalidHeaderLength(usize), #[error("invalid frame length {0}")] InvalidFrameLength(usize), #[error("frame too large: {0}")] FrameTooLarge(usize), #[error("length arithmetic overflow")] LengthOverflow, #[error("payload length mismatch")] PayloadLengthMismatch, #[error("payload type does not match report kind")] PayloadTypeMismatch, #[error("invalid MIMO dimensions")] InvalidDimensions, #[error("invalid bandwidth {0} MHz")] InvalidBandwidth(u16), #[error("non-finite or non-positive numeric metadata/value")] NonFiniteValue, #[error("CRC mismatch: expected {expected:#010x}, actual {actual:#010x}")] CrcMismatch { expected: u32, actual: u32 }, } pub mod simulator { use super::*; #[derive(Debug, Clone)] pub struct SimulatorConfig { pub seed: u64, pub device_id: u64, pub chipset: ChipsetProfile, pub bandwidth_mhz: u16, pub center_freq_khz: u32, pub tx_count: u8, pub rx_count: u8, pub subcarriers: u16, pub frame_period_us: u64, } impl Default for SimulatorConfig { fn default() -> Self { Self { seed: 0x5143_4143_5349_0001, device_id: 0x5255_5651_4341_3031, chipset: ChipsetProfile::Qca9300, bandwidth_mhz: 40, center_freq_khz: 5_210_000, tx_count: 2, rx_count: 3, subcarriers: 114, frame_period_us: 20_000, } } } pub struct QualcommCsiSimulator { config: SimulatorConfig, rng: u64, sequence: u32, timestamp_us: u64, motion_phase: f32, } impl QualcommCsiSimulator { pub fn new(config: SimulatorConfig) -> Result { let s = Self { rng: config.seed, config, sequence: 0, timestamp_us: 0, motion_phase: 0.0, }; s.csi_frame()?.validate()?; Ok(s) } pub fn capabilities_frame(&self) -> CsiFrame { self.base( ReportKind::Capabilities, CsiPayload::Bytes(vec![ 1, 1, self.config.chipset.max_chains(), 2, 1, 0b0000_1111, 3, 2, (self.config.subcarriers & 255) as u8, (self.config.subcarriers >> 8) as u8, ]), ) } pub fn next_frame(&mut self) -> CsiFrame { let frame = self.csi_frame().expect("validated simulator config"); self.sequence = self.sequence.wrapping_add(1); self.timestamp_us = self.timestamp_us.wrapping_add(self.config.frame_period_us); self.motion_phase += 0.037; frame } fn csi_frame(&self) -> Result { let mut rng = self.rng ^ self.sequence as u64; let count = self.config.tx_count as usize * self.config.rx_count as usize * self.config.subcarriers as usize; let values = (0..count) .map(|idx| { rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17; let noise = ((rng >> 48) as i16 % 24) as f32; let sc = (idx % self.config.subcarriers as usize) as f32; let chain = (idx / self.config.subcarriers as usize) as f32; let phase = sc * 0.031 + chain * 0.23 + self.motion_phase; [ ((phase.cos() * 1800.0) + noise) as i16, ((phase.sin() * 1800.0) - noise) as i16, ] }) .collect(); Ok(self.base( ReportKind::Csi, CsiPayload::ComplexI16 { rssi_dbm: (0..self.config.rx_count) .map(|i| -42 - i as i8 * 2) .collect(), values, }, )) } fn base(&self, kind: ReportKind, payload: CsiPayload) -> CsiFrame { CsiFrame { report_kind: kind, sequence: self.sequence, timestamp_us: self.timestamp_us, device_id: self.config.device_id, chipset: self.config.chipset, bandwidth_mhz: self.config.bandwidth_mhz, center_freq_khz: self.config.center_freq_khz, flags: CsiFlags(CsiFlags::CALIBRATED | CsiFlags::SYNTHETIC), tx_count: self.config.tx_count, rx_count: self.config.rx_count, ppdu_type: PpduType::HeSu, subcarrier_count: self.config.subcarriers, noise_floor_dbm: -95, scale: 1.0 / 2048.0, subcarrier_spacing_hz: 312_500.0, calibration_id: 1, payload, } } } } fn u16_at(b: &[u8], o: usize) -> u16 { u16::from_le_bytes([b[o], b[o + 1]]) } fn u32_at(b: &[u8], o: usize) -> u32 { u32::from_le_bytes(b[o..o + 4].try_into().unwrap()) } fn u64_at(b: &[u8], o: usize) -> u64 { u64::from_le_bytes(b[o..o + 8].try_into().unwrap()) } fn f32_at(b: &[u8], o: usize) -> f32 { f32::from_le_bytes(b[o..o + 4].try_into().unwrap()) } fn crc32_ieee(data: &[u8]) -> u32 { let mut crc = 0xffff_ffffu32; for &byte in data { crc ^= byte as u32; for _ in 0..8 { crc = (crc >> 1) ^ ((0u32.wrapping_sub(crc & 1)) & 0xedb8_8320); } } !crc } #[cfg(test)] mod tests { use super::*; use simulator::*; #[test] fn simulator_round_trip_is_deterministic() { let cfg = SimulatorConfig::default(); let mut a = QualcommCsiSimulator::new(cfg.clone()).unwrap(); let mut b = QualcommCsiSimulator::new(cfg).unwrap(); let wa = a.next_frame().to_bytes().unwrap(); assert_eq!(wa, b.next_frame().to_bytes().unwrap()); let (decoded, n) = CsiFrame::from_bytes(&wa).unwrap(); assert_eq!(n, wa.len()); assert!(decoded.flags.contains(CsiFlags::SYNTHETIC)); assert_eq!(decoded.payload.len(), 2 * 3 * 114); } #[test] fn capabilities_round_trip() { let s = QualcommCsiSimulator::new(SimulatorConfig::default()).unwrap(); let f = s.capabilities_frame(); let w = f.to_bytes().unwrap(); assert_eq!(CsiFrame::from_bytes(&w).unwrap().0, f); } #[test] fn crc_corruption_is_rejected() { let mut s = QualcommCsiSimulator::new(SimulatorConfig::default()).unwrap(); let mut w = s.next_frame().to_bytes().unwrap(); w[80] ^= 1; assert!(matches!( CsiFrame::from_bytes(&w), Err(CsiParseError::CrcMismatch { .. }) )); } #[test] fn truncation_is_rejected() { let mut s = QualcommCsiSimulator::new(SimulatorConfig::default()).unwrap(); let w = s.next_frame().to_bytes().unwrap(); assert!(matches!( CsiFrame::from_bytes(&w[..w.len() - 1]), Err(CsiParseError::InsufficientData { .. }) )); } #[test] fn invalid_dimensions_are_rejected() { let cfg = SimulatorConfig { rx_count: 4, chipset: ChipsetProfile::Qca9300, ..Default::default() }; assert!(matches!( QualcommCsiSimulator::new(cfg), Err(CsiParseError::InvalidDimensions) )); } #[test] fn non_finite_float_is_rejected() { let mut s = QualcommCsiSimulator::new(SimulatorConfig::default()).unwrap(); let mut f = s.next_frame(); f.payload = CsiPayload::ComplexF32 { rssi_dbm: vec![-40, -42, -44], values: vec![[f32::NAN, 0.0]; 2 * 3 * 114], }; assert_eq!(f.to_bytes().unwrap_err(), CsiParseError::NonFiniteValue); } #[test] fn parser_never_panics_on_prefixes() { let mut s = QualcommCsiSimulator::new(SimulatorConfig::default()).unwrap(); let w = s.next_frame().to_bytes().unwrap(); for end in 0..w.len() { let _ = CsiFrame::from_bytes(&w[..end]); } } #[test] fn qca9300_rejects_wifi6_bandwidths() { let cfg = SimulatorConfig { bandwidth_mhz: 80, ..Default::default() }; assert!(matches!( QualcommCsiSimulator::new(cfg), Err(CsiParseError::InvalidBandwidth(80)) )); } }