Autor: Burciu Iustin Florian Email: iustin.burciu@stud.fiir.upb.ro Grupa: AAC
This project focuses on building an embedded system to monitor environmental comfort using the ESP32 development board, along with an LM35 temperature sensor and an MQ-4 gas sensor. It continuously evaluates environmental conditions and computes a comfort score based on temperature and gas readings.
The implementation was initially created as a local web server hosted directly on the ESP32, allowing real-time monitoring from any browser within the same Wi-Fi network. Later, a second version was developed using Firebase Realtime Database, enabling remote data access and logging in real time from anywhere in the world.
The project includes:
Used Components:
ESP32 Connections:
Component | ESP32 Pin | Description |
---|---|---|
LM35 VCC | 3.3V | Power supply |
LM35 GND | GND | Ground |
LM35 OUT | GPIO34 | Temperature signal |
MQ-4 VCC | VIN | Power supply (regulated) |
MQ-4 GND | GND | Ground |
MQ-4 A0 | GPIO32 | Gas level (analog) |
LED Blue | GPIO25 | Lights up at comfort score 100 |
LED Red | GPIO26 | Lights up if gas > 30 |
The designed system aims to monitor indoor environmental comfort by evaluating two essential parameters: temperature and gas concentration. These values are processed to calculate a comfort score ranging from 0 to 100, which reflects the overall air quality and thermal comfort.
The core functionalities of the system include:
This system provides continuous assessment of the indoor environment, with a user-friendly interface that enables real-time monitoring and alerts, both locally and remotely via cloud services.
The implementation of this project was carried out in two main phases, using two distinct Arduino sketches for flexibility and modularity.
In the first implementation, a local web server is hosted directly on the ESP32 microcontroller. This server delivers an interactive dashboard accessible via any device connected to the same Wi-Fi network. Key elements include:
This implementation is self-contained and requires no internet connection beyond local Wi-Fi access.
To enable remote monitoring and cloud-based data storage, a second Arduino sketch was developed using the Firebase ESP Client library. In this version:
Each sketch controls the same hardware setup (sensors and LEDs) but uses different methods to present and store the data. This modular design ensures flexibility and adaptability for different deployment scenarios (local vs. cloud).
The ESP32 board runs two separate Arduino sketches, each responsible for different functionalities: one for the local server interface and another for Firebase integration.
This sketch creates a basic HTTP server on the ESP32, responding with both raw sensor data (`/data` endpoint) and a full HTML dashboard. The key responsibilities include:
Code
#include <WiFi.h>
const int pinLM35 = 34; const int pinMQ4 = 32;
const char* ssid = “myWifi”; const char* password = ””;
WiFiServer server(80);
float citesteTemperatura() {
int adc = analogRead(pinLM35); float voltaj = adc * (5 / 4095.0); return voltaj * 100.0;
}
int citesteGaz() {
return analogRead(pinMQ4);
}
int calculeazaScor(float temp, int gaz) {
int scor = 100; if (temp < 19 || temp > 25) scor -= 30; if (temp < 16 || temp > 28) scor -= 30; if (gaz > 30) scor -= 10; if (gaz > 50) scor -= 20; if (gaz > 100) scor -= 30; return max(scor, 0);
}
void setup() {
Serial.begin(115200); WiFi.begin(ssid, password); Serial.print("Conectare la WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nConectat!"); Serial.println(WiFi.localIP()); server.begin(); pinMode(25, OUTPUT); // LED albastru - scor 100 pinMode(26, OUTPUT); // LED rosu - gaz > 30
}
void loop() {
WiFiClient client = server.available(); if (client) { String cerere = client.readStringUntil('\r'); client.read();
if (cerere.indexOf("/data") >= 0) { float temp = citesteTemperatura(); int gaz = citesteGaz(); int scor = calculeazaScor(temp, gaz); String raspuns = String(temp, 1) + ";" + String(gaz) + ";" + String(scor); // LED rosu - gaz periculos digitalWrite(26, (gaz > 30) ? HIGH : LOW); // LED albastru - scor maxim digitalWrite(25, (scor >= 100) ? HIGH : LOW); client.println("HTTP/1.1 200 OK"); client.println("Content-Type: text/plain"); client.println("Connection: close"); client.println(); client.println(raspuns); } else { String pagina = R"rawliteral( <!DOCTYPE html><html> <head> <title>Monitor de evaluare al confortului ambiental</title> <style> body { font-family: Arial, sans-serif; background: #f0f0f0; text-align: center; padding: 20px; } .container { background: #fff; border-radius: 12px; padding: 20px; margin: auto; width: 90%; max-width: 700px; box-shadow: 0 0 10px rgba(0,0,0,0.2); } h2 { color: #0077cc; } p { font-size: 18px; margin: 10px 0; } span { font-weight: bold; } canvas { margin: 20px 0; border: 1px solid #ccc; } #alerta { color: red; font-weight: bold; } ul { padding-left: 0; list-style: none; } li { font-size: 14px; margin: 5px 0; } .led-box { display: flex; justify-content: center; gap: 30px; margin-top: 10px; } .led { width: 25px; height: 25px; border-radius: 50%; background: gray; margin: auto; } </style> <script> let tempData = [], gazData = [], labels = []; let ultimaAlerta = "";
function updateData() { fetch('/data').then(r => r.text()).then(d => { let [t, g, s] = d.trim().split(';'); document.getElementById("temp").innerText = t + " \u00B0C"; document.getElementById("gaz").innerText = g; document.getElementById("scor").innerText = s + " / 100";
const timestamp = new Date().toLocaleTimeString(); const alertaCurenta = g + "-" + t + "-" + s;
// LED virtual gaz document.getElementById("ledGaz").style.background = (parseInt(g) > 30) ? "red" : "gray"; // LED virtual scor document.getElementById("ledScor").style.background = (parseInt(s) === 100) ? "blue" : "gray";
// Alerta gaz if (parseInt(g) > 30) { document.getElementById("alerta").innerText = "ATENTIE: gazul a depasit 30, iar calitatea aerului a scazut!"; if (alertaCurenta !== ultimaAlerta) { ultimaAlerta = alertaCurenta; const li = document.createElement("li"); li.textContent = timestamp + " - Gaz: " + g + ", Temp: " + t + " °C, Scor: " + s + "/100"; li.style.color = "red"; document.getElementById("istoric").appendChild(li); } } else if (parseInt(s) === 100 && alertaCurenta !== ultimaAlerta) { ultimaAlerta = alertaCurenta; const li = document.createElement("li"); li.textContent = timestamp + " - Scorul de confort a atins valoarea maxima (100)"; li.style.color = "blue"; document.getElementById("istoric").appendChild(li); document.getElementById("alerta").innerText = ""; } else { document.getElementById("alerta").innerText = ""; }
let ts = new Date().toLocaleTimeString(); if (labels.length >= 20) { labels.shift(); tempData.shift(); gazData.shift(); } labels.push(ts); tempData.push(parseFloat(t)); gazData.push(parseInt(g));
drawGraph("tempChart", labels, tempData, "Temperatura", "\u00B0C"); drawGraph("gazChart", labels, gazData, "Nivel Gaz", "ppm"); }); }
function drawGraph(id, labels, data, title, unit) { const canvas = document.getElementById(id); const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath(); ctx.moveTo(40, 10); ctx.lineTo(40, 190); ctx.lineTo(490, 190); ctx.strokeStyle = "#aaa"; ctx.stroke();
ctx.font = "14px Arial"; ctx.fillStyle = "#000"; ctx.fillText(title, 200, 20);
const max = Math.max(...data) * 1.1 || 1; ctx.fillText(max.toFixed(1) + " " + unit, 5, 20); ctx.fillText("0 " + unit, 10, 190);
ctx.beginPath(); ctx.strokeStyle = "#0077cc"; data.forEach((val, i) => { const x = 40 + (i * 22); const y = 190 - (val / max) * 160; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke();
ctx.fillStyle = "#0077cc"; ctx.font = "12px Arial"; data.forEach((val, i) => { const x = 40 + (i * 22); const y = 190 - (val / max) * 160; ctx.beginPath(); ctx.arc(x, y, 3, 0, 2 * Math.PI); ctx.fill(); ctx.fillText(val.toFixed(1), x - 10, y - 10); }); }
setInterval(updateData, 2000); </script> </head> <body> <div class="container"> <h2>Monitorizare Mediu</h2> <p>Temperatura: <span id="temp">--</span></p> <p>Gaz: <span id="gaz">--</span></p> <p>Scor Confort: <span id="scor">--</span></p>
<!-- LED-uri virtuale --> <div class="led-box"> <div> <div id="ledGaz" class="led"></div> <p>Gaz Periculos</p> </div> <div> <div id="ledScor" class="led"></div> <p>Scor Maxim</p> </div> </div>
<p id="alerta"></p> <canvas id="tempChart" width="500" height="200"></canvas> <canvas id="gazChart" width="500" height="200"></canvas> <div style="margin-top:20px; text-align:left;"> <h3>Istoric Evenimente</h3> <ul id="istoric"></ul> </div> </div> </body></html>
)rawliteral”;
client.println("HTTP/1.1 200 OK"); client.println("Content-Type: text/html; charset=UTF-8"); client.println("Connection: close"); client.println(); client.print(pagina); } delay(1); client.stop(); }
}
A second Arduino sketch was created to send environmental data to a Firebase Realtime Database, allowing remote access and monitoring from any internet-connected device.
This implementation uses the Firebase ESP Client library to authenticate, connect, and write sensor data securely to the Firebase database.
Main responsibilities:
Firebase Database Structure:
#include <WiFi.h> #include <Firebase_ESP_Client.h> #include “addons/TokenHelper.h” #include “addons/RTDBHelper.h”
WiFi const char* ssid = “mywifi”; const char* password = ””; Firebase
#define API_KEY “AIzaSyCpb_wClxGJonhdSk6B3dnMPWU4KgGPsRE”
#define DATABASE_URL “https://esp32-monitor-d753c-default-rtdb.europe-west1.firebasedatabase.app/”
#define USER_EMAIL “esp32@test.com”
#define USER_PASSWORD “parola123”
FirebaseData fbdo; FirebaseAuth auth; FirebaseConfig config;
unsigned long lastSend = 0;
const int pinLM35 = 34; const int pinMQ4 = 32;
float citesteTemperatura() {
int adc = analogRead(pinLM35); float voltaj = adc * (5.0 / 4095.0); return voltaj * 100.0;
}
int citesteGaz() {
return analogRead(pinMQ4);
}
int calculeazaScor(float temp, int gaz) {
int scor = 100; if (temp < 19 || temp > 25) scor -= 30; if (temp < 16 || temp > 28) scor -= 30; if (gaz > 30) scor -= 10; if (gaz > 50) scor -= 20; if (gaz > 100) scor -= 30; return max(scor, 0);
}
void setup() {
Serial.begin(115200); WiFi.begin(ssid, password); Serial.print("Conectare la WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi conectat");
// Configurare Firebase config.api_key = API_KEY; auth.user.email = USER_EMAIL; auth.user.password = USER_PASSWORD; config.database_url = DATABASE_URL;
Firebase.begin(&config, &auth); Firebase.reconnectWiFi(true);
pinMode(25, OUTPUT); // LED albastru - scor 100 pinMode(26, OUTPUT); // LED rosu - gaz > 30
}
void loop() {
if (Firebase.ready() && (millis() - lastSend > 2000)) { lastSend = millis();
float temperatura = citesteTemperatura(); int gaz = citesteGaz(); int scor = calculeazaScor(temperatura, gaz);
// Trimitere catre Firebase Firebase.RTDB.setFloat(&fbdo, "/monitor/temperatura", temperatura); Firebase.RTDB.setInt(&fbdo, "/monitor/gaz", gaz); Firebase.RTDB.setInt(&fbdo, "/monitor/scor", scor);
// Control LED-uri digitalWrite(26, (gaz > 30) ? HIGH : LOW); digitalWrite(25, (scor >= 100) ? HIGH : LOW);
Serial.printf("Trimis: Temp=%.2f, Gaz=%d, Scor=%d\n", temperatura, gaz, scor); }
}
During the development of the Environmental Comfort Monitoring System, several challenges were encountered:
The MQ-4 gas sensor operates at 5V, while the ESP32’s GPIO pins are not 5V tolerant. Careful wiring and understanding of voltage levels was essential to avoid damaging the board.
When displaying special characters like the degree symbol (°), improper encoding (such as `Â`) was initially shown. This was resolved by setting the correct `Content-Type: text/html; charset=UTF-8` header.
The visual graphs needed consistent scaling despite changing data. Implementing a dynamic Y-axis that adapts to maximum values while remaining readable was a key UX challenge.
Creating a layout that auto-adjusts for both mobile and desktop screens, while updating in real time without full page reloads, required a combination of efficient JavaScript and CSS techniques.
Integrating Firebase required understanding its asynchronous behavior, authentication token refresh mechanisms, and ensuring secure access with proper API key and database rules.
Maintaining two distinct sketches — one for the local server and one for Firebase integration — introduced complexity but ensured better modularity and reduced code overhead on the ESP32.
Environmental factors such as temperature drift and sensor warm-up times needed to be considered to ensure accurate readings and valid scoring logic.
Below are the key resources used throughout the development process of this project: