lmsousa
Published © GPL3+

Air Quality Analyzer

Analyses home air quality and records the values in a SD card.

IntermediateShowcase (no instructions)5,333
Air Quality Analyzer

Things used in this project

Hardware components

ADAFRUIT FEATHER M0 ADALOGGER
×1
Adafruit MONOCHROME 0.96" 128X64 OLED GRAPHIC DISPLAY
×1
ADAFRUIT CCS811 AIR QUALITY SENSOR BREAKOUT - VOC AND ECO2
×1
DHT11 Temperature & Humidity Sensor (4 pins)
DHT11 Temperature & Humidity Sensor (4 pins)
×1
Jumper wires (generic)
Jumper wires (generic)
around 20 male to male 15cm jumpers.
×2

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

Air Quality Analyzer Connections V2.0

This is the second version of the Air Quality Analyzer Connections. Now we have 3 LEDs to display 3 levels of alarm, and a Sby/On switch. Also DHT11 sensor is connected to a new pin (A5) to avoid some instability caused by pin 13 internal LED circuit.

Code

Software version 2.0.3

Arduino
This is the version 2.0.3 of the software. Has better time handling and improved display of data. Date and time now displayed on the main screen. Please inform if you have problems.
/* Air Quality Sensor V2.0.3 2018/03/27 Luis Sousa */

#include <SPI.h> //Standard lib
#include <SD.h> //author=Arduino, SparkFun version=1.2.2
#include <DHT.h>//author=Adafruit version=1.3.0
//#include <Wire.h>
#include <RTCZero.h> //author=Arduino version=1.5.2
#include <RTClib.h> //author=Adafruit version=1.2.1 
#include <Adafruit_GFX.h> //author=Adafruit version=1.2.3
#include <Adafruit_SSD1306.h> //author=Adafruit version=1.1.2
#include <Adafruit_CCS811.h>//author=Adafruit version=1.0.0

//Levels
//temp levels
#define low_temp_level_1 18
#define low_temp_level_2 12
#define low_temp_level_3 8
#define high_temp_level_1 29
#define high_temp_level_2 35
#define high_temp_level_3 40
//rh levels
#define low_rh_level_1 40
#define low_rh_level_2 30
#define low_rh_level_3 20
#define high_rh_level_1 70
#define high_rh_level_2 80
#define high_rh_level_3 90
//co2 levels
#define co2_level_1 1000
#define co2_level_2 1500
#define co2_level_3 3000
//TVOC levels
#define tvoc_level_1 100
#define tvoc_level_2 500
#define tvoc_level_3 1000

//Status LEDS
#define green_led  14
#define yellow_led 15
#define red_led 16

//OLED Display If using software SPI (the default case):
#define OLED_MOSI   6
#define OLED_CLK   10
#define OLED_DC    11
#define OLED_CS    12
#define OLED_RESET 5
#define display_refresh_interval 1 //2 seconds refresh
Adafruit_SSD1306 display(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);

//DHT Sensor
#define DHTPIN 19     // what pin DHT is connected to (19=A5 on this Board)
#define DHTTYPE DHT11   // DHT 11
#define DHT_read_interval 3 //Read temp every X seconds
DHT dht(DHTPIN, DHTTYPE); //object dht creation

//SD Card
#define cardSelect 4  //SD chip select pin
#define ficheiro "dados.csv" // nome do ficheiro de dados
//#define Serial_Header "Date,Hour,Vbat,Temp,%RH,eCO2,TVOC,STATUS"
#define SD_Header "Date,Hour,Vbat,min_Temp,Max_Temp,min_RH,Max_RH,min_eCO2,Max_eCO2,min_TVOC,Max_TVOC,STATUS"
#define SD_write_interval 300 //seconds between SD writes
#define SD_led 8 //Green LED

//RTC
RTCZero rtc;  // Create an rtc object /

//Vbat read
#define vbatpin A7    //pin to read Bat Voltage
#define vbat_read_interval 30 //seconds between vbat readings
float measuredvbat;

//Set up variables using the SD utility library functions:
Sd2Card card;
SdVolume volume;
SdFile root;

