use axum::{Json, extract::State, http::StatusCode};
use chrono::Local;
use std::sync::atomic::Ordering;
use tokio::time::{Duration, sleep};
use tokio_serial::{SerialPort, SerialPortBuilderExt};
use crate::{
models::{ApiResponse, CollectionStatusResponse, OutputMode, StartConfig},
state::AppState,
};
pub async fn get_collection_status(
State(state): State<AppState>,
) -> (StatusCode, Json<CollectionStatusResponse>) {
let port_path = state.port_path.lock().await.clone();
(
StatusCode::OK,
Json(CollectionStatusResponse::from_state(
&state.serial_connected,
&state.collection_running,
port_path,
)),
)
}
pub async fn reset_esp32(State(state): State<AppState>) -> (StatusCode, Json<ApiResponse>) {
state.collection_running.store(false, Ordering::SeqCst);
let _ = state.session_file_tx.send(None);
if !state.serial_connected.load(Ordering::SeqCst) {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse {
success: false,
message: "ESP32 disconnected; serial command unavailable".to_string(),
}),
);
}
let baud: u32 = std::env::var("CSI_BAUD_RATE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(115_200);
let current_port = state.port_path.lock().await.clone();
let mut port = match tokio_serial::new(current_port.as_str(), baud).open_native_async() {
Ok(p) => p,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse {
success: false,
message: format!("Failed to open serial port for reset: {e}"),
}),
);
}
};
#[cfg(unix)]
{
let _ = port.set_exclusive(false);
}
let _ = port.write_data_terminal_ready(false);
if let Err(e) = port.write_request_to_send(true) {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse {
success: false,
message: format!("RTS assert failed (adapter may not support it): {e}"),
}),
);
}
sleep(Duration::from_millis(100)).await;
let _ = port.write_request_to_send(false);
drop(port);
tracing::info!("ESP32 reset via RTS on {}", current_port);
(
StatusCode::OK,
Json(ApiResponse {
success: true,
message: "ESP32 reset triggered via RTS".to_string(),
}),
)
}
pub async fn start_collection(
State(state): State<AppState>,
body: Option<Json<StartConfig>>,
) -> (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 state
.collection_running
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiResponse {
success: false,
message: "Collection already running".to_string(),
}),
);
}
let cmd = body
.map(|Json(b)| b.to_cli_command())
.unwrap_or_else(|| "start".to_string());
match state.cmd_tx.send(cmd.clone()).await {
Ok(_) => {
let path = format!("csi_dump_{}.bin", Local::now().format("%Y%m%d_%H%M%S"));
let current_mode = state.output_mode_tx.borrow().clone();
if matches!(current_mode, OutputMode::Dump | OutputMode::Both) {
tracing::info!("New session dump file: {path}");
}
let _ = state.session_file_tx.send(Some(path));
(
StatusCode::OK,
Json(ApiResponse {
success: true,
message: format!("Collection started: {cmd}"),
}),
)
}
Err(e) => {
state.collection_running.store(false, Ordering::SeqCst);
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 start collection: {e}"),
)
};
(
status,
Json(ApiResponse {
success: false,
message,
}),
)
}
}
}