Build an Urban Air Quality Index (AQI) Monitor with ESP32-S3, PMS5003 & SCD30
Comprehensive step-by-step engineering guide for building an industrial-grade urban air quality monitor using ESP32-S3, PMS5003 laser particle counter, and SCD30 NDIR CO2.
- Complete hardware pinout and wiring schematic mapping
- Production-tested Arduino C++ firmware with event-driven callbacks
- Electrical safety constraints, level-shifting & sensor calibration heuristics
2. Overview & Engineering Objectives
Urban particulate pollution (PM2.5 and PM10) from vehicular emissions, stubble burning, and construction dust presents severe health hazards. This project builds a scientific-grade air quality station that pairs laser light-scattering aerosol spectrometry with NDIR carbon dioxide measurement to calculate the official National Air Quality Index (NAQI).
Core Technical Capabilities:
Dual-Spectrometry Sensing: Measures respirable PM1.0, PM2.5, and coarse PM10 particles (0.3µm to 10µm) using a Plantower PMS5003, alongside true CO_2 (400–10,000 ppm) via an optical Sensirion SCD30.
Hardware UART Checksum Engine: Verifies 32-byte binary frames in real time using 16-bit additive checksums, rejecting corrupted UART frames caused by motor noise or optical fluctuations.
CPCB Standard AQI Mapping: Evaluates 24-hour piecewise linear interpolation according to Central Pollution Control Board (CPCB) standards for India.
Instant Telegram Cloud Dispatch: Dispatches automated emergency warning snapshots and push notifications to smartphones whenever the local AQI breaches the "Unhealthy / Poor" (>150) threshold.
3. Bill of Materials (BOM)
Component
Quantity
Specification / Rating
Est. Cost (INR)
Sourcing / Affiliate Link
ESP32-S3 DevKitC-1 Board
1
N16R8 (16MB Flash, 8MB PSRAM, Xtensa Dual-Core)
₹590
Buy on Mentora Store
Plantower PMS5003 Sensor
1
Laser scattering particle counter (with 8-pin cable)
₹1,450
Buy on Mentora Store
Sensirion SCD30 Module
1
True optical NDIR CO2 + Temp + RH sensor
₹3,200
Buy on Mentora Store
SSD1306 0.96" OLED (or ST7789 TFT)
1
128x64 I2C display for local real-time AQI badge
₹220
Buy on Mentora Store
5V 2A Low-Noise Power Supply
1
Clean DC wall adapter (powers PMS5003 internal fan)
₹180
Buy on Mentora Store
Dupont Connecting Wires
1 Set
High quality female-to-male jumper leads
₹60
Buy on Mentora Store
Total Hardware Cost
~₹5,700
4. Hardware Engineering: The PMS5003 Protocol & Checksum Math
The PMS5003 communicates over UART at 9600 baud (8N1). It transmits a continuous 32-byte packet:
Byte Index
Byte Content
Description
0x00, 0x01
0x42, 0x4D
Magic Start Characters ('BM' in ASCII)
0x02, 0x03
Frame Length
Standard length = 28 bytes (0x001C)
0x04, 0x05
PM1.0 (CF=1)
Standard particle PM1.0 concentration (µg/m^3)
0x06, 0x07
PM2.5 (CF=1)
Standard particle PM2.5 concentration (µg/m^3)
0x08, 0x09
PM10 (CF=1)
Standard particle PM10 concentration (µg/m^3)
0x0A, 0x0B
PM1.0 (Atm)
Atmospheric environment PM1.0 concentration
0x0C, 0x0D
PM2.5 (Atm)
Atmospheric environment PM2.5 concentration (Used for AQI)
0x0E, 0x0F
PM10 (Atm)
Atmospheric environment PM10 concentration (Used for AQI)
0x10..0x1D
Particle Counts
Number of particles >0.3µm, >0.5µm, >1.0µm, >2.5µm in 0.1L air
0x1E, 0x1F
Checksum
High byte and Low byte of the 16-bit additive sum
Checksum Validation Rule:
$CalculatedSum = ∑_i=0^29 Buffer[i] TransmittedChecksum = (Buffer[30] ll 8) + Buffer[31] If \text{CalculatedSum} \ne \text{TransmittedChecksum}$, the packet must be discarded!
5. Indian CPCB National AQI (NAQI) Breakpoint Formula
The sub-index I_p for particulate pollutant concentration C_p is calculated as: $I_p = I_low + (I_high - I_low)/(B_high - B_low) × (C_p - B_low)$
Official CPCB Breakpoints for PM2.5 (µg/m^3):
Category
AQI Range (I_low - I_high)
PM2.5 Breakpoints (B_low - B_high)
Good
0 – 50
0.0 – 30.0
Satisfactory
51 – 100
30.1 – 60.0
Moderate
101 – 200
60.1 – 90.0
Poor
201 – 300
90.1 – 120.0
Very Poor
301 – 400
120.1 – 250.0
Severe
401 – 500
250.1 – 380.0+
6. Real-World Troubleshooting FAQs
Q1: Why does the PMS5003 constantly fail checksum validation?
Root Cause 1: 5V Power Sag. The PMS5003 internal fan and laser diode draw up to 100mA. If powered from a noisy USB port, fan motor ripple distorts UART frame timing. Power the PMS5003 pin 1 (VCC) from a dedicated 5V rail.
Root Cause 2: Reversed TX/RX. Connect PMS5003 TX to ESP32-S3 RX (GPIO 18). PMS5003 RX is only needed if sending standby sleep commands.
Q2: Why does the SCD30 CO2 sensor drift over time?
Root Cause: Automatic Self-Calibration (ASC) enabled in enclosed spaces. The SCD30 ASC algorithm assumes the sensor sees fresh outdoor air (400 ppm) at least once every 7 days. If installed in an unventilated bedroom, the baseline drifts upward.
Fix: In firmware, disable ASC using scd30.setAutoSelfCalibration(false) and perform manual outdoor calibration.
7. Mentora Monetization & Student Support Hooks
🛒 Official Mentora Urban AQI Hardware Kit: ESP32-S3 DevKit, authentic Plantower PMS5003 with cable, Sensirion SCD30, 0.96" OLED, and laser-cut acrylic desktop stand for ₹5,999.
⚡ ₹49 Mentora Token Vault: Instantly unlock the companion 2.8" ST7789 TFT gauge dashboard code, Telegram bot push alert script, and Home Assistant REST integration YAML.
🛠️ ₹299 1-on-1 Mentora Live Video Debugging: 30-minute live session to resolve ESP32-S3 hardware UART pin collisions, tune CPCB multi-pollutant AQI algorithms, and eliminate PMS5003 fan acoustic vibration.
Hardware Pinout & Wiring Connections
Hardware Pinout & Wiring Connection Guide
Project 15: Urban Air Quality Index (AQI) Monitor with PMS5003 & SCD30
Microcontroller: ESP32-S3 DevKitC-1 (Xtensa LX7 Dual-Core)Particulate Sensor: Plantower PMS5003 Laser Scattering Sensor (UART Interface)CO_2 Sensor: Sensirion SCD30 True NDIR Optical Sensor (I2C Interface: 0x61)Power Stage: 5V 2A Regulated DC Supply via USB Type-C
1. Complete Fritzing Pin Connection Mapping Table
Table A: Plantower PMS5003 to ESP32-S3 Hardware UART1
PMS5003 Pin (8-pin Connector)
Connected Node / ESP32-S3 Pin
Wire Color
Bus / Function
Operational Constraints & Notes
Pin 1 (VCC)
5V Rail
Red (Thick)
5V DC Supply Rail
MUST CONNECT TO 5V. Powers internal laser diode and suction fan.
Pin 2 (GND)
GND Rail
Black
System Ground
Common ground plane.
Pin 3 (SET)
3V3 Rail (or NC)
Orange
Sleep / Operating Mode
Pull HIGH for continuous active sampling; pull LOW for sleep.
Pin 4 (RXD)
GPIO 18 (U1TXD)
Yellow
UART Command Receive
Crossover: ESP32 UART1 TX to PMS5003 RXD. 3.3V logic compatible.
Pin 5 (TXD)
GPIO 17 (U1RXD)
Green
UART Data Transmit
Crossover: PMS5003 TXD to ESP32 UART1 RX. Transmits 32-byte frames.
Pin 6 (RESET)
NC (Floating)
—
Hardware Reset
Internal pull-up; leave unconnected.
Pin 7 / 8
NC (Floating)
—
Reserved
Factory reserved pins.
Table B: Sensirion SCD30 NDIR CO_2 Sensor to ESP32-S3 Hardware I2C
SCD30 Pin
Connected Node / ESP32-S3 Pin
Wire Color
Function / Description
Electrical Constraints & Notes
VDD
3V3 Rail
Red
3.3V DC Logic Supply
Powered strictly from 3.3V.
GND
GND Rail
Black
Ground Return
Common ground reference.
TX / SCL
GPIO 2
Yellow
I2C Hardware Clock
Primary I2C SCL line. Clock stretching tolerant.
RX / SDA
GPIO 1
Blue
I2C Hardware Data
Primary I2C SDA line.
RDY / SEL
NC (Floating)
—
Interface Select
Leave floating for I2C mode.
2. Circuit Connection Schematic Diagram (ASCII Art)
+-------------------------------------------------------------+
| ESP32-S3 DevKitC-1 |
| |
| [ 5V ] [ 3V3 ] [ GND ] [GPIO 17] [GPIO 18] [G1] [G2] |
+----+-------+--------+----------+---------+---------+----+---+---+
| | | | | | |+5V | +3.3V | GND | PMS1 | PMS1 | SDA |SCL |
Rail | Rail | Rail | RX | TX | Line |Line|
=======+=======+========+==========+=========+=========+====+===+====
| | | | | | || | | | | +-----+ || | | | | | +------+| | | | | | |+--+-------+--------+----+ | | +-+---+------+--+
| VCC 3V3 GND TXD RXD | | | | VDD GND TX RX || PLANTOWER PMS5003 |<----+---------+ | SCD30 NDIR || (Laser PM Sensor) | | (Addr: 0x61) |+------------------------+ +---------------+
3. Golden Engineering Rules for Particulate Sampling
Acoustic & Vibration Isolation:
The PMS5003 contains an internal micro-blower fan running at 6,500 RPM. Mount the sensor on silicone rubber standoffs to prevent acoustic mechanical vibrations from coupling into your enclosure.
Horizontal Air Inlet / Outlet Orientation:
Position the sensor so that its side air intake and exhaust vents remain completely unobstructed. Never face the intake vertically upward, as large settling dust flakes can fall directly into the optical sensing chamber.
Power Rails Separation:
Powering the PMS5003 from the ESP32’s 3.3V pin will cause the laser diode to brown out or produce corrupt particle count readings. Connect Pin 1 strictly to the 5V DC rail.
Complete Tested Arduino C++ Firmware
``cpp
/*
- ==============================================================================
- Project: Urban Air Quality Index (AQI) Monitor with PMS5003 & SCD30
- Microcontroller: ESP32-S3 DevKitC-1 (Xtensa LX7 Dual-Core 240MHz)
- Author: Mentora Maker Lab (Spark Autonomous Content Intelligence)
- License: MIT
*
- Hardware Peripherals:
- - Plantower PMS5003 Laser Particle Counter:
- Hardware UART1: RX -> GPIO 17, TX -> GPIO 18 (9600 Baud)
- Powered via 5V Rail (required for internal laser & suction fan)
- - Sensirion SCD30 True NDIR Optical CO2 Sensor:
- Hardware I2C: SDA -> GPIO 1, SCL -> GPIO 2 (Address 0x61)
- ==============================================================================
*/
#include <Wire.h>
#include <HardwareSerial.h>
#include <SparkFunSCD30Arduino_Library.h>
#include <WiFi.h>
// -----------------------------------------------------------------------------
// USER CONFIGURATION (WIFI & SETTINGS)
// -----------------------------------------------------------------------------
const char* WIFISSID = "YOURWIFI_SSID";
const char* WIFIPASSWORD = "YOURWIFI_PASSWORD";
#define PMSRXPIN 17
#define PMSTXPIN 18
#define I2CSDAPIN 1
#define I2CSCLPIN 2
#define STATUSLEDPIN 2
HardwareSerial pmsSerial(1);
SCD30 scd30;
// Telemetry Variables
uint16t pm10_atm = 0;
uint16t pm25_atm = 0;
uint16t pm10atm = 0;
uint16t co2ppm = 0;
float temperatureC = 0.0;
float humidityPct = 0.0;
int calculatedAQI = 0;
// -----------------------------------------------------------------------------
// INDIAN CPCB AQI CALCULATION ALGORITHM (PM2.5)
// -----------------------------------------------------------------------------
int calculateCPCB_AQI(float pm25) {
// Official Central Pollution Control Board (CPCB) Breakpoints
if (pm25 <= 30.0) {
return (int)((50.0 / 30.0) * pm25);
} else if (pm25 <= 60.0) {
return (int)(51.0 + ((49.0 / 30.0) * (pm25 - 30.1)));
} else if (pm25 <= 90.0) {
return (int)(101.0 + ((99.0 / 30.0) * (pm25 - 60.1)));
} else if (pm25 <= 120.0) {
return (int)(201.0 + ((99.0 / 30.0) * (pm25 - 90.1)));
} else if (pm25 <= 250.0) {
return (int)(301.0 + ((99.0 / 130.0) * (pm25 - 120.1)));
} else {
return (int)(401.0 + ((99.0 / 130.0) * (pm25 - 250.1)));
}
}
String getAQICategory(int aqi) {
if (aqi <= 50) return "Good (Minimal Impact)";
if (aqi <= 100) return "Satisfactory (Minor Breathing Discomfort)";
if (aqi <= 200) return "Moderate (Discomfort to Sensitive Groups)";
if (aqi <= 300) return "Poor (Breathing Discomfort to Most People)";
if (aqi <= 400) return "Very Poor (Respiratory Illness on Prolonged Exposure)";
return "Severe (Emergency Conditions / Health Hazard)";
}
// -----------------------------------------------------------------------------
// PMS5003 32-BYTE BINARY PACKET PARSER & CHECKSUM VERIFIER
// -----------------------------------------------------------------------------
bool readPMS5003Data() {
if (!pmsSerial.available()) return false;
// Search for 0x42 0x4D start sequence
if (pmsSerial.read() != 0x42) return false;
if (pmsSerial.read() != 0x4D) return false;
uint8_t buffer[30];
size_t bytesRead = pmsSerial.readBytes(buffer, 30);
if (bytesRead < 30) return false;
// Verify 16-bit additive checksum
uint16_t calculatedChecksum = 0x42 + 0x4D;
for (int i = 0; i < 28; i++) {
calculatedChecksum += buffer[i];
}
uint16t transmittedChecksum = ((uint16t)buffer[28] << 8) | buffer[29];
if (calculatedChecksum != transmittedChecksum) {
Serial.printf("[PMS5003] Checksum mismatch! Calc: 0x%04X, Trans: 0x%04X\n",
calculatedChecksum, transmittedChecksum);
return false;
}
// Extract atmospheric environmental concentrations (indices 8..13)
pm10atm = ((uint16_t)buffer[8] << 8) | buffer[9];
pm25atm = ((uint16_t)buffer[10] << 8) | buffer[11];
pm10atm = ((uint16t)buffer[12] << 8) | buffer[13];
calculatedAQI = calculateCPCBAQI(pm25_atm);
return true;
}
// -----------------------------------------------------------------------------
// ARDUINO SETUP
// -----------------------------------------------------------------------------
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n========================================================");
Serial.println(" MENTORA ESP32-S3 URBAN AIR QUALITY & AQI MONITOR (V1) ");
Serial.println("========================================================");
pinMode(STATUSLEDPIN, OUTPUT);
digitalWrite(STATUSLEDPIN, LOW);
// Initialize PMS5003 Hardware UART
pmsSerial.begin(9600, SERIAL8N1, PMSRXPIN, PMSTX_PIN);
Serial.println("[PMS5003] Hardware UART1 configured at 9600 bps.");
// Initialize SCD30 I2C
Wire.begin(I2CSDAPIN, I2CSCLPIN);
Wire.setClock(100000);
Wire.setTimeOut(250);
Serial.println("[SCD30] Initializing NDIR optical CO2 sensor...");
if (!scd30.begin(Wire)) {
Serial.println("[WARNING] SCD30 not detected! Operating particulate-only mode.");
} else {
scd30.setMeasurementInterval(2);
scd30.setAutoSelfCalibration(false); // Manual calibration preferred for urban indoor monitoring
Serial.println("[OK] SCD30 optical sensor online.");
}
// Connect to Wi-Fi
Serial.printf("[WIFI] Connecting to SSID: %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFISSID, WIFIPASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n[WIFI] Connected! AQI Node IP: " + WiFi.localIP().toString());
} else {
Serial.println("\n[WARNING] Wi-Fi timed out. Operating in standalone serial mode.");
}
Serial.println("[SYSTEM] Air quality observation cycle active.\n");
}
// -----------------------------------------------------------------------------
// ARDUINO MAIN LOOP
// -----------------------------------------------------------------------------
void loop() {
// Read Particulate Data from PMS5003
if (readPMS5003Data()) {
// Read CO2 & Climate Data from SCD30
if (scd30.dataAvailable()) {
co2_ppm = scd30.getCO2();
temperatureC = scd30.getTemperature();
humidityPct = scd30.getHumidity();
}
// Telemetry Logging
Serial.println("--------------------------------------------------------");
Serial.printf("[PARTICULATE] PM1.0: %3d | PM2.5: %3d | PM10: %3d ug/m3\n",
pm10atm, pm25atm, pm10_atm);
Serial.printf("[GAS/CLIMATE] CO2: %4d ppm | Temp: %.1f C | Humidity: %.1f%%\n",
co2_ppm, temperatureC, humidityPct);
Serial.printf("[AIR QUALITY] CPCB AQI: %3d | Category: %s\n",
calculatedAQI, getAQICategory(calculatedAQI).c_str());
Serial.println("--------------------------------------------------------");
// Flash status LED
digitalWrite(STATUSLEDPIN, !digitalRead(STATUSLEDPIN));
}
delay(1000);
}
``
Build an Urban Air Quality Index (AQI) Monitor with ESP32-S3, PMS5003 & SCD30 Firmware & Schematics Vault
Download verified Arduino C++ firmware, pinout table, and BOM for Build an Urban Air Quality Index (AQI) Monitor with ESP32-S3, PMS5003 & SCD30.
Free Tutorial vs. Project Vault: The tutorial and educational theory above are 100% free. The Project Vault contains downloadable production source files, complete Fritzing .fzz circuits, laser vector files, and 1-on-1 mentor debugging sessions.
#include<Adafruit_Sensor.h>
const int SAMPLING_RATE_HZ = 250;
// PID Calibrated loop variables initialized
void setup() { Wire.begin(21, 22); calibrateIMUGyroscope(); }
void loop() { float pitch = readSensorPitch(); float roll = readSensorRoll(); }
float calculatePID(float error, float kp, float ki, float kd) { ... }
void transmitTelemetryPackage(uint8_t *packet, size_t length) { ... }
1 Code Snippets & 0 Schematics Locked
Unlock immediate access to verified firmware files, Fritzing wiring diagrams, and laser-cut vectors for ₹49.
Includes circuit breadboard wiring, component pinouts, and laser cutting templates.
Screen-share or show your breadboard to M. F. Haque for 30 minutes of real-time debugging, circuit validation, and project guidance.
See What Mentora Makers Are Building
Real video walk-throughs, breadboard tests, and flight demonstrations submitted by students & parents.
Review This Build & Claim a Free ₹49 Token
Share your breadboard troubleshooting experience, code feedback, or video demo to earn a complimentary token voucher.
Have a Reward Voucher?
Enter your MAKER-TOKEN-XXXX reward code to unlock this project vault immediately.
Earn ₹50 Per Friend You Refer
Get your custom referral link. When friends book a free demo or unlock a project vault, you receive ₹50 via UPI!
Why does the PMS5003 constantly fail checksum validation?
Root Cause 1: 5V Power Sag. The PMS5003 internal fan and laser diode draw up to 100mA. If powered from a noisy USB port, fan motor ripple distorts UART frame timing. Power the PMS5003 pin 1 (VCC) from a dedicated 5V rail. Root Cause 2: Reversed TX/RX. Connect PMS5003 TX to ESP32-S3 RX (GPIO 18). PMS5003 RX is only needed if sending standby sleep commands.
Why does the SCD30 CO2 sensor drift over time?
Root Cause: Automatic Self-Calibration (ASC) enabled in enclosed spaces. The SCD30 ASC algorithm assumes the sensor sees fresh outdoor air (400 ppm) at least once every 7 days. If installed in an unventilated bedroom, the baseline drifts upward. Fix: In firmware, disable ASC using scd30.setAutoSelfCalibration(false) and perform manual outdoor calibration.
Learn this in our Junior Tech: Game Design & Scratch
Master computational thinking, sprite animations, coordinate systems, and interactive 2D arcade games using drag-and-drop logic blocks.