Difficulty: Intermediate
Time Required: 2 hours
Cost: $15-20
ESP Board: ESP32
What You’ll Need
| Component | Approximate Cost | Where to Buy |
|---|---|---|
| ESP32 Dev Board | $5-8 | Amazon |
| MQ-135 Sensor | $2-4 | Amazon |
| DHT22 Sensor | $3-5 | Amazon |
| OLED Display (Optional) | $2-4 | Amazon |
| Jumper Wires | $2 | Amazon |
Circuit Diagram
| ESP32 Pin | Component | Notes |
|---|---|---|
| 3.3V | MQ-135 VCC | Power |
| GND | MQ-135 GND | Ground |
| GPIO 34 | MQ-135 AOUT | Analog output |
| 3.3V | DHT22 VCC | Power |
| GND | DHT22 GND | Ground |
| GPIO 4 | DHT22 DATA | Data pin |
Step 1: Install Libraries
- DHT sensor library – Adafruit GitHub
- Adafruit Unified Sensor – Required dependency
Step 2: The Complete Code
/*********************************************************************
* ESP32 Air Quality Monitor with MQ Sensors
*
* Measures: CO2, Smoke, NH3, Temperature, Humidity
* Hardware: ESP32, MQ-135, DHT22
*
* Libraries Required:
* - DHT sensor library (Adafruit)
* - Adafruit Unified Sensor
*********************************************************************/
#include
#include
#include "DHT.h"
// ============== DHT CONFIGURATION ==============
#define DHTPIN 4 // GPIO 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// ============== MQ-135 CONFIGURATION ==============
#define MQ135_PIN 34 // GPIO 34 (ADC)
// Calibration values
#define RLOAD 10.0 // Load resistance in kOhms
#define RZERO 76.63 // Calibration resistance
#define CO2_RZERO 76.63
#define CO2_SCALINGFACTOR 116.6020682
#define CO2_EXPONENT -2.769034857
// ============== WIFI CONFIGURATION ==============
const char* wifi_ssid = "YOUR_WIFI_NAME";
const char* wifi_password = "YOUR_WIFI_PASSWORD";
// ============== THINGSPEAK CONFIGURATION ==============
String apiKey = "YOUR_THINGSPEAK_API_KEY";
String server = "http://api.thingspeak.com/update";
// ============== DISPLAY CONFIGURATION ==============
#define BAUD_RATE 115200
#define READ_INTERVAL 5000 // 5 seconds
unsigned long lastRead = 0;
// ============== SETUP ==============
void setup() {
Serial.begin(BAUD_RATE);
Serial.println(F("\n======================================"));
Serial.println(F("ESP32 Air Quality Monitor Starting..."));
Serial.println(F("======================================"));
// Initialize DHT sensor
dht.begin();
// Configure ADC
analogReadResolution(12); // 12-bit ADC (0-4095)
// Connect to WiFi
connectToWiFi();
Serial.println(F("Setup complete!"));
Serial.println(F("Reading air quality data...\n"));
}
// ============== MAIN LOOP ==============
void loop() {
if (millis() - lastRead > READ_INTERVAL) {
lastRead = millis();
readSensors();
}
}
// ============== FUNCTIONS ==============
void readSensors() {
// Read DHT22 data
float temperature = dht.readTemperature(); // Celsius
float humidity = dht.readHumidity(); // Percentage
// Read MQ-135 data
int sensorValue = analogRead(MQ135_PIN);
float voltage = sensorValue * (3.3 / 4095.0);
// Calculate CO2 equivalent
float co2 = calculateCO2(sensorValue);
// Calculate resistance
float resistance = calculateResistance(sensorValue);
// Print to Serial
Serial.println(F("========== AIR QUALITY DATA =========="));
Serial.print(F("Temperature: ")); Serial.print(temperature, 1); Serial.println(F(" C"));
Serial.print(F("Humidity: ")); Serial.print(humidity, 1); Serial.println(F(" %"));
Serial.print(F("Sensor Value: ")); Serial.println(sensorValue);
Serial.print(F("Voltage: ")); Serial.print(voltage, 3); Serial.println(F(" V"));
Serial.print(F("Resistance: ")); Serial.print(resistance, 1); Serial.println(F(" kOhms"));
Serial.print(F("CO2: ")); Serial.print(co2, 0); Serial.println(F(" ppm"));
Serial.println(F("=====================================\n"));
// Send to ThingSpeak
sendToThingSpeak(temperature, humidity, co2);
}
float calculateResistance(int rawValue) {
// Calculate sensor resistance
float voltage = rawValue * (3.3 / 4095.0);
return (3.3 - voltage) / voltage * RLOAD;
}
float calculateCO2(int rawValue) {
// Calculate CO2 concentration using MQ-135
float resistance = calculateResistance(rawValue);
float ratio = resistance / RZERO;
float co2 = CO2_SCALINGFACTOR * pow(ratio, CO2_EXPONENT);
return co2;
}
void sendToThingSpeak(float temp, float hum, float co2) {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
String url = server + "?api_key=" + apiKey;
url += "&field1=" + String(temp);
url += "&field2=" + String(hum);
url += "&field3=" + String(co2);
http.begin(url);
int httpCode = http.GET();
if (httpCode > 0) {
Serial.println(F("Data sent to ThingSpeak"));
} else {
Serial.println(F("ThingSpeak connection failed"));
}
http.end();
}
}
void connectToWiFi() {
Serial.print(F("Connecting to WiFi"));
WiFi.begin(wifi_ssid, wifi_password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(F("."));
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println(F(" Connected!"));
Serial.print(F("IP: ")); Serial.println(WiFi.localIP());
} else {
Serial.println(F(" Connection failed!"));
}
}
// ============== END OF CODE ==============
Step 3: Calibration (Important)
MQ-135 sensors need 24-48 hours to burn in, then calibrate:
- Power on and leave in clean air for 24 hours
- Note the RZERO value from serial monitor
- Update
#define RZERO 76.63with your value - Upload calibrated code
Troubleshooting
| Problem | Solution |
|---|---|
| DHT22 reading NaN | Check wiring; add 10K pull-up resistor |
| CO2 reading too high | Recalibrate sensor; check power supply stability |
| Analog readings unstable | Add 100nF capacitor between VCC and GND |
| ThingSpeak not updating | Verify API key; check field numbers |
Official Documentation
- DHT Sensor Library - Official GitHub
- MQ135 Library - Reference code
- ThingSpeak - IoT platform
Frequently Asked Questions
Q: How accurate is MQ-135 for CO2?
A: It's a metal oxide sensor that detects various gases. CO2 readings are estimates, not lab-grade measurements.
Q: How often should I recalibrate?
A: Every 2-3 months, or when readings seem consistently off.
Q: Can I use this outdoors?
A: Protect from rain and direct sunlight. Use a weatherproof enclosure.

