Full width home advertisement

Post Page Advertisement [Top]

 I used to get these afternoon headaches at my desk that I could never explain. Turns out my "well-sealed, energy-efficient" home office was actually just quietly accumulating CO2 while I sat there for hours with the door closed. I only found this out because I built an indoor air quality monitor with an ESP32 — and once I saw the CO2 numbers climbing past 1500ppm by mid-afternoon, the headaches suddenly made a lot more sense.

This guide walks through building your own air quality monitor that tracks CO2, temperature, and humidity, with a simple display so you can actually see what's going on in the air around you before it turns into a headache.

ESP32 Indoor Air Quality Monitor - DIY CO2 Sensor Build Guide

Why Indoor Air Quality Actually Matters (Briefly)

Modern homes are sealed tighter than older ones for energy efficiency, which is great for your heating bill and less great for air exchange. CO2 levels can climb surprisingly fast in a closed room with people in it, and while it's not dangerous at moderate levels, it's a well-documented cause of drowsiness, headaches, and difficulty concentrating — exactly the kind of thing you'd blame on "being tired" without ever suspecting the air itself.

What You'll Need

  • 1x ESP32 development board
  • 1x SCD30 or SCD40 CO2 sensor (these use NDIR sensing technology, which is genuinely accurate for CO2 — cheaper "CO2 sensors" that are actually just VOC sensors in disguise will give you misleading readings, so this part is worth spending a little more on)
  • 1x SSD1306 OLED display (128x64, I2C)
  • Jumper wires and a breadboard
  • Optional: a small enclosure with ventilation slots once you've got it working

A quick honest note on the sensor choice: there are much cheaper "CO2 sensors" on the market (often based on the MQ-135) that actually measure general air quality/VOCs and just estimate a CO2-equivalent number from that. They're fine for a general "air freshness" indicator, but if you actually want real CO2 numbers, an NDIR sensor like the SCD30 or SCD40 is the honest choice, even though it costs more.

How This Actually Works

The SCD30/SCD40 uses NDIR (non-dispersive infrared) sensing — it shines infrared light through a small chamber of air and measures how much of that light gets absorbed by CO2 molecules, since CO2 absorbs infrared light at a very specific wavelength. More absorption means more CO2 present. It's a genuinely clever, well-established method, and it's why these sensors give real, trustworthy numbers instead of estimates.

The sensor also happens to measure temperature and humidity as a side effect of its internal design, so you get three useful readings from one module.

Step 1: Wiring the Sensor and Display

Both the CO2 sensor and the OLED display communicate over I2C, so they can share the same two data lines:

SCD30/SCD40 CO2 sensor:

  • VCC → 3.3V
  • GND → GND
  • SCL → GPIO 22
  • SDA → GPIO 21

OLED Display (SSD1306):

  • VCC → 3.3V
  • GND → GND
  • SCL → GPIO 22 (shared with the sensor)
  • SDA → GPIO 21 (shared with the sensor)

Since both devices use the same I2C bus, they can be wired in parallel to the same pins — I2C is designed to let multiple devices share a bus like this, as long as each has a unique address, which these do by default.

Step 2: Installing the Libraries

In the Arduino IDE's Library Manager, install Adafruit SCD30 (or the SCD4x library if you're using the SCD40 variant), along with Adafruit GFX and Adafruit SSD1306 for the display.

ESP32 Indoor Air Quality Monitor - DIY CO2 Sensor Build Guide 2

Step 3: The Code

Here's a working version that reads CO2, temperature, and humidity, and displays them on the OLED screen with a simple color-coded warning based on CO2 level.

cpp
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_SCD30.h>

Adafruit_SSD1306 display(128, 64, &Wire, -1);
Adafruit_SCD30 scd30;

void setup() {
  Serial.begin(115200);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();

  if (!scd30.begin()) {
    Serial.println("Could not find SCD30 sensor - check wiring");
    while (1);
  }
}

void loop() {
  if (scd30.dataReady()) {
    scd30.read();

    float co2 = scd30.CO2;
    float temp = scd30.temperature;
    float humidity = scd30.relative_humidity;

    display.clearDisplay();
    display.setTextSize(1);
    display.setCursor(0, 0);
    display.print("CO2: "); display.print(co2); display.println(" ppm");
    display.print("Temp: "); display.print(temp); display.println(" C");
    display.print("Humidity: "); display.print(humidity); display.println(" %");

    display.setCursor(0, 40);
    if (co2 < 800) {
      display.println("Air quality: Good");
    } else if (co2 < 1200) {
      display.println("Air quality: OK - vent soon");
    } else {
      display.println("Air quality: Poor - open window!");
    }

    display.display();
  }

  delay(2000);
}

For reference on the CO2 thresholds used here: outdoor air typically sits around 400-420ppm. Indoor levels under 800ppm are generally considered good, 800-1200ppm is noticeably stuffy territory where you'll start feeling a bit sluggish, and anything consistently above 1200-1500ppm is a solid sign it's time to open a window or run an air exchanger.

Common Problems (And How I Actually Fixed Them)

The sensor gives no readings or the code hangs at startup. Double-check your I2C wiring, and make sure you're not accidentally powering the sensor at 5V if it's rated for 3.3V — some SCD30 breakout boards are 5V tolerant, but not all, so check your specific module's documentation.

CO2 readings seem unrealistically low, even in a closed room with people. These sensors need a brief warm-up period after power-on, and some models benefit from an initial calibration against fresh outdoor air. If your readings seem consistently off, look up your specific sensor's forced recalibration (FRC) procedure — it's usually a one-time setup step.

The OLED shows garbled text or nothing at all. This is almost always an I2C address conflict. Confirm your OLED's actual address (0x3C is most common, but not universal) and make sure it doesn't clash with your CO2 sensor's address — run an I2C scanner sketch if you're not sure what's actually on the bus.

Readings jump around a lot from one update to the next. CO2 concentration genuinely does fluctuate somewhat with breathing patterns and air movement in a room, especially close to where people are sitting. Averaging a few readings together, or just glancing at the general trend instead of individual spikes, gives a more useful picture than obsessing over every single number.

Taking It Further

Once the basic version is working, a few upgrades are worth considering: adding a small fan that automatically kicks on above a certain CO2 threshold, logging data over time to see patterns across your day (mine, predictably, spikes hardest during long afternoon video calls with the door shut), or sending alerts to your phone via Home Assistant when air quality crosses into "open a window" territory.

ESP32 Indoor Air Quality Monitor - DIY CO2 Sensor Build Guide 3

Final Thoughts

This is one of those builds where the payoff isn't flashy, but it genuinely changes a daily habit. I don't get those mystery afternoon headaches anymore, mostly because I now actually know when to crack a window instead of just pushing through feeling foggy and blaming it on being tired. Sometimes the most useful electronics project isn't the most exciting one — it's just the one that quietly fixes a problem you didn't fully realize you had.

Bottom Ad [Post Page]