I wanted an air quality monitor I can read from across the room, and I wanted it to look like something from a Fallout vault.
AIR PRO reads CO2, particles (PM1.0 to PM10), VOC, NOx, temperature and humidity every 2 seconds. It turns all of that into one simple status: OPTIMAL, NORMAL, FAIR, CAUTION, WARNING or CRITICAL. It also tells you the main problem, for example "MAIN THREAT: CO2, VENTILATE ROOM".
Everything runs on CircuitPython. No cloud, no app. WiFi is only used for a few seconds every hour to get the time.
Full code, parts list and photos: github.com/ostgate/air-pro
Parts
- Adafruit Qualia ESP32-S3 for TTL RGB-666 Displays
- 4" 720x720 square TFT display (RGB666)
- Sensirion SEN66 (CO2, PM, VOC, NOx, temperature, humidity)
- Adafruit SHT45 Temperature and Humidity Sensor (STEMMA QT)
- Adafruit I2C QT Rotary Encoder with seesaw
- STEMMA QT / Qwiic cables
- USB-C cable and a USB charger
- A small wooden box (for now)
All sensors and the encoder share one I2C bus through STEMMA QT cables. No soldering needed.
Build
The case is a small wooden box I had at home. The display sits in the door, with the Qualia board on its back. The SHT45 and SEN66 are mounted on the outside wall of the box, so they breathe room air and do not get heated by the board and the backlight. The rotary encoder shaft comes out of the top.
Two gotchas I hit
1. The display needs its own I2C bus during init. On the Qualia S3 the display init sequence is sent through the I2C IO expander, and it wants its own bus. So the code releases the shared board I2C first, sends the init sequence on a temporary bus, and only then starts the sensors on the shared bus again.
board.I2C().deinit()
displayio.release_displays()
with busio.I2C(board.SCL, board.SDA) as i2c:
dotclockframebuffer.ioexpander_send_init_sequence(
i2c, bytes(), **dict(board.TFT_IO_EXPANDER)
)
fb = dotclockframebuffer.DotClockFramebuffer(**board.TFT_PINS, **TFT_TIMING)
display = FramebufferDisplay(fb, auto_refresh=False)
# now board.I2C() can be used for the sensors
2. WiFi makes the screen flicker. The ESP32-S3 WiFi radio and the DotClock framebuffer compete for memory bandwidth (DMA), and the screen flickers. So the code turns WiFi on, syncs the clock with NTP, and turns it off again right away:
wifi.radio.enabled = True wifi.radio.connect(ssid, pwd) # ... NTP request ... wifi.radio.enabled = False
The clock syncs once an hour. If a sync fails it tries again after 5 minutes.
Code
The code is split into small files: hardware.py (display, I2C, backlight, encoder), sensors.py, ui.py, wifi_sync.py and config.py. All colors, limits and timings live in config.py, so it is easy to change the thresholds or the time zone.
Full code, parts list and photos: github.com/ostgate/air-pro
What is next
The wooden box works, but the display is held with foam tape. Next step is a 3D printed case with:
- a proper frame for the display
- a separate sensor bay with vents, away from the board heat
- a clear air path for the SEN66 fan
- a real knob cap and a USB-C cutout
Thanks to Adafruit for the Qualia board and the CircuitPython libraries, including the new adafruit_sen6x driver.
This page (AIR PRO: a Pip-Boy style air quality monitor with Qualia S3 and SEN66) was last updated on September 28, 2026.
Text editor powered by tinymce.