A complete, source-accurate reference for the on-device CLI exposed by
esp-csi-cli-rs (crate version 0.7.0). Every command, every argument, every
accepted value, every default, the exact output each command prints, and the
behavioral detail that can be derived from the source.
Sources of truth:
- Command registration / help text:
src/cli/mod.rs(ROOT_MENU) - Command handlers + printed output:
src/cli/cmds.rs - Welcome banner:
src/cli/cli.rs(enter_root) - Runtime state / defaults:
src/config.rs(UserConfig) - Input preprocessing, collection task & lifecycle:
src/main.rs - Serial backend selection:
src/cli/serial.rs - Build features / target gating:
Cargo.toml
⚠️ This document describes firmware behavior over a serial console. The CLI runs on the device; there is no host-side binary with these commands. Interaction is over UART0 or USB-Serial-JTAG via a monitor such asespflash monitor.For web-server / host-automation integration (REST mapping, pairing presets, v0.7.0 delta), see
WEBSERVER.md.
A single UserConfig instance lives in the USER_CONFIG mutex
(src/config.rs:134). It is created with UserConfig::new() at boot
(src/main.rs:256). Every set-* command mutates it in place.
The csi_collection Embassy task snapshots USER_CONFIG (a clone) only
on receipt of a START_SIGNAL (src/main.rs:572) — settings changed during a
running collection do not apply until the next start.
| Signal | Type | Producer | Consumer | Payload |
|---|---|---|---|---|
START_SIGNAL |
Signal<Option<u64>> |
start command |
csi_collection task |
seconds or None |
STOP_REQUEST |
Signal<()> |
main loop on q/Q keypress |
csi_collection → send_stop() |
() |
DONE_SIGNAL |
Signal<()> |
csi_collection at end of run |
main loop (unlocks CLI) | () |
IS_COLLECTING |
AtomicBool |
start (true) / main loop (false) |
main loop input gate | bool |
While IS_COLLECTING == true the CLI is locked (src/main.rs:374): the menu
is bypassed and only q/Q is acted on. All other bytes are read but ignored.
On a START_SIGNAL the task (src/main.rs:562):
- Snapshots
USER_CONFIG. - Maps
node_mode→ anesp-csi-rsNodeRole:WifiSniffer→Collector(Sniffer(channel))WifiStation→Collector(Station { ssid, password, WPA2-Personal })WifiAccessPoint→Collector(AccessPoint(WifiApConfig { ap, channel, ht40, dhcp }))Ht20Emitter/Ht40Emitter→Emitter(EmitterConfig { channel, Ht20|Ht40Above, period = inject_period_ms [, dst_mac = peer_mac] [, ap_interface] })EspNowCentral→Central(EspNow(EspNowConfig { channel, phy_rate [, peer_mac] [, ht40] }))EspNowPeripheral→Peripheral(EspNow(...)), same config builderEspNowFastCollector→Central(EspNowFastCollector(EspNowConfig::fast_default() { channel [, peer_mac] [, ht40] }))EspNowFastSource→Peripheral(EspNowFastSource(...)), same fast builder
- For
WifiAccessPoint,WifiStation,EspNowFastCollectorandEspNowFastSource, callscontroller.set_power_saving(PowerSaveMode::None). - Computes traffic frequency:
trigger_freq == 0→None(traffic generator off); otherwiseSome(trigger_freq as u16)(note theu64 → u16cast — values > 65535 silently truncate). - Constructs
CSINode::new(...), appliesset_csi_output_enabled(csi_output_enabled)andset_io_tasks(io_tasks). - Applies
node.set_protocol(user_config.protocol)(set viaset-protocol; defaultLR). the forced TX PHY.phy_rateis no longer applied by any mode. - Registers the CSI delivery path for the run:
- If
delivery_raw(set-csi-delivery --mode=raw):set_csi_logging_enabled(false)set_csi_raw_callback(raw_csi_noop)— zero-copy, no packet built, no data delivered/logged, no q-key stop peek.
- Otherwise:
set_csi_logging_enabled(csi_output_enabled). This callback runs inline in the WiFi callback for every packet: it (a) peeks the JTAG OUT-EP FIFO forq/Qand signalsSTOP_REQUEST, and (b) clones the packet intolog_csiso CSI lines stream to the host.
- If
- Runs:
Some(secs)→CSINode::run_duration(secs, client)raced against a stop watcher.None→CSINode::run()raced against the stop watcher (indefinite).
- On exit,
esp-csi-rs'sreset_globalsnulls the callback/gates back toOff, so the run-time delivery registration is re-applied everystart. - Signals
DONE_SIGNAL→ main loop unlocks the CLI.
Important consequence: because the task re-registers
csi_log_and_check(or the raw no-op) on everystartand forcesset_csi_logging_enabled(false), the values you set withset-csi-deliverytake effect immediately (between runs) but are overridden at the start of each run by the task's own registration. The exception is--mode=raw, which is stored indelivery_rawand consumed by the task atstart.
Implemented in src/main.rs:371-545. The menu crate's per-keystroke echo is
disabled; the preprocessor echoes bytes itself so quoting renders correctly.
- Quoting:
'…'and"…"both group whitespace. The opening quote style must be closed by the same style; the other style appears literally inside. Quote characters are echoed but not forwarded to the menu tokenizer. - Space sentinel: a space inside quotes is forwarded as
0x1F(US). Handlers that take free-form text (--sta-ssid,--sta-password,--ap-ssid,--ap-password) decode0x1F → ' '. - Underscores: passed through literally — no
_ → ' 'substitution. - Backspace (
0x08/0x7F): pops the visible-character shadow buffer; quote state is recomputed viarecompute_quote_state. - Newline (
\ror\n): drops any half-open quote, clears the shadow, and on\rerases the input line before forwarding tomenu. - Buffer limit: the input buffer is 256 bytes (
CLI_BUF_LEN). At capacity the terminal bell (0x07) rings once and further bytes are dropped silently (preventing themenucrate's per-byte "Buffer overflow!" spam). Space frees on backspace / submit.
| Build | Backend |
|---|---|
esp32 |
UART0 only (RX=GPIO3, TX=GPIO1); no USB-JTAG |
jtag-serial (non-ESP32) |
Forced UsbSerialJtag |
uart (non-ESP32) |
Forced Uart on UART0 |
auto (default, non-ESP32) |
Runtime is_jtag() probe → UsbJtag if USB host present, else Uart |
is_jtag() reads the USB-Serial-JTAG SOF interrupt-raw bit; set ⇒ USB host
present ⇒ JTAG path; clear ⇒ UART0 fallback.
All commands are children of the root menu (src/cli/mod.rs:39). help and
help <command> are provided automatically by the menu crate.
Argument syntax:
--flag— boolean toggle (Parameter::Named); presence is the signal.--key=<value>— value when supplied (Parameter::NamedValue).[--key=<value>]— optional; omission preserves the current state.
Configure traffic generation frequency.
| Argument | Type / Values | Default | Meaning |
|---|---|---|---|
--frequency-hz=<N> |
u64 Hz |
100 |
0 disables traffic generation entirely. |
--type=<T> |
registered, unused | — | Reserved no-op (declared in ROOT_MENU, ignored by the handler). |
Output (always reprints the resulting value):
Updated Traffic Configuration:
Traffic Frequency: 100Hz
Behavior / inferences:
- Stored in
trigger_freq(u64). Atstartit is cast tou16; values65535 truncate silently.
- Invalid (non-numeric)
--frequency-hzprintsInvalid Frequencyand leaves the value unchanged (then still prints the config block). - If
set-io-tasks --tx=off, traffic is not generated regardless of frequency.
Toggle off-device delivery of captured CSI.
| Argument | Values | Default |
|---|---|---|
--enabled=<bool> |
true | false |
true |
Output:
CSI Output: true
Behavior:
- Stored in
csi_output_enabled; applied atstartviaCSINode::set_csi_output_enabledandset_csi_logging_enabled. - With delivery off the radio still captures CSI — RX path and timing unchanged — but nothing is decoded, logged, or handed to a callback. No effect on an node.
- Accepts the shared on/off vocabulary (
true|false,on|off,1|0,enable[d]|disable[d],yes|no). - Unrecognized value prints
Invalid --enabled value. Use 'true' or 'false'.and does not mutate state. - Omitting
--enabledprintsUsage: set-csi-output --enabled=<true|false>and does not mutate.
Pick the CSI packet output format. Calls
esp_csi_rs::logging::logging::set_log_mode directly — takes effect on the
next logged packet, no restart needed.
| Argument | Values | Default (init) |
|---|---|---|
--mode=<fmt> |
text | array-list | serialized | esp-csi-tool |
array-list (init_logger in src/main.rs:234) |
Output: \nLog Mode: <Text|ArrayList|Serialized|EspCsiTool>
Behavior:
text→LogMode::Text— verbose, human-readable, full metadata.array-list→LogMode::ArrayList— compact CSV-style, one line per packet.serialized→LogMode::Serialized— binary COBS-framed postcard (needs a host-side deserializer).esp-csi-tool→LogMode::EspCsiTool— Hernandez 26-columnCSI_DATA,...CSV compatible with the ESP32-CSI-Tool collector.- Unrecognized value prints
Invalid mode. Use 'text', 'array-list', 'serialized', or 'esp-csi-tool'.; missing value prints theUsage:hint. Neither mutates. - Note: the README narrative quotes
textas default, but the logger is initialized toArrayListat boot. Effective initial mode isArrayList.
CSI feature flags. Argument set is target-gated — chips expose different parameters. Both variants reprint the full CSI configuration after applying.
#[cfg(not(any(feature = "esp32c5", feature = "esp32c6")))] (src/cli/cmds.rs:362).
| Argument | Effect (when present) | Default |
|---|---|---|
--disable-lltf |
csi_config.lltf_en = false |
enabled |
--disable-htltf |
csi_config.htltf_en = false |
enabled |
--disable-stbc-htltf |
csi_config.stbc_htltf2_en = false |
enabled |
--disable-ltf-merge |
csi_config.ltf_merge_en = false |
enabled |
Output:
Updated CSI Configuration:
LLTF Enabled: true
HTLTF Enabled: true
STBC HTLTF Enabled: true
LTF Merge Enabled: true
These flags are monotonic within a session — there is no CLI affordance to
re-enable a disabled flag except reset-config.
#[cfg(any(feature = "esp32c5", feature = "esp32c6"))] (src/cli/cmds.rs:85).
| Argument | Effect (when present) | Default |
|---|---|---|
--disable-csi |
csi_config.enable = 0 |
enabled |
--disable-csi-legacy |
acquire_csi_legacy = 0 (L-LTF / 11g) |
enabled |
--disable-csi-ht20 |
acquire_csi_ht20 = 0 |
enabled |
--disable-csi-ht40 |
acquire_csi_ht40 = 0 |
enabled |
--val-scale-cfg=<0-3> |
val_scale_cfg = N (u32) |
2 |
Output:
Updated CSI Configuration:
Acquire CSI: 1
Acquire Legacy CSI: 1
Acquire HT20: 1
Acquire HT40: 1
Scale Value: 2
Behavior:
--val-scale-cfgaccepts anyu32; the0-3range is documented in help but not enforced by the parser.- A non-numeric value prints
Invalid Max Connections(a misnamed but harmless message) and leaves the field unchanged. - Per help text: CSI configuration applies to all modes; AP mode uses the same
CsiConfigsnapshot as other modes.
WiFi / radio operating parameters.
| Argument | Values | Default |
|---|---|---|
--mode=<m> |
station | sniffer | wifi-ap | ht20-emitter | ht40-emitter | esp-now-central | esp-now-peripheral | esp-now-fast-collector | esp-now-fast-source |
sniffer |
--sta-ssid=<SSID> |
UTF-8, ≤ 32 bytes; quoting allowed | empty |
--sta-password=<PASSWORD> |
UTF-8, ≤ 32 bytes; quoting allowed | empty |
--ap-ssid=<SSID> |
UTF-8, ≤ 32 bytes; quoting allowed | esp-csi-ap |
--ap-password=<PASSWORD> |
UTF-8, ≤ 32 bytes; quoting allowed; empty = open AP | empty |
--ap-dhcp=<on|off> |
on/off/true/false/1/0/yes/no |
on |
--set-channel=<N> |
u8; valid WiFi channels 1–14 |
1 |
--peer-mac=<MAC> |
Emitter dst addr / explicit ESP-NOW peer: aa:bb:cc:dd:ee:ff or aa-bb-...; empty clears |
broadcast / auto |
--ht40=<above|below|none> |
softAP secondary channel, or ESP-NOW per-peer HT40 TX PHY; none/off clears |
none (HT20) |
--inject-period-ms=<MS> |
Emitter inter-frame period, positive integer | 20 |
--emitter-iface=<sta|ap> |
Emitter injection interface | sta |
Output (reprints the resulting WiFi config):
Updated WiFi Configuration:
WiFi Mode: WifiSniffer
WiFi Channel: 1
Station WiFi Settings:
SSID: '', Password: ''
Access Point Settings:
SSID: 'esp-csi-ap', Password: (open), DHCP: true
Peer MAC: unset (emitter broadcasts / ESP-NOW auto-pairs)
Secondary Channel: HT20/legacy
Emitter Period: 20ms
Behavior / inferences:
- Mode →
NodeMode:station→WifiStation,sniffer→WifiSniffer,wifi-ap→WifiAccessPoint,ht20-emitter→Ht20Emitter,ht40-emitter→Ht40Emitter,esp-now-central→EspNowCentral,esp-now-peripheral→EspNowPeripheral,esp-now-fast-collector→EspNowFastCollector,esp-now-fast-source→EspNowFastSource. Unknown mode printsInvalid WiFi Mode; field unchanged. There are no legacy aliases — the removedesp-now-*strings are rejected like any other unknown mode. - Channel parsed as
u8; non-numeric printsInvalid Max Connections(misnamed). Out-of-range (>14) is accepted by the parser but rejected by the radio atstart. Channel flows to sniffer, AP, the emitters, and the ESP-NOW modes;WifiStationderives its channel from the associated AP. --sta-ssid/--sta-password/--ap-ssid/--ap-passwordare stored inheapless::String<32>. A value longer than 32 bytes panics (unwraponpush_str). Spaces require quoting (arrive as0x1F, decoded back here). Underscores stay literal.--ap-dhcp:on/true/1/yes→serve_dhcp = true;off/false/0/no→false. Invalid value printsInvalid --ap-dhcp (use on|off).wifi-apmode only (stored regardless of current mode).- AP auth: empty
--ap-password→ open AP (AuthenticationMethod::None); non-empty → WPA2-Personal with the given password. --peer-mac: the destination address of injected frames. Unicasting to a collector usually raises that collector's CSI rate. An empty value resets toNone(broadcast). A malformed MAC printsInvalid --peer-mac (use aa:bb:cc:dd:ee:ff). Emitter and ESP-NOW modes only.--ht40:above/belowrun thewifi-apsoftAP as HT40 with that secondary channel;none/offreverts to HT20. Any other value printsInvalid --ht40 (use above|below|none). Inwifi-apit is the softAP's secondary channel; in the ESP-NOW modes it forces the per-peer TX PHY to HT40. It does not select emitter bandwidth — use--mode=ht40-emitterfor that.--inject-period-ms: emitter inter-frame delay; must be a positive integer, otherwiseInvalid --inject-period-ms (use a positive integer).--emitter-iface:sta|ap, otherwiseInvalid --emitter-iface '<v>' (use sta|ap).
Begin a CSI collection run.
| Argument | Type | Default |
|---|---|---|
--duration=<SECS> |
u64 |
omitted ⇒ run indefinitely |
Output:
- Timed:
Starting CSI collection for <secs>s... - Indefinite:
Starting CSI collection indefinitely... - Invalid
--duration:Invalid durationand the command aborts (no signal, CLI not locked).
Behavior:
- Sets
IS_COLLECTING = trueand signalsSTART_SIGNALwithSome(secs)orNone. See §1.3 for everything the collection task then does. - Timed run →
CSINode::run_duration(secs). Indefinite run →CSINode::run(). In both, the registeredcsi_log_and_checkcallback is the sole writer of CSI lines (the indefinite path no longer joins a separate print loop).
Stop conditions:
- Pressing
q/Qduring a run raisesSTOP_REQUEST, forwarded toesp-csi-rsviaCSINodeClient::send_stop(), which unwindsrun/run_duration. Two parallel reader paths catch the key (§1.3 /src/main.rs:401): an asyncRead::readarm and a 5 msTimerarm that raw-polls the JTAG OUT-EP FIFO — making the stop key deterministic even under ISR starvation. The console printsStopping...once, thenCollection complete.when done. - A timed run also ends when its duration elapses.
--mode=rawruns have no q-key peek — they are duration-bound or reset-driven only.- Both paths fire
DONE_SIGNAL, unlocking the CLI.
Print the current UserConfig. No arguments.
Sections: [WiFi], [Collection], [CSI Config]. The [CSI Config] block is
target-gated (per-PPDU-format acquisition flags on C5/C6, classic fields
elsewhere).
Output (classic-chip example):
====== Current Configuration ======
[WiFi]
Mode : WifiSniffer
Channel : 1
STA SSID: ''
STA Pass: ''
AP SSID : 'esp-csi-ap'
AP Pass : open
AP DHCP : true
Dst MAC : broadcast
AP 2nd : HT20/legacy
[Collection]
CSI Output : true
Traffic Freq : 100Hz
PHY Rate : RateMcs0Lgi
Protocol : LR
IO Tasks : tx=true, rx=true
Emitter : period=20ms
[CSI Config]
LLTF Enabled : true
HTLTF Enabled : true
STBC HTLTF Enabled : true
LTF Merge Enabled : true
Channel Filter : <CsiConfig default>
Manual Scale : <CsiConfig default>
Shift Bits : <CsiConfig default>
Dump ACK : <CsiConfig default>
===================================
On C5/C6 the [CSI Config] block instead prints Acquire CSI, Legacy (11g),
HT20, HT40, Scale Value.
The classic branch additionally prints four fields that cannot be changed via
the CLI: channel_filter_en, manu_scale, shift, dump_ack_en. Restore
them with reset-config.
Replace the live UserConfig with UserConfig::new(). No arguments.
Restores every field to its compiled-in default (§4).
Output: \nConfiguration Reset to Default Values\n
Record the Wi-Fi PHY rate. Nothing applies it since the ESP-NOW transport was
removed: collectors derive their rate from the surrounding radio configuration
The esp-now-central / esp-now-peripheral pair DOES apply it as the per-peer TX PHY. Kept because
show-config still reports it.
| Argument | Values | Default |
|---|---|---|
--rate=<N> |
1m/1m-l, 2m, 5m5/5m5-l, 11m/11m-l, 6m, 9m, 12m, 18m, 24m, 36m, 48m, 54m, mcs0-lgi..mcs7-lgi, mcs0-sgi |
mcs0-lgi |
Output: \nPHY Rate: Rate<...> (Debug form of the WifiPhyRate variant).
Behavior:
1m/5m5/11m: only the-l(long-preamble) variant exists; the bare names alias the sameRate*mLenum value.- Unknown rate prints
Invalid rate. Try mcs0-lgi (default), mcs7-lgi, 6m, 24m, 54m, etc.; no mutation. - Omitting
--rateprintsUsage: set-rate --rate=<rate>.
Set the Wi-Fi PHY protocol applied via CSINode::set_protocol at each start.
| Argument | Values | Default |
|---|---|---|
--protocol=<p> |
b | g | n | lr | a | ac |
lr |
Output: \nProtocol: <Debug form> (e.g. Protocol: LR).
Behavior:
- Stored in
UserConfig.protocol; snapshotted atstart. Ignored by the lrsuits sniffer links between ESP devices.- Use
nfor station mode against a standard AP and for AP + STA lab pairs (wifi-ap+station). - Unknown protocol prints
Invalid protocol. Use one of: b, g, n, lr (default), a, ac. - Omitting
--protocolprintsUsage: set-protocol --protocol=<b|g|n|lr|a|ac>.
Toggle per-direction TX/RX tasks via IOTaskConfig. Applied at start via
node.set_io_tasks(...).
| Argument | Truthy values | Falsy values | Default |
|---|---|---|---|
--tx=<bool> |
on,true,1,yes |
off,false,0,no |
TX enabled |
--rx=<bool> |
(same) | (same) | RX enabled |
Output: \nIO Tasks: tx=true, rx=true
Behavior:
- Both arguments optional and independent; omission leaves current value.
- Disabling RX = pure transmitter (skips WiFi-callback CSI path).
- Disabling TX = pure receiver (no traffic generation, regardless of
set-traffic). - Invalid value prints
Invalid --tx value (use on|off).(or--rx); field unchanged. The resultingtx=…, rx=…line is still printed.
Switch the CSI delivery path and the inline log gate. off/callback/async
call esp_csi_rs::set_csi_delivery_mode immediately (next packet); raw only
sets the delivery_raw flag consumed at the next start. --logging calls
set_csi_logging_enabled immediately.
| Argument | Values | Notes |
|---|---|---|
--mode=<m> |
off | callback | async | raw |
see below |
--logging=<b> |
on/true/1/yes ∕ off/false/0/no |
toggles inline log_csi UART/JTAG gate |
Mode meanings:
off→ drop user-side dispatch (inlinelog_csimay still run). Output:Delivery mode: Off.callback→ dispatch synchronously to the registeredset_csi_callbackhook. Output:Delivery mode: Callback.async→ enqueue forCSINodeClient::next_csi_packet. Output:Delivery mode: Async.raw→ setdelivery_raw = true; registers the zero-copyraw_csi_noopfast-path on the nextstart(no packet built, no data delivered or logged, no q-key stop). Output:Delivery mode: Raw (zero-copy fast-path; applies on next start, no CSI data delivered).off/callback/asyncall cleardelivery_rawback to false.- Unknown mode prints
Invalid mode. Use 'off', 'callback', 'async', or 'raw'.
--logging outputs Inline CSI logging: ON/OFF; an invalid value prints
Invalid --logging value (use on|off).. Both arguments are optional; either or
both may be given in one invocation.
Caveat (see §1.3): the collection task forces
set_csi_logging_enabled(false)and re-registerscsi_log_and_check(or the raw no-op) on everystart, sooff/callback/asyncand--loggingare most useful for runtime tweaks between registrations; onlyrawis durably honored across astart.
Print a one-shot snapshot of runtime CSI / traffic counters. No arguments.
When the statistics feature is absent the command is not registered and
help won't list it.
Output:
====== Runtime Statistics ======
RX Total Packets : 0
TX Total Packets : 0
RX PPS (avg) : 0
TX PPS (avg) : 0
RX Rate (Hz) : 0
TX Rate (Hz) : 0
RX Dropped Pkts : 0
================================
Counters come from esp_csi_rs::get_* (get_total_rx_packets,
get_total_tx_packets, get_pps_rx, get_pps_tx, get_rx_rate_hz,
get_tx_rate_hz, get_dropped_packets_rx). The ESP-NOW TX queued / confirmed /
failed counters are gone with the transport.
Behavior:
- Counters reset on the start of each new
startcollection. - Values remain queryable between runs; not cleared by
reset-config.
Print a machine-parseable firmware identification block. No arguments.
Output format (stable within a protocol value;
CLI_PROTOCOL_VERSION = 2, src/cli/cmds.rs:26):
ESP-CSI-CLI/<version>
name=esp-csi-cli-rs
version=<version>
chip=<esp32|esp32c3|esp32c5|esp32c6|esp32s3|unknown>
protocol=<u32>
mac=<AA:BB:CC:DD:EE:FF>
features=<comma-separated-list>
END-INFO
Behavior:
name/versionfromCARGO_PKG_NAME/CARGO_PKG_VERSION(compile time).chipresolved from the target feature;unknownif no chip feature set.mac(protocol >= 2) is the factory eFuse base MAC (esp_hal::efuse::base_mac_address), uppercase colon-separated hex. On native-USB boards this is also the USBiSerialNumber, so host tooling keys per-device tasks off it instead of the enumeration-order/dev/ttyACM*path. This makesrestart(§2.14) and the USB re-enumeration it triggers a non-event — the per-device task re-binds to the same physical board.featuresenumerates the compile-time subset of{statistics, defmt, println, async-print, auto, jtag-serial, uart}(in that emit order, but host code should treat it as an unordered set). Empty if none.- The first line
ESP-CSI-CLI/<version>is also the first line of the welcome banner on every reset (§6), so a host can identify firmware passively. The banner also carries themac=line for the same purpose.
Reboot the device via esp_hal::system::software_reset(). No arguments.
Output: \nRestarting...\n, flushed to the serial transport before the
reset fires; the firmware then reboots and re-emits the welcome banner.
Behavior / inferences:
- On native-USB boards (built-in USB-Serial-JTAG) the reset drops and
re-enumerates the USB device, so the port may return as a different
/dev/ttyACM*. Paired with themac=device key (§2.13, §8), this is a non-event: the host re-binds to the same board by serial number. - Reachable only from the idle CLI — during a collection run the input loop is
locked to the
q/Qstop key (§1.3), sorestartcannot be invoked mid-run. - Diverges (
software_resetreturns!); nothing after it runs. On reboot the banner (magic line +mac=) is re-emitted, which the host greps to confirm the board is back.
| Form | Effect |
|---|---|
help |
List all commands with their one-line summaries. |
help <command> |
Print that command's full help: block from ROOT_MENU. |
| Setting | WifiSniffer | WifiStation | WifiAccessPoint | Ht20/Ht40Emitter | EspNow* |
|----------------------|:-------------:|:-------------:|:-----------------:|:-----------------------------:|
| --set-channel | ✅ | ❌ (from AP) | ✅ | ✅ | ✅ |
| --sta-ssid/pwd | ❌ | ✅ | ❌ | ❌ | ❌ |
| --ap-ssid/pwd/dhcp | ❌ | ❌ | ✅ | ❌ | ❌ |
| --peer-mac | ❌ | ❌ | ❌ | ✅ (dst addr) | ✅ (peer) |
| --ht40 | ❌ | ❌ | ✅ (AP secondary) | ❌ (use ht40-emitter) | ✅ (HT40 PHY) |
| set-csi-output | ✅ | ✅ | ✅ | ❌ (no capture) | ✅ |
| set-rate | ❌ (no-op) | ❌ (no-op) | ❌ (no-op) | ❌ (forced PHY) | ✅ c/p; ❌ fast |
| set-protocol | ✅ | ✅ | ✅ | ❌ (forced PHY) | ✅ |
| --inject-period-ms | ❌ | ❌ | ❌ | ✅ | ❌ |
| Auth method | n/a | WPA2-Personal (hardcoded) | None or WPA2 from --ap-password | n/a | n/a |
| Setting category | Applied at | Notes |
|---|---|---|
set-traffic, set-csi-output, set-csi, set-wifi, set-rate, set-protocol, set-io-tasks |
Next start |
Snapshotted from USER_CONFIG |
set-log-mode |
Immediate (next packet) | Calls set_log_mode directly |
set-csi-delivery --mode=off/callback/async, --logging |
Immediate (but re-overridden at next start) |
Calls esp-csi-rs setters directly |
set-csi-delivery --mode=raw |
Next start |
Stored in delivery_raw |
Press q/Q |
Immediate during a run | Drains via send_stop() |
| Cargo feature | Effect on CLI |
|---|---|
esp32 |
UART0 only (RX=GPIO3, TX=GPIO1); no JTAG / is_jtag |
esp32c3/esp32c5/esp32c6/esp32s3 |
UART or USB-JTAG; auto enables runtime detection |
esp32c5/esp32c6 |
set-csi per-PPDU-format variant; matching show-config fields |
| (other chips) | set-csi classic LLTF/HTLTF variant |
statistics (default) |
Registers show-stats; otherwise omitted entirely |
auto (default, non-ESP32) |
SerialInterface enum dispatches UART/JTAG at runtime |
jtag-serial |
Forces JTAG backend (pulls in async-print) |
uart |
Forces UART backend (don't combine with async-print) |
println (default) |
println!-based logging |
defmt |
defmt binary framing; needs host decoder; not with async-print/jtag-serial |
async-print |
Non-blocking async logger (auto-enabled by jtag-serial) |
| Field | Default |
|---|---|
node_mode |
WifiSniffer |
csi_output_enabled |
true |
trigger_freq |
100 Hz |
sta_ssid |
empty |
sta_password |
empty |
ap_ssid |
esp-csi-ap |
ap_password |
empty (open AP) |
serve_dhcp |
true |
csi_config |
CsiConfig::default() (all flags enabled / max detail) |
channel |
1 |
phy_rate |
WifiPhyRate::RateMcs0Lgi |
protocol |
Protocol::LR |
io_tasks |
TX + RX both enabled |
peer_mac |
None (auto magic-prefix pairing) |
ht40_secondary |
None (HT20/legacy) |
delivery_raw |
false |
Logger init default (src/main.rs:234): LogMode::ArrayList.
| Failure | Behavior |
|---|---|
| Unparsable numeric arg | Prints an error string; field unchanged |
Unknown enum value (--mode, --rate) |
Prints a usage/error hint; field unchanged |
| Missing required arg | Prints a usage hint; field unchanged |
| Unknown command | menu prints its own "command not found" |
--sta-ssid/--sta-password/--ap-ssid/--ap-password > 32 bytes |
Panics (heapless::String::push_str().unwrap()) |
--ap-dhcp invalid |
Prints `Invalid --ap-dhcp (use on |
--peer-mac malformed |
Prints Invalid --peer-mac (...); field unchanged |
--ht40 not above/below/none |
Prints Invalid --ht40 (...); field unchanged |
--val-scale-cfg outside 0..=3 |
Prints Invalid --val-scale-cfg value ... (use 0-3) |
--set-channel outside 1..=14 |
Accepted by parser; rejected by radio at start |
| Input line > 256 bytes | Bell once, further bytes dropped silently |
Any byte except q/Q during a run |
CLI locked; byte read but ignored |
| Input typed | Forwarded to handler |
|---|---|
set-wifi --sta-ssid=Foo |
Foo |
set-wifi --sta-ssid='My WiFi' |
My WiFi |
set-wifi --sta-ssid="My WiFi" |
My WiFi |
set-wifi --sta-ssid="O'Brien" |
O'Brien (other quote literal) |
set-wifi --sta-ssid='He said "hi"' |
He said "hi" |
set-wifi --mode=wifi-ap --ap-ssid='Lab AP' |
Lab AP |
Backspace correctly retracts quote state via recompute_quote_state.
| Command | Purpose | Apply timing | Build gating |
|---|---|---|---|
set-traffic |
Traffic-gen frequency | Next start |
always |
set-csi-output |
Deliver captured CSI or not | Next start |
always |
set-log-mode |
Output format | Immediate | always |
set-csi |
CSI feature flags (variant per chip) | Next start |
classic / C5-C6 |
set-wifi |
Mode / SSID / pass / AP / channel / peer / emitter period | Next start |
always |
set-protocol |
Wi-Fi PHY protocol | Next start |
always |
start |
Begin collection (timed/indefinite) | — | always |
show-config |
Print current config | — | always |
reset-config |
Restore defaults | Next start |
always |
restart |
Reboot via software reset | Immediate | always |
set-rate |
PHY rate (recorded, never applied) | — | always |
set-io-tasks |
Toggle TX / RX tasks | Next start |
always |
set-csi-delivery |
Delivery mode + inline log gate | Immediate / raw next start |
always |
show-stats |
Runtime counter snapshot | — | statistics |
info |
Firmware identification block | — | always |
help [cmd] |
Help text | — | always (menu) |
Two surfaces emit the magic prefix ESP-CSI-CLI/<version> so host tooling can
recognize this firmware.
enter_root (src/cli/cli.rs:8) emits the magic line as the first line of
the banner on every reset / root-menu re-entry:
ESP-CSI-CLI/0.7.0
mac=D0:CF:13:E2:90:E8
******* Welcome to the CSI Collection CLI utility! *******
Available Commands:
set-wifi Configure WiFi settings (e.g., mode).
...
A host can match the first non-bootloader line against
^ESP-CSI-CLI/\d+\.\d+\.\d+$ after reset — no command roundtrip needed. The
mac= line (protocol >= 2) immediately follows, giving the host the device's
stable serial number passively on every reset, so it can re-bind a per-device
task after a restart/re-enumeration without a command roundtrip.
For on-demand identification, invoke info (§2.13) — same magic prefix plus a
key=value body terminated by END-INFO.
version(afterESP-CSI-CLI/) is cosmetic; bumps with releases.protocol(CLI_PROTOCOL_VERSION) is the wire-format version; host tooling should refuseprotocolvalues it does not understand. Theinfogrammar (line order, keys, sentinel) is stable within aprotocolvalue; adding lines/keys requires aprotocolbump.featuresis informational; presence ofstatisticstells the host whethershow-statsexists. Treat the list as an unordered set.mac(added inprotocol = 2) is the stable device key. Host tooling pins per-device tasks to it instead of the/dev/ttyACM*path, so arestart/ USB re-enumeration re-binds to the same physical board.