CO2 Monitor
May 2026

 

CO2 Monitor

Monitor  Monitor

 

Although most of us are aware of the importance of monitoring carbon monoxide gas (CO) in our homes, a build up of carbon dioxide (CO2) can also cause harmful health effects.

This table shows the effects of increasing CO2 levels in an enclosed space.

400 ppm
0.04%
Normal outdoor air
400 - 1,000 ppm
0.04 - 0.1%
Typical indoor CO2 levels
1,000 - 2,000 ppm
0.1 - 0.2%
Complaints of drowsiness
2,000 - 5,000 ppm
0,.2 - 0.5%
Headaches, fatigue, poor concentration,
loss of focus, nausea, increased heart rate
>50,000 ppm
>5%
Toxicity due to oxygen deprivation
>100,000 ppm
>10%
Oxygen deprivation in seconds, convultions, coma and death

 

This project monitors the CO2 level using a Sensirion SCD40 CO2 sensor and updates an LCD display every 30 seconds. Three LEDs (green, yellow and red) show the approximate CO2 level. The levels are hard-coded in the Arduino sketch.

An alarm sounds at a pre-set level. Additionaly, it can send a notification request to Pushsafer which will send a push notification to your mobile phone.

To use Pushsafer, you need to set up an account to obtain a "private key" and a "Device ID".

The project can, optionally, at 15 minute intervals, send the CO2 level and the Relative Humidity to a simple Windows server program which you can run on a PC. The program plots a chart (see the image above) which is also acessible over the internet.

Obviously, to use the Pushsafer notification and/or the server, you need a WiFi connection.

You can view the chart running on my server here (if the server application is running).

You can download the Windows application here.

(I don't supply source code for my Windows applications so please don't ask.)

 

Circuit Diagram

The circuit is fairly straightforward using an ESP32 30-pin development module. The Nokia 5110 display uses the hardware SPI interface and the SCD40 CO2 sensor uses the I2C interface. Virtually every usable IO pin on the ESP32 board is used.

The ESP32 module, the Nokia 5110 display and the SCD40 sensor are all supplied with 3.3 volts. The 3v3 supply from the ESP32 module isn't quite capable of supplying enough power so I used an LD33CV Low drop-out voltage regulator with a 5 volt supply.

The active buzzer takes 23mA so is just within the ESP32's output pins capability for a short duration. The buzzer is wired through a DIP switch - DIP1 in the circuit diagram - mounted on the rear panel to allow the buzzer to be disabled if it's not required.

A second DIP switch - DIP2 connected to ESP32 input D34, enables/disables the PushSafer notifications.

The push button connected to ESP32 input D35 is used to cancel the buzzer. If the buzzer is not sounding, the button acts as a Test and briefly sounds the buzzer (if DIP1 is closed) and sends a notification request to PushSafer if notifications are enabled (DIP2).

The input on D27 toggles the LCD backlight on or off.

Printed Circuit Board

Download Actual size PCB Artwork in PDF format.

Download PCB Wizard design files.   (PCB Wizard)

 

Setting up the Arduino IDE

If you haven't used the ESP32 in the Arduino environment before, it's necessary to install the ESP32 Board definitions into the Arduino IDE.

In the Arduino IDE, select Tools -> Boards Manager.

In the Search text box, enter: ESP32.

 


Wait for the platforms index to download then scroll down the list to esp32 by Espressif Systems and click Install.

 

Close the Arduino IDE.

 


Copy the CO2 Monitor sketch below and paste it into the IDE.

"The WiFiManager library (by tzapu) is a powerful tool for ESP8266 and ESP32 boards in the Arduino IDE that allows setting up WiFi credentials via a web browser without hardcoding them. It creates a captive portal when it cannot connect to a known network, enabling easy, dynamic reconfiguration of network settings."

How the WiFi Manager works

  

 

ESP32 - CO2 Monitor - Arduino Sketch

Additional Arduino Libraries

Sparkfun SCD4x Library
WiFi Manager Library
Adafruit Graphics Library
Adafruit Nokia 5110 Library 
Arduino_JSON Library
Preferences Library
URL Encode Library

/*  ESP32   LOLIN D32 Board*/

/* CO2 Monitor, Plotter and Pushsafer notifier */

#include <WiFi.h>
#include <WiFiManager.h>                                  // https://github.com/tzapu/WiFiManager 

