Platforms
Serial over TCP
tcp://host:port paths to a TCP serial bridge on every platform.
Any path of the form tcp://host:port talks raw bytes to a TCP serial bridge:
ser2net (raw mode), ESP-Link, Moxa/USR device servers, or socat:
socat TCP-LISTEN:4000,reuseaddr,fork FILE:/dev/ttyUSB0,raw,echo=0,b115200
const port = new SerialPort({ path: "tcp://192.168.1.50:4000", baudRate: 115200 });
await port.open();
await port.watch({ onData: console.log });
- When the bridge closes the connection you get
onDisconnect, like an unplug. TCP keepalive is on (first probe after 10 s idle, then every 5 s; 3 probes on Linux, Android, macOS and iOS, the system's 10 on Windows), so a bridge that vanished without closing (power cut, Wi-Fi drop) also ends inonDisconnectafter about 25 s of silence (about 60 s on Windows) instead of the port timing out forever. If the system rejects a keepalive option the port still opens (a warning is logged), just without that detection. - Baud rate, parity and modem lines are kept locally and not sent; configure the
serial side on the bridge (RFC 2217 is not implemented). The modem-line setters
succeed and the getters return fixed values (CTS/DSR/DCD on, RI off). From Rust,
SerialManager::supports_modem_lines(path)isfalsefortcp://andble://paths (and CMUX channels…#dlci=N, where line calls fail), so code that needs real RTS/CTS can refuse them.
Restricting which bridges the webview may open
By default any tcp://host:port can be opened. To limit what the frontend can reach,
list the allowed addresses in tauri.conf.json:
{
"plugins": {
"serialplugin-config": {
"tcpAllowlist": ["192.168.1.50:4000", "bridge.local:23"]
}
}
}
- With the key set,
open()from JavaScript fails for atcp://address not in the list; an empty list blocks everytcp://. Local andble://ports are not affected. - The whole
host:porttext aftertcp://is compared with each entry as written, ignoring ASCII case only. Nothing is normalized, so10.0.0.5:04000,[::1]:23against an expanded IPv6 form,name.:23(trailing dot),user@host:23or extra whitespace are refused unless listed exactly: list the strings your app opens. A listed DNS name trusts the resolver: every address it returns is tried. - Rust code using
SerialManager/api::SerialPortdirectly is not restricted. - The key is
serialplugin-config, notserialplugin:plugins.serialpluginmust stay unset (the plugin takes no typed config there in 4.x). A malformedtcpAllowlistfails app start-up rather than allowing everything. - Use one client per bridge. A serial line has a single reader: with a forking
bridge and two clients, each reply goes to whichever connection reads first.
ser2netallows one connection per port by default.
The example app on iOS and Android, talking to a Quectel M26 modem through
socat on the host:

