use std::{ collections::HashMap, io::{Read, Write}, sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, }, thread, time::Duration, }; use serialport::{DataBits, FlowControl, Parity, SerialPort, SerialPortType, StopBits}; use tauri::{AppHandle, Emitter}; use crate::domain::serial::{SerialConfig, SerialDataEvent, SerialErrorEvent, SerialPortSummary}; struct SerialSession { writer: Box, stop: Arc, } #[derive(Default)] pub struct SerialService { sessions: Mutex>, } impl SerialService { pub fn list_ports(&self) -> Result, String> { let mut ports = serialport::available_ports() .map_err(|error| format!("failed to enumerate serial ports: {error}"))? .into_iter() .map(|port| SerialPortSummary { path: port.port_name, port_type: describe_port_type(port.port_type), }) .collect::>(); ports.sort_by(|left, right| left.path.cmp(&right.path)); Ok(ports) } pub fn open( &self, app: AppHandle, session_id: String, config: SerialConfig, ) -> Result<(), String> { let data_bits = match config.data_bits { 5 => DataBits::Five, 6 => DataBits::Six, 7 => DataBits::Seven, 8 => DataBits::Eight, value => return Err(format!("unsupported data bits: {value}")), }; let stop_bits = match config.stop_bits { 1 => StopBits::One, 2 => StopBits::Two, value => return Err(format!("unsupported stop bits: {value}")), }; let parity = match config.parity.to_ascii_lowercase().as_str() { "none" => Parity::None, "odd" => Parity::Odd, "even" => Parity::Even, value => return Err(format!("unsupported parity: {value}")), }; let flow_control = match config.flow_control.to_ascii_lowercase().as_str() { "none" => FlowControl::None, "software" => FlowControl::Software, "hardware" => FlowControl::Hardware, value => return Err(format!("unsupported flow control: {value}")), }; let port = serialport::new(&config.path, config.baud_rate) .data_bits(data_bits) .stop_bits(stop_bits) .parity(parity) .flow_control(flow_control) .timeout(Duration::from_millis(100)) .open() .map_err(|error| format!("failed to open {}: {error}", config.path))?; let mut reader = port .try_clone() .map_err(|error| format!("failed to clone serial port: {error}"))?; let stop = Arc::new(AtomicBool::new(false)); let reader_stop = Arc::clone(&stop); let reader_session_id = session_id.clone(); self.sessions .lock() .map_err(|_| "serial session lock poisoned".to_string())? .insert(session_id, SerialSession { writer: port, stop }); thread::spawn(move || { let mut buffer = [0_u8; 4096]; while !reader_stop.load(Ordering::Relaxed) { match reader.read(&mut buffer) { Ok(0) => {} Ok(length) => { let _ = app.emit( "serial:data", SerialDataEvent { session_id: reader_session_id.clone(), data: buffer[..length].to_vec(), }, ); } Err(error) if error.kind() == std::io::ErrorKind::TimedOut => {} Err(error) => { let _ = app.emit( "serial:error", SerialErrorEvent { session_id: reader_session_id.clone(), message: error.to_string(), }, ); break; } } } }); Ok(()) } pub fn write(&self, session_id: &str, data: &[u8]) -> Result<(), String> { let mut sessions = self .sessions .lock() .map_err(|_| "serial session lock poisoned".to_string())?; let session = sessions .get_mut(session_id) .ok_or_else(|| format!("serial session not found: {session_id}"))?; session .writer .write_all(data) .map_err(|error| format!("serial write failed: {error}"))?; session .writer .flush() .map_err(|error| format!("serial flush failed: {error}")) } pub fn close(&self, session_id: &str) -> Result<(), String> { if let Some(session) = self .sessions .lock() .map_err(|_| "serial session lock poisoned".to_string())? .remove(session_id) { session.stop.store(true, Ordering::Relaxed); } Ok(()) } } fn describe_port_type(port_type: SerialPortType) -> String { match port_type { SerialPortType::UsbPort(info) => match info.product { Some(product) => format!("USB · {product}"), None => format!("USB · {:04x}:{:04x}", info.vid, info.pid), }, SerialPortType::BluetoothPort => "Bluetooth".to_string(), SerialPortType::PciPort => "PCI".to_string(), SerialPortType::Unknown => "Serial".to_string(), } } #[cfg(test)] mod tests { use super::*; #[test] fn enumerated_ports_are_sorted_and_have_labels() { let ports = SerialService::default() .list_ports() .expect("serial port enumeration should not fail"); assert!(ports.windows(2).all(|pair| pair[0].path <= pair[1].path)); assert!(ports.iter().all(|port| !port.port_type.is_empty())); } #[test] fn describes_bluetooth_ports() { assert_eq!( describe_port_type(SerialPortType::BluetoothPort), "Bluetooth" ); } }