Skip to content

feat: build-time console baud, CSI source filtering, and esp-csi-rs 0.10.0 - #10

Merged
VoidReg merged 13 commits into
masterfrom
feat/emitter-collector
Aug 25, 2026
Merged

VoidReg merged 13 commits into
masterfrom
feat/emitter-collector

Conversation

@VoidReg

@VoidReg VoidReg commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Brings the CLI onto the released esp-csi-rs 0.10.0 and adds the console-throughput and CSI
filtering work that came out of investigating #7.

Depends on csi-rs/esp-csi-rs#29 for the library side; 0.10.0 is already on crates.io, so this
branch builds from a clean clone with no [patch.crates-io] entry — which is why the CLI was
held back until the library shipped.

Console throughput

#7 reports a collector's sample rate collapsing. It is console-bandwidth-bound, not radio-bound —
the radio captures several times what the host receives, and the gap was invisible because the
statistics counters were being cleared by the end-of-run reset before anyone could read them.

The levers, measured on two classic ESP32 boards:

change effect
baud 115200 → 921600 ~7×
serialized log mode ~3×
set-csi --htltf=off (acquisition) ~2.3× (6.5 → 14.7 CSI/s)
defmt instead of println none (9.26 vs 9.53 CSI/s)

defmt is included because it is the intuitive answer and it is wrong: a CSI line is already
formatted ASCII handed over as a single {=str} argument, so there is nothing left to compress.

  • Console baud is now build-time, via ESP_CSI_CLI_UART_BAUD (default 115200, validated
    1..=5_000_000, with cargo:rerun-if-env-changed). Build-time rather than a runtime command:
    changing the baud of the link you are typing on cannot be done from the far end of that link
    without a reconnect, and the value has to be known before the first byte of boot output.
  • info declares the baud so a host can confirm what it connected at. CLI_PROTOCOL_VERSION
    stays at 2 — the grammar is unchanged.

CSI filtering — set-csi-filter

Reduces what crosses the console at the source rather than truncating records:

  • --peer-mac delivers only frames from one transmitter. Several emitters can share one collector,
    so attribution belongs on the device.
  • --min-phy drops frames below a PHY floor, e.g. legacy beacons in an HT-only capture.

Rejected frames count as RX drops, so the loss stays visible in show-stats instead of vanishing.

ESP-NOW modes restored

esp-now-central, esp-now-peripheral, esp-now-fast-collector and esp-now-fast-source sit
alongside the HT20/HT40 emitters. Both families are needed: on chips whose MAC refuses a forced HT
rate, ESP-NOW is the emitter path that works.

Not included, deliberately

An earlier revision added --max-samples to truncate the emitted payload. It was withdrawn after
measurement: set-csi --htltf=off reduces what the radio acquires and beat truncation on both
axes — 23.92 vs 20.98 CSI/s and 506 vs 446 captured. Truncating output to go faster is the wrong
layer when the acquisition itself can be made smaller.

This crate is firmware

Marked publish = false. The build product is an image flashed to an ESP32, so there is nothing
for a registry consumer to depend on; it ships as source plus a git tag, and esp-csi-rs is the
published artifact.

Verification

All five chip targets build against the registry-resolved crate. On two classic ESP32 boards:
22/22 console, statistics, acquisition and filter checks, and 17/17 ESP-NOW and emitter
checks. A 15-config matrix at 115200 and 8 at 921600, with one known pre-existing failure (an
AP/STA run followed by an ESP-NOW run yields nothing on the first ESP-NOW attempt and recovers on
the second — reproduced on a pre-merge image).

Relates to #7.

VoidReg and others added 13 commits July 27, 2026 04:19
`esp-csi-rs-core` replaced the ESP-NOW central/peripheral architecture with
two exhaustive roles — an emitter puts known RF energy into the channel and
never captures; a collector captures the channel's response — and removed the
ESP-NOW transport outright. This crate defines the `set-wifi --mode=`
vocabulary the whole host-side ecosystem parrots, so the rename has to land
here for the strings downstream to mean anything.

The ESP-NOW modes go with the transport rather than being deprecated in place:
they cannot work, so accepting them would only defer the failure to runtime.
Deliberately NO legacy aliases — an old string must fail loudly at the CLI
instead of silently drifting from what the firmware actually does. In exchange
the HT20/HT40 raw emitters, previously proprietary-only, are available here:
HT is plain 802.11n and core owns the emitter now.

