Overview
This DIY air quality monitoring system integrates precision sensors with a connected processing platform and a low-power e paper display.
it monitors :
- temperature
- pressure
- humidity
- CO2 level
- air quality (particules/dust)
System Architecture
- Processing Unit (Core): The ESP32 S2 Lolin mini manages the sensors. It offers native Wi-Fi connectivity, ideal for sending data to dashboards (Home Assistant, InfluxDB, or MQTT). Its architecture enables efficient management of peripherals via SPI and I2C interfaces.
![]()
So for this project you will have the choice between (non exclusive):
- a simple e-paper display without any connection to Wifi !
- an IoT devoce connected to ThingSpeak
- an IoT device conected to your HomeAssistant server
- Display:
- 3.7-inch E-Paper (240x416): Electronic ink technology allowing for static data display with zero power consumption between refreshes, ensuring a clear and readable interface.
![]()
- 3.7-inch E-Paper (240x416): Electronic ink technology allowing for static data display with zero power consumption between refreshes, ensuring a clear and readable interface.
- Air Quality Sensors:
- SCD40: Photoacoustic sensor for CO2 concentration, temperature, and relative humidity.
![]()
- Plantower PMS5003: Laser scattering sensor for measuring fine particulate matter (PM1.0, PM2.5, PM10).
![]()
- SCD40: Photoacoustic sensor for CO2 concentration, temperature, and relative humidity.
- Complementary Environmental Sensors:
Schematics
it couldn't be simpler !

