// Factory Environment Monitoring System
// Written by Jun Zhi Wong at Asia Pacific University for Assignment Purposes
// This code is designed to run on Arduino Mega2560, however it should run on other micro-controllers that support Arduino IDE
// Miscellaneous Libraries
#include <SPI.h>
#include <Wire.h>
#include <Arduino.h>
#include <Ewma.h> // used for weighted average
#include <stdio.h>
#include <string.h>
#include <uRTCLib.h> // used for RTC
// Include libraries for sensor
#include <MQUnifiedsensor.h> // used for reading MQ sensors properly
#include <DHT.h> // used for reading DHT sensor
#include <DHT_U.h> // used for reading DHT sensor
#include <Adafruit_AHTX0.h> // used for reading AHT20 sensor
#include <Adafruit_BMP280.h> // used for reading BMP280 sensor
// Include libraries for user inputs/displays/outputs
#include <Adafruit_GFX.h> // used for SSD1306 OLED display
#include <Adafruit_SSD1306.h> // used for SSD1306 OLED display
#include <SD.h> // used for SD card
#include <Keypad.h>
// Microcontroller ADC Specification
#define Voltage_Resolution 5
#define ADC_Bit_Resolution 10
// Port and Sensor Definitions
#define S_MQ2 A0
#define S_MQ3 A1
#define S_MQ7 A2
#define S_MQ8 A3
#define S_MQ9 A4
#define S_MQ135 A5
#define S_DHT11 44
#define S_Flame 42
#define S_Proximity 40
Adafruit_AHTX0 aht;
Adafruit_BMP280 bmp; // use I2C interface
Adafruit_Sensor *bmp_temp = bmp.getTemperatureSensor();
Adafruit_Sensor *bmp_pressure = bmp.getPressureSensor();
#define buzzer 8
// Precalibrated clean air PPM value of each MQ series sensor
#define RatioMQ2CleanAir 8.34 // Clean air = 8.34 ppm
#define RatioMQ3CleanAir 1.10 // 1.10 ppm
#define RatioMQ7CleanAir 2.45 // 2.45 ppm
#define RatioMQ8CleanAir 1.27 // 1.27 ppm
#define RatioMQ9CleanAir 8.34 // 8.34 ppm
#define RatioMQ135CleanAir 1.10 // 1.10 ppm
#define DHTTYPE DHT11
#define placa "adc"
// Declare Sensor Objects
MQUnifiedsensor MQ2(placa, Voltage_Resolution, ADC_Bit_Resolution, S_MQ2, "MQ-2");
MQUnifiedsensor MQ3(placa, Voltage_Resolution, ADC_Bit_Resolution, S_MQ3, "MQ-3");
MQUnifiedsensor MQ7(placa, Voltage_Resolution, ADC_Bit_Resolution, S_MQ7, "MQ-7");
MQUnifiedsensor MQ8(placa, Voltage_Resolution, ADC_Bit_Resolution, S_MQ8, "MQ-8");
MQUnifiedsensor MQ9(placa, Voltage_Resolution, ADC_Bit_Resolution, S_MQ9, "MQ-9");
MQUnifiedsensor MQ135(placa, Voltage_Resolution, ADC_Bit_Resolution, S_MQ135, "MQ-135");
DHT_Unified dht(S_DHT11, DHTTYPE);
// Keypad Definitions
const char ROW = 4; //four rows
const char COL = 4; //four columns
char keys[ROW][COL] = {
{ 1, 2, 3, 10 },
{ 4, 5, 6, 11 },
{ 7, 8, 9, 12 },
{ 14, 0, 15, 13 }
};
byte rowPins[ROW] = { 22, 24, 26, 28 }; //connect to the row pinouts of the keypad
byte colPins[COL] = { 23, 25, 27, 29 }; //connect to the column pinouts of the keypad
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROW, COL);
// OLED Definitions
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C // Default 128x64 OLED screen I2C Address
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Miscellaneous Definitions
int counter = 0;
char showState, tempShowState;
File txtFile;
char filename[13];
char sensorValuesForWrite[11], RFID[10]; // temporary values before concatenating into output string
char outputString[120]; // string output for logging or cloud platform
uRTCLib rtc(0x68);
char Time[25]; // used for calibrating RTC
int TimeArray[7]; // used for calibrating RTC
// Sensor Data
float MQ2Val, MQ3Val, MQ7Val, MQ8Val, MQ9Val, MQ135Val, tempAverage, humidAverage = 0;
sensors_event_t dht11Temp, dht11Humid, aht20Temp, aht20Humid, bmp280Temp, bmp280Pressure;
unsigned long sensorDelay, sendDelay, displayDelay = 0; // time elapsed for sensor sensing and POST to cloud
Ewma MQ2Average(0.01); // Define weight for weighted average, 0.01 means new values have 1% significance
Ewma MQ3Average(0.01);
Ewma MQ7Average(0.01);
Ewma MQ8Average(0.01);
Ewma MQ9Average(0.01);
Ewma MQ135Average(0.01);
Ewma temperatureAverage(0.01);
Ewma humidityAverage(0.01);
Ewma airPressureAverage(0.01);
char hazardousEnvironment[11]; // array for storing hazardous environment data
void keypadEvent() { // executes when keypad is pressed
switch (keypad.getState()) {
case PRESSED:
showState = keypad.getKey();
break;
case HOLD:
showState = keypad.getKey();
break;
}
}
void readUID() { // read UID from security system
if ((Serial3.available() > 0)) {
Serial3.readBytesUntil('\n', RFID, 10);
Serial3.flush();
}
}
void readSensor() {
MQ2.update();
MQ3.update();
MQ7.update();
MQ8.update();
MQ9.update();
MQ135.update();
MQ2Val = MQ2.readSensor();
MQ3Val = MQ3.readSensor();
MQ7Val = MQ7.readSensor();
MQ8Val = MQ8.readSensor();
MQ9Val = MQ9.readSensor();
MQ135Val = MQ135.readSensor();
dht.temperature().getEvent(&dht11Temp);
dht.humidity().getEvent(&dht11Humid);
aht.getEvent(&aht20Humid, &aht20Temp);
bmp_temp->getEvent(&bmp280Temp);
bmp_pressure->getEvent(&bmp280Pressure);
tempAverage = (dht11Temp.temperature + aht20Temp.temperature + bmp280Temp.temperature) / 3;
humidAverage = (dht11Humid.relative_humidity + aht20Humid.relative_humidity) / 2;
}
void writeToFile() {
// add a new line to the buffer
rtc.refresh();
memset(outputString, 0, sizeof(outputString));
// Format the date and time into the string
sprintf(Time, "%02d%02d%02d%02d%02d%04d", rtc.hour(), rtc.minute(), rtc.second(), rtc.day(), rtc.month() + 1, rtc.year() + 1900);
strcpy(outputString, Time);
dtostrf(MQ2Val, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(MQ3Val, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(MQ7Val, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(MQ8Val, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(MQ9Val, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(MQ135Val, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(tempAverage, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(humidAverage, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(bmp280Pressure.pressure, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
strncat(outputString, "|", 1);
strncat(outputString, RFID, 9);
strncat(outputString, "|", 1);
strncat(outputString, hazardousEnvironment, 10);
strncat(outputString, "\0", 1);
txtFile.println(outputString);
txtFile.flush();
}
void oledDisplay(unsigned long now) {
display.clearDisplay();
display.setCursor(0, 0);
display.setTextSize(1);
display.setTextColor(WHITE);
switch (showState) {
case '1':
{
display.setTextSize(2);
display.println("MQ2(LPG): ");
display.print(MQ2Average.output);
display.println(" PPM");
break;
}
case '2':
{
display.setTextSize(2);
display.println("MQ3(C2H60): ");
display.print(MQ3Average.output);
display.println(" PPM");
break;
}
case '3':
{
display.setTextSize(2);
display.println("MQ7(H2): ");
display.print(MQ7Average.output);
display.println(" PPM");
break;
}
case '4':
{
display.setTextSize(2);
display.println("MQ8(H2): ");
display.print(MQ8Average.output);
display.println(" PPM");
break;
}
case '5':
{
display.setTextSize(2);
display.println("MQ9(LPG): ");
display.print(MQ9Average.output);
display.println(" PPM");
break;
}
case '6':
{
display.setTextSize(2);
display.println("MQ135(C2H60): ");
display.print(MQ135Average.output);
display.println(" PPM");
break;
}
case '7':
{
display.setTextSize(1);
display.print("DHT11(°C): ");
display.println(dht11Temp.temperature);
display.print("DHT11(RH%): ");
display.println(dht11Humid.relative_humidity);
break;
}
case '8':
{
display.setTextSize(1);
display.print("AHT20(°C): ");
display.println(aht20Temp.temperature);
display.print("AHT20(RH%): ");
display.println(aht20Humid.relative_humidity);
break;
}
case '9':
{
display.setTextSize(1);
display.print("BMP280(°C): ");
display.println(bmp280Temp.temperature);
display.print("BMP280(atm): ");
display.println(bmp280Pressure.pressure);
break;
}
default:
{
if ((now - displayDelay) >= 2000) {
display.print("MQ2(LPG): ");
display.println(MQ2Average.output);
display.print("MQ3(C2H60): ");
display.println(MQ3Average.output);
display.print("MQ7(H2): ");
display.println(MQ7Average.output);
display.print("MQ8(H2): ");
display.println(MQ8Average.output);
displayDelay = now;
}
else if ((now - displayDelay) >= 1000) {
display.print("MQ9(LPG): ");
display.println(MQ9Average.output);
display.print("MQ135(C2H60): ");
display.println(MQ135Average.output);
display.print("Temperature: ");
display.println(temperatureAverage.output);
display.print("Humidity: ");
display.println(humidityAverage.output);
display.print("Air Pressure: ");
display.println(airPressureAverage.output);
}
break;
}
}
display.display();
}
void transferToBlynk() {
// Calculate Average
MQ2Average.filter(MQ2Val);
MQ3Average.filter(MQ3Val);
MQ7Average.filter(MQ7Val);
MQ8Average.filter(MQ8Val);
MQ9Average.filter(MQ9Val);
MQ135Average.filter(MQ135Val);
temperatureAverage.filter(tempAverage);
humidityAverage.filter(humidAverage);
airPressureAverage.filter(bmp280Pressure.pressure);
rtc.refresh();
memset(outputString, 0, sizeof(outputString));
// Format the date and time into the string
sprintf(Time, "%02d%02d%02d%02d%02d%04d", rtc.hour(), rtc.minute(), rtc.second(), rtc.day(), rtc.month() + 1, rtc.year() + 1900);
strcpy(outputString, Time);
dtostrf(MQ2Average.output, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(MQ3Average.output, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(MQ7Average.output, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(MQ8Average.output, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(MQ9Average.output, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(MQ135Average.output, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(temperatureAverage.output, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(humidityAverage.output, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
dtostrf(airPressureAverage.output, 8, 2, sensorValuesForWrite);
strncat(outputString, "|", 1);
strncat(outputString, sensorValuesForWrite, strlen(sensorValuesForWrite));
strncat(outputString, "|", 1);
strncat(outputString, RFID, 9);
strncat(outputString, "|", 1);
strncat(outputString, hazardousEnvironment, 10);
strncat(outputString, "\0", 1);
Serial.println(outputString);
memset(RFID, 48, 10);
//Increment counter
counter++;
if (counter > 1000) { // This part is here to prevent overflow
MQ2Average.reset();
MQ3Average.reset();
MQ7Average.reset();
MQ8Average.reset();
MQ9Average.reset();
MQ135Average.reset();
counter = 0;
}
}
void getInternetTime() {
// Default RTC address for DS3231
URTCLIB_WIRE.begin();
rtc.set_model(URTCLIB_MODEL_DS3231);
while (1) {
if (Serial.available() > 0) {
TimeArray[6] = 0;
Serial.readBytesUntil('\n', Time, 24); // Read internet time from ESP32
char *token = strtok(Time, "|");
int i = 0;
// Split String
while (token != NULL) {
TimeArray[i] = atoi(token);
i++;
token = strtok(NULL, "|");
}
rtc.set(4, 11, 3, 3, 13, 11, 123);
// Check if the values are valid, only proceed if valid (limit is year 2100)
if (TimeArray[6] > 0 && TimeArray[6] < 200) {
sprintf(filename, "%d%d%d.txt", 13, 11, (123 + 1900));
}
break;
}
}
}
void initSensors() {
//Set math model to calculate the PPM concentration and the value of constants for MQ series sensor
MQ2.setRegressionMethod(1); //_PPM = a*ratio^b
MQ3.setRegressionMethod(1);
MQ7.setRegressionMethod(1);
MQ8.setRegressionMethod(1);
MQ9.setRegressionMethod(1);
MQ135.setRegressionMethod(1);
// Configure the equation to calculate pollutant PPM
MQ2.setA(574.25);
MQ2.setB(-2.222);
MQ3.setA(0.3934);
MQ3.setB(-1.504);
MQ7.setA(69.014);
MQ7.setB(-1.374);
MQ8.setA(976.97);
MQ8.setB(-0.688);
MQ9.setA(1000.5);
MQ9.setB(-2.186);
MQ135.setA(77.255);
MQ135.setB(-3.18); // Configure the equation to to calculate LPG concentration
// Init Sensors
MQ2.init();
MQ3.init();
MQ7.init();
MQ8.init();
MQ9.init();
MQ135.init();
dht.begin();
aht.begin();
bmp.begin();
}
void MQCalibrate() { // Calibrates the sensor to match required base PPM using precalibrated data above
// MQ2
float calcR0 = 0;
for (int i = 1; i <= 10; i++) {
MQ2.update(); // Update data, the arduino will read the voltage from the analog pin
calcR0 += MQ2.calibrate(RatioMQ2CleanAir);
}
MQ2.setR0(calcR0 / 10);
// MQ3
for (int i = 1; i <= 10; i++) {
MQ3.update(); // Update data, the arduino will read the voltage from the analog pin
calcR0 += MQ3.calibrate(RatioMQ3CleanAir);
}
MQ3.setR0(calcR0 / 10);
// MQ7
for (int i = 1; i <= 10; i++) {
MQ7.update(); // Update data, the arduino will read the voltage from the analog pin
calcR0 += MQ7.calibrate(RatioMQ7CleanAir);
}
MQ7.setR0(calcR0 / 10);
// MQ8
for (int i = 1; i <= 10; i++) {
MQ8.update(); // Update data, the arduino will read the voltage from the analog pin
calcR0 += MQ8.calibrate(RatioMQ8CleanAir);
}
MQ8.setR0(calcR0 / 10);
// MQ9
for (int i = 1; i <= 10; i++) {
MQ9.update(); // Update data, the arduino will read the voltage from the analog pin
calcR0 += MQ9.calibrate(RatioMQ9CleanAir);
}
MQ9.setR0(calcR0 / 10);
// MQ135
for (int i = 1; i <= 10; i++) {
MQ135.update(); // Update data, the arduino will read the voltage from the analog pin
calcR0 += MQ135.calibrate(RatioMQ135CleanAir);
}
MQ135.setR0(calcR0 / 10);
}
void setup() {
pinMode(S_Flame, INPUT);
pinMode(S_Proximity, INPUT);
//Init the serial port communication, OLED screen and some variables
Serial.begin(115200); //Init serial port
Serial3.begin(115200);
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
display.clearDisplay();
memset(RFID, 48, 10);
memset(hazardousEnvironment, 48, 10);
// Initialise sensors and calibration
initSensors();
MQCalibrate();
// Wait for ESP32 connect to internet and get time
delay(5000);
getInternetTime();
if (!SD.begin()) {
Serial.println("Card failed, or not present");
// Don't do anything more
while (1) {
tone(buzzer, 2000);
}
}
txtFile = SD.open(filename, FILE_WRITE);
if (!txtFile) {
Serial.print("error opening ");
// Don't do anything more
while (1) {
tone(buzzer, 2000);
}
}
// Interrupts
keypad.addEventListener(keypadEvent); // Add an event listener for this keypad
delay(1000);
}
void loop() {
if ((Serial3.available() > 0)) { // Only read if something is available
readUID();
}
if (digitalRead(S_Flame) == LOW) { // Check for fire
hazardousEnvironment[7] = '1';
Serial.println("Fire");
tone(buzzer, 1000);
} else if (digitalRead(S_Proximity) == HIGH) { // Check for obstacles on top of sensors
Serial.println("Movement");
tone(buzzer, 1000);
} else {
hazardousEnvironment[7] = '0';
noTone(buzzer);
}
unsigned long now = millis();
if ((now - sendDelay) >= 1000) { // Referesh display and POST data to cloud every second
oledDisplay(now);
transferToBlynk();
sendDelay = now;
}
if ((now - sensorDelay) >= 250) { // Log sensor data every 250ms or 0.25s
readSensor();
writeToFile();
sensorDelay = now;
}
}
/*
Pin Table
MQ2 - A0
MQ3 - A1
MQ7 - A2
MQ8 - A3
MQ9 - A4
MQ135 - A5
DHT11 - D31
AHT20+BMP280 - I2C
Flame Sensor - D30
Buzzer - 8
OLED - I2C
SD Card - MOSI MISO
CLK: 51
CS: 52
Keypad - R: D22 D24 D26 D28
C: D23 D25 D27 D29
Interrupt: D33
ESP32 Connection - TX RX
******************************************** MQ Series Sensor Regression Table ********************************************
----------------- MQ2 -----------------
Most sensitive to LPG, Propane and Hydrogen
Gas | a | b
H2 | 987.99 | -2.162
LPG | 574.25 | -2.222
CO | 36974 | -3.109
Alcohol| 3616.1 | -2.675
Propane| 658.71 | -2.168
----------------- MQ3 -----------------
Most sensitive to Alcohol and Benzene
Gas | a | b
LPG | 44771 | -3.245
CH4 | 2*10^31| 19.01
CO | 521853 | -3.821
Alcohol| 0.3934 | -1.504
Benzene| 4.8387 | -2.68
Hexane | 7585.3 | -2.849
----------------- MQ7 -----------------
Most sensitive to Carbon Monoxide
GAS | a | b
H2 | 69.014 | -1.374
LPG | 700000000 | -7.703
CH4 | 60000000000000 | -10.54
CO | 99.042 | -1.518
Alcohol | 40000000000000000 | -12.35
----------------- MQ8 -----------------
Most sensitive to Hydrogen, Alcohol, and LPG are considered as noise
GAS | a | b
H2 | 976.97 | -0.688
LPG | 10000000 | -3.123
CH4 | 80000000000000 | -6.666
CO | 2000000000000000000 | -8.074
Alcohol | 76101 | -1.86
----------------- MQ9 -----------------
Most sensitive to Carbon Monoxide, Methane and Propane
GAS | a | b
LPG | 1000.5 | -2.186
CH4 | 4269.6 | -2.648
CO | 599.65 | -2.244
---------------- MQ135 ----------------
Most sensitive to NH3, Nitrogen Oxides, Alcohol, Benzene, smoke and CO2
GAS | a | b
CO | 605.18 | -3.937
Alcohol | 77.255 | -3.18
CO2 | 110.47 | -2.862
Toluen | 44.947 | -3.445
NH4 | 102.2 | -2.473
Aceton | 34.668 | -3.369
---------------------------------------
---------------------------------------
*/