Files
wifi-densepose/rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/src/csi_frame.rs
rUv 92a5182dc3 feat(adr-018): ESP32-S3 firmware, Rust aggregator, and live CSI pipeline
Complete end-to-end WiFi CSI capture pipeline verified on real hardware:

- ESP32-S3 firmware: WiFi STA + promiscuous mode CSI collection,
  ADR-018 binary serialization, UDP streaming at ~20 Hz
- Rust aggregator CLI binary (clap): receives UDP frames, parses with
  Esp32CsiParser, prints per-frame summary (node, seq, rssi, amp)
- UDP aggregator module with per-node sequence tracking and drop detection
- CsiFrame bridge to detection pipeline (amplitude/phase/SNR conversion)
- Python ESP32 binary parser with UDP reader
- Presence detection confirmed: motion score 10/10 from live CSI variance

Hardware verified: ESP32-S3-DevKitC-1 (CP2102, MAC 3C:0F:02:EC:C2:28),
Docker ESP-IDF v5.2 build, esptool 5.1.0 flash, 20 Rust + 6 Python tests pass.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-02-28 13:22:04 -05:00

212 lines
6.2 KiB
Rust

//! 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<SubcarrierData>,
}
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<f64>, Vec<f64>) {
let amplitudes: Vec<f64> = 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<f64> = 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<Utc>,
/// 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,
}
/// 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,
},
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);
}
}