//! CSI frame types representing parsed WiFi Channel State Information. //! //! These types are hardware-agnostic representations of CSI data that //! can be produced by any parser (ESP32, Intel 5300, etc.) and consumed //! by the detection pipeline. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; /// A parsed CSI frame containing subcarrier data and metadata. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CsiFrame { /// Frame metadata (RSSI, channel, timestamps, etc.) pub metadata: CsiMetadata, /// Per-subcarrier I/Q data pub subcarriers: Vec, } impl CsiFrame { /// Number of subcarriers in this frame. pub fn subcarrier_count(&self) -> usize { self.subcarriers.len() } /// Convert to amplitude and phase arrays for the detection pipeline. /// /// Returns (amplitudes, phases) where: /// - amplitude = sqrt(I^2 + Q^2) /// - phase = atan2(Q, I) pub fn to_amplitude_phase(&self) -> (Vec, Vec) { let amplitudes: Vec = self .subcarriers .iter() .map(|sc| (sc.i as f64 * sc.i as f64 + sc.q as f64 * sc.q as f64).sqrt()) .collect(); let phases: Vec = self .subcarriers .iter() .map(|sc| (sc.q as f64).atan2(sc.i as f64)) .collect(); (amplitudes, phases) } /// Get the average amplitude across all subcarriers. pub fn mean_amplitude(&self) -> f64 { if self.subcarriers.is_empty() { return 0.0; } let sum: f64 = self .subcarriers .iter() .map(|sc| (sc.i as f64 * sc.i as f64 + sc.q as f64 * sc.q as f64).sqrt()) .sum(); sum / self.subcarriers.len() as f64 } /// Check if this frame has valid data (non-zero subcarriers with non-zero I/Q). pub fn is_valid(&self) -> bool { !self.subcarriers.is_empty() && self.subcarriers.iter().any(|sc| sc.i != 0 || sc.q != 0) } } /// Metadata associated with a CSI frame (ADR-018 format). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CsiMetadata { /// Timestamp when frame was received pub timestamp: DateTime, /// Node identifier (0-255) pub node_id: u8, /// Number of antennas pub n_antennas: u8, /// Number of subcarriers pub n_subcarriers: u16, /// Channel center frequency in MHz pub channel_freq_mhz: u32, /// RSSI in dBm (signed byte, typically -100 to 0) pub rssi_dbm: i8, /// Noise floor in dBm (signed byte) pub noise_floor_dbm: i8, /// Channel bandwidth (derived from n_subcarriers) pub bandwidth: Bandwidth, /// Antenna configuration (populated from n_antennas) pub antenna_config: AntennaConfig, /// Sequence number for ordering pub sequence: u32, /// ADR-110: PPDU type from ADR-018 byte 18. None on pre-ADR-110 firmware /// (or when CONFIG_CSI_FRAME_HE_TAGGING is disabled — byte stays zero /// and pre-ADR-110 readers see the same zero, full backwards compat). /// Byte 18 = 0 reads as PpduType::HtLegacy (the wire encoding for the /// HT/legacy bucket); 0xFF reads as PpduType::Unknown. pub ppdu_type: PpduType, /// ADR-110: flags from ADR-018 byte 19 — bandwidth bits, STBC, LDPC, /// 802.15.4-time-sync-valid bit. See [`Adr018Flags`]. pub adr018_flags: Adr018Flags, } /// PPDU type encoded in ADR-018 byte 18 (ADR-110 extension). /// /// Wire encoding (matches firmware `csi_collector.c`): /// 0 = HT / legacy bucket (11b/g/HT/VHT all collapse here) /// 1 = HE-SU (802.11ax single-user) /// 2 = HE-MU (802.11ax multi-user) /// 3 = HE-TB (802.11ax trigger-based) /// 0xFF = Unknown #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum PpduType { HtLegacy, HeSu, HeMu, HeTb, Unknown, } impl PpduType { pub fn from_byte(b: u8) -> Self { match b { 0 => Self::HtLegacy, 1 => Self::HeSu, 2 => Self::HeMu, 3 => Self::HeTb, _ => Self::Unknown, } } pub fn to_byte(self) -> u8 { match self { Self::HtLegacy => 0, Self::HeSu => 1, Self::HeMu => 2, Self::HeTb => 3, Self::Unknown => 0xFF, } } pub fn is_he(self) -> bool { matches!(self, Self::HeSu | Self::HeMu | Self::HeTb) } } /// Flags encoded in ADR-018 byte 19 (ADR-110 extension). /// /// Wire encoding: /// bit 0 : bandwidth wide (set = 40 MHz, clear = 20 MHz) /// bit 1 : (reserved for 80/160 future) /// bit 2 : STBC /// bit 3 : LDPC (reserved — not yet populated by firmware) /// bit 4 : 802.15.4 time-sync valid (C6 only) /// bit 5-7 : reserved #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct Adr018Flags { pub bw40: bool, pub stbc: bool, pub ldpc: bool, pub ieee802154_sync_valid: bool, } impl Adr018Flags { pub fn from_byte(b: u8) -> Self { Self { bw40: (b & 0x01) != 0, stbc: (b & 0x04) != 0, ldpc: (b & 0x08) != 0, ieee802154_sync_valid: (b & 0x10) != 0, } } pub fn to_byte(self) -> u8 { let mut b = 0u8; if self.bw40 { b |= 0x01; } if self.stbc { b |= 0x04; } if self.ldpc { b |= 0x08; } if self.ieee802154_sync_valid { b |= 0x10; } b } } impl Default for Adr018Flags { fn default() -> Self { Self { bw40: false, stbc: false, ldpc: false, ieee802154_sync_valid: false } } } /// WiFi channel bandwidth. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Bandwidth { /// 20 MHz (standard) Bw20, /// 40 MHz (HT) Bw40, /// 80 MHz (VHT) Bw80, /// 160 MHz (VHT) Bw160, } impl Bandwidth { /// Expected number of subcarriers for this bandwidth. pub fn expected_subcarriers(&self) -> usize { match self { Bandwidth::Bw20 => 56, Bandwidth::Bw40 => 114, Bandwidth::Bw80 => 242, Bandwidth::Bw160 => 484, } } } /// Antenna configuration for MIMO. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct AntennaConfig { /// Number of transmit antennas pub tx_antennas: u8, /// Number of receive antennas pub rx_antennas: u8, } impl Default for AntennaConfig { fn default() -> Self { Self { tx_antennas: 1, rx_antennas: 1, } } } /// A single subcarrier's I/Q data. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct SubcarrierData { /// In-phase component pub i: i16, /// Quadrature component pub q: i16, /// Subcarrier index (-28..28 for 20MHz, etc.) pub index: i16, } #[cfg(test)] mod tests { use super::*; use approx::assert_relative_eq; fn make_test_frame() -> CsiFrame { CsiFrame { metadata: CsiMetadata { timestamp: Utc::now(), node_id: 1, n_antennas: 1, n_subcarriers: 3, channel_freq_mhz: 2437, rssi_dbm: -50, noise_floor_dbm: -95, bandwidth: Bandwidth::Bw20, antenna_config: AntennaConfig::default(), sequence: 1, ppdu_type: PpduType::HtLegacy, adr018_flags: Adr018Flags::default(), }, subcarriers: vec![ SubcarrierData { i: 100, q: 0, index: -28, }, SubcarrierData { i: 0, q: 50, index: -27, }, SubcarrierData { i: 30, q: 40, index: -26, }, ], } } #[test] fn test_amplitude_phase_conversion() { let frame = make_test_frame(); let (amps, phases) = frame.to_amplitude_phase(); assert_eq!(amps.len(), 3); assert_eq!(phases.len(), 3); // First subcarrier: I=100, Q=0 -> amplitude=100, phase=0 assert_relative_eq!(amps[0], 100.0, epsilon = 0.01); assert_relative_eq!(phases[0], 0.0, epsilon = 0.01); // Second: I=0, Q=50 -> amplitude=50, phase=pi/2 assert_relative_eq!(amps[1], 50.0, epsilon = 0.01); assert_relative_eq!(phases[1], std::f64::consts::FRAC_PI_2, epsilon = 0.01); // Third: I=30, Q=40 -> amplitude=50, phase=atan2(40,30) assert_relative_eq!(amps[2], 50.0, epsilon = 0.01); } #[test] fn test_mean_amplitude() { let frame = make_test_frame(); let mean = frame.mean_amplitude(); // (100 + 50 + 50) / 3 = 66.67 assert_relative_eq!(mean, 200.0 / 3.0, epsilon = 0.1); } #[test] fn test_is_valid() { let frame = make_test_frame(); assert!(frame.is_valid()); let empty = CsiFrame { metadata: frame.metadata.clone(), subcarriers: vec![], }; assert!(!empty.is_valid()); } #[test] fn test_bandwidth_subcarriers() { assert_eq!(Bandwidth::Bw20.expected_subcarriers(), 56); assert_eq!(Bandwidth::Bw40.expected_subcarriers(), 114); } }