ESP32 CSI human detection: capture, robust features, session calibration and false-alarm evaluation
By Wu Jun · Updated 2026-10-29 · 6 min read
ESP32 CSI can be a cheap motion sensor, but "there is data on the serial port" is only the first step. A usable experiment needs a deterministic transmitter, validated CSI fields, an empty-room baseline, session-level calibration and reported false alarms.
This page deliberately defines human detection as a motion proxy. It can notice activity; it cannot prove that a still person is present, identify a person or count occupants. The CSI survey by Ma et al. provides background for the method class [1].
0. Bottom line
| Question | Correct answer |
|---|---|
| Can one ESP32 transmit and receive its own sensing signal? | No; a router, second board or another transmitter must send packets |
| Does 100 packets/s guarantee a 100 Hz CSI time series? | No; calculate matching packets, timestamps and losses |
| Does ESP32 always produce 30–64 subcarriers? | No; LTF mode, 20/40 MHz, STBC and firmware fields change the layout |
| Can a motion score prove the room is empty? | No; still people, occlusion and multipath can all be missed |
| Does a 30-second demo prove accuracy? | No; collect separate sessions and report false alarms/hour and latency |
1. Choose a capture topology
| Route | Hardware | Advantage | Main risk |
|---|---|---|---|
| One ESP32 + router | one board + AP | cheapest | packet rate depends on AP, power save and channel |
| Two ESP32 boards | sender + receiver | more control over packet timing | matching targets, firmware and time axes |
| ESPectre / ESPHome | supported ESP32 | produces HA entities directly | one area, still needs site calibration |
Espressif's esp-csi project includes csi_recv_router, csi_recv and csi_send examples
[2]. ESPectre implements device-side motion detection and entities [3].
1.1 Validate the record before any model
lenmatches the CSI buffer format for this firmware;- the
dataelement count agrees withlen; first_word_invalidis handled; on some ESP32-S3 frames the first four bytes are invalid [4];rssi,noise_floorandlocal_timestampkeep updating;- stopping the traffic source sharply reduces CSI frames, proving capture is packet-triggered.
An ESP32 CSI complex sample is usually int8 imaginary + int8 real. Do not reuse an int16 router-CSI parser.
2. Data quality before algorithms
Capture ten seconds and compute:
frames_total
valid_csi_frames
duration = last_ts - first_ts
effective_rate = (valid_csi_frames - 1) / duration
gap_p50 / gap_p95 / gap_max
len histogram
RSSI min / mean / max
first_word_invalid count
If the effective rate is too low, gaps are unexplained or record lengths keep changing, stop before threshold tuning. A plausible-looking plot on a wrong timeline makes every later result invalid.
3. Normalize per subcarrier first
Assume H is an aligned frames × subcarriers amplitude matrix. A single threshold on raw amplitude is not
robust: every subcarrier has a different baseline, noise and attenuation.
The following code fits the baseline only on a training/empty-room session, then applies it to a separate validation session:
import numpy as np
def baseline_fit(H_quiet):
baseline = np.median(H_quiet, axis=0)
mad = np.median(np.abs(H_quiet - baseline), axis=0)
scale = np.maximum(1.4826 * mad, 1e-6)
return baseline, scale
def motion_series(H, baseline, scale, fs):
# Larger z means the subcarrier is farther from its empty-room baseline.
z = np.abs((H - baseline) / scale)
frame_score = np.median(z, axis=1) # median rejects a few bad subcarriers
width = max(1, int(round(fs))) # about one second
kernel = np.ones(width) / width
return np.convolve(frame_score, kernel, mode="same")
This is a transparent baseline method, not a claim of optimal accuracy. Any deep-learning or ESPectre model must also be validated on separate sessions, not the empty-room data used to tune it.
4. Collection protocol
| Class | Duration per session | Suggested sessions | Record |
|---|---|---|---|
| Empty room | 10 min | ≥ 3 at different times | HVAC, curtains, pets and neighbour interference |
| Normal walking | 10 min | ≥ 3 | different speeds, paths and distances |
| Sit/stand | 5 min | ≥ 5 | 30 seconds before and after each event |
| Still sitting | 10 min | ≥ 3 | measure misses for a stationary person |
| Empty/occupied alternation | 10 transitions | ≥ 2 days | use enter/leave times as truth |
Log:
- ESP32 model, target, ESP-IDF version and firmware commit;
- sender/receiver position, height, orientation and distance;
- channel, bandwidth, RSSI and noise floor;
- effective rate, maximum gap and serial drops;
- subject, room, furniture state and label source.
Cover at least two positions and two subjects, or the threshold will overfit one person and one room. Split train/validation by session or date, never randomly across overlapping windows.
5. Threshold, hysteresis and hold time
Compute:
q_quiet = P99 of the training empty-room motion_series
q_active = P10 of the training active motion_series
A threshold is meaningful only when q_quiet < q_active with visible separation. If the distributions overlap,
change placement or features; do not force a threshold that merely fits one demo.
ENTER = q_quiet
EXIT = max(q_quiet * 0.8, q_active * 0.5)
MIN_ON_S = 0.5
MIN_OFF_S = 3.0
state = False
on_time = off_time = 0.0
dt = 1.0 / fs
for score in motion:
if not state:
on_time = on_time + dt if score > ENTER else 0.0
if on_time >= MIN_ON_S:
state, on_time, off_time = True, 0.0, 0.0
else:
off_time = off_time + dt if score < EXIT else 0.0
if off_time >= MIN_OFF_S:
state, on_time, off_time = False, 0.0, 0.0
These values are examples, not universal constants. Choose the hold time from the cost of a false alarm: a light may tolerate a short one; security or emergency logic must not.
6. Event-level evaluation
Frame accuracy is too optimistic when most frames are empty room. Report:
| Metric | Definition |
|---|---|
| False alarms per hour | false transitions while nobody is present, divided by hours |
| Event recall | true events detected inside a tolerance window / total events |
| Miss rate while still | stationary-person frames or sessions classified as empty |
| Detection latency | true event start to state true |
| Release latency | departure to state false |
| Unavailable time | fraction of time serial/MQTT/HA was down |
Without synchronised video or an event log, call it an uncalibrated motion proxy and do not report accuracy. Include the raw-data path, code commit, threshold, packet rate, placement and failed sessions in every report.
7. Failures in a real room
| Symptom | Likely cause | Correct fix |
|---|---|---|
| Serial logs but no CSI | Wi-Fi not associated or no traffic | verify association and sender traffic |
| Packet rate jumps | channel contention, power save, serial bandwidth | fix sender, disable power save, slow plotting |
| CSI length changes | different LTF/bandwidth/STBC frames | retain len, mode and first_word_invalid |
| Empty room always alarms | HVAC, curtains, pets or neighbours | collect a new baseline, extend window, reject bad subcarriers |
| A still person disappears | insufficient micro-motion | add an independent sensor; do not invent presence |
| Works in one room only | multipath and AP placement changed | calibrate each room separately |
8. When to switch to a router or add sensors
For several rooms, one MT6000 can share capture and networking and reduce the number of ESP32 boards, at the cost of mt76 firmware, a dedicated AP, snapshot-rate limits and router tuning. See the MT6000 reproduction guide.
For reliable security or fall alarms, one CSI sensor is not enough. Add independent sensing, human review, failure alerts and a privacy review; do not replace engineering with an accuracy claim.
9. Acceptance order
- get
CSI_DATAfrom the official example; - validate
len,datacount, timestamps and packet rate; - capture empty-room and walking sessions and plot the motion score;
- test separation on an independent session;
- add hysteresis and hold time;
- run for 24 hours and count false alarms and unavailable time;
- only then connect Home Assistant, lights or HVAC.
Related: ESP32 CSI introduction, Home Assistant integration, vs mmWave radar and Wi-Fi CSI radar.
References and data sources
Every external claim carries a source and an evidence grade (grading scheme). Grade E marks first-party, reproducible measurements from this project.
- Grade A · standard/review
WiFi Sensing with Channel State Information: A Survey
W. Ma, W. Zhou, X. Wang · ACM Computing Surveys · 2019
doi:10.1145/3310194 · citations 857 · verified: CrossRef API, 2026-10-11 · The field's most-cited CSI sensing survey (857 citations, verified via CrossRef). - Grade C · official docs
espressif/esp-csi — Applications based on Wi-Fi CSI
Espressif Systems
github.com/espressif/esp-csi · verified: GitHub API 2026-10-29 (stars 1,723; README confirms the complete ESP32 series list and three sender/receiver routes) · The official README states: "All ESP32 series support CSI, including ESP32 / ESP32-S2 / ESP32-C3 / ESP32-S3 / ESP32-C5 / ESP32-C6 / ESP32-C61." - Grade C · official docs
francescopace/espectre — Wi-Fi CSI motion sensing for ESP32 (ESPHome)
F. Pace
github.com/francescopace/espectre · verified: GitHub API and official README fetched 2026-10-29 (stars 9,700; GPLv3; ESPHome source path, Direct API port 62587 and supported chips documented) · The official project provides ESPHome, native MQTT, Matter and browser flashing paths; one ESP32 covers one area and does not identify people or count occupants. - Grade C · official docs
ESP-IDF — Wi-Fi Channel State Information (ESP32-S3)
Espressif Systems
docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-guides/wifi-driver/wifi-vendor-features.html#wi-fi-channel-state-information · verified: official docs fetched 2026-10-29 (ESP32-S3 CSI section: LTF/byte-count table and the first_word_invalid note) - Grade C · official docs
StevenMHernandez/ESP32-CSI-Tool — active/passive CSI extraction for ESP32
S. M. Hernandez
github.com/StevenMHernandez/ESP32-CSI-Tool · verified: GitHub API 2026-10-11 (stars 571, MIT, last push 2024-07-16)