`CollectionMode::{Collector,Listener}` is replaced by
`set-csi-output --enabled=`. Its two states were never independent of the role
(an emitter never collects; a collector that discards CSI does nothing useful),
and "collector" now names the RX role — keeping the flag would give one word
two meanings in every doc. What remains is a boolean: does CSI leave the device.

`--peer-mac` and `--ht40` survive by name but are retargeted to their only
remaining referents — the emitter's injection destination and the softAP's
secondary channel — matching what `esp-csi-cli-rs-pro` already exposes, so the
two firmwares keep one vocabulary. 40 MHz emission is selected by
`--mode=ht40-emitter`, not by `--ht40`. `--inject-period-ms` is new and is the
emitters' only rate control. `set-rate` becomes reporting-only: nothing applies
a PHY rate now that ESP-NOW is gone.

No HE20 anywhere: those modes stay `esp-csi-cli-rs-pro`-only. The C5/C6
`set-csi` acquisition flags are kept as-is — they are chip capability flags
(legacy / HT20 / HT40 / VHT20 / forced L-LTF), none of them HE — but the prose
calling them the "HE/STBC field set" was renamed, since it implied an HE
capability this firmware does not have, and `--csi-he-stbc` was dropped from
SPECS.md as a flag that never existed in the code.

`esp-csi-rs` / `esp-csi-rs-core` are patched to their `feat/emitter-collector`
branches until the role API is released; `CLI_PROTOCOL_VERSION` stays at 2
because the `info` grammar is unchanged.

Verified with the full CI matrix (both flavors, all five chips): esp32,
esp32s3, esp32c3, esp32c5, esp32c6 — no errors, no new warnings.

Refs #6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pro firmware's `version` help already promised that "an open build answers
`open <semver>` from the same command", but no such command existed here -- so
the claim was false and host tooling had nothing positive to read from an open
board.

This matters beyond tidiness: csi-pipeline's selftest now prefers `version` over
probing a proprietary-only mode string and inspecting the rejection, precisely
because a positive statement survives mode renames. Without this the open case
always fell through to the slower mode-probe fallback.

Refs #6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refs #6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refs #6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EmitterConfig::with_ap_interface has always existed in the library but nothing
exposed it, so an emitter could only ever inject on the STA interface. The driver
accepts raw TX on both, but which one actually radiates turned out to be
chip-dependent while investigating an S3 that accepts every injection and
radiates none, so the choice belongs to the operator rather than being
hard-coded.

Refs #6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On the C5 the station channel hint pins the radio band, and --set-channel is that
hint. Its default is 149 (5 GHz) on that part, so associating to a 2.4 GHz AP
without passing an explicit 2.4 GHz channel makes the AP invisible and reports
only 'no access point found' -- with nothing pointing at the band as the cause.
Cost me a wrong conclusion during hardware testing, so it is now stated in the
help text with a worked example.

Refs #6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four changes, all driven by the same reported problem: a collector delivering far
fewer CSI samples than the traffic rate configured for it.

**Console baud is a build-time setting.** `ESP_CSI_CLI_UART_BAUD` (default 115200),
read in `build.rs`, range-validated, passed to `Config::with_baudrate` at all three
`Uart::new` sites. Not a runtime command: one would have to change the rate of the
console carrying it, so the reply is either lost or sent at the old rate while the
host has already switched. The rate is instead immutable per image and declared —
`info` reports `baud=`, and `manifest.json` carries it per asset for tooling that
must choose a rate before the first byte. `CLI_PROTOCOL_VERSION` stays at 2: the
grammar is unchanged, host parsers end with `_ => {}`, and the host's check is exact
equality, so a bump would refuse otherwise-good boards at attach.

**`set-csi-filter --peer-mac= --min-phy=`.** A collector is promiscuous, so a
capture mixes the AP's beacons and ACKs and third-party devices in with the
configured traffic. Rejection happens in the Wi-Fi callback before the packet copy
and before formatting, so it also returns console bandwidth. Verified on hardware.

**ESP-NOW central/peripheral restored.** All four modes (`esp-now-central`,
`esp-now-peripheral`, `esp-now-fast-collector`, `esp-now-fast-source`) are exposed
again, alongside the HT emitters rather than instead of them — the engine kept both
families, and both are hardware-verified. `--peer-mac` is consequently read per
mode: the emitter's injection destination, or the explicit ESP-NOW peer. `--ht40`
likewise serves the softAP secondary channel or the per-peer HT40 TX PHY, and
`set-rate` applies to the central/peripheral pair rather than being reporting-only.

