Files
opsxshell/src-tauri/src/services/serial.rs
T

189 lines
6.2 KiB
Rust

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<dyn SerialPort>,
stop: Arc<AtomicBool>,
}
#[derive(Default)]
pub struct SerialService {
sessions: Mutex<HashMap<String, SerialSession>>,
}
impl SerialService {
pub fn list_ports(&self) -> Result<Vec<SerialPortSummary>, 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::<Vec<_>>();
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"
);
}
}