Hardware components | ||||||
![]() |
| × | 1 | |||
| × | 1 | ||||
Software apps and online services | ||||||
![]() |
|
I've been following different environmental sensors for a few years now and never before have I seen one that encompasses all of my favorite metrics other than CO2.
In this sketch, I'm only displaying PM2.5, PM10.0, VOC, NOX, Temp, and Humidity but you can modify it to swap out any that you want to see.
I've found that the sensor and Wio Terminal are very sensitive and checked the readings against the Amazon air quality sensor, they were very similar.
Wio Terminal SEN55 Grove Sketch
ArduinoMain sketch to read SEN55 and output the results to the Wio Terminal LCD using TFT eSPI.
#include <Arduino.h>
#include <SensirionI2CSen5x.h>
#include <Wire.h>
#include "TFT_eSPI.h"
// The used commands use up to 48 bytes. On some Arduino's the default buffer
// space is not large enough
#define MAXBUF_REQUIREMENT 48
#if (defined(I2C_BUFFER_LENGTH) && (I2C_BUFFER_LENGTH >= MAXBUF_REQUIREMENT)) || (defined(BUFFER_LENGTH) && BUFFER_LENGTH >= MAXBUF_REQUIREMENT)
#define USE_PRODUCT_INFO
#endif
#define NICE_GREEN 0x7650
#define NICE_YELLOW 0xFF70
#define NICE_ORANGE 0xFE8E
#define NICE_RED 0xFB8A
#define NICE_DARKBORDER 0x3A08
#define LCD_BACKLIGHT (72Ul) // Control Pin of LCD
SensirionI2CSen5x sen5x;
TFT_eSPI tft;
TFT_eSprite spr = TFT_eSprite(&tft); // Sprite
bool ALERT = false;
void printModuleVersions() {
uint16_t error;
char errorMessage[256];
unsigned char productName[32];
uint8_t productNameSize = 32;
error = sen5x.getProductName(productName, productNameSize);
if (error) {
Serial.print("Error trying to execute getProductName(): ");
errorToString(error, errorMessage, 256);
Serial.println(errorMessage);
} else {
Serial.print("ProductName:");
Serial.println((char*)productName);
}
uint8_t firmwareMajor;
uint8_t firmwareMinor;
bool firmwareDebug;
uint8_t hardwareMajor;
uint8_t hardwareMinor;
uint8_t protocolMajor;
uint8_t protocolMinor;
error = sen5x.getVersion(firmwareMajor, firmwareMinor, firmwareDebug,
hardwareMajor, hardwareMinor, protocolMajor,
protocolMinor);
if (error) {
Serial.print("Error trying to execute getVersion(): ");
errorToString(error, errorMessage, 256);
Serial.println(errorMessage);
} else {
Serial.print("Firmware: ");
Serial.print(firmwareMajor);
Serial.print(".");
Serial.print(firmwareMinor);
Serial.print(", ");
Serial.print("Hardware: ");
Serial.print(hardwareMajor);
Serial.print(".");
Serial.println(hardwareMinor);
}
}
void printSerialNumber() {
uint16_t error;
char errorMessage[256];
unsigned char serialNumber[32];
uint8_t serialNumberSize = 32;
error = sen5x.getSerialNumber(serialNumber, serialNumberSize);
if (error) {
Serial.print("Error trying to execute getSerialNumber(): ");
errorToString(error, errorMessage, 256);
Serial.println(errorMessage);
} else {
Serial.print("SerialNumber:");
Serial.println((char*)serialNumber);
}
}
bool inRange(float myfloat, float minimum, float maximum) {
return ((minimum <= myfloat) && (myfloat <= maximum));
}
void setup() {
Serial.begin(115200);
Wire.begin();
sen5x.begin(Wire);
pinMode(WIO_KEY_A, INPUT_PULLUP);
pinMode(WIO_KEY_C, INPUT_PULLUP);
tft.begin();
digitalWrite(LCD_BACKLIGHT, HIGH);
tft.fillScreen(TFT_BLACK);
tft.setRotation(3);
spr.createSprite(tft.width(), tft.height());
uint16_t error;
char errorMessage[256];
error = sen5x.deviceReset();
if (error) {
Serial.print("Error trying to execute deviceReset(): ");
errorToString(error, errorMessage, 256);
Serial.println(errorMessage);
}
// Print SEN55 module information if i2c buffers are large enough
#ifdef USE_PRODUCT_INFO
printSerialNumber();
printModuleVersions();
#endif
// set a temperature offset in degrees celsius
// Note: supported by SEN54 and SEN55 sensors
// By default, the temperature and humidity outputs from the sensor
// are compensated for the modules self-heating. If the module is
// designed into a device, the temperature compensation might need
// to be adapted to incorporate the change in thermal coupling and
// self-heating of other device components.
//
// A guide to achieve optimal performance, including references
// to mechanical design-in examples can be found in the app note
// SEN5x Temperature Compensation Instruction at www.sensirion.com.
// Please refer to those application notes for further information
// on the advanced compensation settings used
// in `setTemperatureOffsetParameters`, `setWarmStartParameter` and
// `setRhtAccelerationMode`.
//
// Adjust tempOffset to account for additional temperature offsets
// exceeding the SEN module's self heating.
float tempOffset = 0.0;
error = sen5x.setTemperatureOffsetSimple(tempOffset);
if (error) {
Serial.print("Error trying to execute setTemperatureOffsetSimple(): ");
errorToString(error, errorMessage, 256);
Serial.println(errorMessage);
} else {
Serial.print("Temperature Offset set to ");
Serial.print(tempOffset);
Serial.println(" deg. Celsius (SEN54/SEN55 only");
}
// Start Measurement
error = sen5x.startMeasurement();
if (error) {
Serial.print("Error trying to execute startMeasurement(): ");
errorToString(error, errorMessage, 256);
Serial.println(errorMessage);
}
}
void loop() {\
loop1();
}
void loop1() {
uint16_t error;
char errorMessage[256];
delay(1000);
spr.setTextDatum(TL_DATUM);
spr.fillSprite(TFT_WHITE);
spr.setFreeFont(&FreeSansBoldOblique18pt7b);
spr.setTextColor(TFT_BLACK);
spr.drawString("AIR QUALITY", 50, 10, 1); // Print the test text in the custom font 1
// Prepare for showing the sensor values
spr.setTextDatum(TL_DATUM);
spr.setTextColor(TFT_BLACK);
spr.setTextFont(2);
//Serial.println(tft.height()); // 320x240
// Read Measurement
float massConcentrationPm1p0;
float massConcentrationPm2p5;
float massConcentrationPm4p0;
float massConcentrationPm10p0;
float ambientHumidity;
float ambientTemperature;
float vocIndex;
float noxIndex;
error = sen5x.readMeasuredValues(
massConcentrationPm1p0, massConcentrationPm2p5, massConcentrationPm4p0,
massConcentrationPm10p0, ambientHumidity, ambientTemperature, vocIndex,
noxIndex);
if (error) {
Serial.print("Error trying to execute readMeasuredValues(): ");
errorToString(error, errorMessage, 256);
Serial.println(errorMessage);
} else {
if (inRange(massConcentrationPm2p5, 0, 50)) {
spr.fillRect(tft.width() / 2 - 128, 50, 80, 60, NICE_GREEN);
} else if (inRange(massConcentrationPm2p5, 51, 150)) {
spr.fillRect(tft.width() / 2 - 128, 50, 80, 60, NICE_YELLOW);
} else if (inRange(massConcentrationPm2p5, 151, 300)) {
spr.fillRect(tft.width() / 2 - 128, 50, 80, 60, NICE_ORANGE);
} else if (inRange(massConcentrationPm2p5, 301, 10000)) {
spr.fillRect(tft.width() / 2 - 128, 50, 80, 60, NICE_RED);
ALERT = true;
}
spr.setTextColor(NICE_DARKBORDER);
spr.setTextDatum(MC_DATUM);
spr.setFreeFont(&FreeSansBold12pt7b);
spr.drawString((String)massConcentrationPm2p5, tft.width() / 2 - 90, 77);
// Draw colored rects based on sensor reading. Based on researchgate.net chart google images
if (inRange(massConcentrationPm10p0, 0, 199)) {
spr.fillRect(tft.width() / 2 - 38, 50, 80, 60, NICE_GREEN);
} else if (inRange(massConcentrationPm10p0, 200, 349)) {
spr.fillRect(tft.width() / 2 - 38, 50, 80, 60, NICE_YELLOW);
} else if (inRange(massConcentrationPm10p0, 350, 549)) {
spr.fillRect(tft.width() / 2 - 38, 50, 80, 60, NICE_ORANGE);
} else if (inRange(massConcentrationPm10p0, 550, 10000)) {
spr.fillRect(tft.width() / 2 - 38, 50, 80, 60, NICE_RED);
ALERT = true;
}
spr.setTextDatum(MC_DATUM);
spr.setFreeFont(&FreeSansBold12pt7b);
spr.drawString((String)massConcentrationPm10p0, tft.width() / 2, 77);
// Serial.print("MassConcentrationPm1p0:");
// Serial.print(massConcentrationPm1p0);
// Serial.print("\t");
// Serial.print("MassConcentrationPm2p5:");
// Serial.print(massConcentrationPm2p5);
// Serial.print("\t");
// Serial.print("MassConcentrationPm4p0:");
// Serial.print(massConcentrationPm4p0);
// Serial.print("\t");
// Serial.print("MassConcentrationPm10p0:");
// Serial.print(massConcentrationPm10p0);
// Serial.print("\t");
// Serial.print("AmbientHumidity:");
if (isnan(ambientHumidity)) {
Serial.print("n/a");
} else {
//Serial.print(ambientHumidity);
spr.setTextDatum(MC_DATUM);
spr.setFreeFont(&FreeSansBold12pt7b);
spr.drawString((String)ambientHumidity, tft.width() / 2, 159);
}
// Serial.print("\t");
// Serial.print("AmbientTemperature:");
if (isnan(ambientTemperature)) {
Serial.print("n/a");
} else {
// Serial.print(ambientTemperature);
spr.setTextDatum(MC_DATUM);
spr.setFreeFont(&FreeSansBold12pt7b);
spr.drawString((String)(ambientTemperature * 9 / 5 + 32), tft.width() / 2 - 90, 159);
}
// Serial.print("\t");
// Serial.print("VocIndex:");
// Draw rectangles
if (inRange(vocIndex, 0, 150)) {
spr.fillRect(tft.width() / 2 + 52, 50, 80, 60, NICE_GREEN);
} else if (inRange(vocIndex, 151, 250)) {
spr.fillRect(tft.width() / 2 + 52, 50, 80, 60, NICE_YELLOW);
} else if (inRange(vocIndex, 251, 400)) {
spr.fillRect(tft.width() / 2 + 52, 50, 80, 60, NICE_ORANGE);
} else if (inRange(vocIndex, 401, 1000)) {
spr.fillRect(tft.width() / 2 + 52, 50, 80, 60, NICE_RED);
ALERT = true;
}
if (isnan(vocIndex)) {
//Serial.print("n/a");
spr.setTextDatum(MC_DATUM);
spr.setFreeFont(&FreeSansBold12pt7b);
spr.drawString("n/a", tft.width() / 2 + 90, 77);
} else {
spr.setTextDatum(MC_DATUM);
spr.setFreeFont(&FreeSansBold12pt7b);
spr.drawString((String)vocIndex, tft.width() / 2 + 90, 77);
// Serial.print(vocIndex);
}
// Serial.print("\t");
// Serial.print("NoxIndex:");
if (inRange(noxIndex, 0, 20)) {
spr.fillRect(tft.width() / 2 + 52, 130, 80, 60, NICE_GREEN);
} else if (inRange(noxIndex, 21, 150)) {
spr.fillRect(tft.width() / 2 + 52, 130, 80, 60, NICE_YELLOW);
} else if (inRange(noxIndex, 151, 300)) {
spr.fillRect(tft.width() / 2 + 52, 130, 80, 60, NICE_ORANGE);
} else if (inRange(noxIndex, 301, 1000)) {
spr.fillRect(tft.width() / 2 + 52, 130, 80, 60, NICE_RED);
ALERT = true;
}
if (isnan(noxIndex)) {
//Serial.println("n/a");
spr.setTextDatum(MC_DATUM);
spr.setFreeFont(&FreeSansBold12pt7b);
spr.drawString("n/a", tft.width() / 2 + 90, 159);
} else {
//Serial.println(noxIndex);
spr.setTextDatum(MC_DATUM);
spr.setFreeFont(&FreeSansBold12pt7b);
spr.drawString((String)noxIndex, tft.width() / 2 + 90, 159);
}
}
spr.setTextDatum(TL_DATUM);
spr.setTextColor(TFT_BLACK);
spr.setTextFont(2);
// Draw rectangles row 1
spr.drawRect(tft.width() / 2 - 128, 50, 80, 60, NICE_DARKBORDER); //draw rectangle with border
spr.drawRect(tft.width() / 2 - 38, 50, 80, 60, NICE_DARKBORDER); //draw rectangle with border
spr.drawRect(tft.width() / 2 + 52, 50, 80, 60, NICE_DARKBORDER); //draw rectangle with border
// Draw rectangles row 2
spr.drawRect(tft.width() / 2 - 128, 130, 80, 60, NICE_DARKBORDER); //draw rectangle with border
spr.drawRect(tft.width() / 2 - 38, 130, 80, 60, NICE_DARKBORDER); //draw rectangle with border
spr.drawRect(tft.width() / 2 + 52, 130, 80, 60, NICE_DARKBORDER); //draw rectangle with border
// Draw Metric Text row 1
spr.drawString("PM2.5", tft.width() / 2 - 122, 50);
spr.drawString("PM10.0", tft.width() / 2 - 32, 50);
spr.drawString("VOC", tft.width() / 2 + 58, 50);
// Draw Metric Text row 2
spr.drawString("Temp", tft.width() / 2 - 122, 130);
spr.drawString("Humidity", tft.width() / 2 - 32, 130);
spr.drawString("NOX", tft.width() / 2 + 58, 130);
// Draw Unit of Measure Text row 1
spr.drawString("ug/m3", tft.width() / 2 - 88, 90);
spr.drawString("ug/m3", tft.width() / 2 + 2, 90);
spr.drawString("Index", tft.width() / 2 + 92, 90);
// Draw Unit of Measure Text row 2
spr.drawString("*F", tft.width() / 2 - 76, 170);
spr.drawString("%", tft.width() / 2 + 15, 170);
spr.drawString("Index", tft.width() / 2 + 95, 170);
spr.pushSprite(0, 0);
if (ALERT) {
analogWrite(WIO_BUZZER, 128);
delay(100);
analogWrite(WIO_BUZZER, 0);
ALERT = false;
}
if (digitalRead(WIO_KEY_A) == LOW) {
Serial.println("A Key pressed");
digitalWrite(LCD_BACKLIGHT, LOW);
}
if (digitalRead(WIO_KEY_C) == LOW) {
Serial.println("C Key pressed");
digitalWrite(LCD_BACKLIGHT, HIGH);
}
}
Comments
Please log in or sign up to comment.