Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 2 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 |
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:
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!
#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.cpp
C/C++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
}
#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);
#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.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
Comments