Skip to content

cajunpanda/benchmux

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

benchmux

A serial and BLE bench monitor/flasher for embedded work. One process owns the port (or BLE link) and tees everything to a shared logfile, so any number of readers can follow it live while you flash or reset without stopping the monitor.

Only one process can open a serial port or a BLE peripheral's link at a time, so two readers can't watch it at once, and you normally have to stop the monitor before flashing. The two CLIs here remove both limits:

  • serial_proxy.py owns a USB-serial port and tees output to a shared logfile. Readers follow it with tail -f. flash and reset hand the port off and take it back, so the monitor keeps running.
  • ble_proxy.py does the same over BLE GATT: owns a device's link, tees its notify stream to a logfile, injects console lines, and flashes firmware over the air.

Both are single files. serial_proxy.py needs pyserial; ble_proxy.py needs bleak.

Use with an AI coding agent

The main reason benchmux exists is to let an AI coding agent (Claude Code or similar) work the bench with you. Because the agent and you read the same logfile, the agent can flash a build and read the boot log to check a change while you watch the same stream in your own terminal. What makes it fit an agent:

  • Reading device output means reading a file. The agent doesn't launch or scrape a monitor, and doesn't fight you for the port.
  • The commands are one-shot: flash, send, reset, and status each run once and exit with a status code. There's no REPL to stay inside. send writes a console line through the running monitor.
  • Flashing doesn't stop the monitor, so the new firmware's boot log lands in the same log the agent is already reading.
  • Nothing requires a TTY. Commands that would prompt (such as flash --manual) fall back instead of blocking, and ports, adapters, and build commands are set by flags or env vars.

It works the same by hand or from CI.

Install

git clone https://github.com/cajunpanda/benchmux
cd benchmux
pip install pyserial          # for serial_proxy.py
pip install bleak             # for ble_proxy.py (BLE)

Run the scripts by path (they're executable), or symlink them onto your PATH.

Most distro pythons are PEP 668 "externally managed", so those pip installs fail there and the packages end up in a venv instead:

python3 -m venv ~/.venvs/bleak && ~/.venvs/bleak/bin/pip install bleak

ble_proxy.py handles that for you: if bleak isn't importable it looks for a venv that has it (~/.venvs/*, ~/.virtualenvs/*, ./.venv, or $BLE_PROXY_PYTHON) and re-execs into it, so ./ble_proxy.py ... works from any interpreter. If it can't find one it prints the venv command above rather than a bare pip install that won't work.

Install as a Claude Code skill

The repo root contains a SKILL.md, so the whole repo is a Claude Code skill. Point Claude Code's skills directory at it. Symlinking the clone means git pull updates the skill in place:

# personal skill (all projects)
ln -s "$PWD" ~/.claude/skills/benchmux

# or a single project
ln -s "$PWD" /path/to/project/.claude/skills/benchmux

Claude Code picks it up on the next session. Ask it to monitor the serial port, flash the firmware, or tail the boot log, and it will use the skill. The dependencies still apply: install pyserial in the Python that Claude Code runs. bleak can live in a venv — see Install — since ble_proxy.py finds it there on its own.

Quick start (serial)

# Own the port and stream to /tmp/serial_proxy.log (backgrounded)
./serial_proxy.py monitor --port FTDI &

# From anywhere, follow it live
./serial_proxy.py tail                 # or: tail -f /tmp/serial_proxy.log

# Flash without stopping the monitor: it releases the port, uploads, re-attaches
./serial_proxy.py flash

# Inject a console line; pulse reset; hold the chip in reset
./serial_proxy.py send "help"
./serial_proxy.py reset
./serial_proxy.py enable off

--port takes a device path or a /dev/serial/by-id substring (FTDI, CP2102, ttyUSB1), so you can pin one adapter among several and survive ttyUSBn renumbering on replug.

Choosing the flash command

The proxy's job is the release / run your build+upload / re-attach handoff; the flash command itself is up to you. flash defaults to PlatformIO (pio run -t upload) when it finds a platformio.ini. For any other toolchain, pass --flash-cmd (or set SERIAL_PROXY_FLASH_CMD). The string is templated with {port}, {env}, and {fw_dir}:

# ESP-IDF
./serial_proxy.py flash --flash-cmd 'idf.py -p {port} flash'

# Arduino CLI
./serial_proxy.py flash --flash-cmd 'arduino-cli upload -p {port} -b <fqbn> {fw_dir}'

# Raw esptool
./serial_proxy.py flash --flash-cmd 'esptool.py --port {port} write_flash 0x10000 build/app.bin'

# Zephyr west, make, or anything that takes a port
./serial_proxy.py flash --flash-cmd 'west flash'
export SERIAL_PROXY_FLASH_CMD='make flash PORT={port}'

With no platformio.ini and no --flash-cmd, flash exits and tells you to pass one; it does not assume a toolchain.

Filesystem / data partitions

Uploading the app usually doesn't write a data/filesystem partition (LittleFS, SPIFFS, FAT). Add a post-flash step with --fs-cmd (or SERIAL_PROXY_FS_CMD), run while the port is still released and templated the same way:

./serial_proxy.py flash --fs-cmd 'esptool.py --port {port} write_flash 0x310000 fs-{env}.bin'

There is also a built-in --fs flag that builds and flashes an ESP-IDF + PlatformIO LittleFS storage partition from the project data/ dir with no extra arguments, for that specific stack. --fs-cmd overrides it.

Quick start (BLE)

ble_proxy.py reads the device name and GATT characteristic UUIDs from a JSON profile or CLI flags. The console defaults to the Nordic UART Service (NUS) UUIDs, so NUS firmware works with no profile.

./ble_proxy.py monitor --name "My Device" &     # or --address AA:BB:...  or a profile
./ble_proxy.py tail
./ble_proxy.py send "help"
./ble_proxy.py flash firmware.bin               # OTA (needs the OTA protocol in firmware)

Put a ble-proxy.json next to your project (found by walking up from cwd):

{
  "name": "My Device",
  "console": { "log_uuid": "...", "cmd_uuid": "..." },
  "ota":     { "control_uuid": "...", "data_uuid": "...", "window": 8192, "chunk_max": 512 }
}

The console side works with any NUS device. OTA requires your firmware to implement the reference protocol documented at the top of ble_proxy.py; there is no universal BLE OTA standard.

Tested hardware

This has only been used with the ESP32 (ESP-IDF and PlatformIO). The serial reset and enable behavior assumes the ESP32's DTR/RTS-to-EN/BOOT auto-reset wiring, and the BLE OTA path assumes the reference protocol in ble_proxy.py. The monitor, log tee, and send paths are not ESP32-specific, and --flash-cmd can run any toolchain, but nothing else has been verified on other MCUs. Reports and patches for other targets are welcome.

OS support

Linux macOS Windows
serial_proxy.py yes (primary) mostly (no /dev/serial/by-id; POSIX signals work) no (COM ports, no SIGUSR)
ble_proxy.py yes (BlueZ) via bleak CoreBluetooth (--adapter is BlueZ-only) via bleak WinRT

serial_proxy.py is developed on Linux and uses /dev/serial/by-id for stable adapter naming and SIGUSR1/2 for the release/resume handoff. ble_proxy.py uses bleak, which runs on all three platforms, though adapter selection and scanning differ. Patches to widen support are welcome.

License

MIT, 2026 Aaron Perkins. See LICENSE.

About

Serial and BLE bench monitor/flasher for embedded work, one port owner, many readers, flash without stopping the monitor. Built for AI coding agents (Claude Code).

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages