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");
}
}