A comprehensive plugin for Tauri applications to communicate with serial ports. This plugin provides a complete API for reading from and writing to serial devices, with support for various configuration options and control signals.
v3.0 breaking change:
listen/startListening/disconnectedwere removed. Usewatch()andSerialPort.getCapabilities(). See Migrating to v3 below.
In v2 you attached stream callbacks separately (startListening / listen / disconnected).
In v3 one call — watch() — registers all of them; stop with the returned handle.
// v2awaitport.startListening();awaitport.listen((data)=>console.log(data));awaitport.disconnected((reason)=>console.log("gone",reason));awaitport.cancelListen();// v3 — same thing in one watch()consthandle=awaitport.watch({onData: (data)=>console.log(data),onDisconnect: (reason)=>console.log("gone",reason),// replaces disconnected(fn)});awaithandle.unwatch();| v2 | v3 |
|---|---|
startListening() + listen(fn) | const handle = await port.watch({ onData: fn }) |
disconnected(fn) | pass onDisconnect: fn into the same watch({ ... }) |
cancelListen() / stopListening() | await handle.unwatch() |
Legacy Tauri events (plugin-serialplugin-read-*) and Android plugin triggers (serialData, serialError) are no longer part of the public API.
New in v3:SerialPort.getCapabilities() — { transport, platform, version } from Rust (cfg!), if the app needs to branch by platform (replaces ad-hoc window / @tauri-apps/plugin-os probing on the app side).
Breaking API changes (v3):
| Field / behavior | v2 | v3 |
|---|---|---|
AtCommandResult.raw / ExchangeResponse.raw | number[] | Uint8Array |
AtCommandResult.timedOut | literal false | boolean (timeouts surfaced by native layer) |
open() | void | returns canonical path (Android re-keys to UsbPath.sessionKey; desktop echoes input) |
cancel_read | also detached watch on some builds | does not unwatch — use close() or handle.unwatch() |
| TX queue after error | port could stay halted until reopen | next exchange / sendAt job runs normally (stopOnError only stops remaining phases in one sendAtPhases batch) |
Early RX before exchange | often lost or raced with wait | native hub keeps idle bytes; exchange replays them via take_idle_bytes after write (no extra line response needed) |
kind | Meaning | Port stays open? |
|---|---|---|
data | Incoming bytes (decoded to string in JS when decode !== false) | Yes |
error | Non-fatal notification (e.g. emitter glitch); watch continues | Yes |
disconnect | Fatal end of stream (unplug, IO manager stopped); triggers auto-reconnect if enabled | No (isOpen = false) |
| Option | Desktop | Android |
|---|---|---|
timeout | Batch coalescing window (ms) | Coalescing / flush hint where supported |
serialDataFlushIntervalMs | Preferred batch interval; falls back to timeout | BufferedEmitter flush (10–2000 ms) |
size | Read chunk size per syscall | Reserved |
decode | JS-only: TextDecoder on onData | JS-only |
- Installation
- Basic Usage
- TypeScript Support
- Log Level Control
- Rust Usage
- Permissions
- API Reference
7.1. Port Discovery
7.2. Connection Management
7.3. Data Transfer
7.4. Port Configuration
7.5. Control Signals
7.6. Buffer Management
7.7. Log Control
7.8. Auto-Reconnect - Common Use Cases
- Android Setup
- Contributing
- Development Setup
- Testing
- Partners
- License
- Rust version 1.70 or higher
- Tauri 2.0 or higher
- Node.js and an npm-compatible package manager (npm, yarn, pnpm)
Use the Tauri CLI to automatically install both the Rust and JavaScript parts of the plugin:
# npm
npm run tauri add serialplugin
# yarn
yarn run tauri add serialplugin
# pnpm
pnpm tauri add serialplugin
# deno
deno task tauri add serialplugin
# bun
bun tauri add serialplugin
# cargo
cargo tauri add serialpluginAdd the plugin using cargo:
cd ./src-tauri
cargo add tauri-plugin-serialpluginInstall the JavaScript API:
npm install tauri-plugin-serialplugin-api
# or
pnpm add tauri-plugin-serialplugin-apiUSB serial on Android uses pure Rust drivers in crates/android-usb-serial (nusb). Kotlin provides USB permission, enumeration, and a dup'd fd; no vendored Java stack or JitPack dependency.
Register the Plugin
// src-tauri/src/main.rsfnmain(){ tauri::Builder::default().plugin(tauri_plugin_serialplugin::init()).run(tauri::generate_context!()).expect("error while running tauri application");}
Configure Permissions
Basic Example
import{SerialPort}from"tauri-plugin-serialplugin-api";// List available portsconstports=awaitSerialPort.available_ports();console.log("Available ports:",ports);// Open a portconstport=newSerialPort({path: "COM1",baudRate: 9600});awaitport.open();// Write dataawaitport.write("Hello, Serial Port!");// Stream incoming data (desktop + Android)consthandle=awaitport.watch({onData: (data)=>console.log("Received:",data),onDisconnect: (reason)=>console.log("Disconnected:",reason),});// Stop streaming when doneawaithandle.unwatch();// Close portawaitport.close();
Error Handling Example
import{SerialPort}from"tauri-plugin-serialplugin-api";asyncfunctionhandleSerialPort(){letport: SerialPort|null=null;try{// List available portsconstports=awaitSerialPort.available_ports();if(Object.keys(ports).length===0){thrownewError("No serial ports found");}// Open portport=newSerialPort({path: "COM1",baudRate: 9600});try{awaitport.open();}catch(error){thrownewError(`Failed to open port: ${error}`);}try{// Write dataawaitport.write("Test data");}catch(error){thrownewError(`Failed to write data: ${error}`);}try{// Read dataconstdata=awaitport.read({timeout: 1000});console.log("Received:",data);}catch(error){thrownewError(`Failed to read data: ${error}`);}try{consthandle=awaitport.watch({onData: (data)=>console.log("Received:",data),});// ... use handle.unwatch() in cleanup}catch(error){thrownewError(`Failed to start watch: ${error}`);}try{// Configure port settingsawaitport.setBaudRate(115200);awaitport.setDataBits(DataBits.Eight);awaitport.setFlowControl(FlowControl.None);awaitport.setParity(Parity.None);awaitport.setStopBits(StopBits.One);awaitport.setTimeout(1000);}catch(error){thrownewError(`Failed to configure port: ${error}`);}}catch(error){// Handle all errors in one placeconsole.error("Serial port error:",error);}finally{// Clean upif(port){try{awaitport.close();}catch(error){console.error("Error during cleanup:",error);}}}}// UsagehandleSerialPort();
This plugin provides full TypeScript support with comprehensive type definitions. All methods, interfaces, and enums are properly typed for better development experience.
import{SerialPort,DataBits,FlowControl,Parity,StopBits,ClearBuffer,PortInfo,SerialportOptions,ReadOptions}from"tauri-plugin-serialplugin-api";SerialPort- Main class for serial port operationsDataBits- Enum:Five,Six,Seven,EightFlowControl- Enum:None,Software,HardwareParity- Enum:None,Odd,EvenStopBits- Enum:One,TwoClearBuffer- Enum:Input,Output,AllPortInfo- Interface for port informationSerialportOptions- Interface for port configurationReadOptions- Interface for read operation options
import{SerialPort,DataBits,FlowControl,Parity,StopBits}from"tauri-plugin-serialplugin-api";constport=newSerialPort({path: "/dev/ttyUSB0",baudRate: 9600,dataBits: DataBits.Eight,// Type-safe enumflowControl: FlowControl.None,// Type-safe enumparity: Parity.None,// Type-safe enumstopBits: StopBits.One,// Type-safe enumtimeout: 1000,size: 1024});// All configuration methods are fully typedawaitport.setBaudRate(115200);awaitport.setDataBits(DataBits.Eight);awaitport.setFlowControl(FlowControl.None);awaitport.setParity(Parity.None);awaitport.setStopBits(StopBits.One);awaitport.setTimeout(500);// Set control signalsawaitport.writeRequestToSend(true);awaitport.writeDataTerminalReady(true);// Alternative methods (writeRequestToSend and writeDataTerminalReady)awaitport.writeRequestToSend(true);awaitport.writeDataTerminalReady(true);// Read control signalsconstcts=awaitport.readClearToSend();constdsr=awaitport.readDataSetReady();constri=awaitport.readRingIndicator();constcd=awaitport.readCarrierDetect();import{ClearBuffer}from"tauri-plugin-serialplugin-api";// Check buffer statusconstbytesToRead=awaitport.bytesToRead();constbytesToWrite=awaitport.bytesToWrite();// Clear buffers with type-safe enumawaitport.clearBuffer(ClearBuffer.Input);awaitport.clearBuffer(ClearBuffer.Output);awaitport.clearBuffer(ClearBuffer.All);// Break signal controlawaitport.setBreak();awaitport.clearBreak();The plugin provides comprehensive logging control to help you manage verbosity in production environments. By default, the plugin logs informational messages, but you can adjust this to reduce noise or enable detailed debugging.
import{SerialPort,LogLevel}from"tauri-plugin-serialplugin-api";// Disable all logs (recommended for production)awaitSerialPort.setLogLevel(LogLevel.None);// Show only errorsawaitSerialPort.setLogLevel(LogLevel.Error);// Show errors and warningsawaitSerialPort.setLogLevel(LogLevel.Warn);// Show errors, warnings, and info (default)awaitSerialPort.setLogLevel(LogLevel.Info);// Enable all logs including debug informationawaitSerialPort.setLogLevel(LogLevel.Debug);// Get current log levelconstcurrentLevel=awaitSerialPort.getLogLevel();console.log("Current log level:",currentLevel);use tauri_plugin_serialplugin::state::{LogLevel, set_log_level};// Set log level on plugin initializationfnmain(){// Disable logs in productionset_log_level(LogLevel::None);
tauri::Builder::default().plugin(tauri_plugin_serialplugin::init()).run(tauri::generate_context!()).expect("error while running tauri application");}Or configure via command:
use tauri_plugin_serialplugin::commands::set_log_level;use tauri_plugin_serialplugin::state::LogLevel;use tauri::{AppHandle,State};#[tauri::command]asyncfnconfigure_production_logging(app:AppHandle<tauri::Wry>,serial:State<'_, tauri_plugin_serialplugin::api::desktop::SerialPort<tauri::Wry>>) -> Result<(),String>{// Only show errors in productionset_log_level(app, serial,LogLevel::Error).map_err(|e| e.to_string())}None- No logging output (recommended for production)Error- Only critical errorsWarn- Errors and warningsInfo- Errors, warnings, and general information (default)Debug- All logging including debug information (for development)
// Disable noisy logs when polling for available portsawaitSerialPort.setLogLevel(LogLevel.None);setInterval(async()=>{constports=awaitSerialPort.available_ports();// No extra console noise from the plugin while polling},1000);// Enable detailed logging for troubleshootingawaitSerialPort.setLogLevel(LogLevel.Debug);constport=newSerialPort({path: "COM1",baudRate: 9600});awaitport.open();// See all internal events and state changes// Set log level based on environmentconstisDevelopment=import.meta.env.DEV;awaitSerialPort.setLogLevel(isDevelopment ? LogLevel.Debug : LogLevel.Error);This plugin can also be used directly from Rust code in your Tauri backend. For complete API documentation, see docs.rs.
Here's how to use it:
You can import and use the command functions directly from the plugin:
use tauri_plugin_serialplugin::commands::{
available_ports, open, write, read, close, set_baud_rate,
set_data_bits, set_flow_control, set_parity, set_stop_bits, set_timeout,
write_request_to_send, write_data_terminal_ready,
read_clear_to_send, read_data_set_ready,
bytes_to_read, bytes_to_write, clear_buffer,
set_break, clear_break
};use tauri_plugin_serialplugin::state::{DataBits,FlowControl,Parity,StopBits,ClearBuffer};use tauri::{AppHandle,State,Runtime};use std::collections::HashMap;#[tauri::command]asyncfnrust_serial_example(app:AppHandle<tauri::Wry>,serial:State<'_, tauri_plugin_serialplugin::api::desktop::SerialPort<tauri::Wry>>) -> Result<(),String>{// Get available portslet ports = available_ports(app.clone(), serial.clone()).map_err(|e| format!("Failed to get ports: {}", e))?;println!("Available ports: {:?}", ports);// Open a serial portlet path = "COM1".to_string();let baud_rate = 9600;open(
app.clone(),
serial.clone(),
path.clone(),
baud_rate,Some(DataBits::Eight),Some(FlowControl::None),Some(Parity::None),Some(StopBits::One),Some(1000u64)// timeout in milliseconds).map_err(|e| format!("Failed to open port: {}", e))?;// Write datalet data = "Hello from Rust!".to_string();let bytes_written = write(app.clone(), serial.clone(), path.clone(), data).map_err(|e| format!("Failed to write: {}", e))?;println!("Wrote {} bytes", bytes_written);// Read datalet received_data = read(
app.clone(),
serial.clone(),
path.clone(),Some(1000u64),// timeoutSome(1024usize)// max bytes to read).map_err(|e| format!("Failed to read: {}", e))?;println!("Received: {}", received_data);// Configure port settingsset_baud_rate(app.clone(), serial.clone(), path.clone(),115200).map_err(|e| format!("Failed to set baud rate: {}", e))?;set_data_bits(app.clone(), serial.clone(), path.clone(),DataBits::Eight).map_err(|e| format!("Failed to set data bits: {}", e))?;set_flow_control(app.clone(), serial.clone(), path.clone(),FlowControl::None).map_err(|e| format!("Failed to set flow control: {}", e))?;set_parity(app.clone(), serial.clone(), path.clone(),Parity::None).map_err(|e| format!("Failed to set parity: {}", e))?;set_stop_bits(app.clone(), serial.clone(), path.clone(),StopBits::One).map_err(|e| format!("Failed to set stop bits: {}", e))?;// Set timeoutset_timeout(app.clone(), serial.clone(), path.clone(),1000u64).map_err(|e| format!("Failed to set timeout: {}", e))?;// Control signalswrite_request_to_send(app.clone(), serial.clone(), path.clone(),true).map_err(|e| format!("Failed to set RTS: {}", e))?;write_data_terminal_ready(app.clone(), serial.clone(), path.clone(),true).map_err(|e| format!("Failed to set DTR: {}", e))?;// Read control signalslet cts = read_clear_to_send(app.clone(), serial.clone(), path.clone()).map_err(|e| format!("Failed to read CTS: {}", e))?;println!("CTS: {}", cts);let dsr = read_data_set_ready(app.clone(), serial.clone(), path.clone()).map_err(|e| format!("Failed to read DSR: {}", e))?;println!("DSR: {}", dsr);// Buffer managementlet bytes_to_read = bytes_to_read(app.clone(), serial.clone(), path.clone()).map_err(|e| format!("Failed to get bytes to read: {}", e))?;println!("Bytes available to read: {}", bytes_to_read);let bytes_to_write = bytes_to_write(app.clone(), serial.clone(), path.clone()).map_err(|e| format!("Failed to get bytes to write: {}", e))?;println!("Bytes waiting to write: {}", bytes_to_write);// Clear buffersclear_buffer(app.clone(), serial.clone(), path.clone(),ClearBuffer::All).map_err(|e| format!("Failed to clear buffer: {}", e))?;// Break signalset_break(app.clone(), serial.clone(), path.clone()).map_err(|e| format!("Failed to set break: {}", e))?;clear_break(app.clone(), serial.clone(), path.clone()).map_err(|e| format!("Failed to clear break: {}", e))?;// Close the portclose(app, serial, path).map_err(|e| format!("Failed to close port: {}", e))?;Ok(())}use tauri_plugin_serialplugin::commands::{
available_ports, open, write, read, close, force_close, managed_ports, watch, unwatch
};use tauri_plugin_serialplugin::state::{DataBits,FlowControl,Parity,StopBits};use tauri::{AppHandle,State};use std::collections::HashMap;#[tauri::command]asyncfnadvanced_serial_example(app:AppHandle<tauri::Wry>,serial:State<'_, tauri_plugin_serialplugin::api::desktop::SerialPort<tauri::Wry>>) -> Result<(),String>{// Get available ports with error handlinglet ports = matchavailable_ports(app.clone(), serial.clone()){Ok(ports) => ports,Err(e) => {eprintln!("Failed to get available ports: {}", e);returnErr("No serial ports available".to_string());}};if ports.is_empty(){returnErr("No serial ports found".to_string());}// Use the first available portlet port_path = ports.keys().next().unwrap().clone();println!("Using port: {}", port_path);// Open port with full configurationlet open_result = open(
app.clone(),
serial.clone(),
port_path.clone(),9600u32,// baud rateSome(DataBits::Eight),Some(FlowControl::None),Some(Parity::None),Some(StopBits::One),Some(5000u64)// 5 second timeout);match open_result {Ok(_) => println!("Port opened successfully"),Err(e) => {eprintln!("Failed to open port: {}", e);returnErr(format!("Failed to open port {}: {}", port_path, e));}}// Poll read (for streaming use `watch` + Channel from the frontend)matchread(app.clone(), serial.clone(), port_path.clone(),Some(1000u64),Some(1024usize)){Ok(data) => println!("Read: {}", data),Err(e) => eprintln!("Read failed: {}", e),}let command = "AT\r\n".to_string();matchwrite(app.clone(), serial.clone(), port_path.clone(), command){Ok(bytes) => println!("Sent {} bytes", bytes),Err(e) => {eprintln!("Failed to write command: {}", e);returnErr(format!("Write failed: {}", e));}}// Read response with timeoutmatchread(
app.clone(),
serial.clone(),
port_path.clone(),Some(2000u64),// 2 second timeoutSome(512usize)// max 512 bytes){Ok(response) => println!("Response: {}", response),Err(e) => {eprintln!("Failed to read response: {}", e);returnErr(format!("Read failed: {}", e));}}// Get managed portslet managed_ports = matchmanaged_ports(app.clone(), serial.clone()){Ok(ports) => ports,Err(e) => {eprintln!("Failed to get managed ports: {}", e);Vec::new()}};println!("Managed ports: {:?}", managed_ports);// Clean uplet cleanup_result = close(app.clone(), serial.clone(), port_path.clone());match cleanup_result {Ok(_) => println!("Port closed successfully"),Err(e) => {eprintln!("Failed to close port: {}", e);// Try force closeifletErr(e2) = force_close(app, serial, port_path){eprintln!("Failed to force close port: {}", e2);}}}Ok(())}use tauri_plugin_serialplugin::commands::{open, write_binary, read_binary, close};use tauri_plugin_serialplugin::state::{DataBits,FlowControl,Parity,StopBits};use tauri::{AppHandle,State};#[tauri::command]asyncfnbinary_data_example(app:AppHandle<tauri::Wry>,serial:State<'_, tauri_plugin_serialplugin::api::desktop::SerialPort<tauri::Wry>>) -> Result<(),String>{let port_path = "COM1".to_string();// Open portopen(
app.clone(),
serial.clone(),
port_path.clone(),115200u32,Some(DataBits::Eight),Some(FlowControl::None),Some(Parity::None),Some(StopBits::One),Some(1000u64)).map_err(|e| format!("Failed to open port: {}", e))?;// Write binary datalet binary_data = vec![0x48,0x65,0x6C,0x6C,0x6F];// "Hello" in ASCIIlet bytes_written = write_binary(app.clone(), serial.clone(), port_path.clone(), binary_data).map_err(|e| format!("Failed to write binary data: {}", e))?;println!("Wrote {} bytes of binary data", bytes_written);// Read binary datalet received_data = read_binary(
app.clone(),
serial.clone(),
port_path.clone(),Some(1000u64),// timeoutSome(256usize)// max bytes).map_err(|e| format!("Failed to read binary data: {}", e))?;println!("Received {} bytes: {:?}", received_data.len(), received_data);// Close portclose(app, serial, port_path).map_err(|e| format!("Failed to close port: {}", e))?;Ok(())}You have two ways to use the plugin in Rust:
Import and use the command functions directly. These functions are documented in the docs.rs documentation:
use tauri_plugin_serialplugin::commands::{available_ports, open, write, read, close};use tauri::{AppHandle,State};#[tauri::command]asyncfnmy_serial_function(app:AppHandle<tauri::Wry>,serial:State<'_, tauri_plugin_serialplugin::api::desktop::SerialPort<tauri::Wry>>) -> Result<(),String>{// Use command functionslet ports = available_ports(app.clone(), serial.clone())?;open(app.clone(), serial.clone(),"COM1".to_string(),9600,None,None,None,None,None)?;// ... rest of your code}Use the SerialPort methods directly:
use tauri::State;use tauri_plugin_serialplugin::api::desktop::SerialPort;#[tauri::command]asyncfnmy_serial_function(serial:State<'_,SerialPort<tauri::Wry>>) -> Result<(),String>{// Use serial methods directlylet ports = serial.available_ports(false)?;// ... rest of your code}The plugin provides the following Rust types for configuration:
use tauri_plugin_serialplugin::state::{DataBits,// Five, Six, Seven, EightFlowControl,// None, Software, HardwareParity,// None, Odd, EvenStopBits,// One, TwoClearBuffer// Input, Output, All};Here are all the available command functions you can import and use. For detailed documentation with examples, see the docs.rs documentation:
use tauri_plugin_serialplugin::commands::{// Port discovery
available_ports,// Get list of available ports
managed_ports,// Get list of currently managed ports// Connection management
open,// Open a serial port
close,// Close a serial port
close_all,// Close all open ports
force_close,// Force close a port// Data transfer
write,// Write string data
write_binary,// Write binary data
read,// Read string data
read_binary,// Read binary data// Listening (streaming)
capabilities,// Runtime info
watch,// Stream events via Channel
unwatch,// Stop watch session
cancel_read,// Cancel poll read or active watch (shared stop channel)// Port configuration
set_baud_rate,// Set baud rate
set_data_bits,// Set data bits
set_flow_control,// Set flow control
set_parity,// Set parity
set_stop_bits,// Set stop bits
set_timeout,// Set timeout// Control signals
write_request_to_send,// Set RTS signal
write_data_terminal_ready,// Set DTR signal
read_clear_to_send,// Read CTS signal
read_data_set_ready,// Read DSR signal
read_ring_indicator,// Read RI signal
read_carrier_detect,// Read CD signal// Buffer management
bytes_to_read,// Get bytes available to read
bytes_to_write,// Get bytes waiting to write
clear_buffer,// Clear buffers// Break signal
set_break,// Start break signal
clear_break,// Stop break signal};All command functions follow this pattern:
pubfnfunction_name<R:Runtime>(app:AppHandle<R>,serial:State<'_,SerialPort<R>>,// ... additional parameters specific to the function) -> Result<ReturnType,Error>For example:
// Open portpubfnopen<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>,) -> Result<(),Error>// Write datapubfnwrite<R:Runtime>(app:AppHandle<R>,serial:State<'_,SerialPort<R>>,path:String,value:String,) -> Result<usize,Error>- "Failed to lock serialports mutex" - Error acquiring mutex lock when listing ports
- "Invalid response format" - Invalid response format from plugin
- "Plugin error: {error}" - Plugin execution error
- "Failed to acquire lock: {error}" - Error acquiring mutex lock
- "Port '{path}' not found" - Port does not exist
- "Serial port {path} is not open!" - Port is not open
- "Failed to open serial port: {error}" - Error opening port
- "Failed to clone serial port: {error}" - Error cloning port
- "Failed to set short timeout: {error}" - Error setting timeout
- "Failed to cancel serial port watch: {error}" - Error stopping watch thread
- "Failed to join thread: {error}" - Error waiting for thread completion
- "Failed to cancel serial port data reading: {error}" - Error canceling data reading
- "Failed to write data: {error}" - Error writing data
- "Failed to write binary data: {error}" - Error writing binary data
- "Failed to read data: {error}" - Error reading data
- "no data received within {timeout} ms" - Read timeout
- "Failed to set timeout: {error}" - Error setting timeout
- "Failed to set baud rate: {error}" - Error setting baud rate
- "Failed to set data bits: {error}" - Error setting data bits
- "Failed to set flow control: {error}" - Error setting flow control
- "Failed to set parity: {error}" - Error setting parity
- "Failed to set stop bits: {error}" - Error setting stop bits
- "Failed to set RTS: {error}" - Error setting RTS signal
- "Failed to set DTR: {error}" - Error setting DTR signal
- "Failed to read CTS: {error}" - Error reading CTS signal
- "Failed to read DSR: {error}" - Error reading DSR signal
- "Failed to read RI: {error}" - Error reading RI signal
- "Failed to read CD: {error}" - Error reading CD signal
- "Failed to set break: {error}" - Error setting break signal
- "Failed to clear break: {error}" - Error clearing break signal
- "Failed to clear buffer: {error}" - Error clearing buffer
- "Failed to get bytes to read: {error}" - Error getting bytes available to read
- "Failed to get bytes to write: {error}" - Error getting bytes waiting to write
Below is a list of all permissions the plugin supports. Granting or denying them allows fine-grained control over what your application can do with serial ports.
| Permission | Description |
|---|---|
serialplugin:allow-available-ports | Allows listing of available serial ports |
serialplugin:deny-available-ports | Denies listing of available serial ports |
serialplugin:allow-cancel-read | Allows canceling of read operations |
serialplugin:deny-cancel-read | Denies canceling of read operations |
serialplugin:allow-close | Allows closing of serial ports |
serialplugin:deny-close | Denies closing of serial ports |
serialplugin:allow-close-all | Allows closing of all open serial ports |
serialplugin:deny-close-all | Denies closing of all open serial ports |
serialplugin:allow-force-close | Allows forcefully closing of serial ports |
serialplugin:deny-force-close | Denies forcefully closing of serial ports |
serialplugin:allow-open | Allows opening of serial ports |
serialplugin:deny-open | Denies opening of serial ports |
serialplugin:allow-read | Allows reading data from serial ports |
serialplugin:deny-read | Denies reading data from serial ports |
serialplugin:allow-read-binary | Allows reading binary data from serial ports |
serialplugin:deny-read-binary | Denies reading binary data from serial ports |
serialplugin:allow-write | Allows writing data to serial ports |
serialplugin:deny-write | Denies writing data to serial ports |
serialplugin:allow-write-binary | Allows writing binary data to serial ports |
serialplugin:deny-write-binary | Denies writing binary data to serial ports |
serialplugin:allow-set-baud-rate | Allows changing the baud rate of serial ports |
serialplugin:deny-set-baud-rate | Denies changing the baud rate of serial ports |
serialplugin:allow-set-data-bits | Allows changing the data bits configuration |
serialplugin:deny-set-data-bits | Denies changing the data bits configuration |
serialplugin:allow-set-flow-control | Allows changing the flow control mode |
serialplugin:deny-set-flow-control | Denies changing the flow control mode |
serialplugin:allow-set-parity | Allows changing the parity checking mode |
serialplugin:deny-set-parity | Denies changing the parity checking mode |
serialplugin:allow-set-stop-bits | Allows changing the stop bits configuration |
serialplugin:deny-set-stop-bits | Denies changing the stop bits configuration |
serialplugin:allow-set-timeout | Allows changing the timeout duration |
serialplugin:deny-set-timeout | Denies changing the timeout duration |
serialplugin:allow-write-rts | Allows setting the RTS (Request To Send) control signal |
serialplugin:deny-write-rts | Denies setting the RTS control signal |
serialplugin:allow-write-dtr | Allows setting the DTR (Data Terminal Ready) control signal |
serialplugin:deny-write-dtr | Denies setting the DTR control signal |
serialplugin:allow-read-cts | Allows reading the CTS (Clear To Send) control signal state |
serialplugin:deny-read-cts | Denies reading the CTS control signal state |
serialplugin:allow-read-dsr | Allows reading the DSR (Data Set Ready) control signal state |
serialplugin:deny-read-dsr | Denies reading the DSR control signal state |
serialplugin:allow-read-ri | Allows reading the RI (Ring Indicator) control signal state |
serialplugin:deny-read-ri | Denies reading the RI control signal state |
serialplugin:allow-read-cd | Allows reading the CD (Carrier Detect) control signal state |
serialplugin:deny-read-cd | Denies reading the CD control signal state |
serialplugin:allow-bytes-to-read | Allows checking the number of bytes available to read |
serialplugin:deny-bytes-to-read | Denies checking the number of bytes available to read |
serialplugin:allow-bytes-to-write | Allows checking the number of bytes waiting to be written |
serialplugin:deny-bytes-to-write | Denies checking the number of bytes waiting to be written |
serialplugin:allow-clear-buffer | Allows clearing input/output buffers |
serialplugin:deny-clear-buffer | Denies clearing input/output buffers |
serialplugin:allow-set-break | Allows starting break signal transmission |
serialplugin:deny-set-break | Denies starting break signal transmission |
serialplugin:allow-clear-break | Allows stopping break signal transmission |
serialplugin:deny-clear-break | Denies stopping break signal transmission |
serialplugin:allow-capabilities | Allows reading runtime plugin capabilities |
serialplugin:deny-capabilities | Denies reading runtime plugin capabilities |
serialplugin:allow-watch | Allows streaming port data through a Tauri Channel |
serialplugin:deny-watch | Denies streaming port data through a Tauri Channel |
serialplugin:allow-unwatch | Allows stopping an active watch session |
serialplugin:deny-unwatch | Denies stopping an active watch session |
serialplugin:allow-set-log-level | Allows setting the global log level |
serialplugin:deny-set-log-level | Denies setting the global log level |
serialplugin:allow-get-log-level | Allows getting the current log level |
serialplugin:deny-get-log-level | Denies getting the current log level |
"permissions": [
"core:default",
"serialplugin:default",
"serialplugin:allow-available-ports",
"serialplugin:allow-cancel-read",
"serialplugin:allow-close",
"serialplugin:allow-close-all",
"serialplugin:allow-force-close",
"serialplugin:allow-open",
"serialplugin:allow-read",
"serialplugin:allow-write",
"serialplugin:allow-write-binary",
"serialplugin:allow-set-baud-rate",
"serialplugin:allow-set-data-bits",
"serialplugin:allow-set-flow-control",
"serialplugin:allow-set-parity",
"serialplugin:allow-set-stop-bits",
"serialplugin:allow-set-timeout",
"serialplugin:allow-write-rts",
"serialplugin:allow-write-dtr",
"serialplugin:allow-read-cts",
"serialplugin:allow-read-dsr",
"serialplugin:allow-read-ri",
"serialplugin:allow-read-cd",
"serialplugin:allow-bytes-to-read",
"serialplugin:allow-bytes-to-write",
"serialplugin:allow-clear-buffer",
"serialplugin:allow-set-break",
"serialplugin:allow-clear-break",
"serialplugin:allow-capabilities",
"serialplugin:allow-watch",
"serialplugin:allow-unwatch",
"serialplugin:allow-set-log-level",
"serialplugin:allow-get-log-level"
]Removed in 3.0.0:
available_ports_direct— useavailable_ports().
macOS duplicates:
serialport-rslists both/dev/cu.*(callout) and/dev/tty.*(dial-in) per device. Pass{ singlePortPerDevice: true }to keep one path per device (prefers/dev/cu.*, like Node.jsSerialPort.list()). Default returns all paths.
classSerialPort{/** * Lists all available serial ports on the system * @param options.macOS `singlePortPerDevice` — see note above * @returns {Promise<{[key: string]: PortInfo}>} Map of port names to port information * @example * const ports = await SerialPort.available_ports(); * const onePerDevice = await SerialPort.available_ports({ singlePortPerDevice: true }); * console.log(ports); */staticasyncavailable_ports(options?: AvailablePortsOptions): Promise<{[key: string]: PortInfo}>;/** * Subscribe to available-port hotplug (attach/detach). Sends an initial snapshot, * then `added` / `removed` events through a Tauri Channel. * @param handlers Callbacks for snapshot / added / removed * @param options `singlePortPerDevice`, `pollIntervalMs` (desktop default 2000) * @returns Handle with `unwatch()` to stop * @example * const handle = await SerialPort.watchAvailablePorts({ * onSnapshot: (ports) => console.log('ports', ports), * onAdded: (path, info) => console.log('plugged', path, info.type), * onRemoved: (path) => console.log('unplugged', path), * }, { singlePortPerDevice: true }); * // later: await handle.unwatch(); */staticasyncwatchAvailablePorts(handlers: WatchPortsHandlers,options?: WatchPortsOptions,): Promise<WatchHandle>;/** * @description Lists all managed serial ports (ports that are currently open and managed by the application). * @returns {Promise<string[]>} A promise that resolves to an array of port paths (names). */staticasyncmanaged_ports(): Promise<string[]>;}classSerialPort{/** * Opens the serial port with specified configuration * @returns {Promise<void>} * @throws {Error} If port is already open or invalid configuration * @example * const port = new SerialPort({ path: "COM1", baudRate: 9600 }); * await port.open(); */asyncopen(): Promise<void>;/** * Closes the serial port connection * @returns {Promise<void>} * @throws {Error} If port is not open * @example * await port.close(); */asyncclose(): Promise<void>;/** * Streams serial port events through a Tauri Channel. * @returns {Promise<WatchHandle>} Handle with `channelId` and `unwatch()` * @example * const handle = await port.watch({ * onData: (data) => console.log("Data:", data), * onError: (message) => console.warn("Non-fatal:", message), * onDisconnect: (reason) => console.log("Disconnected:", reason), * }); * await handle.unwatch(); */asyncwatch(handlers: WatchHandlers,options?: WatchOptions): Promise<WatchHandle>;/** * Runtime plugin info (transport, platform, version). * @example * const caps = await SerialPort.getCapabilities(); */staticgetCapabilities(): Promise<Capabilities>;/** * Forces a serial port to close regardless of its state * @param {string} path Port path to force close * @returns {Promise<void>} * @example * await SerialPort.forceClose("COM1"); */staticasyncforceClose(path: string): Promise<void>;/** * Closes all open serial port connections * @returns {Promise<void>} * @example * await SerialPort.closeAll(); */staticasynccloseAll(): Promise<void>;}classSerialPort{/** * Writes string data to the serial port * @param {string} data Data to write * @returns {Promise<number>} Number of bytes written * @throws {Error} If write fails or port is not open * @example * const bytesWritten = await port.write("Hello"); */asyncwrite(data: string): Promise<number>;/** * Reads data from the serial port * @param {ReadOptions} [options] Read options * @returns {Promise<string>} A promise that resolves to a string */asyncread(options?: ReadOptions): Promise<string>;/** * Reads binary data from the serial port * @param {ReadOptions} [options] Read options * @returns {Promise<Uint8Array>} A promise that resolves with binary data */asyncreadBinary(options?: ReadOptions): Promise<Uint8Array>;/** * Writes binary data to the serial port * @param {Uint8Array | number[]} data Binary data to write * @returns {Promise<number>} Number of bytes written * @throws {Error} If write fails or port is not open * @example * const data = new Uint8Array([0x01, 0x02, 0x03]); * const bytesWritten = await port.writeBinary(data); */asyncwriteBinary(data: Uint8Array|number[]): Promise<number>;}classSerialPort{/** * Sets the baud rate * @param {number} baudRate Speed in bits per second * @returns {Promise<void>} * @example * await port.setBaudRate(115200); */asyncsetBaudRate(baudRate: number): Promise<void>;/** * Sets the number of data bits * @param {DataBits} dataBits Number of bits per character (5-8) * @returns {Promise<void>} * @example * await port.setDataBits(DataBits.Eight); */asyncsetDataBits(dataBits: DataBits): Promise<void>;/** * Sets the flow control mode * @param {FlowControl} flowControl Flow control setting * @returns {Promise<void>} * @example * await port.setFlowControl(FlowControl.Hardware); */asyncsetFlowControl(flowControl: FlowControl): Promise<void>;/** * Sets the parity checking mode * @param {Parity} parity Parity checking mode * @returns {Promise<void>} * @example * await port.setParity(Parity.None); */asyncsetParity(parity: Parity): Promise<void>;/** * Sets the number of stop bits * @param {StopBits} stopBits Number of stop bits * @returns {Promise<void>} * @example * await port.setStopBits(StopBits.One); */asyncsetStopBits(stopBits: StopBits): Promise<void>;/** * Sets the timeout for read operations * @param {number} timeout Timeout value in milliseconds * @returns {Promise<void>} * @example * await port.setTimeout(1000); */asyncsetTimeout(timeout: number): Promise<void>;}classSerialPort{/** * Sets the RTS (Request to Send) signal * @param {boolean} level Signal level (true = high, false = low) * @returns {Promise<void>} * @example * await port.writeRequestToSend(true); */asyncwriteRequestToSend(level: boolean): Promise<void>;/** * Sets the DTR (Data Terminal Ready) signal * @param {boolean} level Signal level (true = high, false = low) * @returns {Promise<void>} * @example * await port.writeDataTerminalReady(true); */asyncwriteDataTerminalReady(level: boolean): Promise<void>;/** * Reads the CTS (Clear to Send) signal state * @returns {Promise<boolean>} Signal state * @example * const cts = await port.readClearToSend(); */asyncreadClearToSend(): Promise<boolean>;/** * Reads the DSR (Data Set Ready) signal state * @returns {Promise<boolean>} Signal state * @example * const dsr = await port.readDataSetReady(); */asyncreadDataSetReady(): Promise<boolean>;/** * Reads the RI (Ring Indicator) signal state * @returns {Promise<boolean>} Signal state * @example * const ri = await port.readRingIndicator(); */asyncreadRingIndicator(): Promise<boolean>;/** * Reads the CD (Carrier Detect) signal state * @returns {Promise<boolean>} Signal state * @example * const cd = await port.readCarrierDetect(); */asyncreadCarrierDetect(): Promise<boolean>;}classSerialPort{/** * Gets number of bytes available to read * @returns {Promise<number>} Number of bytes in read buffer * @example * const available = await port.bytesToRead(); */asyncbytesToRead(): Promise<number>;/** * Gets number of bytes waiting to be written * @returns {Promise<number>} Number of bytes in write buffer * @example * const pending = await port.bytesToWrite(); */asyncbytesToWrite(): Promise<number>;/** * Clears the specified buffer * @param {ClearBuffer} buffer Buffer to clear * @returns {Promise<void>} * @example * await port.clearBuffer(ClearBuffer.Input); */asyncclearBuffer(buffer: ClearBuffer): Promise<void>;/** * Sets the break signal * @returns {Promise<void>} * @example * await port.setBreak(); */asyncsetBreak(): Promise<void>;/** * Clears the break signal * @returns {Promise<void>} * @example * await port.clearBreak(); */asyncclearBreak(): Promise<void>;}classSerialPort{/** * Sets the global log level for the plugin * @param {LogLevel} level The log level to set (None, Error, Warn, Info, Debug) * @returns {Promise<void>} * @example * // Disable all logs in production * await SerialPort.setLogLevel(LogLevel.None); * * // Show only errors * await SerialPort.setLogLevel(LogLevel.Error); * * // Enable debug logs * await SerialPort.setLogLevel(LogLevel.Debug); */staticasyncsetLogLevel(level: LogLevel): Promise<void>;/** * Gets the current global log level * @returns {Promise<LogLevel>} A promise that resolves to the current log level * @example * const currentLevel = await SerialPort.getLogLevel(); * console.log("Current log level:", currentLevel); */staticasyncgetLogLevel(): Promise<LogLevel>;}// Available log levelsenumLogLevel{None="None",// No logging outputError="Error",// Only critical errorsWarn="Warn",// Errors and warningsInfo="Info",// Errors, warnings, and info (default)Debug="Debug"// All logging including debug information}classSerialPort{/** * Enables auto-reconnect functionality * @param {Object} options Auto-reconnect configuration options * @param {number} [options.interval=5000] Reconnection interval in milliseconds * @param {number | null} [options.maxAttempts=10] Maximum number of reconnection attempts (null for infinite) * @param {Function} [options.onReconnect] Callback function called on each reconnection attempt * @returns {void} * @example * port.enableAutoReconnect({ * interval: 3000, * maxAttempts: 5, * onReconnect: (success, attempt) => { * console.log(`Reconnect attempt ${attempt}: ${success ? 'success' : 'failed'}`); * } * }); */enableAutoReconnect(options?: {interval?: number;maxAttempts?: number|null;onReconnect?: (success: boolean,attempt: number)=>void;}): void;/** * Disables auto-reconnect functionality * @returns {void} * @example * await port.disableAutoReconnect(); */disableAutoReconnect(): void;/** * Gets auto-reconnect status and configuration * @returns {Object} Auto-reconnect information * @example * const info = port.getAutoReconnectInfo(); * console.log('Auto-reconnect enabled:', info.enabled); * console.log('Current attempts:', info.currentAttempts); */getAutoReconnectInfo(): {enabled: boolean;interval: number;maxAttempts: number|null;currentAttempts: number;hasCallback: boolean;};/** * Manually triggers a reconnection attempt * @returns {Promise<boolean>} A promise that resolves to true if reconnection was successful * @example * const success = await port.manualReconnect(); * if (success) { * console.log('Manual reconnection successful'); * } */asyncmanualReconnect(): Promise<boolean>;}constport=newSerialPort({path: "COM1",baudRate: 9600});awaitport.open();consthandle=awaitport.watch({onData: (data)=>{constsensorValue=parseFloat(String(data));console.log("Sensor reading:",sensorValue);},});// await handle.unwatch() when doneconstport=newSerialPort({path: "COM1",baudRate: 115200});awaitport.open();// Send commandconstcommand=newUint8Array([0x02,0x01,0x03]);awaitport.writeBinary(command);consthandle=awaitport.watch({onData: (data)=>{constresponse=datainstanceofUint8Array ? data : newUint8Array();console.log("Response:",response);},},{decode: false});constport=newSerialPort({path: "COM1",baudRate: 9600,dataBits: DataBits.Eight,stopBits: StopBits.One,parity: Parity.None});awaitport.open();functioncreateModbusRequest(address: number,length: number): Uint8Array{returnnewUint8Array([0x01,// Device ID0x03,// Function code: Read Holding Registersaddress>>8,address&0xFF,length>>8,length&0xFF]);}// Send Modbus requestconstrequest=createModbusRequest(0x1000,10);awaitport.writeBinary(request);constport=newSerialPort({path: "COM1",baudRate: 9600});awaitport.open();// Enable auto-reconnect: restores both open() and watch() after disconnect// Reconnection uses a fixed interval (options.interval); exponential backoff is not implemented.awaitport.enableAutoReconnect({interval: 3000,maxAttempts: 5,onReconnect: (success,attempt)=>{console.log(success ? `Reconnected on attempt ${attempt}` : `Attempt ${attempt} failed`);},});consthandle=awaitport.watch({onData: (data)=>console.log("Received data:",data),onDisconnect: ()=>console.log("Port disconnected — auto-reconnect will reopen and re-watch"),});// Manual reconnect also re-establishes watch when a session was active before disconnectconstsuccess=awaitport.manualReconnect();if(success){console.log("Manual reconnection successful");}// Check auto-reconnect statusconstinfo=port.getAutoReconnectInfo();console.log("Auto-reconnect enabled:",info.enabled);console.log("Current attempts:",info.currentAttempts);// Disable auto-reconnect when no longer neededawaitport.disableAutoReconnect();For modems and AT devices, use sendAt() / sendAtPhases() / sendSmsPdu() — native FIFO queue over exchange with line-framed AT completion (OK / ERROR / +CME ERROR as the final line).
| Mode | RX | Use when |
|---|---|---|
watch() | Streaming Channel events + optional onUrc | General I/O, live URC |
sendAt() | One structured response per command | AT modems, request/response scripts |
Capabilities (v3.0):
| Feature | Description |
|---|---|
AtCommandResult | Structured result: command, response, status, lines, solicitedBody, urcLines, raw |
| Native queue | Parallel exchange() / sendAt()wait in FIFO (no "Exchange already in progress") |
configureAtSession() | Session defaults: expectOk, stopOnError, appendCr, timeouts, resultFormat |
Default rxPrepare: 'drain' | Soft idle drain before each command; use purge only for recovery. On Android, drain uses the same hub drain() path as desktop (Rust reader → PortRxHub). |
expectOk, solicitedPrefixes | Per-command control via session + AtCommandOptions |
| Watch during AT | Watch stays active; live SerialEvent::Urc via watch({ onUrc }) |
| Vendor grammar | Auto solicitedPrefixes from command (^, #, $, %, *) |
resultFormat: 'numeric' | ATV0 line codes (0/3/4…) |
completionMode: 'atIntermediate' | CMGS > prompt and other intermediate lines |
sendAtPhases() / sendSmsPdu() | Multi-phase SMS (prompt → PDU → SEND OK) |
exchangeBinary() | Binary write + read-until (PDU + Ctrl+Z) |
| CMUX | enableMux(), openMuxChannel(dlci), virtual paths physical#dlci=N |
Migration (v3.0 major):
| Was | Now |
|---|---|
port.at.enqueue('AT') | port.sendAt('AT') |
port.at.enqueuePhases(...) | port.sendAtPhases(...) |
port.at.sendSmsPdu(...) | port.sendSmsPdu(...) |
port.at.cancel() | port.cancelAt() |
new SerialPort({ atSession }) | atSession still applies on open() via configureAtSession |
Parallel exchange() throws | exchange() awaits turn in native queue |
// Session defaults (optional — also via constructor `atSession`)awaitport.configureAtSession({expectOk: true,defaultTimeoutMs: 5000});awaitport.sendAt('AT^SYSCFG?');awaitport.sendAt('ATV0',{resultFormat: 'numeric'});awaitport.sendSmsPdu(pduLength,pduBytes);// CMUX: second logical channel on same USB portawaitport.enableMux({command: 'AT+CMUX=0,0,5,31,10,2'});constdataPort=awaitport.openMuxChannel(2);awaitdataPort.sendAt('AT');Low-level exchange / exchangeBinary return ExchangeResponse (also queued); cancel_exchange / cancelAt() cancels in-flight work and rejects queued waiters.
Android USB serial runs in Rust (android-usb-serial + nusb). Kotlin holds the UsbDeviceConnection fd; the plugin duplicates it and claims interfaces in native code. Add a device_filter.xml in your app for your VID/PID and grant USB permission at runtime.
If you previously added maven { url = uri("https://jitpack.io") } only for usb-serial-for-android, you can remove it.
See android/README.md and android/BUILD_INSTRUCTIONS.md.
Pull requests are welcome! Please read our contributing guidelines before you start.
git clone https://github.com/s00d/tauri-plugin-serialplugin.git
cd tauri-plugin-serialplugin
pnpm i
pnpm run build
pnpm run playgroundRun the full suite locally:
./scripts/verify-android-usb-migration.sh # full Android USB migration gate
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test
pnpm install && pnpm check && pnpm test&& pnpm build
cd android && ./gradlew testFor manual or integration testing without hardware, pair two PTY endpoints with socat:
socat -d -d pty,raw,echo=0 pty,raw,echo=0
# open the printed /dev/tty* path (or COM* on Windows with com0com)
pnpm playgroundUse watch(), exchange(), and sendAt() against that path; unplugging the peer exercises disconnect fail-fast and hub restart behavior.
- Rust:
src/tests/mock_serial.rs— scripted RX/TX mock used bycargo test - JS:
tests/*.test.ts— Jest mocks forinvoke/Channel - Android:
android/src/test/...— Robolectric forUsbFdBridge; Rust driver tests incrates/android-usb-serial(fake-transport)
Note:
bytesToWrite()returns 0 on Android (writes are synchronous over JNI). Desktop returns the driver queue depth when available.
- Windows port enumeration uses
wmicfor supplemental metadata on some builds. This path is not exercised in CI and may behave differently on Windows 11 ifwmicis unavailable or restricted.
If you find this plugin valuable and would like to support further development, feel free to donate via DonationAlerts. Any contribution is greatly appreciated!
This code is dual-licensed under MIT or Apache-2.0, where applicable, © 2019–2025 Tauri Programme within The Commons Conservancy.