Home IoT Hub Series · 12

[Part 12] Is bad air making you foggy? CO2 and PM2.5 air-quality node

Measure indoor CO2 and PM2.5 with MH-Z19B and PMS5003 sensors.

This is the air-quality node. MH-Z19B measures CO2, and PMS5003 measures fine dust, so this page is more wiring-heavy than the earlier sensor nodes.

Both sensors use serial communication. Keep the wiring neat and check RX/TX direction carefully before debugging the code.

[Part 12] Is bad air making you foggy? CO2 and PM2.5 air-quality node hero image
System architecture and data pipeline note
This article is part of the Home IoT Hub system connected to the central NodeMCU server built in Part 2: Building the Brain. The MH-Z19B and PMS5003 report two air-quality axes used for ventilation and purifier decisions. The collected data is sent over the local IoT_Hub_Net network and updates slot 13 for CO2 and slot 14 for PM2.5 on the central server (/set?no13=CO2&no14=PM2.5), becoming part of the decision pipeline for the 20-channel smart-home controller.

Parts and cost

PartQtyUnit price
Wemos D1 Mini1 pc$1.50
MH-Z19B CO2 sensor1 pc$15
PMS5003 fine dust sensor1 pc$11
Jumper wires8 pc
Total$27

Pin wiring

[Part 12] Is bad air making you foggy? CO2 and PM2.5 air-quality node wiring diagram
Sensor / moduleBoard pin
VCCVU (5V)
GNDGND
TXD2 (GPIO4)
RXD3 (GPIO0)
VCCVU (5V)
GNDGND
TXD6 (GPIO12)
RXD7 (GPIO13) ← PMS5003 RX is optional, but wired here

Full code

[Part 12] Is bad air making you foggy? CO2 and PM2.5 air-quality node - Code 2
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <SoftwareSerial.h>
#include <MHZ19.h>
#include <PMS.h>

const char* ssid     = "IoT_Hub_Net";
const char* password = "iothub1234";
const char* hubURL   = "http://192.168.1.1";

#define MHZ_RX  4   // D2
#define MHZ_TX  0   // D3
#define PMS_RX  12  // D6
#define PMS_TX  13  // D7
#define INTERVAL 30000

SoftwareSerial mhzSerial(MHZ_RX, MHZ_TX);
SoftwareSerial pmsSerial(PMS_RX, PMS_TX);
MHZ19 mhz;
PMS pms(pmsSerial);
PMS::DATA pmsData;

void setup() {
  Serial.begin(115200);
  mhzSerial.begin(9600);
  pmsSerial.begin(9600);
  mhz.begin(mhzSerial);
  pms.passiveMode();

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting to IoT_Hub_Net");
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.println("\nConnected!");
  Serial.println("Sensors warming up (180s)...");
  delay(180000); // warm up the CO2 sensor for 3 minutes
  Serial.println("Ready.");
}

void loop() {
  // CO2 note
  int co2 = mhz.getCO2();
  if (co2 < 0) co2 = 0;

  // fine dust note
  pms.wakeUp();
  delay(30000); // PMS5003 note
  pms.requestRead();
  int pm25 = 0;
  if (pms.readUntil(pmsData)) {
    pm25 = pmsData.PM_AE_UG_2_5;
  }
  pms.sleep();

  Serial.printf("CO2: %d ppm, PM2.5: %d ug/m3\n", co2, pm25);

  WiFiClient client; HTTPClient http;
  String url = String(hubURL) + "/set?no13=" + String(co2)
             + "&no14=" + String(pm25);
  http.begin(client, url);
  int code = http.GET(); http.end();
  Serial.print("HTTP "); Serial.print(code); Serial.println(" -> " + url);
  Serial.println();

  delay(INTERVAL);
}

Upload and check

Connect the Wemos D1 Mini to the PC with a USB data cable, select the ESP8266 board and port in the Arduino IDE, upload the sketch, then open Tools → Serial Monitor and set the baud rate to 115200.

Serial Monitor

After uploading, open Tools → Serial Monitor in the Arduino IDE and set the baud rate to 115200. The following is an example Serial Monitor flow for checking normal operation. This is not a real screenshot. It shows MH-Z19B and PMS5003 readings entering slots 13 and 14.

[BOOT] Home IoT Node #12 - CO2 and PM2.5
[UART] MH-Z19B ready, PMS5003 wake
[SENSOR] co2=742ppm pm2_5=11ug/m3
[FILTER] fan_warmup=30s, abc=enabled
[HTTP] GET /set?no13=742&no14=11
[HUB] 200 OK - slots no13/no14 updated
[POWER] peak current OK, next sample in 300000 ms
  • If Connected or connected appears, the Wi-Fi step passed.
  • If HTTP 200 or 200 OK appears, the value reached the brain server.
  • If the value does not update, check both the slot number in the URL and the sensor wiring.

