Project showcase
CO2 Monitor

CO2 Monitor © GPL3+

CO2 monitor and log based on MQ-135 with temperature and relative humidity correction.

  • 1,724 views
  • 0 comments
  • 9 respects

Components and supplies

About this project

I felt a concern about insufficient ventilation in our home. So, I decided to build a CO2 monitor.

I found graphs of sensitivity of MQ-135 for hydrogen as a function of temperature and relative humidity here:

https://www.winsen-sensor.com/d/files/PDF/Semiconductor%20Gas%20Sensor/MQ135%20(Ver1.4)%20-%20Manual.pdf

With excel I derived a fit to these data with the formula:

f_RH_T = a + b*RH + c*T + d*T*RH

With T in degrees C and RH as a fraction (number in between 0 and 1).

a=1.78266314

b=-0.7481779

c=-0.0159248

d=0.00667796

I calculated the relative humidity from a wetted temperature sensor and a dry temperature sensor.

The CO2 concentration is calculated from the measured resistance and the temperature/relative humidity correction. It may be written as:

CO2 = 410*ppm*(Rs/f_RH_T / Rs410/f_RH_T_410)^b

Where the exponent b is equal to -2.769 according to Mad Frog. And where Rs410 is the sensor resistance measured in clean air (410 ppm of CO2) and f_RH_T_410 is the correction factor at the moment that the clean air sensor resistance is measured. Here we assume that the correction factor as a function of relative humidity and temperature is equal for CO2 and hydrogen. I do not have any calibration gasses, so I cannot check whether this is true or not. As long as the calibration is done under similar conditions as the readout, this is not very important. If however, the calibration is done at 10 degrees C and the readout is done at 30 degrees C, this may introduce a significant error.

I spent quite some time to get the SD card code to accept dynamic names for the files (depending on the date). The trick was to change the SD.cpp card library file.

Change: int pathidx;

To: int pathidx = 0;

As suggested on the Arduino forum:

https://forum.arduino.cc/index.php?topic=586134.0

Many thanks for this suggestion, it really fixed the problem!

Her another picture of my setup, where you can see how I measure the temperature with a wetted sensor. The brownish material is a piece of paper coffee filter that transports the water from the reservoir to the sensor. I covered the sensor with a plastic bag to prevent direct contact of the sensor with the water in the paper.

Any questions or remarks? Let me know!

Code

CO2v2.inoC/C++
Main program
#include <SPI.h>
#include <SD.h>
#include <LiquidCrystal.h>
#include "Time.h"
#include "Calculations.h"

const int rs = 8, en = 7, d4 = 6, d5 = 5, d6 = 4, d7 = 3;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

// set up variables using the SD utility library functions:
Sd2Card card;
SdVolume volume;
SdFile root;
//pin connected to CS of SD card shield
const int chipSelect = 2;

#define _dd_   20
#define _mm_    1
#define _yy_   20

#define _hr_   17
#define _min_   54
#define _sec_   0

char fileName[13];

//Set start time and date
Time myTime(_hr_,_min_,_sec_,_dd_,_mm_,_yy_); //hr,min,sec,d,m,y

//used connections to Arduino
const byte Td_sensor  = A0;
const byte Tw_sensor  = A1;
const byte CO2_sensor = A2;
const byte checkLight =  9;


//Data are stored in data array (double data[]) to ease printing and writing to SD card and LCD screen. Here the positions of the data in the array are given. 
const byte Td   = 0; //deg Celcius
const byte Tw   = 1;  
const byte RH   = 2; //relative humidity
const byte CO2  = 3; //ppm
const byte pWd  = 4; //kPa
const byte pWw  = 5; 
const byte fRHT = 6; //-
const byte Rs   = 7; //kohm
const byte numData = 8; //number of data in data []


const String degC = String ("\xC2\xB0") + String("C");
const String labels[numData] = {"Td" ,"Tw" ,"RH","CO2","pWd","pWw","fRHT","Rs"  };
const String units [numData] = {degC ,degC ,"-" ,"ppm","Pa" ,"Pa" ,"-"   ,"kohm"};


