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-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

ExportPurpose
AppState, DeviceRegistry, DeviceHandle, DeviceAttachSpecShared runtime state
ServerConfig, build_router, serveHTTP server
SupervisorConfig, run_supervisor, detect_esp_ports, probe_portHotplug discovery
modelsJSON request/response types and CLI command mappers
csiCOBS + postcard frame decoder
routesAxum handlers, for custom routers
serial, parquet_sinkThe lower-level pipelines
CsiProfile, StandardCsiProfileThe 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.