Back to Ruview

Core Modules -- WiFi-DensePose Edge Intelligence

docs/edge-modules/core.md

0.7.028.3 KB
Original Source

Core Modules -- WiFi-DensePose Edge Intelligence

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.

Overview

ModuleFileWhat It DoesCompute Budget
Gesture Classifiergesture.rsRecognizes hand gestures from CSI phase sequences using DTW template matching~2,400 f32 ops/frame (60x40 cost matrix)
Coherence Monitorcoherence.rsMeasures signal quality via phasor coherence across subcarriers~100 trig ops/frame (32 subcarriers)
Anomaly Detectoradversarial.rsFlags physically impossible signals: phase jumps, flatlines, energy spikes~130 f32 ops/frame
Intrusion Detectorintrusion.rsDetects unauthorized entry via phase velocity and amplitude disturbance~130 f32 ops/frame
Occupancy Detectoroccupancy.rsDivides sensing area into spatial zones and reports which are occupied~100 f32 ops/frame
Vital Trend Analyzervital_trend.rsMonitors breathing/heart rate over 1-min and 5-min windows for clinical alerts~20 f32 ops/timer tick
RVF Containerrvf.rsBinary container format that packages WASM modules with manifest and signatureBuilder only (std), no per-frame cost

Modules


Gesture Classifier (gesture.rs)

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.

API

ItemTypeDescription
GestureDetectorstructMain state holder. Contains ring buffer, templates, and cooldown timer.
GestureDetector::new()const fnCreates a detector with 4 built-in templates.
GestureDetector::process_frame(&mut self, phases: &[f32]) -> Option<u8>methodFeed one frame of phase data. Returns Some(gesture_id) on match.
MAX_TEMPLATE_LENconst (40)Maximum number of samples in a gesture template.
MAX_WINDOW_LENconst (60)Maximum observation window length.
NUM_TEMPLATESconst (4)Number of built-in templates.
DTW_THRESHOLDconst (2.5)Normalized DTW distance threshold for a match.
BAND_WIDTHconst (5)Sakoe-Chiba band width (limits warping).

Configuration

ParameterDefaultRangeDescription
DTW_THRESHOLD2.50.5 -- 10.0Lower = stricter matching, fewer false positives but may miss soft gestures
BAND_WIDTH51 -- 20Width of the Sakoe-Chiba band. Wider = more flexible time warping but more computation
Cooldown frames4010 -- 200Frames to wait before next detection. At 20 Hz, 40 frames = 2 seconds

Events Emitted

Event IDConstantWhen Emitted
1event_types::GESTURE_DETECTEDA gesture template matched. Value = gesture ID (1=wave, 2=push, 3=pull, 4=swipe).

Example Usage

rust
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
}

Tutorial: Adding a Custom Gesture Template

  1. 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.

  2. 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.

  3. Edit the template array: In gesture.rs, increase NUM_TEMPLATES by 1 and add a new entry in the templates array inside GestureDetector::new():

    rust
    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
    },
    
  4. Tune the threshold: Run test data through dtw_distance() directly to see the distance between your template and real observations. Adjust DTW_THRESHOLD if your gesture is consistently matched at a distance higher than 2.5.

  5. Test: Add a unit test that feeds the template values as phase inputs and verifies that process_frame returns your new gesture ID.


Coherence Monitor (coherence.rs)

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).

API

ItemTypeDescription
CoherenceMonitorstructTracks phasor sums, EMA score, and gate state.
CoherenceMonitor::new()const fnCreates a monitor with initial coherence of 1.0 (Accept).
process_frame(&mut self, phases: &[f32]) -> f32methodFeed one frame of phase data. Returns EMA-smoothed coherence [0, 1].
gate_state(&self) -> GateStatemethodCurrent gate classification (Accept, Warn, Reject).
mean_phasor_angle(&self) -> f32methodDominant phase drift direction in radians.
coherence_score(&self) -> f32methodCurrent EMA-smoothed coherence score.
GateStateenumAccept, Warn, Reject -- signal quality classification.

Configuration

ParameterDefaultRangeDescription
ALPHA0.10.01 -- 0.5EMA smoothing factor. Lower = slower response, more stable. Higher = faster response, more noisy
HIGH_THRESHOLD0.70.5 -- 0.95Coherence above this = Accept
LOW_THRESHOLD0.40.1 -- 0.6Coherence below this = Reject
MAX_SC321 -- 64Maximum subcarriers tracked (compile-time)

Events Emitted

Event IDConstantWhen Emitted
2event_types::COHERENCE_SCOREEmitted every 20 frames with the current coherence score (from the combined pipeline in lib.rs).

Example Usage

rust
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 */ }
}

Anomaly Detector (adversarial.rs)

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:

  1. Phase jump: If more than 50% of subcarriers show a phase discontinuity greater than 2.5 radians, something non-physical happened.
  2. Amplitude flatline: If amplitude variance across subcarriers is near zero (below 0.001) while the mean is nonzero, the sensor may be stuck.
  3. Energy spike: If total signal energy exceeds 50x the baseline, an external source may be injecting power.

