Gráficas dinámicas de temperatura y humedad (Chart.js / Plotly)

Librerías de visualización, gráficas interactivas, dashboards dinámicos

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

Introducción

En esta clase avanzada de 3 horas, aprenderemos a crear dashboards dinámicos e interactivos para visualizar datos de temperatura y humedad en tiempo real. Utilizaremos nuestro stack completo ESP32 + DHT11 + Mosquitto MQTT + Flask, integrando las poderosas librerías de visualización Chart.js y Plotly para crear gráficas profesionales que se actualizan automáticamente.

Los dashboards dinámicos son fundamentales en aplicaciones IoT modernas, permitiendo monitoreo en tiempo real, análisis de tendencias y toma de decisiones basada en datos. Al finalizar esta clase, tendrás las competencias para crear interfaces web profesionales que rivalizan con soluciones comerciales.

Objetivos de aprendizaje:
  • Implementar gráficas interactivas con Chart.js y Plotly
  • Crear dashboards responsive que se actualicen en tiempo real
  • Optimizar el rendimiento de visualizaciones con grandes volúmenes de datos
  • Integrar WebSockets para actualizaciones instantáneas

Conceptos Fundamentales

Antes de implementar las gráficas dinámicas, es crucial comprender los conceptos teóricos que sustentan la visualización de datos en tiempo real:

1. Librerías de Visualización

  • Chart.js: Librería ligera y flexible, ideal para gráficas estándar con excelente rendimiento
  • Plotly.js: Potente librería con capacidades avanzadas de interactividad y gráficas científicas
  • D3.js: Base de muchas librerías, ofrece control total pero mayor complejidad

2. Arquitectura de Datos en Tiempo Real

Flujo de Datos:
  1. ESP32 lee sensores DHT11
  2. Publica datos vía MQTT a Mosquitto
  3. Flask recibe y procesa datos
  4. WebSocket envía actualizaciones al frontend
  5. JavaScript actualiza gráficas dinámicamente
Consideraciones de Rendimiento:
  • Límite de puntos de datos visualizados
  • Frecuencia de actualización optimizada
  • Buffering y agregación de datos
  • Lazy loading para datos históricos

3. Patrones de Diseño para Dashboards

  • Observer Pattern: Para actualizaciones automáticas de componentes
  • Model-View-Controller (MVC): Separación de lógica de datos y presentación
  • Singleton Pattern: Para gestión centralizada de conexiones WebSocket
  • Factory Pattern: Para creación dinámica de diferentes tipos de gráficas
Concepto Clave: Interpolación y Suavizado

Para datos de sensores que pueden tener ruido o variaciones bruscas, implementaremos algoritmos de suavizado como media móvil y filtros pasa-bajos para mejorar la visualización sin perder información crítica.

Implementación Práctica

Desarrollaremos un sistema completo de visualización dinámica, comenzando desde la configuración del ESP32 hasta el dashboard final con múltiples tipos de gráficas interactivas.

ESP32 - Código Optimizado para Visualización


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

// Configuración de pines y constantes
#define DHT_PIN 4
#define DHT_TYPE DHT11
#define LED_PIN 2

// Configuración WiFi y MQTT
const char* ssid = "TU_WIFI";
const char* password = "TU_PASSWORD";
const char* mqtt_server = "192.168.1.100";
const int mqtt_port = 1883;

// Inicialización de objetos
DHT dht(DHT_PIN, DHT_TYPE);
WiFiClient espClient;
PubSubClient client(espClient);

// Variables para control de timing y datos
unsigned long lastMsg = 0;
const long interval = 5000; // 5 segundos entre lecturas
float tempBuffer[10] = {0}; // Buffer para suavizado
float humBuffer[10] = {0};
int bufferIndex = 0;
bool bufferFull = false;

// Estructura para datos del sensor
struct SensorData {
    float temperature;
    float humidity;
    unsigned long timestamp;
    float temp_smoothed;
    float hum_smoothed;
    String quality_status;
};

void setup() {
    Serial.begin(115200);
    pinMode(LED_PIN, OUTPUT);
    
    // Inicializar DHT11
    dht.begin();
    Serial.println("DHT11 inicializado");
    
    // Conectar a WiFi
    setup_wifi();
    
    // Configurar cliente MQTT
    client.setServer(mqtt_server, mqtt_port);
    client.setCallback(callback);
    
    // Configurar tiempo NTP para timestamps
    configTime(0, 0, "pool.ntp.org");
    
    Serial.println("Sistema inicializado para dashboard dinámico");
    digitalWrite(LED_PIN, HIGH);
    delay(1000);
    digitalWrite(LED_PIN, LOW);
}

void setup_wifi() {
    delay(10);
    Serial.println();
    Serial.print("Conectando a ");
    Serial.println(ssid);
    
    WiFi.begin(ssid, password);
    
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    
    randomSeed(micros());
    Serial.println("");
    Serial.println("WiFi conectado");
    Serial.println("IP: ");
    Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
    String message;
    for (int i = 0; i < length; i++) {
        message += (char)payload[i];
    }
    
    if (String(topic) == "esp32/config") {
        // Procesar comandos de configuración del dashboard
        DynamicJsonDocument doc(1024);
        deserializeJson(doc, message);
        
        if (doc["interval"]) {
            // Cambiar intervalo de muestreo dinámicamente
            // interval = doc["interval"];
        }
    }
}

