feat: release OpsXShell 2.1.0 native Tauri edition
This commit is contained in:
1 parent
e9a0d21758
commit
a3dba0c6ed
311 files changed
+31748
-14897
No files matched your search
@@ -0,0 +1,188 @@
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user