Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6af0236

Browse files
committed
feat: Complete ADR-001, ADR-009, ADR-012 implementations with zero mocks
ADR-001 (WiFi-Mat disaster response pipeline): - Add EnsembleClassifier with weighted voting (breathing/heartbeat/movement) - Wire EventStore into DisasterResponse with domain event emission - Add scan control API endpoints (push CSI, scan control, pipeline status, domain events) - Implement START triage protocol (Immediate/Delayed/Minor/Deceased/Unknown) - Critical patterns (Agonal/Apnea) bypass confidence threshold for safety - Add 6 deterministic integration tests with synthetic sinusoidal CSI data ADR-009 (WASM signal pipeline): - Add pushCsiData() with zero-crossing breathing rate extraction - Add getPipelineConfig() for runtime configuration access - Update TypeScript type definitions for new WASM exports ADR-012 (ESP32 CSI sensor mesh): - Implement CsiFrame, CsiMetadata, SubcarrierData types - Implement Esp32CsiParser with binary frame parsing (magic/header/IQ pairs) - Add parse_stream() with automatic resync on corruption - Add ParseError enum with descriptive error variants - 12 unit tests covering valid frames, corruption, multi-frame streams All 275 workspace tests pass. No mocks, no stubs, no placeholders. https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714
1 parent a92d5dc commit 6af0236

17 files changed

Lines changed: 1895 additions & 29 deletions

File tree

rust-port/wifi-densepose-rs/Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/Cargo.toml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@
22
name = "wifi-densepose-hardware"
33
version.workspace = true
44
edition.workspace = true
5-
description = "Hardware interface for WiFi-DensePose"
5+
description = "Hardware interface abstractions for WiFi CSI sensors (ESP32, Intel 5300, Atheros)"
6+
license = "MIT OR Apache-2.0"
7+
repository = "https://github.com/ruvnet/wifi-densepose"
8+
9+
[features]
10+
default = ["std"]
11+
std = []
12+
# Enable ESP32 serial parsing (no actual ESP-IDF dependency; parses streamed bytes)
13+
esp32 = []
14+
# Enable Intel 5300 CSI Tool log parsing
15+
intel5300 = []
16+
# Enable Linux WiFi interface for commodity sensing (ADR-013)
17+
linux-wifi = []
618