Implementation Considerations
- Power Management: The PMS5003 consumes significant current during startup (approx. 100mA). The 5V USB power source of the ESP32 can handle this peak load. The other sensors are directly powered at 3.3V witth the EPS32's power regulator. BME and DS18B20 are powered via an I/O pin of the ESP32
- Bus Sharing:
- The BME280 and SCD40 typically use the I2C bus. (they don't have the same addresses so do not conflict)
- The E-Paper uses an SPI interface, allowing it to function simultaneously with the I2C sensors without pin communication interference.
- Airflow: The PMS5003 requires constant airflow to function accurately. When designing the enclosure, the inlet and outlet are unobstructed and air circulates freely around the temperature and CO2 sensors.
PCB
The PCB can be bought at PCBWay


As you can see corners are "rounded" in order to fit into the 3d printed enclosure. The PCB is delivered CNC milled with this exact shape. No screws are needed for the PCB to fit into the enclosure.

Soldering the PCB is easy. Sensors go on both sides:

Result is a clean and compact setup.
The PCB was kindly sponsored by PCBWay and is as usual of excellent quality.

You can order it here : PCBWay shared project. It's cheap, delivered very fast, and so professional looking!
and if you are new to PCBWay please use this affiliated link : https://pcbway.com/g/o35z4O
Enclosure
You can print the enclosure, the PCB will fit inside

All the sensors fit into the box, venting holes are preset to help air to circulate into the Plantower laser particules sensor and the CO2 sensor.
Here is the box once closed

Firmware
Source code for the ESP32-S2 is quite simple and is available on my github pages
main characteristics are :
* ============================================================================
* ESP32-S2 Environmental & Air Quality Monitoring Station
* ============================================================================
*
* Description:
* A comprehensive environmental monitoring station utilizing an E-Paper display.
* The firmware implements a non-blocking asynchronous state machine to manage
* sensor warm-up sequences, take stable multi-sample averages, and refresh the UI.
*
* Integrated Hardware:
* - SCD4x : CO2, Temperature & Humidity (I2C)
* - BME280 : Pressure, Temperature & Humidity (I2C)
* - Plantower PMS5003 : PM1.0 & PM2.5 Particulate Matter (HardwareSerial)
* - DS18B20 : High-precision Temperature (OneWire)
* - E-Paper : Screen display (SPI via GxEPD2)
*
* Architecture & Power Strategy:
* This device is designed for MAINS POWER (USB / Wall Adapter).
*
* Workflow:
* Boot -> Init Sensors -> Async Warm-up & Multi-sampling -> Render E-Ink -> Deep Sleep
*
* ============================================================================
You will probably have to calibrate the D18D20 temperature sensor and more importantly the BME280 to take into account the difference of pressure between your location and sea level.
Two constants are provided :
//*****************************************************************************
// CONST
//*****************************************************************************
#define DS18B20_OFFSET -0.4 //calibration offset for temperature sensor
#define PRESSURE_OFFSET 24 //compensation of pressure to sea level
configuration
by default the firmware acts as a standalone device not connected to internet.
If you want to connect to Thingspeak and/or to your Home Assistant then you will have to activate the options you want
go to "wifi" section and uncomment these lines and enter your credentials
//*****************************************************************************
// WIFI (To be completed)
//*****************************************************************************
#define USE_THINGSPEAK //Uncomment if you wish to use it.
#define USE_MQTT //Uncomment if you wish to use it.
// --- Wifi config ---
const char* ssid = "Your SSID";
const char* password = "your password";
ThingSpeak
If you want to connect to thingspeak, you will have to create an account then a new channel, get its number and its API Write key. Then select the fields to display as shown below.

enter these values here :
#ifdef USE_THINGSPEAK
#include "ThingSpeak.h"
unsigned long myChannelNumber = 11111;
const char* myWriteAPIKey = "yourAPIkey";
And you are good to go, compile and have a look at your channel !

Note that I have added "gauge widgets" to have an overview of each sensor as well as the graphs for long term trends.
Do what you want it's easy.
Home Assistant
Be aware that home assistant integration is easy into the firmware but quite complex in home assistant itself !
for the firmware side
- integrate the pubSubClient Library
- uncomment the USE_MQTT line (as seen above)
- enter your MQTT credentials
#ifdef USE_MQTT #include //http://pubsubclient.knolleary.net/ // MQTT Credentials -- These will override the global settings const char* mqtt_server = "192.168.1.174"; //http://192.168.1.174:8123 #define mqtt_port 1883 // Default MQTT port is 1883 #define MQTT_USER "mqtt_broker" #define MQTT_PASSWORD "your MQTT password" #define MQTT_RECEIVER_CH "garden/cmd" //reserved for future use (control and command) WiFiClient wifiClient; PubSubClient mqttClient(wifiClient); #endif
And you can compile again, your code will try to connect to the MQTT server into Home assistant and "register" the sensors as MQTT integration
void sendDiscovery() {
// Liste des capteurs à déclarer
const char* configs[][2] = {
{"co2", "{\"name\":\"CO2\",\"unique_id\":\"AQ_co2_01\",\"state_topic\":\"sensorValue/CO2\",\"unit_of_measurement\":\"ppm\",\"icon\":\"mdi:molecule-co2\",\"device_class\":\"carbon_dioxide\",\"state_class\":\"measurement\"}"},
{"temperature", "{\"name\":\"Temperature\",\"unique_id\":\"AQ_temperature_01\",\"state_topic\":\"sensorValue/temperature\",\"unit_of_measurement\":\"°C\",\"icon\":\"mdi:thermometer\",\"device_class\":\"temperature\",\"state_class\":\"measurement\"}"},
{"humidity", "{\"name\":\"Humidity\",\"unique_id\":\"AQ_humidity_01\",\"state_topic\":\"sensorValue/humidity\",\"unit_of_measurement\":\"%\",\"icon\":\"mdi:water-percent\",\"device_class\":\"humidity\",\"state_class\":\"measurement\"}"},
{"pressure", "{\"name\":\"Pressure\",\"unique_id\":\"AQ_pressure_01\",\"state_topic\":\"sensorValue/pressure\",\"unit_of_measurement\":\"hPa\",\"icon\":\"mdi:gauge\",\"device_class\":\"atmospheric_pressure\",\"state_class\":\"measurement\"}"},
{"aqi", "{\"name\":\"AQI\",\"unique_id\":\"AQ_aqi_01\",\"state_topic\":\"sensorValue/aqi\",\"icon\":\"mdi:air-filter\",\"device_class\":\"aqi\",\"state_class\":\"measurement\"}"}
};
for (int i = 0; i < 5; i++) {
String topic = "homeassistant/sensor/" + String(configs[i][0]) + "/config";
mqttClient.publish(topic.c_str(), configs[i][1], true);
delay(200); // Petite pause pour éviter la saturation du bus MQTT
}
In this part of the code you can change the name and the uniqueID values
Then firmware will publish the sensors values
//send sensors values to MQTT broker
mqttClient.publish("sensorValue/temperature", data.temp.value.c_str(), true);
mqttClient.publish("sensorValue/humidity", data.humidity.value.c_str(), true);
mqttClient.publish("sensorValue/pressure", data.pressure.value.c_str(), true);
mqttClient.publish("sensorValue/aqi", data.aqi.value.c_str(), true);
mqttClient.publish("sensorValue/CO2", data.co2.value.c_str(), true);
Serial.println("MQTT publish ok");

But to get these graphs you will need to configure Home Assistant !
As it is quite long it will be explained into this log.
Note that if you are familiar with Home Assistant and your MQTT broker is running, you can skip this log as your sensors will be automatically discovered when connected to your Wifi !
JP Gleyzes




