Articles › Microcontrollers and code
Use Wi-Fi as a motion sensor with CSI on the ESP32
Wi-Fi Channel State Information shifts when people move. Build a camera-free presence sensor with esp-radar and esp_wifi_sensing, with privacy in mind.
- CSI gives amplitude and phase per subcarrier, far richer than the single RSSI number. A person walking by makes these values wobble.
- esp-radar turns CSI into waveform_jitter (movement) and waveform_wander (presence); esp_wifi_sensing adds an ACTIVE/INACTIVE state machine with debounce.
- Train once per installation with the room empty and still. Move the board, the router or big furniture and you must train again.
- No camera does not mean no surveillance. Tell the people in the house, and never point it at someone else's space.
Wi-Fi is not only for carrying data. For every packet an ESP32 receives, the chip also knows how much the signal weakened and how far its phase shifted at each small frequency slice. That information is called Channel State Information (CSI). When someone walks through a room, their body reflects and absorbs the signal, so the CSI wobbles, and you can use that wobble as a motion sensor with no camera and no PIR.
This is an advanced lesson. It uses ESP-IDF (not Arduino) and two Espressif components, esp-radar and esp_wifi_sensing. Everything runs from 5 V USB power. If you are still choosing a chip, Espressif ranks the ESP32-C5 best in the family for CSI quality.
How CSI differs from RSSI
| RSSI | CSI | |
|---|---|---|
| What it is | one overall signal strength | amplitude and phase per subcarrier |
| Per packet (20 MHz, LLTF) | 1 number | 64 subcarriers = 128 bytes |
| Sensitivity to movement | low | high |
| What it is good for | rough near/far | movement and presence |
The ESP-IDF guide says each subcarrier is stored as two signed bytes, the imaginary part first and the real part second. On a 20 MHz non-HT channel, the LLTF part covers subcarriers 031 and -32-1, 128 bytes in total. An HT (802.11n) packet adds an HT-LTF set, for 256 bytes.
Worked example: how much CSI per second
CSI only arrives when a packet arrives, so you have to create traffic. The simplest way is to have the ESP32 ping the router quickly and read CSI from the replies. The console_test example in esp-csi pings every 10 ms by default.
- A ping every 10 ms = 1,000 ÷ 10 = 100 per second.
- A non-HT reply carries 128 bytes of CSI: 100 × 128 = 12,800 bytes per second.
- An HT reply carries 256 bytes: 100 × 256 = 25,600 bytes per second.
- Each second you have 100 × 64 = 6,400 complex values to process (LLTF only).
An ESP32 can keep up with that, but not if you do heavy work in the callback. The guide warns that the CSI callback runs in the Wi-Fi task: post the data to a queue and process it in a lower-priority task. esp-radar does this part for you.
Three ways to set it up
From the esp-csi README:
| Method | Hardware | Plus | Minus |
|---|---|---|---|
| Router CSI | 1 ESP32 + router | simplest | results depend on router position and the protocols it uses |
| ESP32 to ESP32 | several ESP32s | less dependent on router position | still depends on the router and surroundings |
| Dedicated transmitter | broadcast sender + several ESP32 receivers | most accurate and reliable of the three | needs extra hardware |
For a first build, start with router CSI. What it senses is the space between the board and the router, so put the walkway you care about in between the two. The README also notes that an external IPEX antenna performs better than a directional PCB antenna.
How esp-radar and esp_wifi_sensing fit together
- esp-radar takes CSI and computes two features:
waveform_jitterfor movement andwaveform_wanderfor presence. - esp_wifi_sensing sits on top. It takes those features and runs a state machine with three states, INACTIVE, DEBOUNCE and ACTIVE, with debounce, an adaptive baseline, callbacks on state changes, and a built-in router ping.
Per-channel settings: sensitivity in (0, 1], larger is more sensitive (changing it relearns the baseline); presence_sensitivity in [0, 1]; and active_filter_ms, the minimum time to stay ACTIVE, which keeps an LED from flickering.
Try Espressif’s example
You need ESP-IDF 5.4 or later. Run this on a computer with ESP-IDF installed:
idf.py create-project-from-example "espressif/esp-radar=*:console_test"
cd console_test
idf.py set-target esp32c5
idf.py flash monitor
Swap esp32c5 for your board’s chip. This example already enables CSI in its sdkconfig.defaults. In your own project, turn it on in idf.py menuconfig under Components config > Wi-Fi > Wi-Fi CSI (Channel State Information).
The wifi_sensing_demo example in esp-csi goes further: it joins Wi-Fi, starts the state machine, turns an LED fully on when the state is ACTIVE and fades it out when it returns to INACTIVE. A Web Serial page lets you watch graphs and change settings from a Chromium-based browser.
Train it on an empty room
The order from the esp_wifi_sensing docs:
- Create the FSM and add a channel (for example the router’s).
- Get everyone out, stop any oscillating fan, then call
esp_wifi_sensing_fsm_train_start(). - Let it collect background samples, then call
esp_wifi_sensing_fsm_train_stop(). You get wander and jitter thresholds between 0 and 1; smaller means a stiller room. - Start detection with the start control.
Train with the room truly empty and still. If someone is sitting there while it trains, it learns that person as “empty room”.
Privacy
CSI takes no pictures, but it still knows when someone is there and when they move. esp-csi even mentions sensing small actions such as breathing. Treat it the way you would treat a camera:
- Tell everyone in the house the sensor exists and which room it covers.
- Do not place it to cover a neighbour’s space or someone else’s rented room.
- Keep results on the device or the home network. If anything goes to the cloud, send the state, not raw CSI.
- Make it easy to switch off, with a switch or a visible status light.
Common mistakes
- No traffic. The board is on Wi-Fi but no CSI arrives because no packets arrive. Ping the router or add a transmitter.
- Heavy work in the CSI callback. It stalls Wi-Fi. Queue the data and process it elsewhere.
- Forgetting to enable Wi-Fi CSI in menuconfig in your own project (Espressif’s examples already do it).
- Training with people in the room or a fan swinging. The thresholds come out wrong.
- Moving the board or router without retraining. The radio picture changes and results drift.
- Testing while others walk around. The esp-csi README recommends testing where nobody else is moving.
A CSI sensor is only as good as its training. Whenever the room changes, retrain before you suspect the code.
Sources
FAQ
Which ESP32 chips work?
esp-radar and esp_wifi_sensing support the ESP32, S2, S3, C3, C5, C6 and C61, on ESP-IDF 5.4 or later. Espressif ranks CSI quality as C5 best, then C6, then C3 and S3, with the original ESP32 last.
Can it replace a PIR sensor?
They work differently. A PIR sees moving heat inside its field of view. CSI sees changes in the radio channel between transmitter and receiver, so it does not need to face anyone, but it needs training and reacts more to the environment. Start with a PIR if you want something simple and steady.
Can it tell who walked past?
The Espressif libraries in this lesson only report movement and presence. They do not identify people.
Circuits and code were tested on sample boards. Boards differ in pin layout, so check your board's pinout before you power anything.