#include <HTTPClient.h>  
#include <Arduino_JSON.h>                                 // https://github.com/arduino-libraries/Arduino_JSON
#include <UrlEncode.h>


#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>
#include <Fonts/FreeSansBold12pt7b.h>

#include <Wire.h>

#include <Preferences.h>

Preferences preferences;


#include "SparkFun_SCD4x_Arduino_Library.h" //Click here to get the library:http://librarymanager/All#SparkFun_SCD4x

SCD4x mySensor;


  //mySensor.enableDebugging(); // Uncomment this line to get helpful debug messages on Serial


  // CLK = 18  - Hardware SPI             // Hardware connections to Nokia 5110 LCD display
  // MOSI = 23 - Hardware SPI
  // CS = 5    - Hardware SPI

  // D/C = 17  - Arbitrary selection
  // RST = 16  - Arbitrary selection

  // LED = 4  - Backlight.
  // Vcc - 3v3.
  // GND - GND.


 Adafruit_PCD8544 display = Adafruit_PCD8544(17, 5, 16);  // (D/C, CS, RST)
 
// Note with hardware SPI MISO and CS pins aren't used but will still be read  MISO = 19.  CS = 5
// and written to during SPI transfer.  Be careful sharing these pins!



String serverIP;          // These values will be entered during the WiFi Manager's "Configure WiFi" routine.
String id;                // The server IP is the LAN address of the computer running the server followed
                          // by a colon (:) and the Port which must be set up in the WiFi roouter's Port-Forwarding.

char serverIPDefault[] = "192.168.1.3:8841";    // This is the default server IP and port I used. Yours will be different.
                                                // Setting a default saves having to re-enter the details in the WiFi Manager
                                                // if the loss of the Network was only temporary and you will be re-connecting
                                                // to the same network.
                                                
char idDefault[] = "co2_sensor";                // This is the default ID sent from the CO2 sensor to the server so that..
                                                // the server knows which device is sending data.


#define CO2_WARNING 3500                // Sound buzzer id co2 level is above this value.
#define CO2_HIGH 4000                   // Send notification if notification_sent flag is false.

#define PLOT_MAX 30                     // SCD40 sensor provides data every 30 seconds but we only need to send data
                                        // to the server/plotter every 15 minutes, so count 30 sensor readings before sending
int plotCounter = 30;                   // data to the server.
                                                                           

#define WEB_PORTAL_TIMEOUT 300          // The WiFi Manager's configuration screen will time out after 5 minutes.
                                        

int rssi;

bool notification_sent = false;         // Flag to indicate a PushSafer notification has been sent to disable repeats.


int reset_notification = 1000;          // reset 'already sent' notification to allow next alarm.

int numLeft = -1;                       // PushSafer tells how many left before account needs renewing and briefly displays
                                        // on Nokia 5110 screen.

bool alarm_sounding = false;
bool alarm_cancelled = false;

String pushSaferURL = "http://www.pushsafer.com/api";     // PushSafer server URL.

String pushSaferKey;                                      // PushSafer Key and DeviceID are obtained when you set up..
String deviceID;                                          // your PushSafer Account.
 
char pushSaferKeyDefault[] = "                    ";      // You can enter your PushSafer Key and Device ID here to..
char deviceIDDefault[] = "      ";                        // pre-load the WiFi configuration screen.
                                                          // You can over-write WiFi configuration screen with new data..
                                                          // if you change your PushSafer Key or Device ID.

String title = "CO2 Monitor";                             // PushSafer notification Title
String icon = "11";                                       // PushSafter "Leave building" icon. [->

String pushSaferPath;                                     // Full url to PushSafer including paramneters.

bool displayLight = false;


bool shouldSaveConfig = false;                            // WiFimanager Flag for saving data


void saveConfigCallback() {                               // Callback notifying us of the need to save configuration.
  shouldSaveConfig = true;
}


void configModeCallback(WiFiManager *myWiFiManager) {     // (Very) brief instructions shown on Nokia 5110 screen
                                                          // Called when config mode launched.

  display.clearDisplay();

  display.println("Connect phone:");                      // On your phone, search Settings -> Connections ->
  display.println("'CO2 Monitor'");                       // WiFi networks for "CO2 Monitor".  Connect to that
  display.println("Browser type: ");                      // network.
  display.println(WiFi.softAPIP());                       // Opern a web browser and enter the IP address shown.
  display.println("Configure WiFi");                      // Press 'Configure WiFi' then tap on your WiFi router
  display.print("Press 'Save'");                          // in the list, then fill in the text boxes and hit
  display.display();                                      // 'Save'. 

}



