The Problem: Every Serial Monitor Is Too Much
I spend a lot of time debugging microcontroller firmware over UART. Tools like PlatformIO's serial monitor, minicom, and screen all work, but each one has something that annoys me. PlatformIO's monitor is tied to its ecosystem. minicom has a labyrinthine config UI. screen doesn't timestamp lines or let me log to file without a workaround.
So I did what any developer would do: I built my own. This is the project log for serialwatch — a minimal, single-file C program with an ncurses TUI that does exactly what I need and nothing else.
Goals and Non-Goals
Before writing a line of code, I wrote down what this tool must and must not do.
Must Have
- Connect to any
/dev/tty*device at a configurable baud rate - Display incoming data with optional per-line timestamps
- Log all output to a file automatically
- Allow typing and sending text (for interactive firmware prompts)
- Compile with no dependencies beyond
ncursesand a C99 compiler
Non-Goals
- No hex viewer (use
xxdpiped fromcatif you need it) - No scripting, macros, or automation
- No config files — flags only
Architecture: Split Panes with ncurses
The TUI has two windows: a scrolling output pane taking up the top 80% of the terminal, and a one-line input bar at the bottom. ncurses makes split-pane layout surprisingly manageable with newwin() and wrefresh().
The trickiest part was handling non-blocking reads from the serial fd while also reading keyboard input. I ended up using a select() loop with a 50ms timeout — the serial fd and STDIN_FILENO both go into the read set. This keeps the UI responsive without spinning a CPU core.
The Timestamp Decision
Timestamps are opt-in via a -t flag. When enabled, each newline-terminated chunk gets prefixed with [HH:MM:SS.mmm] before display and before being written to the log file. The millisecond resolution has already saved me twice when debugging timing-sensitive firmware interactions.
One edge case that bit me: devices that output partial lines (no trailing \n) or that send binary data. I now buffer incoming bytes and only timestamp on newline boundaries, flushing on a 200ms idle timeout as a fallback.
Baud Rate and Port Config
Configuration is entirely through command-line flags:
serialwatch -p /dev/ttyUSB0 -b 115200 -t -l output.log
Baud rate is set via termios with cfsetispeed() / cfsetospeed(). I added a lookup table that maps common integer baud values to the B* constants so users don't need to know about B115200 vs 115200.
What I Learned
- ncurses is old but solid. The API is clunky, but once it clicks, split-pane terminal UIs are surprisingly achievable in pure C.
- termios is fiddly but well-documented. The Linux man page for
termios(3)is actually excellent — read it front to back once. - Edge cases come from hardware, not your code. USB-serial adapters behave differently. A CH340 and an FTDI232 will both surprise you in different ways at high baud rates.
Current Status and What's Next
The tool is at a stable v0.3 state on my local git repo. I'm planning to push it to GitHub under the MIT license in the next few weeks after I clean up the build system. Planned additions include a simple hex mode toggle and auto-reconnect when a device is unplugged and re-plugged.