Security & Safety Modules -- WiFi-DensePose Edge Intelligence

April 26, 2026 ยท View on GitHub

Perimeter monitoring and threat detection using WiFi Channel State Information (CSI). Works through walls, in complete darkness, without visible cameras. Each module runs on an $8 ESP32-S3 chip at 20 Hz frame rate. All modules are no_std-compatible and compile to WASM for hot-loading via ADR-040 Tier 3.

Overview

ModuleFileWhat It DoesEvent IDsBudget
Intrusion Detectionintrusion.rsPhase/amplitude anomaly intrusion alarm with arm/disarm200-203S (<5 ms)
Perimeter Breachsec_perimeter_breach.rsMulti-zone perimeter crossing with approach/departure210-213S (<5 ms)
Weapon Detectionsec_weapon_detect.rsConcealed metallic object detection via RF reflectivity ratio220-222S (<5 ms)
Tailgating Detectionsec_tailgating.rsDouble-peak motion envelope for unauthorized following230-232L (<2 ms)
Loitering Detectionsec_loitering.rsProlonged stationary presence with 4-state machine240-242L (<2 ms)
Panic Motionsec_panic_motion.rsErratic motion, struggle, and fleeing patterns250-252S (<5 ms)

Budget key: S = Standard (<5 ms per frame), L = Light (<2 ms per frame).

Shared Design Patterns

All security modules follow these conventions:

  • const fn new(): Zero-allocation constructor, no heap, suitable for static mut on ESP32.
  • process_frame(...) -> &[(i32, f32)]: Returns event tuples (event_id, value) via a static buffer (safe in single-threaded WASM).
  • Calibration phase: First N frames (typically 100-200 at 20 Hz = 5-10 seconds) learn ambient baseline. No events during calibration.
  • Debounce: Consecutive-frame counters prevent single-frame noise from triggering alerts.
  • Cooldown: After emitting an event, a cooldown window suppresses duplicate emissions (40-100 frames = 2-5 seconds).
  • Hysteresis: Debounce counters use saturating_sub(1) for gradual decay rather than hard reset, reducing flap on borderline signals.

Modules

Intrusion Detection (intrusion.rs)

What it does: Monitors a previously-empty space and triggers an alarm when someone enters. Works like a traditional motion alarm -- the environment must settle before the system arms itself.

How it works: During calibration (200 frames), the detector learns per-subcarrier amplitude mean and variance. After calibration, it waits for the environment to be quiet (100 consecutive frames with low disturbance) before arming. Once armed, it computes a composite disturbance score from phase velocity (sudden phase jumps between frames) and amplitude deviation (amplitude departing from baseline by more than 3 sigma). If the disturbance exceeds 0.8 for 3+ consecutive frames, an alert fires.

State Machine

Calibrating --> Monitoring --> Armed --> Alert
                   ^                      |
                   |        (quiet for     |
                   |         50 frames)    |
                   +---- Armed <----------+
  • Calibrating: Accumulates baseline amplitude statistics for 200 frames.
  • Monitoring: Waits for 100 consecutive quiet frames before arming.
  • Armed: Active detection. Triggers alert on 3+ consecutive high-disturbance frames.
  • Alert: Active alert. Returns to Armed after 50 consecutive quiet frames. 100-frame cooldown prevents re-triggering.

API

ItemTypeDescription
IntrusionDetector::new()const fnCreate detector in Calibrating state
process_frame(phases, amplitudes)fnProcess one CSI frame, returns events
state()fn -> DetectorStateCurrent state (Calibrating/Monitoring/Armed/Alert)
total_alerts()fn -> u32Cumulative alert count

Events Emitted

Event IDConstantWhen Emitted
200EVENT_INTRUSION_ALERTIntrusion detected (disturbance score as value)
201EVENT_INTRUSION_ZONEZone index of highest disturbance
202EVENT_INTRUSION_ARMEDSystem transitioned to Armed state
203EVENT_INTRUSION_DISARMEDSystem disarmed (currently unused -- reserved)

Configuration

ParameterDefaultRangeDescription
INTRUSION_VELOCITY_THRESH1.50.5-3.0Phase velocity threshold (rad/frame)
AMPLITUDE_CHANGE_THRESH3.02.0-5.0Sigma multiplier for amplitude deviation
ARM_FRAMES10040-200Quiet frames required before arming (5s at 20 Hz)
DETECT_DEBOUNCE32-10Consecutive disturbed frames before alert
ALERT_COOLDOWN10020-200Frames between re-alerts (5s at 20 Hz)
BASELINE_FRAMES200100-500Calibration frames (10s at 20 Hz)

Perimeter Breach Detection (sec_perimeter_breach.rs)

What it does: Divides the monitored area into 4 zones (mapped to subcarrier groups) and detects movement crossing zone boundaries. Classifies motion direction as approaching or departing using energy gradient trends.

How it works: Subcarriers are split into 4 equal groups, each representing a spatial zone. Per-zone metrics are computed every frame:

  1. Phase gradient: Mean absolute phase difference between current and previous frame within the zone's subcarrier range.
  2. Variance ratio: Current zone variance divided by calibrated baseline variance.

A breach is flagged when phase gradient exceeds 0.6 rad/subcarrier AND variance ratio exceeds 2.5x baseline. Direction is determined by linear regression slope over an 8-frame energy history buffer -- positive slope = approaching, negative = departing.

State Machine

There is no explicit state machine enum. Instead, per-zone counters track:

  • disturb_run: Consecutive breach frames (resets to 0 when zone is quiet).
  • approach_run / departure_run: Consecutive frames with positive/negative energy trend (debounced to 3 frames).
  • Four independent cooldown timers for breach, approach, departure, and transition events.

No stuck states possible: all counters either reset on quiet input or are bounded by saturating_add.

API

ItemTypeDescription
PerimeterBreachDetector::new()const fnCreate uncalibrated detector
process_frame(phases, amplitudes, variance, motion_energy)fnProcess one frame, returns up to 4 events
is_calibrated()fn -> boolWhether baseline calibration is complete
frame_count()fn -> u32Total frames processed

Events Emitted

Event IDConstantWhen Emitted
210EVENT_PERIMETER_BREACHSignificant disturbance in any zone (value = energy score)
211EVENT_APPROACH_DETECTEDEnergy trend rising in a breached zone (value = zone index)
212EVENT_DEPARTURE_DETECTEDEnergy trend falling in a zone (value = zone index)
213EVENT_ZONE_TRANSITIONMovement shifted from one zone to another (value = from*10 + to)

Configuration

ParameterDefaultRangeDescription
BASELINE_FRAMES10060-200Calibration frames (5s at 20 Hz)
BREACH_GRADIENT_THRESH0.60.3-1.5Phase gradient for breach (rad/subcarrier)
VARIANCE_RATIO_THRESH2.51.5-5.0Variance ratio above baseline for disturbance
DIRECTION_DEBOUNCE32-8Consecutive trend frames for direction confirmation
COOLDOWN4020-100Frames between events of same type (2s at 20 Hz)
HISTORY_LEN84-16Energy history buffer for trend estimation
MAX_ZONES42-4Number of perimeter zones

Example Usage

use wifi_densepose_wasm_edge::sec_perimeter_breach::*;

let mut detector = PerimeterBreachDetector::new();

// Feed CSI frames (phases, amplitudes, variance arrays, motion energy scalar)
let events = detector.process_frame(&phases, &amplitudes, &variance, motion_energy);

for &(event_id, value) in events {
    match event_id {
        EVENT_PERIMETER_BREACH => {
            // value = energy score (higher = more severe)
            log!("Breach detected, energy={:.2}", value);
        }
        EVENT_APPROACH_DETECTED => {
            // value = zone index (0-3)
            log!("Approach in zone {}", value as u32);
        }
        EVENT_ZONE_TRANSITION => {
            // value encodes from*10 + to
            let from = (value as u32) / 10;
            let to = (value as u32) % 10;
            log!("Movement from zone {} to zone {}", from, to);
        }
        _ => {}
    }
}

