Skip to content

Latest commit

History

295 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

npm versionCrates.ioDocumentationGitHub issuesGitHub starsDonate

Tauri Plugin — SerialPort

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 / disconnected were removed. Use watch() and SerialPort.getCapabilities(). See Migrating to v3 below.


Migrating to v3

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();
v2v3
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 / behaviorv2v3
AtCommandResult.raw / ExchangeResponse.rawnumber[]Uint8Array
AtCommandResult.timedOutliteral falseboolean (timeouts surfaced by native layer)
open()voidreturns canonical path (Android re-keys to UsbPath.sessionKey; desktop echoes input)
cancel_readalso detached watch on some buildsdoes not unwatch — use close() or handle.unwatch()
TX queue after errorport could stay halted until reopennext exchange / sendAt job runs normally (stopOnError only stops remaining phases in one sendAtPhases batch)
Early RX before exchangeoften lost or raced with waitnative hub keeps idle bytes; exchange replays them via take_idle_bytes after write (no extra line response needed)

Watch events (SerialEvent)

kindMeaningPort stays open?
dataIncoming bytes (decoded to string in JS when decode !== false)Yes
errorNon-fatal notification (e.g. emitter glitch); watch continuesYes
disconnectFatal end of stream (unplug, IO manager stopped); triggers auto-reconnect if enabledNo (isOpen = false)

Watch options

OptionDesktopAndroid
timeoutBatch coalescing window (ms)Coalescing / flush hint where supported
serialDataFlushIntervalMsPreferred batch interval; falls back to timeoutBufferedEmitter flush (10–2000 ms)
sizeRead chunk size per syscallReserved
decodeJS-only: TextDecoder on onDataJS-only

Table of Contents

  1. Installation
  2. Basic Usage
  3. TypeScript Support
  4. Log Level Control
  5. Rust Usage
  6. Permissions
  7. 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
  8. Common Use Cases
  9. Android Setup
  10. Contributing
  11. Development Setup
  12. Testing
  13. Partners
  14. License

Installation

Prerequisites

  • Rust version 1.70 or higher
  • Tauri 2.0 or higher
  • Node.js and an npm-compatible package manager (npm, yarn, pnpm)

Automatic Installation (Recommended)

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 serialplugin

Manual Installation

Backend (Rust)

Add the plugin using cargo:

cd ./src-tauri
cargo add tauri-plugin-serialplugin

Frontend (JavaScript/TypeScript)

Install the JavaScript API:

npm install tauri-plugin-serialplugin-api
# or
pnpm add tauri-plugin-serialplugin-api

Android

USB 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.


Basic Usage

  1. 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");}
  2. Configure Permissions

    // src-tauri/capabilities/default.json
    {
    "$schema": "../gen/schemas/desktop-schema.json",
    "identifier": "default",
    "description": "Capability for the main window",
    "windows": ["main"],
    "permissions": [
    "core:default",
    "serialplugin:default"
    ]
    }
  3. 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();
  4. 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();

TypeScript Support

This plugin provides full TypeScript support with comprehensive type definitions. All methods, interfaces, and enums are properly typed for better development experience.

Available Types

import{SerialPort,DataBits,FlowControl,Parity,StopBits,ClearBuffer,PortInfo,SerialportOptions,ReadOptions}from"tauri-plugin-serialplugin-api";

Type Definitions

  • SerialPort - Main class for serial port operations
  • DataBits - Enum: Five, Six, Seven, Eight
  • FlowControl - Enum: None, Software, Hardware
  • Parity - Enum: None, Odd, Even
  • StopBits - Enum: One, Two
  • ClearBuffer - Enum: Input, Output, All
  • PortInfo - Interface for port information
  • SerialportOptions - Interface for port configuration
  • ReadOptions - Interface for read operation options

Configuration Example with Types

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);

Control Signals with Types

// 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();

Buffer Management with Types

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();

Log Level Control

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.

TypeScript/JavaScript Usage

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);

Rust Usage

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())}

Log Levels

  • None - No logging output (recommended for production)
  • Error - Only critical errors
  • Warn - Errors and warnings
  • Info - Errors, warnings, and general information (default)
  • Debug - All logging including debug information (for development)