float calculateSmoothValue(float newValue, float buffer[], int size) {
    buffer[bufferIndex] = newValue;
    bufferIndex = (bufferIndex + 1) % size;
    if (bufferIndex == 0) bufferFull = true;
    
    float sum = 0;
    int count = bufferFull ? size : bufferIndex;
    
    for (int i = 0; i < count; i++) {
        sum += buffer[i];
    }
    
    return sum / count;
}

String getQualityStatus(float temp, float hum) {
    if (temp >= 20 && temp <= 25 && hum >= 40 && hum <= 60) {
        return "optimal";
    } else if (temp >= 15 && temp <= 30 && hum >= 30 && hum <= 70) {
        return "good";
    } else {
        return "poor";
    }
}

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())) {
            Serial.println("conectado");
            client.subscribe("esp32/config");
        } else {
            Serial.print("falló, rc=");
            Serial.print(client.state());
            Serial.println(" reintentando en 5 segundos");
            delay(5000);
        }
    }
}

void loop() {
    if (!client.connected()) {
        reconnect();
    }
    client.loop();
    
    unsigned long now = millis();
    if (now - lastMsg > interval) {
        lastMsg = now;
        
        // Leer sensores
        SensorData data;
        data.temperature = dht.readTemperature();
        data.humidity = dht.readHumidity();
        data.timestamp = now;
        
        // Verificar lecturas válidas
        if (isnan(data.temperature) || isnan(data.humidity)) {
            Serial.println("Error leyendo DHT11!");
            return;
        }
        
        // Calcular valores suavizados
        data.temp_smoothed = calculateSmoothValue(data.temperature, tempBuffer, 10);
        data.hum_smoothed = calculateSmoothValue(data.humidity, humBuffer, 10);
        data.quality_status = getQualityStatus(data.temp_smoothed, data.hum_smoothed);
        
        // Crear JSON para dashboard
        DynamicJsonDocument doc(1024);
        doc["device_id"] = "ESP32_DHT11_001";
        doc["timestamp"] = data.timestamp;
        doc["temperature"] = {
            {"raw", data.temperature},
            {"smoothed", data.temp_smoothed},
            {"unit", "°C"}
        };
        doc["humidity"] = {
            {"raw", data.humidity},
            {"smoothed", data.hum_smoothed},
            {"unit", "%"}
        };
        doc["quality"] = data.quality_status;
        doc["location"] = "Laboratorio IoT";
        
        String jsonString;
        serializeJson(doc, jsonString);
        
        // Publicar datos
        client.publish("sensors/dht11/data", jsonString.c_str());
        
        // Feedback visual
        digitalWrite(LED_PIN, HIGH);
        delay(100);
        digitalWrite(LED_PIN, LOW);
        
        // Debug
        Serial.println("Datos enviados: " + jsonString);
    }
}
            

Flask - Servidor con WebSocket y API


from flask import Flask, render_template, jsonify, request
from flask_socketio import SocketIO, emit
import paho.mqtt.client as mqtt
import json
import threading
import time
from datetime import datetime, timedelta
import sqlite3
import numpy as np
from collections import deque
import logging

# Configuración de la aplicación
app = Flask(__name__)
app.config['SECRET_KEY'] = 'dashboard_secret_key_2024'
socketio = SocketIO(app, cors_allowed_origins="*", logger=True, engineio_logger=True)

# Configuración de logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Configuración MQTT
MQTT_BROKER = "localhost"
MQTT_PORT = 1883
MQTT_TOPIC = "sensors/dht11/data"

# Buffer de datos para gráficas dinámicas
data_buffer = deque(maxlen=1000)  # Últimas 1000 lecturas
current_data = {
    'temperature': {'raw': 0, 'smoothed': 0},
    'humidity': {'raw': 0, 'smoothed': 0},
    'quality': 'good',
    'timestamp': datetime.now().isoformat()
}

class DatabaseManager:
    def __init__(self, db_path='sensor_data.db'):
        self.db_path = db_path
        self.init_database()
    
    def init_database(self):
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS sensor_readings (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                device_id TEXT,
                timestamp DATETIME,
                temperature_raw REAL,
                temperature_smoothed REAL,
                humidity_raw REAL,
                humidity_smoothed REAL,
                quality_status TEXT,
                location TEXT
            )
        ''')
        conn.commit()
        conn.close()
        logger.info("Base de datos inicializada")
    
    def insert_reading(self, data):
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        cursor.execute('''
            INSERT INTO sensor_readings 
            (device_id, timestamp, temperature_raw, temperature_smoothed,
             humidity_raw, humidity_smoothed, quality_status, location)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        ''', (
            data.get('device_id', 'unknown'),
            datetime.now(),
            data['temperature']['raw'],
            data['temperature']['smoothed'],
            data['humidity']['raw'],
            data['humidity']['smoothed'],
            data['quality'],
            data.get('location', 'unknown')
        ))
        conn.commit()
        conn.close()
    
    def get_historical_data(self, hours=24):
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        since = datetime.now() - timedelta(hours=hours)
        
        cursor.execute('''
            SELECT timestamp, temperature_smoothed, humidity_smoothed, quality_status
            FROM sensor_readings 
            WHERE timestamp >= ? 
            ORDER BY timestamp
        ''', (since,))
        
        results = cursor.fetchall()
        conn.close()
        
        return [{
            'timestamp': row[0],
            'temperature': row[1],
            'humidity': row[2],
            'quality': row[3]
        } for row in results]

db_manager = DatabaseManager()

class MQTT