void setup()   {
  WiFi.mode(WIFI_STA);    //Set Wi-Fi Mode as station
  
  Serial.begin(115200);

  preferences.begin("CO2 Monitor", false);  
   

  displayLight = preferences.getBool("displayLight", displayLight);            // Retrieve default values from File System

  serverIP = preferences.getString("serverIP", serverIPDefault);               // Server local IP address and Port.
  id = preferences.getString("co2_sensor", idDefault);                         // Data identifier from CO2 sensor to server.

  pushSaferKey = preferences.getString("pushSafer Key", pushSaferKeyDefault);  // PushSafer private key
  deviceID = preferences.getString("device ID", deviceIDDefault);              // PushSafer device iD.                         


  pinMode(26, OUTPUT);                      // Buzzer
  digitalWrite(26, LOW);

  pinMode(4, OUTPUT);                       // Backlight 
  digitalWrite(4,  displayLight);

  pinMode(32, OUTPUT);                      // Green LED
  pinMode(33, OUTPUT);                      // Yellow LED 
  pinMode(25, OUTPUT);                      // Red LED
  
  pinMode(34, INPUT);                       // Enable PushSafer notifications.  | These INPUTS seem to have no
  pinMode(35, INPUT);                       // Cancel Buzzer                    | internal pullup resistors.
  pinMode(27, INPUT);                       // Backlight on/off                 |
  
  
  display.begin();
  display.setContrast(60);
  display.setRotation(2);

  display.clearDisplay();   

  
  WiFiManager wm;
  
//  wm.resetSettings();              // To clear WiFi settings for testing, uncomment this line.
//  preferences.clear();             // Clears saved preferences for testing.


// Set config save notify callback
  wm.setSaveConfigCallback(saveConfigCallback);
 
  // Set callback that gets called when connecting to previous WiFi fails, and enters Access Point mode
  wm.setAPCallback(configModeCallback);
 
  wm.setConfigPortalTimeout(WEB_PORTAL_TIMEOUT);        // 5 minutes
  
// Configure web portal with custom parameters

  WiFiManagerParameter serverIP_text_box("serverIP", "Enter your Server IP address and Port", serverIPDefault, 20);
  wm.addParameter(&serverIP_text_box);

  WiFiManagerParameter serverID_text_box("serverID", "Enter the CO2 Sensor server ID", idDefault, 10);
  wm.addParameter(&serverID_text_box);

  WiFiManagerParameter pushSaferKey_text_box("pushSaferKey", "Enter your pushSafer Key", pushSaferKeyDefault, 25);
  wm.addParameter(&pushSaferKey_text_box);
   
  WiFiManagerParameter deviceID_text_box("deviceID", "Enter your PushSafer Device ID", deviceIDDefault, 10);
  wm.addParameter(&deviceID_text_box);

  display.println("Connecting to WiFi");
  
  display.display();

  bool res;
  res = wm.autoConnect("CO2 Monitor");                     // WiFi Manager attempts to connect to the WiFi network
  if(!res) {                                               // and calls loopback  'configModeCallback()' if it fails.
     Serial.println("Failed to connect");

     digitalWrite(26, HIGH);                               // 'Beep' buzzer to warn of failure to connect to WiFi.     
     delay(200);
     digitalWrite(26, LOW);    
     
     display.clearDisplay();
     display.println("No WiFi!");
     display.print("Continuing   without");
     display.display();
     delay(1000);
  } 
  
 // Save the custom parameters to FS
  if (shouldSaveConfig) {
    serverIP = serverIP_text_box.getValue();
    id = serverID_text_box.getValue();

    pushSaferKey = pushSaferKey_text_box.getValue();
    deviceID = deviceID_text_box.getValue();

 
    preferences.putString("serverIP", serverIP);
    preferences.putString("id", id);

    preferences.putString("pushSafer Key", pushSaferKey);
    preferences.putString("device ID", deviceID);

  } 

  rssi = WiFi.RSSI();
    
   
  Wire.begin();
  
  display.println();
  display.println("CO2 Sensor");
  display.print("is starting...");
  display.display(); 

  if (mySensor.begin() == false) {

    display.println("Sensor not detected. Freezing...");
    display.display();
    while (1);
  }

  //By default, the SCD4x has data ready every five seconds.
  //We can enable low power operation and receive a reading every ~30 seconds

  //But first, we need to stop periodic measurements otherwise startLowPowerPeriodicMeasurement will fail
  if (mySensor.stopPeriodicMeasurement() == true) {
   Serial.println(F("Periodic measurement is disabled!"));
 }  

  //Now we can enable low power periodic measurements
  if (mySensor.startLowPowerPeriodicMeasurement() == true) {
    Serial.println(F("Low power mode enabled!"));
  }

  //The SCD4x has data ready every thirty seconds

}