A 20-frame cooldown prevents event flooding.

API

ItemTypeDescription
AnomalyDetectorstructTracks baseline, previous phases, cooldown, and anomaly count.
AnomalyDetector::new()const fnCreates an uncalibrated detector.
process_frame(&mut self, phases: &[f32], amplitudes: &[f32]) -> boolmethodReturns true if an anomaly is detected on this frame.
total_anomalies(&self) -> u32methodLifetime count of detected anomalies.

Configuration

ParameterDefaultRangeDescription
PHASE_JUMP_THRESHOLD2.5 rad1.0 -- piPhase jump to flag per subcarrier
MIN_AMPLITUDE_VARIANCE0.0010.0001 -- 0.1Below this = flatline
MAX_ENERGY_RATIO50.05.0 -- 500.0Energy spike threshold vs baseline
BASELINE_FRAMES10050 -- 500Frames to calibrate baseline
ANOMALY_COOLDOWN205 -- 100Frames between anomaly reports

Events Emitted

Event IDConstantWhen Emitted
3event_types::ANOMALY_DETECTEDWhen any anomaly check fires (after cooldown).

Example Usage

rust
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, &amplitudes);
}

// Now anomalies are reported.
if detector.process_frame(&phases, &amplitudes) {
    log!("Signal anomaly detected! Total: {}", detector.total_anomalies());
}

Intrusion Detector (intrusion.rs)

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:

  1. Calibrating (200 frames): Learns baseline amplitude mean and variance per subcarrier.
  2. Monitoring: Waits for the environment to be quiet (low disturbance for 100 consecutive frames) before arming.
  3. 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.
  4. Alert: Intrusion detected. Returns to Armed once disturbance drops below 0.3 for 50 frames.

API

ItemTypeDescription
IntrusionDetectorstructState machine with baseline, debounce, and cooldown.
IntrusionDetector::new()const fnCreates a detector in Calibrating state.
process_frame(&mut self, phases: &[f32], amplitudes: &[f32]) -> &[(i32, f32)]methodReturns a slice of events (up to 4 per frame).
state(&self) -> DetectorStatemethodCurrent state machine state.
total_alerts(&self) -> u32methodLifetime alert count.
DetectorStateenumCalibrating, Monitoring, Armed, Alert.

Configuration

ParameterDefaultRangeDescription
INTRUSION_VELOCITY_THRESH1.5 rad/frame0.5 -- 3.0Phase velocity that counts as fast movement
AMPLITUDE_CHANGE_THRESH3.0 sigma1.0 -- 10.0Amplitude deviation in standard deviations
ARM_FRAMES10020 -- 500Quiet frames needed to arm (at 20 Hz: 5 sec)
DETECT_DEBOUNCE31 -- 10Consecutive detection frames before alert
ALERT_COOLDOWN10020 -- 500Frames between alerts
BASELINE_FRAMES200100 -- 1000Calibration window

Events Emitted

Event IDConstantWhen Emitted
200EVENT_INTRUSION_ALERTIntrusion detected. Value = disturbance score.
201EVENT_INTRUSION_ZONEIdentifies which subcarrier zone has the most disturbance.
202EVENT_INTRUSION_ARMEDDetector has armed after a quiet period.
203EVENT_INTRUSION_DISARMEDDetector disarmed (not currently emitted).

Example Usage

rust
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);
    }
}

Occupancy Detector (occupancy.rs)

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).

API

ItemTypeDescription
OccupancyDetectorstructPer-zone state, calibration accumulators, frame counter.
OccupancyDetector::new()const fnCreates uncalibrated detector.
process_frame(&mut self, phases: &[f32], amplitudes: &[f32]) -> &[(i32, f32)]methodReturns events (up to 12 per frame).
occupied_count(&self) -> u8methodNumber of currently occupied zones.
is_zone_occupied(&self, zone_id: usize) -> boolmethodCheck a specific zone.

Configuration

ParameterDefaultRangeDescription
MAX_ZONES81 -- 16Maximum number of spatial zones
ZONE_THRESHOLD0.020.005 -- 0.5Score above this = occupied. Hysteresis exit at 0.5x
ALPHA0.150.05 -- 0.5EMA smoothing factor for zone scores
BASELINE_FRAMES200100 -- 1000Calibration window length

Events Emitted

Event IDConstantWhen Emitted
300EVENT_ZONE_OCCUPIEDEvery 10 frames for each occupied zone. Value = zone_id + confidence.
301EVENT_ZONE_COUNTEvery 10 frames. Value = total occupied zone count.
302EVENT_ZONE_TRANSITIONImmediately on zone state change. Value = zone_id + 0.5 (entered) or zone_id + 0.0 (vacated).

Example Usage

rust
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));

Vital Trend Analyzer (vital_trend.rs)

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.

