Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 simply csi-webserver. Earlier drafts of this book called it esp-csi-webserver-rs; that name was never published. The esp- 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
OptionDefaultPurpose
--interface <ADDR>0.0.0.0Bind address
--port <PORT>3000TCP port
--baud-rate <RATE>115200Serial baud (env CSI_BAUD_RATE)
--device <ALIAS=PORT_OR_MAC>Stable device id override, repeatable
--scan-interval-ms <MS>2000Hotplug 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.

MethodPathPurpose
GET/Health
GET/api/devicesList attached devices and their status
GET/api/devices/{id}/infoFirmware identification; refreshes cache
GET/api/devices/{id}/configRead the cached configuration
POST/api/devices/{id}/config/resetRestore firmware defaults
POST/api/devices/{id}/config/wifiMode, credentials, channel, peer, emitter
POST/api/devices/{id}/config/trafficTraffic generator frequency
POST/api/devices/{id}/config/csiCSI acquisition flags
POST/api/devices/{id}/config/csi-outputMaster delivery gate
POST/api/devices/{id}/config/output-modestream, dump, or both
POST/api/devices/{id}/config/ratePHY rate
POST/api/devices/{id}/config/protocolWi-Fi PHY protocol
POST/api/devices/{id}/config/io-tasksToggle TX / RX tasks
POST/api/devices/{id}/config/csi-deliveryDelivery mode and inline log gate
GET/api/devices/{id}/control/statusRuntime status
POST/api/devices/{id}/control/startBegin collection
POST/api/devices/{id}/control/stopEnd collection
POST/api/devices/{id}/control/resetReset the chip and re-verify
POST/api/devices/{id}/control/statsRuntime counter snapshot
GET/api/devices/{id}/wsPer-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:

ModeWebSocketParquet dump
stream (default)yesno
dumpnoyes
bothyesyes

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 to stream, 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.

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.md is the current reference; treat the spec as historical.