smon
Minimalistic TUI serial monitor.
Install
cargo install smon
Updating
smon update
This downloads the newest release built for your platform, checks it against the
published SHA256SUMS, runs it once to confirm it works, and only then swaps the
installed binary. Name a version to install that exact one, for example
smon update v0.1.2, which also lets you go back to an older release. Add
--from-source to build with cargo instead of downloading.
Replacing the file on disk changes nothing for an smon that is already running,
so the update finishes by telling each one on this machine to stand down. A
daemon comes back on the new binary. A TUI just exits, so open sessions end.
You are asked first, unless you pass --yes.
Usage
smon
smon lists what you can attach to and lets you pick with an fzf-style filter.
That list is the consoles a running smon already holds, followed by the serial
ports on this machine. Type to filter, arrow keys to move, Enter to select. The list refreshes
about once a second. A port already opened by another program or another smon is
shown dimmed and marked busy, and cannot be picked. Busy detection currently
works on Windows. After you pick a port smon asks for a baud rate and connects.
If you type a path that does not match any detected port, Enter accepts it as a raw device path. This is useful for virtual PTYs and adapters that do not enumerate.
Run smon --help for the full flag list.
Sending and receiving
The bottom box is an input line. Type a command and press Enter to send it
followed by the line ending. Device output scrolls in the pane above, with a
scrollbar when there is more than one screen. Your sent lines are echoed in the
pane in cyan with a > prefix.
- Enter sends the current line plus the line ending.
- Up and Down recall previously sent commands.
- The mouse wheel scrolls the output history, three lines per notch.
- Tab, or Right at the end of the line, accepts the ghost autocomplete suggestion.
- Ctrl key combos such as Ctrl+C pass straight through to the device.
- Ctrl+Q quits.
Scrolling
The wheel scrolls the output pane. While you are scrolled up the view stays on what you are reading, new output does not drag it away. Scrolling up stops at the oldest full screen, and the scrollbar shows where you are. Scrolling back down to the bottom pins the view to live output again, and sending a command with Enter snaps it there too.
smon captures the mouse to receive wheel events, which also means the terminal's own text selection needs its usual override while smon runs, holding Shift while dragging in most terminals. Text selection without Shift is on the roadmap.
If the device disappears mid-session, for example it reboots or the adapter is replugged, smon keeps the scrollback, marks the session as disconnected in the title, and reconnects on its own as soon as the port is back.
Autocomplete
Sent commands are saved and offered back as you type. The best fuzzy match from your history is shown dimmed at the right of the input box with a Tab hint. Press Tab, or Right at the end of the line, to accept it. The history is global, stored in the config file, de-duplicated, and capped at the 200 most recent commands.
Remembered baud
The baud rate is remembered per port and preselected the next time you open that port. It is stored in the config file. Pressing Esc on the baud picker returns to port selection.
Session logs
Every console is written to a log file in real time. Each entry has a timestamp
and a direction marker: incoming bytes are tagged RX, lines you send are tagged
TX. Control bytes are escaped so the file stays readable plain text. The file
name holds the console and the start time, for example
smon-COM3-20260629-143205.log.
A log is a sequence of segments. A new one starts when the date changes, and a
client can start one at any time with log_roll, which returns the path. A run
that does that first can read its own output back from exactly that file instead
of picking its own lines out of a whole day. Segments older than
log_retention_days are deleted, 30 days by default.
Logs are stored in:
$XDG_STATE_HOME/smon/logs/on Linux and macOS, or~/.local/state/smon/logs/%LOCALAPPDATA%\smon\logs\on Windows
While a session is running its log file is held open, so on Windows the size and
last write time shown by a directory listing are stale. Windows does not flush
them to the directory entry until the file is closed. dir, ls and
Get-ChildItem can report the active log as 0 bytes or with an old timestamp
even while bytes are being written to it. Do not decide a log is empty or
unchanged from its listed size or time. Read the file contents.
Config file
The baud per port and the command history live in config.json, found in:
$XDG_CONFIG_HOME/smon/or~/.config/smon/on Linux and macOS%APPDATA%\smon\on Windows
Daemon
A daemon owns its serial ports and never lets go, so the consoles are up from boot with nobody logged in, everything the devices say is captured whether or not anyone is watching, and a monitor that dies does not take the port with it.
smon daemon --config /etc/smon/daemon.toml
The config lists the consoles. Only device is required:
= "127.0.0.1:4123"
= 30
[[]]
= "/dev/serial/by-id/usb-FTDI_TTL232R-3V3_FTCBUHQA-if00-port0"
= "left"
= 115200
= 512
= 4224
Point device at a /dev/serial/by-id/... path rather than /dev/ttyUSB0. The
ttyUSB numbers are handed out in probe order, so with two adapters a replug can
swap them and a label would then name the wrong device.
label is a name for the console and nothing more. smon attaches no meaning to
it, it just lets a client say left instead of the whole path.
With no --config the first of $SMON_CONFIG, <config dir>/smon/daemon.toml
and /etc/smon/daemon.toml is used.
Attaching
Plain smon finds a daemon on this machine and offers its consoles. Picking one
attaches over a websocket rather than opening the device, so it works while the
daemon holds the port, and several people can watch the same console at once and
see each other's input.
Picking a serial port the daemon does not hold hands it to the daemon first, so it stays up and keeps being logged after you quit the viewer. With no daemon running at all, the port is opened here and everything behaves as it always did.
--host <ssh target> attaches to a daemon on another machine:
smon --host pi
The daemon binds loopback only and is never put on a network. --host opens an
ssh tunnel itself and talks through that, so it inherits whatever already guards
ssh to that host and opens nothing new.
Raw bridge
A console with a bridge_port is also offered as a plain byte stream on that
loopback port, which is what pyserial reaches with socket://127.0.0.1:4224. A
program that would otherwise need the device node can use that instead, so
nothing has to be stopped first, and what it says to the device is recorded in
the console log like any other input.
For anything that truly needs the device node, console_release makes smon let
go and console_hold takes it back. See docs/mcp.md.
The line ending is chosen once at launch with --eol and defaults to crlf:
smon --eol crlf # \r\n, the default
smon --eol cr # \r
smon --eol lf # \n
smon --eol none # send nothing extra
MCP server
smon also serves a small Model Context Protocol
endpoint, so an agent or any MCP client can drive the serial console the same way
you can at the TUI. It exposes tools such as serial_send, serial_read, and
serial_expect.
It is always on and listens on http://127.0.0.1:4123/mcp over Streamable HTTP,
loopback only. Change the bind with --mcp:
smon --mcp 127.0.0.1:5000
Every tool takes an optional console. With one console open it can be left
out, with several it is required.
Driving it from a shell
The smon binary is its own client, so no curl and no MCP session are needed.
smon list prints every console a running smon owns. smon call <tool> [json]
calls one tool and prints the result. Both take --host to reach another
machine over ssh.
smon list
smon call console_list
smon call serial_send '{"console":"left","text":"version"}'
smon call serial_status --host pi
See docs/mcp.md for the tool list and how to connect a client.
Testing with a fake device
A Makefile target spawns a virtual serial pair via socat so you can try smon without real hardware:
make device
This symlinks one end at /tmp/smon-fake and runs the other end interactively in your terminal. In another terminal, run cargo run, type /tmp/smon-fake at the port picker. Lines you type in the make device terminal show up in smon, and lines you send from smon appear there. Ctrl+C stops the device.
Requires socat (brew install socat on macOS).
WSL2
On WSL2 the Windows COM ports are not directly visible. Forward the USB adapter into WSL with usbipd-win and smon will list attachable devices in the port picker and attach them for you. See docs/wsl.md for the full setup.
Cutting a release
gh workflow run Release -R VladasZ/smon -f bump=patch
bump takes patch, minor or major. The workflow raises the version in
Cargo.toml, commits it as release vX.Y.Z, tags and pushes, builds the binary
for five targets, and publishes a GitHub release carrying those archives and a
SHA256SUMS file. smon update reads exactly those assets, so a release is what
makes the new version reachable.
A green workflow is not the whole release. Publishing to crates.io is left out of CI on purpose, because the token belongs on your machine and not in a runner. So when the run finishes:
git pull --rebase
cargo publish
Only after that does cargo install smon give people the new version. Skipping it
leaves crates.io behind while the GitHub release looks finished.
License
Dual-licensed under MIT or Apache-2.0.