Files
wifi-densepose/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/integration/csi_receiver.rs
Claude 7c00482314 fix: Clean up Rust warnings and add Python vital signs detection
Rust changes:
- Fix unused variable warnings in wifi-densepose-nn (densepose.rs, inference.rs, tensor.rs, translator.rs)
- Remove unused imports in wifi-densepose-mat (breathing.rs, pipeline.rs, csi_receiver.rs, debris_model.rs, vital_signs_classifier.rs)
- All tests continue to pass

Python changes:
- Add vital_signs.py module with BreathingDetector and HeartbeatDetector classes
- Mirror Rust wifi-densepose-mat detection functionality
- Update v1 package version to 1.2.0
- Export new vital signs classes from core module
2026-01-14 17:42:37 +00:00

1409 lines
44 KiB
Rust

//! CSI packet receivers for different input sources.
//!
//! This module provides receivers for:
//! - UDP packets (network streaming from remote sensors)
//! - Serial port (ESP32 and similar embedded devices)
//! - PCAP files (offline analysis and replay)
//!
//! # Example
//!
//! ```ignore
//! use wifi_densepose_mat::integration::csi_receiver::{
//! UdpCsiReceiver, ReceiverConfig, CsiPacketFormat,
//! };
//!
//! let config = ReceiverConfig::udp("0.0.0.0", 5500);
//! let mut receiver = UdpCsiReceiver::new(config)?;
//!
//! while let Some(packet) = receiver.receive().await? {
//! println!("Received CSI packet: {:?}", packet.metadata);
//! }
//! ```
use super::AdapterError;
use super::hardware_adapter::{
Bandwidth, CsiMetadata, CsiReadings, DeviceType, FrameControlType, SensorCsiReading,
};
use chrono::{DateTime, Utc};
use std::collections::VecDeque;
use std::io::{BufReader, Read};
use std::path::Path;
/// Configuration for CSI receivers
#[derive(Debug, Clone)]
pub struct ReceiverConfig {
/// Input source type
pub source: CsiSource,
/// Expected packet format
pub format: CsiPacketFormat,
/// Buffer size for incoming data
pub buffer_size: usize,
/// Maximum packets to queue
pub queue_size: usize,
/// Timeout for receive operations (ms)
pub timeout_ms: u64,
}
impl Default for ReceiverConfig {
fn default() -> Self {
Self {
source: CsiSource::Udp(UdpSourceConfig::default()),
format: CsiPacketFormat::Auto,
buffer_size: 65536,
queue_size: 1000,
timeout_ms: 5000,
}
}
}
impl ReceiverConfig {
/// Create UDP receiver configuration
pub fn udp(bind_addr: &str, port: u16) -> Self {
Self {
source: CsiSource::Udp(UdpSourceConfig {
bind_address: bind_addr.to_string(),
port,
multicast_group: None,
}),
..Default::default()
}
}
/// Create serial receiver configuration
pub fn serial(port: &str, baud_rate: u32) -> Self {
Self {
source: CsiSource::Serial(SerialSourceConfig {
port: port.to_string(),
baud_rate,
data_bits: 8,
stop_bits: 1,
parity: SerialParity::None,
}),
format: CsiPacketFormat::Esp32Csi,
..Default::default()
}
}
/// Create PCAP file reader configuration
pub fn pcap(file_path: &str) -> Self {
Self {
source: CsiSource::Pcap(PcapSourceConfig {
file_path: file_path.to_string(),
playback_speed: 1.0,
loop_playback: false,
start_offset: 0,
}),
format: CsiPacketFormat::Auto,
..Default::default()
}
}
}
/// CSI data source types
#[derive(Debug, Clone)]
pub enum CsiSource {
/// UDP network source
Udp(UdpSourceConfig),
/// Serial port source
Serial(SerialSourceConfig),
/// PCAP file source
Pcap(PcapSourceConfig),
}
/// UDP source configuration
#[derive(Debug, Clone)]
pub struct UdpSourceConfig {
/// Address to bind
pub bind_address: String,
/// Port number
pub port: u16,
/// Multicast group to join (optional)
pub multicast_group: Option<String>,
}
impl Default for UdpSourceConfig {
fn default() -> Self {
Self {
bind_address: "0.0.0.0".to_string(),
port: 5500,
multicast_group: None,
}
}
}
/// Serial source configuration
#[derive(Debug, Clone)]
pub struct SerialSourceConfig {
/// Serial port path
pub port: String,
/// Baud rate
pub baud_rate: u32,
/// Data bits (5-8)
pub data_bits: u8,
/// Stop bits (1, 2)
pub stop_bits: u8,
/// Parity setting
pub parity: SerialParity,
}
/// Serial parity options
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SerialParity {
None,
Odd,
Even,
}
/// PCAP source configuration
#[derive(Debug, Clone)]
pub struct PcapSourceConfig {
/// Path to PCAP file
pub file_path: String,
/// Playback speed multiplier (1.0 = realtime)
pub playback_speed: f64,
/// Loop playback when reaching end
pub loop_playback: bool,
/// Start offset in bytes
pub start_offset: u64,
}
/// Supported CSI packet formats
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CsiPacketFormat {
/// Auto-detect format
Auto,
/// ESP32 CSI format (ESP-CSI firmware)
Esp32Csi,
/// Intel 5300 BFEE format (Linux CSI Tool)
Intel5300Bfee,
/// Atheros CSI format
AtherosCsi,
/// Nexmon CSI format (Broadcom)
NexmonCsi,
/// PicoScenes format
PicoScenes,
/// Generic JSON format
JsonCsi,
/// Raw binary format
RawBinary,
}
/// Parsed CSI packet
#[derive(Debug, Clone)]
pub struct CsiPacket {
/// Timestamp of packet
pub timestamp: DateTime<Utc>,
/// Source identifier
pub source_id: String,
/// CSI amplitude values per subcarrier
pub amplitudes: Vec<f64>,
/// CSI phase values per subcarrier
pub phases: Vec<f64>,
/// RSSI value
pub rssi: i8,
/// Noise floor
pub noise_floor: i8,
/// Packet metadata
pub metadata: CsiPacketMetadata,
/// Raw packet data (if preserved)
pub raw_data: Option<Vec<u8>>,
}
/// Metadata for a CSI packet
#[derive(Debug, Clone)]
pub struct CsiPacketMetadata {
/// Transmitter MAC address
pub tx_mac: [u8; 6],
/// Receiver MAC address
pub rx_mac: [u8; 6],
/// WiFi channel
pub channel: u8,
/// Channel bandwidth
pub bandwidth: Bandwidth,
/// Number of transmit streams (Ntx)
pub ntx: u8,
/// Number of receive streams (Nrx)
pub nrx: u8,
/// Sequence number
pub sequence_num: u16,
/// Frame control field
pub frame_control: u16,
/// Rate/MCS index
pub rate: u8,
/// Secondary channel offset
pub secondary_channel: i8,
/// Packet format
pub format: CsiPacketFormat,
}
impl Default for CsiPacketMetadata {
fn default() -> Self {
Self {
tx_mac: [0; 6],
rx_mac: [0; 6],
channel: 6,
bandwidth: Bandwidth::HT20,
ntx: 1,
nrx: 3,
sequence_num: 0,
frame_control: 0,
rate: 0,
secondary_channel: 0,
format: CsiPacketFormat::Auto,
}
}
}
/// UDP CSI receiver
pub struct UdpCsiReceiver {
config: ReceiverConfig,
socket: Option<tokio::net::UdpSocket>,
buffer: Vec<u8>,
parser: CsiParser,
stats: ReceiverStats,
}
impl UdpCsiReceiver {
/// Create a new UDP receiver
pub async fn new(config: ReceiverConfig) -> Result<Self, AdapterError> {
let udp_config = match &config.source {
CsiSource::Udp(c) => c,
_ => return Err(AdapterError::Config("Invalid config for UDP receiver".into())),
};
let addr = format!("{}:{}", udp_config.bind_address, udp_config.port);
let socket = tokio::net::UdpSocket::bind(&addr)
.await
.map_err(|e| AdapterError::Hardware(format!("Failed to bind UDP socket: {}", e)))?;
// Join multicast if specified
if let Some(ref group) = udp_config.multicast_group {
let multicast_addr: std::net::Ipv4Addr = group
.parse()
.map_err(|e| AdapterError::Config(format!("Invalid multicast address: {}", e)))?;
socket
.join_multicast_v4(multicast_addr, std::net::Ipv4Addr::UNSPECIFIED)
.map_err(|e| AdapterError::Hardware(format!("Failed to join multicast: {}", e)))?;
tracing::info!("Joined multicast group {}", group);
}
tracing::info!("UDP receiver bound to {}", addr);
Ok(Self {
buffer: vec![0u8; config.buffer_size],
parser: CsiParser::new(config.format),
stats: ReceiverStats::default(),
config,
socket: Some(socket),
})
}
/// Receive next CSI packet
pub async fn receive(&mut self) -> Result<Option<CsiPacket>, AdapterError> {
let socket = self
.socket
.as_ref()
.ok_or_else(|| AdapterError::Hardware("Socket not initialized".into()))?;
let timeout = tokio::time::Duration::from_millis(self.config.timeout_ms);
match tokio::time::timeout(timeout, socket.recv_from(&mut self.buffer)).await {
Ok(Ok((len, addr))) => {
self.stats.packets_received += 1;
self.stats.bytes_received += len as u64;
let data = &self.buffer[..len];
match self.parser.parse(data) {
Ok(packet) => {
self.stats.packets_parsed += 1;
Ok(Some(packet))
}
Err(e) => {
self.stats.parse_errors += 1;
tracing::debug!("Failed to parse packet from {}: {}", addr, e);
Ok(None)
}
}
}
Ok(Err(e)) => Err(AdapterError::Hardware(format!("Socket receive error: {}", e))),
Err(_) => Ok(None), // Timeout
}
}
/// Get receiver statistics
pub fn stats(&self) -> &ReceiverStats {
&self.stats
}
/// Close the receiver
pub async fn close(&mut self) {
self.socket = None;
}
}
/// Serial CSI receiver
pub struct SerialCsiReceiver {
config: ReceiverConfig,
port_path: String,
buffer: VecDeque<u8>,
parser: CsiParser,
stats: ReceiverStats,
running: bool,
}
impl SerialCsiReceiver {
/// Create a new serial receiver
pub fn new(config: ReceiverConfig) -> Result<Self, AdapterError> {
let serial_config = match &config.source {
CsiSource::Serial(c) => c,
_ => return Err(AdapterError::Config("Invalid config for serial receiver".into())),
};
// Verify port exists
#[cfg(unix)]
{
if !Path::new(&serial_config.port).exists() {
return Err(AdapterError::Hardware(format!(
"Serial port {} not found",
serial_config.port
)));
}
}
tracing::info!(
"Serial receiver configured for {} at {} baud",
serial_config.port,
serial_config.baud_rate
);
Ok(Self {
port_path: serial_config.port.clone(),
buffer: VecDeque::with_capacity(config.buffer_size),
parser: CsiParser::new(config.format),
stats: ReceiverStats::default(),
running: false,
config,
})
}
/// Start receiving (blocking, typically run in separate thread)
pub fn start(&mut self) -> Result<(), AdapterError> {
self.running = true;
// In production, this would open the serial port using serialport crate
// and start reading data
Ok(())
}
/// Receive next CSI packet (non-blocking if data available)
pub fn receive(&mut self) -> Result<Option<CsiPacket>, AdapterError> {
if !self.running {
return Err(AdapterError::Hardware("Receiver not started".into()));
}
// Try to parse a complete packet from buffer
if let Some(packet_data) = self.extract_packet_from_buffer() {
self.stats.packets_received += 1;
match self.parser.parse(&packet_data) {
Ok(packet) => {
self.stats.packets_parsed += 1;
return Ok(Some(packet));
}
Err(e) => {
self.stats.parse_errors += 1;
tracing::debug!("Failed to parse serial packet: {}", e);
}
}
}
Ok(None)
}
/// Extract a complete packet from the buffer
fn extract_packet_from_buffer(&mut self) -> Option<Vec<u8>> {
// Look for packet delimiter based on format
match self.config.format {
CsiPacketFormat::Esp32Csi => self.extract_esp32_packet(),
CsiPacketFormat::JsonCsi => self.extract_json_packet(),
_ => self.extract_newline_delimited(),
}
}
/// Extract ESP32 CSI packet (CSV format with newline delimiter)
fn extract_esp32_packet(&mut self) -> Option<Vec<u8>> {
// ESP32 CSI uses newline-delimited CSV
self.extract_newline_delimited()
}
/// Extract JSON packet
fn extract_json_packet(&mut self) -> Option<Vec<u8>> {
// Look for complete JSON object
let mut depth = 0;
let mut start = None;
let mut end = None;
for (i, &byte) in self.buffer.iter().enumerate() {
if byte == b'{' {
if depth == 0 {
start = Some(i);
}
depth += 1;
} else if byte == b'}' {
depth -= 1;
if depth == 0 && start.is_some() {
end = Some(i + 1);
break;
}
}
}
if let (Some(s), Some(e)) = (start, end) {
let packet: Vec<u8> = self.buffer.drain(..e).skip(s).collect();
return Some(packet);
}
None
}
/// Extract newline-delimited packet
fn extract_newline_delimited(&mut self) -> Option<Vec<u8>> {
if let Some(pos) = self.buffer.iter().position(|&b| b == b'\n') {
let packet: Vec<u8> = self.buffer.drain(..=pos).collect();
return Some(packet);
}
None
}
/// Add data to receive buffer (called from read thread)
pub fn feed_data(&mut self, data: &[u8]) {
self.buffer.extend(data);
self.stats.bytes_received += data.len() as u64;
}
/// Stop receiving
pub fn stop(&mut self) {
self.running = false;
}
/// Get receiver statistics
pub fn stats(&self) -> &ReceiverStats {
&self.stats
}
}
/// PCAP file CSI reader
pub struct PcapCsiReader {
config: ReceiverConfig,
file_path: String,
parser: CsiParser,
stats: ReceiverStats,
packets: Vec<PcapPacket>,
current_index: usize,
start_time: Option<DateTime<Utc>>,
playback_time: Option<DateTime<Utc>>,
}
/// Internal PCAP packet representation
struct PcapPacket {
timestamp: DateTime<Utc>,
data: Vec<u8>,
}
impl PcapCsiReader {
/// Create a new PCAP reader
pub fn new(config: ReceiverConfig) -> Result<Self, AdapterError> {
let pcap_config = match &config.source {
CsiSource::Pcap(c) => c,
_ => return Err(AdapterError::Config("Invalid config for PCAP reader".into())),
};
if !Path::new(&pcap_config.file_path).exists() {
return Err(AdapterError::Hardware(format!(
"PCAP file not found: {}",
pcap_config.file_path
)));
}
tracing::info!("PCAP reader configured for {}", pcap_config.file_path);
Ok(Self {
file_path: pcap_config.file_path.clone(),
parser: CsiParser::new(config.format),
stats: ReceiverStats::default(),
packets: Vec::new(),
current_index: 0,
start_time: None,
playback_time: None,
config,
})
}
/// Load PCAP file into memory
pub fn load(&mut self) -> Result<usize, AdapterError> {
tracing::info!("Loading PCAP file: {}", self.file_path);
let file = std::fs::File::open(&self.file_path)
.map_err(|e| AdapterError::Hardware(format!("Failed to open PCAP file: {}", e)))?;
let mut reader = BufReader::new(file);
// Read PCAP global header
let global_header = self.read_pcap_global_header(&mut reader)?;
tracing::debug!(
"PCAP file: magic={:08x}, version={}.{}, snaplen={}",
global_header.magic,
global_header.version_major,
global_header.version_minor,
global_header.snaplen
);
// Determine byte order from magic number
let swapped = global_header.magic == 0xD4C3B2A1 || global_header.magic == 0x4D3CB2A1;
// Read all packets
self.packets.clear();
let mut packet_count = 0;
loop {
match self.read_pcap_packet(&mut reader, swapped) {
Ok(Some(packet)) => {
self.packets.push(packet);
packet_count += 1;
}
Ok(None) => break, // EOF
Err(e) => {
tracing::warn!("Error reading packet {}: {}", packet_count, e);
break;
}
}
}
self.stats.packets_received = packet_count as u64;
tracing::info!("Loaded {} packets from PCAP file", packet_count);
Ok(packet_count)
}
/// Read PCAP global header
fn read_pcap_global_header<R: Read>(
&self,
reader: &mut R,
) -> Result<PcapGlobalHeader, AdapterError> {
let mut buf = [0u8; 24];
reader
.read_exact(&mut buf)
.map_err(|e| AdapterError::Hardware(format!("Failed to read PCAP header: {}", e)))?;
Ok(PcapGlobalHeader {
magic: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
version_major: u16::from_le_bytes([buf[4], buf[5]]),
version_minor: u16::from_le_bytes([buf[6], buf[7]]),
thiszone: i32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
sigfigs: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
snaplen: u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]),
network: u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]),
})
}
/// Read a single PCAP packet
fn read_pcap_packet<R: Read>(
&self,
reader: &mut R,
swapped: bool,
) -> Result<Option<PcapPacket>, AdapterError> {
// Read packet header
let mut header_buf = [0u8; 16];
match reader.read_exact(&mut header_buf) {
Ok(_) => {}
Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => {
return Err(AdapterError::Hardware(format!(
"Failed to read packet header: {}",
e
)))
}
}
let (ts_sec, ts_usec, incl_len, _orig_len) = if swapped {
(
u32::from_be_bytes([header_buf[0], header_buf[1], header_buf[2], header_buf[3]]),
u32::from_be_bytes([header_buf[4], header_buf[5], header_buf[6], header_buf[7]]),
u32::from_be_bytes([header_buf[8], header_buf[9], header_buf[10], header_buf[11]]),
u32::from_be_bytes([
header_buf[12],
header_buf[13],
header_buf[14],
header_buf[15],
]),
)
} else {
(
u32::from_le_bytes([header_buf[0], header_buf[1], header_buf[2], header_buf[3]]),
u32::from_le_bytes([header_buf[4], header_buf[5], header_buf[6], header_buf[7]]),
u32::from_le_bytes([header_buf[8], header_buf[9], header_buf[10], header_buf[11]]),
u32::from_le_bytes([
header_buf[12],
header_buf[13],
header_buf[14],
header_buf[15],
]),
)
};
// Read packet data
let mut data = vec![0u8; incl_len as usize];
reader.read_exact(&mut data).map_err(|e| {
AdapterError::Hardware(format!("Failed to read packet data: {}", e))
})?;
// Convert timestamp
let timestamp = chrono::DateTime::from_timestamp(ts_sec as i64, ts_usec * 1000)
.unwrap_or_else(Utc::now);
Ok(Some(PcapPacket { timestamp, data }))
}
/// Read next CSI packet with timing
pub async fn read_next(&mut self) -> Result<Option<CsiPacket>, AdapterError> {
if self.current_index >= self.packets.len() {
let pcap_config = match &self.config.source {
CsiSource::Pcap(c) => c,
_ => return Ok(None),
};
if pcap_config.loop_playback {
self.current_index = 0;
self.start_time = None;
self.playback_time = None;
} else {
return Ok(None);
}
}
let packet = &self.packets[self.current_index];
// Initialize timing on first packet
if self.start_time.is_none() {
self.start_time = Some(packet.timestamp);
self.playback_time = Some(Utc::now());
}
// Calculate delay for realtime playback
let pcap_config = match &self.config.source {
CsiSource::Pcap(c) => c,
_ => return Ok(None),
};
if pcap_config.playback_speed > 0.0 {
let packet_offset = packet.timestamp - self.start_time.unwrap();
let real_offset = Utc::now() - self.playback_time.unwrap();
let scaled_offset = packet_offset
.num_milliseconds()
.checked_div((pcap_config.playback_speed * 1000.0) as i64)
.unwrap_or(0);
let delay_ms = scaled_offset - real_offset.num_milliseconds();
if delay_ms > 0 {
tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms as u64)).await;
}
}
// Parse the packet
let result = match self.parser.parse(&packet.data) {
Ok(mut csi_packet) => {
csi_packet.timestamp = packet.timestamp;
self.stats.packets_parsed += 1;
Ok(Some(csi_packet))
}
Err(e) => {
self.stats.parse_errors += 1;
tracing::debug!("Failed to parse PCAP packet: {}", e);
Ok(None)
}
};
self.current_index += 1;
result
}
/// Reset playback to beginning
pub fn reset(&mut self) {
self.current_index = 0;
self.start_time = None;
self.playback_time = None;
}
/// Get current position
pub fn position(&self) -> (usize, usize) {
(self.current_index, self.packets.len())
}
/// Seek to specific packet index
pub fn seek(&mut self, index: usize) -> Result<(), AdapterError> {
if index >= self.packets.len() {
return Err(AdapterError::Config(format!(
"Seek index {} out of range (max {})",
index,
self.packets.len()
)));
}
self.current_index = index;
self.start_time = None;
self.playback_time = None;
Ok(())
}
/// Get receiver statistics
pub fn stats(&self) -> &ReceiverStats {
&self.stats
}
}
/// PCAP global header structure
struct PcapGlobalHeader {
magic: u32,
version_major: u16,
version_minor: u16,
thiszone: i32,
sigfigs: u32,
snaplen: u32,
network: u32,
}
/// CSI packet parser
pub struct CsiParser {
format: CsiPacketFormat,
}
impl CsiParser {
/// Create a new parser
pub fn new(format: CsiPacketFormat) -> Self {
Self { format }
}
/// Parse raw data into CSI packet
pub fn parse(&self, data: &[u8]) -> Result<CsiPacket, AdapterError> {
let format = if self.format == CsiPacketFormat::Auto {
self.detect_format(data)
} else {
self.format
};
match format {
CsiPacketFormat::Esp32Csi => self.parse_esp32(data),
CsiPacketFormat::Intel5300Bfee => self.parse_intel_5300(data),
CsiPacketFormat::AtherosCsi => self.parse_atheros(data),
CsiPacketFormat::NexmonCsi => self.parse_nexmon(data),
CsiPacketFormat::PicoScenes => self.parse_picoscenes(data),
CsiPacketFormat::JsonCsi => self.parse_json(data),
CsiPacketFormat::RawBinary => self.parse_raw_binary(data),
CsiPacketFormat::Auto => Err(AdapterError::DataFormat("Unable to detect format".into())),
}
}
/// Detect packet format from data
fn detect_format(&self, data: &[u8]) -> CsiPacketFormat {
// Check for JSON
if data.first() == Some(&b'{') {
return CsiPacketFormat::JsonCsi;
}
// Check for ESP32 CSV format (starts with "CSI_DATA,")
if data.starts_with(b"CSI_DATA,") {
return CsiPacketFormat::Esp32Csi;
}
// Check for Intel 5300 format (look for magic bytes)
if data.len() >= 4 && data[0] == 0xBB && data[1] == 0x00 {
return CsiPacketFormat::Intel5300Bfee;
}
// Check for PicoScenes format
if data.len() >= 8 && data[0..4] == [0x50, 0x53, 0x43, 0x53] {
// "PSCS"
return CsiPacketFormat::PicoScenes;
}
// Default to raw binary
CsiPacketFormat::RawBinary
}
/// Parse ESP32 CSI format
fn parse_esp32(&self, data: &[u8]) -> Result<CsiPacket, AdapterError> {
let line = std::str::from_utf8(data)
.map_err(|e| AdapterError::DataFormat(format!("Invalid UTF-8: {}", e)))?
.trim();
// Format: CSI_DATA,mac,rssi,channel,len,data...
let parts: Vec<&str> = line.split(',').collect();
if parts.len() < 5 {
return Err(AdapterError::DataFormat("Invalid ESP32 CSI format".into()));
}
let _prefix = parts[0]; // "CSI_DATA"
let mac_str = parts[1];
let rssi: i8 = parts[2]
.parse()
.map_err(|_| AdapterError::DataFormat("Invalid RSSI value".into()))?;
let channel: u8 = parts[3]
.parse()
.map_err(|_| AdapterError::DataFormat("Invalid channel value".into()))?;
let _len: usize = parts[4]
.parse()
.map_err(|_| AdapterError::DataFormat("Invalid length value".into()))?;
// Parse MAC address
let mut tx_mac = [0u8; 6];
let mac_parts: Vec<&str> = mac_str.split(':').collect();
if mac_parts.len() == 6 {
for (i, part) in mac_parts.iter().enumerate() {
tx_mac[i] = u8::from_str_radix(part, 16).unwrap_or(0);
}
}
// Parse CSI data (remaining parts as comma-separated values)
let mut amplitudes = Vec::new();
let mut phases = Vec::new();
for (i, part) in parts[5..].iter().enumerate() {
if let Ok(val) = part.parse::<f64>() {
// Alternate between amplitude and phase
if i % 2 == 0 {
amplitudes.push(val);
} else {
phases.push(val);
}
}
}
// Ensure phases vector matches amplitudes
while phases.len() < amplitudes.len() {
phases.push(0.0);
}
Ok(CsiPacket {
timestamp: Utc::now(),
source_id: mac_str.to_string(),
amplitudes,
phases,
rssi,
noise_floor: -92,
metadata: CsiPacketMetadata {
tx_mac,
rx_mac: [0; 6],
channel,
bandwidth: Bandwidth::HT20,
format: CsiPacketFormat::Esp32Csi,
..Default::default()
},
raw_data: Some(data.to_vec()),
})
}
/// Parse Intel 5300 BFEE format
fn parse_intel_5300(&self, data: &[u8]) -> Result<CsiPacket, AdapterError> {
// Intel 5300 BFEE structure (from Linux CSI Tool)
if data.len() < 25 {
return Err(AdapterError::DataFormat("Intel 5300 packet too short".into()));
}
// Parse header
let timestamp_low = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
let bfee_count = u16::from_le_bytes([data[4], data[5]]);
let _nrx = data[8];
let ntx = data[9];
let rssi_a = data[10] as i8;
let rssi_b = data[11] as i8;
let rssi_c = data[12] as i8;
let noise = data[13] as i8;
let agc = data[14];
let perm = [data[15], data[16], data[17]];
let rate = u16::from_le_bytes([data[18], data[19]]);
// Average RSSI
let rssi = ((rssi_a as i16 + rssi_b as i16 + rssi_c as i16) / 3) as i8;
// Parse CSI matrix (30 subcarriers for Intel 5300)
let csi_start = 20;
let num_subcarriers = 30;
let mut amplitudes = Vec::with_capacity(num_subcarriers);
let mut phases = Vec::with_capacity(num_subcarriers);
// CSI is stored as complex values (I/Q pairs)
for i in 0..num_subcarriers {
let offset = csi_start + i * 2;
if offset + 1 < data.len() {
let real = data[offset] as i8 as f64;
let imag = data[offset + 1] as i8 as f64;
let amplitude = (real * real + imag * imag).sqrt();
let phase = imag.atan2(real);
amplitudes.push(amplitude);
phases.push(phase);
}
}
Ok(CsiPacket {
timestamp: Utc::now(),
source_id: format!("intel5300_{}", bfee_count),
amplitudes,
phases,
rssi,
noise_floor: noise,
metadata: CsiPacketMetadata {
tx_mac: [0; 6],
rx_mac: [0; 6],
channel: 6, // Would need to be extracted from context
bandwidth: Bandwidth::HT20,
ntx,
nrx: 3,
rate: (rate & 0xFF) as u8,
format: CsiPacketFormat::Intel5300Bfee,
..Default::default()
},
raw_data: Some(data.to_vec()),
})
}
/// Parse Atheros CSI format
fn parse_atheros(&self, data: &[u8]) -> Result<CsiPacket, AdapterError> {
// Atheros CSI structure varies by driver
if data.len() < 20 {
return Err(AdapterError::DataFormat("Atheros packet too short".into()));
}
// Basic header (simplified)
let rssi = data[0] as i8;
let noise = data[1] as i8;
let channel = data[2];
let bandwidth = if data[3] == 1 {
Bandwidth::HT40
} else {
Bandwidth::HT20
};
let num_subcarriers = match bandwidth {
Bandwidth::HT20 => 56,
Bandwidth::HT40 => 114,
_ => 56,
};
// Parse CSI data
let csi_start = 20;
let mut amplitudes = Vec::with_capacity(num_subcarriers);
let mut phases = Vec::with_capacity(num_subcarriers);
for i in 0..num_subcarriers {
let offset = csi_start + i * 4;
if offset + 3 < data.len() {
let real = i16::from_le_bytes([data[offset], data[offset + 1]]) as f64;
let imag = i16::from_le_bytes([data[offset + 2], data[offset + 3]]) as f64;
let amplitude = (real * real + imag * imag).sqrt();
let phase = imag.atan2(real);
amplitudes.push(amplitude);
phases.push(phase);
}
}
Ok(CsiPacket {
timestamp: Utc::now(),
source_id: "atheros".to_string(),
amplitudes,
phases,
rssi,
noise_floor: noise,
metadata: CsiPacketMetadata {
channel,
bandwidth,
format: CsiPacketFormat::AtherosCsi,
..Default::default()
},
raw_data: Some(data.to_vec()),
})
}
/// Parse Nexmon CSI format
fn parse_nexmon(&self, data: &[u8]) -> Result<CsiPacket, AdapterError> {
// Nexmon CSI UDP packet format
if data.len() < 18 {
return Err(AdapterError::DataFormat("Nexmon packet too short".into()));
}
// Parse header
let _magic = u16::from_le_bytes([data[0], data[1]]);
let rssi = data[2] as i8;
let fc = u16::from_le_bytes([data[3], data[4]]);
let _src_mac = &data[5..11];
let seq = u16::from_le_bytes([data[11], data[12]]);
let _core_revid = u16::from_le_bytes([data[13], data[14]]);
let chan_spec = u16::from_le_bytes([data[15], data[16]]);
let chip = u16::from_le_bytes([data[17], data[18]]);
// Determine bandwidth from chanspec
let bandwidth = match (chan_spec >> 8) & 0x7 {
0 => Bandwidth::HT20,
1 => Bandwidth::HT40,
2 => Bandwidth::VHT80,
_ => Bandwidth::HT20,
};
let channel = (chan_spec & 0xFF) as u8;
// Parse CSI data
let csi_start = 18;
let bytes_per_sc = 4; // 2 bytes real + 2 bytes imag
let num_subcarriers = (data.len() - csi_start) / bytes_per_sc;
let mut amplitudes = Vec::with_capacity(num_subcarriers);
let mut phases = Vec::with_capacity(num_subcarriers);
for i in 0..num_subcarriers {
let offset = csi_start + i * bytes_per_sc;
if offset + 3 < data.len() {
let real = i16::from_le_bytes([data[offset], data[offset + 1]]) as f64;
let imag = i16::from_le_bytes([data[offset + 2], data[offset + 3]]) as f64;
amplitudes.push((real * real + imag * imag).sqrt());
phases.push(imag.atan2(real));
}
}
Ok(CsiPacket {
timestamp: Utc::now(),
source_id: format!("nexmon_{}", chip),
amplitudes,
phases,
rssi,
noise_floor: -92,
metadata: CsiPacketMetadata {
channel,
bandwidth,
sequence_num: seq,
frame_control: fc,
format: CsiPacketFormat::NexmonCsi,
..Default::default()
},
raw_data: Some(data.to_vec()),
})
}
/// Parse PicoScenes format
fn parse_picoscenes(&self, data: &[u8]) -> Result<CsiPacket, AdapterError> {
// PicoScenes has a complex structure with multiple segments
if data.len() < 100 {
return Err(AdapterError::DataFormat("PicoScenes packet too short".into()));
}
// Simplified parsing - real implementation would parse all segments
let rssi = data[20] as i8;
let channel = data[24];
// Placeholder - full implementation would parse the CSI segment
Ok(CsiPacket {
timestamp: Utc::now(),
source_id: "picoscenes".to_string(),
amplitudes: vec![],
phases: vec![],
rssi,
noise_floor: -92,
metadata: CsiPacketMetadata {
channel,
format: CsiPacketFormat::PicoScenes,
..Default::default()
},
raw_data: Some(data.to_vec()),
})
}
/// Parse JSON CSI format
fn parse_json(&self, data: &[u8]) -> Result<CsiPacket, AdapterError> {
let json_str = std::str::from_utf8(data)
.map_err(|e| AdapterError::DataFormat(format!("Invalid UTF-8: {}", e)))?;
let json: serde_json::Value = serde_json::from_str(json_str)
.map_err(|e| AdapterError::DataFormat(format!("Invalid JSON: {}", e)))?;
let rssi = json
.get("rssi")
.and_then(|v| v.as_i64())
.unwrap_or(-50) as i8;
let channel = json
.get("channel")
.and_then(|v| v.as_u64())
.unwrap_or(6) as u8;
let amplitudes: Vec<f64> = json
.get("amplitudes")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_f64())
.collect()
})
.unwrap_or_default();
let phases: Vec<f64> = json
.get("phases")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_f64())
.collect()
})
.unwrap_or_default();
let source_id = json
.get("source_id")
.and_then(|v| v.as_str())
.unwrap_or("json")
.to_string();
Ok(CsiPacket {
timestamp: Utc::now(),
source_id,
amplitudes,
phases,
rssi,
noise_floor: -92,
metadata: CsiPacketMetadata {
channel,
format: CsiPacketFormat::JsonCsi,
..Default::default()
},
raw_data: Some(data.to_vec()),
})
}
/// Parse raw binary format (minimal processing)
fn parse_raw_binary(&self, data: &[u8]) -> Result<CsiPacket, AdapterError> {
// Just store raw data without parsing
Ok(CsiPacket {
timestamp: Utc::now(),
source_id: "raw".to_string(),
amplitudes: vec![],
phases: vec![],
rssi: 0,
noise_floor: 0,
metadata: CsiPacketMetadata {
format: CsiPacketFormat::RawBinary,
..Default::default()
},
raw_data: Some(data.to_vec()),
})
}
}
/// Receiver statistics
#[derive(Debug, Clone, Default)]
pub struct ReceiverStats {
/// Total packets received
pub packets_received: u64,
/// Successfully parsed packets
pub packets_parsed: u64,
/// Parse errors
pub parse_errors: u64,
/// Total bytes received
pub bytes_received: u64,
/// Dropped packets (buffer overflow)
pub packets_dropped: u64,
}
impl ReceiverStats {
/// Get parse success rate
pub fn success_rate(&self) -> f64 {
if self.packets_received > 0 {
self.packets_parsed as f64 / self.packets_received as f64
} else {
0.0
}
}
/// Reset statistics
pub fn reset(&mut self) {
*self = Self::default();
}
}
/// Convert CsiPacket to CsiReadings for integration with HardwareAdapter
impl From<CsiPacket> for CsiReadings {
fn from(packet: CsiPacket) -> Self {
// Capture length before moving amplitudes
let num_subcarriers = packet.amplitudes.len();
CsiReadings {
timestamp: packet.timestamp,
readings: vec![SensorCsiReading {
sensor_id: packet.source_id,
amplitudes: packet.amplitudes,
phases: packet.phases,
rssi: packet.rssi as f64,
noise_floor: packet.noise_floor as f64,
tx_mac: Some(format!(
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
packet.metadata.tx_mac[0],
packet.metadata.tx_mac[1],
packet.metadata.tx_mac[2],
packet.metadata.tx_mac[3],
packet.metadata.tx_mac[4],
packet.metadata.tx_mac[5]
)),
rx_mac: Some(format!(
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
packet.metadata.rx_mac[0],
packet.metadata.rx_mac[1],
packet.metadata.rx_mac[2],
packet.metadata.rx_mac[3],
packet.metadata.rx_mac[4],
packet.metadata.rx_mac[5]
)),
sequence_num: Some(packet.metadata.sequence_num),
}],
metadata: CsiMetadata {
device_type: match packet.metadata.format {
CsiPacketFormat::Esp32Csi => DeviceType::Esp32,
CsiPacketFormat::Intel5300Bfee => DeviceType::Intel5300,
CsiPacketFormat::AtherosCsi => {
DeviceType::Atheros(super::hardware_adapter::AtherosDriver::Ath10k)
}
_ => DeviceType::UdpReceiver,
},
channel: packet.metadata.channel,
bandwidth: packet.metadata.bandwidth,
num_subcarriers,
rssi: Some(packet.rssi as f64),
noise_floor: Some(packet.noise_floor as f64),
fc_type: FrameControlType::Data,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_receiver_config_udp() {
let config = ReceiverConfig::udp("0.0.0.0", 5500);
assert!(matches!(config.source, CsiSource::Udp(_)));
}
#[test]
fn test_receiver_config_serial() {
let config = ReceiverConfig::serial("/dev/ttyUSB0", 921600);
assert!(matches!(config.source, CsiSource::Serial(_)));
assert_eq!(config.format, CsiPacketFormat::Esp32Csi);
}
#[test]
fn test_receiver_config_pcap() {
let config = ReceiverConfig::pcap("/tmp/test.pcap");
assert!(matches!(config.source, CsiSource::Pcap(_)));
}
#[test]
fn test_parser_detect_json() {
let parser = CsiParser::new(CsiPacketFormat::Auto);
let data = b"{\"rssi\": -50}";
let format = parser.detect_format(data);
assert_eq!(format, CsiPacketFormat::JsonCsi);
}
#[test]
fn test_parser_detect_esp32() {
let parser = CsiParser::new(CsiPacketFormat::Auto);
let data = b"CSI_DATA,AA:BB:CC:DD:EE:FF,-45,6,128,1.0,0.5";
let format = parser.detect_format(data);
assert_eq!(format, CsiPacketFormat::Esp32Csi);
}
#[test]
fn test_parse_json() {
let parser = CsiParser::new(CsiPacketFormat::JsonCsi);
let data = br#"{"rssi": -50, "channel": 6, "amplitudes": [1.0, 2.0, 3.0], "phases": [0.1, 0.2, 0.3]}"#;
let packet = parser.parse(data).unwrap();
assert_eq!(packet.rssi, -50);
assert_eq!(packet.metadata.channel, 6);
assert_eq!(packet.amplitudes.len(), 3);
}
#[test]
fn test_parse_esp32() {
let parser = CsiParser::new(CsiPacketFormat::Esp32Csi);
let data = b"CSI_DATA,AA:BB:CC:DD:EE:FF,-45,6,128,1.0,0.5,2.0,0.6,3.0,0.7";
let packet = parser.parse(data).unwrap();
assert_eq!(packet.rssi, -45);
assert_eq!(packet.metadata.channel, 6);
assert_eq!(packet.amplitudes.len(), 3);
}
#[test]
fn test_receiver_stats() {
let mut stats = ReceiverStats::default();
stats.packets_received = 100;
stats.packets_parsed = 95;
assert!((stats.success_rate() - 0.95).abs() < 0.001);
stats.reset();
assert_eq!(stats.packets_received, 0);
}
#[test]
fn test_csi_packet_to_readings() {
let packet = CsiPacket {
timestamp: Utc::now(),
source_id: "test".to_string(),
amplitudes: vec![1.0, 2.0, 3.0],
phases: vec![0.1, 0.2, 0.3],
rssi: -45,
noise_floor: -92,
metadata: CsiPacketMetadata {
channel: 6,
..Default::default()
},
raw_data: None,
};
let readings: CsiReadings = packet.into();
assert_eq!(readings.readings.len(), 1);
assert_eq!(readings.readings[0].amplitudes.len(), 3);
assert_eq!(readings.metadata.channel, 6);
}
#[test]
fn test_serial_receiver_buffer() {
let config = ReceiverConfig::serial("/dev/ttyUSB0", 921600);
// Skip actual port check in test
let mut receiver = SerialCsiReceiver {
config,
port_path: "/dev/ttyUSB0".to_string(),
buffer: VecDeque::new(),
parser: CsiParser::new(CsiPacketFormat::Esp32Csi),
stats: ReceiverStats::default(),
running: true,
};
// Feed some data
let test_data = b"CSI_DATA,AA:BB:CC:DD:EE:FF,-45,6,128,1.0,0.5\n";
let expected_len = test_data.len() as u64;
receiver.feed_data(test_data);
assert_eq!(receiver.stats.bytes_received, expected_len);
// Extract packet
let packet_data = receiver.extract_packet_from_buffer();
assert!(packet_data.is_some());
}
}