Tutorial: Setting Up a 4-Zone Perimeter System

  1. Sensor placement: Mount the ESP32-S3 at the center of the monitored boundary (e.g., warehouse entrance, property line). The WiFi AP should be on the opposite side so the sensing link crosses all 4 zones.

  2. Zone mapping: Subcarriers are divided equally among 4 zones. With 32 subcarriers:

    • Zone 0: subcarriers 0-7 (nearest to the ESP32)
    • Zone 1: subcarriers 8-15
    • Zone 2: subcarriers 16-23
    • Zone 3: subcarriers 24-31 (nearest to the AP)
  3. Calibration: Power on the system with no one in the monitored area. Wait 5 seconds (100 frames) for calibration to complete. is_calibrated() returns true.

  4. Alert integration: Forward events to your security system:

    • EVENT_PERIMETER_BREACH (210) -> Trigger alarm siren / camera recording
    • EVENT_APPROACH_DETECTED (211) -> Pre-alert: someone approaching
    • EVENT_ZONE_TRANSITION (213) -> Track movement direction through zones
  5. Tuning: If false alarms occur in windy or high-traffic environments, increase BREACH_GRADIENT_THRESH and VARIANCE_RATIO_THRESH. If detections are missed, decrease them.


Concealed Metallic Object Detection (sec_weapon_detect.rs)

What it does: Detects concealed metallic objects (knives, firearms, tools) carried by a person walking through the sensing area. Metal has significantly higher RF reflectivity than human tissue, producing a characteristic amplitude-variance-to-phase-variance ratio.

How it works: During calibration (100 frames in an empty room), the detector computes baseline amplitude and phase variance per subcarrier using online variance accumulation. After calibration, running Welford statistics track amplitude and phase variance in real-time. The ratio of running amplitude variance to running phase variance is computed across all subcarriers. Metal produces a high ratio (amplitude swings wildly from specular reflection while phase varies less than diffuse tissue).

Two thresholds are applied:

  • Metal anomaly (ratio > 4.0, debounce 4 frames): General metallic object detection.
  • Weapon alert (ratio > 8.0, debounce 6 frames): High-reflectivity alert for larger metal masses.

Detection requires presence >= 1 and motion_energy >= 0.5 to avoid false positives on environmental noise.

Important: This module is research-grade and experimental. It requires per-environment calibration and should not be used as a sole security measure.

API

ItemTypeDescription
WeaponDetector::new()const fnCreate uncalibrated detector
process_frame(phases, amplitudes, variance, motion_energy, presence)fnProcess one frame, returns up to 3 events
is_calibrated()fn -> boolWhether baseline calibration is complete
frame_count()fn -> u32Total frames processed

Events Emitted

Event IDConstantWhen Emitted
220EVENT_METAL_ANOMALYMetallic object signature detected (value = amp/phase ratio)
221EVENT_WEAPON_ALERTHigh-reflectivity metal signature (value = amp/phase ratio)
222EVENT_CALIBRATION_NEEDEDBaseline drift exceeds threshold (value = max drift ratio)

Configuration

ParameterDefaultRangeDescription
BASELINE_FRAMES10060-200Calibration frames (empty room, 5s at 20 Hz)
METAL_RATIO_THRESH4.02.0-8.0Amp/phase variance ratio for metal detection
WEAPON_RATIO_THRESH8.05.0-15.0Ratio for weapon-grade alert
MIN_MOTION_ENERGY0.50.2-2.0Minimum motion to consider detection valid
METAL_DEBOUNCE42-10Consecutive frames for metal anomaly
WEAPON_DEBOUNCE63-12Consecutive frames for weapon alert
COOLDOWN6020-120Frames between events (3s at 20 Hz)
RECALIB_DRIFT_THRESH3.02.0-5.0Drift ratio triggering recalibration alert

Example Usage

use wifi_densepose_wasm_edge::sec_weapon_detect::*;

let mut detector = WeaponDetector::new();

// Calibrate in empty room (100 frames)
for _ in 0..100 {
    detector.process_frame(&phases, &amplitudes, &variance, 0.0, 0);
}
assert!(detector.is_calibrated());

// Normal operation: person walks through
let events = detector.process_frame(&phases, &amplitudes, &variance, motion_energy, presence);

for &(event_id, value) in events {
    match event_id {
        EVENT_METAL_ANOMALY => {
            log!("Metal detected, ratio={:.1}", value);
        }
        EVENT_WEAPON_ALERT => {
            log!("WEAPON ALERT, ratio={:.1}", value);
            // Trigger security response
        }
        EVENT_CALIBRATION_NEEDED => {
            log!("Environment changed, recalibration recommended");
        }
        _ => {}
    }
}

Tailgating Detection (sec_tailgating.rs)

What it does: Detects tailgating at doorways -- two or more people passing through in rapid succession. A single authorized passage produces one smooth energy peak; a tailgater following closely produces a second peak within a configurable window (default 3 seconds).

How it works: The detector uses temporal clustering of motion energy peaks through a 3-state machine:

  1. Idle: Waiting for motion energy to exceed the adaptive threshold.
  2. InPeak: Tracking an active peak. Records peak maximum energy and duration. Peak ends when energy drops below 30% of peak maximum. Noise spikes (peaks shorter than 3 frames) are discarded.
  3. Watching: Peak ended, monitoring for another peak within the tailgate window (60 frames = 3s). If another peak arrives, it transitions back to InPeak. When the window expires, it evaluates: 1 peak = single passage, 2+ peaks = tailgating.

The threshold adapts to ambient noise via exponential moving average of variance.

State Machine

Idle ----[energy > threshold]----> InPeak
                                      |
                          [energy < 30% of peak max]
                                      |
             [peak too short]         v
Idle <------------------------- InPeak end
                                      |
                          [peak valid (>= 3 frames)]
                                      v
                                  Watching
                                   /    \
              [new peak starts]   /      \  [window expires]
                                 v        v
                              InPeak    Evaluate
                                        /     \
                               [1 peak]        [2+ peaks]
                                  |                |
                          SINGLE_PASSAGE    TAILGATE_DETECTED
                                  |           + MULTI_PASSAGE
                                  v                v
                                Idle             Idle

API

ItemTypeDescription
TailgateDetector::new()const fnCreate detector
process_frame(motion_energy, presence, n_persons, variance)fnProcess one frame, returns up to 3 events
frame_count()fn -> u32Total frames processed
tailgate_count()fn -> u32Total tailgating events detected
single_passages()fn -> u32Total single passages recorded

Events Emitted

Event IDConstantWhen Emitted
230EVENT_TAILGATE_DETECTEDTwo or more peaks within window (value = peak count)
231EVENT_SINGLE_PASSAGESingle peak followed by quiet window (value = peak energy)
232EVENT_MULTI_PASSAGEThree or more peaks within window (value = peak count)

Configuration

ParameterDefaultRangeDescription
ENERGY_PEAK_THRESH2.01.0-5.0Motion energy threshold for peak start
ENERGY_VALLEY_FRAC0.30.1-0.5Fraction of peak max to end peak
TAILGATE_WINDOW6020-120Max inter-peak gap for tailgating (3s at 20 Hz)
MIN_PEAK_ENERGY1.50.5-3.0Minimum peak energy for valid passage
COOLDOWN10040-200Frames between events (5s at 20 Hz)
MIN_PEAK_FRAMES32-10Minimum peak duration to filter noise spikes
MAX_PEAKS84-16Maximum peaks tracked in one window

Example Usage

