mod models;
mod routes;
mod serial;
mod state;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use axum::{
Router,
routing::{get, post},
};
use clap::Parser;
use tokio::sync::{Mutex, broadcast, mpsc, watch};
use models::{DeviceConfig, LogMode, OutputMode};
use state::AppState;
#[derive(Parser, Debug)]
#[command(
version,
about = "CSI WebServer — streams ESP32 CSI data over WebSocket"
)]
struct Cli {
#[arg(long, default_value = "0.0.0.0")]
interface: String,
#[arg(long, default_value_t = 3000)]
port: u16,
#[arg(long, env = "CSI_BAUD_RATE", default_value_t = 115_200)]
baud_rate: u32,
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "csi_webserver=debug".into()),
)
.init();
let port_path = match serial::detect_esp_port() {
Ok(p) => p,
Err(e) => {
tracing::error!("{e}");
std::process::exit(1);
}
};
let (cmd_tx, cmd_rx) = mpsc::channel::<String>(64);
let (csi_tx, _) = broadcast::channel::<Vec<u8>>(256);
let (log_mode_tx, log_mode_rx) = watch::channel(LogMode::default());
let (output_mode_tx, output_mode_rx) = watch::channel(OutputMode::default());
let (session_file_tx, session_file_rx) = watch::channel::<Option<String>>(None);
let (info_request_tx, info_request_rx) = mpsc::channel::<state::InfoResponder>(4);
let state = AppState {
port_path: Arc::new(Mutex::new(port_path.clone())),
baud_rate: cli.baud_rate,
serial_connected: Arc::new(AtomicBool::new(false)),
collection_running: Arc::new(AtomicBool::new(false)),
cmd_tx,
csi_tx: csi_tx.clone(),
log_mode_tx: Arc::new(log_mode_tx),
output_mode_tx: Arc::new(output_mode_tx),
session_file_tx: Arc::new(session_file_tx),
config: Arc::new(Mutex::new(DeviceConfig::default())),
info_request_tx,
firmware_verified: Arc::new(AtomicBool::new(false)),
device_info: Arc::new(Mutex::new(None)),
};
tokio::spawn(serial::run_serial_task(
port_path,
cli.baud_rate,
cmd_rx,
csi_tx,
log_mode_rx,
output_mode_rx,
session_file_rx,
info_request_rx,
state.serial_connected.clone(),
state.collection_running.clone(),
state.firmware_verified.clone(),
state.device_info.clone(),
state.port_path.clone(),
));
let app = Router::new()
.route("/", get(|| async { "CSI Server Active" }))
.route("/api/config", get(routes::config::get_config))
.route("/api/config/reset", post(routes::config::reset_config))
.route("/api/config/wifi", post(routes::config::set_wifi))
.route("/api/config/traffic", post(routes::config::set_traffic))
.route("/api/config/csi", post(routes::config::set_csi))
.route(
"/api/config/collection-mode",
post(routes::config::set_collection_mode),
)
.route("/api/config/log-mode", post(routes::config::set_log_mode))
.route(
"/api/config/output-mode",
post(routes::config::set_output_mode),
)
.route("/api/config/rate", post(routes::config::set_rate))
.route("/api/config/io-tasks", post(routes::config::set_io_tasks))
.route(
"/api/config/csi-delivery",
post(routes::config::set_csi_delivery),
)
.route(
"/api/control/start",
post(routes::control::start_collection),
)
.route(
"/api/control/stop",
post(routes::control::stop_collection),
)
.route(
"/api/control/status",
get(routes::control::get_collection_status),
)
.route("/api/control/reset", post(routes::control::reset_esp32))
.route("/api/control/stats", post(routes::config::show_stats))
.route("/api/info", get(routes::info::get_info))
.route("/api/ws", get(routes::ws::ws_handler))
.with_state(state);
let addr = format!("{}:{}", cli.interface, cli.port);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
tracing::info!("CSI server listening on http://{addr}");
axum::serve(listener, app).await.unwrap();
}