//Set up CCS811 object ccs
Adafruit_CCS811 ccs;
#define CCS811_read_interval 3 //Read data every X seconds

//variable declaration
unsigned long nseconds; //seconds since start of day.
unsigned long SD_last_write = 0;
unsigned long vbat_last_read = 0;
unsigned long DHT_last_read = 0;
unsigned long CCS811_last_read = 0;
unsigned long display_last_refresh = 0;
boolean error = false; //clear errors
boolean alarm = false; //clear alarms
boolean alarm_level_0 = false;
boolean alarm_level_1 = false;
boolean alarm_level_2 = false;
boolean alarm_level_3 = false;
float eco2 = 0.0;
float max_eco2 = 0.0;
float min_eco2 = 10000;
float tvoc = 0.0;
float max_tvoc = 0.0;
float min_tvoc = 10000;
float temp = 0.0;
float max_temp = 0.0;
float min_temp = 100;
float rh = 0.0;
float max_rh = 0.0;
float min_rh = 100;
String errorString = "";
String alarmString = "";
//byte day = 27;
//byte month = 03;
//byte year = 18;
//byte seconds = 00;
//byte minutes = 00;
//byte hours = 12;

void setup() {
  pinMode(SD_led, OUTPUT);// initialize digital pin 8 as an output.
  pinMode(green_led, OUTPUT);// initialize digital pin as an output.
  pinMode(yellow_led, OUTPUT);// initialize digital pin as an output.
  pinMode(red_led, OUTPUT);// initialize digital pin as an output.

  //Serial Port Initialization
  Serial.begin(57600);
  delay (300);//wait for the serial to be ready
  /*
    while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
    }
  */

  //RTC initialization
  DateTime now;
  now = (DateTime(F(__DATE__), F(__TIME__))); //Compilation time
  now=now+20; //compensation for Compile + Reset time
  rtc.begin(); // initialize RTCZero
  rtc.setTime(now.hour(), now.minute(), now.second());
  rtc.setDate(now.day(), now.month(), now.year()-2000);

  //DHT Initialisation
  dht.begin();

  // CCS811 test
  if (!ccs.begin()) {
    Serial.println("Failed to start CCS811 sensor! Please check your wiring.");
    errorString = "CCS811 Error";
    //while (1);
  }
  //calibrate CCS811 temperature sensor
  while (!ccs.available());
  temp = ccs.calculateTemperature();
  ccs.setTempOffset(temp - 25.0);

  //Display initialization
  display.begin(SSD1306_SWITCHCAPVCC);// by default, we'll generate the high voltage from the 3.3v line internally!
  display.clearDisplay();  // Clear the buffer.

  //SD Initialization and Test
  Serial.print("\nInitializing SD card...");
  // we'll use the initialization code from the utility libraries
  // since we're just testing if the card is working!
  if (!card.init(SPI_HALF_SPEED, cardSelect)) {
    Serial.println("initialization failed. Things to check: ");
    Serial.println("* is a card inserted ? ");
    Serial.println("* is your wiring correct ? ");
    Serial.println("* did you change the chipSelect pin to match your shield or module ? ");
    //while (1);
    errorString = "SD Card Error";
  } else {
    Serial.println("Wiring is correct and a card is present.");
  }
  // print the type of card
  Serial.println();
  Serial.print("Card type :         ");
  switch (card.type()) {
    case SD_CARD_TYPE_SD1:
      Serial.println("SD1");
      break;
    case SD_CARD_TYPE_SD2:
      Serial.println("SD2");
      break;
    case SD_CARD_TYPE_SDHC:
      Serial.println("SDHC");
      break;
    default:
      Serial.println("Unknown");
  }
  // Now we will try to open the 'volume'/'partition' - it should be FAT16 or FAT32
  if (!volume.init(card)) {
    Serial.println("Could not find FAT16 / FAT32 partition.\nMake sure you've formatted the card");
    //while (1);
    errorString = "SD Format Error";
  }
  Serial.print("Clusters:          ");
  Serial.println(volume.clusterCount());
  Serial.print("Blocks x Cluster:  ");
  Serial.println(volume.blocksPerCluster());
  Serial.print("Total Blocks:      ");
  Serial.println(volume.blocksPerCluster() * volume.clusterCount());
  Serial.println();
  // print the type and size of the first FAT-type volume
  uint32_t volumesize;
  Serial.print("Volume type is:    FAT");
  Serial.println(volume.fatType(), DEC);
  volumesize = volume.blocksPerCluster();    // clusters are collections of blocks
  volumesize *= volume.clusterCount();       // we'll have a lot of clusters
  volumesize /= 2;                           // SD card blocks are always 512 bytes (2 blocks are 1KB)
  Serial.print("Volume size (Kb):  ");
  Serial.println(volumesize);
  Serial.print("Volume size (Mb):  ");
  volumesize /= 1024;
  Serial.println(volumesize);
  Serial.print("Volume size (Gb):  ");
  Serial.println((float)volumesize / 1024.0);
  Serial.println("\nFiles found on the card (name, date and size in bytes): ");
  root.openRoot(volume);
  // list all files in the card with date and size
  root.ls(LS_R | LS_DATE | LS_SIZE);
  if (!SD.begin(cardSelect)) {
    errorString = ("No SD Card");
  }
  else {
    File logfile = SD.open(ficheiro, FILE_WRITE);
    if (!logfile)
      errorString = ("File Error");
    else {
      Serial.print("Gravando em: ");
      Serial.println(ficheiro);
      logfile.println(SD_Header);
      logfile.close();
    }
  }
}