Common Use Cases

Production Environment

// 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);

Development with Debugging

// Enable detailed logging for troubleshootingawaitSerialPort.setLogLevel(LogLevel.Debug);constport=newSerialPort({path: "COM1",baudRate: 9600});awaitport.open();// See all internal events and state changes

Conditional Logging

// Set log level based on environmentconstisDevelopment=import.meta.env.DEV;awaitSerialPort.setLogLevel(isDevelopment ? LogLevel.Debug : LogLevel.Error);

Rust Usage

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:

Using Commands Directly

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(())}

Advanced Rust Example with Error Handling

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(())}

Binary Data Handling in Rust

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(())}

Using Commands vs Direct API

You have two ways to use the plugin in Rust:

Option 1: Using Commands (Recommended)

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}

Option 2: Using Direct API

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}

Available Rust Types

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};

Complete Command Functions Reference

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};

Command Function Signatures

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>

Error Messages

Port Discovery

  • "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

Port Management

  • "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

Data Operations

  • "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

Port Configuration

  • "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

Control Signals

  • "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

Buffer Management

  • "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

Permissions

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.

PermissionDescription
serialplugin:allow-available-portsAllows listing of available serial ports
serialplugin:deny-available-portsDenies listing of available serial ports
serialplugin:allow-cancel-readAllows canceling of read operations
serialplugin:deny-cancel-readDenies canceling of read operations
serialplugin:allow-closeAllows closing of serial ports
serialplugin:deny-closeDenies closing of serial ports
serialplugin:allow-close-allAllows closing of all open serial ports
serialplugin:deny-close-allDenies closing of all open serial ports
serialplugin:allow-force-closeAllows forcefully closing of serial ports
serialplugin:deny-force-closeDenies forcefully closing of serial ports
serialplugin:allow-openAllows opening of serial ports
serialplugin:deny-openDenies opening of serial ports
serialplugin:allow-readAllows reading data from serial ports
serialplugin:deny-readDenies reading data from serial ports
serialplugin:allow-read-binaryAllows reading binary data from serial ports
serialplugin:deny-read-binaryDenies reading binary data from serial ports
serialplugin:allow-writeAllows writing data to serial ports
serialplugin:deny-writeDenies writing data to serial ports
serialplugin:allow-write-binaryAllows writing binary data to serial ports
serialplugin:deny-write-binaryDenies writing binary data to serial ports
serialplugin:allow-set-baud-rateAllows changing the baud rate of serial ports
serialplugin:deny-set-baud-rateDenies changing the baud rate of serial ports
serialplugin:allow-set-data-bitsAllows changing the data bits configuration
serialplugin:deny-set-data-bitsDenies changing the data bits configuration
serialplugin:allow-set-flow-controlAllows changing the flow control mode
serialplugin:deny-set-flow-controlDenies changing the flow control mode
serialplugin:allow-set-parityAllows changing the parity checking mode
serialplugin:deny-set-parityDenies changing the parity checking mode
serialplugin:allow-set-stop-bitsAllows changing the stop bits configuration
serialplugin:deny-set-stop-bitsDenies changing the stop bits configuration
serialplugin:allow-set-timeoutAllows changing the timeout duration
serialplugin:deny-set-timeoutDenies changing the timeout duration
serialplugin:allow-write-rtsAllows setting the RTS (Request To Send) control signal
serialplugin:deny-write-rtsDenies setting the RTS control signal
serialplugin:allow-write-dtrAllows setting the DTR (Data Terminal Ready) control signal
serialplugin:deny-write-dtrDenies setting the DTR control signal
serialplugin:allow-read-ctsAllows reading the CTS (Clear To Send) control signal state
serialplugin:deny-read-ctsDenies reading the CTS control signal state
serialplugin:allow-read-dsrAllows reading the DSR (Data Set Ready) control signal state
serialplugin:deny-read-dsrDenies reading the DSR control signal state
serialplugin:allow-read-riAllows reading the RI (Ring Indicator) control signal state
serialplugin:deny-read-riDenies reading the RI control signal state
serialplugin:allow-read-cdAllows reading the CD (Carrier Detect) control signal state
serialplugin:deny-read-cdDenies reading the CD control signal state
serialplugin:allow-bytes-to-readAllows checking the number of bytes available to read
serialplugin:deny-bytes-to-readDenies checking the number of bytes available to read
serialplugin:allow-bytes-to-writeAllows checking the number of bytes waiting to be written
serialplugin:deny-bytes-to-writeDenies checking the number of bytes waiting to be written
serialplugin:allow-clear-bufferAllows clearing input/output buffers
serialplugin:deny-clear-bufferDenies clearing input/output buffers
serialplugin:allow-set-breakAllows starting break signal transmission
serialplugin:deny-set-breakDenies starting break signal transmission
serialplugin:allow-clear-breakAllows stopping break signal transmission
serialplugin:deny-clear-breakDenies stopping break signal transmission
serialplugin:allow-capabilitiesAllows reading runtime plugin capabilities
serialplugin:deny-capabilitiesDenies reading runtime plugin capabilities
serialplugin:allow-watchAllows streaming port data through a Tauri Channel
serialplugin:deny-watchDenies streaming port data through a Tauri Channel
serialplugin:allow-unwatchAllows stopping an active watch session
serialplugin:deny-unwatchDenies stopping an active watch session
serialplugin:allow-set-log-levelAllows setting the global log level
serialplugin:deny-set-log-levelDenies setting the global log level
serialplugin:allow-get-log-levelAllows getting the current log level
serialplugin:deny-get-log-levelDenies getting the current log level

