189 lines
6.2 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|