This shows you the differences between two versions of the page.
iothings:proiecte:2025sric:esp32environmentalcomfortmonitoringsystemweb [2025/05/29 04:53] iustin.burciu [Firebase Integration Sketch] |
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 116: | Line 123: | ||
* Virtual LEDs (colored indicators) | * Virtual LEDs (colored indicators) | ||
- | **Snippet: Sensor reading and score calculation** | + | **Code** |
- | float citesteTemperatura() { | + | #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); | int adc = analogRead(pinLM35); | ||
- | float voltaj = adc * (5.0 / 4095.0); | + | float voltaj = adc * (5 / 4095.0); |
return voltaj * 100.0; | return voltaj * 100.0; | ||
- | } | + | } |
- | int calculeazaScor(float temp, int gaz) { | + | int citesteGaz() { |
+ | return analogRead(pinMQ4); | ||
+ | } | ||
+ | |||
+ | int calculeazaScor(float temp, int gaz) { | ||
int scor = 100; | int scor = 100; | ||
if (temp < 19 || temp > 25) scor -= 30; | if (temp < 19 || temp > 25) scor -= 30; | ||
Line 132: | Line 153: | ||
if (gaz > 100) scor -= 30; | if (gaz > 100) scor -= 30; | ||
return max(scor, 0); | 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 ==== | ==== Firebase Integration Sketch ==== | ||
Line 246: | Line 486: | ||
{{iothings:proiecte:2025sric:firebaseimg.png?800 |}} | {{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) | ||
+ | |||