Air Quality Monitor (ESP32)
A dashboard that only reacts to new messages shows blank data on every single reconnect — and a browser refresh is a reconnect.
- ESP32
- C++
- MQTT
- Dashboard
About this project
An ESP32 reads PM2.5 from a Plantower PMS5003 and temperature/humidity from a DHT22, builds one small JSON reading, and publishes it over MQTT every few minutes — a real, low-power publish interval, not a demo shortcut. A small Flask dashboard subscribes to every sensor's topic and shows the latest reading per location, with a CPCB PM2.5 category badge.
A dashboard that only reacts to new MQTT messages shows blank or stale data every single time it connects or reconnects — and a browser refresh is a reconnect — until the next publish actually happens. That is not a bug in Flask or in this dashboard's own code; it is how a plain publish() with no retain flag works by design. The fix is one flag on the exact same publish call: MQTT's own retain flag, which asks the broker — not the publisher, not the subscriber — to remember the last value on a topic and hand it to any client that subscribes afterwards, immediately. Proven with a real, timed standalone script (a fresh subscriber times out after 3+ seconds with no retain, then receives the same reading in under 3 milliseconds with it), and again through the actual broker/dashboard/publisher pipeline: a dashboard restarted after publishing shows every reading instantly, tagged as retained, instead of sitting blank until the next scheduled reading arrives.
What you will learn
- Understand MQTT publish/subscribe as a broker-mediated pattern, not a direct connection
- Explain why a late-connecting subscriber gets nothing without the retain flag, and prove it with a timed measurement
- Use the MQTT retain flag correctly on both an ESP32 publisher and a Python one
- Read and apply India's CPCB PM2.5 breakpoints, with their real limitations honestly disclosed
- Reuse the exact retain pattern in any pub/sub system where state changes slowly and clients reconnect
Before you start
- Arduino/C++ basics and this catalogue's arduino-weather-station or iot-plant-watering-system
- Basic Python — reading a Flask app and an MQTT client
- An ESP32 dev board, a PMS5003 and a DHT22 for real hardware use — none needed to install, verify, or read this project
New to ESP32? The free ESP32 course covers everything this project assumes.
Academic integrity notice
These projects are provided for learning, experimentation and reference. Understand the code, modify it for your own requirements, and acknowledge external work according to your institution’s academic policies. Priodemy does not supply work to be submitted as your own.
Features
- A real, timed proof of the retain flag: a measured 3-second timeout with no retain vs. sub-3ms delivery with it
- A real MQTT broker (amqtt) running locally — no cloud account, Mosquitto install, or ESP32 needed to verify
- A four-locality simulated sensor fleet publishing genuine JSON readings over a real MQTT connection
- India's CPCB PM2.5 category breakpoints, reproduced exactly as published, with both simplifications named honestly
- A real ESP32 sketch — PMS5003 frame parsing and DHT22 bit-timing reasoned through against their datasheets
- The exact same retain pattern generalised to any slow-changing pub/sub system — e.g. a ride-hailing driver-location topic
Technology stack
Track: IoT & Hardware · Level: Pro
What is included
- air_quality_sensor.ino, aqi_common.py, broker.py, dashboard.py, simulate_publisher.py and prove_retain.py — commented throughout
- A README with wiring diagram, parts list, CPCB category table, and the real timed retain-flag proof
- prove_retain.py — an isolated, self-contained proof script, runnable with zero other project files running
Delivered as a 36 KB ZIP. SHA-256 published under Changelog so you can verify it.
System requirements
- Operating system
- Any (Arduino IDE for the real sketch)
- Runtime
- Python 3.10+. An ESP32, PMS5003 and DHT22 for real hardware use.
Licence
Learn from it, change it, and use it in your own coursework and personal projects. Do not resell it or pass it off as your own original work.
- Personal and educational use
- Modifying the code however you like
- Using it as the basis of your own coursework, with honest acknowledgement
- Putting your modified version in a portfolio or on your own GitHub
- Reselling or redistributing the project, modified or not
- Presenting it as your own original work
- Commercial deployment — see the Commercial licence
Support window: 6 months from download.
Frequently asked
Do I need an ESP32 or real sensors to try this?
No. broker.py is a real, locally-runnable MQTT broker (via the amqtt Python library), and simulate_publisher.py opens a genuine MQTT connection to it and publishes the exact JSON shape the real sketch publishes — not a mock. prove_retain.py proves the core retain-flag fix on its own, with a real timed transcript, no hardware required. The ESP32 sketch itself could not be compiled on the machine this was built on (no ESP32 toolchain here) — the README says so honestly and documents how its PMS5003 frame parsing and DHT22 timing were reasoned through against their datasheets instead.
Is the PM2.5 badge the official Indian AQI?
No, and the README is explicit about this. It is a PM2.5 sub-index category using CPCB's own published breakpoints, applied to a single instantaneous reading rather than the 24-hour average CPCB actually defines them for, and it covers PM2.5 alone rather than the worst of eight pollutants the real National AQI is computed from. Both simplifications are named in the README and in aqi_common.py itself, not hidden.
What does the retain flag actually fix?
A subscriber that connects after the last publish — including a browser refreshing the dashboard — gets nothing until the next publish, because a plain MQTT publish only reaches clients subscribed at that exact instant. retain=True asks the broker to remember the last value on a topic and hand it to any later subscriber immediately. prove_retain.py measures this directly: a fresh subscriber times out after 3+ seconds without retain, and receives the same reading in under 3 milliseconds with it.
Changelog
- First release
SHA-25680b8f7f1d36d456b6b03dddae89e0c2391dbd03c763e04c8d5e60b7e8096757e