void setup() {
  pinMode(Td_sensor ,INPUT);
  pinMode(Tw_sensor ,INPUT);
  pinMode(CO2_sensor,INPUT);
  pinMode(checkLight,OUTPUT);

  lcd.begin(16, 2);
  lcd.print("hello, world!");
  
  Serial.begin(9600);
  String fileNameConst = String("WK") + myTime.dateToString("") + String(".txt");
  strcpy(fileName,fileNameConst.c_str());

  Serial.print("Initializing SD card...");

  // see if the card is present and can be initialized:
  if (!SD.begin(chipSelect)) {
    Serial.println("Card failed, or not present");
    // don't do anything more:
    while (1);
  }
  Serial.println("card initialized.");
  delay(200);
  
  // if the file is available, write to it:
  // prepare datastring with current time and datalabels as column headers
  myTime.updTime();
  String dataString = myTime.dateToString();
  dataString+= ",";
  dataString+= myTime.timeToString();
  for (int i = 0; i<numData; i++) {
    dataString+= ",";   
    dataString+= labels[i];
  }

  File dataFile = SD.open(fileName,FILE_WRITE);
  if (dataFile) {
    dataFile.println();
    dataFile.println(dataString);
    dataFile.close();
  }
  // if the file isn't open, pop up an error:
  else {
    Serial.println("error opening datalog.txt");
  } 
}


void loop() {
  myTime.updTime();
  digitalWrite(checkLight,LOW);   //put here to see if the system is working. now not needed anymore as LCD was installed

  Serial.println(myTime.timeToString() + " " + myTime.dateToString());

  //fill data array with fresh measurement results
  double data [numData];
  data[Td]   = calcTemp(readSensor(Td_sensor));  //degrees C
  data[Tw]   = calcTemp(readSensor(Tw_sensor));  
  data[pWd]  = calcPw(data[Td]);                 //kPa
  data[pWw]  = calcPw(data[Tw]);
  data[RH]   = calcRH(data[Td],data[Tw],data[pWd],data[pWw]); //dimensionless
  data[fRHT] = calcfRHT(data[RH],data[Td]);      //humidity and temperature correction factor, dimensionless
  data[Rs]   = calcRs(readSensor(CO2_sensor));   //kohm
  data[CO2]  = calcCO2(data[Rs],data[fRHT]);     //ppm

  dataToScreen(labels,data,units);

  dataToLCD(labels,data);
  
  // make a string for assembling the data to log:
  String dataString = "";

  dataString += myTime.dateToString();  //start with current time and date
  dataString += ",";
  dataString += myTime.timeToString();  

  for (int i=0; i<numData; i++) {          //add all data to dataString
    dataString += ",";    
    dataString += String(data[i]);
  }

  // open the file. note that only one file can be open at a time,
  // so you have to close this one before opening another.
  File dataFile = SD.open(fileName, FILE_WRITE);

  // if the file is available, write to it:
  if (dataFile) {
    dataFile.println(dataString);
    dataFile.close();
  }
  // if the file isn't open, pop up an error:
  else {
    Serial.println("error opening datalog.txt");
  }
  
  //halfway time: show time, date, Rs and CO2
  lcd.clear();
  lcd.print(myTime.timeToString());
  lcd.print("   ");
  lcd.print(myTime.dateToString());
  lcd.setCursor(0,1);  
  lcd.print("Rs=");
  lcd.print(data[Rs],0);
  lcd.print("  CO2=");
  lcd.print(data[CO2],0); 
  
  digitalWrite(checkLight,HIGH);
  const unsigned long loopDuration = 30000;
  while (millis() - myTime.lastMillis() < loopDuration) { //wait until loopDuration 
    ;
  }
}
  

