Air Quality Monitoring with ESP32: Choosing and Wiring PM2.5, CO2, and VOC Sensors
Our existing Raspberry Pi weather station project covers ambient temperature, humidity, and pressure — genuinely useful, but a different measurement category from air quality, which is about what's actually suspended in the air: particulates, VOCs, and CO2. A maker shop is a specific reason to care about this beyond general home air-quality curiosity, since laser cutting, resin printing, and soldering all put real particulates and off-gassing into shop air. This guide covers the common ESP32-compatible sensor options, how they actually differ, and a gotcha that trips up a lot of first-time builders around what "CO2" readings really mean.
Sensor Comparison
SensorMeasuresInterfaceNotes PMS5003 (Plantower)Particulate matter PM1.0/PM2.5/PM10UART, 3.3V logicLaser-scattering sensor, has a small internal fan, genuinely useful for shop dust/fume particulates SCD40 / SCD41 (Sensirion)True CO2 (NDIR), plus temp/humidityI2CReal, calibrated CO2 in ppm — the sensor to use if CO2 specifically matters SGP30 / SGP40 (Sensirion)VOC index, eCO2 (estimated, not measured)I2CGood general "is the air getting stale" indicator; eCO2 is not a true CO2 reading — see below MQ-135 and similar MQ-seriesBroad, unspecific gas/VOC responseAnalogCheap but poorly calibrated and cross-sensitive to multiple gases; treat as a rough relative indicator onlyThe eCO2 vs. True CO2 Gotcha
This trips up more first-time builds than almost anything else in this space: an SGP30's "eCO2" output is not a measured CO2 concentration — it's an estimate derived mathematically from the sensor's actual VOC reading, calibrated against typical indoor air patterns. It can track real CO2 reasonably well in a normal occupied room, but it will not reliably reflect true CO2 levels in a shop with soldering fumes, resin off-gassing, or laser fumes present, since those VOCs skew the estimate independent of actual CO2 concentration. If you specifically want a real CO2 number — for shop ventilation adequacy, for instance — use an NDIR sensor like the SCD40, which measures CO2 directly via infrared absorption rather than estimating it from something else.
Wiring the I2C Sensors (SCD40/SGP30)
Both connect over standard I2C: VCC to the ESP32's 3.3V rail, GND to ground, SDA to the ESP32's I2C data pin (commonly GPIO21 on most ESP32 dev boards), and SCL to the I2C clock pin (commonly GPIO22). Both sensors have well-supported Arduino libraries (Sensirion publishes official ones for both) that handle the sensor-specific initialization and register reads, so the wiring is the easy part — the SCD40 in particular needs a short warm-up period after power-on before its first reading stabilizes, so don't judge the sensor as faulty from an odd number in the first few seconds after boot.
Wiring the PMS5003 (UART)
The PMS5003 communicates over UART rather than I2C, so wire its TX pin to one of the ESP32's RX-capable GPIO pins (using a hardware or software serial port, not the same UART used for USB programming/debug output) and power it from 5V if your specific breakout requires it for the internal fan, checking your board's regulator situation for logic-level compatibility on the data lines. The sensor's internal fan needs a few seconds to draw a stable air sample before a reading is meaningful, and continuous operation shortens the fan's mechanical lifespan somewhat — many builds duty-cycle it (run for 30 seconds, sleep for a few minutes) rather than running it continuously, which is a reasonable tradeoff for most non-critical monitoring use cases.
Shop-Specific Applications
A PMS5003 near a laser cutter's fume extraction outlet or a CNC's dust collection port gives you an actual number for whether your extraction and filtration setup is keeping ambient particulates down, rather than judging purely by smell or visible haze. An SCD40 in a room with a heated 3D print enclosure or a resin printer gives you a real read on whether ventilation is adequate, especially relevant if the room is small or doesn't have great natural air exchange.
Safety Notes
None of these sensors are a substitute for a certified carbon monoxide detector if you're running any combustion source (a shop heater, an engine) in the same space — use a real UL-listed CO detector for that specific hazard rather than relying on a hobbyist gas sensor's cross-sensitivity to CO as an incidental warning system. Likewise, an MQ-series sensor's broad, uncalibrated response should be treated as a relative trend indicator (getting worse vs. better) rather than a number you'd make a health or safety decision on in isolation.
Air quality sensing is one of the more genuinely useful applications for a spare ESP32 sitting in a shop — paired with the right sensor for the question you're actually asking (particulates vs. true CO2 vs. general VOC trend), it turns "does this room feel stuffy" into an actual logged number you can act on.
Related Guides
- I2C vs SPI vs UART: How to Choose and Use Serial Communication Protocols
- I2C Wiring and Protocol Guide for Arduino, ESP32, and Raspberry Pi
- How to Use a Logic Analyzer for Digital Signal Debugging: Saleae, Sigrok, and Protocol Decoding
- How to Use the Flipper Zero GPIO for Hardware Hacking: UART, SPI, I2C, and Debugging
- Using Flipper Zero as a USB-to-UART Serial Bridge
- Flipper Zero GPIO Pinout and Hardware Expansion: UART, I2C, ADC, 1-Wire, and Sensor Wiring
- Raspberry Pi GPIO: Complete Beginner Guide with Python Examples
- Shift Registers and I/O Expanders for Arduino and ESP32: 74HC595, MCP23017, and PCF8574 Explained