feat: Add 12 ADRs for RuVector RVF integration and proof-of-reality
Comprehensive architecture decision records for integrating ruvnet/ruvector
into wifi-densepose, covering:
- ADR-002: Master integration strategy (phased rollout, new crate design)
- ADR-003: RVF cognitive containers for CSI data persistence
- ADR-004: HNSW vector search replacing fixed-threshold detection
- ADR-005: SONA self-learning with LoRA + EWC++ for online adaptation
- ADR-006: GNN-enhanced pattern recognition with temporal modeling
- ADR-007: Post-quantum cryptography (ML-DSA-65 hybrid signatures)
- ADR-008: Raft consensus for multi-AP distributed coordination
- ADR-009: RVF WASM runtime for edge/browser/IoT deployment
- ADR-010: Witness chains for tamper-evident audit trails
- ADR-011: Mock elimination and proof-of-reality (fixes np.random.rand
placeholders, ships CSI capture + SHA-256 verified pipeline)
- ADR-012: ESP32 CSI sensor mesh ($54 starter kit specification)
- ADR-013: Feature-level sensing on commodity gear (zero-cost RSSI path)
ADR-011 directly addresses the credibility gap by cataloging every
mock/placeholder in the Python codebase and specifying concrete fixes.
https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714
This commit is contained in:
383
docs/adr/ADR-013-feature-level-sensing-commodity-gear.md
Normal file
383
docs/adr/ADR-013-feature-level-sensing-commodity-gear.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# ADR-013: Feature-Level Sensing on Commodity Gear (Option 3)
|
||||
|
||||
## Status
|
||||
Proposed
|
||||
|
||||
## Date
|
||||
2026-02-28
|
||||
|
||||
## Context
|
||||
|
||||
### Not Everyone Can Deploy Custom Hardware
|
||||
|
||||
ADR-012 specifies an ESP32 CSI mesh that provides real CSI data. However, it requires:
|
||||
- Purchasing ESP32 boards
|
||||
- Flashing custom firmware
|
||||
- ESP-IDF toolchain installation
|
||||
- Physical placement of nodes
|
||||
|
||||
For many users - especially those evaluating WiFi-DensePose or deploying in managed environments - modifying hardware is not an option. We need a sensing path that works with **existing, unmodified consumer WiFi gear**.
|
||||
|
||||
### What Commodity Hardware Exposes
|
||||
|
||||
Standard WiFi drivers and tools expose several metrics without custom firmware:
|
||||
|
||||
| Signal | Source | Availability | Sampling Rate |
|
||||
|--------|--------|-------------|---------------|
|
||||
| RSSI (Received Signal Strength) | `iwconfig`, `iw`, NetworkManager | Universal | 1-10 Hz |
|
||||
| Noise floor | `iw dev wlan0 survey dump` | Most Linux drivers | ~1 Hz |
|
||||
| Link quality | `/proc/net/wireless` | Linux | 1-10 Hz |
|
||||
| MCS index / PHY rate | `iw dev wlan0 link` | Most drivers | Per-packet |
|
||||
| TX/RX bytes | `/sys/class/net/wlan0/statistics/` | Universal | Continuous |
|
||||
| Retry count | `iw dev wlan0 station dump` | Most drivers | ~1 Hz |
|
||||
| Beacon interval timing | `iw dev wlan0 scan dump` | Universal | Per-scan |
|
||||
| Channel utilization | `iw dev wlan0 survey dump` | Most drivers | ~1 Hz |
|
||||
|
||||
**RSSI is the primary signal**. It varies when humans move through the propagation path between any transmitter-receiver pair. Research confirms RSSI-based sensing for:
|
||||
- Presence detection (single receiver, threshold on variance)
|
||||
- Device-free motion detection (RSSI variance increases with movement)
|
||||
- Coarse room-level localization (multi-receiver RSSI fingerprinting)
|
||||
- Breathing detection (specialized setups, marginal quality)
|
||||
|
||||
### Research Support
|
||||
|
||||
- **RSSI-based presence**: Youssef et al. (2007) demonstrated device-free passive detection using RSSI from multiple receivers with >90% accuracy.
|
||||
- **RSSI breathing**: Abdelnasser et al. (2015) showed respiration detection via RSSI variance in controlled settings with ~85% accuracy using 4+ receivers.
|
||||
- **Device-free tracking**: Multiple receivers with RSSI fingerprinting achieve room-level (3-5m) accuracy.
|
||||
|
||||
## Decision
|
||||
|
||||
We will implement a Feature-Level Sensing module that extracts motion, presence, and coarse activity information from standard WiFi metrics available on any Linux machine without hardware modification.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ Feature-Level Sensing Pipeline │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Data Sources (any Linux WiFi device): │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────────┐ │
|
||||
│ │ RSSI │ │ Noise │ │ Link │ │ Packet Stats │ │
|
||||
│ │ Stream │ │ Floor │ │ Quality │ │ (TX/RX/Retry)│ │
|
||||
│ └────┬────┘ └────┬────┘ └────┬────┘ └──────┬───────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ └───────────┴───────────┴──────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────┐ │
|
||||
│ │ Feature Extraction Engine │ │
|
||||
│ │ │ │
|
||||
│ │ 1. Rolling statistics (mean, var, skew, kurt) │ │
|
||||
│ │ 2. Spectral features (FFT of RSSI time series) │ │
|
||||
│ │ 3. Change-point detection (CUSUM, PELT) │ │
|
||||
│ │ 4. Cross-receiver correlation │ │
|
||||
│ │ 5. Packet timing jitter analysis │ │
|
||||
│ └────────────────────────┬───────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────┐ │
|
||||
│ │ Classification / Decision │ │
|
||||
│ │ │ │
|
||||
│ │ • Presence: RSSI variance > threshold │ │
|
||||
│ │ • Motion class: spectral peak frequency │ │
|
||||
│ │ • Occupancy change: change-point event │ │
|
||||
│ │ • Confidence: cross-receiver agreement │ │
|
||||
│ └────────────────────────┬───────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────┐ │
|
||||
│ │ Output: Presence/Motion Events │ │
|
||||
│ │ │ │
|
||||
│ │ { "timestamp": "...", │ │
|
||||
│ │ "presence": true, │ │
|
||||
│ │ "motion_level": "active", │ │
|
||||
│ │ "confidence": 0.87, │ │
|
||||
│ │ "receivers_agreeing": 3, │ │
|
||||
│ │ "rssi_variance": 4.2 } │ │
|
||||
│ └────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Feature Extraction Specification
|
||||
|
||||
```python
|
||||
class RssiFeatureExtractor:
|
||||
"""Extract sensing features from RSSI and link statistics.
|
||||
|
||||
No custom hardware required. Works with any WiFi interface
|
||||
that exposes standard Linux wireless statistics.
|
||||
"""
|
||||
|
||||
def __init__(self, config: FeatureSensingConfig):
|
||||
self.window_size = config.window_size # 30 seconds
|
||||
self.sampling_rate = config.sampling_rate # 10 Hz
|
||||
self.rssi_buffer = deque(maxlen=self.window_size * self.sampling_rate)
|
||||
self.noise_buffer = deque(maxlen=self.window_size * self.sampling_rate)
|
||||
|
||||
def extract_features(self) -> FeatureVector:
|
||||
rssi_array = np.array(self.rssi_buffer)
|
||||
|
||||
return FeatureVector(
|
||||
# Time-domain statistics
|
||||
rssi_mean=np.mean(rssi_array),
|
||||
rssi_variance=np.var(rssi_array),
|
||||
rssi_skewness=scipy.stats.skew(rssi_array),
|
||||
rssi_kurtosis=scipy.stats.kurtosis(rssi_array),
|
||||
rssi_range=np.ptp(rssi_array),
|
||||
rssi_iqr=np.subtract(*np.percentile(rssi_array, [75, 25])),
|
||||
|
||||
# Spectral features (FFT of RSSI time series)
|
||||
spectral_energy=self._spectral_energy(rssi_array),
|
||||
dominant_frequency=self._dominant_freq(rssi_array),
|
||||
breathing_band_power=self._band_power(rssi_array, 0.1, 0.5), # Hz
|
||||
motion_band_power=self._band_power(rssi_array, 0.5, 3.0), # Hz
|
||||
|
||||
# Change-point features
|
||||
num_change_points=self._cusum_changes(rssi_array),
|
||||
max_step_magnitude=self._max_step(rssi_array),
|
||||
|
||||
# Noise floor features (environment stability)
|
||||
noise_mean=np.mean(np.array(self.noise_buffer)),
|
||||
snr_estimate=np.mean(rssi_array) - np.mean(np.array(self.noise_buffer)),
|
||||
)
|
||||
|
||||
def _spectral_energy(self, rssi: np.ndarray) -> float:
|
||||
"""Total spectral energy excluding DC component."""
|
||||
spectrum = np.abs(scipy.fft.rfft(rssi - np.mean(rssi)))
|
||||
return float(np.sum(spectrum[1:] ** 2))
|
||||
|
||||
def _dominant_freq(self, rssi: np.ndarray) -> float:
|
||||
"""Dominant frequency in RSSI time series."""
|
||||
spectrum = np.abs(scipy.fft.rfft(rssi - np.mean(rssi)))
|
||||
freqs = scipy.fft.rfftfreq(len(rssi), d=1.0/self.sampling_rate)
|
||||
return float(freqs[np.argmax(spectrum[1:]) + 1])
|
||||
|
||||
def _band_power(self, rssi: np.ndarray, low_hz: float, high_hz: float) -> float:
|
||||
"""Power in a specific frequency band."""
|
||||
spectrum = np.abs(scipy.fft.rfft(rssi - np.mean(rssi))) ** 2
|
||||
freqs = scipy.fft.rfftfreq(len(rssi), d=1.0/self.sampling_rate)
|
||||
mask = (freqs >= low_hz) & (freqs <= high_hz)
|
||||
return float(np.sum(spectrum[mask]))
|
||||
|
||||
def _cusum_changes(self, rssi: np.ndarray) -> int:
|
||||
"""Count change points using CUSUM algorithm."""
|
||||
mean = np.mean(rssi)
|
||||
cusum_pos = np.zeros_like(rssi)
|
||||
cusum_neg = np.zeros_like(rssi)
|
||||
threshold = 3.0 * np.std(rssi)
|
||||
changes = 0
|
||||
for i in range(1, len(rssi)):
|
||||
cusum_pos[i] = max(0, cusum_pos[i-1] + rssi[i] - mean - 0.5)
|
||||
cusum_neg[i] = max(0, cusum_neg[i-1] - rssi[i] + mean - 0.5)
|
||||
if cusum_pos[i] > threshold or cusum_neg[i] > threshold:
|
||||
changes += 1
|
||||
cusum_pos[i] = 0
|
||||
cusum_neg[i] = 0
|
||||
return changes
|
||||
```
|
||||
|
||||
### Data Collection (No Root Required)
|
||||
|
||||
```python
|
||||
class LinuxWifiCollector:
|
||||
"""Collect WiFi statistics from standard Linux interfaces.
|
||||
|
||||
No root required for most operations.
|
||||
No custom drivers or firmware.
|
||||
Works with NetworkManager, wpa_supplicant, or raw iw.
|
||||
"""
|
||||
|
||||
def __init__(self, interface: str = "wlan0"):
|
||||
self.interface = interface
|
||||
|
||||
def get_rssi(self) -> float:
|
||||
"""Get current RSSI from connected AP."""
|
||||
# Method 1: /proc/net/wireless (no root)
|
||||
with open("/proc/net/wireless") as f:
|
||||
for line in f:
|
||||
if self.interface in line:
|
||||
parts = line.split()
|
||||
return float(parts[3].rstrip('.'))
|
||||
|
||||
# Method 2: iw (no root for own station)
|
||||
result = subprocess.run(
|
||||
["iw", "dev", self.interface, "link"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'signal:' in line:
|
||||
return float(line.split(':')[1].strip().split()[0])
|
||||
|
||||
raise SensingError(f"Cannot read RSSI from {self.interface}")
|
||||
|
||||
def get_noise_floor(self) -> float:
|
||||
"""Get noise floor estimate."""
|
||||
result = subprocess.run(
|
||||
["iw", "dev", self.interface, "survey", "dump"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'noise:' in line:
|
||||
return float(line.split(':')[1].strip().split()[0])
|
||||
return -95.0 # Default noise floor estimate
|
||||
|
||||
def get_link_stats(self) -> dict:
|
||||
"""Get link quality statistics."""
|
||||
result = subprocess.run(
|
||||
["iw", "dev", self.interface, "station", "dump"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
stats = {}
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'tx bytes:' in line:
|
||||
stats['tx_bytes'] = int(line.split(':')[1].strip())
|
||||
elif 'rx bytes:' in line:
|
||||
stats['rx_bytes'] = int(line.split(':')[1].strip())
|
||||
elif 'tx retries:' in line:
|
||||
stats['tx_retries'] = int(line.split(':')[1].strip())
|
||||
elif 'signal:' in line:
|
||||
stats['signal'] = float(line.split(':')[1].strip().split()[0])
|
||||
return stats
|
||||
```
|
||||
|
||||
### Classification Rules
|
||||
|
||||
```python
|
||||
class PresenceClassifier:
|
||||
"""Rule-based presence and motion classifier.
|
||||
|
||||
Uses simple, interpretable rules rather than ML to ensure
|
||||
transparency and debuggability.
|
||||
"""
|
||||
|
||||
def __init__(self, config: ClassifierConfig):
|
||||
self.variance_threshold = config.variance_threshold # 2.0 dBm²
|
||||
self.motion_threshold = config.motion_threshold # 5.0 dBm²
|
||||
self.spectral_threshold = config.spectral_threshold # 10.0
|
||||
self.confidence_min_receivers = config.min_receivers # 2
|
||||
|
||||
def classify(self, features: FeatureVector,
|
||||
multi_receiver: list[FeatureVector] = None) -> SensingResult:
|
||||
|
||||
# Presence: RSSI variance exceeds empty-room baseline
|
||||
presence = features.rssi_variance > self.variance_threshold
|
||||
|
||||
# Motion level
|
||||
if features.rssi_variance > self.motion_threshold:
|
||||
motion = MotionLevel.ACTIVE
|
||||
elif features.rssi_variance > self.variance_threshold:
|
||||
motion = MotionLevel.PRESENT_STILL
|
||||
else:
|
||||
motion = MotionLevel.ABSENT
|
||||
|
||||
# Confidence from spectral energy and receiver agreement
|
||||
spectral_conf = min(1.0, features.spectral_energy / self.spectral_threshold)
|
||||
if multi_receiver:
|
||||
agreeing = sum(1 for f in multi_receiver
|
||||
if (f.rssi_variance > self.variance_threshold) == presence)
|
||||
receiver_conf = agreeing / len(multi_receiver)
|
||||
else:
|
||||
receiver_conf = 0.5 # Single receiver = lower confidence
|
||||
|
||||
confidence = 0.6 * spectral_conf + 0.4 * receiver_conf
|
||||
|
||||
return SensingResult(
|
||||
presence=presence,
|
||||
motion_level=motion,
|
||||
confidence=confidence,
|
||||
dominant_frequency=features.dominant_frequency,
|
||||
breathing_band_power=features.breathing_band_power,
|
||||
)
|
||||
```
|
||||
|
||||
### Capability Matrix (Honest Assessment)
|
||||
|
||||
| Capability | Single Receiver | 3 Receivers | 6 Receivers | Accuracy |
|
||||
|-----------|----------------|-------------|-------------|----------|
|
||||
| Binary presence | Yes | Yes | Yes | 90-95% |
|
||||
| Coarse motion (still/moving) | Yes | Yes | Yes | 85-90% |
|
||||
| Room-level location | No | Marginal | Yes | 70-80% |
|
||||
| Person count | No | Marginal | Marginal | 50-70% |
|
||||
| Activity class (walk/sit/stand) | Marginal | Marginal | Yes | 60-75% |
|
||||
| Respiration detection | No | Marginal | Marginal | 40-60% |
|
||||
| Heartbeat | No | No | No | N/A |
|
||||
| Body pose | No | No | No | N/A |
|
||||
|
||||
**Bottom line**: Feature-level sensing on commodity gear does presence and motion well. It does NOT do pose estimation, heartbeat, or reliable respiration. Any claim otherwise would be dishonest.
|
||||
|
||||
### Decision Matrix: Option 2 (ESP32) vs Option 3 (Commodity)
|
||||
|
||||
| Factor | ESP32 CSI (ADR-012) | Commodity (ADR-013) |
|
||||
|--------|---------------------|---------------------|
|
||||
| Headline capability | Respiration + motion | Presence + coarse motion |
|
||||
| Hardware cost | $54 (3-node kit) | $0 (existing gear) |
|
||||
| Setup time | 2-4 hours | 15 minutes |
|
||||
| Technical barrier | Medium (firmware flash) | Low (pip install) |
|
||||
| Data quality | Real CSI (amplitude + phase) | RSSI only |
|
||||
| Multi-person | Marginal | Poor |
|
||||
| Pose estimation | Marginal | No |
|
||||
| Reproducibility | High (controlled hardware) | Medium (varies by hardware) |
|
||||
| Public credibility | High (real CSI artifact) | Medium (RSSI is "obvious") |
|
||||
|
||||
### Proof Bundle for Commodity Sensing
|
||||
|
||||
```
|
||||
v1/data/proof/commodity/
|
||||
├── rssi_capture_30sec.json # 30 seconds of RSSI from 3 receivers
|
||||
├── rssi_capture_meta.json # Hardware: Intel AX200, Router: TP-Link AX1800
|
||||
├── scenario.txt # "Person walks through room at t=10s, sits at t=20s"
|
||||
├── expected_features.json # Feature extraction output
|
||||
├── expected_classification.json # Classification output
|
||||
├── expected_features.sha256 # Verification hash
|
||||
└── verify_commodity.py # One-command verification
|
||||
```
|
||||
|
||||
### Integration with WiFi-DensePose Pipeline
|
||||
|
||||
The commodity sensing module outputs the same `SensingResult` type as the CSI pipeline, allowing graceful degradation:
|
||||
|
||||
```python
|
||||
class SensingBackend(Protocol):
|
||||
"""Common interface for all sensing backends."""
|
||||
|
||||
def get_features(self) -> FeatureVector: ...
|
||||
def get_capabilities(self) -> set[Capability]: ...
|
||||
|
||||
class CsiBackend(SensingBackend):
|
||||
"""Full CSI pipeline (ESP32 or research NIC)."""
|
||||
def get_capabilities(self):
|
||||
return {Capability.PRESENCE, Capability.MOTION, Capability.RESPIRATION,
|
||||
Capability.LOCATION, Capability.POSE}
|
||||
|
||||
class CommodityBackend(SensingBackend):
|
||||
"""RSSI-only commodity hardware."""
|
||||
def get_capabilities(self):
|
||||
return {Capability.PRESENCE, Capability.MOTION}
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Zero-cost entry**: Works with existing WiFi hardware
|
||||
- **15-minute setup**: `pip install wifi-densepose && wdp sense --interface wlan0`
|
||||
- **Broad adoption**: Any Linux laptop, Pi, or phone can participate
|
||||
- **Honest capability reporting**: `get_capabilities()` tells users exactly what works
|
||||
- **Complements ESP32**: Users start with commodity, upgrade to ESP32 for more capability
|
||||
- **No mock data**: Real RSSI from real hardware, deterministic pipeline
|
||||
|
||||
### Negative
|
||||
- **Limited capability**: No pose, no heartbeat, marginal respiration
|
||||
- **Hardware variability**: RSSI calibration differs across chipsets
|
||||
- **Environmental sensitivity**: Commodity RSSI is more affected by interference than CSI
|
||||
- **Not a "pose estimation" demo**: This module honestly cannot do what the project name implies
|
||||
- **Lower credibility ceiling**: RSSI sensing is well-known; less impressive than CSI
|
||||
|
||||
## References
|
||||
|
||||
- [Youssef et al. - Challenges in Device-Free Passive Localization](https://doi.org/10.1145/1287853.1287880)
|
||||
- [Device-Free WiFi Sensing Survey](https://arxiv.org/abs/1901.09683)
|
||||
- [RSSI-based Breathing Detection](https://ieeexplore.ieee.org/document/7127688)
|
||||
- [Linux Wireless Tools](https://wireless.wiki.kernel.org/en/users/documentation/iw)
|
||||
- ADR-011: Python Proof-of-Reality and Mock Elimination
|
||||
- ADR-012: ESP32 CSI Sensor Mesh
|
||||
Reference in New Issue
Block a user