Introducción
En esta clase construiremos un dashboard profesional en tiempo real para visualizar datos de temperatura y humedad utilizando gráficas interactivas. Implementaremos una solución completa que incluye la captura de datos con ESP32 y DHT11, comunicación MQTT a través de Mosquitto, y un frontend moderno desarrollado con Flask.
El objetivo es crear un sistema de monitoreo IoT profesional que permita visualizar datos ambientales en tiempo real con una interfaz de usuario intuitiva y responsiva, aplicando principios de UX/UI para ingenieros.
Conceptos Fundamentales
Para desarrollar un dashboard efectivo en tiempo real, necesitamos comprender varios conceptos clave:
Arquitectura del Sistema
- ESP32 + DHT11: Captura de datos ambientales cada 2 segundos
- Broker MQTT (Mosquitto): Intermediario de mensajes asíncrono
- Flask Backend: API REST y servidor de WebSockets
- Frontend: Dashboard con Chart.js y Socket.IO para actualizaciones en tiempo real
Tecnologías de Visualización
- WebSockets: Comunicación bidireccional en tiempo real
- Chart.js: Biblioteca para gráficas interactivas y responsivas
- Bootstrap 5: Framework CSS para diseño profesional
- MQTT sobre WebSockets: Integración directa navegador-broker
Principios de UX/UI para Dashboards
- Jerarquía visual: Información más importante destacada
- Actualización suave: Transiciones animadas para cambios de datos
- Indicadores de estado: Conexión, calidad de datos, alertas
- Responsividad: Adaptación a diferentes tamaños de pantalla
Implementación Práctica
Desarrollaremos el sistema completo paso a paso, comenzando con la configuración del ESP32, seguido por el backend Flask y finalmente el frontend interactivo.
Código ESP32 - Sensor DHT11 y 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 de tu servidor Mosquitto
const int mqtt_port = 1883;
const char* mqtt_topic = "sensors/environment";
// Configuración DHT11
#define DHT_PIN 4
#define DHT_TYPE DHT11
DHT dht(DHT_PIN, DHT_TYPE);
// Clientes WiFi y MQTT
WiFiClient espClient;
PubSubClient client(espClient);
// Variables para timing
unsigned long lastMeasurement = 0;
const unsigned long measurementInterval = 2000; // 2 segundos
void setup() {
Serial.begin(115200);
dht.begin();
// Conectar a WiFi
setupWiFi();
// Configurar MQTT
client.setServer(mqtt_server, mqtt_port);
client.setCallback(mqttCallback);
Serial.println("Sistema iniciado correctamente");
}
void setupWiFi() {
delay(10);
Serial.println("Conectando a WiFi...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Conectado a: ");
Serial.println(ssid);
Serial.print("IP asignada: ");
Serial.println(WiFi.localIP());
}
void mqttCallback(char* topic, byte* payload, unsigned int length) {
// Manejar mensajes MQTT entrantes si es necesario
Serial.print("Mensaje recibido en [");
Serial.print(topic);
Serial.println("]");
}
void reconnectMQTT() {
while (!client.connected()) {
Serial.println("Intentando conexión MQTT...");
if (client.connect("ESP32_Sensor", "admin", "password")) {
Serial.println("Conectado al broker MQTT");
client.subscribe("sensors/commands");
} else {
Serial.print("Error de conexión, rc=");
Serial.print(client.state());
Serial.println(" Reintentando en 5 segundos");
delay(5000);
}
}
}
void publishSensorData(float temperature, float humidity) {
// Crear JSON con los datos
StaticJsonDocument<200> doc;
doc["timestamp"] = millis();
doc["temperature"] = temperature;
doc["humidity"] = humidity;
doc["device_id"] = "ESP32_01";
doc["location"] = "Sala_Principal";
char jsonString[256];
serializeJson(doc, jsonString);
// Publicar datos
if (client.publish(mqtt_topic, jsonString)) {
Serial.print("Datos publicados: ");
Serial.println(jsonString);
} else {
Serial.println("Error al publicar datos");
}
}
void loop() {
if (!client.connected()) {
reconnectMQTT();
}
client.loop();
// Leer sensores cada 2 segundos
if (millis() - lastMeasurement > measurementInterval) {
float humidity = dht.readHumidity();
float temperature = dht.readTemperature();
// Verificar si las lecturas son válidas
if (!isnan(humidity) && !isnan(temperature)) {
publishSensorData(temperature, humidity);
Serial.printf("Temperatura: %.1f°C, Humedad: %.1f%%\n",
temperature, humidity);
} else {
Serial.println("Error al leer el sensor DHT11");
}
lastMeasurement = millis();
}
delay(100);
}
Backend Flask - Servidor y WebSockets
from flask import Flask, render_template, jsonify
from flask_socketio import SocketIO, emit
import paho.mqtt.client as mqtt
import json
import threading
import time
from datetime import datetime
import sqlite3
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_TOPIC = "sensors/environment"
MQTT_USERNAME = "admin"
MQTT_PASSWORD = "password"
# Base de datos para almacenar histórico
DATABASE = 'sensor_data.db'
def init_database():
"""Inicializar base de datos SQLite"""
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS sensor_readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
device_id TEXT,
temperature REAL,
humidity REAL,
location TEXT
)
''')
conn.commit()
conn.close()
def save_sensor_data(data):
"""Guardar datos en la base de datos"""
try:
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO sensor_readings (device_id, temperature, humidity, location)
VALUES (?, ?, ?, ?)
''', (data.get('device_id'), data.get('temperature'),
data.get('humidity'), data.get('location')))
conn.commit()
conn.close()
return True
except Exception as e:
print(f"Error guardando en BD: {e}")
return False
def on_connect(client, userdata, flags, rc):
"""Callback para conexión MQTT"""
if rc == 0:
print("Conectado al broker MQTT")
client.subscribe(MQTT_TOPIC)
else:
print(f"Error de conexión MQTT: {rc}")
def on_message(client, userdata, msg):
"""Callback para mensajes MQTT"""
try:
# Decodificar mensaje JSON
payload = json.loads(msg.payload.decode())
print(f"Datos recibidos: {payload}")
# Agregar timestamp del servidor
payload['server_timestamp'] = datetime.now().isoformat()
# Guardar en base de datos
save_sensor_data(payload)
# Emitir a todos los clientes WebSocket conectados
socketio.emit('sensor_data', payload, broadcast=True)
except json.JSONDecodeError as e:
print(f"Error decodificando JSON: {e}")
except Exception as e:
print(f"Error procesando mensaje: {e}")
def setup_mqtt():
"""Configurar cliente MQTT"""
client = mqtt.Client()
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
client.on_connect = on_connect
client.on_message = on_message
try:
client.connect(MQTT_BROKER, MQTT_PORT, 60)
client.loop_start()
return client
except Exception as e:
print(f"Error conectando a MQTT: {e}")
return None
@app.route('/')
def dashboard():
"""Página principal del dashboard"""
return render_template('dashboard.html')
@app.route('/api/historical')
def get_historical_data():
"""API para obtener datos históricos"""
try:
hours = request.args.get('hours', 24, type=int)
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
cursor.execute('''
SELECT timestamp, temperature, humidity, device_id, location
FROM sensor_readings
WHERE datetime(timestamp) >= datetime('now', '-{} hours')
ORDER BY timestamp ASC
'''.format(hours))
rows = cursor.fetchall()
conn.close()
data = []
for row in rows:
data.append({
'timestamp': row[0],
'temperature': row[1],
'humidity': row[2],
'device_id': row[3],
'location': row[4]
})
return jsonify(data)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/stats')
def get_statistics():
"""API para estadísticas básicas"""
try:
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
cursor.execute('''
SELECT
COUNT(*) as total_readings,
AVG(temperature) as avg_temp,
MIN(temperature) as min_temp,
MAX(temperature) as max_temp,
AVG(humidity) as avg_humidity,
MIN(humidity) as min_humidity,
MAX(humidity) as max_humidity
FROM sensor_readings
WHERE datetime(timestamp) >= datetime('now', '-24 hours')
''')
row = cursor.fetchone()
conn.close()
if row:
stats = {
'total_readings': row[0],
'temperature': {
'avg': round(row[1] or 0, 1),
'min': round(row[2] or 0, 1),
'max': round(row[3] or 0, 1)
},
'humidity': {
'avg': round(row[4] or 0, 1),
'min': round(row[5] or 0, 1),
'max': round(row[6] or 0, 1)
}
}
else:
stats = {'error': 'No data available'}
return jsonify(stats)
except Exception as e:
return jsonify({'error': str(e)}), 500
@socketio.on('connect')
def handle_connect():
"""Manejar nueva conexión WebSocket"""
print('Cliente conectado')
emit('status', {'message': 'Conectado al dashboard'})
@socketio.on('disconnect')
def handle_disconnect():
"""Manejar desconexión WebSocket"""
print('Cliente desconectado')
if __name__ == '__main__':
# Inicializar base de datos
init_database()
# Configurar MQTT
mqtt_client = setup_mqtt()
if mqtt_client:
print("Sistema iniciado correctamente")
# Ejecutar servidor Flask con SocketIO
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
else:
print("No se pudo conectar a MQTT. Verifique la configuración.")
Frontend HTML - Dashboard Interactivo
<!-- templates/dashboard.html -->
<!DOCTYPE html>