void loop() {
  //How many seconds since 0H (start of day)
  nseconds = 3600 * rtc.getHours() + 60 * rtc.getMinutes() + rtc.getSeconds();

  //measure Vbat
  if (abs(nseconds - vbat_last_read >= vbat_read_interval)) //time to read vbat
  {
    measuredvbat = analogRead(vbatpin);
    measuredvbat *= 2; // we divided by 2, so multiply back
    measuredvbat *= 3.3; // Multiply by 3.3V, our reference voltage
    measuredvbat /= 1024; // convert to voltage
    vbat_last_read = nseconds;
  }
  // Reading temperature and humidity
  //if (nseconds % DHT_read_interval == 0) //Time to read temp
  if (abs(nseconds - DHT_last_read >= DHT_read_interval)) //time to read DHT
  {
    rh = dht.readHumidity();
    temp = dht.readTemperature();  // Read temperature as Celsius (the default)
    float f = dht.readTemperature(true);   // Read temperature as Fahrenheit (isFahrenheit = true)

    if (isnan(rh) || isnan(temp) || isnan(f)) {
      Serial.println("Failed to read from DHT sensor!");// Check if any reads failed and exit early (to try again).
      error = true;
      errorString = ("DHT Error");
      //return;
    }
    DHT_last_read = nseconds;
  }
  // Reading eCO2 & TVOC
  //if (nseconds % CCS811_read_interval == 0) //Time to read data
  if (abs(nseconds - CCS811_last_read >= CCS811_read_interval)) //time to read CCS811
  {
    if (ccs.available()) {
      //temp = ccs.calculateTemperature();
      if (!ccs.readData()) {
        eco2 = ccs.geteCO2();
        tvoc = ccs.getTVOC();
      }
      else {
        error = true;
        Serial.println("CCS811 sensor error!");
        errorString = "CCS811 error";
        //while(1);
      }
      CCS811_last_read = nseconds;
    }
  }

  //Alarm levels
  alarm_level_3 = temp > high_temp_level_3 || temp < low_temp_level_3 || rh > high_rh_level_3 || rh < low_rh_level_3 || eco2 > co2_level_3 || tvoc > tvoc_level_3;
  alarm_level_2 = temp > high_temp_level_2 || temp < low_temp_level_2 || rh > high_rh_level_2 || rh < low_rh_level_2 || eco2 > co2_level_2 || tvoc > tvoc_level_2;
  alarm_level_1 = temp > high_temp_level_1 || temp < low_temp_level_1 || rh > high_rh_level_1 || rh < low_rh_level_1 || eco2 > co2_level_1 || tvoc > tvoc_level_1;
  alarm_level_0 = !alarm_level_3 & !alarm_level_2 & !alarm_level_2;

  //Alarm LEDS &  Alarm Strings
  if (alarm_level_3) {
    alarm = true;
    digitalWrite(green_led, LOW);
    digitalWrite(yellow_led, LOW);
    digitalWrite(red_led, HIGH);
    if (eco2 > co2_level_3) alarmString = "CO2 High ++";
    else if (tvoc > tvoc_level_3) alarmString = "TVOC High ++";
    else if (rh > high_rh_level_3) alarmString = "RH High ++";
    else if (rh < low_rh_level_3) alarmString = "RH Low --";
    else if (temp > high_temp_level_3) alarmString = "Temp High ++";
    else if (temp < low_temp_level_3) alarmString = "Temp Low --";
  }
  else if (alarm_level_2) {
    alarm = true;
    digitalWrite(green_led, LOW);
    digitalWrite(yellow_led, HIGH);
    digitalWrite(red_led, LOW);
    if (eco2 > co2_level_2) alarmString = "CO2 High +";
    else if (tvoc > tvoc_level_2) alarmString = "TVOC High +";
    else if (rh > high_rh_level_2) alarmString = "RH High +";
    else if (rh < low_rh_level_2) alarmString = "RH Low -";
    else if (temp > high_temp_level_2) alarmString = "Temp High +";
    else if (temp < low_temp_level_2) alarmString = "Temp Low -";
  }
  else if (alarm_level_1) {
    alarm = true;
    digitalWrite(green_led, LOW);
    digitalWrite(yellow_led, HIGH);
    digitalWrite(red_led, LOW);
    if (eco2 > co2_level_1)alarmString = ("CO2 High");
    else if (tvoc > tvoc_level_1) alarmString = "TVOC High";
    else if (rh > high_rh_level_1) alarmString = "RH High";
    else if (rh < low_rh_level_1) alarmString = "RH Low";
    else if (temp > high_temp_level_1) alarmString = "Temp High";
    else if (temp < low_temp_level_1) alarmString = "Temp Low";
  }
  else if (alarm_level_0) {
    alarm = false;
    digitalWrite(green_led, HIGH);
    digitalWrite(yellow_led, LOW);
    digitalWrite(red_led, LOW);
    alarmString = "OK";
  }
  //Max and min determination
  if (temp > max_temp) max_temp = temp;
  if (temp < min_temp) min_temp = temp;
  if (rh > max_rh) max_rh = rh;
  if (rh < min_rh) min_rh = rh;
  if (eco2 > max_eco2) max_eco2 = eco2;
  if (eco2 < min_eco2) min_eco2 = eco2;
  if (tvoc > max_tvoc) max_tvoc = tvoc;
  if (tvoc < min_tvoc) min_tvoc = tvoc;

  //Relatorio serial
  Serial.print("Data:" );
  serial2digits(rtc.getDay());
  Serial.print("/");
  serial2digits(rtc.getMonth());
  Serial.print("/");
  Serial.print(2000 + rtc.getYear(), DEC);
  Serial.print(" Hora:" );
  serial2digits(rtc.getHours());
  Serial.print(":");
  serial2digits(rtc.getMinutes());
  Serial.print(":");
  serial2digits(rtc.getSeconds());
  Serial.print("  ");
  Serial.print("VBat:" ); Serial.print(measuredvbat);
  Serial.print(" Temp:");   Serial.print(temp);
  Serial.print(" Humid:");   Serial.print(rh);
  Serial.print(" CO2:");   Serial.print(eco2);
  Serial.print(" ppm, TVOC:");  Serial.print(tvoc); Serial.print(" ppb:");
  Serial.print(" Status:");
  if (error)
    Serial.println(errorString);
  else if (alarm)
    Serial.println(alarmString);
  else Serial.println("OK");

  //Data display
  if (abs(nseconds - display_last_refresh >= display_refresh_interval)) //time to display information
  {
    display.clearDisplay();// Clean Display
    display.setTextSize(1);
    display.setTextColor(WHITE);
    display.setCursor(0, 0);
    display.println("Air Quality Analyzer");
    display.setCursor(0, 8);
    display2digits(rtc.getDay());
    display.print("/");
    display2digits(rtc.getMonth());
    display.print("/");
    display.print(2000 + rtc.getYear(), DEC);
    display.print(" ");
    display2digits(rtc.getHours());
    display.print(":");
    display2digits(rtc.getMinutes());
    /*display.print(":");
      display.print(rtc.getSeconds(), DEC);*/
    display.setCursor(0, 16);
    display.print("Vbat:");
    display.setCursor(50, 16);
    display.print(measuredvbat); display.print(" V");
    display.setCursor(0, 24);
    display.print("Temp:");
    display.setCursor(50, 24);
    display.print(temp); display.print(" C");
    display.setCursor(0, 32);
    display.print("RH:");
    display.setCursor(50, 32);
    display.print(rh); display.print(" %");
    display.setCursor(0, 40);
    display.print("eCO2:");
    display.setCursor(50, 40);
    display.print(eco2); display.print(" ppm");
    display.setCursor(0, 48);
    display.print("TVOC:");
    display.setCursor(50, 48);
    display.print(tvoc); display.print(" ppb");
    display.setCursor(0, 56);
    display.print("Status:");
    display.setCursor(50, 56);
    if (error) {
      display.print(errorString);
    } else if (alarm) {
      display.print(alarmString);
    }
    else
    {
      display.print("OK");
    }
    display.display();
    display_last_refresh = nseconds;
  }
  //SD datalog
  //if (nseconds % SD_write_interval == 0) //Time to log data
  if (abs(nseconds - SD_last_write >= SD_write_interval)) //Time to log data
  {
    File logfile = SD.open(ficheiro, FILE_WRITE);
    if (logfile) {
      digitalWrite(SD_led, HIGH); //SD Write begins
      logfile2digits(rtc.getDay(), logfile);
      logfile.print("/");
      logfile2digits(rtc.getMonth(), logfile);
      logfile.print("/");
      logfile.print(2000 + rtc.getYear(), DEC);
      logfile.print(",");
      logfile2digits(rtc.getHours(), logfile);
      logfile.print(":");
      logfile2digits(rtc.getMinutes(), logfile);
      logfile.print(":");
      logfile2digits(rtc.getSeconds(), logfile);
      logfile.print(",");
      logfile.print(measuredvbat, 2);
      logfile.print(",");
      logfile.print(min_temp, 2);
      logfile.print(",");
      logfile.print(max_temp, 2);
      logfile.print(",");
      logfile.print(min_rh, 2);
      logfile.print(",");
      logfile.print(max_rh, 2);
      logfile.print(",");
      logfile.print(min_eco2, 2);
      logfile.print(",");
      logfile.print(max_eco2, 2);
      logfile.print(",");
      logfile.print(min_tvoc, 2);
      logfile.print(",");
      logfile.print(max_tvoc, 2);
      logfile.print(",");
      if (error)
        logfile.println(errorString);
      else if (alarm)
        logfile.println(alarmString);
      else logfile.println("OK");
      logfile.close();
      SD_last_write = nseconds;
      digitalWrite(SD_led, LOW); //SD Write ends
    }
    else
    {
      error = true;
      errorString = ("logfile error");
    }
    //Max and min RESET
    max_eco2 = 0.0;
    min_eco2 = 10000;
    max_tvoc = 0.0;
    min_tvoc = 10000;
    max_temp = 0.0;
    min_temp = 100;
    max_rh = 0.0;
    min_rh = 100;
  }
  delay(200); //don't use values higher than 1000 or nseconds won't be properly updated
}

void display2digits(int number)// create a leading zero for month/day/hour/minute when <10
{
  if (number >= 0 && number < 10) {
    display.write('0');
  }
  display.print(number);
}

void logfile2digits(int number, File logfile) // create a leading zero for month/day/hour/minute when <10
{
  if (number >= 0 && number < 10) {
    logfile.write('0');
  }
  logfile.print(number);
}

void serial2digits(int number)// create a leading zero for month/day/hour/minute when <10
{
  if (number >= 0 && number < 10) {
    Serial.write('0');
  }
  Serial.print(number);
}

Credits

lmsousa

lmsousa

5 projects • 21 followers

Comments