csi-webserver-core
csi-webserver-core is the library that
csi-webserver is built from. The executable is a
thin wrapper: it parses command-line flags and calls into this crate. Everything
described in the previous section — device discovery, the firmware gate, the
HTTP routes, the WebSocket stream, the Parquet sink — lives here.
Reach for it when you want the CSI server running inside your own process rather than beside it: to register devices programmatically, to mount the API under your own routes, or to build a host application that does something more than bridge.
The published crate is at 0.1.1 at the time of writing, with the repository ahead at 0.2.0.
Embedding the Server
[dependencies]
csi-webserver-core = "0.1"
tokio = { version = "1", features = ["full"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
The default arrangement is three calls: build the shared state, spawn the hotplug supervisor against it, and serve.
use std::time::Duration;
use csi_webserver_core::{
AppState, ServerConfig, SupervisorConfig, run_supervisor, serve,
};
#[tokio::main]
async fn main() -> std::io::Result<()> {
tracing_subscriber::fmt::init();
let state = AppState::new();
tokio::spawn(run_supervisor(SupervisorConfig {
registry: state.devices.clone(),
baud_rate: 115_200,
scan_interval: Duration::from_secs(2),
aliases: vec![],
}));
serve(ServerConfig { bind: "0.0.0.0:3000".into() }, state).await
}
Registering a Device Directly
Hotplug discovery is convenient but not mandatory. If you already know which port a board is on — a fixed lab rig, a container with a device passed through, a test harness — attach it explicitly and skip the supervisor entirely:
use csi_webserver_core::{AppState, DeviceAttachSpec};
let state = AppState::new();
state.devices.attach(DeviceAttachSpec {
id: "lab1".into(),
port_path: "/dev/ttyUSB0".into(),
baud_rate: 115_200,
native_usb: false,
mac: None,
..Default::default()
});
native_usb matters more than it looks: it decides which reset strategy the
firmware gate uses. supervisor::probe_port reads it, along with the MAC, from
USB enumeration, so the honest way to fill the struct is to probe first rather
than to guess.
Mounting It Inside Your Own Router
build_router hands back a plain Axum Router, which you can serve as-is or
nest under your own paths alongside whatever else your application does:
use axum::Router;
use csi_webserver_core::{build_router, AppState};
let state = AppState::new();
let app: Router = build_router(state);
The individual handler functions are exported through the routes module for
cases where you want to compose routes yourself rather than take the set
wholesale.
Public API Surface
| Export | Purpose |
|---|---|
AppState, DeviceRegistry, DeviceHandle, DeviceAttachSpec | Shared runtime state |
ServerConfig, build_router, serve | HTTP server |
SupervisorConfig, run_supervisor, detect_esp_ports, probe_port | Hotplug discovery |
models | JSON request/response types and CLI command mappers |
csi | COBS + postcard frame decoder |
routes | Axum handlers, for custom routers |
serial, parquet_sink | The lower-level pipelines |
CsiProfile, StandardCsiProfile | The extension seam, below |
The Extension Seam
The crate’s node-mode table names five modes — station, sniffer, wifi-ap,
ht20-emitter, and ht40-emitter — mirroring that subset of the firmware’s
set-wifi --mode= grammar.
Any mode this crate does not name can be supplied by an embedder through
CsiProfile::extra_wifi_modes. Its mode-specific flags ride through a flattened
extra map on the request body and are re-emitted verbatim as --{key}={value}
to the firmware, so the crate forwards modes it knows nothing about. The effect
is that the core stays honest — it never names a mode it does not itself
implement — and an embedder can extend the mode set without forking the server.
Relevant Links & Resources
- Crate:
csi-webserver-coreon crates.io - Documentation: docs.rs/csi-webserver-core
- Source: csi-rs/csi-webserver-core-rs