Mostrar dashboard en tiempo real con gráficas de temperatura y humedad

Frontend interactivo, visualización en tiempo real, UX/UI profesional

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

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>