AirAlert is a small indoor air-quality monitor I built on an Arduino UNO with three sensors and a strip of LEDs. Green means the air in the room is fine. Yellow means I should think about opening a window. Red means I already should have. There is an optional NodeMCU ESP8266 that can push the readings to a server over Wi-Fi, but the default mode is offline, glanceable, and stupid in the good way. Repo: github.com/aelmufti/AirAlert.
The thing I want to write down is not the build process. It is what I learned about consumer air-quality sensors after spending a few weeks staring at their datasheets side by side. The short version: the three sensors I used do not measure the same thing, and most online tutorials gloss over that until the readings contradict each other and someone files an issue.
The three sensors, and what they actually do
BME680 (Bosch). A four-in-one MEMS sensor: temperature, relative humidity, barometric pressure, and a gas resistance reading that Bosch's BSEC library massages into an "Indoor Air Quality" index. Temperature, humidity, and pressure are trustworthy. The IAQ index is a derived, slow-moving, vendor proprietary number. It is fine for trend lines, not for absolute claims.
MQ-135. A cheap tin-dioxide resistor that changes resistance when exposed to a mixed bag of gases — ammonia, NOx, alcohol, benzene, smoke, and yes, CO₂. The number printed on its datasheet under "CO₂ sensitivity" is essentially a vibe. If a sensor that costs less than a coffee could measure CO₂ to ppm accuracy, no one would buy MH-Z19. Treat MQ-135 as a coarse "something changed in the room" detector, not a CO₂ meter.
MH-Z19 (Winsen). A non-dispersive infrared (NDIR) CO₂ sensor. NDIR is the actual technology used in commercial CO₂ monitors. It is selective for CO₂ in a way the MQ-135 is not. It also costs maybe ten times more. When the two sensors disagree about CO₂, MH-Z19 is right.
The problem this caused
My first wiring put all three sensors on the breadboard, summed their "air quality" outputs into one score, and lit the LEDs from that score. The system behaved erratically. The MQ-135 would spike on a cup of hot coffee or a passing perfume cloud and the LED would go red while CO₂ from MH-Z19 was still in the low 500 ppm range. The score was the average of "is there a chemical anomaly in the room" and "how stuffy is it" — two different questions that deserve different answers.
The fix was conceptual, not electrical. I split the indicator into two layers: a primary ventilation signal driven only by MH-Z19 (because the use case is "should I open a window") and a secondary anomaly signal that uses MQ-135 + the BME680 gas index. The anomaly signal does not light the headline LED; it flashes a small auxiliary one. Most of the time only the ventilation signal is doing anything, which is what I actually wanted.
Calibration is the boring step you cannot skip
MH-Z19 ships with Automatic Baseline Calibration (ABC) enabled. The assumption baked into ABC is that the sensor is exposed to outdoor air (~415 ppm CO₂) at least once every 24 hours, and it re-zeros against that minimum. In a real apartment that assumption is sometimes false. If you keep the sensor in a closed bedroom permanently it will drift up because it never sees a clean baseline.
Either you periodically air the room and trust ABC, or you turn ABC off and do manual zero calibration outdoors every few months. For a hobby device I left ABC on and added a comment in the firmware so future-me would know why readings might creep.
MQ-135 also needs a long burn-in. The datasheet says 24 hours powered before the readings stabilize. Mine took closer to 48. Plan a power-cycle window before you start interpreting the numbers.
Why I made the Wi-Fi optional
The minimal viable monitor is three LEDs. It does not need to live on the network, it does not need an app, and it does not need to upload sensor data about my breathing to anyone. The ESP8266 uplink path is there because I wanted to graph long-term trends, but it is opt-in: leave the NodeMCU unconnected and the UNO runs the same firmware and lights the same LEDs. Choosing offline-by-default for a sensor in your bedroom is the polite design.
What I would change today
- Drop the MQ-135. Its readings rarely added information that the BME680 gas index did not already cover, and it muddied the calibration story.
- Move from an Arduino UNO to a single ESP32. The UNO + ESP8266 combo predates my familiarity with the ESP32, and a single board with Wi-Fi and Bluetooth on-chip is simpler than two boards that have to agree.
- Log to flash, not just to the network. If you ever want to reason about overnight CO₂ in your bedroom, having three months of local data beats whatever was on a dashboard you forgot to look at.
The takeaway
Hobby sensor projects fail the same way: they trust their cheapest sensor as if it were calibrated lab equipment, average unrelated signals into one number, and ship a green/yellow/red light that says everything is fine while the air is not. AirAlert is mainly a study in not doing that. Pick one signal you actually care about, route it through the sensor that actually measures it, and reserve everything else for the secondary display.
Top comments (0)