void loop() {
  int co2 = 0;
  int humidity = 0;

  display.setFont();

  display.setCursor(0,0);                                                 // Display an asterisk at th etop left
  if( (digitalRead(34) == LOW) && (WiFi.status() == WL_CONNECTED) ) {     // of the screen if PushSafer notifications
     display.print("*");                                                  // are enabled and WiFi is connected.
  }  else
     display.print(" ");   
      
  display.display();
  
  
  if ( (digitalRead(35) == LOW) && (alarm_sounding) ) {                    // Cancel Alarm Buzzer
    alarm_cancelled = true;
    alarm_sounding = false;
    digitalWrite(26, LOW);
    while (digitalRead(35) == LOW);
  }

  if ( (digitalRead(35) == LOW) && (!alarm_sounding) ) {                   // The 'Cancel Alarm' button is also the
    test_alarms();                                                         // 'Test' button if there is no alarm.
    while (digitalRead(35) == LOW);
  }
    

 if (digitalRead(27) == LOW) {                                             // Display light on/off
    displayLight = !displayLight;
    digitalWrite(4, displayLight);

    preferences.putBool("displayLight", displayLight);                     // Save current state of backlight.
    while (digitalRead(27) == LOW);
  }

  

  if (mySensor.readMeasurement()) {               // readMeasurement will return true when fresh data is available

    plotCounter++;  

    display.clearDisplay();
    display.setCursor(15, 0);
    display.print("CO2 [ppm]");                   // Print the 'header' on the screen

    if (WiFi.status() == WL_CONNECTED) {          // Print an 'i' at top right of screen if WiFi is available.
      display.setCursor(75, 0);
      display.print("i");
    }

  
    display.setFont(&FreeSansBold12pt7b);         // Set large text.

    co2 = mySensor.getCO2();                      // Get the CO2 reading from the sensor.

    switch(co2) {                                 // Control the Green, Yellow and Red LEDs
     case 0 ... 999: 
       digitalWrite(32, HIGH);                    // Green LED
       digitalWrite(33, LOW);                     // Yellow LED
       digitalWrite(25, LOW);                     // Red LED

       digitalRead(35) == LOW;                    // Turn off Alarm buzzer
       alarm_cancelled = false;  
       alarm_sounding = false;

       notification_sent = false;                 // Reset notification sent ready for next time
       break;
     case 1000 ... 1499:
       digitalWrite(32, LOW);      
       digitalWrite(33, HIGH); 
       digitalWrite(25, LOW);

       digitalRead(35) == LOW;                    // Turn off Alarm buzzer
       alarm_cancelled = false;  
       alarm_sounding = false;
       break;
     case 1500 ... 2999:
       digitalWrite(32,LOW); 
       digitalWrite(33, LOW); 
       digitalWrite(25, HIGH);  
       break;
     case 3000 ... 15000:
       digitalWrite(32, LOW);      
       digitalWrite(33, LOW); 
       digitalWrite(25, HIGH); 
       if( (!alarm_cancelled) && (co2 > CO2_WARNING) ) {
          digitalWrite(26, HIGH);                          // Turn ON Alarm Buzzer if not cancelled by button.
          alarm_sounding = true;
       } 
       break;
     default:
       break;
     }

     (co2 < 1000)?  display.setCursor(23,30) : display.setCursor(15, 30);


    display.print(co2);  
    Serial.println(co2);
      
    display.setFont();                                      // Reset to small font and print 'footer' on screen.
    display.drawLine(0, 38, 80, 38, BLACK);
    display.setCursor(0, 40);
    display.print(mySensor.getTemperature(), 1);
    display.print("c");
    
    display.setCursor(66, 40);
    humidity = mySensor.getHumidity();
    display.print(humidity, 1);
    display.print("%");

    if (numLeft > -1) {                           // If a PushSafer njotification has been requested, PushSafer  
      display.setCursor(32, 40);                  // will respond with the number of notifications that remain         
      display.print("(");                         // before the account needs topping up. 
      display.print(numLeft);                     // Display at bottom of screen.
      display.print(")");
      numLeft = -1;                 // Only display number of notifications left until next sensor reading.
    } 

     display.display();
     

    // Send data to the server.
    
    if ((plotCounter >= PLOT_MAX) && (WiFi.status() == WL_CONNECTED)  && (humidity < 100)  )  { 

      plotCounter = 0;

      WiFiClient client;
      HTTPClient http;

     rssi = WiFi.RSSI();
  
    // Specify request destination, including your GET variables - Send values to web server
     String http_request = "";

     http_request = "http://" + serverIP  + "/apage?";
     http_request += "id=" + id;
     http_request += "&leftaxis=" + String(co2);
     http_request += "&rightaxis=" +  String(humidity);

     http_request += "&rssi=" + String(rssi);  
    
     http.begin(client, http_request);  

    // Send the request
     int httpCode = http.GET();

    // Check the returning HTTP code
     if (httpCode > 0) {
      // Get a response back from the server
        String payload = http.getString();
      // Print the response
        Serial.println(payload);
     }

    // Close the HTTP connection
      http.end(); 
    }


    if ((digitalRead(34) == LOW) &&  (co2 > CO2_HIGH)  && 
        (notification_sent == false) && (WiFi.status() == WL_CONNECTED) ) {

      sendNotification("HIGH CO2 LEVEL: " + String(co2) + " (ppm)" );
    }

   }
   else
     Serial.print(F("."));  // Data not available

   delay(1000);

}


