use axum::{
Json,
extract::{State, rejection::JsonRejection},
http::StatusCode,
};
use std::sync::atomic::Ordering;
use crate::{
models::{
ApiResponse, CollectionModeConfig, CsiConfig, CsiDeliveryConfig, DeviceConfig,
IoTasksConfig, LogModeConfig, OutputMode, OutputModeConfig, RateConfig, TrafficConfig,
WifiConfig,
},
state::AppState,
};
pub async fn get_config(State(state): State<AppState>) -> Json<DeviceConfig> {
let config = state.config.lock().await;
Json(config.clone())
}
pub async fn reset_config(State(state): State<AppState>) -> (StatusCode, Json<ApiResponse>) {
let result = send_cmd(&state, "reset-config".to_string()).await;
if result.0 == StatusCode::OK {
*state.config.lock().await = DeviceConfig::firmware_defaults();
}
result
}
pub async fn set_wifi(
State(state): State<AppState>,
Json(body): Json<WifiConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let cmd = match body.to_cli_command() {
Ok(c) => c,
Err(message) => return bad_request(message),
};
let result = send_cmd(&state, cmd).await;
if result.0 == StatusCode::OK {
let mut cfg = state.config.lock().await;
cfg.wifi.mode = Some(body.mode);
if body.channel.is_some() {
cfg.wifi.channel = body.channel;
}
if body.sta_ssid.is_some() {
cfg.wifi.sta_ssid = body.sta_ssid;
}
}
result
}
pub async fn set_traffic(
State(state): State<AppState>,
Json(body): Json<TrafficConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let cmd = body.to_cli_command();
let result = send_cmd(&state, cmd).await;
if result.0 == StatusCode::OK {
state.config.lock().await.collection.traffic_hz = Some(body.frequency_hz);
}
result
}
pub async fn set_csi(
State(state): State<AppState>,
Json(body): Json<CsiConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let result = send_cmd(&state, body.to_cli_command()).await;
if result.0 == StatusCode::OK {
let mut cfg = state.config.lock().await;
if body.disable_lltf == Some(true) {
cfg.csi_config.lltf_enabled = Some(false);
}
if body.disable_htltf == Some(true) {
cfg.csi_config.htltf_enabled = Some(false);
}
if body.disable_stbc_htltf == Some(true) {
cfg.csi_config.stbc_htltf_enabled = Some(false);
}
if body.disable_ltf_merge == Some(true) {
cfg.csi_config.ltf_merge_enabled = Some(false);
}
if body.disable_csi == Some(true) {
cfg.csi_config.acquire_csi = Some(0);
}
if body.disable_csi_legacy == Some(true) {
cfg.csi_config.acquire_csi_legacy = Some(0);
}
if body.disable_csi_ht20 == Some(true) {
cfg.csi_config.acquire_csi_ht20 = Some(0);
}
if body.disable_csi_ht40 == Some(true) {
cfg.csi_config.acquire_csi_ht40 = Some(0);
}
if body.disable_csi_su == Some(true) {
cfg.csi_config.acquire_csi_su = Some(0);
}
if body.disable_csi_mu == Some(true) {
cfg.csi_config.acquire_csi_mu = Some(0);
}
if body.disable_csi_dcm == Some(true) {
cfg.csi_config.acquire_csi_dcm = Some(0);
}
if body.disable_csi_beamformed == Some(true) {
cfg.csi_config.acquire_csi_beamformed = Some(0);
}
if let Some(stbc) = body.csi_he_stbc {
cfg.csi_config.csi_he_stbc = Some(stbc);
}
if let Some(scale) = body.val_scale_cfg {
cfg.csi_config.val_scale_cfg = Some(scale);
}
}
result
}
pub async fn set_collection_mode(
State(state): State<AppState>,
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(&state, cmd).await;
if result.0 == StatusCode::OK {
state.config.lock().await.collection.mode = Some(body.mode);
}
result
}
pub async fn set_log_mode(
State(state): State<AppState>,
body: Result<Json<LogModeConfig>, JsonRejection>,
) -> (StatusCode, Json<ApiResponse>) {
let body = match body {
Ok(Json(body)) => body,
Err(_) => {
return bad_request(
"Invalid log mode. Use one of: text, array-list, serialized, esp-csi-tool"
.to_string(),
);
}
};
let cmd = body.to_cli_command();
let result = send_cmd(&state, cmd).await;
if result.0 == StatusCode::OK {
let mut cfg = state.config.lock().await;
cfg.log_mode = Some(body.mode.as_cli_value().to_string());
let _ = state.log_mode_tx.send(body.mode);
}
result
}
pub async fn set_output_mode(
State(state): State<AppState>,
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 _ = state.output_mode_tx.send(mode);
(
StatusCode::OK,
Json(ApiResponse {
success: true,
message: format!("Output mode set to {}", body.mode),
}),
)
}
pub async fn set_rate(
State(state): State<AppState>,
Json(body): Json<RateConfig>,
) -> (StatusCode, Json<ApiResponse>) {
let cmd = body.to_cli_command();
let result = send_cmd(&state, cmd).await;
if result.0 == StatusCode::OK {
state.config.lock().await.collection.phy_rate = Some(body.rate);
}
result
}
pub async fn set_io_tasks(
State(state): State<AppState>,
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(&state, cmd).await;
if result.0 == StatusCode::OK {
let mut cfg = state.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(
State(state): State<AppState>,
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(&state, cmd).await;
if result.0 == StatusCode::OK {
let mut cfg = state.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
}
fn bad_request(message: String) -> (StatusCode, Json<ApiResponse>) {
(
StatusCode::BAD_REQUEST,
Json(ApiResponse {
success: false,
message,
}),
)
}
async fn send_cmd(state: &AppState, cmd: String) -> (StatusCode, Json<ApiResponse>) {
if !state.serial_connected.load(Ordering::SeqCst) {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse {
success: false,
message: "ESP32 disconnected; serial command unavailable".to_string(),
}),
);
}
if let Some(blocked) = state.require_firmware() {
return blocked;
}
match state.cmd_tx.send(cmd.clone()).await {
Ok(_) => (
StatusCode::OK,
Json(ApiResponse {
success: true,
message: format!("Sent: {cmd}"),
}),
),
Err(e) => {
let (status, message) = if !state.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(State(state): State<AppState>) -> (StatusCode, Json<ApiResponse>) {
send_cmd(&state, "show-stats".to_string()).await
}