//return sensor value
int readSensor(const byte address) {
  int value = 0;
  byte numMeas = 32; //should not be larger than 2^6=64 (int value may roll over)
  for (int i=0; i<numMeas; i++) {
    value +=  analogRead(address);
    delay(100);  //this makes the sketch slow (32 times 100 times 3 = 9600 millisec (almost 10 sec)
  }
  value /= numMeas;
  return value;
}


//write data to screen
void dataToScreen(const String label[],const double data[],const String unit[]) {  //all arguments declared constant as this procedure is not supposed to change any data
  for (int i=0; i<numData; i++) {
    if (i==RH) { //RH is printed as % instead of fraction
      Serial.print(label[i]), Serial.print(":\t"), Serial.print(data[i]*100,2),  Serial.print("\t"), Serial.println("%");
    }
    else {
      Serial.print(label[i]), Serial.print(":\t"), Serial.print(data[i],2),      Serial.print("\t"), Serial.println(unit[i]);  
    }
  }
  Serial.println();
}


//write data to LCD
void dataToLCD(const String label[],const double data[]) {
  lcd.clear(); 
  for (int i = Td; i<=CO2; i++) {
       lcd.print(label[i]);
       lcd.print("=");
       int decimals = 1;
       if (i==CO2) decimals = 0;
       if (i== RH) decimals = 2;
       lcd.print(data[i],decimals);  
       lcd.print(" ");  
       if (i== Td) lcd.print(" ");
       if (i== Tw) lcd.setCursor(0,1); 
  }
}
Calculations.cppC/C++
Calculation of temperature, relative humidity, sensor resistance based on values read from A0, A1 and A2.
From these date it calculates water vapour pressures and sensor correction factor are derived.
Finally the CO2 concentration is measured
#include "Arduino.h"
#include "Calculations.h"


double calcTemp(int Value) {  //https://playground.arduino.cc/ComponentLib/Thermistor2/
  double Temp;
  Temp = log(10000.0/(1024.0/Value-1)); // for pull-up configuration
  Temp = 1 / (0.001129148 + (0.000234125 + (0.0000000876741 * Temp * Temp )) * Temp );
  Temp = Temp - 273.15;                // Convert Kelvin to Celcius
  
  return Temp;//degrees C
}


double calcPw(double Temp) {  //R.C. Rodgers and G.E. Hill, Brittish Journal of Anaesthesia (1978),50, 415
  const double A = 7.16728;
  const double B = 1716.984;
  const double C = 232.538;
  double exponent = A - B/(Temp+C); 
  double Pw = pow(10,exponent);
  
  return Pw;//kPa
}


double calcRH(double Td, double Tw, double pWd, double pWw) {  //https://www.1728.org/relhum.htm
  double RH;
  if(Tw >= Td) { //to prevent RH>1 due to measurement errors where Tw >= Td
    RH = 1.0;
  }
  else {
    double pav = 101.55; //atmospheric pressure in kPa http://www.klimaatatlas.nl/klimaatatlas.php
    double N = 0.0006687451584; //kPa/K
    RH  = (pWw - N*pav*(1+0.00115*Tw)*(Td-Tw))/pWd;
  }
  
  return RH; //dimensionless
}  


double calcfRHT (double RH, double T) {   //Own fit from MQ-135 datasheet. It is presumed that CO2 readings are equally influenced by temperature and RH as ammonia readings in given graphs
  const double Intercept = 1.782663144;
  const double RC_RH     = -0.748177946;
  const double RC_T      = -0.015924756;
  const double RC_RHT    =  0.006677957;
  double calcfRHT = Intercept + RC_RH*RH + RC_T*T + RC_RHT*RH*T;
  
  return calcfRHT; //Rs/R0 dimensionless
}


double calcRs(int sensorReading) {   //Calculates Rs from amplified analog signal
  const double gain = 1.0 + 10000.0/1000.0; //amplification of opamp as function of resistances in opamp circuit
  const double Rref = 10.0; //Reference resistance (in kOhm) built in MQ-135 board 
  double sensorOrigReading = sensorReading/gain;
/*formula derivation:
 *sensorOrigReading = 1024*Rref/(Rref+Rsensor)
 *sensorOrigReading = 1024*1/(1+Rsensor/Rref)
 *(1+Rsensor/Rref)  = 1024/sensorOrigReading
 *Rsensor = (1024/sensorOrigReading-1)*Rref
 */
  double Rsensor = (1024.0/sensorOrigReading-1.0)*Rref; 
  
  return Rsensor; //kOhm
}


