Suscripción MQTT y control de actuadores
Hasta ahora el ESP32 solo publica datos (sensor → broker → Node-RED). En este tutorial lo configuraremos también como suscriptor: Node-RED enviará comandos MQTT y el ESP32 los recibirá para controlar actuadores como LEDs o relés.
Arquitectura bidireccional
Sección titulada «Arquitectura bidireccional»[Node-RED] │ publica en "actuadores/led" │ payload: {"estado": true} ▼[Mosquitto broker] │ entrega a suscriptores del topic ▼[ESP32] │ enciende/apaga LED según el comando │ publica confirmación en "actuadores/led/estado" ▼[Mosquitto broker] ▼[Node-RED dashboard] ← muestra el estado actual confirmadoPaso 1: Sketch — publicar y suscribirse simultáneamente
Sección titulada «Paso 1: Sketch — publicar y suscribirse simultáneamente»#include <WiFi.h>#include <PubSubClient.h>#include <ArduinoJson.h>
// ── Configuración WiFi ─────────────────────────────────────────────────────const char* WIFI_SSID = "TU_NOMBRE_DE_RED";const char* WIFI_PASSWORD = "TU_PASSWORD_WIFI";
// ── Configuración MQTT ─────────────────────────────────────────────────────const char* MQTT_SERVER = "192.168.1.100";const int MQTT_PORT = 1883;const char* MQTT_CLIENT = "esp32-actuador-01";
// Topicsconst char* TOPIC_CMD = "actuadores/led"; // recibe comandosconst char* TOPIC_ESTADO = "actuadores/led/estado"; // confirma estadoconst char* TOPIC_SENSOR = "sensores/temperatura"; // publica datos
// ── Pines ──────────────────────────────────────────────────────────────────const int LED_PIN = 2; // LED integrado en la mayoría de placas ESP32
const unsigned long PUBLISH_INTERVAL = 10000;
WiFiClient espClient;PubSubClient mqtt(espClient);unsigned long lastPublish = 0;bool estadoLED = false;
// ──────────────────────────────────────────────────────────────────────────// Callback: se ejecuta cada vez que llega un mensaje en un topic suscrito// ──────────────────────────────────────────────────────────────────────────void onMensaje(char* topic, byte* payload, unsigned int length) { // Convertir payload a string terminado en null char msg[length + 1]; memcpy(msg, payload, length); msg[length] = '\0';
Serial.print("Mensaje recibido ["); Serial.print(topic); Serial.print("]: "); Serial.println(msg);
// Parsear JSON JsonDocument doc; DeserializationError error = deserializeJson(doc, msg); if (error) { Serial.print("Error JSON: "); Serial.println(error.c_str()); return; }
// Procesar comando según el topic if (strcmp(topic, TOPIC_CMD) == 0) { if (doc["estado"].is<bool>()) { estadoLED = doc["estado"].as<bool>(); } else if (doc["estado"].is<int>()) { estadoLED = doc["estado"].as<int>() != 0; }
digitalWrite(LED_PIN, estadoLED ? HIGH : LOW); Serial.print("LED -> "); Serial.println(estadoLED ? "ENCENDIDO" : "APAGADO");
// Publicar confirmación de estado JsonDocument confirm; confirm["estado"] = estadoLED; confirm["sensor"] = MQTT_CLIENT; confirm["uptime_s"] = millis() / 1000;
char confirmPayload[128]; serializeJson(confirm, confirmPayload); mqtt.publish(TOPIC_ESTADO, confirmPayload, true); // true = mensaje retenido }}
// ──────────────────────────────────────────────────────────────────────────void conectarWiFi() { Serial.print("Conectando a WiFi"); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("WiFi conectado — IP: "); Serial.println(WiFi.localIP());}
void conectarMQTT() { while (!mqtt.connected()) { Serial.print("Conectando a MQTT..."); if (mqtt.connect(MQTT_CLIENT)) { Serial.println(" conectado!"); // Suscribirse al topic de comandos después de cada (re)conexión mqtt.subscribe(TOPIC_CMD); Serial.print("Suscrito a: "); Serial.println(TOPIC_CMD); } else { Serial.print(" error rc="); Serial.print(mqtt.state()); Serial.println(" — reintentando en 5s"); delay(5000); } }}
// ──────────────────────────────────────────────────────────────────────────void setup() { Serial.begin(115200); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW);
conectarWiFi(); mqtt.setServer(MQTT_SERVER, MQTT_PORT); mqtt.setCallback(onMensaje); // registrar la función de callback}
// ──────────────────────────────────────────────────────────────────────────void loop() { if (!mqtt.connected()) conectarMQTT(); mqtt.loop(); // ← procesa mensajes entrantes y mantiene la conexión viva
// Publicar sensor periódicamente unsigned long ahora = millis(); if (ahora - lastPublish >= PUBLISH_INTERVAL) { lastPublish = ahora;
float temperatura = 20.0 + (random(0, 100) / 10.0);
JsonDocument doc; doc["valor"] = temperatura; doc["unidad"] = "C"; doc["sensor"] = MQTT_CLIENT; doc["uptime_s"] = millis() / 1000;
char payload[128]; serializeJson(doc, payload); mqtt.publish(TOPIC_SENSOR, payload); }}Punto crítico: mqtt.loop()
Sección titulada «Punto crítico: mqtt.loop()»La línea mqtt.loop() en el loop() principal es obligatoria. Es la que:
- Procesa los mensajes entrantes y llama al callback
onMensaje - Envía los pings de keepalive al broker para mantener la conexión activa
- Sin ella, el ESP32 se desconectará del broker en ~60 segundos
Paso 2: Enviar comandos desde Node-RED
Sección titulada «Paso 2: Enviar comandos desde Node-RED»Flujo de control en Node-RED
Sección titulada «Flujo de control en Node-RED»[ui-switch] ──→ [change] ──→ [mqtt out]-
En Node-RED, arrastra un nodo
ui-switch(del Dashboard)- Label:
LED - On payload:
{"estado": true}(tipo JSON) - Off payload:
{"estado": false}(tipo JSON)
- Label:
-
Conecta a un nodo
mqtt out:- Server:
mosquitto:1883 - Topic:
actuadores/led - QoS: 1
- Retain: desactivado (el comando no debe ser retenido)
- Server:
-
Para mostrar el estado confirmado de vuelta:
- Nodo
mqtt insuscrito aactuadores/led/estado - Conecta a un nodo
ui-textoui-led
- Nodo
Prueba desde la terminal
Sección titulada «Prueba desde la terminal»También puedes enviar comandos manualmente desde tu PC:
# Encender el LEDdocker exec -it mosquitto mosquitto_pub \ -h localhost -t "actuadores/led" \ -m '{"estado": true}'
# Apagar el LEDdocker exec -it mosquitto mosquitto_pub \ -h localhost -t "actuadores/led" \ -m '{"estado": false}'
# Ver la confirmación del ESP32docker exec -it mosquitto mosquitto_sub \ -h localhost -t "actuadores/led/estado" -vPaso 3: Control de relé (carga de 220V)
Sección titulada «Paso 3: Control de relé (carga de 220V)»Para controlar cargas de 220V (luces, electrodomésticos) usa un módulo relé de 5V controlado por el ESP32.
Conexión
Sección titulada «Conexión»Módulo Relé ESP32─────────── ──────VCC ──→ VIN (5V) ← usa el pin de 5V, no 3.3VGND ──→ GNDIN ──→ GPIO5 (cualquier GPIO digital)
Carga 220V va en los terminales COM/NO del reléAjuste en el sketch
Sección titulada «Ajuste en el sketch»const int RELAY_PIN = 5;
// En setup()pinMode(RELAY_PIN, OUTPUT);digitalWrite(RELAY_PIN, HIGH); // HIGH = relé apagado (lógica invertida en la mayoría de módulos)
// En el callback, reemplaza la lógica del LED:bool activar = doc["estado"].as<bool>();digitalWrite(RELAY_PIN, activar ? LOW : HIGH); // LOW = relé encendidoBuenas prácticas para proyectos reales
Sección titulada «Buenas prácticas para proyectos reales»Manejo de reconexión robusta
Sección titulada «Manejo de reconexión robusta»// En loop(), antes de mqtt.loop()if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi perdido — reconectando..."); WiFi.reconnect(); delay(5000); return;}
if (!mqtt.connected()) { conectarMQTT();}Guardar estado en memoria no volátil
Sección titulada «Guardar estado en memoria no volátil»Si el ESP32 se reinicia, puede recuperar el último estado guardado usando Preferences:
#include <Preferences.h>Preferences prefs;
// En setup(), después de conectar MQTT:prefs.begin("estado", false);estadoLED = prefs.getBool("led", false); // leer último estadodigitalWrite(LED_PIN, estadoLED ? HIGH : LOW);
// Cuando cambia el estado, guardar:prefs.putBool("led", estadoLED);Mensaje retenido en el topic de comandos
Sección titulada «Mensaje retenido en el topic de comandos»Si el ESP32 se reinicia y el relé debe mantener su estado anterior, publica el comando con retain = true desde Node-RED. Mosquitto reenviará el último mensaje retenido al ESP32 cuando se reconecte y suscriba.