use wifi_densepose_wasm_edge::sec_tailgating::*;

let mut detector = TailgateDetector::new();

// Process frames from host
let events = detector.process_frame(motion_energy, presence, n_persons, variance_mean);

for &(event_id, value) in events {
    match event_id {
        EVENT_TAILGATE_DETECTED => {
            log!("TAILGATE: {} people in rapid succession", value as u32);
            // Lock door / alert security
        }
        EVENT_SINGLE_PASSAGE => {
            log!("Normal passage, energy={:.2}", value);
        }
        EVENT_MULTI_PASSAGE => {
            log!("Multi-passage: {} people", value as u32);
        }
        _ => {}
    }
}

Loitering Detection (sec_loitering.rs)

What it does: Detects prolonged stationary presence in a monitored area. Distinguishes between a person passing through (normal) and someone standing still for an extended time (loitering). Default dwell threshold is 5 minutes.

How it works: Uses a 4-state machine that tracks presence duration and motion level. Only stationary frames (motion energy below 0.5) count toward the dwell threshold -- a person actively walking through does not accumulate loitering time. The exit cooldown (30 seconds) prevents false "loitering ended" events from brief signal dropouts or occlusions.

State Machine

Absent --[presence + no post_end cooldown]--> Entering
                                                  |
                                   [60 frames with presence]
                                                  |
            [absence before 60]                   v
Absent <------------------------------ Entering confirmed
                                                  |
                                                  v
                                              Present
                                             /       \
                          [6000 stationary   /         \ [absent > 300
                            frames]         /           \  frames]
                                           v             v
                                      Loitering       Absent
                                       /     \
                    [presence continues]       [absent >= 600 frames]
                              |                        |
                     LOITERING_ONGOING          LOITERING_END
                     (every 600 frames)                |
                              |                        v
                              v                     Absent
                          Loitering              (post_end_cd = 200)

API

ItemTypeDescription
LoiteringDetector::new()const fnCreate detector in Absent state
process_frame(presence, motion_energy)fnProcess one frame, returns up to 2 events
state()fn -> LoiterStateCurrent state (Absent/Entering/Present/Loitering)
frame_count()fn -> u32Total frames processed
loiter_count()fn -> u32Total loitering events
dwell_frames()fn -> u32Current accumulated stationary dwell frames

Events Emitted

Event IDConstantWhen Emitted
240EVENT_LOITERING_STARTDwell threshold exceeded (value = dwell time in seconds)
241EVENT_LOITERING_ONGOINGPeriodic report while loitering (value = total dwell seconds)
242EVENT_LOITERING_ENDLoiterer departed after exit cooldown (value = total dwell seconds)

Configuration

ParameterDefaultRangeDescription
ENTER_CONFIRM_FRAMES6020-120Presence confirmation (3s at 20 Hz)
DWELL_THRESHOLD60001200-12000Stationary frames for loitering (5 min at 20 Hz)
EXIT_COOLDOWN600200-1200Absent frames before ending loitering (30s at 20 Hz)
STATIONARY_MOTION_THRESH0.50.2-1.5Motion energy below which person is stationary
ONGOING_REPORT_INTERVAL600200-1200Frames between ongoing reports (30s at 20 Hz)
POST_END_COOLDOWN200100-600Cooldown after end before re-detection (10s at 20 Hz)

Example Usage

use wifi_densepose_wasm_edge::sec_loitering::*;

let mut detector = LoiteringDetector::new();

let events = detector.process_frame(presence, motion_energy);

for &(event_id, value) in events {
    match event_id {
        EVENT_LOITERING_START => {
            log!("Loitering started after {:.0}s", value);
            // Alert security
        }
        EVENT_LOITERING_ONGOING => {
            log!("Still loitering, total {:.0}s", value);
        }
        EVENT_LOITERING_END => {
            log!("Loiterer departed after {:.0}s total", value);
        }
        _ => {}
    }
}

