If you have read our complete guide to Wi-Fi CSI, you know what Channel State Information is and why it is so much richer than signal strength. This article is the practical follow-up: how to actually pull CSI out of an ESP32 and see it change in your own room, on your own network.
01What you are actually capturing
Wi-Fi splits a channel into dozens of narrow subcarriers using OFDM. Every packet carries a known training field, and the receiver works out how the channel bent that field on each subcarrier. The result, per packet, is a list of complex numbers: one amplitude and phase value for each subcarrier. That list is the CSI.
On an ESP32 the driver hands you this list as a block of signed 8-bit bytes. The bytes are interleaved: for each subcarrier you get an imaginary value and a real value, one after the other. Turn each pair into a magnitude and you have the shape of the channel at that instant. Watch that shape over time and you are watching the room.
02What you need
- An ESP32, ESP32-S3 or ESP32-C board. Everything here runs on a single board.
- Your own Wi-Fi access point, and its password. Only connect to a network you own.
- Arduino IDE with the Arduino-ESP32 core 3.x installed (tested on 3.0).
- A USB-C cable and the serial monitor set to 115200 baud.
That is the whole bill of materials. No extra antenna, no second radio, no cloud. The board connects to your access point as a normal station, and we ask the Wi-Fi driver to report CSI for the packets it receives.
03Enabling CSI: three calls
Espressif exposes CSI through three functions in esp_wifi.h. You call them once, after Wi-Fi has started and connected:
- Describe what you want. Fill a
wifi_csi_config_tand pass it toesp_wifi_set_csi_config(). This chooses which training fields contribute to the CSI and whether the driver filters or rescales the values. - Register a callback. Call
esp_wifi_set_csi_rx_cb()with your function. The driver will call it from Wi-Fi context for every packet that carries CSI, so keep that function short. - Turn it on. Call
esp_wifi_set_csi(true). From this point your callback starts receiving records.
04The CSI callback
Your callback receives a pointer to a wifi_csi_info_t. The two fields that matter for a first project are buf, the array of signed bytes, and len, how many bytes are in it. The structure also carries the sender's MAC address and an rx_ctrl block with RSSI, channel and rate, which are useful later for filtering.
Because the bytes are interleaved imaginary-then-real pairs, the number of subcarriers is len / 2. The exact count depends on the channel bandwidth and which training fields you enabled, so read it from len rather than hard-coding it.
The rx_ctrl block is worth knowing about even if you ignore it at first. It carries the RSSI of the packet, the Wi-Fi channel, the data rate, and whether the signal was 20 MHz or 40 MHz wide. Those fields let you reject records that do not belong to your link, or weight your motion signal by how strong the packet was. For a first capture you can leave them alone, but they are the difference between a toy and a stable sensor once you start filtering. We lean on exactly this metadata in the alarm build.
05Amplitude, and a single energy number
For each subcarrier, the amplitude is the magnitude of its complex value: the square root of the real part squared plus the imaginary part squared. For a motion signal you rarely need every subcarrier individually at first. A good starting point is one scalar per packet: the total energy, the sum of amplitude squared across all subcarriers.
Energy alone already reacts clearly to a person walking through the link. Later you can keep the full per-subcarrier vector and compute variance, which is what our CSI motion alarm uses to decide when something moved.
06The complete sketch
Put your own network name and password in the two constants, flash the board, and open the serial monitor. It connects, enables CSI, and then prints one line per captured packet.
// Minimal Wi-Fi CSI capture for ESP32 / ESP32-S3 / ESP32-C. // Connects to YOUR OWN access point and prints CSI energy per packet. #include <WiFi.h> #include "esp_wifi.h" const char* SSID = "YOUR_WIFI"; // your own network const char* PASS = "YOUR_PASSWORD"; // Called by the Wi-Fi driver for every packet that carries CSI. // Keep it short: it runs inside the Wi-Fi task. static void onCsi(void* ctx, wifi_csi_info_t* info) { if (!info || !info->buf) return; int8_t* d = info->buf; // interleaved int8: [imag, real] per subcarrier int len = info->len; // number of bytes in the buffer int n = len / 2; // subcarrier count double energy = 0; for (int i = 0; i < n; i++) { int imag = d[i * 2]; // imaginary part int real = d[i * 2 + 1]; // real part energy += (double)(real * real + imag * imag); // amplitude^2 } Serial.printf("n=%d energy=%.0f rssi=%d\n", n, energy, info->rx_ctrl.rssi); } void setup() { Serial.begin(115200); delay(300); WiFi.mode(WIFI_STA); WiFi.begin(SSID, PASS); // connect to YOUR OWN AP Serial.print("Connecting"); while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); } Serial.printf("\nConnected, IP %s\n", WiFi.localIP().toString().c_str()); // 1) Describe which parts of the packet we read CSI from. wifi_csi_config_t csi_config = { .lltf_en = true, .htltf_en = true, .stbc_htltf2_en = true, .ltf_merge_en = true, .channel_filter_en = false, .manu_scale = false, .shift = 0, }; esp_wifi_set_csi_config(&csi_config); // 2) Register the callback that receives each CSI record. esp_wifi_set_csi_rx_cb(&onCsi, NULL); // 3) Turn CSI capture on. esp_wifi_set_csi(true); Serial.println("CSI on. Move near the antenna and watch 'energy' change."); } void loop() { delay(100); // CSI arrives in the callback; nothing to do here }
07Watch it move
With the serial monitor open you will see a stream of lines like n=64 energy=41280 rssi=-47. Stand still and the energy settles into a noisy band. Now walk across the space between the board and your router, or wave a hand close to the antenna, and the number jumps. That jump is the whole idea: a person changes the paths the signal takes, and CSI records the change.
A few practical notes. The subcarrier count n you see depends on bandwidth and the fields you enabled, so do not be surprised if it is not a round number. RSSI is printed too, so you can confirm the link is healthy. And if the values look wild, that is expected before any filtering, which is exactly what the next projects address.
It helps to log the stream somewhere you can plot it. Paste a run of lines into a spreadsheet, or pipe the serial output to a file, and draw the energy over time. You will see a flat, fuzzy band while the room is empty, then clear humps whenever you move. That picture makes the whole idea click, and it is the quickest way to pick a sensible threshold before you write any detection logic. Keep the board in one spot while you capture, because moving the antenna itself changes every path at once and swamps the gentler changes you are trying to see.
Once you trust the raw stream, the natural next steps are comparing CSI against plain signal strength in CSI vs RSSI, and turning the energy into a real trigger in our motion alarm build. Both build directly on this sketch.
08FAQ
How many CSI packets per second will I get?
It depends entirely on how much Wi-Fi traffic crosses the link, because CSI is produced for each received packet. An idle link may give only a few records per second, while a steady ping or a video stream can push it to tens or hundreds per second. For motion sensing, aim for a stable rate of at least about 20 to 50 packets per second.
Do I need two ESP32 boards to capture CSI?
No. A single board connected to your own access point is enough, because that one board both receives the packets and reports their CSI. A second transmitter only helps when you want a controlled, steady stream of packets or a fixed geometry between the two radios.
Which ESP32 variants support CSI?
CSI is exposed across the ESP32 family, including the original ESP32, the ESP32-S3, and the ESP32-C3 and C6. Any of them can enable CSI through the same callback, so you can pick the variant that fits the rest of your project.
Why is my CSI so noisy?
Some noise is normal, because CSI reacts to every small change in the radio channel. The usual fixes are to keep the board and antenna still, average several subcarriers or several packets together, discard obvious outliers, and let the system learn the empty room before you trust any reading.