**Docs** across README, SPECS.md and WEBSERVER.md: the nine-mode vocabulary, a
Console throughput section with measured per-format/per-baud figures, a note that
there is deliberately no payload-truncation option (acquiring less via
`set-csi --htltf=off` measured *faster* than truncating to the same size, because
the radio does less work per frame), that `defmt` is not a throughput lever, and
that the rows at the head of a collection are real CSI from management and foreign
frames rather than dummy packets.

`esp-csi-rs` 0.10 absorbed the engine, so the requirement moves to `"0.10"` — at
`"0.9"` cargo could resolve 0.9.0, which has neither the engine nor
`set_csi_peer_filter`/`set_csi_min_sig_mode` — and the now-dead
`esp-csi-rs-core` patch line is removed.
The engine moved into `esp-csi-rs`, so the lock no longer carries an
`esp-csi-rs-core` entry and pins the facade at 0.10.0 from the patched branch.
…ss-reference

Three things in the unreleased entry were wrong or shouldn't have been there:

- It announced that the ESP-NOW modes were removed. They were not — the engine
  retained them and all four are exposed, alongside the HT emitters rather than
  instead of them. `set-wifi --mode=` takes nine values.
- `--peer-mac` and `--ht40` were described as retargeted to a single referent
  each. With both families supported they are read per mode, and `set-rate` is
  not reporting-only: the central/peripheral pair applies it as the per-peer TX
  PHY.
- It referenced another firmware build that has no bearing on this repository.
  Removed; the emitter modes are simply described on their own terms.

Dependency line updated for `esp-csi-rs 0.10`, which absorbed the engine.
`esp-csi-rs 0.10.0` is published, so the `[patch.crates-io]` redirect to the
`feat/emitter-collector` branch is no longer needed — `esp-csi-rs = "0.10"`
resolves from the registry. Drop the patch section and its now-stale comment,
and re-lock onto the registry source.

A clean clone of this repo now builds the firmware without any local checkout
of the library, which was the point of holding the CLI release back until the
library shipped.

Verified against the registry-resolved crate: all five chip targets build
(esp32, esp32c3, esp32c5, esp32c6, esp32s3), and on two classic ESP32 boards
the 22-check console/filter suite and the 17-check ESP-NOW + emitter suite
both pass in full.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This crate has never been on crates.io and should not be: its build product is
an image flashed to an ESP32, so a registry consumer has nothing to depend on.
It ships as source plus a git tag, and `esp-csi-rs` is the published artifact.

`publish = false` makes that explicit and stops an accidental `cargo publish`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both Xtensa jobs failed, with and without `defmt`, before compiling a single
crate:

    error: rustc 1.93.0-nightly is not supported by the following packages:
      esp-metadata-generated@0.5.0 requires rustc 1.95.0
      esp-rom-sys@0.1.5 requires rustc 1.95.0

That is dependency resolution refusing the toolchain, not a code or `defmt`
problem — which is why the four RISC-V jobs passed: they resolve against a
runner-provided toolchain that is already past the floor.

1.95.0.0 is the version this firmware is built and hardware-tested against
locally, and `cargo metadata` confirms 1.95.0 is the maximum `rust-version` in
the graph on all three targets (riscv32imac, xtensa-esp32, xtensa-esp32s3), so
it clears the floor exactly rather than by luck.

Kept pinned rather than floating so a toolchain release cannot break CI on its
own, with a comment recording that the pin tracks the `esp-hal` / `esp-radio`
bumps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0.10.1 is published. Setting the floor there rather than leaving `"0.10"` is
deliberate: `esp-csi-rs 0.10.0` fails to compile for
`--features esp32,async-print` — `auto` is in its default set, so one added
feature is enough — and `"0.10"` is `^0.10`, which would let cargo resolve 0.10.0
and leave that build broken from a clean checkout.

`^0.10.1` still accepts every later 0.10.x, so this narrows nothing that matters.

Verified: all five chip targets build, and `--features esp32,async-print`, the
combination that failed against 0.10.0, now compiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@VoidReg
VoidReg merged commit 5f6a55c into master Aug 25, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant