FireFli (PTY) LTD
Published © GPL3+

Air Quality Monitor - A Particle Photon 2 tutorial

The purpose of this project is to build a simple air quality sensor that will monitor the eCO2 and TVOC levels or it’s surrounding area. The

IntermediateProtip6 hours503
Air Quality Monitor - A Particle Photon 2 tutorial

Things used in this project

Hardware components

Photon 2
Particle Photon 2
×1
Solderless Breadboard Half Size
Solderless Breadboard Half Size
×1
Adafruit SGP30 Air Quality Sensor module
×1
Li-Ion Battery 1000mAh
Li-Ion Battery 1000mAh
×1
Jumper wires (generic)
Jumper wires (generic)
×1
WAVE 2" 7789 TFT Display
×1
M2 Hex socket cap screw - 6mm
×1
M2 heat inserts
×1
DC Axial fan 40x40x10mm
×1
Power MOSFET N-Channel
Power MOSFET N-Channel
×1

Software apps and online services

Microsoft Visual Studio Code
Particle Workbench enxtension for VSC
Cura Slicer
Fusion 360
Autodesk Fusion 360

Hand tools and fabrication machines

Ultimaker S3 3D printer
Weller WT1010N Soldering station

Story

Read more

Custom parts and enclosures

AQS

Zip files containing .STEP file of the enclosure

Schematics

Project Document

This document includes all the instructions and links listed above.

Code

Air_Quality Monitor.ino

C/C++
// Include Air quality sensor libraries //
#include "../lib/SGP30/src/Adafruit_SPIDevice.h"
#include "../lib/SGP30/src/Adafruit_I2CDevice.h"
#include "../lib/SGP30/src/Adafruit_BusIO_Register.h"
#include "../lib/SGP30/src/Adafruit_SGP30.h"

// Include ST7789 TFT Display libraries //
#include "../lib/Adafruit_GFX_RK/src/Adafruit_GFX.h"
#include "../lib/Adafruit_ST7735_RK/src/Adafruit_ST7789.h"
#include "../lib/Adafruit_GFX_RK/src/FreeSansBold12pt7b.h"
#include "../lib/Adafruit_GFX_RK/src/FreeSansOblique12pt7b.h"
#include <SPI.h>

// ST7789 TFT  definitions // 
#define TFT_CS        S3                                            // Define CS pin for TFT display
#define TFT_RST       D6                                            // Define RST pin for TFT display
#define TFT_DC        D5                                            // Define DC pin for TFT display

int tvoc_state[4] = {0,0,0,0};                                      // initialise array to handle TVOC readings display
int co2_state[4] = {0,0,0,0};                                       // initialise array to handle CO2 readings display

unsigned long previousMillis = 0;                                   // Timer that will used in print_values() funtion.  
const long interval = 10000;                                        // Set duration between readings in millis e.g. 10 000 = 10s 
int counter = 0;                                                    // start counter.  used in SPG30 fucntion to call baseline readings 

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);     // Hardware SPI

float p = 3.1415926;

Adafruit_SGP30 sgp;                                                 // call SPG30 sensor

/* return absolute humidity [mg/m^3] with approximation formula
* @param temperature [°C]
* @param humidity [%RH]
*/

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;
}

void setup() {

  Serial.begin(115200);                                                                // Start serial

  pinMode(A2, OUTPUT);                                                                 // sets the pin as output
  analogWriteResolution(A2, 12);                                                    // sets analogWrite resolution to 12 bits
  int maxFreq = analogWriteMaxFrequency(A2);
  // analogWrite (A2,255,maxFreq);  
  analogWrite(A2, 3000, maxFreq / 2);                                               // 2000/4095 = ~50% duty cycle                                                             
  
  tft.init(240, 320);                                                                  // Init ST7789 320x240 
  tft.fillScreen(ST77XX_BLACK);                                                        // creates black background in dsiplay

  while (!Serial) { delay(10); }                                                       // Wait for serial console to open!

  //Particle.publish("SGP30 test", PRIVATE);                                           // DEBUG

  if (! sgp.begin()){                                                                  // initialise SGP30 sensor
    Particle.publish("Sensor not found", PRIVATE);                                     // DEBUG -- uncomment if you are not sure whether your sensor is working.
    while (1);
  }
  
  delay(50);

  // 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!
  
}

