diff --git a/README.md b/README.md index 474bc3e..c1135d7 100644 --- a/README.md +++ b/README.md @@ -1302,6 +1302,20 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +## Changelog + +### v2.1.0 — 2026-02-28 + +- **RuVector RVF integration** — Architecture Decision Records (ADR-002 through ADR-013) defining integration of RVF cognitive containers, HNSW vector search, SONA self-learning, GNN pattern recognition, post-quantum cryptography, distributed consensus, WASM edge runtime, and witness chains +- **ESP32 CSI sensor mesh** — Firmware specification for $54 starter kit with 3-6 ESP32-S3 nodes, feature-level fusion aggregator, and UDP streaming (ADR-012) +- **Commodity WiFi sensing** — Zero-cost presence/motion detection via RSSI from any Linux WiFi adapter using `/proc/net/wireless` and `iw` (ADR-013) +- **Deterministic proof bundle** — One-command pipeline verification (`./verify`) with SHA-256 hash matching against a published reference signal +- **Real Doppler extraction** — Temporal phase-difference FFT across CSI history frames for true Doppler spectrum computation +- **Three.js visualization** — 3D body model with 24 DensePose body parts, signal visualization, environment rendering, and WebSocket streaming +- **Commodity sensing module** — `RssiFeatureExtractor` with FFT spectral analysis, CUSUM change detection, and `PresenceClassifier` with rule-based logic +- **CI verification pipeline** — GitHub Actions workflow that verifies pipeline determinism and scans for unseeded random calls in production code +- **Rust hardware adapters** — ESP32, Intel 5300, Atheros, UDP, and PCAP adapters now return explicit errors when no hardware is connected instead of silent empty data + ## 🙏 Acknowledgments - **Research Foundation**: Based on groundbreaking research in WiFi-based human sensing diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/integration/csi_receiver.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/integration/csi_receiver.rs index aa75fb3..ec99529 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/integration/csi_receiver.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/integration/csi_receiver.rs @@ -1104,25 +1104,12 @@ impl CsiParser { 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()), - }) + // PicoScenes CSI segment parsing is not yet implemented. + // The format requires parsing DeviceType, RxSBasic, CSI, and MVMExtra segments. + // See https://ps.zpj.io/packet-format.html for the full specification. + Err(AdapterError::DataFormat( + "PicoScenes CSI parser not yet implemented. Packet received but segment parsing (DeviceType, RxSBasic, CSI, MVMExtra) is required. See https://ps.zpj.io/packet-format.html".into() + )) } /// Parse JSON CSI format diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/integration/hardware_adapter.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/integration/hardware_adapter.rs index 728cd23..117514a 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/integration/hardware_adapter.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/integration/hardware_adapter.rs @@ -745,88 +745,28 @@ impl HardwareAdapter { _ => return Err(AdapterError::Config("Invalid settings for ESP32".into())), }; - // In a real implementation, this would read from the serial port - // and parse ESP-CSI format data - tracing::trace!("Reading ESP32 CSI from {}", settings.port); - - // Simulate read delay - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - - // Return placeholder - real implementation would parse serial data - Ok(CsiReadings { - timestamp: Utc::now(), - readings: vec![], - metadata: CsiMetadata { - device_type: DeviceType::Esp32, - channel: config.channel_config.channel, - bandwidth: config.channel_config.bandwidth, - num_subcarriers: config.channel_config.num_subcarriers, - rssi: None, - noise_floor: None, - fc_type: FrameControlType::Data, - }, - }) + Err(AdapterError::Hardware(format!( + "ESP32 CSI hardware adapter not yet implemented. Serial port {} configured but no parser available. See ADR-012 for ESP32 firmware specification.", + settings.port + ))) } /// Read CSI from Intel 5300 NIC - async fn read_intel_5300_csi(config: &HardwareConfig) -> Result { - // Intel 5300 uses connector interface from Linux CSI Tool - tracing::trace!("Reading Intel 5300 CSI"); - - // In a real implementation, this would: - // 1. Open /proc/net/connector (netlink socket) - // 2. Listen for BFEE_NOTIF messages - // 3. Parse the bfee struct - - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - - Ok(CsiReadings { - timestamp: Utc::now(), - readings: vec![], - metadata: CsiMetadata { - device_type: DeviceType::Intel5300, - channel: config.channel_config.channel, - bandwidth: config.channel_config.bandwidth, - num_subcarriers: 30, // Intel 5300 provides 30 subcarriers - rssi: None, - noise_floor: None, - fc_type: FrameControlType::Data, - }, - }) + async fn read_intel_5300_csi(_config: &HardwareConfig) -> Result { + Err(AdapterError::Hardware( + "Intel 5300 CSI adapter not yet implemented. Requires Linux CSI Tool kernel module and netlink connector parsing.".into() + )) } /// Read CSI from Atheros NIC async fn read_atheros_csi( - config: &HardwareConfig, + _config: &HardwareConfig, driver: AtherosDriver, ) -> Result { - tracing::trace!("Reading Atheros ({:?}) CSI", driver); - - // In a real implementation, this would: - // 1. Read from debugfs CSI buffer - // 2. Parse driver-specific CSI format - - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - - let num_subcarriers = match driver { - AtherosDriver::Ath9k => 56, - AtherosDriver::Ath10k => 114, - AtherosDriver::Ath11k => 234, - }; - - Ok(CsiReadings { - timestamp: Utc::now(), - readings: vec![], - metadata: CsiMetadata { - device_type: DeviceType::Atheros(driver), - channel: config.channel_config.channel, - bandwidth: config.channel_config.bandwidth, - num_subcarriers, - rssi: None, - noise_floor: None, - fc_type: FrameControlType::Data, - }, - }) + Err(AdapterError::Hardware(format!( + "Atheros {:?} CSI adapter not yet implemented. Requires debugfs CSI buffer parsing.", + driver + ))) } /// Read CSI from UDP socket @@ -836,24 +776,10 @@ impl HardwareAdapter { _ => return Err(AdapterError::Config("Invalid settings for UDP".into())), }; - tracing::trace!("Reading UDP CSI on {}:{}", settings.bind_address, settings.port); - - // Placeholder - real implementation would receive and parse UDP packets - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - - Ok(CsiReadings { - timestamp: Utc::now(), - readings: vec![], - metadata: CsiMetadata { - device_type: DeviceType::UdpReceiver, - channel: config.channel_config.channel, - bandwidth: config.channel_config.bandwidth, - num_subcarriers: config.channel_config.num_subcarriers, - rssi: None, - noise_floor: None, - fc_type: FrameControlType::Data, - }, - }) + Err(AdapterError::Hardware(format!( + "UDP CSI receiver not yet implemented. Bind address {}:{} configured but no packet parser available.", + settings.bind_address, settings.port + ))) } /// Read CSI from PCAP file @@ -863,27 +789,10 @@ impl HardwareAdapter { _ => return Err(AdapterError::Config("Invalid settings for PCAP".into())), }; - tracing::trace!("Reading PCAP CSI from {}", settings.file_path); - - // Placeholder - real implementation would read and parse PCAP packets - tokio::time::sleep(tokio::time::Duration::from_millis( - (10.0 / settings.playback_speed) as u64, - )) - .await; - - Ok(CsiReadings { - timestamp: Utc::now(), - readings: vec![], - metadata: CsiMetadata { - device_type: DeviceType::PcapFile, - channel: config.channel_config.channel, - bandwidth: config.channel_config.bandwidth, - num_subcarriers: config.channel_config.num_subcarriers, - rssi: None, - noise_floor: None, - fc_type: FrameControlType::Data, - }, - }) + Err(AdapterError::Hardware(format!( + "PCAP CSI reader not yet implemented. File {} configured but no packet parser available.", + settings.file_path + ))) } /// Generate simulated CSI data diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/localization/fusion.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/localization/fusion.rs index 6653527..e002d2f 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/localization/fusion.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/localization/fusion.rs @@ -73,17 +73,21 @@ impl LocalizationService { Some(position_3d) } - /// Simulate RSSI measurements (placeholder for real sensor data) + /// Read RSSI measurements from sensors. + /// + /// Returns empty when no real sensor hardware is connected. + /// Real RSSI readings require ESP32 mesh (ADR-012) or Linux WiFi interface (ADR-013). + /// Caller handles empty readings by returning None/default. fn simulate_rssi_measurements( &self, - sensors: &[crate::domain::SensorPosition], + _sensors: &[crate::domain::SensorPosition], _vitals: &VitalSignsReading, ) -> Vec<(String, f64)> { - // In production, this would read actual sensor values - // For now, return placeholder values - sensors.iter() - .map(|s| (s.id.clone(), -50.0 + rand_range(-10.0, 10.0))) - .collect() + // No real sensor hardware connected - return empty. + // Real RSSI readings require ESP32 mesh (ADR-012) or Linux WiFi interface (ADR-013). + // Caller handles empty readings by returning None from estimate_position. + tracing::warn!("No sensor hardware connected. Real RSSI readings require ESP32 mesh (ADR-012) or Linux WiFi interface (ADR-013)."); + vec![] } /// Estimate debris profile for the zone @@ -309,18 +313,6 @@ impl Default for PositionFuser { } } -/// Simple random range (for simulation) -fn rand_range(min: f64, max: f64) -> f64 { - use std::time::{SystemTime, UNIX_EPOCH}; - - let seed = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_nanos() as u64) - .unwrap_or(0); - - let pseudo_random = ((seed * 1103515245 + 12345) % (1 << 31)) as f64 / (1u64 << 31) as f64; - min + pseudo_random * (max - min) -} #[cfg(test)] mod tests { diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/densepose.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/densepose.rs index e9be69a..cb9c61d 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/densepose.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/densepose.rs @@ -233,15 +233,17 @@ impl DensePoseHead { /// /// This performs inference using loaded weights. For ONNX-based inference, /// use the ONNX backend directly. + /// + /// # Errors + /// Returns an error if no model weights are loaded. Load weights with + /// `with_weights()` before calling forward(). Use `forward_mock()` in tests. pub fn forward(&self, input: &Tensor) -> NnResult { self.validate_input(input)?; - // If we have native weights, use them if let Some(ref _weights) = self.weights { self.forward_native(input) } else { - // Return mock output for testing when no weights are loaded - self.forward_mock(input) + Err(NnError::inference("No model weights loaded. Load weights with with_weights() before calling forward(). Use MockBackend for testing.")) } } @@ -283,6 +285,7 @@ impl DensePoseHead { } /// Mock forward pass for testing + #[cfg(test)] fn forward_mock(&self, input: &Tensor) -> NnResult { let shape = input.shape(); let batch = shape.dim(0).unwrap_or(1); @@ -551,13 +554,24 @@ mod tests { assert!(!head.has_weights()); } + #[test] + fn test_forward_without_weights_errors() { + let config = DensePoseConfig::new(256, 24, 2); + let head = DensePoseHead::new(config).unwrap(); + + let input = Tensor::zeros_4d([1, 256, 64, 64]); + let result = head.forward(&input); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("No model weights loaded")); + } + #[test] fn test_mock_forward_pass() { let config = DensePoseConfig::new(256, 24, 2); let head = DensePoseHead::new(config).unwrap(); let input = Tensor::zeros_4d([1, 256, 64, 64]); - let output = head.forward(&input).unwrap(); + let output = head.forward_mock(&input).unwrap(); // Check output shapes assert_eq!(output.segmentation.shape().dim(1), Some(25)); // 24 + 1 background diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/translator.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/translator.rs index 7ec1591..85595fa 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/translator.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/translator.rs @@ -282,47 +282,49 @@ impl ModalityTranslator { } /// Forward pass through the translator + /// + /// # Errors + /// Returns an error if no model weights are loaded. Load weights with + /// `with_weights()` before calling forward(). Use `forward_mock()` in tests. pub fn forward(&self, input: &Tensor) -> NnResult { self.validate_input(input)?; if let Some(ref _weights) = self.weights { self.forward_native(input) } else { - self.forward_mock(input) + Err(NnError::inference("No model weights loaded. Load weights with with_weights() before calling forward(). Use MockBackend for testing.")) } } /// Encode input to latent space + /// + /// # Errors + /// Returns an error if no model weights are loaded. pub fn encode(&self, input: &Tensor) -> NnResult> { self.validate_input(input)?; - let shape = input.shape(); - let batch = shape.dim(0).unwrap_or(1); - let height = shape.dim(2).unwrap_or(64); - let width = shape.dim(3).unwrap_or(64); - - // Mock encoder features at different scales - let mut features = Vec::new(); - let mut current_h = height; - let mut current_w = width; - - for (i, &channels) in self.config.hidden_channels.iter().enumerate() { - if i > 0 { - current_h /= 2; - current_w /= 2; - } - let feat = Tensor::zeros_4d([batch, channels, current_h.max(1), current_w.max(1)]); - features.push(feat); + if self.weights.is_none() { + return Err(NnError::inference("No model weights loaded. Cannot encode without weights.")); } - Ok(features) + // Real encoding through the encoder path of forward_native + let output = self.forward_native(input)?; + output.encoder_features.ok_or_else(|| { + NnError::inference("Encoder features not available from forward pass") + }) } /// Decode from latent space + /// + /// # Errors + /// Returns an error if no model weights are loaded or if encoded features are empty. pub fn decode(&self, encoded_features: &[Tensor]) -> NnResult { if encoded_features.is_empty() { return Err(NnError::invalid_input("No encoded features provided")); } + if self.weights.is_none() { + return Err(NnError::inference("No model weights loaded. Cannot decode without weights.")); + } let last_feat = encoded_features.last().unwrap(); let shape = last_feat.shape(); @@ -385,6 +387,7 @@ impl ModalityTranslator { } /// Mock forward pass for testing + #[cfg(test)] fn forward_mock(&self, input: &Tensor) -> NnResult { let shape = input.shape(); let batch = shape.dim(0).unwrap_or(1); @@ -669,28 +672,48 @@ mod tests { assert!(!translator.has_weights()); } + #[test] + fn test_forward_without_weights_errors() { + let config = TranslatorConfig::new(128, vec![256, 512, 256], 256); + let translator = ModalityTranslator::new(config).unwrap(); + + let input = Tensor::zeros_4d([1, 128, 64, 64]); + let result = translator.forward(&input); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("No model weights loaded")); + } + #[test] fn test_mock_forward() { let config = TranslatorConfig::new(128, vec![256, 512, 256], 256); let translator = ModalityTranslator::new(config).unwrap(); let input = Tensor::zeros_4d([1, 128, 64, 64]); - let output = translator.forward(&input).unwrap(); + let output = translator.forward_mock(&input).unwrap(); assert_eq!(output.features.shape().dim(1), Some(256)); } #[test] - fn test_encode_decode() { + fn test_encode_without_weights_errors() { let config = TranslatorConfig::new(128, vec![256, 512], 256); let translator = ModalityTranslator::new(config).unwrap(); let input = Tensor::zeros_4d([1, 128, 64, 64]); - let encoded = translator.encode(&input).unwrap(); - assert_eq!(encoded.len(), 2); + let result = translator.encode(&input); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("No model weights loaded")); + } - let decoded = translator.decode(&encoded).unwrap(); - assert_eq!(decoded.shape().dim(1), Some(256)); + #[test] + fn test_decode_without_weights_errors() { + let config = TranslatorConfig::new(128, vec![256, 512], 256); + let translator = ModalityTranslator::new(config).unwrap(); + + let features = vec![Tensor::zeros_4d([1, 512, 32, 32])]; + let result = translator.decode(&features); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("No model weights loaded")); } #[test]