pio-flash-cli
v0.2.0
Published
Flash or monitor a PlatformIO ESP32 project over serial or OTA, with automatic multi-board port detection and compile-time -D flag injection.
Readme
pio-flash-cli
A small CLI for flashing and monitoring PlatformIO ESP32 projects — over USB or
over Wi-Fi — that fixes two specific annoyances plain pio leaves for you to
deal with yourself.
Why I built this
I have a small server tucked in my living room with a handful of microcontrollers wired into it at any given time — some permanently, mid-build projects that are just parked on a USB hub between tinkering sessions, plus whatever new board I'm currently prototyping with. Every one of them is an ESP32 variant, and every one of them wants the same handful of things: find its port, flash it, watch its serial output, and — since a lot of them end up mounted somewhere in the house once they're done — eventually get updated over Wi-Fi instead of by walking over with a cable.
pio run --target upload works fine when there's one board plugged in. It
does not work fine when there are three, and it has no idea that
"kitchen-sensor" and "office-sensor" are the same sketch that just needs to
know which one it is. I got tired of re-deriving /dev/cu.usbmodemXXXXX port
paths by process of elimination and hand-editing #defines before every
flash, so I wrote the auto-detection and --define injection this tool does
once, instead of solving it fresh for every project.
The problems this fixes
"Which port is my board again?" — Plug in one ESP32 and pio run --target
upload finds it automatically. Plug in two, and it either uploads to the
wrong one or refuses outright. Worse, boards like the ESP32-C3/S3 that use
native USB all identify themselves as the same generic device over the wire,
so even matching by board name doesn't help — you're stuck manually digging
through arduino-cli board list output for a /dev/cu.usbmodemXXXXX path,
every single time.
"How do I flash the same firmware to five different sensors?" — If one
sketch runs on multiple physical units (a temperature sensor in every room,
say), it usually needs to know which one it is — a name to report over MQTT, a
room label, a feature flag for a unit that has an extra relay wired up. pio
has no first-class way to pass that in from the command line; you either
maintain a platformio.ini environment per device or hand-edit a #define
before every flash.
pio-flash solves the first with better board detection and the second with
a --define KEY=VALUE flag. It doesn't touch anything else — it's still just
pio underneath, and it makes no assumptions about how your project is laid
out.
Install
npm install --save-dev pio-flash-cli
# or run it ad hoc, no install:
npx pio-flash-cli --dir ./firmware/my-project --fqbn esp32:esp32:esp32 --target uploadRequires PlatformIO (pio
on PATH, or installed at ~/.platformio/penv/bin/pio) and, for serial transport,
arduino-cli (used only for
board/port auto-detection — you don't need an Arduino sketch anywhere).
Use case: flashing over USB, especially with more than one board plugged in
Point it at your project directory and board FQBN; it finds the port for you:
pio-flash --dir ./firmware/env-sensor --fqbn esp32:esp32:esp32c3 --target uploadFound: ESP32-C3 Dev Module serial=54:32:04:AB:12:F1 → /dev/cu.usbmodem14201If more than one matching board is plugged in, you don't get an error telling you to go figure it out yourself — running in a terminal, it asks:
Multiple esp32:esp32 boards detected:
[1] /dev/cu.usbmodem14201 ESP32-C3 Dev Module serial=54:32:04:AB:12:F1
[2] /dev/cu.usbmodem14401 ESP32-C3 Dev Module serial=7C:9E:BD:33:90:0A
Select port [1-2]: 1If you're writing a script that flashes the same physical board over and
over, you don't want to be prompted each time, and you don't want to hardcode
a port path either — those shift around depending on what else is plugged in
and in what order. Instead, note the board's serial= value once (it's a
stable USB identifier, unaffected by reboots or plugging into a different
port) and target it directly:
pio-flash --dir ./firmware/env-sensor --fqbn esp32:esp32:esp32c3 --target upload \
--serial 54:32:04:AB:12:F1That command also works unattended (CI, a cron job, a script with no one
watching): pass --serial or --port and it never has to ask. If you don't,
and stdin isn't an interactive terminal, it fails fast with a clear message
instead of hanging.
(A caveat: cheap CH340/CP2102 USB-serial clones often report a blank or
identical serial number across units, in which case --serial can't help
you and --port is the only way to disambiguate.)
Use case: updating a device that's already deployed, over Wi-Fi
Once a board is out in the field — screwed to a wall, sealed in an enclosure
— plugging in a USB cable to update it is a hassle. If your firmware has OTA
(ArduinoOTA/espota) built in, pio-flash can upload to it and watch its
serial output over the network instead:
pio-flash --dir ./firmware/env-sensor --fqbn esp32:esp32:esp32c3 --target upload \
--transport ota --name living-room-sensor
pio-flash --dir ./firmware/env-sensor --fqbn esp32:esp32:esp32c3 --target monitor \
--transport ota --name living-room-sensor--name is resolved as <name>.local via mDNS. If mDNS isn't reliable on
your network, skip it and target an IP or hostname directly with --host.
Use case: one sketch, many physical units
Say you have the same sensor firmware running in five rooms. Rather than
maintaining five near-identical copies of the project, or five PlatformIO
environments, pass identity in at flash time as compile-time -D flags:
pio-flash --dir ./firmware/env-sensor --fqbn esp32:esp32:esp32c3 --target upload \
--transport ota --name kitchen-sensor \
--define DEVICE_NAME=kitchen-sensor --define ROOM=kitchenYour sketch reads these exactly like any other -D flag — for a string
value, that usually means a stringize macro:
#define STR(x) #x
#define XSTR(x) STR(x)
const char* deviceName = XSTR(DEVICE_NAME); // "kitchen-sensor"--define is repeatable and generic — pass whatever your firmware actually
needs (--define HAS_RELAY=1, --define FIRMWARE_CHANNEL=beta, ...), not
just a name and room.
Full flag reference
pio-flash --dir <path> --fqbn <fqbn> [--target upload|monitor] [--transport serial|ota]
[--name <device>] [--port <COMx>] [--serial <id>] [--host <hostname>] [--env <pio-env>]
[--define KEY=VALUE ...]| Flag | Meaning |
| --- | --- |
| --dir | PlatformIO project directory (required). Resolved against cwd if relative. |
| --fqbn | Board FQBN, e.g. esp32:esp32:esp32c3 (required). |
| --target | upload (default) or monitor. |
| --transport | serial (default) or ota. |
| --port | Serial port; skips auto-detection entirely. |
| --serial | USB serial number to select among several connected boards; skips the prompt. |
| --name | OTA target device — resolved as <name>.local. Unrelated to --define. |
| --host | OTA target host/IP directly, bypassing mDNS. |
| --env | PlatformIO environment (pio run -e <env>). Defaults to usb for serial, ota for OTA transport. |
| --define | Repeatable. KEY=VALUE → -DKEY=VALUE build flag, upload only. |
Design notes
- No repo-layout assumptions.
--diris always an explicit path. If your projects live under ahardware/<name>convention (or any other), write a thin wrapper script that resolves your own--project <name>flag to--dir hardware/<name>and forwards everything else — that's the intended integration point, not a config file this package reads. --nameand--defineare independent.--nameonly names the OTA target; it has no effect on the build. If you want the device's name baked into the firmware too, pass it explicitly via--define DEVICE_NAME=<name>.
Also in this repo (not published)
contrib/pio-flash-device.js is a reference script for
a fuller flow — upload, log the release (JSONL by default, or a pluggable adapter like
contrib/postgres-log-adapter.js), wait for the
device to come back online, then monitor. It's opinionated about things specific to a
home device-fleet setup (a timestamp-derived version scheme, a release log schema) that
don't generalize well, so it isn't part of the published pio-flash-cli package. Copy it
into your own project and adapt it, or run it straight out of a clone of this repo:
git clone https://github.com/nries1/pio-flash-cli.git
node pio-flash-cli/contrib/pio-flash-device.js --dir ./firmware/env-sensor \
--fqbn esp32:esp32:esp32c3 --model "Nologo ESP32-C3 Super Mini" \
--transport ota --name living-room-sensor