// Check state programmatically
if detector.state() == LoiterState::Loitering {
    // Continuous monitoring actions
}

Panic/Erratic Motion Detection (sec_panic_motion.rs)

What it does: Detects three categories of distress-related motion:

  1. Panic: Erratic, high-jerk motion with rapid random direction changes (e.g., someone flailing, being attacked).
  2. Struggle: Elevated jerk with moderate energy and some direction changes (e.g., physical altercation, trying to break free).
  3. Fleeing: Sustained high energy with low entropy -- running in one direction.

How it works: Maintains a 100-frame (5-second) circular buffer of motion energy and variance values. Computes window-level statistics each frame:

  • Mean jerk: Average absolute rate-of-change of motion energy across the window. High jerk = erratic, unpredictable motion.
  • Entropy proxy: Fraction of frames with direction reversals (energy transitions from increasing to decreasing or vice versa). High entropy = chaotic motion.
  • High jerk fraction: Fraction of individual frame-to-frame jerks exceeding JERK_THRESH. Ensures the high mean is not from a single spike.

Detection logic:

  • Panic = mean_jerk > 2.0 AND entropy > 0.35 AND high_jerk_frac > 0.3
  • Struggle = mean_jerk > 1.5 AND energy in [1.0, 5.0) AND entropy > 0.175 AND not panic
  • Fleeing = mean_energy > 5.0 AND mean_jerk > 0.05 AND entropy < 0.25 AND not panic

API

ItemTypeDescription
PanicMotionDetector::new()const fnCreate detector
process_frame(motion_energy, variance_mean, phase_mean, presence)fnProcess one frame, returns up to 3 events
frame_count()fn -> u32Total frames processed
panic_count()fn -> u32Total panic events detected

Events Emitted

Event IDConstantWhen Emitted
250EVENT_PANIC_DETECTEDErratic high-jerk + high-entropy motion (value = severity 0-10)
251EVENT_STRUGGLE_PATTERNElevated jerk at moderate energy (value = mean jerk)
252EVENT_FLEEING_DETECTEDSustained high-energy directional motion (value = mean energy)

Configuration

ParameterDefaultRangeDescription
WINDOW10040-200Analysis window size (5s at 20 Hz)
JERK_THRESH2.01.0-4.0Per-frame jerk threshold for panic
ENTROPY_THRESH0.350.2-0.6Direction reversal rate threshold
MIN_MOTION1.00.3-2.0Minimum motion energy (ignore idle)
TRIGGER_FRAC0.30.2-0.5Fraction of window frames exceeding thresholds
COOLDOWN10040-200Frames between events (5s at 20 Hz)
FLEE_ENERGY_THRESH5.03.0-10.0Minimum energy for fleeing detection
FLEE_JERK_THRESH0.050.01-0.5Minimum jerk for fleeing (above noise floor)
FLEE_MAX_ENTROPY0.250.1-0.4Maximum entropy for fleeing (directional motion)
STRUGGLE_JERK_THRESH1.50.8-3.0Minimum mean jerk for struggle pattern

Example Usage

use wifi_densepose_wasm_edge::sec_panic_motion::*;

let mut detector = PanicMotionDetector::new();

let events = detector.process_frame(motion_energy, variance_mean, phase_mean, presence);

for &(event_id, value) in events {
    match event_id {
        EVENT_PANIC_DETECTED => {
            log!("PANIC: severity={:.1}", value);
            // Immediate security dispatch
        }
        EVENT_STRUGGLE_PATTERN => {
            log!("Struggle detected, jerk={:.2}", value);
            // Investigate
        }
        EVENT_FLEEING_DETECTED => {
            log!("Person fleeing, energy={:.1}", value);
            // Track direction via perimeter module
        }
        _ => {}
    }
}

Event ID Registry (Security Range 200-299)

