Buenas prácticas: QoS, persistencia y seguridad básica

Niveles de QoS, mensajes retenidos, autenticación básica, mejores prácticas

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

Introducción

En esta lección profundizaremos en las buenas prácticas para sistemas IoT robustos y seguros utilizando nuestro stack ESP32 + DHT11 + Mosquitto + Flask. Exploraremos tres pilares fundamentales: Quality of Service (QoS) para garantizar la entrega confiable de mensajes, persistencia para mantener datos críticos disponibles, y seguridad básica para proteger nuestras comunicaciones.

¿Por qué son importantes estas prácticas?
  • QoS: Asegura que los datos críticos lleguen a su destino sin pérdidas
  • Persistencia: Mantiene información vital disponible ante desconexiones
  • Seguridad: Protege nuestros datos y sistemas contra accesos no autorizados

Conceptos Fundamentales

1. Quality of Service (QoS) en MQTT

MQTT define tres niveles de QoS que determinan cómo se garantiza la entrega de mensajes:

QoS 0 - At Most Once
  • Entrega "disparar y olvidar"
  • Sin confirmación
  • Menor overhead
  • Posible pérdida de mensajes
QoS 1 - At Least Once
  • Garantiza entrega
  • Requiere ACK (PUBACK)
  • Posibles duplicados
  • Balance overhead/confiabilidad
QoS 2 - Exactly Once
  • Entrega exactamente una vez
  • Handshake de 4 pasos
  • Mayor overhead
  • Máxima confiabilidad

2. Persistencia en MQTT

La persistencia incluye dos aspectos principales:

  • Retained Messages: Mensajes que el broker almacena y entrega inmediatamente a nuevos suscriptores
  • Persistent Sessions: El broker mantiene información de suscripciones y mensajes pendientes para clientes desconectados
  • Clean Session: Flag que determina si la sesión persiste después de la desconexión

3. Seguridad Básica

Implementaremos múltiples capas de seguridad:

  • Autenticación: Usuario y contraseña para el broker MQTT
  • Autorización: Control de acceso a topics específicos
  • Encriptación: TLS para comunicaciones seguras
  • Validación de datos: Verificación de integridad en la aplicación Flask

Implementación Práctica

1. Configuración del Broker Mosquitto con Seguridad

Archivo de configuración mosquitto.conf:


# /etc/mosquitto/mosquitto.conf
port 1883
listener 8883
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key

# Configuración de seguridad
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl

# Configuración de persistencia
persistence true
persistence_location /var/lib/mosquitto/
persistent_client_expiration 2d
max_queued_messages 1000
max_inflight_messages 100

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

Creación de usuarios y ACL:


# Crear usuarios
mosquitto_passwd -c /etc/mosquitto/passwd esp32_sensor
mosquitto_passwd /etc/mosquitto/passwd flask_app

# Archivo ACL (/etc/mosquitto/acl)
# Usuario esp32_sensor: solo puede publicar en sensores/
user esp32_sensor
topic write sensores/+/temperatura
topic write sensores/+/humedad

# Usuario flask_app: puede leer todos los sensores
user flask_app
topic read sensores/#
topic write comandos/#
            

2. Código ESP32 con QoS y Seguridad

ESP32 - Implementación completa con buenas prácticas:


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

// Configuración de pines
#define DHT_PIN 4
#define DHT_TYPE DHT11
#define LED_STATUS 2

// Configuración WiFi
const char* ssid = "Tu_WiFi";
const char* password = "Tu_Password";

// Configuración MQTT
const char* mqtt_server = "192.168.1.100";
const int mqtt_port = 8883;
const char* mqtt_user = "esp32_sensor";
const char* mqtt_password = "sensor_password";
const char* client_id = "ESP32_DHT11_001";

// Topics MQTT
const char* topic_temperatura = "sensores/salon/temperatura";
const char* topic_humedad = "sensores/salon/humedad";
const char* topic_status = "sensores/salon/status";
const char* topic_comandos = "comandos/esp32_001";

// Objetos
DHT dht(DHT_PIN, DHT_TYPE);
WiFiClientSecure espClient;
PubSubClient client(espClient);

// Variables de control
unsigned long lastMsg = 0;
unsigned long lastHeartbeat = 0;
const long interval = 30000; // 30 segundos
const long heartbeat_interval = 60000; // 1 minuto
bool sensor_error = false;
int reconnect_attempts = 0;
const int max_reconnect_attempts = 5;

// Estructura para datos del sensor
struct SensorData {
    float temperature;
    float humidity;
    unsigned long timestamp;
    bool valid;
};

void setup() {
    Serial.begin(115200);
    pinMode(LED_STATUS, OUTPUT);
    digitalWrite(LED_STATUS, LOW);
    
    dht.begin();
    setup_wifi();
    
    // Configurar TLS (para producción, usar certificados reales)
    espClient.setInsecure(); // Solo para testing - NO en producción
    
    client.setServer(mqtt_server, mqtt_port);
    client.setCallback(callback);
    
    // Configurar buffer para mensajes grandes
    client.setBufferSize(512);
    
    Serial.println("Sistema iniciado correctamente");
}

void setup_wifi() {
    delay(10);
    Serial.println();
    Serial.print("Conectando a ");
    Serial.println(ssid);
    
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
        delay(500);
        Serial.print(".");
        attempts++;
    }
    
    if (WiFi.status() == WL_CONNECTED) {
        digitalWrite(LED_STATUS, HIGH);
        Serial.println("");
        Serial.println("WiFi conectado");
        Serial.println("IP address: ");
        Serial.println(WiFi.localIP());
    } else {
        Serial.println("Error: No se pudo conectar al WiFi");
        ESP.restart();
    }
}

