Receive CO2 data wirelessly from ESP8266 D1 mini with SGP30 and display on 3.5" ILI9488

The SGP30 CO2 module outputs 4 kinds of data: CO2, VOC, H2 and ethanol. These are sent wirelessly from a ESP8266 through ESP-now to a ESP32 C3 driving a 3.5" 480*320 ILI9488 display. ( e.g here)

In my last project I used a delay() function to redraw the screen, which works well for a lot of applications. In this project I use a blink without delay function to redraw the screen to allow the microcontroller to do other functions if necessary. I use three such functions to update the CO2 display, draw the columns of the graph and make the averages, and redraw the graph space every 12.5 hours.

The main data I'm concerned with is the CO2 measurement. Unfortunately with this module the CO2 reading is greatly influenced by the other quantities it is sensitive to. There may be a tricky way to use these other three readings to adjust for this. For comparison my SCD40 CO2 module only measures CO2, temp and humidity, and is far more resilient in resisting the influence of these other gases, such as methane and ethanol.

11 minute columns show the average of CO2 for that time along with minimum and maximum CO2 for that 11 minutes. Takes 12.5 hours to fill the screen from left to right and then it redraws and resets the CO2 min max in the top right.

For ESP32 C3 and ILI9488 3.5" 480*320 I've used Bodmer library setup 70c with ILI9488 uncommented.

#define TFT_CS 7
#define TFT_MOSI 6
#define TFT_MISO 5
#define TFT_SCLK 4
#define TFT_DC 8
#define TFT_RST 10

/*
ESP32 C3   ILI9488 3.5" 480*320
#define TFT_CS    7
#define TFT_MOSI  6
#define TFT_MISO  5
#define TFT_SCLK  4
#define TFT_DC    8
#define TFT_RST   10

 for ESP32 C3 use Bodmer library setup 70c with ILI9488 uncommented
*/

#include <esp_now.h>
#include <WiFi.h>
#include <TFT_eSPI.h>
#include <SPI.h>

#define day  86400000 //  milliseconds in a day
#define hour  3600000 //  milliseconds in an hour
#define minute  60000 //  milliseconds in a minute
#define second   1000 //  milliseconds in a second

unsigned long previousMillis = 0;  // will store last time LED was updated
unsigned long previousMillis2 = 0;
int height = 0;
int xpos  = 0;
int ypos  = 0;
unsigned long co2inst = 0;
unsigned long co2av;
int co2count = 0;

const long interval = 10000; // constants won't change:

unsigned long lastday ;
unsigned long timenow;

unsigned long maxco2 = 0;
unsigned long minco2 = 9999;

unsigned long Adat = 0;
unsigned long Bdat = 0;
unsigned long co2 = 0;
unsigned long Ddat = 0;

unsigned long minco2short = 0;
unsigned long maxco2short = 9999;

TFT_eSPI tft = TFT_eSPI();   

// Structure example to receive data
// Must match the sender structure
typedef struct struct_message {
    unsigned long a;
    unsigned long b;
    unsigned long c;
    unsigned long d;
} struct_message;

// Create a struct_message called myData
struct_message myData;

// callback function that will be executed when data is received
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  memcpy(&myData, incomingData, sizeof(myData));

}
 
void setup() {
  // Initialize Serial Monitor
 // Serial.begin(115200);
  
  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);

  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }
  
  // Once ESPNow is successfully Init, we will register for recv CB to
  // get recv packer info
  esp_now_register_recv_cb(OnDataRecv);

  tft.begin();

tft.setRotation(1);
     tft.fillScreen(0x1000); // Clear screen to navy background

  tft.print(WiFi.macAddress());
    tft.setTextColor(TFT_BLUE);
        tft.setTextSize (3);
       tft.setCursor (60,19); 
         tft.print("Starts in "); 
        tft.print(interval/1000);  
          tft.print(" seconds"); 
delay(interval);
  tft.fillRect(0,0,480,80,TFT_BLACK);
}
 
void loop() {
  unsigned long currentMillis = millis();

// Update the displayed CO2 value and collect data for the averages

  if (currentMillis - previousMillis >= interval) {

Adat = myData.a;
Bdat = myData.b;
co2 = myData.c;
Ddat = myData.d;

     if (co2 >= maxco2) {
    maxco2=co2; }

  if (co2 <= minco2) {
    minco2=co2; 
 }
 
  if (co2 >= minco2short) {minco2short = co2;}
if (co2 <= maxco2short) {maxco2short = co2;}

co2inst = co2inst + co2;
co2count = co2count + 1;
 
    tft.setTextWrap(false);

    tft.fillRect(0,300,480,20,TFT_BLACK);

    tft.drawLine(0,80,480,80,0xA01F);
     tft.drawLine(0,280,480,280,0xA01F);   
    tft.drawLine(0,180,480,180,0xA01F);
    tft.setTextSize (1);
           tft.setTextColor(TFT_YELLOW,0xA01F); 
       tft.setCursor (450,65);    
  tft.print("1000");
        tft.setCursor (455,165);    
  tft.print("500");
  tft.fillRect(125,2,78,55,TFT_BLACK);
    tft.fillRect(462,24,17,24,TFT_BLACK);
       tft.setCursor (0,2); 
           tft.setTextSize (8);   
  tft.setTextColor(TFT_BLUE,0x1802);
  int co2round = round(co2);
 tft.print(co2round);

       tft.setCursor (196,36); 
           tft.setTextSize (3);   
  tft.print("ppm"); 
 
  tft.setTextColor(TFT_WHITE);
    tft.setCursor (300,0); 
    tft.setTextSize (1);  
    tft.print("Max CO2");

  tft.setTextColor(TFT_WHITE);
    tft.setCursor (420,0); 
    tft.setTextSize (1);  
    tft.print("Min CO2");

  tft.setTextColor(TFT_BLUE,0x1802);
    tft.setCursor (290,25); 
    tft.setTextSize (3);
    int maxco2round = round(maxco2);  
    tft.print(maxco2round);

    tft.setCursor (410,25); 
       int minco2round = round(minco2);  
    tft.print(minco2round);

     tft.setTextSize (2);
         tft.setCursor (8,300);
         tft.setTextColor(TFT_YELLOW);      
  tft.print(Adat);  tft.print(" H2  ");
  tft.print(Bdat);  tft.print(" VOC  ");
   tft.print(Ddat);  tft.print(" eth  ");  tft.print(currentMillis/1000);

     previousMillis = currentMillis;
 } 

// Draw columns every 11 minutes. Adjust this time to alter e.g. second*20, day/5

        if (currentMillis - previousMillis2 >= (minute*11)) {
    // save the last time you blinked the LED
    previousMillis2 = currentMillis;

  co2av = round(co2inst/co2count);

 height = round(co2av/5);  // scale bars higher or lower

ypos = 80+(200 - height);  // make the bars go from bottom to top
 
  tft.drawRect(xpos,ypos,7,height,TFT_CYAN);  //  can change 7 to something else to change column width

    tft.drawLine(0,80,480,80,0xA01F);
     tft.drawLine(0,280,480,280,0xA01F);   
    tft.drawLine(0,180,480,180,0xA01F);
    tft.setTextSize (1);
           tft.setTextColor(TFT_YELLOW); 
       tft.setCursor (450,65);    
  tft.print("1000");
        tft.setCursor (455,165);    
  tft.print("500");

int minline = round(minco2short/5);
  int maxline = round(maxco2short/5);

   tft.drawLine(xpos,280-minline,xpos + 7,280-minline,TFT_GREEN); 
    tft.drawLine(xpos,280-maxline,xpos + 7,280-maxline,TFT_RED);

xpos = xpos + 7;
co2count = 0;
co2inst = 0;
  minco2short = 0;
 maxco2short = 9999;
 }

 // Redraw the graph area after it fills, about 12.5 hours. Reset min/max

 if (xpos >= 480) {
  tft.fillRect(0,80,480,210,0x1000);

      tft.drawLine(0,80,480,80,0xA01F);
     tft.drawLine(0,280,480,280,0xA01F);   
    tft.drawLine(0,180,480,180,0xA01F);
    tft.setTextSize (1);
           tft.setTextColor(TFT_YELLOW); 
       tft.setCursor (450,65);    
  tft.print("1000");
        tft.setCursor (455,165);    
  tft.print("500");

     maxco2=0;
     minco2=9999;
     xpos = 0;
  }
}

