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 in onDisconnect after 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) is false for tcp:// and ble:// 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 a tcp:// address not in the list; an empty list blocks every tcp://. Local and ble:// ports are not affected.
  • The whole host:port text after tcp:// is compared with each entry as written, ignoring ASCII case only. Nothing is normalized, so 10.0.0.5:04000, [::1]:23 against an expanded IPv6 form, name.:23 (trailing dot), user@host:23 or 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::SerialPort directly is not restricted.
  • The key is serialplugin-config, not serialplugin: plugins.serialplugin must stay unset (the plugin takes no typed config there in 4.x). A malformed tcpAllowlist fails 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. ser2net allows one connection per port by default.

The example app on iOS and Android, talking to a Quectel M26 modem through socat on the host: