Panel web para monitoreo de sensores

Interface web completa, responsive design, experiencia de usuario

Módulo 5 ⏱️ 2 horas 🛠️ ESP32 + DHT11 🌐 Flask + MQTT

Introducción

En esta lección desarrollaremos un panel web profesional para monitorear sensores en tiempo real utilizando el stack completo ESP32 + DHT11 + Mosquitto (MQTT) + Flask. Crearemos una interfaz web responsive con capacidades de visualización de datos en tiempo real, gráficos interactivos y alertas automáticas.

El objetivo es construir un sistema IoT completo que permita monitorear temperatura y humedad desde cualquier dispositivo, con una experiencia de usuario moderna y profesional.

Stack Tecnológico
  • Hardware: ESP32 + Sensor DHT11
  • Comunicación: Mosquitto MQTT Broker
  • Backend: Flask (Python)
  • Frontend: HTML5, CSS3, JavaScript, Bootstrap 5, Chart.js

Conceptos Fundamentales

Arquitectura del Sistema

Nuestro sistema sigue una arquitectura basada en microservicios con comunicación asíncrona:

  • Capa de Sensores: ESP32 con DHT11 captura datos ambientales
  • Capa de Comunicación: MQTT para transmisión de datos en tiempo real
  • Capa de Aplicación: Flask maneja la lógica de negocio y APIs
  • Capa de Presentación: Interface web responsive con visualización de datos

Componentes del Panel Web

  • Dashboard Principal: Vista general con métricas actuales
  • Gráficos en Tiempo Real: Visualización histórica de datos
  • Sistema de Alertas: Notificaciones automáticas por umbrales
  • Panel de Configuración: Ajustes de umbrales y preferencias
  • API REST: Endpoints para datos históricos y configuración

Patrón de Diseño Utilizado

Implementaremos el patrón Publisher-Subscriber usando MQTT y WebSockets para actualizaciones en tiempo real sin necesidad de recargar la página.

Conexiones Hardware

Diagrama de conexión ESP32 + DHT11:

  • DHT11 VCC → ESP32 3.3V
  • DHT11 GND → ESP32 GND
  • DHT11 DATA → ESP32 GPIO 4
  • Resistor pull-up de 10kΩ entre DATA y VCC

Implementación Práctica

1. Configuración del ESP32 (Publicador MQTT)

Código ESP32 - Sensor y Publicación MQTT:


#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <ArduinoJson.h>

// Configuración WiFi
const char* ssid = "TU_RED_WIFI";
const char* password = "TU_PASSWORD";

// Configuración MQTT
const char* mqtt_server = "192.168.1.100";  // IP del servidor Mosquitto
const int mqtt_port = 1883;
const char* mqtt_user = "sensor_user";
const char* mqtt_password = "sensor_pass";
const char* mqtt_topic = "home/sensors/dht11";

// Configuración DHT11
#define DHT_PIN 4
#define DHT_TYPE DHT11
DHT dht(DHT_PIN, DHT_TYPE);

// Clientes
WiFiClient espClient;
PubSubClient client(espClient);

// Variables de control
unsigned long lastMsg = 0;
const long interval = 5000; // Enviar datos cada 5 segundos
int sensor_id = 1;

void setup() {
    Serial.begin(115200);
    dht.begin();
    
    // Conectar a WiFi
    WiFi.begin(ssid, password);
    Serial.print("Conectando a WiFi");
    
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    
    Serial.println();
    Serial.println("WiFi conectado!");
    Serial.print("IP asignada: ");
    Serial.println(WiFi.localIP());
    
    // Configurar cliente MQTT
    client.setServer(mqtt_server, mqtt_port);
    client.setCallback(callback);
}

void loop() {
    if (!client.connected()) {
        reconnect();
    }
    client.loop();
    
    unsigned long now = millis();
    if (now - lastMsg > interval) {
        lastMsg = now;
        publishSensorData();
    }
}

void publishSensorData() {
    float humidity = dht.readHumidity();
    float temperature = dht.readTemperature();
    
    // Verificar si las lecturas son válidas
    if (isnan(humidity) || isnan(temperature)) {
        Serial.println("Error al leer el sensor DHT11");
        return;
    }
    
    // Crear JSON con los datos
    StaticJsonDocument<200> doc;
    doc["sensor_id"] = sensor_id;
    doc["temperature"] = temperature;
    doc["humidity"] = humidity;
    doc["timestamp"] = millis();
    doc["location"] = "Living Room";
    
    char buffer[256];
    serializeJson(doc, buffer);
    
    // Publicar datos
    if (client.publish(mqtt_topic, buffer)) {
        Serial.println("Datos publicados:");
        Serial.println(buffer);
    } else {
        Serial.println("Error al publicar datos");
    }
}

void callback(char* topic, byte* payload, unsigned int length) {
    Serial.print("Mensaje recibido en tópico: ");
    Serial.println(topic);
}

void reconnect() {
    while (!client.connected()) {
        Serial.print("Intentando conexión MQTT...");
        
        String clientId = "ESP32Client-";
        clientId += String(random(0xffff), HEX);
        
        if (client.connect(clientId.c_str(), mqtt_user, mqtt_password)) {
            Serial.println("Conectado al broker MQTT");
            client.subscribe("home/sensors/config");
        } else {
            Serial.print("Error de conexión, rc=");
            Serial.print(client.state());
            Serial.println(" Reintentando en 5 segundos");
            delay(5000);
        }
    }
}
            

2. Configuración del Broker Mosquitto

Archivo mosquitto.conf:


# Configuración básica de Mosquitto
port 1883
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd

# Configuración para WebSockets (para el navegador)
listener 9001
protocol websockets

