ESP32 CO2 Monitor
Due to suspecting a slightly stuffy air in my office, having a CO2 sensor seemed like a good idea.
I decided to build it using an ESP32 module with an MH-Z19b CO2 sensor due to how easy it would be to integrate in Home Assistant with ESPHome in the future should I want to do so.
In a first stage the sensor is to be USB powered, with no integrated display and with a DHT22 added to also provide a temperatura readout (its relative humidity readout is not used at this point due to being borked on this specific sensor).
In a second stage I’d like to integrate an e-ink screen and possibly make it battery powered, heavily inspired by this project.
This may not be possible with the CO2 sensor: the one specifically used is designed to be kept powered on for a long time, and in fact should calibrate itself every 24 hours on the lowest reading detected by assuming that it should be equal to 400 ppm - this would not work well with a device that frequently goes to sleep, or it might compromise battery life too much by itself.
Here’s also the current version of the controller’s program, with a sample of the generated webpage.
#include <WiFi.h>
#include <DHT.h>
#include <MHZ19.h>
#define RX_PIN 16 // Rx pin which the MHZ19 Tx pin is attached to
#define TX_PIN 17 // Tx pin which the MHZ19 Rx pin is attached to
#define DHTTYPE DHT22 // DHT 22 (AM2302)
#define DHTPIN 23
#define BAUDRATE 9600
#define WLAN_SSID "yourSSID"
#define WLAN_PASS "yourPASSWORD"
// Constructors
MHZ19 myMHZ19; // Constructor for CO2 sensor
DHT dht(DHTPIN, DHTTYPE); // Constructor for temp and RH sensor
// SSID, Password, hostname
const char* ssid = WLAN_SSID; // Enter your SSID here
const char* password = WLAN_PASS; // Enter your Password here
String hostname = "co2sensor";
// Sensor readout
unsigned long currentMillis = 0;
const int co2Interval = 10000;
const int tempInterval = 10000;
const int rhInterval = 10000;
const int ipInterval = 10000;
unsigned long previousCo2 = 0;
unsigned long previousTemp = 0;
unsigned long previousRH = 0;
unsigned long previousIp = 0;
int co2 = 1234;
float temp = 10;
float rh = 10;
String HTML;
// Used for HTML page
int co2Level1 = 1000;
int co2Level2 = 1500;
int co2Level3 = 2000;
String string1 = "<" + String(co2Level1) + " ppm: Typical level for occupied spaces with good air exchange.";
String string2 = String(co2Level1) + "-" + String(co2Level2) + " ppm: Start ventilation of the room.";
String string3 = String(co2Level2) + "-" + String(co2Level3) + " ppm: Level associated with complaints of drowsiness and poor air. Ventilation recommended!";
String string4 = ">" + String(co2Level3) + " ppm: Level associated with headaches, sleepiness, and stagnant, stale, stuffy air. Ventilate!";
WiFiServer server(80); // Object of WebServer(HTTP port, 80 is defult)
void setup() {
Serial.begin(115200);
Serial.println("Attempting connection to ");
Serial.println(ssid);
// Set hostname
WiFi.setHostname(hostname.c_str());
// Connect to your wi-fi modem
WiFi.begin(ssid, password);
// Check wi-fi is connected to wi-fi network
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected successfully");
Serial.print("Got IP: ");
Serial.println(WiFi.localIP()); //Show ESP32 IP on serial
// Attempt at light power saving while preserving connectivity https://www.mischianti.org/it/2021/03/06/esp32-risparmio-energetico-pratico-gestire-wifi-e-cpu-1/
setWiFiPowerSavingMode();
// Start web server
server.begin();
Serial.println("HTTP server started");
delay(100);
Serial.print("hostname: ");
Serial.println(WiFi.getHostname());
// Setup second serial port (to co2 sensor) and start it
Serial2.begin(BAUDRATE, SERIAL_8N1, RX_PIN, TX_PIN); // (Uno example) device to MH-Z19 serial start
myMHZ19.begin(Serial2); // *Serial(Stream) refence must be passed to library begin().
// Enable co2 sensor autocalibration - autoCalibration(false) to turn it off like in this case
myMHZ19.autoCalibration(false);
// Start temperature and humidity sensor
dht.begin();
}
void loop() {
currentMillis = millis();
updateCo2();
updateTemp();
updateIp();
// updateRH(); Deactivated due to faulty sensor
WiFiClient client = server.available();
if (client)
{
Serial.println("\n[Client connected]");
while (client.connected())
{
// read line by line what the client (web browser) is requesting
if (client.available())
{
String line = client.readStringUntil('\r');
Serial.print(line);
// wait for end of client's request, that is marked with an empty line
if (line.length() == 1 && line[0] == '\n')
{
client.println(prepareHtmlPage());
break;
}
}
}
while (client.available()) {
// but first, let client finish its request
// that's diplomatic compliance to protocols
// (and otherwise some clients may complain, like curl)
// (that is an example, prefer using a proper webserver library)
client.read();
}
// close the connection:
client.stop();
Serial.println("[Client disconnected]");
}
}
void setWiFiPowerSavingMode() {
WiFi.setSleep(true);
}
void updateIp() {
if (currentMillis - previousIp >= ipInterval) {
previousIp += ipInterval;
Serial.println(WiFi.localIP());
}
}
void updateCo2()
{
if (currentMillis - previousCo2 >= co2Interval) {
previousCo2 += co2Interval;
//co2 = 1500 + random(-1000, 1000); // Fake data to test webpage
co2 = myMHZ19.getCO2(); // Request CO2 (as ppm)
Serial.println("Co2 = " + String(co2));
HTML = prepareHtmlPage();
}
}
void updateTemp()
{
if (currentMillis - previousTemp >= tempInterval) {
previousTemp += tempInterval;
temp = dht.readTemperature();
Serial.println("Temp = " + String(temp));
}
}
void updateRH()
{
if (currentMillis - previousRH >= rhInterval) {
previousRH += rhInterval;
rh = dht.readHumidity();
Serial.println("RH = " + String(rh));
}
}
// Webpage preparation
// Bits and bobs stolen from https://randomnerdtutorials.com/esp32-web-server-arduino-ide/
String prepareHtmlPage()
{
String htmlPage;
htmlPage.reserve(1024); // prevent ram fragmentation
htmlPage = F("HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Connection: close\r\n" // the connection will be closed after completion of the response
"Refresh: 15\r\n" // refresh the page automatically every 15 sec
"\r\n"
"<!DOCTYPE HTML>"
"<html>"
"<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
"<style>html { font-family: Helvetica; text-decoration: none; font-size: 24px; margin: 2px; } </style>"
"</head>"
"<body style=""background-color:cornsilk;""><h1>CO<sub>2</sub> monitor</h1>"
"<br> ");
htmlPage += "<p>CO<sub>2</sub>: " + String(co2) + " ppm </p>";
htmlPage += "<p>Temp: " + String(temp) + " C </p>"; // had to write ° and C as two separate hex characters
// Advisory messages
if (co2 < co2Level1) { // Warning levels stolen from https://www.technoline-berlin.de/product/en/WL_1030
htmlPage += "<p style=\"font-size:50px\">👍</p>";
htmlPage += "<p>" + string1 + "</p>";
htmlPage += "<p style=\"color:grey;\">" + string2 + "</p>";
htmlPage += "<p style=\"color:grey;\">" + string3 + "</p>";
htmlPage += "<p style=\"color:grey;\">" + string4 + "</p>";
}
if (co2 >= co2Level1 && co2 < co2Level2) {
htmlPage += "<p style=\"font-size:50px\">🪟</p>";
htmlPage += "<p style=\"color:grey;\">" + string1 + "</p>";
htmlPage += "<p>" + string2 + "</p>";
htmlPage += "<p style=\"color:grey;\">" + string3 + "</p>";
htmlPage += "<p style=\"color:grey;\">" + string4 + "</p>";
}
if (co2 >= co2Level2 && co2 < co2Level3) {
htmlPage += "<p style=\"font-size:50px\">👎</p>";
htmlPage += "<p style=\"color:grey;\">" + string1 + "</p>";
htmlPage += "<p style=\"color:grey;\">" + string2 + "</p>";
htmlPage += "<p>" + string3 + "</p>";
htmlPage += "<p style=\"color:grey;\">" + string4 + "</p>";
}
if (co2 >= co2Level3) {
htmlPage += "<p style=\"font-size:50px\">💀</p>";
htmlPage += "<p style=\"color:grey;\">" + string1 + "</p>";
htmlPage += "<p style=\"color:grey;\">" + string2 + "</p>";
htmlPage += "<p style=\"color:grey;\">" + string3 + "</p>";
htmlPage += "<p>" + string4 + "</p>";
}
htmlPage += F("</html>"
"\r\n");
return htmlPage;
}