ESP32 & IoT24 October 202619 mins

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.

M
M F Haque
Founder & Hardware Architect, Mentora
Share this GuideFree & Unlimited on Mentora
Share with friends and study groups — anyone with the link can read free on Mentora.
Key Strategic Takeaways
  • 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] |

+----+-------+--------+----------+---------+---------+----+---+---+

textcode snippet
|       |        |          |         |         |    |

+5V | +3.3V | GND | PMS1 | PMS1 | SDA |SCL |

Rail | Rail | Rail | RX | TX | Line |Line|

=======+=======+========+==========+=========+=========+====+===+====

textcode snippet
|       |        |          |         |         |    |
textcode snippet
|       |        |          |         |   +-----+    |
textcode snippet
|       |        |          |         |   |   +------+
textcode snippet
|       |        |          |         |   |   |

+--+-------+--------+----+ | | +-+---+------+--+

textcode snippet
| VCC 3V3  GND  TXD  RXD |     |         | | VDD GND TX RX |
textcode snippet
|   PLANTOWER PMS5003    |<----+---------+ |   SCD30 NDIR  |
textcode snippet
|    (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);

}

``

Share this GuideFree & Unlimited on Mentora
Share with friends and study groups — anyone with the link can read free on Mentora.
Microcontroller Hub: ESP32-S3 DevKitC-1 (Xtensa LX7 Dual-Core 240MHz)Particulate Sensing: Plantower PMS5003 Laser Scattering Particle Counter (PM1.0, PM2.5, PM10) via Hardware UARTGaseous Environmental Sensing: Sensirion SCD30 True NDIR Optical $CO_2$, Temperature & Humidity via I2CAnalytics & Alerts: Indian Central Pollution Control Board (CPCB) NAQI Breakpoint Engine, Automated Telegram Hazard Alerts (AQI > 150), Local Color DisplayTarget Search Intent: "PMS5003 UART checksum failure ESP32-S3", "SCD30 CO2 calibration drift fix", "ESP32-S3 hardware serial2 pin collision", "CPCB Indian AQI calculation code" Token-Gated Asset

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.

Access Fee
₹49₹199

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.

ESP32_S3_Urban_AQI_Monitor_PMS5003_SCD30.ino(Locked Asset)
Token-Gated
#include<Wire.h>
#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.

Fritzing & CAD Schematics Pack.FZZ & .PDF

Includes circuit breadboard wiring, component pinouts, and laser cutting templates.

Stuck on Hardware or Code? Book a 1-on-1 Video SessionLive Mentor

Screen-share or show your breadboard to M. F. Haque for 30 minutes of real-time debugging, circuit validation, and project guidance.

Peer Review & Rewards

Review This Build & Claim a Free ₹49 Token

Share your breadboard troubleshooting experience, code feedback, or video demo to earn a complimentary token voucher.

5/5 - Outstanding Build

Have a Reward Voucher?

Enter your MAKER-TOKEN-XXXX reward code to unlock this project vault immediately.

Ambassador Program

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!

Frequently Asked Questions

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.

Live Evening Cohort • Interactive Micro-Cohort (Max 6–8 Kids)

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.

Tuesday & Thursday • 5:30 PM – 6:30 PM IST • Monthly After-School Pass: ₹800/mo ₹1,500
Topics:#ESP32#ESP32 & IoT#Circuit Wiring#Arduino Code#DIY Electronics#ESP32-S3 air quality monitor#PMS5003 ESP32-S3 UART tutorial#SCD30 CO2 sensor ESP32#Indian CPCB AQI calculation algorithm#PMS5003 checksum verification fix