The foundation modules that every ESP32 node runs. These handle gesture detection, signal quality monitoring, anomaly detection, zone occupancy, vital sign tracking, intrusion classification, and model packaging.
All seven modules compile to wasm32-unknown-unknown and run inside the WASM3 interpreter on ESP32-S3 after Tier 2 DSP completes (ADR-040). They share a common no_std-compatible design: a struct with const fn new(), a process_frame (or on_timer) entry point, and zero heap allocation.
| Module | File | What It Does | Compute Budget |
|---|---|---|---|
| Gesture Classifier | gesture.rs |
Recognizes hand gestures from CSI phase sequences using DTW template matching | ~2,400 f32 ops/frame (60x40 cost matrix) |
| Coherence Monitor | coherence.rs |
Measures signal quality via phasor coherence across subcarriers | ~100 trig ops/frame (32 subcarriers) |
| Anomaly Detector | adversarial.rs |
Flags physically impossible signals: phase jumps, flatlines, energy spikes | ~130 f32 ops/frame |
| Intrusion Detector | intrusion.rs |
Detects unauthorized entry via phase velocity and amplitude disturbance | ~130 f32 ops/frame |
| Occupancy Detector | occupancy.rs |
Divides sensing area into spatial zones and reports which are occupied | ~100 f32 ops/frame |
| Vital Trend Analyzer | vital_trend.rs |
Monitors breathing/heart rate over 1-min and 5-min windows for clinical alerts | ~20 f32 ops/timer tick |
| RVF Container | rvf.rs |
Binary container format that packages WASM modules with manifest and signature | Builder only (std), no per-frame cost |
What it does: Recognizes predefined hand gestures from WiFi CSI phase sequences. It compares a sliding window of phase deltas against 4 built-in templates (wave, push, pull, swipe) using Dynamic Time Warping.
How it works: Each incoming frame provides subcarrier phases. The detector computes the phase delta from the previous frame and pushes it into a 60-sample ring buffer. When enough samples accumulate, it runs constrained DTW (with a Sakoe-Chiba band of width 5) between the tail of the observation window and each template. If the best normalized distance falls below the threshold (2.5), the corresponding gesture ID is emitted. A 40-frame cooldown prevents duplicate detections.
| Item | Type | Description |
|---|---|---|
GestureDetector |
struct | Main state holder. Contains ring buffer, templates, and cooldown timer. |
GestureDetector::new() |
const fn |
Creates a detector with 4 built-in templates. |
GestureDetector::process_frame(&mut self, phases: &[f32]) -> Option<u8> |
method | Feed one frame of phase data. Returns Some(gesture_id) on match. |
MAX_TEMPLATE_LEN |
const (40) | Maximum number of samples in a gesture template. |
MAX_WINDOW_LEN |
const (60) | Maximum observation window length. |
NUM_TEMPLATES |
const (4) | Number of built-in templates. |
DTW_THRESHOLD |
const (2.5) | Normalized DTW distance threshold for a match. |
BAND_WIDTH |
const (5) | Sakoe-Chiba band width (limits warping). |
| Parameter | Default | Range | Description |
|---|---|---|---|
DTW_THRESHOLD |
2.5 | 0.5 -- 10.0 | Lower = stricter matching, fewer false positives but may miss soft gestures |
BAND_WIDTH |
5 | 1 -- 20 | Width of the Sakoe-Chiba band. Wider = more flexible time warping but more computation |
| Cooldown frames | 40 | 10 -- 200 | Frames to wait before next detection. At 20 Hz, 40 frames = 2 seconds |
| Event ID | Constant | When Emitted |
|---|---|---|
| 1 | event_types::GESTURE_DETECTED |
A gesture template matched. Value = gesture ID (1=wave, 2=push, 3=pull, 4=swipe). |
use wifi_densepose_wasm_edge::gesture::GestureDetector;
let mut detector = GestureDetector::new();
// Feed frames from CSI data (typically at 20 Hz).
let phases: Vec<f32> = get_csi_phases(); // your phase data
if let Some(gesture_id) = detector.process_frame(&phases) {
println!("Detected gesture {}", gesture_id);
// 1 = wave, 2 = push, 3 = pull, 4 = swipe
}-
Collect reference data: Record the phase-delta sequence for your gesture by feeding CSI frames through the detector and logging the delta values in the ring buffer.
-
Normalize the template: Scale the phase-delta values so they span roughly -1.0 to 1.0. This ensures consistent DTW distances across different signal strengths.
-
Edit the template array: In
gesture.rs, increaseNUM_TEMPLATESby 1 and add a new entry in thetemplatesarray insideGestureDetector::new():GestureTemplate { values: { let mut v = [0.0f32; MAX_TEMPLATE_LEN]; v[0] = 0.2; v[1] = 0.6; // ... your values v }, len: 8, // number of valid samples id: 5, // unique gesture ID },
-
Tune the threshold: Run test data through
dtw_distance()directly to see the distance between your template and real observations. AdjustDTW_THRESHOLDif your gesture is consistently matched at a distance higher than 2.5. -
Test: Add a unit test that feeds the template values as phase inputs and verifies that
process_framereturns your new gesture ID.
What it does: Measures the phase coherence of the WiFi signal across subcarriers. High coherence means the signal is stable and sensing is accurate. Low coherence means multipath interference or environmental changes are degrading the signal.
How it works: For each frame, it computes the inter-frame phase delta per subcarrier, converts each delta to a unit phasor (cos + j*sin), and averages them. The magnitude of this mean phasor is the raw coherence (0 = random, 1 = perfectly aligned). This raw value is smoothed with an exponential moving average (alpha = 0.1). A hysteresis gate classifies the result into Accept (>0.7), Warn (0.4--0.7), or Reject (<0.4).
| Item | Type | Description |
|---|---|---|
CoherenceMonitor |
struct | Tracks phasor sums, EMA score, and gate state. |
CoherenceMonitor::new() |
const fn |
Creates a monitor with initial coherence of 1.0 (Accept). |
process_frame(&mut self, phases: &[f32]) -> f32 |
method | Feed one frame of phase data. Returns EMA-smoothed coherence [0, 1]. |
gate_state(&self) -> GateState |
method | Current gate classification (Accept, Warn, Reject). |
mean_phasor_angle(&self) -> f32 |
method | Dominant phase drift direction in radians. |
coherence_score(&self) -> f32 |
method | Current EMA-smoothed coherence score. |
GateState |
enum | Accept, Warn, Reject -- signal quality classification. |
| Parameter | Default | Range | Description |
|---|---|---|---|
ALPHA |
0.1 | 0.01 -- 0.5 | EMA smoothing factor. Lower = slower response, more stable. Higher = faster response, more noisy |
HIGH_THRESHOLD |
0.7 | 0.5 -- 0.95 | Coherence above this = Accept |
LOW_THRESHOLD |
0.4 | 0.1 -- 0.6 | Coherence below this = Reject |
MAX_SC |
32 | 1 -- 64 | Maximum subcarriers tracked (compile-time) |
| Event ID | Constant | When Emitted |
|---|---|---|
| 2 | event_types::COHERENCE_SCORE |
Emitted every 20 frames with the current coherence score (from the combined pipeline in lib.rs). |
use wifi_densepose_wasm_edge::coherence::{CoherenceMonitor, GateState};
let mut monitor = CoherenceMonitor::new();
let phases: Vec<f32> = get_csi_phases();
let score = monitor.process_frame(&phases);
match monitor.gate_state() {
GateState::Accept => { /* full accuracy */ }
GateState::Warn => { /* predictions may be degraded */ }
GateState::Reject => { /* sensing unreliable, recalibrate */ }
}What it does: Detects physically impossible or suspicious CSI signals that may indicate sensor malfunction, RF jamming, replay attacks, or environmental interference. It runs three independent checks on every frame.
How it works: During the first 100 frames it accumulates a baseline (mean amplitude per subcarrier and mean total energy). After calibration, it checks each frame for three anomaly types:
- Phase jump: If more than 50% of subcarriers show a phase discontinuity greater than 2.5 radians, something non-physical happened.
- Amplitude flatline: If amplitude variance across subcarriers is near zero (below 0.001) while the mean is nonzero, the sensor may be stuck.
- Energy spike: If total signal energy exceeds 50x the baseline, an external source may be injecting power.
A 20-frame cooldown prevents event flooding.
| Item | Type | Description |
|---|---|---|
AnomalyDetector |
struct | Tracks baseline, previous phases, cooldown, and anomaly count. |
AnomalyDetector::new() |
const fn |
Creates an uncalibrated detector. |
process_frame(&mut self, phases: &[f32], amplitudes: &[f32]) -> bool |
method | Returns true if an anomaly is detected on this frame. |
total_anomalies(&self) -> u32 |
method | Lifetime count of detected anomalies. |
| Parameter | Default | Range | Description |
|---|---|---|---|
PHASE_JUMP_THRESHOLD |
2.5 rad | 1.0 -- pi | Phase jump to flag per subcarrier |
MIN_AMPLITUDE_VARIANCE |
0.001 | 0.0001 -- 0.1 | Below this = flatline |
MAX_ENERGY_RATIO |
50.0 | 5.0 -- 500.0 | Energy spike threshold vs baseline |
BASELINE_FRAMES |
100 | 50 -- 500 | Frames to calibrate baseline |
ANOMALY_COOLDOWN |
20 | 5 -- 100 | Frames between anomaly reports |
| Event ID | Constant | When Emitted |
|---|---|---|
| 3 | event_types::ANOMALY_DETECTED |
When any anomaly check fires (after cooldown). |
use wifi_densepose_wasm_edge::adversarial::AnomalyDetector;
let mut detector = AnomalyDetector::new();
// First 100 frames calibrate the baseline (always returns false).
for _ in 0..100 {
detector.process_frame(&phases, &litudes);
}
// Now anomalies are reported.
if detector.process_frame(&phases, &litudes) {
log!("Signal anomaly detected! Total: {}", detector.total_anomalies());
}What it does: Detects unauthorized entry into a monitored area. It is designed for security applications with a bias toward low false-negative rate (it would rather alarm falsely than miss a real intrusion).
How it works: The detector goes through four states:
- Calibrating (200 frames): Learns baseline amplitude mean and variance per subcarrier.
- Monitoring: Waits for the environment to be quiet (low disturbance for 100 consecutive frames) before arming.
- Armed: Actively watching. Computes a disturbance score combining phase velocity (60% weight) and amplitude deviation (40% weight). If disturbance exceeds 0.8 for 3 consecutive frames, it triggers an alert.
- Alert: Intrusion detected. Returns to Armed once disturbance drops below 0.3 for 50 frames.
| Item | Type | Description |
|---|---|---|
IntrusionDetector |
struct | State machine with baseline, debounce, and cooldown. |
IntrusionDetector::new() |
const fn |
Creates a detector in Calibrating state. |
process_frame(&mut self, phases: &[f32], amplitudes: &[f32]) -> &[(i32, f32)] |
method | Returns a slice of events (up to 4 per frame). |
state(&self) -> DetectorState |
method | Current state machine state. |
total_alerts(&self) -> u32 |
method | Lifetime alert count. |
DetectorState |
enum | Calibrating, Monitoring, Armed, Alert. |
| Parameter | Default | Range | Description |
|---|---|---|---|
INTRUSION_VELOCITY_THRESH |
1.5 rad/frame | 0.5 -- 3.0 | Phase velocity that counts as fast movement |
AMPLITUDE_CHANGE_THRESH |
3.0 sigma | 1.0 -- 10.0 | Amplitude deviation in standard deviations |
ARM_FRAMES |
100 | 20 -- 500 | Quiet frames needed to arm (at 20 Hz: 5 sec) |
DETECT_DEBOUNCE |
3 | 1 -- 10 | Consecutive detection frames before alert |
ALERT_COOLDOWN |
100 | 20 -- 500 | Frames between alerts |
BASELINE_FRAMES |
200 | 100 -- 1000 | Calibration window |
| Event ID | Constant | When Emitted |
|---|---|---|
| 200 | EVENT_INTRUSION_ALERT |
Intrusion detected. Value = disturbance score. |
| 201 | EVENT_INTRUSION_ZONE |
Identifies which subcarrier zone has the most disturbance. |
| 202 | EVENT_INTRUSION_ARMED |
Detector has armed after a quiet period. |
| 203 | EVENT_INTRUSION_DISARMED |
Detector disarmed (not currently emitted). |
use wifi_densepose_wasm_edge::intrusion::{IntrusionDetector, DetectorState};
let mut detector = IntrusionDetector::new();
// Calibrate and arm (feed quiet frames).
for _ in 0..300 {
detector.process_frame(&quiet_phases, &quiet_amps);
}
assert_eq!(detector.state(), DetectorState::Armed);
// Now process live data.
let events = detector.process_frame(&live_phases, &live_amps);
for &(event_type, value) in events {
if event_type == 200 {
trigger_alarm(value);
}
}What it does: Divides the sensing area into spatial zones (based on subcarrier groupings) and determines which zones are currently occupied by people. Useful for smart building applications such as HVAC control and lighting automation.
How it works: Subcarriers are divided into groups of 4, with each group representing a spatial zone (up to 8 zones). For each zone, the detector computes the variance of amplitude values within that group. During calibration (200 frames), it learns the baseline variance. After calibration, it computes the deviation from baseline, applies EMA smoothing (alpha=0.15), and uses a hysteresis threshold to classify each zone as occupied or empty. Events include per-zone occupancy (emitted every 10 frames) and zone transitions (emitted immediately on change).
| Item | Type | Description |
|---|---|---|
OccupancyDetector |
struct | Per-zone state, calibration accumulators, frame counter. |
OccupancyDetector::new() |
const fn |
Creates uncalibrated detector. |
process_frame(&mut self, phases: &[f32], amplitudes: &[f32]) -> &[(i32, f32)] |
method | Returns events (up to 12 per frame). |
occupied_count(&self) -> u8 |
method | Number of currently occupied zones. |
is_zone_occupied(&self, zone_id: usize) -> bool |
method | Check a specific zone. |
| Parameter | Default | Range | Description |
|---|---|---|---|
MAX_ZONES |
8 | 1 -- 16 | Maximum number of spatial zones |
ZONE_THRESHOLD |
0.02 | 0.005 -- 0.5 | Score above this = occupied. Hysteresis exit at 0.5x |
ALPHA |
0.15 | 0.05 -- 0.5 | EMA smoothing factor for zone scores |
BASELINE_FRAMES |
200 | 100 -- 1000 | Calibration window length |
| Event ID | Constant | When Emitted |
|---|---|---|
| 300 | EVENT_ZONE_OCCUPIED |
Every 10 frames for each occupied zone. Value = zone_id + confidence. |
| 301 | EVENT_ZONE_COUNT |
Every 10 frames. Value = total occupied zone count. |
| 302 | EVENT_ZONE_TRANSITION |
Immediately on zone state change. Value = zone_id + 0.5 (entered) or zone_id + 0.0 (vacated). |
use wifi_densepose_wasm_edge::occupancy::OccupancyDetector;
let mut detector = OccupancyDetector::new();
// Calibrate with empty-room data.
for _ in 0..200 {
detector.process_frame(&empty_phases, &empty_amps);
}
// Live monitoring.
let events = detector.process_frame(&live_phases, &live_amps);
println!("Occupied zones: {}", detector.occupied_count());
println!("Zone 0 occupied: {}", detector.is_zone_occupied(0));What it does: Monitors breathing rate and heart rate over time and alerts on clinically significant conditions. It tracks 1-minute and 5-minute trends and detects apnea, bradypnea, tachypnea, bradycardia, and tachycardia.
How it works: Called at 1 Hz with current vital sign readings (from Tier 2 DSP). It pushes each reading into a 300-sample ring buffer (5-minute history). Each call checks for:
- Apnea: Breathing BPM below 1.0 for 20+ consecutive seconds.
- Bradypnea: Sustained breathing below 12 BPM (5+ consecutive samples).
- Tachypnea: Sustained breathing above 25 BPM (5+ consecutive samples).
- Bradycardia: Sustained heart rate below 50 BPM (5+ consecutive samples).
- Tachycardia: Sustained heart rate above 120 BPM (5+ consecutive samples).
Every 60 seconds, it emits 1-minute averages for both breathing and heart rate.
| Item | Type | Description |
|---|---|---|
VitalTrendAnalyzer |
struct | Two ring buffers (breathing, heartrate), debounce counters, apnea counter. |
VitalTrendAnalyzer::new() |
const fn |
Creates analyzer with empty history. |
on_timer(&mut self, breathing_bpm: f32, heartrate_bpm: f32) -> &[(i32, f32)] |
method | Called at 1 Hz. Returns clinical alerts (up to 8). |
breathing_avg_1m(&self) -> f32 |
method | 1-minute breathing rate average. |
breathing_trend_5m(&self) -> f32 |
method | 5-minute breathing trend (positive = increasing). |
| Parameter | Default | Range | Description |
|---|---|---|---|
BRADYPNEA_THRESH |
12.0 BPM | 8 -- 15 | Below this = dangerously slow breathing |
TACHYPNEA_THRESH |
25.0 BPM | 20 -- 35 | Above this = dangerously fast breathing |
BRADYCARDIA_THRESH |
50.0 BPM | 40 -- 60 | Below this = dangerously slow heart rate |
TACHYCARDIA_THRESH |
120.0 BPM | 100 -- 150 | Above this = dangerously fast heart rate |
APNEA_SECONDS |
20 | 10 -- 60 | Seconds of near-zero breathing before alert |
ALERT_DEBOUNCE |
5 | 2 -- 15 | Consecutive abnormal samples before alert |
| Event ID | Constant | When Emitted |
|---|---|---|
| 100 | EVENT_VITAL_TREND |
Reserved for generic trend events. |
| 101 | EVENT_BRADYPNEA |
Sustained slow breathing. Value = current BPM. |
| 102 | EVENT_TACHYPNEA |
Sustained fast breathing. Value = current BPM. |
| 103 | EVENT_BRADYCARDIA |
Sustained slow heart rate. Value = current BPM. |
| 104 | EVENT_TACHYCARDIA |
Sustained fast heart rate. Value = current BPM. |
| 105 | EVENT_APNEA |
Breathing stopped. Value = seconds of apnea. |
| 110 | EVENT_BREATHING_AVG |
1-minute breathing average. Emitted every 60 seconds. |
| 111 | EVENT_HEARTRATE_AVG |
1-minute heart rate average. Emitted every 60 seconds. |
use wifi_densepose_wasm_edge::vital_trend::VitalTrendAnalyzer;
let mut analyzer = VitalTrendAnalyzer::new();
// Called at 1 Hz from the on_timer WASM export.
let events = analyzer.on_timer(breathing_bpm, heartrate_bpm);
for &(event_type, value) in events {
match event_type {
105 => alert_apnea(value as u32),
101 => alert_bradypnea(value),
104 => alert_tachycardia(value),
110 => log_breathing_avg(value),
_ => {}
}
}
// Query trend data.
let avg = analyzer.breathing_avg_1m();
let trend = analyzer.breathing_trend_5m();What it does: Defines the RVF (RuVector Format) binary container that packages a compiled WASM module with its manifest (name, author, capabilities, budget, hash) and an optional Ed25519 signature. This is the file format that gets uploaded to ESP32 nodes via the /api/wasm/upload endpoint.
How it works: The format has four sections laid out sequentially:
[Header: 32 bytes][Manifest: 96 bytes][WASM: N bytes][Signature: 0|64 bytes]
The header contains magic bytes (RVF\x01), format version, section sizes, and flags. The manifest describes the module's identity (name, author), resource requirements (max frame time, memory limit), and capability flags (which host APIs it needs). The WASM section is the raw compiled binary. The signature section is optional (indicated by FLAG_HAS_SIGNATURE) and covers everything before it.
The builder (available only with the std feature) creates RVF files from WASM binary data and a configuration struct. It automatically computes a SHA-256 hash of the WASM payload and embeds it in the manifest for integrity verification.
| Item | Type | Description |
|---|---|---|
RvfHeader |
#[repr(C, packed)] struct |
32-byte header with magic, version, section sizes. |
RvfManifest |
#[repr(C, packed)] struct |
96-byte manifest with module metadata. |
RvfConfig |
struct (std only) | Builder configuration input. |
build_rvf(wasm_data: &[u8], config: &RvfConfig) -> Vec<u8> |
function (std only) | Build a complete RVF container. |
patch_signature(rvf: &mut [u8], signature: &[u8; 64]) |
function (std only) | Patch an Ed25519 signature into an existing RVF. |
RVF_MAGIC |
const (0x0146_5652) |
Magic bytes: RVF\x01 as little-endian u32. |
RVF_FORMAT_VERSION |
const (1) | Current format version. |
RVF_HEADER_SIZE |
const (32) | Header size in bytes. |
RVF_MANIFEST_SIZE |
const (96) | Manifest size in bytes. |
RVF_SIGNATURE_LEN |
const (64) | Ed25519 signature length. |
RVF_HOST_API_V1 |
const (1) | Host API version this crate supports. |
| Flag | Value | Description |
|---|---|---|
CAP_READ_PHASE |
1 << 0 |
Module reads phase data |
CAP_READ_AMPLITUDE |
1 << 1 |
Module reads amplitude data |
CAP_READ_VARIANCE |
1 << 2 |
Module reads variance data |
CAP_READ_VITALS |
1 << 3 |
Module reads vital sign data |
CAP_READ_HISTORY |
1 << 4 |
Module reads phase history |
CAP_EMIT_EVENTS |
1 << 5 |
Module emits events |
CAP_LOG |
1 << 6 |
Module uses logging |
CAP_ALL |
0x7F |
All capabilities |
use wifi_densepose_wasm_edge::rvf::builder::{build_rvf, RvfConfig, patch_signature};
use wifi_densepose_wasm_edge::rvf::*;
// Read compiled WASM binary.
let wasm_data = std::fs::read("target/wasm32-unknown-unknown/release/my_module.wasm")?;
// Configure the module.
let config = RvfConfig {
module_name: "my-gesture-v2".into(),
author: "team-alpha".into(),
capabilities: CAP_READ_PHASE | CAP_EMIT_EVENTS,
max_frame_us: 5000, // 5 ms budget per frame
max_events_per_sec: 20,
memory_limit_kb: 64,
min_subcarriers: 8,
max_subcarriers: 64,
..Default::default()
};
// Build the RVF container.
let rvf = build_rvf(&wasm_data, &config);
// Optionally sign and patch.
let signature = sign_with_ed25519(&rvf[..rvf.len() - RVF_SIGNATURE_LEN]);
let mut rvf_mut = rvf;
patch_signature(&mut rvf_mut, &signature);
// Upload to ESP32.
std::fs::write("my-gesture-v2.rvf", &rvf_mut)?;From the crate directory:
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge
cargo test --features std -- gesture coherence adversarial intrusion occupancy vital_trend rvfThis runs all tests whose names contain any of the seven module names. The --features std flag is required because the RVF builder tests need sha2 and std::io.
All tests should pass:
running 32 tests
test adversarial::tests::test_anomaly_detector_init ... ok
test adversarial::tests::test_calibration_phase ... ok
test adversarial::tests::test_normal_signal_no_anomaly ... ok
test adversarial::tests::test_phase_jump_detection ... ok
test adversarial::tests::test_amplitude_flatline_detection ... ok
test adversarial::tests::test_energy_spike_detection ... ok
test adversarial::tests::test_cooldown_prevents_flood ... ok
test coherence::tests::test_coherence_monitor_init ... ok
test coherence::tests::test_empty_phases_returns_current_score ... ok
test coherence::tests::test_first_frame_returns_one ... ok
test coherence::tests::test_constant_phases_high_coherence ... ok
test coherence::tests::test_incoherent_phases_lower_coherence ... ok
test coherence::tests::test_gate_hysteresis ... ok
test coherence::tests::test_mean_phasor_angle_zero_for_no_drift ... ok
test gesture::tests::test_gesture_detector_init ... ok
test gesture::tests::test_empty_phases_returns_none ... ok
test gesture::tests::test_first_frame_initializes ... ok
test gesture::tests::test_constant_phase_no_gesture_after_cooldown ... ok
test gesture::tests::test_dtw_identical_sequences ... ok
test gesture::tests::test_dtw_different_sequences ... ok
test gesture::tests::test_dtw_empty_input ... ok
test gesture::tests::test_cooldown_prevents_duplicate_detection ... ok
test gesture::tests::test_window_ring_buffer_wraps ... ok
test intrusion::tests::test_intrusion_init ... ok
test intrusion::tests::test_calibration_phase ... ok
test intrusion::tests::test_arm_after_quiet ... ok
test intrusion::tests::test_intrusion_detection ... ok
test occupancy::tests::test_occupancy_detector_init ... ok
test occupancy::tests::test_occupancy_calibration ... ok
test occupancy::tests::test_occupancy_detection ... ok
test vital_trend::tests::test_vital_trend_init ... ok
test vital_trend::tests::test_normal_vitals_no_alerts ... ok
test vital_trend::tests::test_apnea_detection ... ok
test vital_trend::tests::test_tachycardia_detection ... ok
test vital_trend::tests::test_breathing_average ... ok
test rvf::builder::tests::test_build_rvf_roundtrip ... ok
test rvf::builder::tests::test_build_hash_integrity ... ok
| Module | Tests | Coverage |
|---|---|---|
gesture.rs |
8 | Init, empty input, first frame, constant input, DTW identical/different/empty, ring buffer wrap, cooldown |
coherence.rs |
7 | Init, empty input, first frame, constant phases, incoherent phases, gate hysteresis, phasor angle |
adversarial.rs |
7 | Init, calibration, normal signal, phase jump, flatline, energy spike, cooldown |
intrusion.rs |
4 | Init, calibration, arming, intrusion detection |
occupancy.rs |
3 | Init, calibration, zone detection |
vital_trend.rs |
5 | Init, normal vitals, apnea, tachycardia, breathing average |
rvf.rs |
2 | Build roundtrip, hash integrity |
All seven core modules share these design patterns:
Every module's main struct can be created with const fn new(), which means it can be placed in a static variable without runtime initialization. This is essential for WASM modules where there is no allocator.
static mut STATE: MyModule = MyModule::new();Modules that need a baseline (adversarial, intrusion, occupancy) follow the same pattern: accumulate statistics for N frames, compute mean/variance, then switch to detection mode. The calibration frame count is always a compile-time constant.
Both gesture (phase deltas) and vital_trend (BPM readings) use fixed-size ring buffers with modular index arithmetic. The pattern is:
self.values[self.idx] = new_value;
self.idx = (self.idx + 1) % MAX_SIZE;
if self.len < MAX_SIZE { self.len += 1; }Modules that return multiple events per frame (intrusion, occupancy, vital_trend) use static mut arrays as return buffers to avoid heap allocation. This is safe in single-threaded WASM but requires unsafe blocks. The pattern is:
static mut EVENTS: [(i32, f32); N] = [(0, 0.0); N];
let mut n_events = 0;
// ... populate EVENTS[n_events] ...
unsafe { &EVENTS[..n_events] }Every detection module uses a cooldown counter to prevent event flooding. After firing an event, the counter is set to a constant value and decremented each frame. No new events are emitted while the counter is positive.
Modules that track continuous scores (coherence, occupancy) use exponential moving average smoothing: smoothed = alpha * raw + (1 - alpha) * smoothed. The alpha constant controls responsiveness vs. stability.
To prevent oscillation at detection boundaries, modules use different thresholds for entering and exiting a state. For example, the coherence monitor requires a score above 0.7 to enter Accept but only drops to Reject below 0.4.