IoT Project Report [Air Quality Monitoring System With Arduino]
IoT Project Report [Air Quality Monitoring System With Arduino]
A Project Proposal on
Air Quality Monitoring System with Arduino
Course Name & Code: IoT Based Project Development (CSE 342)
Submitted to:
Dr. Mohammad Mahbubur Rahman
Assistant Professor
School of Science Engineering & Technology
East Delta University
Chattogram – 4209, Bangladesh
Submitted by:
1. Abstract 3
2. Introduction 3
3. Objective 3
5. Hardware Implementation 4
6. Software Implementation 7
9. Conclusion 11
10. Reference 12
11. Appendices 12
2
Abstract
This project report documents the design, implementation, and evaluation of a cost-effective, real-
time Air Quality Monitoring System leveraging an Arduino-based sensor network. The system
integrates three MQ-series gas sensors (MQ-7 for CO, MQ-4 for Methane, and MQ-135 for general
air quality) with supplementary environmental data fetched via the OpenWeatherMap API. A
backend built with Node.js and Express aggregates data, computes an aggregate Air Quality Index
(AQI), and interacts with a React-driven dashboard for real-time visualization. Additionally, an
email alert system notifies users based on personalized health conditions.
Introduction
Air quality is a critical environmental parameter directly affecting public health. Traditional air
monitoring systems are often expensive and lack real-time capabilities. With the proliferation of
low-cost sensors and IoT technologies, there is a strong need to design systems that enable
continuous monitoring of air quality both indoors and outdoors. This project is developed as a
multidisciplinary initiative integrating hardware sensor modules with advanced web technologies
to deliver comprehensive, actionable insights into ambient air conditions.
Objectives
• Low-Cost Monitoring: Build a functional hardware prototype using an Arduino Mega and
cost-effective MQ sensors.
• Data Integration: Collect indoor air quality readings and combine them with outdoor
environmental data from the OpenWeatherMap API.
• Real-Time Insights: Calculate an aggregate AQI in real time and display it on both a local
interface and a web-based dashboard.
• User Alerts: Implement an email alert system to notify subscribers with customized health
recommendations based on AQI values.
• Future Prediction: Lay the foundation for predictive analytics using historical data and a
HuggingFace time series transformer model.
3
System Architecture Overview
Hardware Architecture
Software Architecture
• Backend: Node.js/Express server fetching real-time data from both Arduino and
OpenWeatherMap.
• Database: MongoDB used for historical data storage (if needed for analytics).
• Frontend: React-based dashboard for data visualization and user interaction.
• Alert System: Automated email notifications tailored to health conditions.
• Future Prediction Module: A planned integration with a HuggingFace time series
transformer model for AQI forecasting.
Hardware Implementation
Components & Sensors
• Arduino Mega: Chosen for its multiple I/O pins and enhanced memory capacity.
• Sensors:
o MQ-7: Detects CO levels.
4
o MQ-4: Measures methane concentration.
• Other Components:
o USB cable for serial communication.
o Breadboard and standard wiring components.
The sensors are connected to specific analog pins of the Arduino as follows:
Note: Although an OLED display was originally planned for real-time display, the final
implementation transmits data via serial communication over USB.
5
Arduino Sensor Data Acquisition Code
6
float voltage = sensorValue * (5.0 / 1023.0);
float rs = ((5.0 * 10.0) / voltage) - 10.0; // 10K load resistor
// MQ135 is primarily for CO2 and other gases
// Rs/R0 = 1 at 400ppm CO2 in clean air
// For MQ135, a≈400, b≈-2.2
float r0 = 10.0 * MQ135_RATIO_CLEAN_AIR;
return 400.0 * pow(rs / r0, -2.2);
}
// Function to read sensors
void readSensors() {
// Read raw analog values
mq7Value = analogRead(MQ7_PIN);
mq135Value = analogRead(MQ135_PIN);
mq4Value = analogRead(MQ4_PIN);
This snippet highlights the core calculations and JSON data transmission used by the Arduino to
communicate with the server.
Software Implementation
Backend System
The backend system is responsible for two major tasks:
Serial Communication
The server connects to the Arduino via USB serial communication:
// Connect to Arduino port
(async () => {
try {
const arduinoPort = "COM3";
7
console.log(`Attempting to connect to port: ${arduinoPort}`);
arduinoPortInstance = new SerialPort({
path: arduinoPort,
baudRate: 9600,
autoOpen: false,
});
arduinoPortInstance.open((err) => {
if (err) {
console.error("Serial Port Error:", err.message);
return;
}
console.log("Serial port opened successfully");
const parser = arduinoPortInstance.pipe(new ReadlineParser({ delimiter:
"\r\n" }));
parser.on("data", async (rawData) => {
// Process incoming data from Arduino (parsing and forwarding for API
integration)
});
});
} catch (error) {
console.error("Serial Port Init Error:", error.message);
}
})();
API Integration
The system fetches environmental data from OpenWeatherMap, combining it with
sensor data:
The server has RESTful endpoints to serve the dashboard and trigger email notifications
based on preset thresholds.
if (apiKey) {
try {
const [airPollution, weather] = await Promise.all([
axios.get(`https://api.openweathermap.org/data/2.5/air_pollution?lat=22.3569&lon=91.783
2&appid=${apiKey}`),
axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${process.env.CITY_NAME ||
"Chittagong,BD"}&appid=${apiKey}&units=metric`),
]);
const components = airPollution.data.list[0].components;
// Proceed with data processing and storage
} catch (apiError) {
8
console.error("Failed to fetch API data:", apiError.message);
}
}
Data Processing and Storage
The backend combines Arduino data with API data, computes AQI, and stores the record
in MongoDB:
// Create new sensor data entry merging Arduino and API data
const newEntry = new SensorData({
co: parseFloat(sensorData.co) || 0,
methane: parseFloat(sensorData.methane) || 0,
airQuality: parseFloat(sensorData.airQuality) || 0,
temperature: weather.data.main.temp || 0,
humidity: weather.data.main.humidity || 0,
pm25: components.pm2_5 || 0,
pm10: components.pm10 || 0,
o3: components.o3 || 0,
so2: components.so2 || 0,
no2: components.no2 || 0,
nh3: components.nh3 || 0,
aqi: calculateAQI({
pm25: components.pm2_5,
pm10: components.pm10,
o3: components.o3,
co: components.co / 1000, // Approximate conversion from μg/m³ to ppm
so2: components.so2,
no2: components.no2,
nh3: components.nh3,
}),
});
await newEntry.save();
Alert System
A scheduled check uses NodeMailer to send an email alert if air quality thresholds are
exceeded:
// Set up alert check interval (every 2 minutes)
const ALERT_CHECK_INTERVAL = 2 * 60 * 1000;
setInterval(async () => {
try {
const count = await checkAndSendAlerts();
console.log(`Alert check complete: Sent ${count || 0} notifications`);
} catch (error) {
console.error("Alert Check Error:", error);
}
}, ALERT_CHECK_INTERVAL);
9
Frontend Dashboard
The frontend is designed using React to provide users with a dynamic, real-time view of air
quality data. The dashboard fetches data from the backend API periodically and displays:
1. Data Acquisition:
o Sensors on the Arduino read analog signals and send data via serial (USB/COM3).
2. Backend Processing:
o The Node.js server listens to serial port data, fetches supplementary data from the
OpenWeatherMap API, and computes an overall AQI.
3. Data Presentation:
o The REST API serves combined data to the React dashboard.
4. Alert System:
o If the computed AQI exceeds set thresholds, the backend triggers an email alert via
a service (using NodeMailer or similar), sending customized health advice to
subscribed users.
10
Future Enhancements: Air Quality Prediction
As an ambitious extension, the system will incorporate a prediction module using a HuggingFace
time series transformer model. By utilizing the last two years of data (including O₃ and PM2.5
metrics), the model can forecast future air quality trends. This involves:
• Data preprocessing and feature extraction from historical sensor and API data.
• Training a transformer-based model on the time series data.
• Integrating model inference into the backend to display predictions on the dashboard.
The implemented project successfully demonstrates the integration of sensor data with external
environmental parameters to calculate an AQI in real time. Key outcomes include:
Challenges encountered include sensor calibration and ensuring robust error handling in
asynchronous API calls. Future iterations will focus on refining sensor accuracy and optimizing
prediction algorithms.
Conclusion
This project serves as a proof-of-concept for a low-cost, real-time air quality monitoring system.
By bridging hardware sensor data with external environmental information, the system offers
comprehensive air quality insights and proactive health alerts. The integration of a robust MERN
stack and plans for predictive analytics ensure that the solution is both scalable and adaptable to
future requirements in environmental monitoring.
11
References
The complete source code for this project is available on GitHub. The repository includes all the
Arduino sketches, backend and frontend code, as well as configuration files and documentation.
Repository Structure:
• /arduino: Contains the Arduino code including sensor reading and JSON data
transmission.
• /backend: Node.js/Express server code that handles serial communication, API
integration, and data processing.
• /frontend: React dashboard code for real-time data visualization.
• /docs: Additional documentation and project resources.
Appendix
Appendix A: Arduino Code
#include <Wire.h>
// Sensor Pins
const int MQ7_PIN = A0; // CO Sensor
const int MQ135_PIN = A1; // Air Quality Sensor
const int MQ4_PIN = A2; // Methane Sensor
// Variables to store sensor readings
int mq7Value = 0;
int mq135Value = 0;
int mq4Value = 0;
float co_ppm = 0;
float ch4_ppm = 0;
float air_quality_ppm = 0;
// Variables for displaying AQI (will be calculated on server)
int aqi = 0;
String airQualityMessage = "Calculating...";
12
// Sensor calibration values (adjust based on datasheet or calibration)
const float MQ7_RATIO_CLEAN_AIR = 9.83;
const float MQ135_RATIO_CLEAN_AIR = 3.6;
const float MQ4_RATIO_CLEAN_AIR = 4.4;
// Timing variables
unsigned long previousMillis = 0;
const long sensorReadInterval = 2000; // Read sensors every 2 seconds
const long serialTransmitInterval = 5000; // Send to PC every 5 seconds
void setup() {
Serial.begin(9600);
// Sensor warm-up period
delay(30000);
// Initial readings to stabilize
for (int i = 0; i < 10; i++) {
readSensors();
delay(1000);
}
}
void loop() {
unsigned long currentMillis = millis();
// Read sensor values at specified interval
if (currentMillis - previousMillis >= sensorReadInterval) {
previousMillis = currentMillis;
readSensors();
// Send data to PC at specified interval
if (currentMillis % serialTransmitInterval < sensorReadInterval) {
sendDataToPC();
}
}
// Check if there's data from the server (for AQI feedback)
receiveFromServer();
}
// Function to calculate CO (MQ7)
float calculateCOppm(int sensorValue) {
float voltage = sensorValue * (5.0 / 1023.0);
float rs = ((5.0 * 10.0) / voltage) - 10.0; // 10K load resistor
float r0 = 10.0 * MQ7_RATIO_CLEAN_AIR;
return 100.0 * pow(rs / r0, -1.5);
}
// Function to calculate Methane/CH4 (MQ4)
float calculateCH4ppm(int sensorValue) {
float voltage = sensorValue * (5.0 / 1023.0);
float rs = ((5.0 * 10.0) / voltage) - 10.0;
float r0 = 10.0 * MQ4_RATIO_CLEAN_AIR;
13
return 1000.0 * pow(rs / r0, -2.95);
}
// Function to calculate air quality (MQ135)
float calculateAirQualityppm(int sensorValue) {
float voltage = sensorValue * (5.0 / 1023.0);
float rs = ((5.0 * 10.0) / voltage) - 10.0;
float r0 = 10.0 * MQ135_RATIO_CLEAN_AIR;
return 400.0 * pow(rs / r0, -2.2);
}
void readSensors() { // Function to read sensors
mq7Value = analogRead(MQ7_PIN);
mq135Value = analogRead(MQ135_PIN);
mq4Value = analogRead(MQ4_PIN);
co_ppm = calculateCOppm(mq7Value);
ch4_ppm = calculateCH4ppm(mq4Value);
air_quality_ppm = calculateAirQualityppm(mq135Value);
co_ppm = constrain(co_ppm, 0.1, 1000.0);
ch4_ppm = constrain(ch4_ppm, 500.0, 10000.0);
air_quality_ppm = constrain(air_quality_ppm, 400.0, 5000.0);
}
void sendDataToPC() { // Function to send data to PC/server
Serial.print(F("{\"co\":"));
Serial.print(co_ppm, 1);
Serial.print(F(",\"methane\":"));
Serial.print(ch4_ppm, 1);
Serial.print(F(",\"airQuality\":"));
Serial.print(air_quality_ppm, 1);
float estimated_pm25 = air_quality_ppm * 0.3;
float estimated_pm10 = air_quality_ppm * 0.5;
Serial.print(F(",\"pm25\":"));
Serial.print(estimated_pm25, 1);
Serial.print(F(",\"pm10\":"));
Serial.print(estimated_pm10, 1);
Serial.println(F("}"));
}
void receiveFromServer() { // Function to receive and parse data from server
if (Serial.available() > 0) {
String data = Serial.readStringUntil('\n');
if (data.startsWith("{") && data.indexOf("aqi") > 0) {
int aqiStart = data.indexOf("aqi") + 5;
int aqiEnd = data.indexOf(",", aqiStart);
if (aqiEnd < 0) aqiEnd = data.indexOf("}", aqiStart);
String aqiStr = data.substring(aqiStart, aqiEnd);
aqi = aqiStr.toInt();
int statusStart = data.indexOf("status") + 9;
14
if (statusStart > 9) {
int statusEnd = data.indexOf("\"", statusStart);
if (statusEnd > statusStart) {
airQualityMessage = data.substring(statusStart, statusEnd);
}
}
}
}
}
Appendix B: Backend Server-Side Code
import express from "express";
import dotenv from "dotenv";
import cors from "cors";
import connectDB from "./config/db.js";
import sensorRoutes from "./routes/sensorRoutes.js";
import weatherRoutes from "./routes/weatherRoutes.js";
import emailRoutes from "./routes/emailRoutes.js";
import predictionRoutes from "./routes/predictionRoutes.js";
import { SerialPort } from "serialport";
import { ReadlineParser } from "@serialport/parser-readline";
import SensorData from "./models/SensorData.js";
import { checkAndSendAlerts } from "./controllers/emailController.js";
import axios from "axios";
import { calculateAQI } from "./utils/aqiCalculator.js";
dotenv.config();
const app = express();
app.use(express.json());
app.use(cors({ origin: "http://localhost:8080" }));
connectDB();
app.use("/api/sensors", sensorRoutes);
app.use("/api/weather", weatherRoutes);
app.use("/api/alerts", emailRoutes);
app.use("/api/predictions", predictionRoutes);
app.get("/", (req, res) => res.send("API is running..."));
let arduinoPortInstance = null;
(async () => {
try {
const arduinoPort = "COM3";
console.log(`Connecting to Arduino at: ${arduinoPort}`);
arduinoPortInstance = new SerialPort({
path: arduinoPort,
baudRate: 9600,
autoOpen: false,
});
arduinoPortInstance.open((err) => {
15
if (err) {
console.error("Serial Port Error:", err.message);
return;
}
console.log("Serial port opened");
const parser = arduinoPortInstance.pipe(new ReadlineParser({ delimiter: "\r\n" }));
parser.on("data", async (rawData) => {
try {
const trimmedData = rawData.trim();
if (!trimmedData) return;
16
});
await newEntry.save();
console.log("Saved data to DB");
checkAndSendAlerts();
} catch {
await saveArduinoOnly(sensorData);
}
} else {
await saveArduinoOnly(sensorData);
}
} catch (error) {
console.error("Data Handling Error:", error.message);
}
});
arduinoPortInstance.on("error", (err) => {
console.error("Serial Port Error:", err.message);
});
arduinoPortInstance.on("close", () => {
console.log("Serial port closed. Reconnecting...");
setTimeout(() => arduinoPortInstance.open(), 5000);
} catch (error) {
console.error("Serial Init Error:", error.message);
}
})();
await newEntry.save();
console.log("Saved Arduino-only data");
checkAndSendAlerts();
17
}
(async () => {
try {
await checkAndSendAlerts();
} catch (error) {
console.error("Initial Alert Check Error:", error);
}
})();
18
Appendix C: Website Screenshot
19