Your USB serial devices, in a browser tab — from anywhere on your network.
Plug an Arduino, ESP32, Pi Pico, or any USB‑to‑serial adapter into a Linux box, point Serial Monitor at it, and watch every line stream live in your browser. Run a dozen boards at once, each color‑coded. Drop into a full interactive terminal to talk back to a device. No more crawling under the desk to reach the one machine the cable is plugged into.
It's a single static Go binary with the web UI baked in. Drop it on a Raspberry Pi, run it in Docker, or deploy it to Kubernetes — then forget about it.
- Watch every board at once. One scrollback view, all your ports merged and color‑coded, with per‑port filters so you can mute the noisy one.
- Talk back. A real xterm.js terminal per port — type into a bootloader prompt, poke a CLI, send AT commands. Bytes flow both ways, unbuffered, so prompts like
login:and$show up the instant the device emits them. - Configure from the couch. Add devices, set baud/parity/stop bits, rename ports, pick colors — all from the Configuration tab. Hit Scan for devices to enumerate what's plugged in. Changes are written back to
config.yaml, so the UI is your config editor. - Never miss a reconnect. Ports auto‑reconnect every 5s. Unplug a board, plug it back in, and the stream just resumes.
- Grafana‑ready logs. Every line is also emitted to stdout as structured JSON (
slog), perfect for scraping into Loki / Alloy. There's an example config to get you started. - One binary, no dependencies. Pure Go (
CGO_ENABLED=0), web UI embedded viago:embed. Copy it to a Pi and run it. Ships for linux/amd64 and linux/arm64.
The Live Stream tab (above) merges every enabled port into a single, filterable, color‑coded feed — great for keeping an eye on a whole bench at once.
The Terminal tab gives each port its own interactive console. Same WebSocket plumbing, but raw and bidirectional:
Got Docker and a device at /dev/ttyUSB0? You're 30 seconds away:
docker run -d --name serialmonitor \
-p 8080:8080 \
--device /dev/ttyUSB0 \
-v "$(pwd)/config.yaml:/data/config.yaml" \
dsmithson/serialmonitor:latestOpen http://localhost:8080, click Configuration → Scan for devices, add /dev/ttyUSB0, set your baud rate, enable it — and watch it stream.
Serial ports usually require membership in the
dialoutgroup, or--privileged/ mapping/devfor broad access. The image runs as a non‑rootdialoutuser by default.
Pick whatever fits how you run things.
Grab the latest release for your architecture — no toolchain required:
# arm64 (Raspberry Pi 3/4/5, etc.) — use -linux-amd64 for x86_64
curl -L -o serialmonitor \
https://github.com/dsmithson/serialmonitor/releases/latest/download/serialmonitor-linux-arm64
chmod +x serialmonitor
./serialmonitor --config config.yamlChecksums are published alongside each release (checksums.txt). Releases include both serialmonitor-linux-amd64 and serialmonitor-linux-arm64.
The docker-compose.yml in this repo is a ready‑made starting point — it maps a device, mounts your config, and sets up log rotation:
docker compose up -dEdit the devices: list to match what's plugged in (or switch to privileged: true for full host access during development). The container reads /data/config.yaml and writes UI edits back to it, so mount that path on a volume to keep your setup across restarts.
Serial adapters are physically attached to one node, so the chart pins the pod there and mounts the host device paths. Config lives on a PVC so your UI edits survive restarts.
helm install serialmonitor ./helm/serialmonitor \
--set nodeName=worker-node-01 \
--set 'devices={/dev/ttyUSB0,/dev/ttyUSB1}'For a node where you'd rather not enumerate every device, set hostDev: true and privileged: true to mount all of /dev. See helm/serialmonitor/values.yaml for ingress (remember the WebSocket read‑timeout annotation), persistence, and scheduling options.
Requires Go 1.26+. This is a Linux‑only project (the port enumeration is behind a //go:build linux tag):
make build # -> ./serialmonitor
make run # build and run with --config config.yamlCross‑compiling from macOS/Windows? GOOS=linux CGO_ENABLED=0 go build ./cmd/serialmonitor.
Everything lives in a single config.yaml. You can hand‑edit it, or just use the Configuration tab in the UI — both write to the same file, and the running service reloads on every change (or on SIGHUP).
server:
host: "0.0.0.0"
port: 8080
buffer_size: 300 # lines of scrollback replayed to new viewers (0 = off)
ports:
- device: /dev/ttyUSB0
name: arduino # how it's labelled in the UI and logs
enabled: true
baud_rate: 115200
data_bits: 8
parity: none # none | odd | even
stop_bits: 1 # 1 | 1.5 | 2
color: "#4CAF50" # auto-assigned from a palette if omittedA port without a color gets one auto‑assigned from a built‑in palette. See config.remote.yaml for a real 4‑port example.
Every received line is logged to stdout as JSON:
{"time":"2026-06-12T15:04:05Z","level":"INFO","msg":"received","port":"arduino","device":"/dev/ttyUSB0","data":"boot complete"}Point Grafana Alloy / Promtail at the container's stdout and you get searchable, labelled serial history across all your boards. A working scrape config is in docs/loki-alloy-example.yaml.
serial device → internal/serial → broadcast hub → WebSocket → browser
│
└── stdout JSON (slog) → Loki
internal/serialopens each port and reconnects on failure. It fans output two ways: line‑buffered messages to the hub, and raw bytes to any attached terminal.internal/broadcastis a fan‑out hub with a ring‑buffer history that's replayed to each new viewer. Slow clients get dropped frames, never block the device.internal/serveris a chi router exposing the REST config API and two WebSocket endpoints —/ws/stream(JSON lines, all ports) and/ws/port/{name}(raw binary terminal tunnel).web/is a dependency‑free vanilla‑JS UI (plus xterm.js), embedded into the binary at build time.
Contributors: see CLAUDE.md for a deeper architecture tour and the build/test workflow.
MIT © Derek Smithson

