use crate::api::SerialPort;
use crate::error::Error;
use crate::events::{Capabilities, ExchangeOptions, SerialEvent, WatchOptions};
use crate::state::{ClearBuffer, DataBits, FlowControl, Parity, StopBits};
use std::collections::HashMap;
use std::time::Duration;
use tauri::ipc::Channel;
use tauri::{AppHandle, Manager, Runtime, State};
async fn blocking<R: Runtime, T: Send + 'static>(
serial: &State<'_, SerialPort<R>>,
f: impl FnOnce(SerialPort<R>) -> Result<T, Error> + Send + 'static,
) -> Result<T, Error> {
let serial = serial.inner().clone();
tauri::async_runtime::spawn_blocking(move || f(serial))
.await
.map_err(|e| Error::String(format!("serial task failed: {e}")))?
}
pub(crate) const CONFIG_KEY: &str = "serialplugin-config";
pub(crate) struct TcpAllowlist(pub(crate) Vec<String>);
pub(crate) fn parse_tcp_allowlist(
config: Option<&serde_json::Value>,
) -> Result<Option<Vec<String>>, String> {
match config.and_then(|c| c.get("tcpAllowlist")) {
None | Some(serde_json::Value::Null) => Ok(None),
Some(v) => serde_json::from_value(v.clone()).map(Some).map_err(|e| {
format!(
"plugins.{CONFIG_KEY}.tcpAllowlist must be a list of \"host:port\" strings: {e}"
)
}),
}
}
pub(crate) fn check_tcp_allowed(allowlist: Option<&[String]>, path: &str) -> Result<(), Error> {
let (Some(list), Some(addr)) = (allowlist, crate::port::tcp::tcp_address(path)) else {
return Ok(());
};
if list.iter().any(|a| a.eq_ignore_ascii_case(addr)) {
Ok(())
} else {
Err(Error::String(format!(
"{path}: not in plugins.{CONFIG_KEY}.tcpAllowlist"
)))
}
}
#[tauri::command]
pub async fn available_ports<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
single_port_per_device: Option<bool>,
) -> Result<HashMap<String, HashMap<String, String>>, Error> {
blocking(&serial, move |s| {
s.available_ports(single_port_per_device.unwrap_or(false))
})
.await
}
#[tauri::command]
pub fn managed_ports<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
) -> Result<Vec<String>, Error> {
serial.managed_ports()
}
#[tauri::command]
pub async fn cancel_read<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<(), Error> {
blocking(&serial, move |s| s.cancel_read(path)).await
}
#[tauri::command]
pub async fn close<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<(), Error> {
blocking(&serial, move |s| s.close(path)).await
}
#[tauri::command]
pub async fn close_all<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
) -> Result<(), Error> {
blocking(&serial, move |s| s.close_all()).await
}
#[tauri::command]
pub async fn force_close<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<(), Error> {
blocking(&serial, move |s| s.force_close(path)).await
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
pub async fn open<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
baud_rate: u32,
data_bits: Option<DataBits>,
flow_control: Option<FlowControl>,
parity: Option<Parity>,
stop_bits: Option<StopBits>,
timeout: Option<u64>,
dtr: Option<bool>,
rts: Option<bool>,
) -> Result<String, Error> {
if let Some(list) = _app.try_state::<TcpAllowlist>() {
check_tcp_allowed(Some(&list.0), &path)?;
}
blocking(&serial, move |s| {
s.open_with_lines(
path,
baud_rate,
data_bits,
flow_control,
parity,
stop_bits,
timeout,
crate::state::ModemLinesOnOpen { dtr, rts },
)
})
.await
}
#[tauri::command]
pub async fn write<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
value: String,
) -> Result<usize, Error> {
blocking(&serial, move |s| s.write(path, value)).await
}
#[tauri::command]
pub async fn write_binary<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
value: Vec<u8>,
) -> Result<usize, Error> {
blocking(&serial, move |s| s.write_binary(path, value)).await
}
#[tauri::command]
pub async fn read<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
timeout: Option<u64>,
size: Option<usize>,
) -> Result<String, Error> {
blocking(&serial, move |s| s.read(path, timeout, size)).await
}
#[tauri::command]
pub async fn read_binary<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
timeout: Option<u64>,
size: Option<usize>,
) -> Result<Vec<u8>, Error> {
blocking(&serial, move |s| s.read_binary(path, timeout, size)).await
}
#[tauri::command]
pub fn capabilities() -> Result<Capabilities, Error> {
Ok(Capabilities::current())
}
pub async fn watch<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
options: WatchOptions,
channel: Channel<SerialEvent>,
) -> Result<u32, Error> {
blocking(&serial, move |s| s.watch(path, options, channel)).await
}
#[doc(hidden)]
pub use framed::{__cmd__watch, __tauri_command_name_watch};
pub mod framed {
use super::*;
#[tauri::command]
pub async fn watch<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
options: WatchOptions,
parser: Option<serial_codec::framing::FramerConfig>,
channel: Channel<SerialEvent>,
) -> Result<u32, Error> {
blocking(&serial, move |s| {
s.watch_framed(path, options, parser, channel)
})
.await
}
}
#[tauri::command]
pub async fn unwatch<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
channel_id: u32,
) -> Result<(), Error> {
blocking(&serial, move |s| s.unwatch(channel_id)).await
}
#[tauri::command]
pub async fn watch_ports<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
options: Option<crate::events::WatchPortsOptions>,
channel: tauri::ipc::Channel<crate::events::PortListEvent>,
) -> Result<u32, Error> {
blocking(&serial, move |s| {
s.watch_ports(options.unwrap_or_default(), channel)
})
.await
}
#[tauri::command]
pub async fn unwatch_ports<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
channel_id: u32,
) -> Result<(), Error> {
blocking(&serial, move |s| s.unwatch_ports(channel_id)).await
}
#[tauri::command]
pub async fn set_baud_rate<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
baud_rate: u32,
) -> Result<(), Error> {
blocking(&serial, move |s| s.set_baud_rate(path, baud_rate)).await
}
#[tauri::command]
pub async fn set_data_bits<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
data_bits: DataBits,
) -> Result<(), Error> {
blocking(&serial, move |s| s.set_data_bits(path, data_bits)).await
}
#[tauri::command]
pub async fn set_flow_control<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
flow_control: FlowControl,
) -> Result<(), Error> {
blocking(&serial, move |s| s.set_flow_control(path, flow_control)).await
}
#[tauri::command]
pub async fn set_parity<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
parity: Parity,
) -> Result<(), Error> {
blocking(&serial, move |s| s.set_parity(path, parity)).await
}
#[tauri::command]
pub async fn set_stop_bits<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
stop_bits: StopBits,
) -> Result<(), Error> {
blocking(&serial, move |s| s.set_stop_bits(path, stop_bits)).await
}
#[tauri::command]
pub async fn set_timeout<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
timeout: u64,
) -> Result<(), Error> {
blocking(&serial, move |s| {
let timeout_duration = Duration::from_millis(timeout);
s.set_timeout(path, timeout_duration)
})
.await
}
#[tauri::command]
pub async fn write_request_to_send<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
level: bool,
) -> Result<(), Error> {
blocking(&serial, move |s| s.write_request_to_send(path, level)).await
}
#[tauri::command]
pub async fn write_data_terminal_ready<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
level: bool,
) -> Result<(), Error> {
blocking(&serial, move |s| s.write_data_terminal_ready(path, level)).await
}
#[tauri::command]
pub async fn read_clear_to_send<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<bool, Error> {
blocking(&serial, move |s| s.read_clear_to_send(path)).await
}
#[tauri::command]
pub async fn read_data_set_ready<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<bool, Error> {
blocking(&serial, move |s| s.read_data_set_ready(path)).await
}
#[tauri::command]
pub async fn read_ring_indicator<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<bool, Error> {
blocking(&serial, move |s| s.read_ring_indicator(path)).await
}
#[tauri::command]
pub async fn read_carrier_detect<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<bool, Error> {
blocking(&serial, move |s| s.read_carrier_detect(path)).await
}
#[tauri::command]
pub async fn bytes_to_read<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<u32, Error> {
blocking(&serial, move |s| s.bytes_to_read(path)).await
}
#[tauri::command]
pub async fn bytes_to_write<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<u32, Error> {
blocking(&serial, move |s| s.bytes_to_write(path)).await
}
#[tauri::command]
pub async fn clear_buffer<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
buffer_type: ClearBuffer,
) -> Result<(), Error> {
blocking(&serial, move |s| s.clear_buffer(path, buffer_type)).await
}
#[tauri::command]
pub async fn set_break<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<(), Error> {
blocking(&serial, move |s| s.set_break(path)).await
}
#[tauri::command]
pub async fn clear_break<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<(), Error> {
blocking(&serial, move |s| s.clear_break(path)).await
}
#[tauri::command]
pub fn set_log_level<R: Runtime>(
_app: AppHandle<R>,
_serial: State<'_, SerialPort<R>>,
level: crate::state::LogLevel,
) -> Result<(), Error> {
crate::state::set_log_level(level);
Ok(())
}
#[tauri::command]
pub fn get_log_level<R: Runtime>(
_app: AppHandle<R>,
_serial: State<'_, SerialPort<R>>,
) -> Result<crate::state::LogLevel, Error> {
Ok(crate::state::get_log_level())
}
#[tauri::command]
pub async fn exchange<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
value: String,
options: Option<ExchangeOptions>,
) -> Result<crate::at::parse::ExchangeResponse, Error> {
let serial = serial.inner().clone();
tauri::async_runtime::spawn_blocking(move || {
serial.exchange(path, value, options.unwrap_or_default())
})
.await
.map_err(|e| Error::String(format!("exchange task failed: {e}")))?
}
#[tauri::command]
pub async fn cancel_exchange<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<(), Error> {
blocking(&serial, move |s| s.cancel_exchange(path)).await
}
#[tauri::command]
pub async fn at<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
command: String,
options: Option<crate::at::session::AtCommandOptions>,
) -> Result<crate::at::parse::AtCommandResult, Error> {
let serial = serial.inner().clone();
tauri::async_runtime::spawn_blocking(move || serial.at(path, command, options))
.await
.map_err(|e| Error::String(format!("at task failed: {e}")))?
}
#[tauri::command]
pub async fn at_phases<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
phases: Vec<crate::at::session::AtPhase>,
) -> Result<Vec<crate::at::parse::AtCommandResult>, Error> {
let serial = serial.inner().clone();
tauri::async_runtime::spawn_blocking(move || serial.at_phases(path, phases))
.await
.map_err(|e| Error::String(format!("at_phases task failed: {e}")))?
}
#[tauri::command]
pub async fn send_sms_pdu<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
length: u32,
pdu: Vec<u8>,
options: Option<crate::at::session::SendSmsPduOptions>,
) -> Result<Vec<crate::at::parse::AtCommandResult>, Error> {
let serial = serial.inner().clone();
tauri::async_runtime::spawn_blocking(move || serial.send_sms_pdu(path, length, pdu, options))
.await
.map_err(|e| Error::String(format!("send_sms_pdu task failed: {e}")))?
}
#[tauri::command]
pub async fn configure_at_session<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
session: crate::at::session::AtSessionConfig,
) -> Result<(), Error> {
blocking(&serial, move |s| s.configure_at_session(path, session)).await
}
#[tauri::command]
pub async fn exchange_binary<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
value: Vec<u8>,
options: Option<crate::events::ExchangeOptions>,
) -> Result<crate::at::parse::ExchangeResponse, Error> {
let serial = serial.inner().clone();
tauri::async_runtime::spawn_blocking(move || {
serial.exchange_binary(path, value, options.unwrap_or_default())
})
.await
.map_err(|e| Error::String(format!("exchange_binary task failed: {e}")))?
}
#[derive(Debug, Clone, Default, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnableMuxOptions {
pub command: Option<String>,
pub timeout_ms: Option<u64>,
}
#[tauri::command]
pub async fn enable_mux<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
options: Option<EnableMuxOptions>,
) -> Result<(), Error> {
let opts = options.unwrap_or_default();
let command = opts
.command
.unwrap_or_else(|| "AT+CMUX=0,0,5,31,10,2".to_string());
let timeout_ms = opts.timeout_ms.unwrap_or(5000);
let serial = serial.inner().clone();
tauri::async_runtime::spawn_blocking(move || serial.enable_mux(path, command, timeout_ms))
.await
.map_err(|e| Error::String(format!("enable_mux task failed: {e}")))?
}
#[tauri::command]
pub async fn open_mux_channel<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
dlci: u8,
) -> Result<String, Error> {
blocking(&serial, move |s| s.open_mux_channel(path, dlci)).await
}
#[tauri::command]
pub async fn disable_mux<R: Runtime>(
_app: AppHandle<R>,
serial: State<'_, SerialPort<R>>,
path: String,
) -> Result<(), Error> {
blocking(&serial, move |s| s.disable_mux(path)).await
}
#[tauri::command]
pub async fn ble_scan(
timeout_ms: Option<u64>,
) -> Result<HashMap<String, HashMap<String, String>>, Error> {
let duration = Duration::from_millis(timeout_ms.unwrap_or(3000).clamp(500, 30_000));
#[cfg(feature = "ble")]
{
#[cfg(not(target_os = "android"))]
use crate::port::ble::scan;
#[cfg(target_os = "android")]
use crate::port::ble_android::scan;
tauri::async_runtime::spawn_blocking(move || scan(duration))
.await
.map_err(|e| Error::String(format!("BLE scan task failed: {e}")))?
.map_err(|e| Error::String(e.to_string()))
}
#[cfg(not(feature = "ble"))]
{
let _ = duration;
Err(Error::String(
"BLE needs the plugin's `ble` cargo feature".into(),
))
}
}
#[cfg(test)]
mod tcp_allowlist_tests {
use super::*;
use serde_json::json;
#[test]
fn no_config_allows_every_tcp_address() {
assert_eq!(parse_tcp_allowlist(None), Ok(None));
assert_eq!(parse_tcp_allowlist(Some(&json!({}))), Ok(None));
assert_eq!(
parse_tcp_allowlist(Some(&serde_json::Value::Null)),
Ok(None)
);
assert!(check_tcp_allowed(None, "tcp://10.0.0.9:23").is_ok());
}
#[test]
fn allowlist_allows_listed_and_denies_the_rest() {
let list = parse_tcp_allowlist(Some(
&json!({"tcpAllowlist": ["10.0.0.5:4000", "Bridge.local:23"]}),
))
.unwrap()
.unwrap();
assert!(check_tcp_allowed(Some(list.as_slice()), "tcp://10.0.0.5:4000").is_ok());
assert!(check_tcp_allowed(Some(list.as_slice()), "tcp://bridge.LOCAL:23").is_ok());
assert!(check_tcp_allowed(Some(list.as_slice()), "tcp://10.0.0.5:4001").is_err());
assert!(check_tcp_allowed(Some(list.as_slice()), "tcp://evil:23").is_err());
assert!(check_tcp_allowed(Some(list.as_slice()), "/dev/ttyUSB0").is_ok());
assert!(check_tcp_allowed(Some(&[][..]), "tcp://10.0.0.5:4000").is_err());
}
#[test]
fn malformed_allowlist_is_an_error_not_allow_all() {
assert!(parse_tcp_allowlist(Some(&json!({"tcpAllowlist": "10.0.0.5:4000"}))).is_err());
assert!(parse_tcp_allowlist(Some(&json!({"tcpAllowlist": [1]}))).is_err());
}
}