use axum::{Json, http::StatusCode};
use std::sync::atomic::Ordering;
use crate::{
models::{
default_wifi_channel, ApiResponse, CollectionModeConfig, CsiConfig, CsiDeliveryConfig,
DeviceConfig, IoTasksConfig, OutputMode, OutputModeConfig, ProtocolConfig, RateConfig,
TrafficConfig, WifiConfig,
},
routes::Device,
state::DeviceHandle,
};
pub async fn get_config(Device(dev): Device) -> Json<DeviceConfig> {
let config = dev.config.lock().await;
Json(config.clone())
}
pub async fn reset_config(Device(dev): Device) -> (StatusCode, Json<ApiResponse>) {
let result = send_cmd(&dev, "reset-config".to_string()).await;
if result.0 == StatusCode::OK {
let chip = device_chip(&dev).await;
*dev.config.lock().await = DeviceConfig::firmware_defaults_for_chip(chip.as_deref());
}
result
}
pub async fn set_wifi(
Device(dev): Device,
Json(body): Json<WifiConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let chip = device_chip(&dev).await;
let cmd = match body.to_cli_command(chip.as_deref()) {
Ok(c) => c,
Err(message) => return bad_request(message),
};
let result = send_cmd(&dev, cmd).await;
if result.0 == StatusCode::OK {
let mut cfg = dev.config.lock().await;
cfg.wifi.mode = Some(body.mode.clone());
if body.mode == "station" {
if let Some(ch) = body.channel {
cfg.wifi.channel = Some(ch);
}
} else {
cfg.wifi.channel = Some(
body
.channel
.unwrap_or_else(|| default_wifi_channel(chip.as_deref())),
);
}
if body.sta_ssid.is_some() {
cfg.wifi.sta_ssid = body.sta_ssid;
}
if let Some(ap_ssid) = body.ap_ssid {
cfg.wifi.ap_ssid = Some(ap_ssid);
}
if let Some(ap_dhcp) = body.ap_dhcp {
cfg.wifi.ap_dhcp = Some(ap_dhcp);
}
if let Some(ap_leases) = body.ap_leases {
cfg.wifi.ap_leases = Some(ap_leases);
}
if let Some(ap_burst) = body.ap_burst {
cfg.wifi.ap_burst = Some(ap_burst);
}
if let Some(mac) = body.peer_mac {
cfg.wifi.peer_mac = Some(if mac.is_empty() { "auto".to_string() } else { mac });
}
if let Some(ht40) = body.ht40 {
cfg.wifi.ht40 = Some(if ht40 == "off" { "none".to_string() } else { ht40 });
}
}
result
}
pub async fn set_traffic(
Device(dev): Device,
Json(body): Json<TrafficConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let cmd = body.to_cli_command();
let result = send_cmd(&dev, cmd).await;
if result.0 == StatusCode::OK {
let mut config = dev.config.lock().await;
config.collection.traffic_hz = Some(body.frequency_hz);
if body.unsolicited.is_some() {
config.collection.unsolicited = body.unsolicited;
}
}
result
}
pub async fn set_csi(
Device(dev): Device,
Json(body): Json<CsiConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let cmd = match body.to_cli_command(dev.profile.as_ref()) {
Ok(c) => c,
Err(message) => return bad_request(message),
};
let result = send_cmd(&dev, cmd).await;
if result.0 == StatusCode::OK {
body.apply_to_cache(&mut dev.config.lock().await.csi_config, dev.profile.as_ref());
}
result
}
pub async fn set_collection_mode(
Device(dev): Device,
Json(body): Json<CollectionModeConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let cmd = match body.to_cli_command() {
Ok(c) => c,
Err(message) => return bad_request(message),
};
let result = send_cmd(&dev, cmd).await;
if result.0 == StatusCode::OK {
dev.config.lock().await.collection.mode = Some(body.mode);
}
result
}
pub async fn set_output_mode(
Device(dev): Device,
Json(body): Json<OutputModeConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let mode = match body.mode.to_ascii_lowercase().as_str() {
"stream" => OutputMode::Stream,
"dump" => OutputMode::Dump,
"both" => OutputMode::Both,
other => {
return bad_request(format!(
"Unknown output mode '{other}'; expected stream, dump, or both"
));
}
};
let _ = dev.output_mode_tx.send(mode);
(
StatusCode::OK,
Json(ApiResponse {
success: true,
message: format!("Output mode set to {}", body.mode),
}),
)
}
pub async fn set_rate(
Device(dev): Device,
Json(body): Json<RateConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let cmd = body.to_cli_command();
let result = send_cmd(&dev, cmd).await;
if result.0 == StatusCode::OK {
dev.config.lock().await.collection.phy_rate = Some(body.rate);
}
result
}
pub async fn set_protocol(
Device(dev): Device,
Json(body): Json<ProtocolConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let cmd = match body.to_cli_command(dev.profile.as_ref()) {
Ok(c) => c,
Err(message) => return bad_request(message),
};
let result = send_cmd(&dev, cmd).await;
if result.0 == StatusCode::OK {
dev.config.lock().await.collection.protocol = Some(body.protocol.to_ascii_lowercase());
}
result
}
pub async fn set_io_tasks(
Device(dev): Device,
Json(body): Json<IoTasksConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let cmd = match body.to_cli_command() {
Ok(c) => c,
Err(message) => return bad_request(message),
};
let result = send_cmd(&dev, cmd).await;
if result.0 == StatusCode::OK {
let mut cfg = dev.config.lock().await;
if let Some(tx) = body.tx {
cfg.collection.io_tx_enabled = Some(tx);
}
if let Some(rx) = body.rx {
cfg.collection.io_rx_enabled = Some(rx);
}
}
result
}
pub async fn set_csi_delivery(
Device(dev): Device,
Json(body): Json<CsiDeliveryConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let cmd = match body.to_cli_command() {
Ok(c) => c,
Err(message) => return bad_request(message),
};
let result = send_cmd(&dev, cmd).await;
if result.0 == StatusCode::OK {
let mut cfg = dev.config.lock().await;
if let Some(mode) = body.mode {
cfg.csi_delivery_mode = Some(mode);
}
if let Some(logging) = body.logging {
cfg.csi_logging_enabled = Some(logging);
}
}
result
}
async fn device_chip(dev: &DeviceHandle) -> Option<String> {
dev.device_info
.lock()
.await
.as_ref()
.and_then(|info| info.chip.clone())
}
fn bad_request(message: String) -> (StatusCode, Json<ApiResponse>) {
(
StatusCode::BAD_REQUEST,
Json(ApiResponse {
success: false,
message,
}),
)
}
async fn send_cmd(dev: &DeviceHandle, cmd: String) -> (StatusCode, Json<ApiResponse>) {
if !dev.serial_connected.load(Ordering::SeqCst) {
tracing::warn!(
"Rejected command for {} ({}): serial disconnected — never sent: {cmd}",
dev.id,
dev.port_path
);
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse {
success: false,
message: "ESP32 disconnected; serial command unavailable".to_string(),
}),
);
}
if let Some(blocked) = dev.require_firmware() {
tracing::warn!(
"Rejected command for {} ({}): firmware not verified — never sent: {cmd}",
dev.id,
dev.port_path
);
return blocked;
}
match dev.cmd_tx.send(cmd.clone()).await {
Ok(_) => (
StatusCode::OK,
Json(ApiResponse {
success: true,
message: format!("Sent: {cmd}"),
}),
),
Err(e) => {
let (status, message) = if !dev.serial_connected.load(Ordering::SeqCst) {
(
StatusCode::SERVICE_UNAVAILABLE,
"ESP32 disconnected; serial command unavailable".to_string(),
)
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to send command: {e}"),
)
};
(
status,
Json(ApiResponse {
success: false,
message,
}),
)
}
}
}
pub async fn show_stats(Device(dev): Device) -> (StatusCode, Json<ApiResponse>) {
send_cmd(&dev, "show-stats".to_string()).await
}