use crate::serial_core::codecs::test::{export_recording_csv, TestCodec, TestCsvImporter, TestHandler}; use crate::serial_core::error::SerialError; use crate::serial_core::record::CsvImporter; use crate::serial_core::{TestRecording, serial}; use log::info; use serde::Serialize; use std::fs::File; use std::io::Cursor; use std::sync::{Arc, Mutex}; use std::time::{Instant, SystemTime, UNIX_EPOCH}; use tauri::{async_runtime::JoinHandle, AppHandle, Manager, State}; use tokio_serial::{available_ports, SerialPortBuilderExt}; use tokio_util::sync::CancellationToken; type SharedTestRecording = Arc>; #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SerialConnectResponse { pub port: String, pub connected: bool, pub message: String, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SerialExportResponse { pub path: String, pub frame_count: usize, pub message: String, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SerialImportFrame { pub data: Vec, pub dts_ms: u64, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SerialImportResponse { pub file_name: String, pub frame_count: usize, pub channel_count: usize, pub frames: Vec, pub message: String, } struct SerialSession { port: String, cancel: CancellationToken, task: JoinHandle<()>, current_record: SharedTestRecording, } #[derive(Default)] pub struct SerialConnectionState { session: Mutex>, last_record: Mutex> } #[tauri::command] pub fn serial_enum() -> Result, SerialError> { let ports = available_ports() .map_err(|_| SerialError::ScanError)? .into_iter() .map(|info| info.port_name) .collect(); Ok(ports) } #[tauri::command] pub async fn serial_connect( app: AppHandle, port: String, state: State<'_, SerialConnectionState>, ) -> Result { let port_name = port.trim().to_string(); if port_name.is_empty() { return Err(SerialError::InvalidConfig); } { let session = state.session.lock().map_err(|_| SerialError::StateError)?; if session.is_some() { return Err(SerialError::AlreadyConnected); } } let cancel = CancellationToken::new(); let current_record = Arc::new(Mutex::new(TestRecording::new())); let task_record = current_record.clone(); let task_cancel = cancel.clone(); let task_app = app.clone(); let task_port_name = port_name.clone(); let port = tokio_serial::new(&port_name, 115200) .open_native_async() .map_err(|_| SerialError::OpenError)?; let session_started_at = Instant::now(); let task = tauri::async_runtime::spawn(async move { let codec = TestCodec::new(); let handler = TestHandler; if let Err(error) = serial::run_serial( task_app.clone(), port, codec, handler, session_started_at, task_record.clone(), task_cancel, ) .await { eprintln!("serial task exited with error: {error}"); } let manager = task_app.state::(); if let Ok(mut last_record) = manager.last_record.lock() { *last_record = Some(task_record); } let mut session = match manager.session.lock() { Ok(session) => session, Err(_) => return, }; { let should_clear = session .as_ref() .map(|current| current.port.as_str() == task_port_name.as_str()) .unwrap_or(false); if should_clear { session.take(); } } }); let mut session = state.session.lock().map_err(|_| SerialError::StateError)?; if session.is_some() { cancel.cancel(); task.abort(); return Err(SerialError::AlreadyConnected); } *session = Some(SerialSession { port: port_name.clone(), cancel, task, current_record }); Ok(SerialConnectResponse { port: port_name, connected: true, message: "connected".to_string(), }) } #[tauri::command] pub async fn serial_disconnect( state: State<'_, SerialConnectionState>, ) -> Result { let session = { let mut guard = state.session.lock().map_err(|_| SerialError::StateError)?; guard.take() }; let Some(SerialSession { port, cancel, task, current_record, }) = session else { return Ok(SerialConnectResponse { port: String::new(), connected: false, message: "already disconnected".to_string(), }); }; cancel.cancel(); let _ = task.await; let frame_count = current_record.lock().map(|record| { record.frames.len() }).unwrap_or(0); info!("last_record has {} frames", frame_count); if let Ok(mut last_record) = state.last_record.lock() { *last_record = Some(current_record); } Ok(SerialConnectResponse { port, connected: false, message: "disconnected".to_string(), }) } #[tauri::command] pub fn serial_export_csv( app: AppHandle, state: State<'_, SerialConnectionState>, ) -> Result { let current_record = { let session = state.session.lock().map_err(|_| SerialError::StateError)?; session .as_ref() .map(|current_session| current_session.current_record.clone()) }; let record = if let Some(recording) = current_record { recording } else { let last_record = state.last_record.lock().map_err(|_| SerialError::StateError)?; last_record.clone().ok_or(SerialError::NoRecordedData)? }; let mut output_dir = match app.path().desktop_dir() { Ok(path) => path, Err(_) => std::env::current_dir().map_err(|_| SerialError::ExportError)?, }; let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_millis()) .unwrap_or_default(); output_dir.push(format!("joyson_export_{timestamp}.csv")); let mut file = File::create(&output_dir).map_err(|_| SerialError::ExportError)?; let frame_count = { let recording = record.lock().map_err(|_| SerialError::StateError)?; if recording.frames.is_empty() { return Err(SerialError::NoRecordedData); } export_recording_csv(&recording, &mut file).map_err(|_| SerialError::ExportError)?; recording.frames.len() }; let path = output_dir.display().to_string(); info!("csv exported to {path}, frame_count={frame_count}"); Ok(SerialExportResponse { path, frame_count, message: "exported".to_string(), }) } #[tauri::command] pub fn serial_import_csv(file_name: String, csv_content: String) -> Result { let mut importer = TestCsvImporter::new(file_name.as_str()); let packets = importer .load(Cursor::new(csv_content.into_bytes())) .map_err(|_| SerialError::ImportError)?; if packets.is_empty() { return Err(SerialError::NoRecordedData); } let channel_count = packets.first().map(|item| item.data.len()).unwrap_or(0); let frame_count = packets.len(); let frames = packets .into_iter() .map(|packet| SerialImportFrame { data: packet.data, dts_ms: packet.dts_ms, }) .collect(); Ok(SerialImportResponse { file_name, frame_count, channel_count, frames, message: "imported".to_string(), }) }