ESP32nowReceiver3.5C3Graph.ino (5.8 KB)

One glitch I had was the CO2 min kept reading zero, when it shouldn't go lower than 400ppm, but I haven't seen that in the latest sketches.

Sender is D1 mini ESP8266 with 0.66 OLED shield display. Attached is the SGP30 with i2c bus in the following way:

SGP30 --> ESP8266 D1 mini

VCC --> 3.3V
GND --> GND
SCL --> 5 SCL (labelled D1 on mine)
SDA --> 4 SDA (labelled D2 on mine)

Library: GitHub - adafruit/Adafruit_SGP30: Arduino library for SGP30

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp-now-esp8266-nodemcu-arduino-ide/
  
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.

  SGP30 -->  ESP8266 D1 mini
VCC -->  3.3V
GND --> GND
SCL -->  5  SCL  (labelled D1 on mine)
SDA -->  4  SDA  (labelled D2 on mine)
*/

#include <ESP8266WiFi.h>
#include <espnow.h>
#include <ESP_SSD1306.h>    // Modification of Adafruit_SSD1306 for ESP8266 compatibility
#include <Adafruit_GFX.h>   // Needs a little change in original Adafruit library (See README.txt file)
#include <SPI.h>            // For SPI comm (needed for not getting compile error)
#include <Wire.h>           // For I2C comm, but needed for not getting compile error
#include "Adafruit_SGP30.h"

Adafruit_SGP30 sgp;

unsigned long time2 = 0;

uint32_t getAbsoluteHumidity(float temperature, float humidity) {
    // approximation formula from Sensirion SGP30 Driver Integration chapter 3.15
    const float absoluteHumidity = 216.7f * ((humidity / 100.0f) * 6.112f * exp((17.62f * temperature) / (243.12f + temperature)) / (273.15f + temperature)); // [g/m^3]
    const uint32_t absoluteHumidityScaled = static_cast<uint32_t>(1000.0f * absoluteHumidity); // [mg/m^3]
    return absoluteHumidityScaled;
}

// REPLACE WITH RECEIVER MAC Address
uint8_t broadcastAddress[] = {0x80, 0x65, 0x99, 0x6A, 0xF8, 0x40};
// uint8_t broadcastAddress[] = {0xE8, 0x6B, 0xEA, 0xF6, 0x95, 0x64};

// Structure example to send data
// Must match the receiver structure
typedef struct struct_message {
  unsigned long a;
  unsigned long b;
  unsigned long c;
  unsigned long d;
} struct_message;

// Create a struct_message called myData
struct_message myData;

unsigned long lastTime = 0;  
unsigned long timerDelay = 2000;  // send readings timer

// Callback when data is sent
void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus) {
  Serial.print("Last Packet Send Status: ");
  if (sendStatus == 0){
    Serial.println("Delivery success");
  }
  else{
    Serial.println("Delivery fail");
  }
}
 
// Pin definitions
#define OLED_RESET  16  // Pin 15 -RESET digital signal

ESP_SSD1306 display(OLED_RESET); // FOR I2C


void setup() {
  // Init Serial Monitor
  Serial.begin(115200);
   while (!Serial) { delay(10); } // Wait for serial console to open!

  Serial.println("SGP30 test");

  if (! sgp.begin()){
    Serial.println("Sensor not found :(");
    while (1);
  }
  Serial.print("Found SGP30 serial #");
  Serial.print(sgp.serialnumber[0], HEX);
  Serial.print(sgp.serialnumber[1], HEX);
  Serial.println(sgp.serialnumber[2], HEX);

  // If you have a baseline measurement from before you can assign it to start, to 'self-calibrate'
  //sgp.setIAQBaseline(0x8E68, 0x8F41);  // Will vary for each sensor!

  display.begin(SSD1306_SWITCHCAPVCC);  // Switch OLED

  // Show image buffer on the display hardware.
  // Since the buffer is intialized with an Adafruit splashscreen
  // internally, this will display the splashscreen.
   display.clearDisplay();
  display.setRotation(2);

      display.setCursor(33,00);
       display.setTextColor(WHITE);
          display.print(" ppm");
  display.display();
  delay(1000);
  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);

  // Init ESP-NOW
  if (esp_now_init() != 0) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Once ESPNow is successfully Init, we will register for Send CB to
  // get the status of Trasnmitted packet
  esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
  esp_now_register_send_cb(OnDataSent);
  
  // Register peer
  esp_now_add_peer(broadcastAddress, ESP_NOW_ROLE_SLAVE, 1, NULL, 0);
}
 