void measure() {

// 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");                                             // cofirm wiring if this fails
    return;
  }
  
  //Particle.publish("eCO2 " + String(sgp.eCO2) +" ppm", PRIVATE);                     // DEBUG

  if ((sgp.TVOC >= 0) && (sgp.TVOC <= 220) && (tvoc_state[0] == 0)) {                  // TVOC measurement brackets to define warning indicator colour.
    Serial.print("G_TVOC "); Serial.print(sgp.TVOC); Serial.print(" ppb\t");           // DEBUG
    tft.fillRect(0,165,240,95,ST77XX_GREEN);
  
    tvoc_state[0] = 1;                                                                  // Array is contructed to prevent code from writing the images consitently if there has been no change
    tvoc_state[1] = 0;                                                                  // This applies to the entire IF stament for both TVOC and CO2   
    tvoc_state[2] = 0;
    tvoc_state[3] = 0;

    print_values();
  
  } else if ((sgp.TVOC >= 221) && (sgp.TVOC <= 660) && (tvoc_state[1] == 0)) {
          Serial.print("G_TVOC "); Serial.print(sgp.TVOC); Serial.print(" ppb\t");     // DEBUG
          tft.fillRect(0,165,240,95,ST77XX_YELLOW);
          tvoc_state[0] = 0;
          tvoc_state[1] = 1;
          tvoc_state[2] = 0;
          tvoc_state[3] = 0;
    
  } else if ((sgp.TVOC >= 661) && (sgp.TVOC <= 1430) && (tvoc_state[2] == 0)) {
          Serial.print("G_TVOC "); Serial.print(sgp.TVOC); Serial.print(" ppb\t");    // DEBUG
          tft.fillRect(0,165,240,95,ST77XX_ORANGE);
          tvoc_state[0] = 0;
          tvoc_state[1] = 0;
          tvoc_state[2] = 1;
          tvoc_state[3] = 0;

  } else if ((sgp.TVOC >= 1431) && (tvoc_state[3] == 0)) {
          Serial.print("G_TVOC "); Serial.print(sgp.TVOC); Serial.print(" ppb\t");     // DEBUG
          tft.fillRect(0,165,240,95,ST77XX_RED);
          tvoc_state[0] = 0;
          tvoc_state[1] = 0;
          tvoc_state[2] = 0;
          tvoc_state[3] = 1;
  } 


