Environmental Comfort Monitoring System with ESP32

Autor: Burciu Iustin Florian Email: iustin.burciu@stud.fiir.upb.ro Grupa: AAC

Introduction

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:

  • Real-time data acquisition from temperature and gas sensors
  • Live display of sensor values and computed comfort score
  • Visual charts and indicator LEDs (both virtual and physical)
  • Automatic alerts and a persistent log for critical events
  • Optional Firebase integration for remote access

Hardware

Used Components:

  • ESP32 DevKit V1 – WiFi + Bluetooth enabled development board
  • LM35 – analog temperature sensor
  • MQ-4 – analog gas sensor
  • 2x LEDs (red and blue) – for visual alerts
  • Jumper wires, breadboard

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

Front wiring view Top view of the breadboard Connection to ESP32 Complete physical assembly

System Diagram

Functionality

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:

  • Reading analog values from the LM35 temperature sensor (in °C) and the MQ-4 gas sensor (in ppm).
  • Computing the comfort score based on pre-defined thresholds for temperature and gas, penalizing deviations from optimal ranges.
  • Real-time display of measurements through a web-based dashboard, hosted locally on the ESP32 microcontroller.
  • Automatic dashboard refresh every 2 seconds, showing:
    • Current temperature
    • Gas level (methane/LPG)
    • Comfort score out of 100
    • Two animated line charts visualizing historical values
  • Visual indicators (LEDs):
    • Red LED – lights up when gas level exceeds 30 ppm
    • Blue LED – lights up when the comfort score reaches 100
  • Virtual LEDs on the web interface, mimicking the behavior of physical ones
  • Persistent event log displaying timestamps, temperature, gas level, and comfort score whenever a threshold is crossed
  • *(Optional)* Cloud integration with Firebase Realtime Database, using a separate sketch, allowing data to be viewed remotely

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.

Implementation

The implementation of this project was carried out in two main phases, using two distinct Arduino sketches for flexibility and modularity.

Phase 1: Local Web Server

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:

  • 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();
}

}

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_URLhttps://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);
}

}

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:

iothings/proiecte/2025sric/esp32environmentalcomfortmonitoringsystemweb.txt · Last modified: 2025/05/29 05:41 by iustin.burciu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0