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](/iot-lab/assets/co2-dust-air-quality-hero.png)
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
| Part | Qty | Unit price |
|---|---|---|
| Wemos D1 Mini | 1 pc | $1.50 |
| MH-Z19B CO2 sensor | 1 pc | $15 |
| PMS5003 fine dust sensor | 1 pc | $11 |
| Jumper wires | 8 pc | — |
| Total | $27 |
Pin wiring
| Sensor / module | Board pin |
|---|---|
| VCC | VU (5V) |
| GND | GND |
| TX | D2 (GPIO4) |
| RX | D3 (GPIO0) |
| VCC | VU (5V) |
| GND | GND |
| TX | D6 (GPIO12) |
| RX | D7 (GPIO13) ← PMS5003 RX is optional, but wired here |
Full code
#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
Connectedorconnectedappears, the Wi-Fi step passed. - If
HTTP 200or200 OKappears, 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.
| Symptom | Likely cause | Field fix |
|---|---|---|
| CO2 stays near 400 ppm | ABC auto-calibration treats a closed room as outside air | Disable ABC in rooms that are not regularly ventilated and calibrate after a full fresh-air ventilation cycle. |
| PM2.5 is always zero or too low | The PMS5003 fan has not warmed up or the inlet is blocked | Run the fan for at least 30 seconds before sampling and keep inlet/outlet paths open. |
| The two UART sensors fail alternately | ESP8266 SoftwareSerial is unstable with multiple serial sensors | Separate read timing or move long-term operation to ESP32 with more hardware UARTs. |
| Dust readings spike too much while cooking | Oil mist and steam enter the PMS5003 directly | Avoid 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.
This article is part of the 20-channel Home IoT system built from the Part 2 central brain server and the distributed edge nodes in Parts 3-19. Use this map to check the slot and sensor flow across the series.
[Part 1] Home IoT Hub overview[Part 2] Central brain server · slots 1-20[Part 3] Stop guessing room comfort · slots 1/2[Part 4] Did I close the door? · slots 9[Part 5] Is the fridge really cooling? · slots 12[Part 6] Should I open the window? · slots 3[Part 7] Stop bathroom mold before it starts · slots 19[Part 8] Is the boiler actually running? · slots 5/17[Part 9] Did I leave the gas stove on? · slots 6[Part 10] Get warned before gas smell becomes obvious · slots 7[Part 11] Catch flame signs while away from home · slots 8[Part 12] Is bad air making you foggy? · slots 13/14[Part 13] Why is the power bill so high? · slots 15[Part 14] Is water leaking, and how much are we using? · slots 16[Part 15] Do not leave windows open in the rain · slots 18[Part 16] Are the lights off and the door locked? · slots 11/10[Part 17] Control the air conditioner from the IoT hub · slots 4[Part 18] Is the dog bowl empty? · slots 20[Part 19] Cool the room only when someone is there · slots 1/4[Part 20] The house finally fits on one screen · slots all