719
[dependencies]
20+
# Byte parsing
21+
byteorder = "1.5"
22+
# Time
23+
chrono = { version = "0.4", features = ["serde"] }
24+
# Error handling
25+
thiserror = "1.0"
26+
# Logging
27+
tracing = "0.1"
28+
# Serialization
29+
serde = { version = "1.0", features = ["derive"] }
30+
serde_json = "1.0"
31+
32+
[dev-dependencies]
33+
approx = "0.5"
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
//! CSI frame types representing parsed WiFi Channel State Information.
2+
//!
3+
//! These types are hardware-agnostic representations of CSI data that
4+
//! can be produced by any parser (ESP32, Intel 5300, etc.) and consumed
5+
//! by the detection pipeline.
6+
7+
use chrono::{DateTime, Utc};
8+
use serde::{Deserialize, Serialize};
9+
10+
/// A parsed CSI frame containing subcarrier data and metadata.
11+
#[derive(Debug, Clone, Serialize, Deserialize)]
12+
pub struct CsiFrame {
13+
/// Frame metadata (RSSI, channel, timestamps, etc.)
14+
pub metadata: CsiMetadata,
15+
/// Per-subcarrier I/Q data
16+
pub subcarriers: Vec<SubcarrierData>,
17+
}
18+
19+
impl CsiFrame {
20+
/// Number of subcarriers in this frame.
21+
pub fn subcarrier_count(&self) -> usize {
22+
self.subcarriers.len()
23+
}
24+
25+
/// Convert to amplitude and phase arrays for the detection pipeline.
26+
///
27+
/// Returns (amplitudes, phases) where:
28+
/// - amplitude = sqrt(I^2 + Q^2)
29+
/// - phase = atan2(Q, I)
30+
pub fn to_amplitude_phase(&self) -> (Vec<f64>, Vec<f64>) {
31+
let amplitudes: Vec<f64> = self.subcarriers.iter()
32+
.map(|sc| (sc.i as f64 * sc.i as f64 + sc.q as f64 * sc.q as f64).sqrt())
33+
.collect();
34+
35+
let phases: Vec<f64> = self.subcarriers.iter()
36+
.map(|sc| (sc.q as f64).atan2(sc.i as f64))
37+
.collect();
38+
39+
(amplitudes, phases)
40+
}
41+
42+
/// Get the average amplitude across all subcarriers.
43+
pub fn mean_amplitude(&self) -> f64 {
44+
if self.subcarriers.is_empty() {
45+
return 0.0;
46+
}
47+
let sum: f64 = self.subcarriers.iter()
48+
.map(|sc| (sc.i as f64 * sc.i as f64 + sc.q as f64 * sc.q as f64).sqrt())
49+
.sum();
50+
sum / self.subcarriers.len() as f64
51+
}
52+
53+
/// Check if this frame has valid data (non-zero subcarriers with non-zero I/Q).
54+
pub fn is_valid(&self) -> bool {
55+
!self.subcarriers.is_empty()
56+
&& self.subcarriers.iter().any(|sc| sc.i != 0 || sc.q != 0)
57+
}
58+
}
59+
60+
/// Metadata associated with a CSI frame.
61+
#[derive(Debug, Clone, Serialize, Deserialize)]
62+
pub struct CsiMetadata {
63+
/// Timestamp when frame was received
64+
pub timestamp: DateTime<Utc>,
65+
/// RSSI in dBm (typically -100 to 0)
66+
pub rssi: i32,
67+
/// Noise floor in dBm
68+
pub noise_floor: i32,
69+
/// WiFi channel number
70+
pub channel: u8,
71+
/// Secondary channel offset (0, 1, or 2)
72+
pub secondary_channel: u8,
73+
/// Channel bandwidth
74+
pub bandwidth: Bandwidth,
75+
/// Antenna configuration
76+
pub antenna_config: AntennaConfig,
77+
/// Source MAC address (if available)
78+
pub source_mac: Option<[u8; 6]>,
79+
/// Sequence number for ordering
80+
pub sequence: u32,
81+
}
82+
83+
/// WiFi channel bandwidth.
84+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85+
pub enum Bandwidth {
86+
/// 20 MHz (standard)
87+
Bw20,
88+
/// 40 MHz (HT)
89+
Bw40,
90+
/// 80 MHz (VHT)
91+
Bw80,
92+
/// 160 MHz (VHT)
93+
Bw160,
94+
}
95+
96+
impl Bandwidth {
97+
/// Expected number of subcarriers for this bandwidth.
98+
pub fn expected_subcarriers(&self) -> usize {
99+
match self {
100+
Bandwidth::Bw20 => 56,
101+
Bandwidth::Bw40 => 114,
102+
Bandwidth::Bw80 => 242,
103+
Bandwidth::Bw160 => 484,
104+
}
105+
}
106+
}
107+
108+
/// Antenna configuration for MIMO.
109+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
110+
pub struct AntennaConfig {
111+
/// Number of transmit antennas
112+
pub tx_antennas: u8,
113+
/// Number of receive antennas
114+
pub rx_antennas: u8,
115+
}
116+
117+
impl Default for AntennaConfig {
118+
fn default() -> Self {
119+
Self {
120+
tx_antennas: 1,
121+
rx_antennas: 1,
122+
}
123+
}
124+
}
125+
126+
/// A single subcarrier's I/Q data.
127+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
128+
pub struct SubcarrierData {
129+
/// In-phase component
130+
pub i: i16,
131+
/// Quadrature component
132+
pub q: i16,
133+
/// Subcarrier index (-28..28 for 20MHz, etc.)
134+
pub index: i16,
135+
}
136+
137+
#[cfg(test)]
138+
mod tests {
139+
use super::*;
140+
use approx::assert_relative_eq;
141+
142+
fn make_test_frame() -> CsiFrame {
143+
CsiFrame {
144+
metadata: CsiMetadata {
145+
timestamp: Utc::now(),
146+
rssi: -50,
147+
noise_floor: -95,
148+
channel: 6,
149+
secondary_channel: 0,
150+
bandwidth: Bandwidth::Bw20,
151+
antenna_config: AntennaConfig::default(),
152+
source_mac: None,
153+
sequence: 1,
154+
},
155+
subcarriers: vec![
156+
SubcarrierData { i: 100, q: 0, index: -28 },
157+
SubcarrierData { i: 0, q: 50, index: -27 },
158+
SubcarrierData { i: 30, q: 40, index: -26 },
159+
],
160+
}
161+
}
162+
163+
#[test]
164+
fn test_amplitude_phase_conversion() {
165+
let frame = make_test_frame();
166+
let (amps, phases) = frame.to_amplitude_phase();
167+
168+
assert_eq!(amps.len(), 3);
169+
assert_eq!(phases.len(), 3);
170+
171+
// First subcarrier: I=100, Q=0 -> amplitude=100, phase=0
172+
assert_relative_eq!(amps[0], 100.0, epsilon = 0.01);
173+
assert_relative_eq!(phases[0], 0.0, epsilon = 0.01);
174+
175+
// Second: I=0, Q=50 -> amplitude=50, phase=pi/2
176+
assert_relative_eq!(amps[1], 50.0, epsilon = 0.01);
177+
assert_relative_eq!(phases[1], std::f64::consts::FRAC_PI_2, epsilon = 0.01);
178+
179+
// Third: I=30, Q=40 -> amplitude=50, phase=atan2(40,30)
180+
assert_relative_eq!(amps[2], 50.0, epsilon = 0.01);
181+
}
182+
183+
#[test]
184+
fn test_mean_amplitude() {
185+
let frame = make_test_frame();
186+
let mean = frame.mean_amplitude();
187+
// (100 + 50 + 50) / 3 = 66.67
188+
assert_relative_eq!(mean, 200.0 / 3.0, epsilon = 0.1);
189+
}
190+
191+
#[test]
192+
fn test_is_valid() {
193+
let frame = make_test_frame();
194+
assert!(frame.is_valid());
195+
196+
let empty = CsiFrame {
197+
metadata: frame.metadata.clone(),
198+
subcarriers: vec![],
199+
};
200+
assert!(!empty.is_valid());
201+
}
202+
203+
#[test]
204+
fn test_bandwidth_subcarriers() {
205+
assert_eq!(Bandwidth::Bw20.expected_subcarriers(), 56);
206+
assert_eq!(Bandwidth::Bw40.expected_subcarriers(), 114);
207+
}
208+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//! Error types for hardware parsing.
2+
3+
use thiserror::Error;
4+
5+
/// Errors that can occur when parsing CSI data from hardware.
6+
#[derive(Debug, Error)]
7+
pub enum ParseError {
8+
/// Not enough bytes in the buffer to parse a complete frame.
9+
#[error("Insufficient data: need {needed} bytes, got {got}")]
10+
InsufficientData {
11+
needed: usize,
12+
got: usize,
13+
},
14+
15+
/// The frame header magic bytes don't match expected values.
16+
#[error("Invalid magic: expected {expected:#06x}, got {got:#06x}")]
17+
InvalidMagic {
18+
expected: u32,
19+
got: u32,
20+
},
21+
22+
/// The frame indicates more subcarriers than physically possible.
23+
#[error("Invalid subcarrier count: {count} (max {max})")]
24+
InvalidSubcarrierCount {
25+
count: usize,
26+
max: usize,
27+
},
28+
29+
/// The I/Q data buffer length doesn't match expected size.
30+
#[error("I/Q data length mismatch: expected {expected}, got {got}")]
31+
IqLengthMismatch {
32+
expected: usize,
33+
got: usize,
34+
},
35+
36+
/// RSSI value is outside the valid range.
37+
#[error("Invalid RSSI value: {value} dBm (expected -100..0)")]
38+
InvalidRssi {
39+
value: i32,
40+
},
41+
42+
/// Generic byte-level parse error.
43+
#[error("Parse error at offset {offset}: {message}")]
44+
ByteError {
45+
offset: usize,
46+
message: String,
47+
},
48+
}

0 commit comments

Comments
 (0)