void callback(char* topic, byte* payload, unsigned int length) {
    Serial.print("Mensaje recibido [");
    Serial.print(topic);
    Serial.print("] ");
    
    String message;
    for (int i = 0; i < length; i++) {
        message += (char)payload[i];
    }
    Serial.println(message);
    
    // Procesar comandos
    if (String(topic) == topic_comandos) {
        processCommand(message);
    }
}

void processCommand(String command) {
    DynamicJsonDocument doc(200);
    DeserializationError error = deserializeJson(doc, command);
    
    if (error) {
        Serial.println("Error parsing JSON command");
        return;
    }
    
    String cmd = doc["command"];
    
    if (cmd == "restart") {
        publishStatus("Reiniciando dispositivo...", 1); // QoS 1 para comandos importantes
        delay(1000);
        ESP.restart();
    } else if (cmd == "status") {
        publishDeviceInfo();
    }
}

void reconnect() {
    while (!client.connected() && reconnect_attempts < max_reconnect_attempts) {
        Serial.print("Intentando conexión MQTT...");
        
        // Intentar conectar con Clean Session = false para persistencia
        if (client.connect(client_id, mqtt_user, mqtt_password, topic_status, 1, true, "offline")) {
            Serial.println("conectado");
            
            // Publicar estado online con retain = true
            publishStatus("online", 1, true);
            
            // Suscribirse a comandos con QoS 1
            client.subscribe(topic_comandos, 1);
            
            // Publicar información del dispositivo
            publishDeviceInfo();
            
            reconnect_attempts = 0;
            digitalWrite(LED_STATUS, HIGH);
        } else {
            Serial.print("falló, rc=");
            Serial.print(client.state());
            Serial.println(" intentando de nuevo en 5 segundos");
            digitalWrite(LED_STATUS, LOW);
            delay(5000);
            reconnect_attempts++;
        }
    }
    
    if (reconnect_attempts >= max_reconnect_attempts) {
        Serial.println("Máximo número de intentos alcanzado. Reiniciando...");
        ESP.restart();
    }
}

SensorData readSensorData() {
    SensorData data;
    data.timestamp = millis();
    
    float h = dht.readHumidity();
    float t = dht.readTemperature();
    
    // Validar lecturas
    if (isnan(h) || isnan(t)) {
        data.valid = false;
        sensor_error = true;
        Serial.println("Error al leer del sensor DHT11!");
    } else {
        // Validar rangos razonables
        if (t >= -40 && t <= 80 && h >= 0 && h <= 100) {
            data.temperature = t;
            data.humidity = h;
            data.valid = true;
            sensor_error = false;
        } else {
            data.valid = false;
            sensor_error = true;
            Serial.println("Valores del sensor fuera de rango");
        }
    }
    
    return data;
}

void publishSensorData(SensorData data) {
    if (!data.valid) return;
    
    // Crear JSON con datos del sensor
    DynamicJsonDocument doc(300);
    doc["device_id"] = client_id;
    doc["timestamp"] = data.timestamp;
    doc["temperature"] = data.temperature;
    doc["humidity"] = data.humidity;
    doc["wifi_rssi"] = WiFi.RSSI();
    doc["free_heap"] = ESP.getFreeHeap();
    
    String jsonString;
    serializeJson(doc, jsonString);
    
    // Publicar temperatura con QoS 1 (datos importantes)
    DynamicJsonDocument tempDoc(200);
    tempDoc["value"] = data.temperature;
    tempDoc["unit"] = "°C";
    tempDoc["timestamp"] = data.timestamp;
    tempDoc["device_id"] = client_id;
    
    String tempString;
    serializeJson(tempDoc, tempString);
    
    bool temp_sent = client.publish(topic_temperatura, tempString.c_str(), true); // retain = true
    
    // Publicar humedad con QoS 1
    DynamicJsonDocument humDoc(200);
    humDoc["value"] = data.humidity;
    humDoc["unit"] = "%";
    humDoc["timestamp"] = data.timestamp;
    humDoc["device_id"] = client_id;
    
    String humString;
    serializeJson(humDoc, humString);
    
    bool hum_sent = client.publish(topic_humedad, humString.c_str(), true); // retain = true
    
    if (temp_sent && hum_sent) {
        Serial.println("Datos enviados correctamente:");
        Serial.println("Temperatura: " + String(data.temperature) + "°C");
        Serial.println("Humedad: " + String(data.humidity) + "%");
    } else {
        Serial.println("Error al enviar datos");
    }
}

void publishStatus(const char* message, int qos = 1, bool retain = true) {
    DynamicJsonDocument doc(200);
    doc["device_id"] = client_id;
    doc["timestamp"] = millis();
    doc["status"] = message;
    String payload;
    serializeJson(doc, payload);
    client.publish(topic_status, payload.c_str(), retain);
}

void publishDeviceInfo() {
    DynamicJsonDocument info(300);
    info["device_id"] = client_id;
    info["ip"] = WiFi.localIP().toString();
    info["rssi"] = WiFi.RSSI();
    info["uptime_ms"] = millis();
    String payload;
    serializeJson(info, payload);
    client.publish(topic_status, payload.c_str(), true);
}

void loop() {
    if (!client.connected()) {
        reconnect();
    }
    client.loop();

    unsigned long now = millis();
    if (now - lastMsg > interval) {
        lastMsg = now;
        SensorData data = readSensorData();
        publishSensorData(data);
    }

    if (now - lastHeartbeat > heartbeat_interval) {
        lastHeartbeat = now;
        publishStatus("heartbeat");
    }
}