npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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 upload

Requires 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 upload
Found: ESP32-C3 Dev Module  serial=54:32:04:AB:12:F1  →  /dev/cu.usbmodem14201

If 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]: 1

If 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:F1

That 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=kitchen

Your 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. --dir is always an explicit path. If your projects live under a hardware/<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.
  • --name and --define are independent. --name only 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