API

ItemTypeDescription
VitalTrendAnalyzerstructTwo ring buffers (breathing, heartrate), debounce counters, apnea counter.
VitalTrendAnalyzer::new()const fnCreates analyzer with empty history.
on_timer(&mut self, breathing_bpm: f32, heartrate_bpm: f32) -> &[(i32, f32)]methodCalled at 1 Hz. Returns clinical alerts (up to 8).
breathing_avg_1m(&self) -> f32method1-minute breathing rate average.
breathing_trend_5m(&self) -> f32method5-minute breathing trend (positive = increasing).

Configuration

ParameterDefaultRangeDescription
BRADYPNEA_THRESH12.0 BPM8 -- 15Below this = dangerously slow breathing
TACHYPNEA_THRESH25.0 BPM20 -- 35Above this = dangerously fast breathing
BRADYCARDIA_THRESH50.0 BPM40 -- 60Below this = dangerously slow heart rate
TACHYCARDIA_THRESH120.0 BPM100 -- 150Above this = dangerously fast heart rate
APNEA_SECONDS2010 -- 60Seconds of near-zero breathing before alert
ALERT_DEBOUNCE52 -- 15Consecutive abnormal samples before alert

Events Emitted

Event IDConstantWhen Emitted
100EVENT_VITAL_TRENDReserved for generic trend events.
101EVENT_BRADYPNEASustained slow breathing. Value = current BPM.
102EVENT_TACHYPNEASustained fast breathing. Value = current BPM.
103EVENT_BRADYCARDIASustained slow heart rate. Value = current BPM.
104EVENT_TACHYCARDIASustained fast heart rate. Value = current BPM.
105EVENT_APNEABreathing stopped. Value = seconds of apnea.
110EVENT_BREATHING_AVG1-minute breathing average. Emitted every 60 seconds.
111EVENT_HEARTRATE_AVG1-minute heart rate average. Emitted every 60 seconds.

Example Usage

rust
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();

RVF Container (rvf.rs)

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.

API

ItemTypeDescription
RvfHeader#[repr(C, packed)] struct32-byte header with magic, version, section sizes.
RvfManifest#[repr(C, packed)] struct96-byte manifest with module metadata.
RvfConfigstruct (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_MAGICconst (0x0146_5652)Magic bytes: RVF\x01 as little-endian u32.
RVF_FORMAT_VERSIONconst (1)Current format version.
RVF_HEADER_SIZEconst (32)Header size in bytes.
RVF_MANIFEST_SIZEconst (96)Manifest size in bytes.
RVF_SIGNATURE_LENconst (64)Ed25519 signature length.
RVF_HOST_API_V1const (1)Host API version this crate supports.

Capability Flags

FlagValueDescription
CAP_READ_PHASE1 << 0Module reads phase data
CAP_READ_AMPLITUDE1 << 1Module reads amplitude data
CAP_READ_VARIANCE1 << 2Module reads variance data
CAP_READ_VITALS1 << 3Module reads vital sign data
CAP_READ_HISTORY1 << 4Module reads phase history
CAP_EMIT_EVENTS1 << 5Module emits events
CAP_LOG1 << 6Module uses logging
CAP_ALL0x7FAll capabilities

Example Usage

rust
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)?;

Testing

Running Core Module Tests

From the crate directory:

bash
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge
cargo test --features std -- gesture coherence adversarial intrusion occupancy vital_trend rvf

This 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.

Expected Output

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

Test Coverage Notes

ModuleTestsCoverage
gesture.rs8Init, empty input, first frame, constant input, DTW identical/different/empty, ring buffer wrap, cooldown
coherence.rs7Init, empty input, first frame, constant phases, incoherent phases, gate hysteresis, phasor angle
adversarial.rs7Init, calibration, normal signal, phase jump, flatline, energy spike, cooldown
intrusion.rs4Init, calibration, arming, intrusion detection
occupancy.rs3Init, calibration, zone detection
vital_trend.rs5Init, normal vitals, apnea, tachycardia, breathing average
rvf.rs2Build roundtrip, hash integrity

Common Patterns

All seven core modules share these design patterns:

1. Const-constructible state

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.

rust
static mut STATE: MyModule = MyModule::new();

2. Calibration-then-detect lifecycle

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.

3. Ring buffer for history

Both gesture (phase deltas) and vital_trend (BPM readings) use fixed-size ring buffers with modular index arithmetic. The pattern is:

rust
self.values[self.idx] = new_value;
self.idx = (self.idx + 1) % MAX_SIZE;
if self.len < MAX_SIZE { self.len += 1; }

4. Static event buffers

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:

rust
static mut EVENTS: [(i32, f32); N] = [(0, 0.0); N];
let mut n_events = 0;
// ... populate EVENTS[n_events] ...
unsafe { &EVENTS[..n_events] }

5. Cooldown/debounce

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.

6. EMA smoothing

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.

7. Hysteresis thresholds

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.