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.