I know, I know, there is TMUX - this was a fun experiment with AI.
Serial-over-TCP multiplexer for macOS and Linux. Lets multiple processes — AI test agents, interactive consoles, loggers — share a single physical serial port simultaneously. Servers advertise themselves via mDNS so clients discover them by friendly name across the local network without knowing the IP or port.
Copyright (c) 2026 DBML Group Inc. — MIT License
Author: Michael Loh (with Claude)
- Multi-client fan-out — one serial port, unlimited simultaneous readers. Every byte from the device is broadcast to all connected clients at once.
- LAN-wide discovery — mDNS advertisement means
mux_client -n "my-device"works from any machine on the network, no IP or port configuration needed. - Named presets — save device/baud pairs by friendly name; start a server instantly with
mux_server -n "my-device". - Multiple servers — run one server per serial port from the same interactive menu. Each runs as its own OS process (true isolation, no GIL contention).
- Device resilience — server survives USB unplug and embedded system reboots. TCP clients stay connected; serial port reconnects automatically when the device reappears.
- Password auth — optional per-server password. Clients must send the correct password during the
AUTHhandshake or are rejected. - Identity — username advertised in mDNS so other developers on the LAN can see who is running which server.
- Electron desktop app — native Mac app (
Mux Serial.app) with multi-server management, live terminal, client activity indicators, dark/light mode, and font size controls. - Interactive console —
mux_clientprovides a terminal session to the device. Exit withctrl+]. - Scripting API — import
MuxClientinto test agents for programmatic send/expect via the full pexpect API. - Timestamped logger —
mux_loggercaptures everything on the wire with timestamps, to stdout or a file.
- Python 3.8+ (tested on 3.14)
- macOS or Linux (Windows not supported — uses
select.poll()and Unix fd semantics) - Dependencies:
pip3 install pyserial pexpect zeroconf
# Install dependencies
pip3 install pyserial pexpect zeroconf
# Symlink scripts to /usr/local/bin (or anywhere on PATH)
ln -s "$(pwd)/mux_server.py" /usr/local/bin/mux_server
ln -s "$(pwd)/mux_client.py" /usr/local/bin/mux_client
ln -s "$(pwd)/mux_logger.py" /usr/local/bin/mux_logger
chmod +x mux_server.py mux_client.py mux_logger.pyOn Linux, add yourself to the dialout group for serial port access:
sudo usermod -aG dialout $USER # log out and back in to take effectInstall the .dmg from mux-serial-app/dist/. The app provides a GUI alternative to the Python server — start/stop multiple serial port servers, watch the live terminal, and manage presets. Settings and presets are shared with the Python CLI (~/.mux_serial_presets.json, ~/.mux_serial_settings.json).
Only mux_client is needed on machines that connect to a server running elsewhere:
pip3 install pexpect zeroconf
# copy mux_client.py to PATHmux_serverThe menu lists running servers (each as its own OS process) and saved presets. From here you can:
- Enter a preset number to start that server immediately
[n]— configure and start a new server (prompts for device, baud, name, password)[m]/[d]— modify or delete a saved preset[x]— stop a running server[q]— quit cleanly (stops all running servers)
# Start instantly from a saved preset
mux_server -n "my-device"
# Override a preset value (offers to update the saved preset)
mux_server -n "my-device" -b 460800
# Fully headless — no prompts, no preset interaction
mux_server -d /dev/cu.usbserial-1420 -b 115200 -n "my-device"
# With password and username
mux_server -d /dev/cu.usbserial-1420 -b 115200 -n "my-device" -P secret -U alice
# All options
mux_server [-d DEVICE] [-b BAUD] [-n NAME] [-P PASSWORD] [-U USERNAME] [-p TCP_PORT] [-H BIND_ADDR]Multiple serial ports on one machine: Start the server multiple times in separate terminals (each invocation is its own OS process), or use the interactive menu which manages multiple processes from a single session.
mux_client # browse mDNS, pick from list
mux_client -n "my-device" # connect by friendly name (any machine on LAN)
mux_client -H 192.168.1.10 -p 5000 # direct connect, skip mDNS
mux_client -U alice # identify yourself as aliceExit with ctrl+].
from mux_client import MuxClient
# Connect by mDNS friendly name (works across the LAN)
c = MuxClient.from_name("my-device")
c.connect()
# Full pexpect API via c.term
c.term.sendline("status")
c.term.expect("OK", timeout=5)
print(c.term.before)
c.close()
# Or connect directly by host/port
c = MuxClient(host="192.168.1.10", port=5000)
c.connect()mux_logger -H 192.168.1.10 -p 5000 # stdout with timestamps
mux_logger -H 192.168.1.10 -p 5000 -f run.log # also write to file
mux_logger -H 192.168.1.10 -p 5000 -l # line-buffered mode~/.mux_serial_settings.json stores your default username (shared between Python CLI and Electron app):
{ "username": "alice", "theme": "dark", "fontSize": 12 }~/.mux_serial_presets.json stores named device/baud configurations (shared between Python CLI and Electron app):
{ "presets": [{ "name": "my-device", "device": "/dev/cu.usbserial-1420", "baud": 115200 }] }Physical device (/dev/cu.* or /dev/ttyACM*)
│
│ pyserial (exclusive open)
│
mux_server ──── mDNS (_mux-serial._tcp.local.)
│ TXT: name, device, baud, user, auth
│ TCP (OS-assigned port, 0.0.0.0)
│ AUTH handshake: AUTH <user> [<password>]\r\n → OK\r\n / ERR\r\n
├─── mux_client (interactive console or scripting API)
├─── mux_client (second agent on same or different machine)
├─── Mux Serial.app (Electron desktop terminal)
└─── mux_logger (timestamped capture)
All reads from the serial device are broadcast to every connected client simultaneously. All writes from any client are forwarded to the device immediately. The server does not arbitrate concurrent writes — agents that need exclusive access must coordinate among themselves.
- No write arbitration — two clients writing simultaneously may interleave bytes. Planned: in-band token/lock protocol (
\x00LOCK\n/\x00UNLOCK\n). - mux_logger does not yet support mDNS discovery (
-n name); connect by host/port directly. - Client reconnect — if the server process restarts, connected clients exit and must be restarted manually. Planned: auto-reconnect with backoff.
MIT License — see LICENSE.
Copyright (c) 2026 DBML Group Inc.
Author: Michael Loh (with Claude)