evidence

what is proven

Where WiFi sensing fits beside other sensors, what each detector does, how three implementations are held to one spec, and which phases are finished. Every figure on this page was measured in simulation. There is no hardware yet, so nothing here comes from a real room.

how it compares

No sensor is best at everything. Here is where WiFi sensing fits, including where it does not.

saans · wificamerammWave radarPIRwearable
Captures no images or audiostrong fitnostrong fitstrong fitstrong fit
Notices a person sitting stillstrong fitbreathing bandstrong fitstrong fitnoneeds movementwith caveatsif worn
Works in the darkstrong fitwith caveatswith IRstrong fitstrong fitstrong fit
Breathing rate without contactstrong fitopt-in per spacewith caveatsresearchstrong fitnonocontact sensor
Nothing to wear, carry or chargestrong fitstrong fitstrong fitstrong fitno
Cannot tell who someone isstrong fitby designnostrong fitstrong fitno
Position within the roomnonot yetstrong fitstrong fitnowith caveatswith UWB anchors
Hardware for one roomtwo ~$6–10 boardscamera + storageone radar moduleone sensorone per person
Measured in real roomsnonot yet · phase S5strong fitstrong fitstrong fitstrong fit
strong fitwith caveatsno

what saans is not for

Saying no is part of the design.

  • Identifying anyone, or taking attendanceDevice-free sensing cannot tell people apart. Saans reports spaces, never individuals.
  • DiagnosisNot a medical device. Alerts are advisory and never replace existing procedures.
  • Fall detectionAmplitude-only WiFi cannot see height, so a fall and a sit-down look the same.
  • Tracking people around a roomPosition at this node count lost to a trivial baseline in simulation.
  • Pose, skeletons, multi-person trackingNone of this exists here, and none of it is claimed.
  • Counting crowds across campusExisting access-point analytics count devices per building. Saans works one room at a time.

detectors

Each detector writes an event that records what it saw and how confident it was. Health detectors are advisory and run only in spaces that have opted in.

simulated · measured built off
OccupancyStarted / Endedsimulated

Occupancy

A space becomes occupied or empty. The exit event carries the dwell time. 98.2–99.5% accurate, depending on the number of links.

RestStarted / Endedbuilt

Rest period

A long still period with steady breathing, reported with start, end, restlessness and pauses. It is not sleep staging.

BreathingPausesimulated

Breathing pause

Fires after confident breathing when the band falls quiet for at least 10 s. 98.7% precision. Reported about 9–10 s late because of filter lag.

RateHigh / RateLowsimulated

Fast or slow breathing

Rate outside 8–24 BPM at rest for at least 2 min. Raised in 31 of 36 fast-breathing runs, and falsely in 2 of 36 reader runs.

TrendDeviationbuilt

Trend deviation

Resting breathing rate compared with that space's own baseline: median ± 3 MAD over at least three days.

Inactivitysimulated

Lone-worker inactivity

Present, with no gross motion for N minutes. Escalates if breathing is not detected. It also fires for a 40-minute reader, which is correct by definition and why it is advisory.

PossibleCollapseoff · not usable

Possible collapse

Caught 100% of simulated falls, and also 100% of sit-downs. It ships off and should stay off.

SpaceBlind / Visiblebuilt

Blind space

No usable link means the space reports “I can't see”, never “empty”.

LinkDown / Up / Degradedbuilt

Link health

Silence, falling rate, packet loss and interference, raised for each link so a wiring fault is never mistaken for an empty room.

CalibrationStalebuilt

Stale calibration

The age of each link's empty-room calibration, which presence depends on.

RawModeActivebuilt

Raw mode on

A node streaming raw CSI for debugging sets a flag in every frame, so the debug mode is always visible.

one spec, three implementations

Python is the reference. Rust and C++ must reproduce it on golden vectors. When they disagree, the implementation that disagrees gets fixed. Tolerances are never loosened quietly.

referencePython

Standard library only. Every stage is plain float arithmetic.

numbers
f64
role
generates the golden vectors
gatewayRust

A dependency-free port of the whole reference.

tolerance
1e-9 relative
frames
672 / 672 byte-identical
sensorC++17

Header-only, no heap after init, single-precision FPU.

tolerance
1e-4 relative
checks
845 checks pass, g++ and clang++

Pre-hardware: the golden vectors behind these counts, and the frames behind the bars below, come from the simulator rather than from a radio.

Frames per second, one core

32 subcarriers, 20 Hz, 3,267 frames, same input
Python · feature extractor175
Python · pipeline191
Rust · same algorithmestimate every frame2,851
Rust · feature extractorestimate only when a frame is due25,283
C++ · edge DSP, float32host build, separate harness≈26k

The language is worth about 15×. Computing spectral estimates only when a frame is due (twice a second rather than twenty times) is worth another ~9×, with identical output.

Every number is measured in simulation, including the bad ones.

Unit tests passed. The evaluation on scripted timelines still found three bugs. Each fix went into the Python reference first, the golden vectors were regenerated, and those vectors caught the same bug in both the Rust and the C++ ports before either was edited by hand.

Presence threshold too low

fixed

Thirty seconds of an 8 s sliding RMS holds only a few independent windows, so the noise estimate came out far too small. The fix was a floor derived from the filter bandwidth.

empty room read occupied 36% → 0.6–1.2%

The motion band leaked breathing

fixed

One biquad's skirt passed about 19% of a 0.25 Hz breathing component into the 1–4 Hz motion band. The fix was two cascaded sections.

still breather vs empty 1.33 / 0.88 → 0.79 / 0.75

One noisy sample reset every clock

fixed

The heartbeat sits in the motion band, so a single sample could restart the stillness timer. The fix was a higher threshold plus three confirming samples.

false stillness resets 60/h → 0.15/h

where it stands

Every phase has an exit gate that is measured, not declared. Simulation is done. Real rooms need hardware, and that comes next.

  1. S0Spec and golden vectorsWire format, decision records, 23 vector sets from the reference.done
  2. S1Rust parityAll vectors at 1e-9. 672/672 frames byte-identical. Benchmark reported.done
  3. S2C++ parity and firmware845 float32 checks, two compilers. Firmware builds for esp32s3.done
  4. S3Health detectorsEvent-level precision and recall on held-out scripted timelines, including false alarms per hour.done · simulated
  5. S4GatewayReplays a recording and a simulated 8-hour night end to end, with API, WebSocket and /metrics.done
  6. S5Hardware bring-upPresence F1, still person read as empty, breathing against a chest belt, in real rooms.needs hardware
  7. S6Multi-link spacesBlind-spot rate with 1, 2 and 3 links, first in simulation and then in real rooms.in progress
  8. S7PilotTwo weeks, ≥ 99% uptime, alarm rates per space per day, with ethics approval first.later

Every gate marked done was passed in simulation. No gate on this list has been passed on hardware.

keep reading