feat: Rust hardware adapters return errors instead of silent empty data, add changelog
- densepose.rs: forward() returns NnError when no weights loaded instead of zeros - translator.rs: forward/encode/decode require loaded weights, error otherwise - fusion.rs: remove rand_range() RNG, RSSI reads return empty with warning log - hardware_adapter.rs: ESP32/Intel/Atheros/UDP/PCAP adapters return AdapterError explaining hardware not connected instead of silent empty readings - csi_receiver.rs: PicoScenes parser returns error instead of empty amplitudes - README.md: add v2.1.0 changelog with all recent changes https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714
This commit is contained in:
14
README.md
14
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<CsiReadings, AdapterError> {
|
||||
// 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<CsiReadings, AdapterError> {
|
||||
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<CsiReadings, AdapterError> {
|
||||
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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<DensePoseOutput> {
|
||||
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<DensePoseOutput> {
|
||||
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
|
||||
|
||||
@@ -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<TranslatorOutput> {
|
||||
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<Vec<Tensor>> {
|
||||
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<Tensor> {
|
||||
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<TranslatorOutput> {
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user