double calcCO2(double Rs, double fRHT) {
  const double R410    = 270.0; //Measured resistance (in kOhm) after one night with open window
  const double fRHT410 = 1.08;  //calcutated correction factor after one night with open window
  const double a  =  410.0;
  const double b  = -2.769034857; //slope from Mad Frog
  
  double CO2 = a*pow((Rs/fRHT)/(R410/fRHT410),b); //CO2atm = 410 ppm
  
  return CO2;//ppm
}
Calculations.hC/C++
Header to Calculations.ccp
#include "Arduino.h"

//this library holds all physiscs calculations

//Calculate Temperature from analog input value
double calcTemp(int Value);

//Calculate vapour pressure from temperature
double calcPw(double Temp);

//Calculate relative humidity from wet and dry bulb temperature and vapour pressures at respective temperatures
double calcRH(double Td, double Tw, double Pwd, double Pww);

//Calculate sensitivity factor (Rs/R0) as function of relative humidity and temperature
double calcfRHT (double RH, double T); 

//Calculate sensor resistance (Rs) from analog input, after signal amplification with OpAmp
double calcRs(int sensorReading);

//Calculate CO2 concentration from measured sensor resistance (Rs) and relative humidity and temperature correction factor
double calcCO2(double Rs,double fRHT);
Time.cppC/C++
Time and date utility
#include "Arduino.h"
#include "Time.h"

Time::Time (byte hr, byte mnt, byte sec, byte d, byte m, byte y) { 
  _timeStart = (hr*60UL+mnt)*60UL+ sec; //initialize _timeStart 
  _lastMillis = millis();               //last time millis() was called
  _curTime = _lastMillis/1000UL;        //time since start of program in seconds (rollover after 49000 days, rollover is prevented by updDate() after 1 day)

  //initialize _Time[]
  _Time[0]      = hr;
  _Time[1]      = mnt;
  _Time[2]      = sec; 

  //initialize _Date[]
  _Date[0]      = d;
  _Date[1]      = m;
  _Date[2]      = y;

  _dayRollover  = false; 
}


void Time::updTime() {
  unsigned long curMillis = millis();
  _curTime   += curMillis/1000UL - _lastMillis/1000UL;  //update _curTime
  _lastMillis = curMillis;                              //record _lastMilles for use during next call of updTime()

  unsigned long curTime = _curTime + _timeStart;
  if               (curTime/(24UL*60UL*60UL) > 0) _dayRollover = true;  //if time goes beyond 23:23:59 dayrollover occurs and time returs toe
  //update _Time[]
  curTime      =    curTime%(24UL*60UL*60UL);
  _Time[0]     =    curTime/(60UL*60UL);
  curTime      =    curTime%(60UL*60UL);
  _Time[1]     =    curTime/60UL;
  curTime      =    curTime%60UL;
  _Time[2]     =    curTime;

  //if needed, update _Date
  if (_dayRollover) updDate(); //if dayrollover, then adjust date
}


void Time::updDate() {
  const byte maxDays[13] = {0,31,29,31,30,31,30,31,31,30,31,30,31}; //2020 is leap year, next year change num days of February
  const byte day   = 0; //makes code more easy to read
  const byte month = 1;
  const byte year  = 2;
  
  _Date[day]++;
  if (_Date[day] > maxDays[_Date[month]]) {  //if monthrollover then adjust month
    _Date[day]     = 1;
    _Date[month]++;
    if (_Date[month] > 12) {                 //if yearrollover then adjust year
      _Date[month] = 1;
      _Date[year]++;
    }
  }
  _curTime = _curTime - (24UL*60UL*60UL);    //update _curTime (subtract one day)
  _dayRollover = false;
}