Build notes

  • First confirm each sensor prints values in the serial monitor. Then check that the hub receives both CO2 and dust values.
  • Power on the hub brain first so the node can join IoT_Hub_Net.
  • Use the same slot number written in the source code.
  • Open the serial monitor at 115200 baud and confirm HTTP 200.
  • If the value does not appear on the hub, recheck wiring, GPIO number, and Wi-Fi connection.

Air-Quality Sensor Uses & Placement (MH-Z19B + PMS5003)

CO2 and fine dust are invisible but directly affect focus, sleep, and health. Adding both sensors to your home IoT hub turns 'should I ventilate now?' into a real number.

Troubleshooting checkpoints

The node code can look short, but real home installation is affected by placement, power, Wi-Fi quality, and sensor noise. Check these points before letting the node become a decision input for the brain server.

SymptomLikely causeField fix
CO2 stays near 400 ppmABC auto-calibration treats a closed room as outside airDisable ABC in rooms that are not regularly ventilated and calibrate after a full fresh-air ventilation cycle.
PM2.5 is always zero or too lowThe PMS5003 fan has not warmed up or the inlet is blockedRun the fan for at least 30 seconds before sampling and keep inlet/outlet paths open.
The two UART sensors fail alternatelyESP8266 SoftwareSerial is unstable with multiple serial sensorsSeparate read timing or move long-term operation to ESP32 with more hardware UARTs.
Dust readings spike too much while cookingOil mist and steam enter the PMS5003 directlyAvoid mounting directly above the stove and use moving averages for alerts while keeping peaks in logs.

How to use them

  • Ventilation-timing alerts: When CO2 passes 1000ppm, the hub pushes an 'open a window' alert to your phone. Especially useful in sealed winter rooms.
  • Sleep & focus environment: If bedroom CO2 climbs to 1500ppm overnight, you wake up groggy. Check it before bed and ventilate first. Keep a study under 800ppm to hold concentration.
  • Cooking dust watch: Grilling or frying spikes PM2.5 to hundreds of ㎍/㎥ in seconds. Use the kitchen reading to decide when to run the exhaust fan harder.
  • Auto-link to purifier/fan: Add a hub rule to switch a smart-plugged air purifier ON when PM2.5 hits 'bad' and OFF once it clears.
  • Kids' room care: Children breathe more and are more sensitive to dust. Set stricter thresholds for their room.

Recommended placement

  • Breathing height: Desk height (about 1–1.5m off the floor), roughly nose level when seated, best matches the air you actually breathe.
  • Avoid direct airflow: Right in front of windows, vents, or AC outlets makes readings spike and hides the room's real state. Keep 30cm+ away.
  • Beware kitchen smoke: Directly above the stove, oily smoke coats the PMS5003 fan and shortens its life. Mount it a bit away.
  • CO2 vs dust layout: Put CO2 where people are (living room, bedroom); put the dust sensor where you track outside intake and cooking. Keep both on one hub but avoid a fully sealed case so air can flow through.

Field tips & cautions

  • MH-Z19B warm-up & ABC: Allow ~3 minutes of warm-up after power-on for stable values. ABC (auto-calibration) treats the daily minimum as 400ppm outdoor air; in rarely-ventilated rooms this drifts, so disable it and calibrate manually if needed.
  • PMS5003 fan life & dust: The internal fan is a consumable (~3-year life). Measure in short bursts every few minutes instead of running constantly to save the fan and reduce dust buildup.
  • Reference values: Keep CO2 under 1000ppm (over 1500ppm causes drowsiness). For PM2.5 (Korean grades): Good 0–15, Moderate 16–35, Bad 36–75, Very Bad 76㎍/㎥ and up.
  • Two UART devices: Both use UART, so stacking SoftwareSerial corrupts data. Use an ESP32 with multiple hardware serials, or run SoftwareSerial on only one and keep 9600bps.

FAQ

Q. CO2 stays high — is it the people? Human breathing is the main source. More people or a closed door raises it fast. Ten minutes of airing usually drops it to 400–600ppm.

Q. At what dust level should I worry? PM2.5 becomes 'Bad' at 36㎍/㎥. From there, close windows and run a purifier; over 76, limit outdoor activity too.

Q. What if the two readings disagree? CO2 needs ventilation (windows); dust needs filtration (purifier). On high-dust days use the purifier instead of opening windows; if only CO2 is high, ventilate briefly.