void sendNotification(String message) {                     // Send request to PushSafer to push notification to your phone.
   WiFiClient client;
   HTTPClient http;

// Clean up strings for pushSafer http request

   title = urlEncode(title);
   message = urlEncode(message);

  
   pushSaferPath = pushSaferURL +                          // String to send to PushSafer server.
                       "?d=" + deviceID + 
                       "&i=" + icon + 
                       "&t=" + title + 
                       "&m=" + message + 
                       "&k=" + pushSaferKey;
                       
   Serial.println("  ");
   Serial.println(pushSaferPath);                           // Check PushSafer URL on Serial Monitor for debugging.

   
      
   Serial.println("Making HTTP request to ...");
   Serial.println(pushSaferPath);
 
   http.begin(client, pushSaferPath.c_str());   

   int httpCode = http.GET();                               // Send the request
   
     if (httpCode > 0) {                                    // Check the returning HTTP code
                      
       String payload = http.getString();                   // Get the response back from the server
       
       Serial.println("HTTP Response: ");                   // Print the response on Serial Monitor (for debugging)
       Serial.println(payload);
      
       JSONVar doc = JSON.parse(payload);                   // Parse the JSON data into JSONVar 'doc'

       int result = doc["status"];                          // Get 'status' from JSON doc. (1 = success)
    
       notification_sent = (result == 1);                   // Set notification_sent flag if valid response from PushSafer

       if (notification_sent) {                             // Look for number of remaining notifications available
        
         digitalWrite(4, HIGH);                             // Turn on backlight
         numLeft = doc["available"];                              

         Serial.print("Available: ");
         Serial.println(numLeft);
      } 
   } else {
     notification_sent = false;                             // No response received from PushSafer.
   }

   http.end();                                              // Close the HTTP connection  
   delay(1000); 
} 

void test_alarms() {

    Serial.println("Send test notification");

    digitalWrite(26, HIGH);                // Sound buzzer for half a second (if switch is On).
    delay(500);
    digitalWrite(26, LOW);    
    
    if( (digitalRead(34) == LOW)  && (WiFi.status() == WL_CONNECTED) ){      // if Notifications are enabled...
      sendNotification("Test");
    }
}    

 

Back to Index

 


This site and its contents are © Copyright 2005 - All Rights Reserved.