Contactless health monitoring using WiFi signals. No wearables, no cameras -- just an ESP32 sensor reading WiFi reflections off a person's body to detect breathing problems, heart rhythm issues, walking difficulties, and seizures.
These modules are research tools, not FDA-approved medical devices. They should supplement -- not replace -- professional medical monitoring. WiFi CSI-derived vital signs are inherently noisier than clinical instruments (ECG, pulse oximetry, respiratory belts). False positives and false negatives will occur. Always validate findings against clinical-grade equipment before acting on alerts.
| Module | File | What It Does | Event IDs | Budget |
|---|---|---|---|---|
| Sleep Apnea Detection | med_sleep_apnea.rs |
Detects apnea episodes when breathing ceases for >10s; tracks AHI score | 100-102 | L (< 2 ms) |
| Cardiac Arrhythmia | med_cardiac_arrhythmia.rs |
Detects tachycardia, bradycardia, missed beats, HRV anomalies | 110-113 | S (< 5 ms) |
| Respiratory Distress | med_respiratory_distress.rs |
Detects tachypnea, labored breathing, Cheyne-Stokes, composite distress score | 120-123 | H (< 10 ms) |
| Gait Analysis | med_gait_analysis.rs |
Extracts step cadence, asymmetry, shuffling, festination, fall-risk score | 130-134 | H (< 10 ms) |
| Seizure Detection | med_seizure_detect.rs |
Detects tonic-clonic seizures with phase discrimination (fall vs tremor) | 140-143 | S (< 5 ms) |
All modules:
- Compile to
no_stdfor WASM (ESP32 WASM3 runtime) - Use
const fn new()for zero-cost initialization - Return events via
&[(i32, f32)]slices (no heap allocation) - Include NaN and division-by-zero protections
- Implement cooldown timers to prevent event flooding
What it does: Monitors breathing rate from the host CSI pipeline and detects when breathing drops below 4 BPM for more than 10 consecutive seconds, indicating an apnea episode. It tracks all episodes and computes the Apnea-Hypopnea Index (AHI) -- the number of apnea events per hour of monitored sleep time. AHI is the standard clinical metric for sleep apnea severity.
Clinical basis: Obstructive and central sleep apnea are defined by cessation of airflow for 10 seconds or more. The module uses a breathing rate threshold of 4 BPM (essentially near-zero breathing) with a 10-second onset delay to confirm cessation is sustained. AHI severity classification: < 5 normal, 5-15 mild, 15-30 moderate, > 30 severe.
How it works:
- Each second, checks if breathing BPM is below 4.0
- Increments a consecutive-low-breath counter
- After 10 consecutive seconds, declares apnea onset (backdated to when breathing first dropped)
- When breathing resumes above 4 BPM, records the episode with its duration
- Every 5 minutes, computes AHI = (total episodes) / (monitoring hours)
- Only monitors when presence is detected; if subject leaves during apnea, the episode is ended
| Item | Type | Description |
|---|---|---|
SleepApneaDetector |
struct | Main detector state |
SleepApneaDetector::new() |
const fn |
Create detector with zeroed state |
process_frame(breathing_bpm, presence, variance) |
method | Process one frame at ~1 Hz; returns event slice |
ahi() |
method | Current AHI value |
episode_count() |
method | Total recorded apnea episodes |
monitoring_seconds() |
method | Total seconds with presence active |
in_apnea() |
method | Whether currently in an apnea episode |
APNEA_BPM_THRESH |
const | 4.0 BPM -- below this counts as apnea |
APNEA_ONSET_SECS |
const | 10 seconds -- minimum duration to declare apnea |
AHI_REPORT_INTERVAL |
const | 300 seconds (5 min) -- how often AHI is recalculated |
MAX_EPISODES |
const | 256 -- maximum episodes stored per session |
| Event ID | Constant | Value | Clinical Meaning |
|---|---|---|---|
| 100 | EVENT_APNEA_START |
Current breathing BPM | Breathing has ceased or dropped below 4 BPM for >10 seconds |
| 101 | EVENT_APNEA_END |
Duration in seconds | Breathing has resumed after an apnea episode |
| 102 | EVENT_AHI_UPDATE |
AHI score (events/hour) | Periodic severity metric; >5 = mild, >15 = moderate, >30 = severe |
presence lost
[Monitoring] -----> [Not Monitoring] (no events, counter paused)
| |
| bpm < 4.0 | presence regained
v v
[Low Breath Counter] [Monitoring]
|
| count >= 10s
v
[In Apnea] ---------> [Episode End] (bpm >= 4.0 or presence lost)
| |
| v
| [Record Episode, emit APNEA_END]
|
+-- emit APNEA_START (once)
| Parameter | Default | Clinical Range | Description |
|---|---|---|---|
APNEA_BPM_THRESH |
4.0 | 0-6 BPM | Breathing rate below which apnea is suspected |
APNEA_ONSET_SECS |
10 | 10-20 s | Seconds of low breathing before apnea is declared |
AHI_REPORT_INTERVAL |
300 | 60-3600 s | How often AHI is recalculated and emitted |
MAX_EPISODES |
256 | -- | Fixed buffer size for episode history |
PRESENCE_ACTIVE |
1 | -- | Minimum presence flag value for monitoring |
use wifi_densepose_wasm_edge::med_sleep_apnea::*;
let mut detector = SleepApneaDetector::new();
// Normal breathing -- no events
let events = detector.process_frame(14.0, 1, 0.1);
assert!(events.is_empty());
// Simulate apnea: feed low BPM for 15 seconds
for _ in 0..15 {
let events = detector.process_frame(1.0, 1, 0.1);
for &(event_id, value) in events {
match event_id {
EVENT_APNEA_START => println!("Apnea detected! BPM: {}", value),
_ => {}
}
}
}
assert!(detector.in_apnea());
// Resume normal breathing
let events = detector.process_frame(14.0, 1, 0.1);
for &(event_id, value) in events {
match event_id {
EVENT_APNEA_END => println!("Apnea ended after {} seconds", value),
_ => {}
}
}
println!("Episodes: {}", detector.episode_count());
println!("AHI: {:.1}", detector.ahi());-
ESP32 placement: Mount the ESP32-S3 on the wall or ceiling 1-2 meters from the bed, at chest height. The sensor should have line-of-sight to the sleeping area. Avoid placing near metal objects or moving fans that create CSI interference.
-
WiFi router: Ensure a stable WiFi AP is within range. The ESP32 monitors the CSI (Channel State Information) of WiFi signals reflected off the person's body. The AP should be on the opposite side of the bed from the sensor for best body reflection capture.
-
Firmware configuration: Flash the ESP32 firmware with Tier 2 edge processing enabled (provides breathing BPM). The sleep apnea WASM module runs as a Tier 3 algorithm on top of the Tier 2 vitals output.
-
Threshold tuning: The default 4 BPM threshold is conservative (near-complete cessation). For a more sensitive detector, lower to 6-8 BPM, but expect more false positives from shallow breathing. The 10-second onset delay matches clinical apnea definitions.
-
Reading AHI results: AHI is emitted every 5 minutes. After a full night (7-8 hours), the final AHI value represents the overnight severity. Compare against clinical thresholds: < 5 (normal), 5-15 (mild), 15-30 (moderate), > 30 (severe).
-
Limitations: WiFi-based breathing detection works best when the subject is relatively still (sleeping). Tossing and turning may cause momentary breathing detection loss, which could either mask or falsely trigger apnea events. A single-night study should always be confirmed with clinical polysomnography.
What it does: Monitors heart rate from the host CSI pipeline and detects four types of cardiac rhythm abnormalities: tachycardia (sustained fast heart rate), bradycardia (sustained slow heart rate), missed beats (sudden HR drops), and HRV anomalies (heart rate variability outside normal bounds).
Clinical basis: Tachycardia is defined as HR > 100 BPM sustained for 10+ seconds. Bradycardia is HR < 50 BPM sustained for 10+ seconds (the 50 BPM threshold is used instead of the typical 60 BPM to account for CSI measurement noise and to avoid false positives in athletes with naturally low resting HR). Missed beats are detected as a >30% drop from the running average. HRV is assessed via RMSSD (root mean square of successive differences) with a widened normal band (10-120 ms equivalent) to account for the coarser CSI-derived HR measurement compared to ECG.
How it works:
- Maintains an exponential moving average (EMA) of heart rate with alpha=0.1
- Tracks consecutive seconds above 100 BPM (tachycardia) or below 50 BPM (bradycardia)
- After 10 consecutive seconds in an abnormal range, emits the corresponding alert
- Computes fractional drop from EMA to detect missed beats
- Maintains a 30-second ring buffer of successive HR differences for RMSSD calculation
- RMSSD is converted from BPM units to approximate ms-equivalent (scale factor ~17)
- All alerts have a 30-second cooldown to prevent event flooding
- Invalid readings (< 1 BPM or NaN) are silently ignored to prevent contamination
| Item | Type | Description |
|---|---|---|
CardiacArrhythmiaDetector |
struct | Main detector state |
CardiacArrhythmiaDetector::new() |
const fn |
Create detector with zeroed state |
process_frame(hr_bpm, phase) |
method | Process one frame at ~1 Hz; returns event slice |
hr_ema() |
method | Current EMA heart rate |
frame_count() |
method | Total frames processed |
TACHY_THRESH |
const | 100.0 BPM |
BRADY_THRESH |
const | 50.0 BPM |
SUSTAINED_SECS |
const | 10 seconds |
MISSED_BEAT_DROP |
const | 0.30 (30% drop from EMA) |
HRV_WINDOW |
const | 30 seconds |
RMSSD_LOW / RMSSD_HIGH |
const | 10.0 / 120.0 ms (widened for CSI) |
COOLDOWN_SECS |
const | 30 seconds |
| Event ID | Constant | Value | Clinical Meaning |
|---|---|---|---|
| 110 | EVENT_TACHYCARDIA |
Current HR in BPM | Heart rate sustained above 100 BPM for 10+ seconds |
| 111 | EVENT_BRADYCARDIA |
Current HR in BPM | Heart rate sustained below 50 BPM for 10+ seconds |
| 112 | EVENT_MISSED_BEAT |
Current HR in BPM | Sudden HR drop >30% from running average |
| 113 | EVENT_HRV_ANOMALY |
RMSSD value (ms) | Heart rate variability outside 10-120 ms normal range |
The cardiac module does not have a formal state machine -- it uses independent detectors with cooldown timers:
For each frame:
1. Tick cooldowns (4 independent timers)
2. Reject invalid inputs (< 1 BPM or NaN)
3. Update EMA (alpha = 0.1)
4. Update RR-diff ring buffer
5. Check tachycardia (HR > 100 for 10+ consecutive seconds)
6. Check bradycardia (HR < 50 for 10+ consecutive seconds)
7. Check missed beat (>30% drop from EMA)
8. Check HRV anomaly (RMSSD outside 10-120 ms, requires full 30s window)
9. Each check respects its own 30-second cooldown
| Parameter | Default | Clinical Range | Description |
|---|---|---|---|
TACHY_THRESH |
100.0 | 90-120 BPM | HR threshold for tachycardia |
BRADY_THRESH |
50.0 | 40-60 BPM | HR threshold for bradycardia |
SUSTAINED_SECS |
10 | 5-30 s | Consecutive seconds required for alert |
MISSED_BEAT_DROP |
0.30 | 0.20-0.40 | Fractional HR drop to flag missed beat |
RMSSD_LOW |
10.0 | 5-20 ms | Minimum normal RMSSD |
RMSSD_HIGH |
120.0 | 80-150 ms | Maximum normal RMSSD |
EMA_ALPHA |
0.1 | 0.05-0.2 | EMA smoothing coefficient |
COOLDOWN_SECS |
30 | 10-60 s | Minimum time between repeated alerts |
use wifi_densepose_wasm_edge::med_cardiac_arrhythmia::*;
let mut detector = CardiacArrhythmiaDetector::new();
// Normal heart rate -- no events
for _ in 0..60 {
let events = detector.process_frame(72.0, 0.0);
assert!(events.is_empty() || events.iter().all(|&(t, _)| t == EVENT_HRV_ANOMALY));
}
// Sustained tachycardia
for _ in 0..15 {
let events = detector.process_frame(120.0, 0.0);
for &(event_id, value) in events {
if event_id == EVENT_TACHYCARDIA {
println!("Tachycardia alert! HR: {} BPM", value);
}
}
}What it does: Detects four types of respiratory abnormalities from the host CSI pipeline: tachypnea (fast breathing), labored breathing (high amplitude variance), Cheyne-Stokes respiration (a crescendo-decrescendo breathing pattern), and a composite respiratory distress severity score from 0-100.
Clinical basis: Tachypnea is defined clinically as > 20 BPM in adults. This module uses a threshold of 25 BPM (more conservative) to reduce false positives from the inherently noisier CSI-derived breathing rate. Labored breathing is detected as a 3x increase in amplitude variance relative to a learned baseline. Cheyne-Stokes respiration is a pathological breathing pattern with 30-90 second periodicity, commonly associated with heart failure and neurological conditions. The module detects it via autocorrelation of the breathing amplitude envelope.
How it works:
- Maintains a 120-second ring buffer of breathing BPM for autocorrelation analysis
- Maintains a 60-second ring buffer of amplitude variance
- Learns a baseline variance over the first 60 seconds (Welford online mean)
- Checks for tachypnea: breathing rate > 25 BPM sustained for 8+ seconds
- Checks for labored breathing: current variance > 3x baseline variance
- Checks for Cheyne-Stokes: significant autocorrelation peak in 30-90s lag range
- Computes composite distress score (0-100) every 30 seconds based on: rate deviation from normal (16 BPM center), variance ratio, tachypnea flag, and recent Cheyne-Stokes detection
- NaN inputs are excluded from ring buffers to prevent contamination
| Item | Type | Description |
|---|---|---|
RespiratoryDistressDetector |
struct | Main detector state |
RespiratoryDistressDetector::new() |
const fn |
Create detector with zeroed state |
process_frame(breathing_bpm, phase, variance) |
method | Process one frame at ~1 Hz; returns event slice |
last_distress_score() |
method | Most recent composite score (0-100) |
frame_count() |
method | Total frames processed |
TACHYPNEA_THRESH |
const | 25.0 BPM (conservative; clinical is 20 BPM) |
SUSTAINED_SECS |
const | 8 seconds |
LABORED_VAR_RATIO |
const | 3.0x baseline |
CS_LAG_MIN / CS_LAG_MAX |
const | 30 / 90 seconds (Cheyne-Stokes period range) |
CS_PEAK_THRESH |
const | 0.35 (normalized autocorrelation) |
BASELINE_SECS |
const | 60 seconds (learning period) |
COOLDOWN_SECS |
const | 20 seconds |
| Event ID | Constant | Value | Clinical Meaning |
|---|---|---|---|
| 120 | EVENT_TACHYPNEA |
Current breathing BPM | Breathing rate sustained above 25 BPM for 8+ seconds |
| 121 | EVENT_LABORED_BREATHING |
Variance ratio | Breathing effort > 3x baseline; possible respiratory distress |
| 122 | EVENT_CHEYNE_STOKES |
Period in seconds | Crescendo-decrescendo breathing pattern; associated with heart failure |
| 123 | EVENT_RESP_DISTRESS_LEVEL |
Score 0-100 | Composite severity: 0-20 normal, 20-50 mild, 50-80 moderate, 80-100 severe |
The respiratory distress module uses independent detector tracks with cooldowns rather than a single state machine:
For each frame:
1. Tick cooldowns (3 independent timers)
2. Skip NaN inputs for ring buffer updates
3. Update breathing BPM ring buffer (120s) and variance ring buffer (60s)
4. Learn baseline variance during first 60 seconds (Welford)
5. Tachypnea check: BPM > 25 for 8+ consecutive seconds
6. Labored breathing: current variance mean > 3x baseline (after baseline period)
7. Cheyne-Stokes: autocorrelation peak > 0.35 in 30-90s lag range (needs full 120s buffer)
8. Composite distress score emitted every 30 seconds
| Parameter | Default | Clinical Range | Description |
|---|---|---|---|
TACHYPNEA_THRESH |
25.0 | 20-30 BPM | Breathing rate for tachypnea alert |
SUSTAINED_SECS |
8 | 5-15 s | Debounce period for tachypnea |
LABORED_VAR_RATIO |
3.0 | 2.0-5.0 | Variance ratio above baseline |
AC_WINDOW |
120 | 90-180 s | Autocorrelation buffer for Cheyne-Stokes |
CS_PEAK_THRESH |
0.35 | 0.25-0.50 | Autocorrelation peak threshold |
CS_LAG_MIN / CS_LAG_MAX |
30 / 90 | 20-120 s | Cheyne-Stokes period search range |
BASELINE_SECS |
60 | 30-120 s | Duration to learn baseline variance |
DISTRESS_REPORT_INTERVAL |
30 | 10-60 s | How often composite score is emitted |
COOLDOWN_SECS |
20 | 10-60 s | Minimum time between repeated alerts |
use wifi_densepose_wasm_edge::med_respiratory_distress::*;
let mut detector = RespiratoryDistressDetector::new();
// Build baseline with normal breathing (60 seconds)
for _ in 0..60 {
detector.process_frame(16.0, 0.0, 0.5);
}
// Simulate respiratory distress: high rate + high variance
for _ in 0..30 {
let events = detector.process_frame(30.0, 0.0, 3.0);
for &(event_id, value) in events {
match event_id {
EVENT_TACHYPNEA => println!("Tachypnea! Rate: {} BPM", value),
EVENT_LABORED_BREATHING => println!("Labored breathing! Variance ratio: {:.1}x", value),
EVENT_RESP_DISTRESS_LEVEL => println!("Distress score: {:.0}/100", value),
_ => {}
}
}
}-
Placement: Mount the ESP32 at the foot of the bed or on the ceiling directly above the patient. The sensor needs clear WiFi signal reflection from the patient's torso.
-
Baseline learning: The module automatically learns a 60-second baseline variance when first activated. Ensure the patient is breathing normally during this calibration period. If the patient is already in distress at module start, the baseline will be skewed and labored-breathing detection will be unreliable.
-
Cheyne-Stokes detection: Requires at least 120 seconds of data to begin autocorrelation analysis. The 30-90 second periodicity search range covers the clinically documented Cheyne-Stokes cycle range. In practice, detection typically becomes reliable after 3-4 minutes of monitoring.
-
Distress score interpretation: The composite score (0-100) combines four factors: rate deviation from normal, variance ratio, tachypnea presence, and Cheyne-Stokes detection. A score above 50 warrants clinical attention. Above 80 suggests acute distress.
What it does: Extracts gait parameters from CSI phase variance periodicity to assess mobility and fall risk. Detects step cadence, gait asymmetry (limping), stride variability, shuffling gait patterns (associated with Parkinson's disease), festination (involuntary acceleration), and computes a composite fall-risk score from 0-100.
Clinical basis: Normal walking cadence is 80-120 steps/min for healthy adults. Shuffling gait (>140 steps/min with low energy) is characteristic of Parkinson's disease and other neurological conditions. Festination (involuntary cadence acceleration) is a Parkinsonian feature. Gait asymmetry (left/right step interval ratio deviating from 1.0 by >15%) indicates limping or musculoskeletal issues. High stride variability (coefficient of variation) is a strong predictor of fall risk in elderly patients.
How it works:
- Maintains a 60-second ring buffer of phase variance and motion energy
- Detects steps as local maxima in the phase variance signal (peak-to-trough ratio > 1.5)
- Records step intervals in a 64-entry buffer
- Every 10 seconds, computes: cadence (60 / mean step interval), asymmetry (odd/even step interval ratio), variability (coefficient of variation)
- Tracks cadence history over 6 reporting periods for festination detection
- Shuffling is flagged when cadence > 140 and motion energy is low
- Festination is detected as cadence accelerating by > 1.5 steps/min/sec
- Fall-risk score (0-100) is a weighted composite of: abnormal cadence (25%), asymmetry (25%), variability (25%), low energy (15%), festination (10%)
| Item | Type | Description |
|---|---|---|
GaitAnalyzer |
struct | Main analyzer state |
GaitAnalyzer::new() |
const fn |
Create analyzer with zeroed state |
process_frame(phase, amplitude, variance, motion_energy) |
method | Process one frame at ~1 Hz; returns event slice |
last_cadence() |
method | Most recent cadence (steps/min) |
last_asymmetry() |
method | Most recent asymmetry ratio (1.0 = symmetric) |
last_fall_risk() |
method | Most recent fall-risk score (0-100) |
frame_count() |
method | Total frames processed |
NORMAL_CADENCE_LOW / HIGH |
const | 80.0 / 120.0 steps/min |
SHUFFLE_CADENCE_HIGH |
const | 140.0 steps/min |
ASYMMETRY_THRESH |
const | 0.15 (15% deviation from 1.0) |
FESTINATION_ACCEL |
const | 1.5 steps/min/sec |
REPORT_INTERVAL |
const | 10 seconds |
COOLDOWN_SECS |
const | 15 seconds |
| Event ID | Constant | Value | Clinical Meaning |
|---|---|---|---|
| 130 | EVENT_STEP_CADENCE |
Steps/min | Detected walking cadence; <80 or >120 is abnormal |
| 131 | EVENT_GAIT_ASYMMETRY |
Ratio (1.0=symmetric) | Step interval asymmetry; >1.15 or <0.85 indicates limping |
| 132 | EVENT_FALL_RISK_SCORE |
Score 0-100 | Composite: 0-25 low, 25-50 moderate, 50-75 high, 75-100 critical |
| 133 | EVENT_SHUFFLING_DETECTED |
Cadence (steps/min) | High-frequency, low-amplitude gait; Parkinson's indicator |
| 134 | EVENT_FESTINATION |
Cadence (steps/min) | Involuntary cadence acceleration; Parkinsonian feature |
The gait analyzer operates on a periodic reporting cycle:
Continuous (every frame):
- Push variance and energy into ring buffers
- Detect step peaks (local max in variance > 1.5x neighbors)
- Record step intervals
Every REPORT_INTERVAL (10s), if >= 4 steps detected:
1. Compute cadence, asymmetry, variability
2. Emit EVENT_STEP_CADENCE
3. If asymmetry > threshold: emit EVENT_GAIT_ASYMMETRY
4. If cadence > 140 and energy < 0.3: emit EVENT_SHUFFLING_DETECTED
5. If cadence accelerating > 1.5/s over 3 periods: emit EVENT_FESTINATION
6. Compute and emit EVENT_FALL_RISK_SCORE
7. Reset step buffer for next window
| Parameter | Default | Clinical Range | Description |
|---|---|---|---|
GAIT_WINDOW |
60 | 30-120 s | Ring buffer size for phase variance |
STEP_PEAK_RATIO |
1.5 | 1.2-2.0 | Min peak-to-trough ratio for step detection |
NORMAL_CADENCE_LOW |
80.0 | 70-90 steps/min | Lower bound of normal cadence |
NORMAL_CADENCE_HIGH |
120.0 | 110-130 steps/min | Upper bound of normal cadence |
SHUFFLE_CADENCE_HIGH |
140.0 | 120-160 steps/min | Cadence threshold for shuffling |
SHUFFLE_ENERGY_LOW |
0.3 | 0.1-0.5 | Energy ceiling for shuffling detection |
FESTINATION_ACCEL |
1.5 | 1.0-3.0 steps/min/s | Cadence acceleration threshold |
ASYMMETRY_THRESH |
0.15 | 0.10-0.25 | Asymmetry ratio deviation from 1.0 |
REPORT_INTERVAL |
10 | 5-30 s | Gait analysis reporting period |
MIN_MOTION_ENERGY |
0.1 | 0.05-0.3 | Minimum energy for step detection |
COOLDOWN_SECS |
15 | 10-30 s | Cooldown for shuffling/festination alerts |
use wifi_densepose_wasm_edge::med_gait_analysis::*;
let mut analyzer = GaitAnalyzer::new();
// Simulate walking with alternating high/low variance (steps)
for i in 0..30 {
let variance = if i % 2 == 0 { 5.0 } else { 0.5 };
let events = analyzer.process_frame(0.0, 1.0, variance, 1.0);
for &(event_id, value) in events {
match event_id {
EVENT_STEP_CADENCE => println!("Cadence: {:.0} steps/min", value),
EVENT_FALL_RISK_SCORE => println!("Fall risk: {:.0}/100", value),
EVENT_GAIT_ASYMMETRY => println!("Asymmetry: {:.2}", value),
_ => {}
}
}
}-
Placement: Mount the ESP32 in a hallway or corridor at waist height on the wall. The walking path should be 3-5 meters long within the sensor's field of view. Position the WiFi AP at the opposite end of the hallway for optimal body reflection.
-
Calibration: The step detector relies on periodic peaks in phase variance. The
STEP_PEAK_RATIOof 1.5 works well for most flooring surfaces. On carpet (which dampens impact signals), consider lowering to 1.2. On hard floors with shoes, 1.5-2.0 is appropriate. -
Clinical context: The fall-risk score is most useful for longitudinal monitoring. A single reading provides a snapshot, but tracking trends over days/weeks reveals progressive mobility decline. A rising fall-risk score (e.g., from 20 to 40 over a month) warrants clinical assessment even if individual readings are below the "high risk" threshold.
-
Limitations: At a 1 Hz timer rate, the module cannot detect cadences above ~60 steps/min via direct peak counting. For higher cadences, the step detection relies on the host's higher-rate CSI processing to pre-compute variance peaks. Shuffling detection at >140 steps/min requires the host to be providing step-level variance data at higher than 1 Hz.
What it does: Detects tonic-clonic (grand mal) seizures by identifying sustained high-energy rhythmic motion in the 3-8 Hz band. Discriminates seizures from falls (single impulse followed by stillness) and tremor (lower amplitude, higher regularity). Tracks seizure phases: tonic (sustained muscle rigidity), clonic (rhythmic jerking), and post-ictal (sudden cessation of movement).
Clinical basis: Tonic-clonic seizures have a characteristic progression: (1) tonic phase with sustained muscle rigidity causing high motion energy with low variance, lasting 10-20 seconds; (2) clonic phase with rhythmic jerking at 3-8 Hz, lasting 30-60 seconds; (3) post-ictal phase with sudden cessation of movement and deep unresponsiveness. Falls produce a brief (<10 frame) high-energy spike followed by stillness. Tremors have lower amplitude than seizure-grade jerking.
How it works:
- Operates at ~20 Hz frame rate (higher than other modules) for rhythm detection
- Maintains 100-frame ring buffers for motion energy and amplitude
- State machine progresses: Monitoring -> PossibleOnset -> Tonic/Clonic -> PostIctal -> Cooldown
- Onset requires 10+ consecutive frames of high motion energy (>2.0 normalized)
- Fall discrimination: if high energy lasts < 10 frames then drops, it is classified as a fall and ignored
- Tonic phase: high energy with low variance (< 0.5)
- Clonic phase: detected via autocorrelation of amplitude buffer for 2-7 frame period (3-8 Hz at 20 Hz sampling)
- Post-ictal: motion drops below 0.2 for 40+ consecutive frames
- After an episode, 200-frame cooldown prevents re-triggering
- Presence must be active; loss of presence resets the state machine
| Item | Type | Description |
|---|---|---|
SeizureDetector |
struct | Main detector state |
SeizureDetector::new() |
const fn |
Create detector with zeroed state |
process_frame(phase, amplitude, motion_energy, presence) |
method | Process at ~20 Hz; returns event slice |
phase() |
method | Current SeizurePhase enum value |
seizure_count() |
method | Total seizure episodes detected |
frame_count() |
method | Total frames processed |
SeizurePhase |
enum | Monitoring, PossibleOnset, Tonic, Clonic, PostIctal, Cooldown |
HIGH_ENERGY_THRESH |
const | 2.0 (normalized) |
TONIC_MIN_FRAMES |
const | 20 frames (1 second at 20 Hz) |
CLONIC_PERIOD_MIN / MAX |
const | 2 / 7 frames (3-8 Hz at 20 Hz) |
POST_ICTAL_MIN_FRAMES |
const | 40 frames (2 seconds at 20 Hz) |
COOLDOWN_FRAMES |
const | 200 frames (10 seconds at 20 Hz) |
| Event ID | Constant | Value | Clinical Meaning |
|---|---|---|---|
| 140 | EVENT_SEIZURE_ONSET |
Motion energy | Seizure activity detected; immediate clinical attention needed |
| 141 | EVENT_SEIZURE_TONIC |
Duration in frames | Tonic phase identified; sustained rigidity |
| 142 | EVENT_SEIZURE_CLONIC |
Period in frames | Clonic phase identified; rhythmic jerking with detected periodicity |
| 143 | EVENT_POST_ICTAL |
1.0 | Post-ictal phase; movement has ceased after seizure |
presence lost (from any active state)
+-----------------------------------------+
v |
[Monitoring] --> [PossibleOnset] --> [Tonic] --> [Clonic] --> [PostIctal] --> [Cooldown]
^ | | | | |
| | | +------> [PostIctal] -----+ |
| | | (direct if energy drops) |
| | +--------> [Clonic] |
| | (skip tonic) |
| | |
| +-- timeout (200 frames) --> [Monitoring] |
| +-- fall (<10 frames) -----> [Monitoring] |
| |
+------ cooldown expires (200 frames) ------------------------------------+
Transitions:
- Monitoring -> PossibleOnset: 10+ frames of motion energy > 2.0
- PossibleOnset -> Tonic: Low energy variance + high energy (muscle rigidity pattern)
- PossibleOnset -> Clonic: Rhythmic autocorrelation peak + amplitude above tremor floor
- PossibleOnset -> Monitoring: Energy drop within 10 frames (fall) or timeout at 200 frames
- Tonic -> Clonic: Energy variance increases and rhythm is detected
- Tonic -> PostIctal: Motion energy drops below 0.2 for 40+ frames
- Clonic -> PostIctal: Motion energy drops below 0.2 for 40+ frames
- PostIctal -> Cooldown: After 40 frames in post-ictal
- Cooldown -> Monitoring: After 200 frames (10 seconds)
| Parameter | Default | Clinical Range | Description |
|---|---|---|---|
ENERGY_WINDOW / PHASE_WINDOW |
100 | 60-200 frames | Ring buffer sizes for analysis |
HIGH_ENERGY_THRESH |
2.0 | 1.5-3.0 | Motion energy threshold for onset |
TONIC_ENERGY_THRESH |
1.5 | 1.0-2.0 | Energy threshold during tonic phase |
TONIC_VAR_CEIL |
0.5 | 0.3-1.0 | Max energy variance for tonic classification |
TONIC_MIN_FRAMES |
20 | 10-40 frames | Min frames to confirm tonic phase |
CLONIC_PERIOD_MIN / MAX |
2 / 7 | 2-10 frames | Period range for 3-8 Hz rhythm |
CLONIC_AUTOCORR_THRESH |
0.30 | 0.20-0.50 | Autocorrelation threshold for rhythm |
CLONIC_MIN_FRAMES |
30 | 20-60 frames | Min frames to confirm clonic phase |
POST_ICTAL_ENERGY_THRESH |
0.2 | 0.1-0.5 | Energy threshold for cessation |
POST_ICTAL_MIN_FRAMES |
40 | 20-80 frames | Min frames of low energy |
FALL_MAX_DURATION |
10 | 5-20 frames | Max high-energy duration classified as fall |
TREMOR_AMPLITUDE_FLOOR |
0.8 | 0.5-1.5 | Min amplitude to distinguish from tremor |
COOLDOWN_FRAMES |
200 | 100-400 frames | Cooldown after episode completes |
ONSET_MIN_FRAMES |
10 | 5-20 frames | Min high-energy frames before onset |
use wifi_densepose_wasm_edge::med_seizure_detect::*;
let mut detector = SeizureDetector::new();
// Normal motion -- no seizure
for _ in 0..200 {
let events = detector.process_frame(0.0, 0.5, 0.3, 1);
assert!(events.is_empty());
}
assert_eq!(detector.phase(), SeizurePhase::Monitoring);
// Tonic phase: sustained high energy, low variance
for _ in 0..50 {
let events = detector.process_frame(0.0, 2.0, 3.0, 1);
for &(event_id, value) in events {
match event_id {
EVENT_SEIZURE_ONSET => println!("SEIZURE ONSET! Energy: {}", value),
EVENT_SEIZURE_TONIC => println!("Tonic phase: {} frames", value),
_ => {}
}
}
}
// Post-ictal: sudden cessation
for _ in 0..100 {
let events = detector.process_frame(0.0, 0.05, 0.05, 1);
for &(event_id, _) in events {
if event_id == EVENT_POST_ICTAL {
println!("Post-ictal phase detected -- patient needs immediate assessment");
}
}
}-
Placement: Mount the ESP32 on the ceiling directly above the bed or monitoring area. Seizure detection requires the highest sensitivity to body motion, so minimize distance to the patient. Ensure no other people or moving objects are in the sensor's field of view (pets, curtains, fans).
-
Frame rate: Unlike other medical modules that operate at 1 Hz, the seizure detector expects ~20 Hz frame input for accurate rhythm detection in the 3-8 Hz band. Ensure the host firmware is configured for high-rate CSI processing when this module is loaded.
-
Sensitivity tuning: The
HIGH_ENERGY_THRESHof 2.0 andONSET_MIN_FRAMESof 10 balance sensitivity against false positives. In a quiet bedroom environment, these defaults work well. In noisier environments (shared ward, nearby equipment vibration), consider raisingHIGH_ENERGY_THRESHto 2.5-3.0. -
Fall vs seizure discrimination: The module automatically distinguishes falls (brief energy spike < 10 frames) from seizures (sustained energy). If the patient is known to be a fall risk, consider running the gait analysis module in parallel for complementary monitoring.
-
Response protocol: When
EVENT_SEIZURE_ONSETfires, immediately notify clinical staff. TheEVENT_POST_ICTALevent indicates the active seizure has ended and the patient is entering post-ictal state -- they need assessment but are no longer in the convulsive phase.
All medical modules include comprehensive unit tests covering initialization, normal operation, clinical scenario detection, edge cases, and cooldown behavior.
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge
cargo test --features std -- med_Expected output: 38 tests passed, 0 failed.
| Module | Tests | Scenarios Covered |
|---|---|---|
| Sleep Apnea | 7 | Init, normal breathing, apnea onset/end, no monitoring without presence, AHI update, multiple episodes, presence-loss during apnea |
| Cardiac Arrhythmia | 7 | Init, normal HR, tachycardia, bradycardia, missed beat, HRV anomaly (low variability), cooldown flood prevention, EMA convergence |
| Respiratory Distress | 6 | Init, normal breathing, tachypnea, labored breathing, distress score emission, Cheyne-Stokes detection, distress score range |
| Gait Analysis | 7 | Init, no events without steps, cadence extraction, fall-risk score range, asymmetry detection, shuffling detection, variability (uniform + varied) |
| Seizure Detection | 7 | Init, normal motion, fall discrimination, seizure onset with sustained energy, post-ictal detection, no detection without presence, energy variance, cooldown after episode |
| Condition | Normal Range | Module Threshold | Clinical Standard | Notes |
|---|---|---|---|---|
| Breathing rate | 12-20 BPM | -- | -- | Normal adult at rest |
| Bradypnea | < 12 BPM | Not directly detected | < 12 BPM | Gap: covered implicitly by distress score |
| Tachypnea | > 20 BPM | > 25 BPM | > 20 BPM | Conservative threshold for CSI noise tolerance |
| Apnea | 0 BPM | < 4 BPM for > 10s | Cessation > 10s | 4 BPM threshold accounts for CSI noise floor |
| Bradycardia | < 60 BPM | < 50 BPM | < 60 BPM | Lower threshold avoids false positives in athletes |
| Tachycardia | > 100 BPM | > 100 BPM | > 100 BPM | Matches clinical standard |
| Heart rate (normal) | 60-100 BPM | -- | 60-100 BPM | -- |
| AHI (mild apnea) | -- | > 5 events/hr | > 5 events/hr | Matches clinical standard |
| AHI (moderate) | -- | > 15 events/hr | > 15 events/hr | Matches clinical standard |
| AHI (severe) | -- | > 30 events/hr | > 30 events/hr | Matches clinical standard |
| RMSSD (normal HRV) | 20-80 ms | 10-120 ms | 19-75 ms | Widened band for CSI-derived HR |
| Gait cadence (normal) | 80-120 steps/min | 80-120 steps/min | 90-120 steps/min | Slightly wider range |
| Gait asymmetry | 1.0 ratio | > 0.15 deviation | > 0.10 deviation | Slightly higher threshold for CSI |
| Cheyne-Stokes period | 30-90 s | 30-90 s lag search | 30-100 s | Matches clinical range |
| Seizure clonic frequency | 3-8 Hz | 3-8 Hz (period 2-7 frames at 20 Hz) | 3-8 Hz | Matches clinical standard |
Several thresholds differ from strict clinical standards. This is intentional:
- WiFi CSI is not ECG/pulse oximetry. The signal-to-noise ratio is lower, so thresholds are widened to reduce false positives while maintaining clinical relevance.
- Conservative thresholds favor specificity over sensitivity. A missed alert is preferable to alert fatigue in a non-clinical-grade system.
- All thresholds are compile-time constants. To adjust for a specific deployment, modify the constants at the top of each module file and recompile.
-
Not a substitute for medical devices. These modules are research/assistive tools. They have not been validated through clinical trials and are not FDA/CE cleared. Never rely on them as the sole source of patient monitoring.
-
False positive rates. WiFi CSI is affected by environmental factors: moving objects (fans, pets, curtains), multipath changes (opening doors, people walking nearby), and electromagnetic interference. Expect false positive rates of 5-15% in typical home environments and 1-5% in controlled clinical settings.
-
False negative rates. The conservative thresholds mean some borderline conditions may not trigger alerts. Specifically:
- Bradypnea (12-20 BPM dropping to 12-4 BPM) is not directly flagged -- only sub-4 BPM apnea is detected
- Mild tachycardia (100-120 BPM) is detected, but the 10-second sustained requirement means brief episodes are missed
- Low-amplitude seizures without strong motor components may not exceed the energy threshold
-
Environmental factors affecting accuracy:
- Multi-person environments: All modules assume a single subject. Multiple people in the sensor's field of view will corrupt readings.
- Distance: CSI sensitivity drops with distance. Place sensor within 2 meters of the subject.
- Obstructions: Thick walls, metal furniture, and large water bodies (aquariums) between sensor and subject degrade performance.
- WiFi congestion: Heavy WiFi traffic on the same channel increases noise in CSI measurements.
-
Power and connectivity: The ESP32 must maintain continuous WiFi connectivity for CSI monitoring. Power loss or WiFi disconnection will silently stop all monitoring. Consider UPS power and redundant AP placement for critical applications.
-
Data privacy: These modules process health-related data. Ensure compliance with HIPAA, GDPR, or local health data regulations when deploying in clinical or home care settings. CSI data and emitted events should be encrypted in transit and at rest.