Sharing /dev/ttyUSB0 between Claude Code and a Human via tmux
When you debug an embedded board with Claude Code, sooner or later you want the model to drive the serial console too — type commands, read kernel messages, react to a panic. But /dev/ttyUSB0 is a single-owner device: one process opens it, everyone else gets Device or resource busy.
This is the pattern I settled on. One picocom instance owns the port, running inside a detached tmux session. The human attaches with tmux attach. The model reads picocom’s log file and writes back through tmux send-keys. Both sides see exactly the same console, in real time.
The setup
# One-time install on the host
sudo apt install -y tmux picocom
# Start picocom inside a detached tmux session, with full I/O logging.
# --noreset is important: without it, picocom toggles DTR on close and may
# reset the target board the next time you restart the session.
tmux new -d -s board \
"picocom -b 115200 --noreset --nolock \
--imap lfcrlf --omap crlf \
--log /tmp/ttyUSB0.log /dev/ttyUSB0"
That’s it. The session is now alive, picocom is talking to the board, and every byte in either direction is being appended to /tmp/ttyUSB0.log.
How the human watches
tmux attach -t board
Ctrl-b d to detach without killing picocom. Ctrl-a Ctrl-x (picocom’s own escape) if you actually want to shut the session down.
Multiple terminals can attach simultaneously and they’ll all mirror the same screen. The model’s keystrokes appear in your terminal exactly as if you typed them yourself — that’s what makes the collaboration feel natural.
How the model reads
Two equivalent options. I let the model pick whichever fits the moment:
# Reading the rolling log file
tail -n 200 /tmp/ttyUSB0.log
# Reading the literal contents of the visible pane
tmux capture-pane -t board -p -S -200
The log file gives you the full transcript including escape sequences; capture-pane gives you what a human would see on screen, post-render.
How the model writes (the part with the trap)
The naive way is tmux send-keys -t board "uname -a" Enter. This works sometimes. It also drops characters on long strings, because tmux’s default mode tries to interpret short tokens as named keys and the timing gets unhappy when bytes pile up faster than picocom can drain them into the serial UART.
Two fixes, both required:
- Use
-l(literal) so tmux doesn’t try to parse anything. - Send one character at a time with a small delay, so the FTDI/CH340 USB bridge has time to clock each byte out at 115 200 baud.
ws() {
local s="$1" i
for ((i=0; i<${#s}; i++)); do
tmux send-keys -t board -l "${s:$i:1}"
sleep 0.05
done
tmux send-keys -t board Enter
}
ws "systemctl status NetworkManager"
0.05s per character is conservative but reliable. Faster works on most adapters; slower wastes nothing. With this in place, even multi-line shell constructs go through cleanly.
A worked example
Suppose the model wants to inspect a DNS misconfiguration on the target. The full round-trip looks like:
# Clear the log so we only capture this turn's output
: > /tmp/ttyUSB0.log
ws "ls -l /etc/resolv.conf"
sleep 2
tail -20 /tmp/ttyUSB0.log
Output the model gets back:
ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 24 Mar 9 2018 /etc/resolv.conf -> /etc/resolv-conf.systemd
root@BOARD-000:~#
From here the model reasons exactly as a human would, issues the next command, and the human looking at tmux attach sees the whole thing scroll past.
Pitfalls I hit
- DTR resets the board. Without
--noreset, killing and restarting picocom toggles DTR and many boards reset. Always pass--noresetif you expect to restart the session during a debugging run. - Two serial clients = silent input loss. If
minicom,screen, or a secondpicocomis already attached, yoursend-keysbytes go nowhere obvious. Check withlsof /dev/ttyUSB0before starting. fuser -k /dev/ttyUSB0is the nuclear option — it works, but on boards whose USB-TTL adapter pulls DTR low on close, that single command also reboots your target. Be intentional.- Boot output flooding the prompt. Right after the kernel comes up, the console is busy with
dmesglines and yoursend-keyscharacters interleave. Wait for the login prompt with a poll loop before issuing commands. /etc/resolv.confpermission group. Make sure your user is indialout(or whatever group owns/dev/ttyUSB0on your distro); otherwise picocom inside tmux will fail with EACCES and the session will exit silently.
Why not screen or socat?
screen -x does the multi-attach piece, but it doesn’t give you a log-file-as-side-channel out of the box, and its escape key collides with common shell bindings.
socat PTY,link=/tmp/ttyV0 /dev/ttyUSB0,raw gives you a virtual port that multiple readers can open, but you’ve now built a small DIY con-server. tmux is two lines and already on every dev machine.
tl;dr
tmux new -d -s board \
"picocom -b 115200 --noreset --nolock \
--imap lfcrlf --omap crlf \
--log /tmp/ttyUSB0.log /dev/ttyUSB0"
# Human: tmux attach -t board
# Model: reads /tmp/ttyUSB0.log
# writes via tmux send-keys -t board -l "..." one char at a time
Same screen, same console, both sides driving. Works for U-Boot prompts, kernel panics, login shells — anything that talks over the UART.
댓글 없음:
댓글 쓰기