use crate::error::Error;
use crate::state::{
ClearBuffer, ConnectedPort, DataBits, FlowControl, Parity, PortState, ReadData, SerialportInfo,
StopBits, BLUETOOTH, PCI, UNKNOWN, USB,
};
use crate::{log_debug, log_error, log_info, log_warn};
use serialport::{
DataBits as SerialDataBits, FlowControl as SerialFlowControl, Parity as SerialParity,
StopBits as SerialStopBits,
};
use std::collections::HashMap;
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender, TryRecvError};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use tauri::{AppHandle, Emitter, Runtime};
use tauri::plugin::PluginHandle;
fn finish_serialport_info(info: SerialportInfo) -> Result<(), Error> {
match info.state {
PortState::Connected(mut cp) => {
if let Some(sender) = cp.sender.take() {
sender.send(1).ok();
}
if let Some(handle) = cp.thread_handle.take() {
handle
.join()
.map_err(|e| Error::String(format!("Failed to join thread: {:?}", e)))?;
}
drop(cp.port);
Ok(())
}
PortState::Opening | PortState::Closed => Ok(()),
}
}
pub struct SerialPort<R: Runtime> {
#[allow(dead_code)]
pub(crate) app: AppHandle<R>,
pub(crate) serialports: Arc<Mutex<HashMap<String, SerialportInfo>>>,
}
impl<R: Runtime> SerialPort<R> {
#[allow(dead_code)]
pub fn new(app: AppHandle<R>) -> Self {
Self {
app,
serialports: Arc::new(Mutex::new(HashMap::new())),
}
}
#[allow(dead_code)]
pub fn from_plugin_handle(plugin_handle: PluginHandle<R>) -> Self {
Self {
app: plugin_handle.app().clone(),
serialports: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn available_ports(&self) -> Result<HashMap<String, HashMap<String, String>>, Error> {
let mut list = serialport::available_ports().unwrap_or_else(|_| vec![]);
list.sort_by(|a, b| a.port_name.cmp(&b.port_name));
let mut result_list: HashMap<String, HashMap<String, String>> = HashMap::new();
for p in list {
result_list.insert(p.port_name, self.get_port_info(p.port_type));
}
Ok(result_list)
}
pub fn available_ports_direct(
&self,
) -> Result<HashMap<String, HashMap<String, String>>, Error> {
let mut result_list: HashMap<String, HashMap<String, String>> = HashMap::new();
#[cfg(target_os = "windows")]
{
use std::process::Command;
let usb_output = Command::new("wmic")
.arg("path")
.arg("Win32_PnPEntity")
.arg("where")
.arg("PNPDeviceID like '%USB%' and Name like '%(COM%'")
.arg("get")
.arg("Name,DeviceID")
.output()
.expect("Failed to execute command");
let usb_devices = String::from_utf8_lossy(&usb_output.stdout);
for line in usb_devices.lines().skip(1) {
let device_info = line.trim();
if !device_info.is_empty() {
let parts: Vec<&str> = device_info.split_whitespace().collect();
if parts.len() >= 2 {
let port_name = parts[1].trim();
let mut port_info = HashMap::new();
port_info.insert("type".to_string(), "USB".to_string());
result_list.insert(port_name.to_string(), port_info);
}
}
}
let com_output = Command::new("wmic")
.arg("path")
.arg("Win32_SerialPort")
.arg("get")
.arg("DeviceID,Name")
.output()
.expect("Failed to execute command");
let com_devices = String::from_utf8_lossy(&com_output.stdout);
for line in com_devices.lines().skip(1) {
let device_info = line.trim();
if !device_info.is_empty() {
let parts: Vec<&str> = device_info.split_whitespace().collect();
if parts.len() >= 2 {
let port_name = parts[0].trim();
let mut port_info = HashMap::new();
port_info.insert("type".to_string(), "COM".to_string());
result_list.insert(port_name.to_string(), port_info);
}
}
}
}
#[cfg(target_os = "linux")]
{
use std::process::Command;
let output = Command::new("lsusb")
.output()
.expect("Failed to execute lsusb command");
let usb_devices = String::from_utf8_lossy(&output.stdout);
for line in usb_devices.lines() {
if line.contains("Serial") || line.contains("USB") {
let mut port_info = HashMap::new();
port_info.insert("type".to_string(), "USB".to_string());
result_list.insert(line.to_string(), port_info);
}
}
let dev_output = Command::new("ls")
.arg("/dev")
.output()
.expect("Failed to execute ls command");
let dev_ports = String::from_utf8_lossy(&dev_output.stdout);
for line in dev_ports.lines() {
if line.starts_with("ttyUSB") || line.starts_with("ttyS") {
let mut port_info = HashMap::new();
port_info.insert(
"type".to_string(),
if line.starts_with("ttyUSB") {
"USB"
} else {
"COM"
}
.to_string(),
);
result_list.insert(format!("/dev/{}", line), port_info);
}
if line.starts_with("rfcomm") {
let mut port_info = HashMap::new();
port_info.insert("type".to_string(), "Bluetooth".to_string());
result_list.insert(format!("/dev/{}", line), port_info);
}
if line.starts_with("ttyACM") {
let mut port_info = HashMap::new();
port_info.insert("type".to_string(), "Virtual".to_string());
result_list.insert(format!("/dev/{}", line), port_info);
}
}
}
#[cfg(target_os = "macos")]
{
use std::process::Command;
let output = Command::new("system_profiler")
.arg("SPUSBDataType")
.output()
.expect("Failed to execute system_profiler");
let usb_devices = String::from_utf8_lossy(&output.stdout);
for line in usb_devices.lines() {
if line.contains("Serial") || line.contains("USB") {
let mut port_info = HashMap::new();
port_info.insert("type".to_string(), "USB".to_string());
result_list.insert(line.to_string(), port_info);
}
}
let dev_output = Command::new("ls")
.arg("/dev")
.output()
.expect("Failed to execute ls command");
let dev_ports = String::from_utf8_lossy(&dev_output.stdout);
for line in dev_ports.lines() {
if line.starts_with("cu.") || line.starts_with("tty.") {
let mut port_info = HashMap::new();
if line.contains("Bluetooth") {
port_info.insert("type".to_string(), "Bluetooth".to_string());
} else if line.starts_with("cu.") {
port_info.insert("type".to_string(), "USB".to_string());
} else {
port_info.insert("type".to_string(), "COM".to_string());
}
result_list.insert(format!("/dev/{}", line), port_info);
}
}
}
Ok(result_list)
}
pub fn managed_ports(&self) -> Result<Vec<String>, Error> {
let ports = self.serialports.lock().map_err(|_| {
Error::String("Failed to lock serialports mutex".to_string())
})?;
let port_list: Vec<String> = ports.keys().cloned().collect();
Ok(port_list)
}
pub fn cancel_read(&self, path: String) -> Result<(), Error> {
self.with_connected_port(path.clone(), |cp| {
if let Some(sender) = &cp.sender {
sender.send(1).map_err(|e| {
Error::String(format!("Failed to cancel serial port data reading: {}", e))
})?;
}
cp.sender = None;
Ok(())
})
}
pub fn close(&self, path: String) -> Result<(), Error> {
log_debug!("close {}", path);
let event_path = path.replace(".", "-").replace("/", "-");
let disconnected_event = format!("plugin-serialplugin-disconnected-{}", &event_path);
if let Err(e) = self.app.emit(
&disconnected_event,
format!("Serial port {} closed", &path),
) {
log_warn!("Failed to send disconnection event on close: {}", e);
}
match self.serialports.lock() {
Ok(mut serialports) => {
if let Some(port_info) = serialports.remove(&path) {
log_debug!("stop {}", path);
finish_serialport_info(port_info)?;
log_debug!("end {}", path);
Ok(())
} else {
Err(Error::String(format!("Serial port {} is not open!", &path)))
}
}
Err(error) => Err(Error::String(format!("Failed to acquire lock: {}", error))),
}
}
pub fn close_all(&self) -> Result<(), Error> {
let mut ports = self
.serialports
.lock()
.map_err(|e| Error::String(e.to_string()))?;
let mut errors = vec![];
for (path, port_info) in ports.drain() {
let event_path = path.replace(".", "-").replace("/", "-");
let disconnected_event = format!("plugin-serialplugin-disconnected-{}", &event_path);
if let Err(e) = self.app.emit(
&disconnected_event,
format!("Serial port {} closed (close_all)", &path),
) {
log_warn!("Failed to send disconnection event for {}: {}", path, e);
}
if let Err(e) = finish_serialport_info(port_info) {
errors.push(e.to_string());
}
}
if errors.is_empty() {
Ok(())
} else {
Err(Error::String(errors.join(", ")))
}
}
pub fn force_close(&self, path: String) -> Result<(), Error> {
match self.serialports.lock() {
Ok(mut map) => {
if let Some(serial) = map.remove(&path) {
finish_serialport_info(serial)?;
}
Ok(())
}
Err(error) => Err(Error::String(format!("Failed to acquire lock: {}", error))),
}
}
pub fn open(
&self,
path: String,
baud_rate: u32,
data_bits: Option<DataBits>,
flow_control: Option<FlowControl>,
parity: Option<Parity>,
stop_bits: Option<StopBits>,
timeout: Option<u64>,
) -> Result<(), Error> {
let mut serialports = self
.serialports
.lock()
.map_err(|e| Error::String(format!("Failed to acquire lock: {}", e)))?;
if let Some(existing) = serialports.remove(&path) {
log_info!("Force closing existing port {}", path);
finish_serialport_info(existing)?;
}
serialports.insert(
path.clone(),
SerialportInfo {
state: PortState::Opening,
},
);
let port_result = serialport::new(path.clone(), baud_rate)
.data_bits(data_bits.map(Into::into).unwrap_or(SerialDataBits::Eight))
.flow_control(
flow_control
.map(Into::into)
.unwrap_or(SerialFlowControl::None),
)
.parity(parity.map(Into::into).unwrap_or(SerialParity::None))
.stop_bits(stop_bits.map(Into::into).unwrap_or(SerialStopBits::One))
.timeout(Duration::from_millis(timeout.unwrap_or(200)))
.open();
match port_result {
Ok(port) => {
let entry = serialports
.get_mut(&path)
.ok_or_else(|| Error::String(format!("Port '{}' disappeared during open", path)))?;
entry.state = PortState::Connected(ConnectedPort {
port,
sender: None,
thread_handle: None,
});
Ok(())
}
Err(e) => {
serialports.remove(&path);
Err(Error::String(format!("Failed to open serial port: {}", e)))
}
}
}
pub fn start_listening(
&self,
path: String,
timeout: Option<u64>,
size: Option<usize>,
) -> Result<(), Error> {
log_debug!("Starting listening on port: {}", path);
self.with_connected_port(path.clone(), |port_info| {
if port_info.sender.is_some() {
log_debug!("Existing listener found, stopping it first");
if let Some(sender) = &port_info.sender {
sender.send(1).map_err(|e| {
log_error!("Failed to stop existing listener: {}", e);
Error::String(format!("Failed to stop existing listener: {}", e))
})?;
}
port_info.sender = None;
if let Some(handle) = port_info.thread_handle.take() {
log_debug!("Waiting for existing thread to finish");
if let Err(e) = handle.join() {
log_error!("Error joining thread: {:?}", e);
}
}
}
let event_path = path.replace(".", "-").replace("/", "-");
let read_event = format!("plugin-serialplugin-read-{}", &event_path);
let disconnected_event = format!("plugin-serialplugin-disconnected-{}", &event_path);
log_debug!("Setting up port monitoring for: {}", read_event);
let mut serial = port_info
.port
.try_clone()
.map_err(|e| Error::String(format!("Failed to clone serial port: {}", e)))?;
let timeout_ms = timeout.unwrap_or(200).min(1);
serial
.set_timeout(Duration::from_millis(timeout_ms))
.map_err(|e| Error::String(format!("Failed to set short timeout: {}", e)))?;
let (tx, rx): (Sender<usize>, Receiver<usize>) = mpsc::channel();
port_info.sender = Some(tx);
let app_clone = self.app.clone();
let path_clone = path.clone();
let thread_handle = thread::spawn(move || {
let mut combined_buffer: Vec<u8> = Vec::with_capacity(size.unwrap_or(1024));
let mut start_time = Instant::now();
loop {
match rx.try_recv() {
Ok(_) => break,
Err(TryRecvError::Disconnected) => {
if let Err(e) = app_clone.emit(
&disconnected_event,
format!("Serial port {} disconnected!", &path_clone),
) {
log_error!("Failed to send disconnection event: {}", e);
}
break;
}
Err(TryRecvError::Empty) => {}
}
let mut buffer = vec![0; size.unwrap_or(1024)];
match serial.read(&mut buffer) {
Ok(n) => {
combined_buffer.extend_from_slice(&buffer[..n]);
}
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {}
Err(e) => {
log_error!("Failed to read data: {}", e);
if let Err(err) = app_clone.emit(
&disconnected_event,
format!(
"Serial port {} disconnected due to error: {}",
&path_clone, e
),
) {
log_error!("Failed to send disconnection event: {}", err);
}
break;
}
}
let elapsed_time = start_time.elapsed();
if elapsed_time > Duration::from_millis(timeout.unwrap_or(200)) {
start_time = Instant::now();
let size = combined_buffer.len();
if size == 0 {
continue;
}
if let Err(e) = app_clone.emit(
&read_event,
ReadData {
size,
data: combined_buffer.as_mut_slice(),
},
) {
log_error!("Failed to send data: {}", e);
}
combined_buffer.clear();
}
}
});
port_info.thread_handle = Some(thread_handle);
Ok({})
})
}
pub fn stop_listening(&self, path: String) -> Result<(), Error> {
log_debug!("Stopping listening on port: {}", path);
self.with_connected_port(path.clone(), |port_info| {
if let Some(sender) = &port_info.sender {
sender.send(1).map_err(|e| {
Error::String(format!("Failed to cancel serial port data reading: {}", e))
})?;
}
port_info.sender = None;
port_info.thread_handle = None;
Ok({})
})
}
pub fn read(
&self,
path: String,
timeout: Option<u64>,
size: Option<usize>,
) -> Result<String, Error> {
self.get_serialport(path.clone(), |port| {
let timeout = timeout.unwrap_or(1000);
let mut buffer = vec![0; size.unwrap_or(1024)];
port.set_timeout(Duration::from_millis(timeout))
.map_err(|e| Error::String(format!("Failed to set timeout: {}", e)))?;
match port.read(&mut buffer) {
Ok(n) => {
let data = String::from_utf8_lossy(&buffer[..n]).to_string();
Ok(data)
}
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => Err(Error::String(format!(
"no data received within {} ms",
timeout
))),
Err(e) => Err(Error::String(format!("Failed to read data: {}", e))),
}
})
}
pub fn read_binary(
&self,
path: String,
timeout: Option<u64>,
size: Option<usize>,
) -> Result<Vec<u8>, Error> {
self.get_serialport(path.clone(), |port| {
let target_size = size.unwrap_or(1024);
let timeout = timeout.unwrap_or(1000);
let mut buffer = Vec::with_capacity(target_size);
let start = std::time::Instant::now();
while buffer.len() < target_size && start.elapsed() < Duration::from_millis(timeout) {
let mut temp_buf = vec![0; target_size - buffer.len()];
match port.read(&mut temp_buf) {
Ok(n) if n > 0 => {
buffer.extend_from_slice(&temp_buf[..n]);
}
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {
if buffer.is_empty() {
return Err(Error::String(format!(
"no data received within {} ms",
timeout
)));
} else {
break;
}
}
Err(e) => return Err(Error::String(format!("Failed to read data: {}", e))),
_ => break,
}
}
Ok(buffer)
})
}
pub fn write(&self, path: String, value: String) -> Result<usize, Error> {
self.get_serialport(path.clone(), |port| {
port.write(value.as_bytes())
.map_err(|e| Error::String(format!("Failed to write data: {}", e)))
})
}
pub fn write_binary(&self, path: String, value: Vec<u8>) -> Result<usize, Error> {
self.get_serialport(path.clone(), |port| {
port.write(&value)
.map_err(|e| Error::String(format!("Failed to write binary data: {}", e)))
})
}
pub fn set_baud_rate(&self, path: String, baud_rate: u32) -> Result<(), Error> {
self.get_serialport(path, |port| {
port.set_baud_rate(baud_rate)
.map_err(|e| Error::String(format!("Failed to set baud rate: {}", e)))
})
}
pub fn set_data_bits(&self, path: String, data_bits: DataBits) -> Result<(), Error> {
self.get_serialport(path, |port| {
port.set_data_bits(data_bits.into()).map_err(Error::from)
})
}
pub fn set_flow_control(&self, path: String, flow_control: FlowControl) -> Result<(), Error> {
self.get_serialport(path, |port| {
port.set_flow_control(flow_control.into()).map_err(Error::from)
})
}
pub fn set_parity(&self, path: String, parity: Parity) -> Result<(), Error> {
self.get_serialport(path, |port| {
port.set_parity(parity.into()).map_err(Error::from)
})
}
pub fn set_stop_bits(&self, path: String, stop_bits: StopBits) -> Result<(), Error> {
self.get_serialport(path, |port| {
port.set_stop_bits(stop_bits.into()).map_err(Error::from)
})
}
pub fn set_timeout(&self, path: String, timeout: Duration) -> Result<(), Error> {
self.get_serialport(path, |port| {
port.set_timeout(timeout).map_err(Error::from)
})
}
pub fn write_request_to_send(&self, path: String, level: bool) -> Result<(), Error> {
self.get_serialport(path, |port| {
port.write_request_to_send(level).map_err(Error::from)
})
}
pub fn write_data_terminal_ready(&self, path: String, level: bool) -> Result<(), Error> {
self.get_serialport(path, |port| {
port.write_data_terminal_ready(level).map_err(Error::from)
})
}
pub fn read_clear_to_send(&self, path: String) -> Result<bool, Error> {
self.get_serialport(path, |port| {
port.read_clear_to_send().map_err(Error::from)
})
}
pub fn read_data_set_ready(&self, path: String) -> Result<bool, Error> {
self.get_serialport(path, |port| {
port.read_data_set_ready().map_err(Error::from)
})
}
pub fn read_ring_indicator(&self, path: String) -> Result<bool, Error> {
self.get_serialport(path, |port| {
port.read_ring_indicator().map_err(Error::from)
})
}
pub fn read_carrier_detect(&self, path: String) -> Result<bool, Error> {
self.get_serialport(path, |port| {
port.read_carrier_detect().map_err(Error::from)
})
}
pub fn bytes_to_read(&self, path: String) -> Result<u32, Error> {
self.get_serialport(path, |port| port.bytes_to_read().map_err(Error::from))
}
pub fn bytes_to_write(&self, path: String) -> Result<u32, Error> {
self.get_serialport(path, |port| port.bytes_to_write().map_err(Error::from))
}
pub fn clear_buffer(&self, path: String, buffer_to_clear: ClearBuffer) -> Result<(), Error> {
self.get_serialport(path, |port| {
port.clear(buffer_to_clear.into()).map_err(Error::from)
})
}
pub fn set_break(&self, path: String) -> Result<(), Error> {
self.get_serialport(path, |port| port.set_break().map_err(Error::from))
}
pub fn clear_break(&self, path: String) -> Result<(), Error> {
self.get_serialport(path, |port| port.clear_break().map_err(Error::from))
}
fn with_connected_port<T, F>(&self, path: String, f: F) -> Result<T, Error>
where
F: FnOnce(&mut ConnectedPort) -> Result<T, Error>,
{
let mut ports = self
.serialports
.lock()
.map_err(|e| Error::String(format!("Mutex lock failed: {}", e)))?;
let info = ports
.get_mut(&path)
.ok_or_else(|| Error::String(format!("Port '{}' not found", path)))?;
match &mut info.state {
PortState::Connected(cp) => f(cp),
other => Err(Error::String(other.not_connected_reason())),
}
}
fn get_serialport<T, F>(&self, path: String, f: F) -> Result<T, Error>
where
F: FnOnce(&mut Box<dyn serialport::SerialPort>) -> Result<T, Error>,
{
self.with_connected_port(path, |cp| f(&mut cp.port))
}
fn get_port_info(&self, port: serialport::SerialPortType) -> HashMap<String, String> {
let mut port_info: HashMap<String, String> = HashMap::new();
port_info.insert("type".to_string(), UNKNOWN.to_string());
port_info.insert("vid".to_string(), UNKNOWN.to_string());
port_info.insert("pid".to_string(), UNKNOWN.to_string());
port_info.insert("serial_number".to_string(), UNKNOWN.to_string());
port_info.insert("manufacturer".to_string(), UNKNOWN.to_string());
port_info.insert("product".to_string(), UNKNOWN.to_string());
match port {
serialport::SerialPortType::UsbPort(info) => {
port_info.insert("type".to_string(), USB.to_string());
port_info.insert("vid".to_string(), info.vid.to_string());
port_info.insert("pid".to_string(), info.pid.to_string());
port_info.insert(
"serial_number".to_string(),
info.serial_number.unwrap_or_else(|| UNKNOWN.to_string()),
);
port_info.insert(
"manufacturer".to_string(),
info.manufacturer.unwrap_or_else(|| UNKNOWN.to_string()),
);
port_info.insert(
"product".to_string(),
info.product.unwrap_or_else(|| UNKNOWN.to_string()),
);
}
serialport::SerialPortType::BluetoothPort => {
port_info.insert("type".to_string(), BLUETOOTH.to_string());
}
serialport::SerialPortType::PciPort => {
port_info.insert("type".to_string(), PCI.to_string());
}
serialport::SerialPortType::Unknown => {
port_info.insert("type".to_string(), UNKNOWN.to_string());
}
}
port_info
}
}