use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use tokio::sync::{Mutex, broadcast, mpsc, oneshot, watch};
use tokio_util::sync::CancellationToken;
use crate::models::{DeviceConfig, DeviceInfo, OutputMode};
use crate::profile::{CsiProfile, StandardCsiProfile};
use crate::serial;
#[derive(Debug, Clone)]
pub struct DeviceAttachSpec {
pub id: String,
pub port_path: String,
pub baud_rate: u32,
pub native_usb: bool,
pub mac: Option<String>,
pub recovery_cycles: u32,
pub fault: Option<String>,
}
impl Default for DeviceAttachSpec {
fn default() -> Self {
Self {
id: String::new(),
port_path: String::new(),
baud_rate: 115_200,
native_usb: false,
mac: None,
recovery_cycles: 0,
fault: None,
}
}
}
pub type InfoResponder = oneshot::Sender<Result<DeviceInfo, String>>;
pub struct DeviceHandle {
pub id: String,
pub mac: Option<String>,
pub port_path: String,
pub baud_rate: u32,
pub native_usb: bool,
pub serial_connected: AtomicBool,
pub collection_running: AtomicBool,
pub firmware_verified: AtomicBool,
pub cmd_tx: mpsc::Sender<String>,
pub csi_tx: broadcast::Sender<Vec<u8>>,
pub output_mode_tx: watch::Sender<OutputMode>,
pub session_file_tx: watch::Sender<Option<String>>,
pub info_request_tx: mpsc::Sender<InfoResponder>,
pub config: Mutex<DeviceConfig>,
pub device_info: Mutex<Option<DeviceInfo>>,
pub fault: Mutex<Option<String>>,
pub recovery_cycles: AtomicU32,
pub shutdown: CancellationToken,
pub profile: Arc<dyn CsiProfile>,
}
impl DeviceHandle {
pub fn require_firmware(
&self,
) -> Option<(axum::http::StatusCode, axum::Json<crate::models::ApiResponse>)> {
if self.firmware_verified.load(Ordering::SeqCst) {
None
} else {
Some((
axum::http::StatusCode::PRECONDITION_FAILED,
axum::Json(crate::models::ApiResponse {
success: false,
message:
"Firmware not verified as esp-csi-cli-rs. Call GET .../info to verify, \
or POST .../control/reset to power-cycle and re-check."
.to_string(),
}),
))
}
}
}
pub struct DeviceRegistry {
map: RwLock<HashMap<String, Arc<DeviceHandle>>>,
pub profile: Arc<dyn CsiProfile>,
}
impl Default for DeviceRegistry {
fn default() -> Self {
Self {
map: RwLock::new(HashMap::new()),
profile: Arc::new(StandardCsiProfile),
}
}
}
impl DeviceRegistry {
pub fn with_profile(profile: Arc<dyn CsiProfile>) -> Self {
Self {
map: RwLock::new(HashMap::new()),
profile,
}
}
pub fn get(&self, id: &str) -> Option<Arc<DeviceHandle>> {
self.map.read().unwrap().get(id).cloned()
}
pub fn insert(&self, dev: Arc<DeviceHandle>) -> Option<Arc<DeviceHandle>> {
self.map.write().unwrap().insert(dev.id.clone(), dev)
}
pub fn remove(&self, id: &str) -> Option<Arc<DeviceHandle>> {
self.map.write().unwrap().remove(id)
}
pub fn snapshot(&self) -> Vec<Arc<DeviceHandle>> {
let mut devices: Vec<Arc<DeviceHandle>> = self.map.read().unwrap().values().cloned().collect();
devices.sort_by(|a, b| a.id.cmp(&b.id));
devices
}
pub fn attach(&self, spec: DeviceAttachSpec) -> Arc<DeviceHandle> {
let handle = serial::spawn_device(&spec, self.profile.clone());
self.insert(handle.clone());
handle
}
pub fn detach(&self, id: &str) -> Option<Arc<DeviceHandle>> {
let handle = self.remove(id)?;
handle.shutdown.cancel();
Some(handle)
}
}
#[derive(Clone)]
pub struct AppState {
pub devices: Arc<DeviceRegistry>,
}
impl AppState {
pub fn new() -> Self {
Self {
devices: Arc::new(DeviceRegistry::default()),
}
}
pub fn with_profile(profile: Arc<dyn CsiProfile>) -> Self {
Self {
devices: Arc::new(DeviceRegistry::with_profile(profile)),
}
}
pub fn profile(&self) -> &Arc<dyn CsiProfile> {
&self.devices.profile
}
}
impl Default for AppState {
fn default() -> Self {
Self::new()
}
}