Full width home advertisement

Post Page Advertisement [Top]

DIY Portable Air Quality Monitor with ESP32, PMS7003 & BME680: Build Your Own PM2.5, CO2 & VOC Detector (2026 Complete Guide)

📊 What You'll Build:
A professional-grade portable air quality monitor that detects PM2.5, PM10, CO2, VOCs, temperature, and humidity — all for under $35!

📑 Table of Contents

  • 1. Why Air Quality Monitoring Matters in 2026
  • 2. Components & Cost Breakdown
  • 3. Circuit Wiring Diagram
  • 4. Arduino IDE Setup
  • 5. Complete Code
  • 6. Understanding AQI Levels
  • 7. WiFi Data Logging
  • 8. 3D Printed Enclosure
  • 9. Calibration Tips
  • 10. Troubleshooting

1. Why Air Quality Monitoring Matters in 2026

Indoor air quality has become a critical health concern. According to the WHO, indoor air can be up to 5 times more polluted than outdoor air. With rising concerns about:

  • 🏭 Urban pollution and wildfire smoke
  • 🦠 Post-pandemic ventilation awareness
  • 🏠 VOC emissions from furniture and cleaning products
  • 💨 CO2 buildup in poorly ventilated spaces

Having a personal air quality monitor is no longer a luxury — it's a health necessity.

⚠️ Health Alert: Long-term exposure to PM2.5 particles has been linked to respiratory diseases, cardiovascular problems, and cognitive decline. Monitoring is your first line of defense!

2. Components & Cost Breakdown

Component Model Purpose Price
Microcontroller ESP32 DevKit V1 Main processor with WiFi $6
Particle Sensor PMS7003 PM1.0, PM2.5, PM10 detection $12
Environmental Sensor BME680 CO2 (IAQ), VOC, Temp, Humidity $9
Display 1.3" OLED SH1106 Real-time data visualization $4
Battery 18650 Li-ion + TP4056 Portable power $3
Misc Wires, switch, case Assembly $1
💰 Total Cost: ~$35 (vs. $200-500 for commercial equivalents like AirVisual Pro)

3. Circuit Wiring Diagram
DIY Portable Air Quality Monitor with ESP32, PMS7003 & BME680: Build Your Own PM2.5, CO2 diagram

PMS7003 Connection (UART):

  • VCC → 5V (ESP32 VIN)
  • GND → GND
  • TX → GPIO16 (ESP32 RX2)
  • RX → GPIO17 (ESP32 TX2)
  • SET & RESET → 3.3V (always active)

BME680 Connection (I2C):

  • VIN → 3.3V
  • GND → GND
  • SCL → GPIO22
  • SDA → GPIO21

OLED Display (I2C):

  • VCC → 3.3V
  • GND → GND
  • SCL → GPIO22 (shared with BME680)
  • SDA → GPIO21 (shared with BME680)
⚡ Important: The PMS7003 requires 5V power but its logic is 3.3V tolerant. Never power it from the ESP32's 3.3V pin!

4. Arduino IDE Setup

1Install ESP32 Board Support

File → Preferences → Add this URL: https://dl.espressif.com/dl/package_esp32_index.json

Then: Tools → Board → Boards Manager → Search "ESP32" → Install

2Install Required Libraries

Sketch → Include Library → Manage Libraries:

  • PMS Library by Mariusz Kacki
  • Zanshin BME680
  • Adafruit SH110X
  • Adafruit GFX
  • WiFi (built-in)
  • ArduinoJson

5. Complete Code

#include <PMS.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <bme680.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

// WiFi credentials
const char* ssid = "YOUR_WIFI";
const char* password = "YOUR_PASSWORD";

// Display
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1);

// Sensors
PMS pms(Serial2);
PMS::DATA pmsData;
struct bme680_dev* gas_sensor;

// Pin definitions
#define PMS_RX 16
#define PMS_TX 17
#define I2C_SDA 21
#define I2C_SCL 22

// AQI calculation
int calculateAQI(int pm25) {
  if (pm25 <= 12) return map(pm25, 0, 12, 0, 50);
  if (pm25 <= 35) return map(pm25, 13, 35, 51, 100);
  if (pm25 <= 55) return map(pm25, 36, 55, 101, 150);
  if (pm25 <= 150) return map(pm25, 56, 150, 151, 200);
  if (pm25 <= 250) return map(pm25, 151, 250, 201, 300);
  return 301;
}

String getAQILevel(int aqi) {
  if (aqi <= 50) return "GOOD";
  if (aqi <= 100) return "MODERATE";
  if (aqi <= 150) return "UNHEALTHY-S";
  if (aqi <= 200) return "UNHEALTHY";
  if (aqi <= 300) return "V.UNHEALTHY";
  return "HAZARDOUS";
}

