esp-csi-cli-rs is a command-line interface (CLI) application that runs on top of the esp-csi-rs crate. esp-csi-cli-rs provides a user friendly interface for configuring and collecting Wi-Fi Channel State Information (CSI) on ESP devices. It allows users to configure various parameters related to CSI data collection.
In order to use this crate, you would need to flash the source code for your target device. Currently supported devices include:
- ESP32
- ESP32-C3
- ESP32-C5
- ESP32-C6
- ESP32-S3
- Emitters, Wi-Fi collectors, and ESP-NOW pairs: an emitter puts known RF energy on the channel and never captures —
ht20-emitter/ht40-emitter(802.11n, all chips), pairing with a sniffer. Wi-Fi collectors capture the channel response — Sniffer, Station, softAP (wifi-ap). The connectionless ESP-NOW pairs need no association or DHCP:esp-now-central+esp-now-peripheralfor a symmetric exchange where both sides capture, andesp-now-fast-collector+esp-now-fast-sourcefor asymmetric simplex at the highest achievable CSI rate. - Traffic Generation: Generate traffic at configurable intervals.
- Fine-grained CSI Control: Enable or disable specific CSI features like LLTF, HTLTF, STBC HTLTF, and LTF Merge.
- IO Task Control: Toggle TX or RX direction tasks at the CLI.
- Runtime Delivery Switching: Flip CSI delivery between async-queued, inline callback, and off without re-flashing.
- Statistics Snapshot:
show-statsreports PPS, rates, and drops on demand. - CSI Output Gate:
set-csi-output --enabled=falsekeeps capture (and its timing) running while suppressing all decoding and logging. - Flexible Log Format: Choose between human-readable text, compact array-list, binary serialized, or ESP-CSI-Tool-compatible CSV output.
- CLI Control: Interact with the device using simple commands over a serial connection.
- Early Stop: Press
qto abort a running collection — even an indefinite one — without resetting the board. - Configuration Management: Show the current configuration or reset to defaults.
- Timed Collection: Start CSI collection for a specific duration or run indefinitely.
- Flexible Logging: Supports standard
println!or the more efficientdefmtlogging.
- Hardware: An ESP development board (ESP32, ESP32-C3, ESP32-C6, or ESP32-S3).
- Rust with ESP target support — full setup guide available here.
espflashfor flashing and monitoring — installation instructions available here.espflashalso supportsdefmtlog decoding out of the box.
Tagged releases (v*) publish per-chip .bin flash images and a
manifest.json for automated host-side flashing.
See docs/RELEASE_MANIFEST.md for the schema and CI
details.
-
Clone the repository:
git clone https://github.com/csi-rs/esp-csi-cli-rs cd esp-csi-cli-rs -
Build & Flash using the provided Cargo aliases — one command builds, flashes, and opens the monitor:
Device println(default)defmtESP32 cargo esp32cargo esp32-defmtESP32-C3 cargo esp32c3cargo esp32c3-defmtESP32-C5 cargo esp32c5cargo esp32c5-defmtESP32-C6 cargo esp32c6cargo esp32c6-defmtESP32-S3 cargo esp32s3cargo esp32s3-defmtTo build without flashing, append
-buildto any of the above (e.g.cargo esp32c6-build,cargo esp32s3-defmt-build, etc.).The
*-defmtvariants automatically: drop theprintlnfeature, enabledefmt, append-Tdefmt.xto the linker script set, and pass--log-format defmttoespflashso the monitor decodes binary log frames. No.cargo/config.tomlediting required.📝 The plain aliases default to
printlnlogging. Fordefmtbuilds use the parallel*-defmt(run + flash + monitor with frame decoding) and*-defmt-build(build only) aliases — they swap in the right features, runner, and linker script automatically. See EnablingdefmtLogging for details.Custom builds — if you need finer control over features, you can invoke
cargo builddirectly. The full set of available features is:Feature Description esp32Target: ESP32 esp32c3Target: ESP32-C3 esp32c5Target: ESP32-C5 esp32c6Target: ESP32-C6 esp32s3Target: ESP32-S3 printlnLog via println!(default)defmtLog via defmt(efficient binary logging)autoAuto-select JTAG or UART backend at runtime (default) async-printNon-blocking async logging (auto-enabled by jtag-serial)statisticsExpose runtime PPS / rate / drop counters via show-stats(default)jtag-serialForce JTAG serial backend (auto-enables async-print)uartForce UART backend (do not combine with async-print)# Example: ESP32-C6, forced JTAG backend (auto-enables async-print) cargo build --no-default-features --features "no-std,esp32c6,println,jtag-serial,statistics" \ --target riscv32imac-unknown-none-elf --release
-
Monitor (if you used a
-buildalias or a manualcargo build): Connect your ESP device over USB and run:espflash flash --monitor
For
defmtbuilds, pass the ELF file to enable log decoding:espflash flash --monitor --log-format defmt
📝
defmtbuilds require a monitoring tool capable of interpretingdefmtencoding, such asespflash. Without it you will observe garbled output. The monitor requires the original ELF to decode incoming log frames.
🛑 Flashing is only required once. After disconnecting and reconnecting the device, run
espflash monitorthen pressctrl+Rto reset.
🛑 If you encounter strange behaviour with the CLI, press
ctrl+Rto reset the device. Pressctrl+Cto terminate the session — you will need to runespflash monitoragain to reconnect.
This is a list of commands available through the CLI interface:
📝 The
set-csicommand options differ on the ESP32-C5 and ESP32-C6 (which expose per-PPDU-format acquisition flags — legacy / HT20 / HT40, plus VHT20 and forced L-LTF on C5 — instead of the classic LLTF/HTLTF flags).
-
help [command]- Description: Display the main help menu or details for a specific command.
- Example:
help set-wifi
-
set-traffic [OPTIONS]- Description: Configure traffic generation parameters.
- Options:
--frequency-hz=<NUMBER>: Specify the traffic frequency in Hertz (default: 100). Set to0to disable traffic generation.
- Examples:
set-traffic --frequency-hz=10set-traffic --frequency-hz=0
-
set-csi-output [OPTIONS]- Description: Toggle off-device delivery of captured CSI. With delivery off the radio still captures — RX path and timing unchanged — but nothing is decoded, logged, or handed to a callback. No effect on an emitter, which captures nothing.
- Options:
--enabled=<true|false>: Deliver captured CSI (default:true).
- Examples:
set-csi-output --enabled=trueset-csi-output --enabled=falseset-csi --htltf=off
-
set-csi-filter [OPTIONS]- Description: Restrict which captured frames are delivered, by source MAC and/or PHY class. A collector is promiscuous — it reports CSI for every frame its radio decodes, including your AP's beacons and ACKs and any third-party device on the channel. See What the first rows of a collection are.
- Options:
--peer-mac=<aa:bb:cc:dd:ee:ff|any>: Deliver CSI only for frames from this source.any(or an empty value) clears the filter (default:any).--min-phy=<any|ht>: Minimum PHY class.htkeeps 802.11n and better, dropping the legacy-rate management/control frames (default:any).
- Examples:
set-csi-filter --peer-mac=aa:bb:cc:dd:ee:ffset-csi-filter --min-phy=htset-csi-filter --peer-mac=any --min-phy=any
- Note: filtering on the device rather than on the host also returns console bandwidth to the
traffic you configured — a rejected frame is dropped in the Wi-Fi callback before the packet
copy and before any formatting. Rejected frames are counted as RX drops in
show-stats.
-
set-log-mode [OPTIONS]- Description: Set the CSI output logging format at runtime.
- Options:
--mode=<text|array-list|serialized|esp-csi-tool>: Output format for CSI packets (default:text).text: Verbose human-readable output with full metadata.array-list: Compact CSV-style array, one line per packet — best for host-side data processing.serialized: Binary COBS-framed postcard format — most compact, requires a compatible deserializer on the host.esp-csi-tool: Hernandez-style 26-column CSV (CSI_DATA,...lines) compatible with the ESP32-CSI-Tool collector.
- Examples:
set-log-mode --mode=textset-log-mode --mode=array-listset-log-mode --mode=esp-csi-tool
-
set-csi [OPTIONS]- Description: Configure CSI feature flags. Each flag is an
on|offtoggle, so a feature can be re-enabled after being turned off (noreset-configneeded). Accepted values:on|off,true|false,1|0,enable|disable,yes|no. - Options (ESP32, ESP32-C3, ESP32-S3):
--lltf=<on|off>: LLTF CSI (default: on).--htltf=<on|off>: HTLTF CSI (default: on).--stbc-htltf=<on|off>: STBC HTLTF CSI (default: on).--ltf-merge=<on|off>: LTF Merge CSI (default: on).
- Options (ESP32-C5, ESP32-C6):
--csi=<on|off>: Acquisition of CSI, master switch (default: on).--csi-legacy=<on|off>: L-LTF acquisition for 11g PPDUs (default: on).--csi-ht20=<on|off>: HT-LTF for HT20 PPDUs (default: on).--csi-ht40=<on|off>: HT-LTF for HT40 PPDUs (default: on).--val-scale-cfg=<0-3>: Value scale configuration (default: 2).--preset=<default>: Apply a CSI acquisition preset.--dump-ack=<on|off>: Dump 802.11 ACK frames (default: on).--csi-force-lltf=<on|off>: Force L-LTF acquisition (ESP32-C5 only).--csi-vht=<on|off>: VHT-LTF for VHT20 PPDUs (ESP32-C5 only).
- Examples:
set-csi --lltf=off --ltf-merge=offset-csi --csi-legacy=off --preset=defaultset-csi --csi-ht40=on --csi-ht20=off
- Description: Configure CSI feature flags. Each flag is an
-
set-wifi [OPTIONS]- Description: Configure WiFi and network settings. Note: SSIDs/passwords with spaces should be wrapped in single or double quotes (e.g.
--sta-ssid='My Network'or--sta-ssid="My Network"). Both quote styles are interchangeable. Underscores (_) are passed through literally. - Options:
--mode=<station|sniffer|wifi-ap|ht20-emitter|ht40-emitter|esp-now-central|esp-now-peripheral|esp-now-fast-collector|esp-now-fast-source>: Specify WiFi operation mode (default:sniffer).--sta-ssid=<SSID>: Set the SSID for Station mode.--sta-password=<PASSWORD>: Set the password for Station mode.--ap-ssid=<SSID>: Set the SSID for wifi-ap mode (default:esp-csi-ap).--ap-password=<PASSWORD>: Set the softAP password (empty = open network).--ap-dhcp=<on|off>: Enable/disable the built-in DHCP server in wifi-ap mode (default: on).--ap-leases=<1-8>: DHCP lease pool size in wifi-ap mode (default: 4). With more than one lease the AP's ICMP flood round-robins across all associated stations, so every station captures CSI;1restores the legacy single-target flood.--ap-burst=<on|off>: Synchronized burst flood in wifi-ap mode (default: off). Each flood tick sends one unicast frame back-to-back to every associated station, so all stations capture their downlink CSI within tens of microseconds of each other (time-aligned multi-receiver capture). Every station sees the fullfrequency-hz, so total offered airtime isfrequency-hz × leases— lower the rate if the channel saturates.offkeeps the round-robin flood (rate shared across stations).--set-channel=<NUMBER>: Set the WiFi channel. Use 1–14 on 2.4 GHz; on ESP32-C5 use 5 GHz channels such as 149 (default: 1 on most chips, 149 on ESP32-C5).--peer-mac=<aa:bb:cc:dd:ee:ff>: One field, two meanings by mode. Emitter modes — destination address of injected frames; empty = broadcast (default), and unicasting to a collector usually raises that collector's CSI rate. ESP-NOW modes — explicit peer address; empty keeps automatic magic-prefix pairing, and setting it switches to source-MAC filtering, which requires both nodes to be configured with the other's address (use it when more than two boards share a channel).--ht40=<above|below|none>:wifi-apmode — softAP secondary channel. ESP-NOW modes — force the per-peer TX PHY to HT40 (default:none= HT20). This does not pick emitter bandwidth; use--mode=ht40-emitterfor that.--inject-period-ms=<MS>: Emitter modes — delay between injected frames (default:20≈ 50 frames/s).--emitter-iface=<sta|ap>: Emitter modes — which interface injects (default:sta).
- Examples:
set-wifi --mode=sniffer --set-channel=6set-wifi --mode=station --sta-ssid="My Network" --sta-password="my password"set-wifi --mode=wifi-ap --set-channel=6 --ap-ssid=esp-csi-apset-wifi --mode=ht20-emitter --set-channel=6 --inject-period-ms=20set-wifi --mode=ht40-emitter --set-channel=6 --peer-mac=aa:bb:cc:dd:ee:ffset-wifi --mode=esp-now-central --set-channel=6set-wifi --mode=esp-now-fast-source --set-channel=6
- Description: Configure WiFi and network settings. Note: SSIDs/passwords with spaces should be wrapped in single or double quotes (e.g.
-
start [OPTIONS]- Description: Start the CSI collection process. Ensure the device is configured first. Press
q(orQ) on the serial console at any time to stop collection early. - Options:
--duration=<SECONDS>: Specify the duration in seconds. If omitted, collection runs indefinitely.
- Examples:
startstart --duration=120
- Description: Start the CSI collection process. Ensure the device is configured first. Press
-
show-config- Description: Display the current configuration settings for all parameters.
- Example:
show-config
-
reset-config- Description: Reset all configurations to their default values.
- Example:
reset-config
-
set-rate [OPTIONS](reporting only)- Description: Set the Wi-Fi PHY rate. Applied as the per-peer TX PHY by the
esp-now-central/esp-now-peripheralpair. Ignored elsewhere: the Wi-Fi collector modes derive their rate from the surrounding radio configuration, an emitter transmits at the rate its forced TX PHY implies, and the fast simplex pair takes its rate from its own profile. - Options:
--rate=<NAME>: One ofmcs0-lgi(default),mcs1-lgi..mcs7-lgi,mcs0-sgi,1m,2m,5m5,11m,6m,9m,12m,18m,24m,36m,48m,54m.
- Examples:
set-rate --rate=mcs0-lgiset-rate --rate=24m
- Description: Set the Wi-Fi PHY rate. Applied as the per-peer TX PHY by the
-
set-io-tasks [OPTIONS]- Description: Toggle the TX and/or RX direction tasks. Useful for asymmetric topologies — disabling RX makes the node a pure transmitter (skips the WiFi-callback CSI path); disabling TX makes it a pure receiver (no traffic generation).
- Options:
--tx=<on|off>: Enable or disable the TX task. Omit to keep the current state.--rx=<on|off>: Enable or disable the RX task. Omit to keep the current state.
- Examples:
set-io-tasks --tx=off(receive-only)set-io-tasks --tx=on --rx=on(default)
- Note: to stop CSI delivery while leaving the RX path and its timing intact, use
set-csi-output --enabled=falseinstead.
-
set-csi-delivery [OPTIONS]- Description: Switch the CSI delivery mode at runtime, and independently toggle the inline UART/JTAG log gate. The two delivery paths are mutually exclusive — the WiFi callback only ever pays for one per packet.
- Options:
--mode=<off|callback|async>:offdrops user delivery,callbackinvokes the registeredset_csi_callbackhook inline in the WiFi callback,asyncqueues toCSINodeClient::next_csi_packet(default for the indefinite collection path).--logging=<on|off>: Toggle the per-packetlog_csiUART/JTAG gate independently.
- Examples:
set-csi-delivery --mode=asyncset-csi-delivery --mode=off --logging=off
-
info- Description: Print a machine-parseable firmware identification block. Intended for host-side tooling that needs to verify which firmware is running on the device. The first line —
ESP-CSI-CLI/<version>— is also emitted at the top of the welcome banner on every reset, so a host can identify the firmware passively without sending this command. - Output format:
ESP-CSI-CLI/<version> name=esp-csi-cli-rs version=<version> chip=<esp32|esp32c3|esp32c5|esp32c6|esp32s3|unknown> protocol=<u32> baud=<u32> features=<comma-separated-list> END-INFO - Example:
info
- Description: Print a machine-parseable firmware identification block. Intended for host-side tooling that needs to verify which firmware is running on the device. The first line —
-
show-stats(requiresstatisticsfeature, on by default)- Description: Print a one-shot snapshot of runtime CSI / traffic counters: RX/TX packet totals, average PPS, RX/TX rate in Hz, and RX dropped packets. Counters reset on the start of each new
startcollection. - Example:
show-stats
- Description: Print a one-shot snapshot of runtime CSI / traffic counters: RX/TX packet totals, average PPS, RX/TX rate in Hz, and RX dropped packets. Counters reset on the start of each new
-
Configure an ESP as a WiFi Sniffer on channel 6 and collect indefinitely in array-list format:
set-wifi --mode=sniffer --set-channel=6 set-log-mode --mode=array-list show-config start -
Configure an ESP as a Station connected to an existing network and collect for 5 minutes:
set-wifi --mode=station --sta-ssid="My Router" --sta-password="router password" set-traffic --frequency-hz=20 show-config start --duration=300 -
HT emitter + sniffer collector (any chip — the controlled pairing):
# Collector board (sniffer on the emitter's channel, no self-generated traffic) set-wifi --mode=sniffer --set-channel=6 set-traffic --frequency-hz=0 set-log-mode --mode=array-list start # Emitter board (use ht40-emitter for 40 MHz bonded) set-wifi --mode=ht20-emitter --set-channel=6 --inject-period-ms=20 startAn emitter never associates and its frames carry no payload meaning, so one emitter sounds any number of sniffer collectors at once; unicasting with
--peer-macto one collector tends to raise that collector's CSI rate. -
ESP-NOW central + peripheral (any chip — connectionless, no AP needed):
# Central board (drives the exchange) set-wifi --mode=esp-now-central --set-channel=6 set-rate --rate=mcs0-lgi set-log-mode --mode=array-list start # Peripheral board (replies; both sides capture CSI) set-wifi --mode=esp-now-peripheral --set-channel=6 set-rate --rate=mcs0-lgi set-log-mode --mode=array-list startBoth boards must be on the same
--set-channel. Pairing is automatic; with more than two boards on the channel, set--peer-macon both to pin the pair. -
Keep a node's traffic on air without paying for CSI delivery, then check stats:
set-wifi --mode=wifi-ap --set-channel=6 set-csi-output --enabled=false set-traffic --frequency-hz=4000 start # ... after pressing 'q' to stop: show-stats -
Emit ESP32-CSI-Tool-compatible CSV for a host pipeline:
set-wifi --mode=sniffer --set-channel=6 set-log-mode --mode=esp-csi-tool start --duration=60 -
SoftAP lab pair (board A = AP collector, board B = station on same SSID):
# Board A (AP collector) set-wifi --mode=wifi-ap --set-channel=6 --ap-ssid=esp-csi-ap set-protocol --protocol=n set-traffic --frequency-hz=4000 start # Board B (station — match AP SSID/channel) set-wifi --mode=station --sta-ssid=esp-csi-ap --set-channel=6 set-protocol --protocol=n set-traffic --frequency-hz=4000 start -
5 GHz associated AP/STA pair (ESP32-C5 — serialized high-rate CSI):
# Board A (AP collector — 5 GHz ch149 on C5) set-wifi --mode=wifi-ap --set-channel=149 --ap-ssid=esp-csi-ap set-protocol --protocol=n set-traffic --frequency-hz=4000 start # Board B (station RX — record serialized CSI) set-wifi --mode=station --sta-ssid=esp-csi-ap --set-channel=149 set-protocol --protocol=n set-log-mode --mode=serialized set-io-tasks --tx=off startOn ESP32-C5 the station channel doubles as the dual-band hint (2.4 GHz:
--set-channel=6, 5 GHz:--set-channel=149). Disable station TX (set-io-tasks --tx=off) so the AP's downlink flood is not competing with a second ICMP generator.The pair scales to multiple stations: the AP's DHCP pool holds 4 leases by default (
set-wifi --ap-leases=<1-8>to change) and the ICMP flood round-robins across all associated stations, so each one captures CSI. Note the offered rate is shared — with N stations each sees roughlyfrequency-hz / Npackets per second.For temporally-synchronized multi-receiver captures, add
set-wifi --ap-burst=onon the AP: every flood tick then fires one unicast frame back-to-back to each associated station, so all stations sample the channel within tens of microseconds of one another and each sees the fullfrequency-hzrate. (A single broadcast frame cannot be used instead — on an ESP32 softAP broadcast is DTIM-buffered and dropped under load.) Total offered airtime becomesfrequency-hz × N, so lowerset-traffic --frequency-hzif the channel saturates. -
ESP-NOW fast simplex pair (highest CSI rate of any pairing):
# Collector board — sparse discovery beacon, then RX-only set-wifi --mode=esp-now-fast-collector --set-channel=6 set-log-mode --mode=serialized start # Source board — learns the collector's MAC, then unicasts a continuous flood set-wifi --mode=esp-now-fast-source --set-channel=6 startAsymmetric on purpose: the collector stops transmitting once it has heard the source, so all the airtime belongs to a single transmitter. Start the collector first so the beacon is already on air when the source comes up.
set-ratedoes not apply here — the fast profile fixes its own PHY.
The console, not the radio, is usually what limits your sample rate. A collector captures at the offered traffic rate; whether you see those samples depends on how many bytes each one costs on the serial link.
At the default 115200 baud, an array-list line for a 384-sample payload is ~2.2 kB — about 190 ms
on the wire. That write is blocking and happens inside the Wi-Fi RX callback, so a line that
takes longer to send costs you the CSI reports that arrive while it is sending. Bytes per sample is
therefore the number that decides your rate.
Measured on two ESP32-WROOM boards over a CH340 bridge, 400 Hz offered, station RX-only:
| log mode / payload | @115200 | @921600 |
|---|---|---|
array-list, all LTF (384 samples) |
8.9 CSI/s | 60.4 CSI/s |
array-list, L-LTF only (128 samples) |
22.7 CSI/s | 115.2 CSI/s |
serialized (binary), all LTF |
27.5 CSI/s | 120.0 CSI/s |
At 115200 the array-list row is pushing ~10.8 kB/s of an 11.5 kB/s line — the wire is saturated, so
that figure is the ceiling for that format at that baud. At 921600 array-list runs at ~86% of line
rate, and what limits the other two rows there is how many frames the radio captured, not the
console.
Expect run-to-run variance: on a shared 2.4 GHz channel the number of frames a collector actually
captures moved by more than 2x between sessions on the same bench, with the boards untouched. Compare
against show-stats' own RX Total Packets rather than against a number from a previous run.
Three levers, cheapest first:
set-csi --htltf=off --stbc-htltf=off— acquire only the L-LTF field. ~2.5x at 115200, no rebuild, and every sample you get is still a complete field. This is the right way to shorten a CSI line: the radio stops capturing what you do not want, so it also does less work per frame and captures more frames (measured 439 vs 205 captured over the same 15 s). Use it when the 64-subcarrier legacy estimate is enough for your application.set-log-mode --mode=serialized— binary COBS/postcard instead of decimal text. ~3x at 115200 while keeping the full payload. Requires a matching deserializer on the host.- Raise the console baud — see below. The only lever that removes the ceiling rather than
working under it: ~7x for
array-listat 921600.
📝 There is deliberately no option to truncate a CSI payload to a sample count. Cutting a line short discards subcarriers the radio already spent airtime capturing, and an arbitrary cap lands mid-LTF-field, which is not a meaningful measurement. Lever 1 does the same thing properly and measured faster than truncating to the same 128 bytes (22.7 vs 21.0 CSI/s, and 439 vs 446 frames captured), because the saving happens before the capture rather than after it.
A fourth, if third-party traffic is part of the problem: set-csi-filter drops frames before they
are ever formatted, so everything it rejects is console bandwidth handed back to the traffic you
configured.
A defmt build is not a throughput lever, despite being the more efficient logger in general.
Measured on the same pair of boards at 115200, station RX-only:
| config | println |
defmt |
|---|---|---|
array-list, 384 samples |
9.26 CSI/s (1185 B/sample) | 9.53 CSI/s (1176 B/sample) |
serialized |
27.80 CSI/s (399 B/sample) | 27.93 CSI/s (389 B/sample) |
Within a few percent either way — run-to-run noise — and every row sits at 94-97% of line rate in
both builds. The reason is that defmt earns its keep by interning format strings at compile time,
and a CSI line has none to intern: the line is formatted first and then handed to defmt as a single
runtime {=str} argument, so the bytes on the wire are the same ASCII either way. In serialized
mode it is mildly counterproductive — the COBS frame is wrapped in a defmt frame, so you pay both
framings.
Choose defmt for what it is actually good at — compact structured logs and host-side decoding
against the ELF — and reach for the levers above for CSI throughput.
Verify with show-stats: RX Total Packets counts what the radio captured and RX Dropped Pkts
counts what was discarded, so the gap between that and the rows your host recorded is the console
loss. Counters are zeroed at the start of each start and survive the end of the run, so read
them after stopping.
The baud rate is fixed when you build, via the ESP_CSI_CLI_UART_BAUD environment variable
(default 115200), and there is deliberately no runtime command to change it: a set-uart
command would have to change the rate of the very console carrying the command, so the reply is
either lost or sent at the old rate while the host has already switched, and surviving a reset would
need a persisted setting or a renegotiation handshake. Instead the rate is immutable per image and
declared — info reports it as baud=, and the release manifest.json carries it per asset for
tooling that must pick a rate before the first byte.
The variable composes with every existing alias, so there is no parallel set of aliases to keep in step:
ESP_CSI_CLI_UART_BAUD=921600 cargo esp32-build
ESP_CSI_CLI_UART_BAUD=460800 cargo esp32c6-defmtOr commit the choice for a checkout by adding it to .cargo/config.toml, which is the better home
for it — the rate then travels with the repo instead of living in whoever's shell history:
[env]
ESP_CSI_CLI_UART_BAUD = { value = "921600", force = true }force = true makes the committed value win over a stale ESP_CSI_CLI_UART_BAUD already exported in
a shell. Changing either the variable or that line rebuilds — the build script declares
rerun-if-env-changed, so an image can never silently keep a previously compiled rate.
🛑 Open your serial port at the matching rate —
espflash monitor --baud 921600, orserial.Serial(port, 921600). The build prints a warning naming the rate whenever it is not the default.
📝 The ROM bootloader banner is always emitted at 115200, whatever you build at. The first few lines after a reset are therefore unreadable at a higher rate. This is normal; the firmware's own banner (
ESP-CSI-CLI/<version>) arrives at the configured rate.
Every collection begins with rows that look wrong: a large arbitrary number in the leading field, and
a CSI payload length that differs from the rest, before settling into a clean 0, 1, 2, 3, …
sequence at a consistent length. None of these are dummy or initialization packets — every one is a
real CSI report. Two things explain the appearance:
The leading field is not a packet counter. It is sequence_number, the raw 802.11
sequence-control value of the received frame. That is per-transmitter and per-TID, so it does not
start at zero and does not share a counter with your traffic. Your data flood appears to "start at 0"
because QoS data frames use their own TID counter, separate from the management frames that precede
them.
A different payload length is a different PHY, and often a different device. A collector is promiscuous; it reports CSI for every frame its radio decodes:
csi_data_len |
sig_mode |
what it is |
|---|---|---|
| 128 | 0 (non-HT) | the L-LTF-only 64-subcarrier estimate: beacons, auth/assoc, ACKs — and any third-party device on your channel |
| 256 / 384 | 1 (HT) | your configured traffic (256 = HT20 L-LTF + HT-LTF, 384 = HT40) |
To keep only your own traffic, either filter on the device with
set-csi-filter --peer-mac=<ap-mac> --min-phy=ht (which also returns console bandwidth, since
rejected frames are never formatted), or filter on the host: in array-list mode the source MAC is
the last field on each line, after the payload array, and sig_mode / csi_data_len identify
the PHY. Filtering by MAC is the reliable one — a busy channel will always give you third-party
frames otherwise.
💡 SSIDs and passwords with spaces can be passed as quoted strings to
set-wifi. Both quote styles work —--sta-ssid='My WiFi'and--sta-ssid="My WiFi"are equivalent — so you can pick whichever your terminal/keyboard makes easier to type. Underscores (_) are passed through literally.
🛑 On ESP32-C5, 5 GHz passive sniffer CSI may return a frozen IQ buffer (driver bug). Use the AP↔STA pair (
wifi-ap+station) instead.
🛑 To stop a running collection early — including indefinite runs started without
--duration— pressq(orQ) on the serial console.
⚡ Throughput (high-rate CSI):
set-log-mode --mode=serialized,set-io-tasks --tx=offon the station board, andset-traffic --frequency-hz=4000on the AP. Avoidset-csi-delivery --mode=callbackduring capture (it was the old default and caps output around ~10 Hz). Theqstop key is polled by the CLI main loop every 5 ms, not in the WiFi callback.
This application can use either the standard println! macros or the defmt framework for logging. defmt produces compact binary frames that the host (espflash, probe-rs, etc.) decodes against the original ELF, so it's both faster on the device and richer on the host.
The recommended way is the *-defmt / *-defmt-build cargo aliases — pick the one matching your chip:
cargo esp32c6-defmt # build + flash + monitor with defmt decoding
cargo esp32c6-defmt-build # build only, skip flashingEach defmt alias automatically:
- drops
printlnfrom the default features and enablesdefmt, - appends
-Tdefmt.xto the linker script set (so the.defmtELF section is emitted), - swaps
espflash's runner toespflash flash --monitor --log-format defmtso log frames are decoded inline.
No edits to .cargo/config.toml are required — the aliases pass everything through cargo --config overrides at invocation time. If you want to invoke cargo build directly (e.g. in CI), the equivalent is:
cargo build --release \
--no-default-features \
--features esp32c6,defmt,no-std,auto,statistics \
--target riscv32imac-unknown-none-elf \
--config 'target.riscv32imac-unknown-none-elf.rustflags=["-C", "link-arg=-Tdefmt.x"]'Xtensa targets (esp32, esp32s3) need the -Wl, prefix on the link arg because their toolchain goes through a GCC linker driver:
--config 'target.xtensa-esp32s3-none-elf.rustflags=["-C", "link-arg=-Wl,-Tdefmt.x"]'specs/WEBSERVER.md— web-server / host-automation integration (REST mapping, pairing presets, v0.7.0 delta)specs/SPECS.md— complete on-device CLI specification- esp-csi-rs on docs.rs — underlying library API
This crate is still in early development and currently supports no-std only. Contributions and suggestions are welcome!
Copyright 2026 The csi-rs Team
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Made with 🦀 for ESP chips