if ((sgp.eCO2 >= 0) && (sgp.eCO2 <= 1000) && (co2_state[0] == 0)) {
  Serial.print("G_CO2 "); Serial.print(sgp.eCO2); Serial.print(" ppm");                // DEBUG
  tft.fillRect(0,0,240,95,ST77XX_GREEN);
  
  co2_state[0] = 1;
  co2_state[1] = 0;
  co2_state[2] = 0;
  co2_state[3] = 0;
  
  } else if ((sgp.eCO2 >= 1001) && (sgp.eCO2 <= 2000) && (co2_state[1] == 0)) {
          Serial.print("G_CO2 "); Serial.print(sgp.eCO2); Serial.print(" ppm");        // DEBUG
          tft.fillRect(0,0,240,95,ST77XX_YELLOW);
          co2_state[0] = 0;
          co2_state[1] = 1;
          co2_state[2] = 0;
          co2_state[3] = 0;

  } else if ((sgp.eCO2 >= 2001) && (sgp.eCO2 <= 5000) && (co2_state[2] == 0)) {
          Serial.print("G_CO2 "); Serial.print(sgp.eCO2); Serial.print(" ppm");        // DEBUG
          tft.fillRect(0,0,240,95,ST77XX_ORANGE);
          co2_state[0] = 0;
          co2_state[1] = 0;
          co2_state[2] = 1;
          co2_state[3] = 0;

  } else if ((sgp.eCO2 >= 5000) && (co2_state[3] == 0)) {
          Serial.print("G_CO2 "); Serial.print(sgp.eCO2); Serial.print(" ppm");        // DEBUG
          tft.fillRect(0,0,240,95,ST77XX_RED);
          co2_state[0] = 0;
          co2_state[1] = 0;
          co2_state[2] = 0;
          co2_state[3] = 1;

  } 

  //Particle.publish("Raw H2 " + String(sgp.rawH2) + " \t", PRIVATE);                  // additional paramenters
  //Particle.publish("Raw Ethanol "+ String (sgp.rawEthanol) + "", PRIVATE);           // additional paramenters

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

    uint16_t TVOC_base, eCO2_base;
    
    if (! sgp.getIAQBaseline(&eCO2_base, &TVOC_base)) {
      Particle.publish("Failed to get baseline readings", PRIVATE);
      return;
    }
  
  //Particle.publish("****Baseline values: eCO2: 0x" + String(eCO2_base, HEX), PRIVATE);
  //Particle.publish(" & TVOC: 0x" + String(TVOC_base, HEX), PRIVATE);
  }

  draw_screen();                                                                         // calls draw_screen() function
}

void draw_screen() {
  
  tft.setFont(&FreeSansBold12pt7b);                                                      // set font
  tft.setTextColor(ST77XX_WHITE);                                                        // set font colour
  tft.setTextWrap(false); 
  tft.setTextSize(3);                                                                    // set font size
  
  tft.setCursor(45, 70);                                                                 // set sursor to start writing text
  tft.print("CO2");
  
  tft.setCursor(15, 235);
  tft.print("TVOC");
  
  delay(50);

}

void print_values() {
 
  tft.fillRect(0,95,120,60,ST77XX_WHITE);                                               // draws background fills for readings
  tft.fillRect(121,95,120,60,ST77XX_BLUE);                                               
    
  tft.fillRect(0,260,120,60,ST77XX_WHITE);
  tft.fillRect(121,260,120,60,ST77XX_BLUE);
   
  tft.setTextWrap(false);
  tft.setCursor(25, 120);
  tft.setTextColor(ST77XX_BLUE);
  tft.setFont(&FreeSansOblique12pt7b);
  tft.setTextSize(1);
  tft.println(sgp.eCO2);
  tft.setCursor(25, 145);
  tft.println("ppm");
  //Serial.print("G_eCO2 "); Serial.print(sgp.eCO2); Serial.print(" ppm");              // DEBUG
  
  tft.setCursor(25, 285);
  tft.println(sgp.TVOC); 
  tft.setCursor(25, 310);
  tft.println("ppb\\t");
  //Serial.print("G_TVOC "); Serial.print(sgp.TVOC); Serial.print(" ppb\t");            // DEBUG

  tft.setCursor(150, 120);
  tft.setTextColor(ST77XX_WHITE);
  tft.println(sgp.rawH2);
  tft.setCursor(150, 145); 
  tft.println("H2 \\t");

  tft.setCursor(150, 285);
  tft.println(sgp.rawEthanol); 
  tft.setCursor(145, 310);
  tft.print("Ethanol"); 

  delay(50);

}

void loop() {

  measure();                                                                            // calls measure() function

  unsigned long currentMillis = millis();                                               // starts timer.  when timer has ellapsed, print_values() function is called.
  if (currentMillis - previousMillis >= interval) {
    print_values();
    previousMillis = currentMillis;
  }
}

Air_Qulaity_Sensor

Simply download the entire repository and open in VSC.

Credits

FireFli (PTY) LTD

FireFli (PTY) LTD

6 projects • 9 followers
New to this... but loving it :)

Comments

Add projectSign up / Login