This shows you the differences between two versions of the page.
iothings:proiecte:2025sric:esp32environmentalcomfortmonitoringsystemweb [2025/05/29 04:37] iustin.burciu [Functionality] |
iothings:proiecte:2025sric:esp32environmentalcomfortmonitoringsystemweb [2025/05/29 05:41] (current) iustin.burciu [Local Web Server Sketch] |
||
---|---|---|---|
Line 41: | Line 41: | ||
| LED Blue | GPIO25 | Lights up at comfort score 100 | | | LED Blue | GPIO25 | Lights up at comfort score 100 | | ||
| LED Red | GPIO26 | Lights up if gas > 30 | | | LED Red | GPIO26 | Lights up if gas > 30 | | ||
+ | |||
+ | {{ iothings:proiecte:2025sric:img_1163.jpeg?600 |Front wiring view }} | ||
+ | {{ iothings:proiecte:2025sric:img_1164.jpeg?600 |Top view of the breadboard }} | ||
+ | {{ iothings:proiecte:2025sric:img_1165.jpeg?600 |Connection to ESP32 }} | ||
+ | {{ iothings:proiecte:2025sric:img_1166.jpeg?600 |Complete physical assembly }} | ||
+ | |||
+ | |||
===== System Diagram ===== | ===== System Diagram ===== | ||
Line 68: | Line 75: | ||
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. | 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. | ||
- | ===== Optional: Firebase Support ===== | + | ===== Implementation ===== |
- | In a separate sketch, the system can also: | + | The implementation of this project was carried out in two main phases, using two distinct Arduino sketches for flexibility and modularity. |
- | * Push live data to Firebase Realtime Database | + | ==== Phase 1: Local Web Server ==== |
- | * Use credentials and Web API key | + | |
- | * Display values remotely via a mobile app or web | + | |
- | ===== Code Snippets ===== | + | 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: |
- | **1. Read Temperature and Gas:** | + | * A full **HTML/CSS/JavaScript dashboard** embedded in the Arduino sketch |
+ | * Real-time temperature and gas readings every 2 seconds | ||
+ | * Visualization using **canvas-based line graphs** | ||
+ | * Web-controlled virtual LEDs that change color based on sensor conditions | ||
+ | * A **scrollable alert history** that logs every gas threshold violation or comfort score event | ||
+ | |||
+ | This implementation is self-contained and requires no internet connection beyond local Wi-Fi access. | ||
+ | |||
+ | ==== Phase 2: Firebase Cloud Integration ==== | ||
+ | |||
+ | To enable remote monitoring and cloud-based data storage, a second Arduino sketch was developed using the **Firebase ESP Client** library. In this version: | ||
+ | |||
+ | * Sensor data is sent to a **Firebase Realtime Database** every 2 seconds | ||
+ | * The sketch authenticates using email/password credentials and an API key | ||
+ | * Comfort metrics are uploaded under a structured path: | ||
+ | * `/monitor/temperatura` | ||
+ | * `/monitor/gaz` | ||
+ | * `/monitor/scor` | ||
+ | * This enables the creation of external web or mobile dashboards for visualization | ||
+ | |||
+ | 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). | ||
+ | |||
+ | ===== Arduino Sketch ===== | ||
+ | |||
+ | The ESP32 board runs two separate Arduino sketches, each responsible for different functionalities: one for the local server interface and another for Firebase integration. | ||
+ | |||
+ | ==== Local Web Server Sketch ==== | ||
+ | |||
+ | 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: | ||
+ | |||
+ | * Reading values from **LM35 (temperature)** and **MQ-4 (gas)** sensors | ||
+ | * Calculating a **comfort score** based on thresholds | ||
+ | * Activating **physical LEDs** for visual alerts | ||
+ | * Serving a live-updating web page with: | ||
+ | * Temperature and gas values | ||
+ | * Comfort score display | ||
+ | * Canvas-based live graphs | ||
+ | * Alert log | ||
+ | * Virtual LEDs (colored indicators) | ||
+ | |||
+ | **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(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | {{ iothings:proiecte:2025sric:alertaocw.png?700 |}} | ||
+ | {{ iothings:proiecte:2025sric:calitateocw.png?700 |}} | ||
+ | |||
+ | ==== Firebase Integration Sketch ==== | ||
+ | |||
+ | 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:** | ||
+ | |||
+ | * Connects the ESP32 to Wi-Fi | ||
+ | * Authenticates using an email, password, and API key | ||
+ | * Initializes communication with Firebase | ||
+ | * Reads temperature and gas levels | ||
+ | * Calculates the comfort score | ||
+ | * Uploads all values to Firebase paths every 2 seconds | ||
+ | * Activates **LED indicators** (same behavior as the local sketch) | ||
+ | |||
+ | **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; | ||
- | ```c | ||
float citesteTemperatura() { | float citesteTemperatura() { | ||
int adc = analogRead(pinLM35); | int adc = analogRead(pinLM35); | ||
Line 90: | Line 430: | ||
return analogRead(pinMQ4); | 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); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | {{iothings:proiecte:2025sric:firebaseimg.png?800 |}} | ||
+ | |||
+ | ===== Challenges ===== | ||
+ | |||
+ | During the development of the Environmental Comfort Monitoring System, several challenges were encountered: | ||
+ | |||
+ | * **Voltage Compatibility for Sensors:** | ||
+ | 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. | ||
+ | |||
+ | * **Character Encoding Issues in Web Dashboard:** | ||
+ | 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. | ||
+ | |||
+ | * **Chart Scaling and Clarity:** | ||
+ | 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. | ||
+ | |||
+ | * **Web Interface Responsiveness:** | ||
+ | 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. | ||
+ | |||
+ | * **Firebase Authentication & Realtime Sync:** | ||
+ | Integrating Firebase required understanding its asynchronous behavior, authentication token refresh mechanisms, and ensuring secure access with proper API key and database rules. | ||
+ | |||
+ | * **Separation of Concerns:** | ||
+ | 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. | ||
+ | |||
+ | * **Data Accuracy and Sensor Calibration:** | ||
+ | Environmental factors such as temperature drift and sensor warm-up times needed to be considered to ensure accurate readings and valid scoring logic. | ||
+ | |||
+ | ===== References ===== | ||
+ | |||
+ | Below are the key resources used throughout the development process of this project: | ||
+ | |||
+ | * [Lab 1: Getting Started](https://ocw.cs.pub.ro/courses/iothings/laboratoare/2022/lab1) | ||
+ | * [Lab 2: Sensors & SPIFFS](https://ocw.cs.pub.ro/courses/iothings/laboratoare/2022/lab2) | ||
+ | * [Lab 3: Web servers](https://ocw.cs.pub.ro/courses/iothings/laboratoare/2022/lab3) | ||
+ | * [Lab 4: Databases for IoT - Firebase](https://ocw.cs.pub.ro/courses/iothings/laboratoare/2022/lab4) | ||
+ | * [Lab 5: Firebase (part 2)](https://ocw.cs.pub.ro/courses/iothings/laboratoare/2022/lab5) | ||
+ | * [Lab 6: InfluxDB](https://ocw.cs.pub.ro/courses/iothings/laboratoare/2022/lab6) | ||
+ | |||