unsigned long Time::lastMillis() {
  return _lastMillis;
}


String Time::timeToString(String sign) {
  return toString(_Time, sign);
}


String Time::dateToString(String sign) {
  return toString(_Date, sign);
}


String Time::toString(byte* tostring, String sign) {
  String string = "";
  string += leadingZeros(tostring[0]);
  for (byte i=1; i<3; i++) {
     string+= sign;
     string+= leadingZeros(tostring[i]);
  }
  return string;
}


String Time::leadingZeros(byte value) {
  String string = "";
  if (value < 10) {
    string += "0";
  }
  string += String(value);   
  return string;
}
Time.hC/C++
Header for Time.ccp
/*
 *Time.h - Time library
 *Records starting time and date of project (as given in source code by programmer)
 *Can return String with current Time (hh:mm:ss) 
 *Can return String with current date (dd-mm-yy)
 *Can return millis since start Time
 *Created by Koen Meesters, december 2019
 *Last edited Februari 2020
 */
#ifndef Time_h
#define Time_h

#include "Arduino.h"

class Time {
  public:
    Time(byte hr = 0, byte mnt = 0, byte sec = 0, byte d = 1, byte m = 1, byte y = 20); // date by default initialized to 1-1-2020
    
    String timeToString(String sign = ":"); //returns time in a String, default sign for time is :
    String dateToString(String sign = "-"); //returns date in a String, default sign for date is -

    unsigned long lastMillis(); 
    void updTime();                         //update the time in _Time[3]
    
  private:
    byte _Time[3];                          //stores time in 3 bytes hr min sec
    byte _Date[3];                          //stores date in 3 bytes day month year
    bool _dayRollover;                      //flag set if time goes beyond 23:59:59   

    unsigned long _timeStart;               //start time in seconds
    unsigned long _curTime;                 //current time in seconds
    unsigned long _lastMillis;              //millis() last time updTime() was run 


    void updDate();                         //update the date in _Date[3] (should be run at least every day, otherwise it misses 1 dayrollover)
    
    String toString(byte*, String sign);    //makes a string from _Time[3] or _Date[3]
    String leadingZeros(byte value);        //adds leading zeros if number is smaller than 10 ( 3 --> "03") 
};

#endif

Schematics

Wiring CO2 measurement and logging project
Measures CO2 concentration. Correction for temperature and relative humidity. Writes results to SD card and LCD display
co2_monitor_uMMD8Nb2JF.fzz

Comments

Author

Default
ArduinoKoen
  • 3 projects
  • 1 follower

Additional contributors

  • Slope of mq-135 sensitivity sensor by Mad Frog
  • Calculate relative humidity from wet and dry bulb temperature by 1728 Software system
  • Thermistor reading to temperature conversion by Arduino playground
  • Vapour pressure of water as function of temperature by R.C. Rodgers and G.E. Hill, Brittish Journal of Anaesthesia (1978),50, 415
  • Examples how to use arduino sd card library by Limor Fried and Tom Igoe

Published on

January 21, 2020

Members who respect this project

PhotoDefaultDefaultDefaultDefaultCapture bh71il9k6gDefaultDefault

and 6 others

See similar projects
you might like

Similar projects you might like

Air Quality Monitor Live Display

Project in progress by Parts Oven

  • 3,258 views
  • 0 comments
  • 10 respects

Water Level Monitor

Project tutorial by NewMC

  • 13,521 views
  • 3 comments
  • 21 respects

Temperature Monitor with DHT22 and I2C 16x2 LCD

Project tutorial by adrakhmat

  • 9,592 views
  • 5 comments
  • 27 respects

2-Way Intersection with Pedestrian Walk Cycle

Project showcase by FalcomDigital

  • 5,421 views
  • 2 comments
  • 11 respects

Water Waste Monitor

Project tutorial by MD R. Islam

  • 2,070 views
  • 2 comments
  • 5 respects
Add projectSign up / Login