Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Build this monitor around an ESP32, an SCD40 or SCD41 CO₂ sensor, and an SSD1306 OLED. That minimum combination gives you a useful local ventilation monitor: CO₂, temperature, humidity, connection status, and sensor freshness on both the OLED and a browser dashboard. Add a PMS5003 when particulate matter matters, or a BME680 when you want environmental context and a relative gas-response signal.
This is not a regulatory air-quality station. CO₂ indicates ventilation and occupancy-related buildup; PM2.5 indicates fine particles; a BME680 indicates changes in a mixed gas environment. No single low-cost sensor measures every indoor pollutant or automatically produces a defensible official AQI.
As an Amazon Associate I earn from qualifying purchases.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →What this ESP32 monitor actually measures
Use separate, honest labels instead of collapsing every reading into one unsupported “air quality” score.
| Measurement | Useful interpretation | What it does not prove |
|---|---|---|
| CO₂ | Ventilation and occupancy-related buildup | That all other pollutants are low |
| PM2.5 | Fine airborne particle concentration | Particle chemistry or source |
| PM10 | Larger inhalable particles | Overall chemical safety |
| Temperature | Thermal comfort and sensor context | Air cleanliness |
| Relative humidity | Moisture and comfort context | The presence or absence of mold |
| BME680 gas signal | Relative changes in a mixed gas environment | Exact VOC, formaldehyde, or CO₂ concentration |
| AQI | An index calculated from defined pollutant data | A valid result from arbitrary ADC scaling |
Call the readings CO₂ level, PM2.5 concentration, and VOC-related signal. Use “below this project’s configured alert threshold,” not “safe,” unless you are explicitly applying a named guideline and its required averaging period.
Popular on Amazon
- Know your air – Amazon Smart Air Quality Monitor makes it easy to understand what’s in your indoor air.
- Track and measure – Keep tabs on 5 key factors: particulate matter (PM 2.5), volatile organic compounds (VOCs), carbon monoxide (CO), humidity, and temperature.
- Stay informed – Get an indication of current air quality from the color-coded LED, and an easy-to-understand air quality score in the Alexa app anytime.
- Get notifications – If Alexa detects poor air quality, you can get notifications on your phone or even announcements on Echo devices.
- Enable Routines to turn your Alexa-enabled devices such as air purifiers, dehumidifiers, and fans on and off when the indoor air quality changes.
Choose a sensor configuration
| Priority | Configuration | Trade-off |
|---|---|---|
| Lowest complexity | SCD40 or SCD41 + SSD1306 | Ventilation-focused; no particulate measurement |
| Balanced indoor monitor | SCD41 + BME680 + SSD1306 | More context, cost, and calibration considerations |
| PM2.5 focus | PMS5003 + ESP32 + OLED | Larger, noisier, and higher power consumption |
| Smart-home integration | ESPHome + Home Assistant | Less custom firmware and UI control |
| Educational analog experiment | MQ-135 + ESP32 ADC | Relative signal only; poor quantitative reliability |
Recommended sensors
The Sensirion SCD40 and SCD41 use photoacoustic NDIR sensing for CO₂ and include temperature and humidity compensation. The SCD40 is generally the lower-cost choice; the SCD41 is the higher-performance variant. Follow the current model-specific documentation for accuracy, operating conditions, calibration, and measurement timing.
The Bosch BME680 measures temperature, humidity, pressure, and gas resistance over I²C or SPI. Bosch also provides BSEC software for processed IAQ-related outputs. Treat its gas result as a relative response or estimate, not a direct measurement of a named pollutant.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
The PMS5003 is a UART optical particle sensor. Adafruit documents 9600-baud output, a nominal 5 V supply, approximately 0–500 µg/m³ effective range, and active current that can reach about 100 mA. Verify the exact module and breakout documentation, especially UART signal levels, before connecting it to an ESP32 input.
Bill of materials
- ESP32 development board with 3.3 V GPIO, accessible I²C pins, and a hardware UART.
- SCD40 or SCD41 breakout for the minimum build.
- 128×64 SSD1306 I²C OLED breakout.
- Stable USB power supply and a good-quality cable.
- Optional BME680 breakout.
- Optional PMS5003 and suitable cable or level-shifting hardware.
- Ventilated enclosure, jumper wires, and a breadboard or soldered prototyping board.
Check the exact board pinout rather than assuming that every ESP32 variant exposes the same GPIO numbers. Espressif’s ESP32 documentation describes the family’s Wi-Fi, I²C, UART, SPI, ADC, and power features, but board layouts differ.
Wiring
Use one shared I²C bus for the OLED, SCD4x, and BME680:
OLED SDA → ESP32 SDA, example GPIO 21
OLED SCL → ESP32 SCL, example GPIO 22
SCD40/SCD41 → same SDA and SCL bus
BME680 → same SDA and SCL bus
All devices → common GND
PMS5003 TX → ESP32 hardware-UART RX pin
GPIO 21 and 22 are example assignments, not universal requirements. Set the pins in firmware according to your board’s pinout. The PMS5003 is typically powered from 5 V and may consume considerably more current than the I²C sensors. Its TX connection must be electrically compatible with the ESP32’s 3.3 V input; do not assume a purchased module is safe without checking its documentation or using appropriate level shifting.
For the OLED, connect VCC to 3.3 V when the module supports it, GND to GND, and SDA/SCL to the I²C bus. Common OLED modules use address 0x3C, but your scanner—not a copied example—should determine the actual address.
Scan the I²C bus before writing the full program
Install the ESP32 board package, select the correct board, upload this diagnostic sketch, and open Serial Monitor at 115200 baud:
Also popular on Amazon
- 【16-in-1 Air Quality Monitor Indoor】Experience the ultimate indoor air quality monitoring with our 16-in-1 Air Quality Monitor, offering real-time detection of 9 key parameters including CO2, PM2.5, PM1.0, PM10, HCHO, TVOC, Temperature, Humidity, AQI, and Time. With 7 distinct AQI alert buzzers, this air quality tester ensures your family breathes with ease.(*Note: "16-in-1" refers to the combination of 9 key detectable parameters and 7 types of AQI alert buzzers.)
- 【Crystal Clear 7-inch Large Display】Enjoy a 7-inch LED display for sharp, clear air quality readings. Provides an instant, comprehensive view of your indoor air without navigating through menus, with three brightness settings for any lighting condition
- 【External High-Precision Sensors with 0.001 Accuracy】Equipped with advanced external high-precision sensors, this device delivers unmatched accuracy (0.001 units) by directly sampling the air. Its innovative multi-sensor array and enhanced airflow design detect even the slightest environmental changes, allowing for instant response and optimal safety. (*Note: Avoid touching the sensors or exposing them to perfumes/strong odors to maintain accuracy.)
- 【Real-Time AQI Alert Buzzers】Our air quality monitor provides real-time monitoring and alerts for pollutants like CO2, PM2.5, PM1.0, PM10, HCHO, TVOC, Temperature, Humidity, and AQI, with 7 distinct alert functions. Stay informed with clear alerts and rest easy with a mute button to silence alarms. Your health and comfort are our priority. Toggle between Fahrenheit and Celsius instantly with a double-press of the "Power" button on this air quality detector.
- 【Up to 8 Hours of Runtime & Flexible USB-C Charging】 Enjoy up to 8 hours of continuous air quality monitoring on a full charge. The USB-C charging interface offers flexible power options and is compatible with widely available USB-C cables, while the included AC adapter provides convenient charging at home. Designed for effortless monitoring wherever you need it—from bedrooms and living spaces to your car and beyond.
#include <Wire.h>
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
Serial.println("I2C scan");
for (uint8_t address = 1; address < 127; address++) {
Wire.beginTransmission(address);
uint8_t error = Wire.endTransmission();
if (error == 0) {
Serial.printf("Found device at 0x%02Xn", address);
}
}
}
void loop() {}
You should see one or more hexadecimal addresses. If the result is empty, check power, ground, SDA/SCL order, selected GPIOs, pull-up resistors, and wire length. Multiple breakout boards may each include pull-ups; excessive parallel pull-ups can make the bus unreliable.
Test each device independently
- OLED: initialize the display and show a test message. If it stays blank, verify the address, dimensions, reset-pin configuration, and whether the controller is SSD1306 or SH1106.
- SCD40/SCD41: start periodic measurements and print CO₂, temperature, and humidity. Allow the sensor to warm up and obtain valid measurements before interpreting them.
- BME680: confirm temperature, humidity, pressure, and gas-resistance output. Handling, solvents, cooking fumes, cleaning products, and enclosure materials can change the gas signal.
- PMS5003: confirm its 5 V supply, common ground, UART RX connection, 9600-baud configuration, fan operation, and frame parser.
- Wi-Fi: connect separately and print the assigned IP address.
- Web server: serve a plain test page before combining sensor rendering and JavaScript.
Testing in this order turns a confusing combined failure into a specific wiring, power, address, or software problem.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Install the firmware dependencies
A custom Arduino implementation normally uses:
WiFi.hfrom the ESP32 Arduino core;WebServer.hfor a simple HTTP server;Wire.hfor I²C;- an SSD1306 library such as Adafruit SSD1306;
- an SCD4x Arduino driver;
- a BME680 library, optionally with Bosch BSEC;
- a PMS5003 parser library or a UART frame parser.
Pin the ESP32 Arduino-core version and library versions in the project repository. APIs and behavior can change, so “latest” is not a permanent reproducibility label.
Connect to Wi-Fi without blocking the monitor
The Arduino-ESP32 Wi-Fi documentation provides station-mode examples. A bounded connection attempt should look like this:
#include <WiFi.h>
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED &&
millis() - start < 20000) {
delay(250);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.print("nIP address: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("nWi-Fi connection failed");
}
}
In the completed firmware, retry periodically rather than waiting forever. Sensor sampling and OLED updates should continue when Wi-Fi is unavailable. Show an offline status and never erase a valid sensor reading merely because the network disappeared.
Use a non-blocking firmware design
Keep independent timers for sensor reads, OLED refresh, Wi-Fi reconnect attempts, logging, and other work. Long delays make the dashboard appear frozen and can prevent timely serial parsing.
Recommended Free Tools
Store each reading with its value, validity flag, and timestamp. For example:
struct Readings {
float temperatureC;
float humidityRH;
uint16_t co2Ppm;
float pm25;
float pm10;
float gasResistance;
unsigned long lastUpdate;
bool co2Valid;
bool particulateValid;
bool bmeValid;
};
When a read fails, retain the previous value only while marking it stale. After a defined timeout, return null, --, or a clear sensor-error state instead of presenting old data as current.
Also popular on Amazon
- 4-in-1 Environmental Monitoring: Simultaneously measures temperature (-40°C to +85°C), humidity (±3% RH), barometric pressure, and VOC gases (IAQ index output).
- Multi-Protocol Interface: 5V compatible with I2C (3.4MHz max) and SPI (10MHz 3/4-wire) for Arduino/Raspberry Pi/ESP32 integration.
- Ultra-Low Power Operation: Current as low as 2.1μA (1Hz temp/humidity) to 3.7μA (triple-sensor mode) with selectable sensor activation.
- Industrial-Grade Precision: VOC response time <1s (new sensor), ±1.5% RH humidity hysteresis, and IAQ air quality indexing capability.
- Ready-to-Use Module: Includes pre-soldered BME680 chip with labeled VCC/GND/SCL/SDA/SDO/CS pins (2x module per order).
Build the local dashboard
The simplest architecture is a web server on the ESP32:
Browser → HTTP request → ESP32
├── HTML/CSS/JavaScript
└── JSON sensor readings
Use routes such as:
GET /— dashboard page;GET /api/readings— latest values and timestamps;GET /api/status— firmware version, uptime, RSSI, IP address, and error flags.
An example JSON response is:
{
"co2_ppm": 812,
"temperature_c": 22.8,
"humidity_rh": 47.2,
"pm25_ugm3": 5,
"pm10_ugm3": 8,
"gas_resistance_ohms": 183420,
"sensor_age_ms": 1240,
"wifi_rssi_dbm": -58
}
Poll every 2–10 seconds, depending on the sensor schedule. Do not confuse browser refresh with measurement speed: a page can refresh every five seconds while a sensor produces a new sample less often.
async function updateReadings() {
try {
const response = await fetch("/api/readings", {cache: "no-store"});
if (!response.ok) throw new Error("HTTP " + response.status);
const data = await response.json();
document.querySelector("#co2").textContent = `${data.co2_ppm} ppm`;
document.querySelector("#temperature").textContent =
`${data.temperature_c.toFixed(1)} °C`;
document.querySelector("#humidity").textContent =
`${data.humidity_rh.toFixed(1)} %`;
} catch (error) {
document.querySelector("#status").textContent =
"Dashboard cannot reach the ESP32";
}
}
setInterval(updateReadings, 5000);
updateReadings();
Use responsive cards with large values and small units. Include a visible timestamp or sensor age, Wi-Fi status, and an offline or stale state. A good OLED layout can rotate through three pages:
- CO₂, temperature, and humidity;
- PM2.5 and PM10;
- VOC-related signal and Wi-Fi status.
Local dashboard, cloud service, or Home Assistant?
A local ESP32 dashboard needs no cloud account, has low latency, and keeps readings on the local network. Its limitations are equally important: it is unreachable when the device or local network is down, the IP address may change, and it has little historical storage unless you add it.
A cloud service can provide remote access and long-term charts, but introduces account management, API keys, internet dependency, rate limits, and privacy concerns. Never publish Wi-Fi passwords or cloud API keys in source code. If you use ThingSpeak or another service, verify current plan limits and API requirements separately.
For an existing smart home, ESPHome with Home Assistant is often the faster route to history, dashboards, automations, and alerts. ESPHome’s web server is intended to view and edit device state, not to replace a full general-purpose application backend.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallInterpretation: avoid homemade AQI
Keep these concepts separate:
- Raw sensor value: an ADC count or resistance.
- Concentration: a measured quantity with units, such as µg/m³.
- Guidance threshold: a configured value that triggers a project warning.
- Official AQI: a pollutant-specific index calculated using defined units, averaging periods, breakpoint tables, and a jurisdiction.
A weighted average of MQ-135 and MQ-2 ADC values is not equivalent to the U.S. EPA AQI. If you implement an official-style index, link to the current U.S. EPA/AirNow AQI methodology and identify the pollutant, units, averaging period, breakpoint revision, and jurisdiction. Otherwise, use labels such as CO₂: 812 ppm, PM2.5: 5 µg/m³, and VOC sensor signal: improving.
MQ-135 modules can be useful for demonstrating analog sensing, but their output varies with heater temperature, humidity, calibration gas, resistor values, supply voltage, sensor variation, warm-up, enclosure, airflow, and cross-sensitivity. Do not present a normalized MQ-135 number as a trustworthy 0–500 AQI.
Also popular on Amazon
- High Accuracy & Fast Refresh Data: With this smart sensor, the PM2.5 accuracy is ±15 µg/m³ while temperature and humidity accuracies are ±0.54°F and ±3%RH.The two-second correction data feature shows the latest changes in PM2.5, temperature, and humidity.Keep sensor clear for accurate detection.
- Multifunctional Air Quality Detector: The GoveeLife Air Quality Monitor conveniently measures 3 important indexes for indoor air quality, including PM2.5, temperature, and humidity.
- Switchable Display: Press the top button for the clock & PM2.5 display. Long press for 2 seconds to switch to bright screen mode & night mode. The LED indicator displays 4 levels of ambient air quality. 2.4G Wi-Fi is required to display the time.
- Connect with GoveeHome Appliances: Set your target air quality and link with your other GoveeHome smart appliances. GoveeLife air purifiers, humidifiers, and space heaters will turn on and off automatically when the indoor air quality changes.
- H5106 needs to be connected to a power source and supports GoveeLife devices: Smart Air Purifiers - H7126, H7120, H7124, H712C, H7122, H7123; Humidifiers - H7140; Fans - H7100, H7102
Placement, warm-up, and calibration
- Allow each sensor to reach its normal operating state before interpreting trends.
- Keep sensors away from the ESP32 regulator, USB converter, and other heat sources.
- Provide enclosure vents without directing strong drafts across the sensing elements.
- Keep condensation away from electronics and sensing openings.
- Give the PMS5003 unobstructed intake and exhaust airflow; keep its warm fan exhaust away from temperature and humidity sensors.
- For CO₂, compare against a known-good reference or outdoor/known-fresh-air condition while following the manufacturer’s calibration guidance. Do not assume automatic baseline calibration is appropriate in every room.
A DIY comparison is not laboratory calibration. Compare this monitor with a known-good reference, a second sensor, or controlled events such as occupancy, opening a window, cooking, or ventilation. Look for plausible direction and response time, not just a matching number.
Troubleshooting
OLED is blank
Run the I²C scanner, verify SDA/SCL and power, try the detected address, confirm the display dimensions and reset-pin setting, shorten the wires, and check whether the module uses SH1106 instead of SSD1306.
No I²C devices appear
Check common ground, supply voltage, pin definitions, pull-ups, connector orientation, and whether the breakout is actually powered. Test one device at a time.
PMS5003 reports zero
Check the specified 5 V supply, current capacity, common ground, UART RX pin, 9600-baud setting, frame parser, fan, airflow, and conflicts with the USB serial port. Confirm the purchased breakout’s signal-level requirements.
CO₂ is fixed or implausibly low
Check initialization and periodic measurement commands, stale-value handling, room-air access, operating time, calibration assumptions, and whether the sensor is sealed inside the enclosure.
Dashboard loads but values do not change
Open /api/readings directly, inspect the browser console, verify route spelling and JSON syntax, check polling frequency, and ensure sensor reads do not block the web server. Cross-origin settings matter only when the page is hosted on a different origin.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteWi-Fi fails or the IP changes
Use a connection timeout, retry periodically, print the assigned IP at boot, and consider a DHCP reservation. An mDNS hostname can help, but support varies by router and client. The OLED should still show local readings while Wi-Fi is offline.
The device resets randomly
Start with power: use a stable supply, a short capable USB cable, and enough current for Wi-Fi and the PM fan. Then investigate voltage drop, watchdog resets, memory fragmentation from repeated String allocation, blocking code, and invalid sensor-data handling.
Useful extensions
- Rolling averages and trend arrows to reduce noisy displays.
- Clearly documented alert thresholds and warning states.
- MQTT for local publishing.
- Home Assistant or ESPHome integration.
- SD-card or server-side historical logging.
- OTA firmware updates.
- Captive-portal Wi-Fi setup instead of hard-coded credentials.
- Touch, e-paper, or larger displays.
- Reduced sampling and Wi-Fi duty cycles for battery experiments.
Continuous Wi-Fi and a continuously running PMS5003 fan make a complete battery-powered monitor substantially more demanding than an I²C-only node. Measure the assembled device before calling it low-power.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

