Flash an AirGradient ONE from the Command Line

I’ve purchased two AirGradient ONE indoor quality monitors to measure air quality in my home. AirGradient devices are open-source, so you can flash your own custom firmware and collect your air data locally rather than sending it to AirGradient’s proprietary cloud dashboard.

I keep an AirGradient ONE air quality monitor in my office to measure CO2 and pollution.

The existing documentation for flashing firmware requires you to use the Arduino IDE, a clunky GUI program:

Existing instructions for flashing AirGradient ONE rely on the Arduino IDE, a clunky GUI program.

I couldn’t find instructions for flashing AirGradient devices using the command-line, and it took me several hours to figure out, so I’ve included the steps below.

Aside: I don’t get the hype about AirGradient ๐Ÿ”—︎

Every time I see AirGradient come up on forum discussions, everyone sounds excited about their products. I’ve found my AirGradient ONE to be mediocre. The software is extremely buggy and the documentation is sparse. But they’re the only company I’ve found that sells pre-made air quality monitors that are open-source, so I bought a second AirGradient monitor.

For years, AirGradient never bothered to publish instructions for flashing software onto the AirGradient ONE. I learned how to do it from these blog posts:

This year finally, AirGradient published official flashing instructions, but they’re still a bit hidden.

Environment ๐Ÿ”—︎

I tested these steps on Debian 13.0, but they should work on any Debian/Ubuntu-like system.

Install packages ๐Ÿ”—︎

First, I install the base packages I need:

sudo apt update && \
  sudo apt install -y \
    git \
    curl \
    python3 \
    python3-serial

Install arduino-cli ๐Ÿ”—︎

Next, I install the Arduino CLI tool:

ARDUINO_CLI_VERSION='1.2.2'
ARDUINO_BIN_DIR="${HOME}/.local/arduino-cli"

mkdir -p "${ARDUINO_BIN_DIR}" && \
  curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
  | BINDIR="${ARDUINO_BIN_DIR}" sh -s "${ARDUINO_CLI_VERSION}"
export PATH="${PATH}:${ARDUINO_BIN_DIR}"

To verify the install was successful, I print out the version string for arduino-cli:

$ arduino-cli version
arduino-cli  Version: 1.2.2 Commit: c11b9dd5 Date: 2025-04-22T13:51:01Z

Download ESP32 libraries ๐Ÿ”—︎

AirGradient ONE depends on the ESP32 Arduino libraries. As of this writing, AirGradient is not yet compatible with the 3.x versions of Arduino, so I have to use the latest stable 2.x version.

ARDUINO_ESP32_VERSION='2.0.17'

arduino-cli config init \
  --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json && \
  arduino-cli core install "esp32:esp32@${ARDUINO_ESP32_VERSION}"

Find the path to my device ๐Ÿ”—︎

Next, I need the device path to my AirGradient ONE. The simplest way to find the device path is:

  1. Run dmesg --follow
  2. Plug my AirGradient ONE into my system via USB
  3. Look for the device path to appear in the dmesg output

Here’s what it looks like on my system:

$ sudo dmesg --follow
[517021.978880] usb 1-4: New USB device found, idVendor=303a, idProduct=1001, bcdDevice= 1.01
[517021.978884] usb 1-4: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[517021.978894] usb 1-4: Product: USB JTAG/serial debug unit
[517021.978896] usb 1-4: Manufacturer: Espressif
[517021.978898] usb 1-4: SerialNumber: D8:3B:DA:1A:EE:C4
[517022.017678] cdc_acm 1-4:1.0: ttyACM0: USB ACM device
                                 ^^^^^^^
                                 Path name

Given this output, the path on my system to my AirGradient ONE is /dev/ttyACM0:

AIRGRADIENT_PATH='/dev/ttyACM0'

Make device path writeable ๐Ÿ”—︎

Next, I ensure that I can write to the AirGradient file path:

sudo chmod a+rw "${AIRGRADIENT_PATH}"

The AirGradient path should now have these permissions:

$ ls -l "${AIRGRADIENT_PATH}"
crw-rw-rw- 1 root dialout 166, 0 Aug 10 10:34 /dev/ttyACM0
 ^^^^^^^^

I also add myself to the dialout group so I can write to the path:

sudo adduser "$(whoami)" dialout

Get AirGradient source ๐Ÿ”—︎

Next, I check the AirGradient factory flashing page to find out the latest production release.

# Current production release, as of this writing.
AIRGRADIENT_RELEASE='3.3.8'
Warning: The latest version on AirGradient’s website does not match the latest release tag on AirGradient’s GitHub repo. When I tested 3.3.9, both of my devices failed to measure CO2 and temperature, so I’m not sure if 3.3.9 is a known-buggy release.

With the version number in hand, I grab the AirGradient source code from AirGradient’s GitHub repo:

git clone --recurse-submodules \
  --branch "${AIRGRADIENT_RELEASE}" \
  --depth 1 \
  https://github.com/airgradienthq/arduino.git \
  ~/airgradient-one

Flash firmware onto AirGradient ONE device ๐Ÿ”—︎

Finally, it’s time to flash the software to my device:

cd ~/airgradient-one && \
  arduino-cli compile \
    --verbose \
    --fqbn esp32:esp32:esp32c3:CDCOnBoot=cdc,PartitionScheme=min_spiffs,DebugLevel=info \
    --library . \
    --port "${AIRGRADIENT_PATH}" \
    --verify \
    --upload \
    examples/OneOpenAir/OneOpenAir.ino
Note: To erase persistent data on the AirGradient ONE device (i.e., a hard reset, including configuration data), add ,EraseFlash=all to the end of the --fqbn flag.

If flashing was successful, I see my device reboot and this output at the end of the process:

Wrote 1753792 bytes (967231 compressed) at 0x00010000 in 14.7 seconds (effective 952.3 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

Optional: View serial log output ๐Ÿ”—︎

While my AirGradient is connected to my computer, I can view its log output through the serial port by using the arduino-cli monitor command:

$ arduino-cli monitor --port "${AIRGRADIENT_PATH}"
Using default monitor configuration for board: esp32:esp32:heltec_wifi_kit_32_V3
Monitor port settings:
  baudrate=9600
  bits=8
  dtr=on
  parity=none
  rts=on
  stop_bits=1

Connecting to /dev/ttyACM0. Press CTRL-C to exit.
[1] Standard Particle PM 2.5 = 7.00 ug/m3
[1] Particle Count 0.3 = 1298.5
[1] Particle Count 0.5 = 383.5
[1] Particle Count 1.0 = 39.7
[1] Particle Count 2.5 = 2.0
[1] Particle Count 5.0 = 2.0
[1] Particle Count 10 = 0.0

Alternative: Nix flake ๐Ÿ”—︎

If you’re a Nix nerd, you might want to do this the Nix way. I’ve created a Nix flake to automate all the above steps:

When I want to flash my repo, I just run:

nix run .#flash

And when I want to view serial output, I run:

nix run .#monitor

I’m not sure how well my Nix flake works across systems, so you’ll probably have to tinker a little bit to get it to work for your system.

Conclusion ๐Ÿ”—︎

I hope these instructions make it easier for you to flash your AirGradient ONE device and customize your firmware as you see fit.

Read My Book

I'm writing a book of simple techniques to help developers improve their writing.

My book will teach you how to:

Read Michael's Book