SGP Official store ↗
Blog / Projects / Capture CSI on ESP32

How to capture Wi-Fi CSI on an ESP32, step by step

Channel State Information is hidden inside every Wi-Fi packet your board receives. This is the hands-on guide: three function calls to turn CSI on, one callback to read it, and a few lines of math to turn raw bytes into a number that moves when you move. Complete, compiling code included.

Published 20 Oct 2026Read 11 minLevel IntermediateTopic ESP32 CSI
Capturing Wi-Fi CSI on an ESP32 board Watch · CSI on ESP32
VIDEO — Capturing CSI on an ESP32: enable the callback, print the energy, and watch it react as a hand moves past the antenna.

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

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:

  1. Describe what you want. Fill a wifi_csi_config_t and pass it to esp_wifi_set_csi_config(). This chooses which training fields contribute to the CSI and whether the driver filters or rescales the values.
  2. 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.
  3. Turn it on. Call esp_wifi_set_csi(true). From this point your callback starts receiving records.
Keep the callback lightThe callback runs inside the Wi-Fi task. Do not print long strings, allocate memory or block inside it. Do the minimum, store a number, and let your main loop do the heavy work.

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.

raw buffer (int8, interleaved) im0 re0 im1 re1 im2 re2 … amplitude = sqrt(re² + im²) per subcarrier energy = sum of amplitude² across all subcarriers → one number per packet
FIG 1 — The driver gives you interleaved bytes. Pair them, take the magnitude per subcarrier, then sum the squares into one energy value that is easy to plot and watch.

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.

csi_capture.ino · Arduino-ESP32 3.x
// 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
}
If no lines appearCSI is produced only when packets arrive. On an idle link that can be just a handful per second. Keep a little traffic flowing: leave a continuous ping to the board running from a phone or laptop on the same network, and the record rate rises immediately.

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.

SGP Card Mini ESP32-S3 board running the CSI capture sketch
The SGP Card Mini is an ESP32-S3 board with Wi-Fi, Bluetooth LE and LoRa. The sketch above runs on it unchanged over USB-C.

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.

SGP Lab — written by the team that designs and builds SGP hardware in Europe. We test every example on our own boards before publishing.

SGP devices are sold only at our official store.

sgpstore.net ↗