esp-csi-rs
A Rust crate for collecting Channel State Information (CSI) on ESP32 series devices using
the no-std embedded framework.
Want CSI without writing code?
esp-csi-cli-rsis a CLI wrapper with pre-built binaries that exposes everything this crate does — flash it and talk to the board over serial.
The node model
A deployment is a CSI sensing network: nodes sharing a channel, in which soundings excite the channel and at least one node reports a measurement of it. Each node is described by four independent attributes, named in IEEE 802.11bf's terms wherever the standard has one.
| Attribute | Values | Question it answers |
|---|---|---|
| Mode | six, below | How does the node reach the channel? |
| Session role | IEEE 802.11bf Initiator / Responder, or none (observer) | Who originates the soundings? |
| Sensing role | IEEE 802.11bf Transmitter / Receiver / both | Who sends the sensing PPDUs, and who measures them? |
| Reporting | Report (to host or local), ThresholdBased, Decimate, None | Whether, and how often, measurements leave the node |
The run is started and stopped by the controller — the host tier, never a node and never the initiator. Not every mode admits every combination, and the ones it does not admit are not offered: where an attribute is fixed there is no setter to call.
| Constructor | Mode | Session role | Sensing role | Reporting |
|---|---|---|---|---|
CsiNode::sniffer |
Wi-Fi sniffer | none | receiver | report, threshold-based or decimate |
CsiNode::station |
Wi-Fi station | initiator or none | transmitter-receiver or receiver | any |
CsiNode::access_point |
Wi-Fi access point | initiator | receiver | any |
CsiNode::emitter |
Emitter (transmit-only sounding) | none | transmitter | none |
CsiNode::esp_now |
ESP-NOW | initiator or responder | transmitter-receiver | any |
CsiNode::esp_now_simplex_source |
ESP-NOW simplex | initiator | transmitter | none |
CsiNode::esp_now_simplex_peer |
ESP-NOW simplex | responder | receiver | report |

docs/network-model.md is the normative description, including how the
model maps onto IEEE 802.11bf.
Architecture

| Abstraction | Role |
|---|---|
Mode, CsiNode |
How the node reaches the channel; one constructor per mode |
SessionRole, SensingRole |
The node's IEEE 802.11bf roles, read back from the mode |
Reporting, ReportTo |
Whether, and how often, measurements leave the node, and where to |
CsiPacket → Envelope |
Format version, node id, session id, source kind, per-run frame counter |
CsiPacket → CsiFrame |
Normalised RxMeta, the Stimulus that excited the channel, an optional HeaderDigest, and a CsiPayload (EspRaw tagged with its LayoutId, Grouped, Variation) |
wire |
The frame format itself: no_std, radio-neutral and free of esp-* imports, so hosts decode with the same types |
MeasurementSource, emit |
Feed frames from any source through the same policy, statistics and delivery |
MeasurementSetup |
The node's measurement setup: periodicity, bandwidth, acquisition |
wire::MeasurementSetup |
The serialized IEEE 802.11bf-style setup a controller sends (periodicity, bandwidth, responders), mapped onto the mode |
RadioProfile |
Seam for custom Wi-Fi bring-up |
get_drop_breakdown, snapshot_tx_stats |
Drops per cause and per-transmitter counters (statistics feature) |
Features
- Devices: ESP32, ESP32-C3, ESP32-C5 (dual-band 2.4/5 GHz), ESP32-C6, ESP32-S3.
- PHYs: HT20 and HT40 on every chip; HE20 (802.11ax) on the ESP32-C5 and C6.
- Output: postcard/COBS frames, plain text, a compact array format, the ESP32-CSI-Tool CSV
layout, or
defmt. - On-device processing: register a
fn(&CsiPacket)to process CSI inline in the Wi-Fi callback.
Getting started
[]
= { = "0.13", = ["esp32c3", "println"] }
The crate uses Rust edition 2024 and tracks esp-hal 1.2, esp-radio 1.0 and esp-rtos 0.4.
For defmt see docs/defmt.md.
A sniffer:
use ;
let mut node = sniffer;
node.run.await;
controller is the WifiController from esp_radio::wifi::WifiController::new; NodeHardware
claims its interfaces, sniffer and ESP-NOW handle.
Reading measurements, naming the session and reporting only on change:
use CsiPacket;
use ;
use ;
set_csi_callback;
set_session;
let sniffer = default.with_threshold;
// A controller's serialized setup, applied to an ESP-NOW initiator.
let setup = new
.with_stimulus;
initiator_node.apply_measurement_setup?;
And an emitter for it to measure:
use ;
let emitter = new
.with_period;
let mut node = emitter;
Examples
| Example | What it does |
|---|---|
sniffer |
Locks a channel and measures every frame overheard |
emitter |
Unassociated sounding at HT20, HT40 or HE20; pair with sniffer |
station |
Associates to an ESP softAP or a commercial router |
access_point |
Self-contained softAP with DHCP; associated stations generate the uplink |
esp_now |
The symmetric connectionless pair |
esp_now_simplex |
The asymmetric pair — the highest CSI rate of any pairing |
csi_callback |
Inline callback vs. queued delivery |
runtime_config |
Reconfiguring one node between runs |
synthetic_source |
A non-radio MeasurementSource feeding 802.11bf-shaped reports through emit (synthetic feature) |
Replace esp32c3 with esp32, esp32c5, esp32c6 or esp32s3. Measurement harnesses are in
experiments/.
Further reading
| Document | What is in it |
|---|---|
docs/network-model.md |
The node model and its relation to IEEE 802.11bf |
docs/bandwidth.md |
HT20, HT40 and HE20, and filtering legacy/ACK CSI |
docs/emitter-support.md |
Which transport each chip's emitter uses |
docs/defmt.md |
Logging backends and using defmt |
wire on docs.rs |
The frame format, subcarrier layouts and measurement setup |
| docs.rs | Full API documentation |
License
Copyright 2026 The csi-rs Team. Licensed under either of
- the Apache License, Version 2.0 (
LICENSE), or - the MIT license (
LICENSE-MIT),
at your option.