How to Build a CO2 Room Monitor with ESP32
Measure true room CO2 with an SCD41 and show ventilation warnings on an OLED
Updated

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
Components needed
Assembly
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.
- Keep the SCD41 away from your fingers, the ESP32 regulator, and direct breath while testing. It reacts quickly, which is useful but can make bench readings jump.
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.
- The SCD41 uses I2C address 0x62. Most SSD1306 OLEDs use 0x3C, so the two devices can sit on the same bus without conflict.
Wire clean power and ground
Connect SCD41 VIN and OLED VCC to ESP32 3V3, then connect both GND pins to ESP32 GND.
- The SCD41 breakout can run from 3.3V to 5V, but quiet power matters. Use short 3.3V wiring for this ESP32 build.
- Do not power the ESP32 from a weak USB port if the readings reset or the OLED flickers.
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.
- The starter warns above 1000 ppm and shows a stronger ventilation prompt above 1500 ppm. Adjust CO2_WARN_PPM and CO2_ALERT_PPM for your room.
- Fresh-air calibration takes time. Treat the first few minutes as a sanity check, not a lab-grade reading.
Pin assignments
| Pin | Connection | Type |
|---|---|---|
| 3V3 | scd41-1 VIN | POWER |
| GND | scd41-1 GND | GROUND |
| GPIO 21 | scd41-1 SDA | I2C |
| GPIO 22 | scd41-1 SCL | I2C |
| 3V3 | oled-1 VCC | POWER |
| GND | oled-1 GND | GROUND |
| GPIO 21 | oled-1 SDA | I2C |
| GPIO 22 | oled-1 SCL | I2C |
Code
#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.ioReady to build this?
Open this project in Schematik to get the full wiring diagram, pin assignments, and deployable code for the CO2 Room Monitor.
Open in Schematik →