int counter = 0;
void loop() {
  
   display.clearDisplay();
  // If you have a temperature / humidity sensor, you can set the absolute humidity to enable the humditiy compensation for the air quality signals
  //float temperature = 22.1; // [°C]
  //float humidity = 45.2; // [%RH]
  //sgp.setHumidity(getAbsoluteHumidity(temperature, humidity));

  if (! sgp.IAQmeasure()) {
    Serial.println("Measurement failed");
    return;
  }
  Serial.print("TVOC "); Serial.print(sgp.TVOC); Serial.print(" ppb\t");
  Serial.print("eCO2 "); Serial.print(sgp.eCO2); Serial.println(" ppm");
  Serial.println(time2);

      display.setCursor(33,00);
       display.setTextColor(WHITE);
         display.setTextSize(2);
         display.print(sgp.eCO2); display.print(" ppm");
         display.setTextSize(1);
        display.setCursor(33,20);
              display.print(sgp.TVOC); display.print(" ppb");
   
       display.setCursor(33,30);
             display.print(sgp.rawH2); display.print(" H2"); 

      display.setCursor(33,40);
               display.print(sgp.rawEthanol); display.print(" eth");  

  if (! sgp.IAQmeasureRaw()) {
    Serial.println("Raw Measurement failed");
    return;
  }
 Serial.println("");
  Serial.print("Raw H2 "); Serial.print(sgp.rawH2); Serial.print(" \t");
  Serial.print("Raw Ethanol "); Serial.print(sgp.rawEthanol); Serial.println("");

 // 
 // display.print(sgp.rawH2);
 // display.print(" H2");

//    display.setCursor(33,20);
 // display.print(sgp.rawEthanol);
 // display.print(" eth");
/*

     display.setTextSize(1);
      display.setCursor(33,40);
  display.print(time2);
  display.print(" time");
 */
  display.display();

  delay(1000);

  counter++;
  if (counter == 30) {
    counter = 0;

    uint16_t TVOC_base, eCO2_base;
    if (! sgp.getIAQBaseline(&eCO2_base, &TVOC_base)) {
      Serial.println("Failed to get baseline readings");
      return;
    }
    Serial.print("****Baseline values: eCO2: 0x"); Serial.print(eCO2_base, HEX);
    Serial.print(" & TVOC: 0x"); Serial.println(TVOC_base, HEX);
  }

 time2 = millis()/1000;
  if ((millis() - lastTime) > timerDelay) {
    // Set values to send
    myData.a = sgp.rawH2;
    myData.b = sgp.TVOC;
    myData.c = sgp.eCO2;
    myData.d = sgp.rawEthanol;
//    myData.e = time2;

    // Send message via ESP-NOW
    esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));

    lastTime = millis();
  }
}

ESP8266nowSender_CO2.ino (6.1 KB)

Sometimes I have to hit reset to get it to display after I've uploaded it.

Delighted with how this turned out. It looks so sciencey with the minimum and maximum bars. I tested it overnight and it did reset as I had hoped once I added the xpos = 0 line which I updated in the above post.

I moved the sensor to the lounge room and these were the overnight readings. You can see toward the right where I got up and opened the windows, the CO2 spikes a little and then down to the minimum of 400ppm for two 11 minute bars.

I realised you can retain more data simply by clearing a rectangle near each new graph column instead of clearing the whole graph space. And I moved the numbers down for a neater appearance.

Updated ino:

ESP32nowReceiver3.5C3Graph.ino (5.8 KB)

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.