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.