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

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:

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

  1. get CSI_DATA from the official example;
  2. validate len, data count, timestamps and packet rate;
  3. capture empty-room and walking sessions and plot the motion score;
  4. test separation on an independent session;
  5. add hysteresis and hold time;
  6. run for 24 hours and count false alarms and unavailable time;
  7. 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.

  1. 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).
  2. 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."
  3. 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.
  4. 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)
  5. 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)