# Logging
log_dest file /var/log/mosquitto/mosquitto.log
log_type error
log_type warning
log_type notice
log_type information

# Persistencia
persistence true
persistence_location /var/lib/mosquitto/

# Configuración de seguridad
max_connections 100
max_inflight_messages 20
            

3. Aplicación Flask (Backend)

Código Python/Flask - app.py:


from flask import Flask, render_template, jsonify, request
from flask_socketio import SocketIO, emit
import paho.mqtt.client as mqtt
import json
import sqlite3
from datetime import datetime, timedelta
import threading
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = 'tu_clave_secreta_aqui'
socketio = SocketIO(app, cors_allowed_origins="*")

# Configuración MQTT
MQTT_BROKER = "localhost"
MQTT_PORT = 1883
MQTT_USERNAME = "sensor_user"
MQTT_PASSWORD = "sensor_pass"
MQTT_TOPIC = "home/sensors/dht11"

# Variables globales para datos actuales
current_data = {
    'temperature': 0,
    'humidity': 0,
    'timestamp': datetime.now(),
    'status': 'Desconectado'
}

# Configuración de umbrales
thresholds = {
    'temp_min': 18,
    'temp_max': 30,
    'humidity_min': 30,
    'humidity_max': 80
}

def init_database():
    """Inicializar la base de datos SQLite"""
    conn = sqlite3.connect('sensor_data.db')
    cursor = conn.cursor()
    
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS sensor_readings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            sensor_id INTEGER,
            temperature REAL,
            humidity REAL,
            timestamp DATETIME,
            location TEXT
        )
    ''')
    
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS alerts (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            alert_type TEXT,
            message TEXT,
            timestamp DATETIME,
            resolved BOOLEAN DEFAULT FALSE
        )
    ''')
    
    conn.commit()
    conn.close()

def save_sensor_data(data):
    """Guardar datos del sensor en la base de datos"""
    conn = sqlite3.connect('sensor_data.db')
    cursor = conn.cursor()
    
    cursor.execute('''
        INSERT INTO sensor_readings (sensor_id, temperature, humidity, timestamp, location)
        VALUES (?, ?, ?, ?, ?)
    ''', (
        data.get('sensor_id', 1),
        data['temperature'],
        data['humidity'],
        datetime.now(),
        data.get('location', 'Unknown')
    ))
    
    conn.commit()
    conn.close()

def check_thresholds(temperature, humidity):
    """Verificar umbrales y generar alertas"""
    alerts = []
    
    if temperature < thresholds['temp_min']:
        alerts.append({
            'type': 'warning',
            'message': f'Temperatura baja: {temperature}°C (mín: {thresholds["temp_min"]}°C)'
        })
    elif temperature > thresholds['temp_max']:
        alerts.append({
            'type': 'danger',
            'message': f'Temperatura alta: {temperature}°C (máx: {thresholds["temp_max"]}°C)'
        })
    
    if humidity < thresholds['humidity_min']:
        alerts.append({
            'type': 'warning',
            'message': f'Humedad baja: {humidity}% (mín: {thresholds["humidity_min"]}%)'
        })
    elif humidity > thresholds['humidity_max']:
        alerts.append({
            'type': 'danger',
            'message': f'Humedad alta: {humidity}% (máx: {thresholds["humidity_max"]}%)'
        })
    
    return alerts

# Callbacks MQTT
def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print("Conectado al broker MQTT")
        client.subscribe(MQTT_TOPIC)
        current_data['status'] = 'Conectado'
    else:
        print(f"Error de conexión MQTT: {rc}")
        current_data['status'] = 'Error de conexión'

def on_message(client, userdata, msg):
    try:
        # Decodificar mensaje JSON
        data = json.loads(msg.payload.decode())
        
        # Actualizar datos actuales
        current_data.update({
            'temperature': data['temperature'],
            'humidity': data['humidity'],
            'timestamp': datetime.now(),
            'status': 'Datos recibidos'
        })
        
        # Guardar en base de datos
        save_sensor_data(data)
        
        # Verificar umbrales y generar alertas
        alerts = check_thresholds(data['temperature'], data['humidity'])
        
        # Enviar datos a clientes web en tiempo real
        socketio.emit('sensor_update', {
            'temperature': data['temperature'],
            'humidity': data['humidity'],
            'timestamp': current_data['timestamp'].strftime('%H:%M:%S'),
            'alerts': alerts
        })
        
        print(f"Datos recibidos - T: {data['temperature']}°C, H: {data['humidity']}%")
        
    except Exception as e:
        print(f"Error procesando mensaje MQTT: {e}")

# Configurar cliente MQTT
mqtt_client = mqtt.Client()
mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
mqtt_client.on_connect = on_connect
mqtt_client.on_message = on_message

def start_mqtt_client():
    """Iniciar cliente MQTT en hilo separado"""
    try:
        mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60)
        mqtt_client.loop_forever()
    except Exception as e:
        print(f"Error conectando a MQTT: {e}")

# Rutas Flask
@app.route('/')
def dashboard():
    return render_template('dashboard.html')

@app.route('/api/current')
def get_current_data():
    """API para obtener datos actuales"""
    return jsonify({
        'temperature': current_data['temperature'],
        'humidity': current_data['humidity'],
        'timestamp': current_data['timestamp'].strftime('%Y-%m-%d %H:%M:%S'),
        'status': current_data['status']
    })

@app.route('/api/history')
def get_historical_data():
    """API para obtener datos históricos"""
    hours = request.args.get('hours', 24, type=int)
    
    conn = sqlite3.connect('sensor_data.db')
    cursor = conn.cursor()
    
    cursor.