How to Build a CO2 Room Monitor with ESP32

Measure true room CO2 with an SCD41 and show ventilation warnings on an OLED

ESP32Smart HomeBeginner35 minutes2 components

Updated

How to Build a CO2 Room Monitor with ESP32
For illustrative purposes only
On this page

What you'll build

This guide takes you through building a small room CO2 monitor with an ESP32, an Adafruit SCD41 true CO2 sensor, and a compact SSD1306 OLED display. The SCD41 measures carbon dioxide directly instead of estimating it from VOCs, then the OLED shows live CO2 in ppm alongside temperature, humidity, and a plain ventilation warning when the room starts getting stale. Both modules share the same I2C bus on GPIO21 and GPIO22, so the wiring stays simple: power, ground, SDA, and SCL for each board.

The starter reads the SCD41 every five seconds and uses two thresholds to make the display useful without turning the build into a full dashboard. Below 1000 ppm, it reports that the air looks good. From 1000 ppm, it says the air is getting stale. From 1500 ppm, it switches to a stronger ventilation prompt. Those values are defined as CO2_WARN_PPM and CO2_ALERT_PPM, so you can tune them once you understand your own room. The SCD41 breakout can run from 3.3V to 5V, but this ESP32 build uses the 3V3 rail and short wiring because quiet power matters for stable readings.

By the end of the build you will have a desk-sized indoor air monitor that reacts visibly when CO2 rises, then settles again after fresh air reaches the sensor. For a build video, the payoff is clear: show the OLED reading in normal room air, breathe near the sensor to prove the response, then open a window and watch the warning disappear. If you want a simpler environmental starter first, try the ESP32 air quality monitor; if you want an outdoor-data version, the ESP32 weather station is the natural companion.

Wiring diagram

Wiring diagram

Interactive wiring diagram

Components needed

ComponentTypeQtyBuy
Adafruit SCD41 CO2 Sensorsensor1€7.85
SSD1306 OLEDdisplay1€4.30

Prices and availability are indicative and may have been updated by the supplier. Schematik may earn a commission from purchases made through affiliate links.

Assembly

1

Mount the ESP32, SCD41, and OLED

Place the ESP32 on a breadboard, then position the SCD41 and OLED so the I2C and power wiring stays short.

2

Connect shared I2C wiring

Connect SCD41 SDA and OLED SDA to GPIO21. Connect SCD41 SCL and OLED SCL to GPIO22. Both modules share the same I2C bus.

3

Wire clean power and ground

Connect SCD41 VIN and OLED VCC to ESP32 3V3, then connect both GND pins to ESP32 GND.

4

Upload and ventilate-test the monitor

Install the Adafruit SCD4X, Adafruit SSD1306, and Adafruit GFX libraries, upload the sketch, then watch the OLED update every five seconds.

Pin assignments

PinConnectionType
3V3scd41-1 VINPOWER
GNDscd41-1 GNDGROUND
GPIO 21scd41-1 SDAI2C
GPIO 22scd41-1 SCLI2C
3V3oled-1 VCCPOWER
GNDoled-1 GNDGROUND
GPIO 21oled-1 SDAI2C
GPIO 22oled-1 SCLI2C

Code

Arduino C++
#include <Wire.h>
#include <Adafruit_SCD4X.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SDA_PIN 21
#define SCL_PIN 22
#define OLED_ADDRESS 0x3C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define CO2_WARN_PPM 1000
#define CO2_ALERT_PPM 1500

Adafruit_SCD4X scd4x;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

unsigned long lastReading = 0;
uint16_t co2 = 0;
float temperatureC = 0;
float humidity = 0;

void drawMonitorScreen() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("Room CO2 monitor");
  display.drawLine(0, 11, 127, 11, SSD1306_WHITE);

  display.setTextSize(2);
  display.setCursor(0, 18);
  display.print(co2);
  display.println("ppm");

  display.setTextSize(1);
  display.setCursor(0, 42);
  display.print(temperatureC, 1);
  display.print("C  ");
  display.print(humidity, 0);
  display.println("%RH");

  display.setCursor(0, 54);
  if (co2 >= CO2_ALERT_PPM) {
    display.fillRect(76, 52, 52, 12, SSD1306_WHITE);
    display.setTextColor(SSD1306_BLACK);
    display.setCursor(81, 54);
    display.print("VENT!");
  } else if (co2 >= CO2_WARN_PPM) {
    display.print("Air getting stale");
  } else {
    display.print("Air looks good");
  }

  display.display();
}

void setup() {
  Serial.begin(115200);
  delay(100);

  Wire.begin(SDA_PIN, SCL_PIN);

  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
    Serial.println("SSD1306 not found. Check OLED wiring and address.");
    while (true) delay(1000);
  }

  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("Starting CO2 sensor...");
  display.display();

  if (!scd4x.begin()) {
    Serial.println("SCD41 not found. Check STEMMA QT cable or I2C wiring.");
    display.clearDisplay();
    display.setCursor(0, 0);
    display.println("SCD41 not found");
    display.println("Check I2C wiring");
    display.display();
    while (true) delay(1000);
  }

  scd4x.stopPeriodicMeasurement();
  scd4x.startPeriodicMeasurement();
}

void loop() {
  if (millis() - lastReading < 5000) {
    return;
  }
  lastReading = millis();

  sensors_event_t humidityEvent;
  sensors_event_t temperatureEvent;
  uint16_t newCo2;

  if (scd4x.readMeasurement(newCo2, temperatureEvent, humidityEvent)) {
    co2 = newCo2;
    temperatureC = temperatureEvent.temperature;
    humidity = humidityEvent.relative_humidity;

    Serial.print("CO2: ");
    Serial.print(co2);
    Serial.print(" ppm, temp: ");
    Serial.print(temperatureC);
    Serial.print(" C, humidity: ");
    Serial.print(humidity);
    Serial.println(" %RH");

    drawMonitorScreen();
  }
}

// Run this and build other cool things at schematik.io
Libraries: Adafruit SCD4X Library, Adafruit SSD1306, Adafruit GFX Library