csi-webserver
csi-webserver is the host-side bridge. It discovers ESP32 boards over USB,
takes the CSI stream arriving from each one over serial, and serves it to
clients over HTTP and WebSocket — so the board does not have to speak HTTP and
the client does not have to speak serial.
It is the piece that makes a capture watchable from another machine: the device session stays where the hardware is, and anything with a network connection can attach. It can also write each session straight to Apache Parquet, which is usually the shortest path from a board to a dataframe.
This is Tier 2 — it runs on your computer, not on a board. The published crate is at 0.2.1 at the time of writing.
Naming. The repository is
csi-webserver-rs, but the published crate drops the suffix and is simplycsi-webserver. Earlier drafts of this book called itesp-csi-webserver-rs; that name was never published. Theesp-prefix marks the crates that run on an Espressif part, and this one runs on your computer.
Prerequisites
The board must already be running esp-csi-cli-rs.
The server drives the device by issuing CLI commands over the serial port, so a
board with custom esp-csi-rs firmware will be discovered but refused.
Install and Run
cargo install csi-webserver
csi-webserver --help
Or from a source checkout:
cargo run -p csi-webserver
cargo run -p csi-webserver -- --interface 127.0.0.1 --port 3000 --baud-rate 921600
cargo run -p csi-webserver -- --device lab1=/dev/ttyUSB0 --scan-interval-ms 1000
| Option | Default | Purpose |
|---|---|---|
--interface <ADDR> | 0.0.0.0 | Bind address |
--port <PORT> | 3000 | TCP port |
--baud-rate <RATE> | 115200 | Serial baud (env CSI_BAUD_RATE) |
--device <ALIAS=PORT_OR_MAC> | — | Stable device id override, repeatable |
--scan-interval-ms <MS> | 2000 | Hotplug rescan interval |
CSI_SERIAL_PORT pins a single port instead of auto-detecting, and RUST_LOG
sets the tracing filter (default csi_webserver_core=debug).
A first capture, end to end:
csi-webserver
curl -sS "http://127.0.0.1:3000/api/devices"
curl -sS "http://127.0.0.1:3000/api/devices/<id>/info"
curl -sS -X POST "http://127.0.0.1:3000/api/devices/<id>/control/start"
The stream itself is at ws://127.0.0.1:3000/api/devices/<id>/ws.
Devices Are Addressed Individually
The server handles multiple boards at once. A hotplug supervisor scans for attached devices, assigns each a stable id, spawns a dedicated serial worker, and tears it down on unplug after a short debounce. It starts and serves happily with no device attached at all.
Every per-device endpoint lives under /api/devices/{id}/.... The id defaults
to the sanitised port basename — /dev/ttyUSB0 becomes ttyUSB0 — and
--device lab1=/dev/ttyUSB0 pins a friendlier one. State is entirely per
device: connection, firmware verification, collection status, config cache, and
the CSI stream are independent, and each device’s WebSocket carries only its own
frames.
The Firmware Gate
Before the server dispatches any command to a device, the firmware must be
verified as esp-csi-cli-rs. On every successful serial connect the server runs
an internal info exchange and looks for the ESP-CSI-CLI/<version> magic
prefix and the END-INFO sentinel — the contract described in
CSI Data Formats.
While a device is unverified, command endpoints return
412 Precondition Failed. A handful of endpoints are always reachable,
because they are how you recover: GET /, GET /api/devices,
GET /api/devices/{id}/info, GET /api/devices/{id}/config,
GET /api/devices/{id}/control/status, and
POST /api/devices/{id}/control/reset.
The reset path differs by adapter, which is worth knowing when a board seems
stuck. On UART adapters (CP210x, CH340) the server pulses RTS, waits for the
chip to boot, and re-runs the info exchange synchronously, so the HTTP
response tells you whether re-verification succeeded. On native USB-Serial-JTAG
boards, pulsing RTS would re-enumerate and wedge the port, so the server sends
the firmware’s own restart command and returns immediately — poll
GET /api/devices to see the board come back verified.
Endpoints
Command endpoints return { "success": true, "message": "..." }. Validation
errors are 400, a disconnected device is 503, and an unverified one is
412.
| Method | Path | Purpose |
|---|---|---|
| GET | / | Health |
| GET | /api/devices | List attached devices and their status |
| GET | /api/devices/{id}/info | Firmware identification; refreshes cache |
| GET | /api/devices/{id}/config | Read the cached configuration |
| POST | /api/devices/{id}/config/reset | Restore firmware defaults |
| POST | /api/devices/{id}/config/wifi | Mode, credentials, channel, peer, emitter |
| POST | /api/devices/{id}/config/traffic | Traffic generator frequency |
| POST | /api/devices/{id}/config/csi | CSI acquisition flags |
| POST | /api/devices/{id}/config/csi-output | Master delivery gate |
| POST | /api/devices/{id}/config/output-mode | stream, dump, or both |
| POST | /api/devices/{id}/config/rate | PHY rate |
| POST | /api/devices/{id}/config/protocol | Wi-Fi PHY protocol |
| POST | /api/devices/{id}/config/io-tasks | Toggle TX / RX tasks |
| POST | /api/devices/{id}/config/csi-delivery | Delivery mode and inline log gate |
| GET | /api/devices/{id}/control/status | Runtime status |
| POST | /api/devices/{id}/control/start | Begin collection |
| POST | /api/devices/{id}/control/stop | End collection |
| POST | /api/devices/{id}/control/reset | Reset the chip and re-verify |
| POST | /api/devices/{id}/control/stats | Runtime counter snapshot |
| GET | /api/devices/{id}/ws | Per-device CSI WebSocket stream |
Two changes will trip up anything written against an older server.
config/csi-output replaces the former config/collection-mode, for the
naming reason given in Crates & Libraries.
And config/log-mode has been removed outright: the server always runs
devices in the serialized format, because that is the only one it decodes.
Two scope notes are worth carrying into any integration work.
First, config/wifi names five modes — station, sniffer, wifi-ap,
ht20-emitter, and ht40-emitter. The firmware still supports the four ESP-NOW
modes, but the server does not name them; they reach a device either through an
embedder’s CsiProfile::extra_wifi_modes (see
csi-webserver-core) or over the serial console
directly.
Second, config/csi-output is documented ahead of its implementation. The
firmware-side contract — set-csi-output --enabled=<true|false>, defaulting to
true — is settled, but the matching handler had not landed in
csi-webserver-core at the time of writing. Treat the HTTP path as
provisional; the command and the body are not.
Output Modes and Parquet Dumps
POST /api/devices/{id}/config/output-mode selects what happens to a session’s
frames:
| Mode | WebSocket | Parquet dump |
|---|---|---|
stream (default) | yes | no |
dump | no | yes |
both | yes | yes |
Dumps are one Apache Parquet file per session, named with the device id so
concurrent devices never collide — csi_dump_ttyUSB0_20260621_120000.parquet.
The server decodes the device’s serialized frames into typed columns, so the
file opens directly in pandas, polars, pyarrow, or DuckDB with no format
knowledge required:
import pyarrow.parquet as pq
t = pq.read_table("csi_dump_ttyUSB0_20260621_120000.parquet")
print(t.schema)
One superset schema covers every chip. Columns that exist only on some parts
are nullable and left null elsewhere, so the classic 802.11n metadata
(sig_mode, mcs, bandwidth, stbc, …) is null on C5/C6 rows, and the C5/C6
driver fields (dump_len, cur_bb_format, the rxmatch flags, …) are null on
ESP32-family rows. Check the chip column to know which apply.
Two timestamps are recorded per row and they mean different things:
host_rx_time is the server’s wall clock, and timestamp is the
device’s microseconds-since-boot counter. Correlating across devices means
using host_rx_time.
Stop cleanly. The Parquet footer is written when a session ends — on
stop, an output-mode switch back tostream, a device disconnect, or server shutdown. A hard crash or power loss leaves the file without a footer and any unflushed rows lost, and such a file will not open at all.
Relevant Links & Resources
- Crate:
csi-webserveron crates.io - Source: csi-rs/csi-webserver-rs
- The complete endpoint reference, with payloads and status codes, is
API.mdin the repository.
The repository also carries
specs/SPECS.MD, which predates multi-device support and documents single-device routes such as/api/config/...without the/devices/{id}segment.API.mdis the current reference; treat the spec as historical.