Terminal UI for bulk-flashing Fri3d Camp 2026 hardware on macOS, Windows and Linux.
| Device | Chip | Tool | Firmware source |
|---|---|---|---|
| Fri3d Badge 2026 | ESP32-S3 | esptool | badge_firmware_MicroPythonOS (full_2026_firmware_for_2026_badge.bin, flashed at 0x0) |
| Communicator 2026 | CH32X035 | wchisp | communicator_2026 (firmware.bin) |
| DJ Addon 2026 | CH32X035 | wchisp | dj_2026 (firmware.bin) |
Prebuilt single-file binaries (no Python needed) are produced by the GitHub
Actions workflow in .github/workflows/build.yml for:
- macOS arm64 / x64
- Linux x64 / arm64
- Windows x64 / arm64
Push the repo to GitHub and every push to main builds all six as artifacts;
pushing a v* tag attaches them to a GitHub Release.
Build one locally for your current platform:
./build-binary.sh # -> dist/fri3d-bulk-flasher-<version>(Windows: run the uv run pyinstaller ... command from the script in
PowerShell — output is dist\\fri3d-bulk-flasher-<version>.exe.)
esptool is bundled inside the binary; wchisp is still downloaded on first use.
With uv (recommended):
uv tool install . # or: uvx --from . fri3d-bulk-flasher
fri3d-bulk-flasherOr with pip:
pip install .
fri3d-bulk-flasherOr straight from the source tree:
uv run fri3d-bulk-flasherEverything goes through uv — it creates the virtualenv and installs dependencies automatically on first run:
uv run fri3d-bulk-flasher # run the app; code edits apply on next run
uv sync # (re)install deps after changing pyproject.tomlDebugging a Textual TUI (the app owns the terminal, so print() is useless):
uv run textual console # in a second terminal
uv run textual run --dev bulk_flasher.app:BulkFlasherApptextual run --dev connects to the console for live log output and enables
CSS hot-reloading (the textual-dev tools are in the dev dependency group,
installed by uv sync/uv run automatically).
There is no USB hardware in CI, so exercise flashing logic against fakes: the
port poller and flash call (_esp_candidate_ports, _flash_esptool_slot in
bulk_flasher/flasher.py) are module-level and easy to monkeypatch.
- Pick a device with the arrow keys.
- Press
fto fetch the latest firmware release from GitHub (cached in~/.fri3d-bulk-flasher/). Starting without firmware fetches it automatically. - Press
sto start bulk flashing, then just keep plugging devices in:- Badge: plug in over USB → it flashes → unplug → plug in the next one. Badges flash in parallel: every newly plugged-in badge claims a free slot (up to 10 by default) and flashes immediately, so you can keep a whole USB hub busy at once.
- Communicator / DJ Addon: hold the BOOT button while plugging in USB (enters the CH32 bootloader) → it flashes → unplug → next one (wchisp can only talk to one bootloader device at a time, so these stay sequential).
- The status grid shows one card per slot — port, progress bar, and
success/failure — and adapts its columns to the terminal width. The sidebar
tracks OK / failed / total counts. Press
sagain (or Stop) to end.
Press c in the app to open the Settings screen and change the parallel
slot count and baud rate — changes are saved to the config file and take
effect immediately (or on the next run if flashing is in progress).
The same settings live in ~/.fri3d-bulk-flasher/config.toml (created on
first run) and can be edited by hand:
# How many badges may be flashed at the same time (1-16).
max_parallel = 10
# Serial baud rate for badge flashing (default: device default, 921600).
# Lower it (e.g. 460800 or 115200) if flashing is unreliable through a hub.
#baud = 921600esptool is installed as a Python dependency. wchisp is found on your PATH,
or automatically downloaded from the wchisp releases
on first use.
- Linux: give yourself access to the USB devices —
serial:
sudo usermod -aG dialout $USER(log out/in), and for wchisp add a udev rule for the WCH bootloader (VID4348/1a86), e.g.SUBSYSTEM=="usb", ATTR{idVendor}=="4348", MODE="0666"in/etc/udev/rules.d/50-wchisp.rules, thensudo udevadm control --reload. - Windows: the CH32 bootloader needs the WinUSB driver; if wchisp can't see the device, install it with Zadig.
- macOS: works out of the box.
- The badge is flashed with the full 16 MB image, so every flash produces an identical, factory-fresh device (no erase step needed). At 921600 baud a badge takes a few minutes.
- The flasher keeps a port claimed until the device is unplugged, so it never flashes the same unit twice — even with many badges connected at once.
- Slots are sticky per physical USB port for the session: the first time a hub port is used it claims a grid card, and every badge plugged into that same port lands on the same card afterwards (based on the USB location path, independent of plug order). Ports whose driver reports no location fall back to first-free assignment.