void setupBME680() {
  gas_sensor = (struct bme680_dev*)malloc(sizeof(struct bme680_dev));
  gas_sensor->dev_id = BME680_I2C_ADDR_PRIMARY;
  gas_sensor->intf = BME680_I2C_INTF;
  gas_sensor->amb_temp = 25;
  bme680_init(gas_sensor);
}

void setup() {
  Serial.begin(115200);
  Serial2.begin(9600, SERIAL_8N1, PMS_RX, PMS_TX);
  
  Wire.begin(I2C_SDA, I2C_SCL);
  
  display.begin(0x3C, true);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  display.println("Air Quality Monitor");
  display.println("Initializing...");
  display.display();
  
  setupBME680();
  
  WiFi.begin(ssid, password);
  display.println("Connecting WiFi...");
  display.display();
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  
  display.clearDisplay();
  display.println("Ready!");
  display.print("IP: ");
  display.println(WiFi.localIP());
  display.display();
  delay(2000);
}

void loop() {
  // Read PMS7003
  if (pms.readUntil(pmsData)) {
    int pm25 = pmsData.PM_AE_UG_2_5;
    int pm10 = pmsData.PM_AE_UG_10_0;
    int aqi = calculateAQI(pm25);
    
    // Read BME680
    struct bme680_field_data data;
    bme680_get_sensor_data(&data, gas_sensor);
    
    // Display
    display.clearDisplay();
    display.setCursor(0, 0);
    display.setTextSize(1);
    
    display.print("PM2.5: ");
    display.print(pm25);
    display.println(" ug/m3");
    
    display.print("PM10:  ");
    display.print(pm10);
    display.println(" ug/m3");
    
    display.print("AQI:   ");
    display.print(aqi);
    display.print(" ");
    display.println(getAQILevel(aqi));
    
    display.print("Temp:  ");
    display.print(data.temperature / 100.0);
    display.println(" C");
    
    display.print("Hum:   ");
    display.print(data.humidity / 1000.0);
    display.println(" %");
    
    display.print("VOC:   ");
    display.print(data.gas_resistance / 1000.0);
    display.println(" kOhm");
    
    display.display();
    
    // Send to cloud every 60 seconds
    static unsigned long lastSend = 0;
    if (millis() - lastSend > 60000) {
      sendToCloud(pm25, pm10, aqi, data.temperature/100.0, 
                  data.humidity/1000.0, data.gas_resistance/1000.0);
      lastSend = millis();
    }
  }
  
  delay(2000);
}

void sendToCloud(int pm25, int pm10, int aqi, float temp, float hum, float voc) {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    http.begin("https://your-api-endpoint.com/airquality");
    http.addHeader("Content-Type", "application/json");
    
    StaticJsonDocument<256> doc;
    doc["pm25"] = pm25;
    doc["pm10"] = pm10;
    doc["aqi"] = aqi;
    doc["temp"] = temp;
    doc["hum"] = hum;
    doc["voc"] = voc;
    doc["timestamp"] = millis();
    
    String json;
    serializeJson(doc, json);
    http.POST(json);
    http.end();
  }
}

6. Understanding AQI Levels

🌈 Air Quality Index Scale

0-50: GOOD - Air quality is satisfactory
51-100: MODERATE - Acceptable quality
101-150: UNHEALTHY for sensitive groups
151-200: UNHEALTHY for everyone
201-300: VERY UNHEALTHY - Health alert
301+: HAZARDOUS - Emergency conditions

7. WiFi Data Logging

The code sends data to a cloud endpoint every 60 seconds. You can use:

  • ThingSpeak (free tier available)
  • Blynk for mobile dashboards
  • InfluxDB + Grafana for advanced analytics
  • Google Sheets via IFTTT
💡 Pro Tip: Use the ESP8266/ESP32 Internet Control Guide to set up your own web dashboard!

8. 3D Printed Enclosure

Design considerations:

  • 📐 Air intake vents on bottom (for PMS7003)
  • 📐 Small holes on sides (for BME680)
  • 📐 Clear window for OLED display
  • 📐 Battery compartment with USB-C charging port
  • 📐 Power switch on top

9. Calibration Tips

1PMS7003: Let it run for 30 seconds before first reading to stabilize the fan.
2BME680: Requires 5-10 minutes warmup for accurate gas readings.
3Baseline: Take readings outdoors (away from pollution) to establish a clean baseline.

10. Troubleshooting

PMS7003 shows 0 values:

  • Check UART wiring (TX/RX swap needed?)
  • Verify 5V power supply
  • Fan should be spinning visibly

BME680 not detected:

  • Run I2C scanner to verify address (0x76 or 0x77)
  • Check pull-up resistors on SDA/SCL (4.7kΩ)

OLED blank:

  • Verify I2C address (usually 0x3C)
  • Check 3.3V power
🎉 Congratulations! You've built a professional air quality monitor that rivals $500 commercial devices. Share your build with #ESPAirMonitor on social media!

Related Projects You'll Love

Bottom Ad [Post Page]