I contacted vcoder, above. He was very helpful in a solution. The key is to find a way to calibrate or normalize the results. Without normalizing the values are in the “death”, very dangerous range. I am still having trouble normalizing the VOC values, need to find time to do that. I will post the code I am using, with vcoder’s help. It would be nice if SEEED would provide a recommended calibration or normalization algorithm. Hope this helps you.
/* Multichannel sensor Grove V2, by vcoder 2021
- vcoder email contact: [email protected]
CO range 0 - 1000 PPM
Calibrated according calibration curve by Winsen: 0 - 150 PPM
RS/R0 PPM
1 0
0.77 1
0.6 3
0.53 5
0.4 10
0.29 20
0.21 50
0.17 100
0.15 150
NO2 range 0 - 10 PPM
Calibrated according calibration curve by Winsen: 0 - 10 PPM
RS/R0 PPM
1 0
1.4 1
1.8 2
2.25 3
2.7 4
3.1 5
3.4 6
3.8 7
4.2 8
4.4 9
4.7 10
/
//https://github.com/Seeed-Studio/Seeed_Multichannel_Gas_Sensor
/
*/
#include <TFT_eSPI.h>
#include <Multichannel_Gas_GMXXX.h>
#include “Air_Quality_Sensor.h” //#include"AirQuality.h"
AirQualitySensor sensor(A2); //Attached to Wio Battery expansion (A2D2/A3D3)
#include <Wire.h>
#include “DHT.h”
#include <SPI.h>
#define BUZZER_PIN WIO_BUZZER
GAS_GMXXX gas;
TFT_eSPI tft;
TFT_eSprite spr = TFT_eSprite(&tft); //sprite
unsigned int no2, c2h5ch, voc, co, aqv;
int temp, humid;
int iBeeps;
// Veslin recommendation
static uint8_t recv_cmd[8] = {};
int PPM = 0;
int CO_PPM; //GM_702B CO
int NO2_PPM; //GM_102B NO2
int Eth_PPM; //GM_302B Ethyl
int VOC_PPM; //GM_502B VOC
double lgPPM;
boolean bDisplay = false;
boolean bPrevButtonPress = false;
boolean bAlerts = false; //Start NO alerts
boolean bWio_PressKeyA = false;
#define DHTPIN 0
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
int iX = 5;
int iY = 60;
int iWidth = tft.width();
int iHeight = tft.height();
int iHlfw = tft.width() /2;
int iHlfh = tft.height() /2;
int iQw = iHlfw /2;
int iQh = iHlfh /2;
String sAQ = “__”;
//Veslin recommend
uint8_t len = 0;
uint8_t addr = 0;
uint8_t i;
uint32_t val = 0;
float sensor_volt;
float CO_Volt;
float NO2_Volt;
float Eth_Volt;
float VOC_Volt;
float RS_gas;
float R0;
float sensorValue = 0;
float COvalue;
float NO2value;
float EthValue;
float VOCValue;
float ratio;
void setup() {
Wire.begin();
Serial.begin(115200);
tft.begin();
tft.setRotation(3);
tft.fillScreen(TFT_BLACK);
gas.begin(Wire, 0x08);
dht.begin();
pinMode(WIO_KEY_A, INPUT_PULLUP);
pinMode(WIO_KEY_B, INPUT_PULLUP);
pinMode(WIO_KEY_C, INPUT_PULLUP);
pinMode(WIO_5S_UP, INPUT_PULLUP);
pinMode(WIO_5S_DOWN, INPUT_PULLUP);
pinMode(WIO_5S_LEFT, INPUT_PULLUP);
pinMode(WIO_5S_RIGHT, INPUT_PULLUP);
pinMode(WIO_5S_PRESS, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
//displayTemplate();
//Air Quality Sensor REsiding on Pin A2 battery expansion
if (sensor.init()) {
Serial.println(“Sensor ready.”);
} else {
Serial.println(“Sensor ERROR!”);
}
}
void loop() {
CO_PPM = getCO_GasValue(); //GM_702B CO = Call getGasValue function
NO2_PPM = getNO2_GasValue(); //GM_102B NO2 = Call getGasValue function
Eth_PPM = getEth_GasValue(); //GM_302B Ethyl - Call getGasValue function
VOC_PPM = getVOC_GasValue(); //GM_502B VOC - Call getGasValue function
int quality = sensor.slope();
int sValue = sensor.getValue();
aqv = sValue;
//AQ voltage:
//current_V - last_V > 400 OR current_V > 700 : AirQualitySensor::FORCE_SIGNAL
//current_V - last_V > 400 AND current_V < 700 OR current_V - std_V > 150 : AirQualitySensor::HIGH_POLLUTION
//current_V - last_V > 200 AND current_V < 700 OR current_V - std_V > 50 : AirQualitySensor::LOW_POLLUTION
//ELSE : AirQualitySensor::FRESH_AIR
if (quality == AirQualitySensor::FORCE_SIGNAL) {
Serial.println(“High pollution! Force signal active.”);
sAQ = “FS!”;
iBeeps = 6;
} else if (quality == AirQualitySensor::HIGH_POLLUTION) {
Serial.println(“High pollution!”);
sAQ = “Hpol”;
iBeeps = 4;
} else if (quality == AirQualitySensor::LOW_POLLUTION) {
Serial.println(“Low pollution!”);
sAQ = “Lpol”;
iBeeps = 2;
} else if (quality == AirQualitySensor::FRESH_AIR) {
Serial.println(“Fresh air.”);
sAQ = “Frsh”;
iBeeps = 0;
}
// if (digitalRead(WIO_KEY_A) == LOW) {
// playTone(1014, 500);
// }
// VOC
voc = gas.getGM502B();
if (voc > 999) voc = 999;
Serial.print(“VOC: “);
Serial.print(voc);
Serial.println(” ppm”);
//CO
co = gas.getGM702B();
if (co > 999) co = 999;
Serial.print(“CO: “);
Serial.print(co);
Serial.println(” ppm”);
//Temp
float t = dht.readTemperature();
temp = t*1.8 + 32;
//int tem = round(t);
//int tem = t;
Serial.print("Temperature: ");
Serial.print(temp);
Serial.println( “*F”);
//NO2
no2 = gas.getGM102B();
if (no2 > 999) no2 = 999;
Serial.print(“NO2: “);
Serial.print(no2);
Serial.println(” ppm”);
//Humidity
float h = dht.readHumidity();
if (h > 99) h = 99;
humid = h;
Serial.print("Humidity: ");
Serial.print(h);
Serial.println( “%”);
//C2H5CH
c2h5ch = gas.getGM302B();
if (c2h5ch > 999) c2h5ch = 999;
Serial.print(“C2H5CH: “);
Serial.print(c2h5ch);
Serial.println(” ppm”);
// ****************************************
//Button state:
//WIO_5S_PRESS NOT pressed (false) bPrevButtonPress (false) (Do NOT Display GAS data)
//Button just pressed WIO_5S_PRESS is True bPtrue (false)
//Button just pressed WIO_5S_PRESS is True bPtrue {true)
//pinMode(WIO_KEY_A, INPUT_PULLUP);
// boolean bWio_PressKeyA = false;
if ( (digitalRead(WIO_KEY_A) == LOW) && !(bWio_PressKeyA) ) {
bAlerts = true; //Turn ON Alerts
bWio_PressKeyA = true;
playTone(956, 500);
}
if ((digitalRead(WIO_KEY_A) == LOW) && (bWio_PressKeyA)){ //Turn OFF Alerts
bAlerts = false; //Turn OFF Alerts
bWio_PressKeyA = false;
playTone(956, 500);
}
if ( (digitalRead(WIO_5S_PRESS) == LOW) && !(bPrevButtonPress) ) {
bDisplay = true;
bPrevButtonPress = true;
playTone(956, 500);
}
if ((digitalRead(WIO_5S_PRESS) == LOW) && (bPrevButtonPress)){ //Turn OFF Display
bDisplay = false;
bPrevButtonPress = false;
playTone(1700, 500);
}
if (bDisplay) {
displayTemplate();
displayGasData();
}
else {
tft.fillScreen(TFT_BLACK);
}
delay(5000);
}
void displayTemplate() {
//Head
//int iX = 5;
//int iY = 60;
//int iWidth = tft.width();
//int iHeight = tft.height();
//int iHlfw = tft.width() /2;
//int iHlfh = tft.height() /2;
//int iQw = iHlfw /2;
//int iQh = iHlfh /2;
//(3rd rectangle) midpoint is iHlfw
//(4th rectangle) iHlfw +iQw
tft.fillScreen(TFT_BLACK);
tft.setFreeFont(&FreeSansBoldOblique18pt7b);
tft.setTextColor(TFT_WHITE);
tft.drawString(“Air Quality”, 70, 10 , 1);
//Line
for (int8_t line_index = 0; line_index < 5 ; line_index++)
{
tft.drawLine(0, 50 + line_index, tft.width(), 50 + line_index, TFT_GREEN);
}
//New VOC
//tft.drawRoundRect(5 , 60, (tft.width() / 2) / 2 , (tft.height() - 65) /2 , 10, TFT_WHITE); // L1
tft.drawRoundRect(iX, iY, (iHlfw - iQw) + 10, iQh +5, 10, TFT_WHITE);
// AQv
//tft.drawRoundRect((tft.width() / 2) / 2, 60, ((tft.width() / 2) / 2) + (tft.width() / 2) / 2 , (tft.height() - 65) /2 , 10, TFT_WHITE); //
tft.drawRoundRect((iHlfw - iQw) + 17, iY, iHlfw -50, iQh +5, 10, TFT_WHITE);
//New CO
tft.drawRoundRect(iX, iHlfh -10, (iHlfw - iQw) + 10, iHlfh -iQh +3, 10, TFT_WHITE); // s3
//Empty New
tft.drawRoundRect((iHlfw - iQw) + 17, iHlfh -10, iHlfw -50, iHlfh -iQh +3, 10, TFT_WHITE); // s3
//VOC Text
tft.setFreeFont(&FreeSansBoldOblique9pt7b);
tft.setTextColor(TFT_RED);
tft.drawString(“VOC”, 7 , 65 , 1);
tft.setTextColor(TFT_GREEN);
tft.drawString(“ppm”, 30, 108, 1);
//AQv Text
tft.setFreeFont(&FreeSansBoldOblique9pt7b);
tft.setTextColor(TFT_RED);
tft.drawString(“AQs”, 100 , 65 , 1);
tft.setTextColor(TFT_GREEN);
tft.drawString(sAQ, 100, 111, 1); //Pollution level!
tft.setTextColor(TFT_BLUE);
tft.drawString(String(iBeeps), 85, 120, 1); //beeps
if (bAlerts) {
tft.setTextColor(TFT_RED);
tft.drawString(String(iBeeps), 85, 120, 1); //beeps
for (int ii = 0; ii < iBeeps; ii++) {
playTone(1900, 25); //(tone, duration)
delay(40);
}
}
//CO Text
tft.setFreeFont(&FreeSansBoldOblique9pt7b);
tft.setTextColor(TFT_RED);
tft.drawString(“CO”, 7 , 150 , 1);
tft.setTextColor(TFT_GREEN);
tft.drawString(“ppm”, 30, 193, 1);
// Temp rect
tft.drawRoundRect((tft.width() / 2) - 10 , 60, (tft.width() / 2) / 2 , (tft.height() - 65) / 2 , 10, TFT_BLUE); // s1
tft.setFreeFont(&FreeSansBoldOblique9pt7b);
tft.setTextColor(TFT_RED) ;
tft.drawString(“Temp”, (tft.width() / 2) - 1 , 70 , 1); // Print the test text in the custom font
tft.setTextColor(TFT_GREEN);
tft.drawString(“o”, (tft.width() / 2) + 30, 95, 1);
tft.drawString(“F”, (tft.width() / 2) + 40, 100, 1);
//No2 rect
tft.drawRoundRect(((tft.width() / 2) + (tft.width() / 2) / 2) - 5 , 60, (tft.width() / 2) / 2 , (tft.height() - 65) / 2 , 10, TFT_BLUE); // s2
tft.setFreeFont(&FreeSansBoldOblique9pt7b);
tft.setTextColor(TFT_RED);
tft.drawString(“NO2”, ((tft.width() / 2) + (tft.width() / 2) / 2) , 70 , 1); // Print the test text in the custom font
tft.setTextColor(TFT_GREEN);
tft.drawString(“ppm”, ((tft.width() / 2) + (tft.width() / 2) / 2) + 30 , 120, 1);
//Humi Rect
tft.drawRoundRect((tft.width() / 2) - 10 , (tft.height() / 2) + 30, (tft.width() / 2) / 2 , (tft.height() - 65) / 2 , 10, TFT_BLUE); // s3
tft.setFreeFont(&FreeSansBoldOblique9pt7b);
tft.setTextColor(TFT_RED) ;
tft.drawString(“Humi”, (tft.width() / 2) - 1 , (tft.height() / 2) + 40 , 1); // Print the test text in the custom font
tft.setTextColor(TFT_GREEN);
tft.drawString("%", (tft.width() / 2) + 30, (tft.height() / 2) + 70, 1);
//c2h5ch Rect
tft.drawRoundRect(((tft.width() / 2) + (tft.width() / 2) / 2) - 5 , (tft.height() / 2) + 30, (tft.width() / 2) / 2 , (tft.height() - 65) / 2 , 10, TFT_BLUE); // s4
tft.setFreeFont(&FreeSansBoldOblique9pt7b);
tft.setTextColor(TFT_RED) ;
tft.drawString(“Ethyl”, ((tft.width() / 2) + (tft.width() / 2) / 2) , (tft.height() / 2) + 40 , 1); // Print the test text in the custom font
tft.setTextColor(TFT_GREEN);
tft.drawString(“ppm”, ((tft.width() / 2) + (tft.width() / 2) / 2) + 30 , (tft.height() / 2) + 90, 1);
}
void displayGasData() {
// VOC : VOC_PPM = getVOC_GasValue(); //GM_502B VOC
spr.createSprite(40, 30);
spr.fillSprite(TFT_BLACK);
spr.setFreeFont(&FreeSansBoldOblique12pt7b);
spr.setTextColor(TFT_WHITE);
spr.drawNumber(VOC_PPM, 0, 0, 1); //was voc
spr.pushSprite(15, 85); //was: 15, 100
spr.deleteSprite();
//CO : CO_PPM = getCO_GasValue(); //GM_702B CO
spr.createSprite(40, 30);
spr.setFreeFont(&FreeSansBoldOblique12pt7b);
spr.setTextColor(TFT_WHITE);
spr.drawNumber(CO_PPM, 0, 0, 1); //Was drawNumber(co, 0, 0, 1);
spr.setTextColor(TFT_GREEN);
spr.pushSprite(15, 170); //Was 15, 185
spr.deleteSprite();
//Air Quality Sensor (aqv)
spr.createSprite(40, 30);
spr.setFreeFont(&FreeSansBoldOblique12pt7b);
spr.setTextColor(TFT_WHITE);
spr.drawNumber(aqv, 0, 0, 1);
//spr.setTextColor(TFT_GREEN);
spr.pushSprite( ((tft.width() / 2)+12) / 2, 85); //
spr.deleteSprite();
//iHeight iWidth
//spr.createSprite(40, 30);
//spr.setFreeFont(&FreeSansBoldOblique12pt7b);
//spr.setTextColor(TFT_WHITE);
//spr.drawNumber(tft.height(), 0, 0, 1);
//spr.setTextColor(TFT_GREEN);
//spr.pushSprite( ((tft.width() / 2)+10) / 2, 110); //
//spr.deleteSprite();
//Temp
spr.createSprite(30, 30);
spr.setFreeFont(&FreeSansBoldOblique12pt7b);
spr.setTextColor(TFT_WHITE);
spr.drawNumber(temp, 0, 0, 1);
spr.setTextColor(TFT_GREEN);
spr.pushSprite((tft.width() / 2) - 1, 100);
spr.deleteSprite();
//NO2 : NO2_PPM = getNO2_GasValue(); //GM_102B NO2
spr.createSprite(45, 30);
spr.setFreeFont(&FreeSansBoldOblique12pt7b);
spr.setTextColor(TFT_WHITE);
spr.drawNumber(NO2_PPM, 0, 0, 1);
spr.pushSprite(((tft.width() / 2) + (tft.width() / 2) / 2), 97);
spr.deleteSprite();
//Humidity
spr.createSprite(30, 30);
spr.setFreeFont(&FreeSansBoldOblique12pt7b);
spr.setTextColor(TFT_WHITE);
spr.drawNumber(humid, 0, 0, 1);
spr.pushSprite((tft.width() / 2) - 1, (tft.height() / 2) + 67);
spr.deleteSprite();
//C2H5CH : Eth_PPM = getEth_GasValue(); //GM_302B Ethyl
spr.createSprite(45, 30);
spr.setFreeFont(&FreeSansBoldOblique12pt7b);
spr.setTextColor(TFT_WHITE);
spr.drawNumber(Eth_PPM, 0 , 0, 1); //was c2h5ch
spr.pushSprite(((tft.width() / 2) + (tft.width() / 2) / 2), (tft.height() / 2) + 67);
spr.deleteSprite();
}
void playTone(int tone, int duration) {
for (long i = 0; i < duration * 1000L; i += tone * 2) {
digitalWrite(BUZZER_PIN, HIGH);
delayMicroseconds(tone);
digitalWrite(BUZZER_PIN, LOW);
delayMicroseconds(tone);
}
} //End PlayTone
/*
*
Gas Measurements
val = gas.measure_NO2(); //GM_102B NO2
val = gas.measure_C2H5OH(); //GM_302B Ethyl
val = gas.measure_VOC(); //GM_502B VOC
val = gas.measure_CO(); //GM_702B CO
*
*/
int getEth_GasValue()
{
//Average
for(int x = 0 ; x < 100 ; x++)
{
EthValue = gas.measure_C2H5OH(); //GM_302B Ethyl
}
Eth_Volt = (EthValue/1024)*3.3;
RS_gas = (3.3-Eth_Volt)/Eth_Volt;
R0 = 1.0; //measured on ambient air
ratio = RS_gas/R0;
//ratio = 4.7; //for tests of the calibration curve
val = gas.measure_C2H5OH(); Serial.print(“Ethyl: “); Serial.print(val); Serial.print(” eq “);
Serial.print(gas.calcVol(val)); Serial.println(” V”);
lgPPM = (log10(ratio) * + 1.9) - 0.2; //+2 -0.3
PPM = pow(10,lgPPM);
return PPM;
}
int getNO2_GasValue()
{
//Average
for(int x = 0 ; x < 100 ; x++)
{
NO2value = gas.measure_NO2();
}
NO2_Volt = (NO2value/1024)*3.3;
RS_gas = (3.3-NO2_Volt)/NO2_Volt;
R0 = 1.07; //measured on ambient air
ratio = RS_gas/R0;
//ratio = 4.7; //for tests of the calibration curve
val = gas.measure_NO2(); Serial.print(“NO2: “); Serial.print(val); Serial.print(” eq “);
Serial.print(gas.calcVol(val)); Serial.println(” V”);
lgPPM = (log10(ratio) * + 1.9) - 0.2; //+2 -0.3
PPM = pow(10,lgPPM);
return PPM;
}
int getCO_GasValue()
{
//Average
for(int x = 0 ; x < 100 ; x++)
{
COvalue = gas.measure_CO();
}
CO_Volt = (COvalue/1024)*3.3;
RS_gas = (3.3-CO_Volt)/CO_Volt;
R0 = 3.21; //measured on ambient air
ratio = RS_gas/R0;
// ratio = 1; //it is for tests of the calibration curve
// The following “val” is for comparison, I believe
val = gas.measure_CO(); Serial.print(“CO: “); Serial.print(val); Serial.print(” eq “);
Serial.print(gas.calcVol(val)); Serial.println(” V”);
lgPPM = (log10(ratio) * - 2.82) - 0.12; //- 3.82) - 0.66; - default - 2.82) - 0.12; - best for range up to 150 ppm
PPM = pow(10,lgPPM);
return PPM;
}
//GM_502B VOC - Call getGasValue function
int getVOC_GasValue()
{
//Average
for(int x = 0 ; x < 100 ; x++)
{
VOCValue = gas.measure_VOC(); //GM_502B VOC
}
VOC_Volt = (VOCValue/1024)*3.3;
RS_gas = (3.3-VOC_Volt)/VOC_Volt;
R0 = 1; //measured on ambient air
ratio = RS_gas/R0;
// ratio = 1; //it is for tests of the calibration curve
// The following “val” is for comparison, I believe
val = gas.measure_VOC(); Serial.print(“VOC: “); Serial.print(val); Serial.print(” eq “);
Serial.print(gas.calcVol(val)); Serial.println(” V”);
lgPPM = (log10(ratio) * + 1.9) - 0.2; //+2 -0.3 - 2.82) - 0.12; - best for range up to 150 ppm
PPM = pow(10,lgPPM);
return ratio;
}