RangeModuleEvents
200-203intrusion.rsINTRUSION_ALERT, INTRUSION_ZONE, INTRUSION_ARMED, INTRUSION_DISARMED
210-213sec_perimeter_breach.rsPERIMETER_BREACH, APPROACH_DETECTED, DEPARTURE_DETECTED, ZONE_TRANSITION
220-222sec_weapon_detect.rsMETAL_ANOMALY, WEAPON_ALERT, CALIBRATION_NEEDED
230-232sec_tailgating.rsTAILGATE_DETECTED, SINGLE_PASSAGE, MULTI_PASSAGE
240-242sec_loitering.rsLOITERING_START, LOITERING_ONGOING, LOITERING_END
250-252sec_panic_motion.rsPANIC_DETECTED, STRUGGLE_PATTERN, FLEEING_DETECTED
253-299Reserved for future security modules

Testing

# Run all security module tests (requires std feature)
cd v2/crates/wifi-densepose-wasm-edge
cargo test --features std -- sec_ intrusion

Test Coverage Summary

ModuleTestsCoverage Notes
intrusion.rs4Init, calibration, arming, intrusion detection
sec_perimeter_breach.rs6Init, calibration, breach, zone transition, approach, quiet signal
sec_weapon_detect.rs6Init, calibration, no presence, metal anomaly, normal person, drift recalib
sec_tailgating.rs7Init, single passage, tailgate, wide spacing, noise spike, multi-passage, low energy
sec_loitering.rs7Init, entering, cancel, loitering start/ongoing/end, brief absence, moving person
sec_panic_motion.rs7Init, window fill, calm motion, panic, no presence, fleeing, struggle, low motion

Deployment Considerations

Coverage Area per Sensor

Each ESP32-S3 with a WiFi AP link covers a single sensing path. The coverage area depends on:

  • Distance: 1-10 meters between ESP32 and AP (optimal: 3-5 meters for indoor).
  • Width: First Fresnel zone width -- approximately 0.5-1.5 meters at 5 GHz.
  • Through-wall: WiFi CSI penetrates drywall and wood but attenuates through concrete/metal. Signal quality degrades beyond one wall.

Multi-Sensor Coordination

For larger areas, deploy multiple ESP32 sensors in a mesh:

  • Each sensor runs its own WASM module instance independently.
  • The aggregator server (wifi-densepose-sensing-server) collects events from all sensors.
  • Cross-sensor correlation (e.g., tracking a person across zones) is done server-side, not on-device.
  • Use EVENT_ZONE_TRANSITION (213) from perimeter breach to correlate movement across adjacent sensors.

False Alarm Reduction

  1. Calibration: Always calibrate in the intended operating conditions (time of day, HVAC state, door positions).
  2. Threshold tuning: Start with defaults, increase thresholds if false alarms occur, decrease if detections are missed.
  3. Debounce tuning: Increase debounce counters in high-noise environments (near HVAC vents, open windows).
  4. Multi-module correlation: Require 2+ modules to agree before triggering high-severity responses. For example: perimeter breach + panic motion = confirmed threat; perimeter breach alone = investigation.
  5. Time-of-day filtering: Server-side logic can suppress certain events during business hours (e.g., single passages are normal during the day).

Integration with Existing Security Systems

  • Event forwarding: Events are emitted via csi_emit_event() to the host firmware, which packs them into UDP packets sent to the aggregator.
  • REST API: The sensing server exposes events at /api/v1/sensing/events for integration with SIEM, VMS, or access control systems.
  • Webhook support: Configure the server to POST event payloads to external endpoints.
  • MQTT: For IoT integration, events can be published to MQTT topics (one per event type or per sensor).

Resource Usage on ESP32-S3

ResourceBudgetNotes
RAM~2-4 KB per moduleStatic buffers, no heap allocation
CPU<5 ms per frame (S budget)Well within 50 ms frame budget at 20 Hz
Flash~3-8 KB WASM per moduleCompiled with opt-level = "s" and LTO
Total (6 modules)~15-25 KB RAM, ~30 KB FlashFits in 925 KB firmware with headroom