Granting All Permissions (Example)

"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"
]

API Reference

Port Discovery

Removed in 3.0.0:available_ports_direct — use available_ports().

macOS duplicates:serialport-rs lists 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.js SerialPort.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[]>;}

Connection Management

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>;}

Data Transfer

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>;}

Port Configuration

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>;}

Control Signals

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>;}

Buffer Management

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>;}

Log Control

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}

Auto-Reconnect Management

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>;}

Common Use Cases

Reading Sensor Data

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 done

Binary Protocol Communication

constport=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});

Modbus Communication

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);

Auto-Reconnect for Reliable Communication

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();

AT commands (native FIFO queue)

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).

ModeRXUse when
watch()Streaming Channel events + optional onUrcGeneral I/O, live URC
sendAt()One structured response per commandAT modems, request/response scripts

Capabilities (v3.0):

FeatureDescription
AtCommandResultStructured result: command, response, status, lines, solicitedBody, urcLines, raw
Native queueParallel 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, solicitedPrefixesPer-command control via session + AtCommandOptions
Watch during ATWatch stays active; live SerialEvent::Urc via watch({ onUrc })
Vendor grammarAuto 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)
CMUXenableMux(), openMuxChannel(dlci), virtual paths physical#dlci=N

Migration (v3.0 major):

WasNow
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() throwsexchange() 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 Setup

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.


Contributing

Pull requests are welcome! Please read our contributing guidelines before you start.


Development Setup

git clone https://github.com/s00d/tauri-plugin-serialplugin.git
cd tauri-plugin-serialplugin
pnpm i
pnpm run build
pnpm run playground

Testing

Run 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 test

Virtual serial port (desktop integration)

For 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 playground

Use watch(), exchange(), and sendAt() against that path; unplugging the peer exercises disconnect fail-fast and hub restart behavior.

Unit tests (no hardware)

  • Rust:src/tests/mock_serial.rs — scripted RX/TX mock used by cargo test
  • JS:tests/*.test.ts — Jest mocks for invoke / Channel
  • Android:android/src/test/... — Robolectric for UsbFdBridge; Rust driver tests in crates/android-usb-serial (fake-transport)

Note:bytesToWrite() returns 0 on Android (writes are synchronous over JNI). Desktop returns the driver queue depth when available.

Known limitations

  • Windows port enumeration uses wmic for supplemental metadata on some builds. This path is not exercised in CI and may behave differently on Windows 11 if wmic is unavailable or restricted.

Partners

If you find this plugin valuable and would like to support further development, feel free to donate via DonationAlerts. Any contribution is greatly appreciated!


License

This code is dual-licensed under MIT or Apache-2.0, where applicable, © 2019–2025 Tauri Programme within The Commons Conservancy.

About

Resources

Stars

178 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages