Ejercicio 02 — SUSCRIBIRSE a un topic MQTT y actuar
Dificultad: verde · Módulo 10 (ESP32 IoT (MQTT / API))
Enunciado
SUSCRIBIRSE a un topic MQTT y actuar: al recibir “on”/“off” encender/apagar el LED interno del ESP32 (callback).
Montaje (ESP32 DevKit)
Ninguno (LED interno = LED_BUILTIN, GPIO2)
Cómo se resuelve
El ejercicio anterior mandaba datos hacia fuera. Este los recibe: la placa se suscribe a un topic y obedece lo que llegue. Es la otra mitad de MQTT, y la que hace que un broker sea algo más que un buzón de sensores.
El montaje sigue siendo ninguno, porque el actuador es el LED que la placa ya lleva soldado. Ahí aparece el primer detalle práctico:
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endifMuchos cores de Arduino definen LED_BUILTIN por ti, pero el ESP32 DevKit genérico no siempre lo hace. El #ifndef dice “si nadie lo ha definido, lo defino yo como GPIO2”, que es donde está el LED interno de esta placa. Así el sketch compila tanto si el core lo trae como si no, sin tocar nada.
La configuración es la misma del ejercicio 01 (Wokwi-GUEST sin contraseña, broker.hivemq.com en el puerto 1883) con un cambio: el topic ahora es esibot/demo/led, y no lo usamos para publicar sino para escuchar. Sigue siendo un broker público y sin cifrar: cualquiera puede publicar en ese topic, así que, literalmente, cualquiera puede encenderte el LED. En este ejercicio eso es una anécdota divertida; el día que al otro lado del topic haya un relé conectado a algo que se mueve, deja de serlo, y por eso en producción van credenciales y TLS.
La pieza nueva es el callback. Recibir no se hace con una función que se quede esperando un mensaje, porque eso bloquearía la placa entera. Lo que se hace es dejar una función escrita y decirle a la librería “llámala tú cuando llegue algo”:
void alRecibir(char* topic, byte* payload, unsigned int length) {
String msg;
for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
...
}Fíjate bien en la firma, porque ahí está el error clásico. El contenido del mensaje llega como byte* más una longitud aparte, y no termina en \0. No es una cadena de C. Si intentaras hacer String(payload) o compararlo con strcmp, seguirías leyendo memoria más allá del mensaje hasta topar con un cero por casualidad, y verías basura pegada al final. Por eso el bucle recorre exactamente length bytes y los va acumulando en un String: es la forma explícita y segura de reconstruirlo.
También llega el topic, que aquí no hace falta para decidir (solo estamos suscritos a uno) pero se imprime. En cuanto te suscribas a varios topics con el mismo callback, ese parámetro será lo que te diga de cuál viene el mensaje.
La decisión es tan simple como puede ser:
if (msg == "on") digitalWrite(LED_BUILTIN, HIGH);
if (msg == "off") digitalWrite(LED_BUILTIN, LOW);Comparación exacta de texto, en minúsculas y sin espacios. Si publicas ON o on no pasará nada, y eso es una buena primera lección sobre protocolos: los dos extremos tienen que estar de acuerdo en el formato exacto, o no se entienden. Cualquier otro mensaje se ignora en silencio, que es un comportamiento razonable en un topic público al que puede escribir cualquiera.
En conectarMQTT() aparece la línea que da nombre al ejercicio:
mqtt.subscribe(TOPIC);Y está dentro del bloque que se ejecuta cuando mqtt.connect() ha tenido éxito, no en setup(). Eso no es un capricho: la suscripción vive dentro de la sesión MQTT, así que si la conexión se cae y se vuelve a levantar, hay que volver a suscribirse. Poner el subscribe en setup() funcionaría hasta la primera reconexión y luego la placa se quedaría muda para siempre, que es un fallo especialmente molesto porque parece intermitente.
En setup() hay dos cosas que ordenar bien. El LED se prepara con pinMode(LED_BUILTIN, OUTPUT) y se deja apagado explícitamente con digitalWrite(LED_BUILTIN, LOW), para arrancar siempre en un estado conocido en vez de en lo que quedara del arranque. Y el callback se registra con mqtt.setCallback(alRecibir) antes de conectar y suscribirse: se le pasa el nombre de la función sin paréntesis, porque no la estás llamando, estás dando su dirección para que la llame otro.
El loop() se queda en dos líneas, y es la mejor demostración de por qué mqtt.loop() importa:
if (!mqtt.connected()) conectarMQTT();
mqtt.loop();No hay nada más. Todo el trabajo (leer del socket lo que ha mandado el broker, decidir a qué suscripción corresponde e invocar tu callback) ocurre dentro de mqtt.loop(). Si la borras, el programa compila, conecta, dice que está suscrito, y no reacciona jamás a nada. Aquí el síntoma es inconfundible; en el ejercicio 01 era más traicionero, porque las publicaciones parecían salir un rato antes de que el broker cortara.
Para probarlo: arranca el sketch, espera a ver Suscrito a esibot/demo/led en el monitor serie a 115200 baudios, y desde la consola web de HiveMQ (o con mosquitto_pub -h broker.hivemq.com -t esibot/demo/led -m on) publica on y luego off. El LED de la placa obedece en el acto. Acabas de mandar una orden a un dispositivo sin conocer su dirección IP, sin abrirle ningún puerto y sin que él sepa quién eres: eso es exactamente lo que hace útil el patrón publicar/suscribir frente a HTTP, donde para mandarle algo a la placa tendrías que ser tú el servidor y que ella preguntara cada poco.
Para practicar — cópialo y complétalo
Pega este esqueleto y completa los TODO. Es la mejor forma de aprender: inténtalo antes de mirar la solución.
/*
* Curso de Arduino — Módulo 10: ESP32 IoT (MQTT / API)
* Ejercicio 02 — PRÁCTICA (rellena los TODO)
* Enunciado: SUSCRIBIRSE a un topic MQTT y actuar: al recibir "on"/"off"
* encender/apagar el LED interno del ESP32 (callback).
* Dificultad: verde
* Placa: ESP32 DevKit Montaje: ninguno (LED interno = LED_BUILTIN, GPIO2)
* Correr: Arduino IDE / Wokwi (wokwi/ej02) / arduino-cli (ver README)
* NOTA: en Wokwi la red es SSID "Wokwi-GUEST" SIN contraseña.
* Broker MQTT público: broker.hivemq.com puerto 1883.
* Para probar: publica "on" u "off" al topic esibot/demo/led.
*/
#include <WiFi.h>
#include <PubSubClient.h>
#ifndef LED_BUILTIN
#define LED_BUILTIN 2 // el ESP32 DevKit genérico no define LED_BUILTIN; su LED interno es GPIO2
#endif
const char* SSID = "Wokwi-GUEST";
const char* PASSWORD = "";
const char* MQTT_BROKER = "broker.hivemq.com";
const int MQTT_PORT = 1883;
const char* TOPIC = "esibot/demo/led";
WiFiClient espClient;
PubSubClient mqtt(espClient);
void alRecibir(char* topic, byte* payload, unsigned int length) {
String msg;
for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
Serial.print("Mensaje: "); Serial.println(msg);
// TODO: si msg == "on" -> digitalWrite(LED_BUILTIN, HIGH)
// TODO: si msg == "off" -> digitalWrite(LED_BUILTIN, LOW)
}
void conectarWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(250); Serial.print("."); }
Serial.print(" IP="); Serial.println(WiFi.localIP());
}
void conectarMQTT() {
while (!mqtt.connected()) {
String clientId = "esibot-esp32-" + String(random(0xffff), HEX);
if (mqtt.connect(clientId.c_str())) {
// TODO: suscríbete al topic -> mqtt.subscribe(TOPIC)
} else {
delay(2000);
}
}
}
void setup() {
Serial.begin(115200);
delay(100);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
conectarWiFi();
mqtt.setServer(MQTT_BROKER, MQTT_PORT);
// TODO: registra el callback -> mqtt.setCallback(alRecibir)
}
void loop() {
if (!mqtt.connected()) conectarMQTT();
// TODO: llama a mqtt.loop() para procesar los mensajes entrantes
}Solución — cópiala y ejecútala
/*
* Curso de Arduino — Módulo 10: ESP32 IoT (MQTT / API)
* Ejercicio 02 — MODELO (resuelto)
* Enunciado: SUSCRIBIRSE a un topic MQTT y actuar: al recibir "on"/"off"
* encender/apagar el LED interno del ESP32 (callback).
* Dificultad: verde
* Placa: ESP32 DevKit Montaje: ninguno (LED interno = LED_BUILTIN, GPIO2)
* Correr: Arduino IDE / Wokwi (wokwi/ej02) / arduino-cli (ver README)
* NOTA: en Wokwi la red es SSID "Wokwi-GUEST" SIN contraseña.
* Broker MQTT público: broker.hivemq.com puerto 1883.
* Para probar: publica "on" u "off" al topic esibot/demo/led
* (desde otro cliente MQTT, p.ej. la consola web de HiveMQ).
*/
#include <WiFi.h>
#include <PubSubClient.h>
#ifndef LED_BUILTIN
#define LED_BUILTIN 2 // el ESP32 DevKit genérico no define LED_BUILTIN; su LED interno es GPIO2
#endif
const char* SSID = "Wokwi-GUEST";
const char* PASSWORD = "";
const char* MQTT_BROKER = "broker.hivemq.com";
const int MQTT_PORT = 1883;
const char* TOPIC = "esibot/demo/led"; // topic que escuchamos
WiFiClient espClient;
PubSubClient mqtt(espClient);
// Callback: PubSubClient lo llama cada vez que llega un mensaje a un topic suscrito.
// El payload NO es texto terminado en \0: viene con su longitud (length) aparte.
void alRecibir(char* topic, byte* payload, unsigned int length) {
String msg;
for (unsigned int i = 0; i < length; i++) msg += (char)payload[i]; // bytes -> String
Serial.print("Mensaje en ");
Serial.print(topic);
Serial.print(": ");
Serial.println(msg);
if (msg == "on") digitalWrite(LED_BUILTIN, HIGH); // enciende LED
if (msg == "off") digitalWrite(LED_BUILTIN, LOW); // apaga LED
}
void conectarWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
Serial.print("Conectando WiFi");
while (WiFi.status() != WL_CONNECTED) { delay(250); Serial.print("."); }
Serial.print(" OK IP=");
Serial.println(WiFi.localIP());
}
void conectarMQTT() {
while (!mqtt.connected()) {
String clientId = "esibot-esp32-" + String(random(0xffff), HEX);
if (mqtt.connect(clientId.c_str())) {
Serial.println("MQTT conectado.");
mqtt.subscribe(TOPIC); // SUBSCRIBE: nos apuntamos al topic (hay que rehacerlo tras cada reconexión)
Serial.print("Suscrito a ");
Serial.println(TOPIC);
} else {
Serial.print("fallo MQTT rc=");
Serial.print(mqtt.state());
Serial.println(", reintento en 2 s");
delay(2000);
}
}
}
void setup() {
Serial.begin(115200);
delay(100);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW); // arrancamos con el LED apagado
conectarWiFi();
mqtt.setServer(MQTT_BROKER, MQTT_PORT);
mqtt.setCallback(alRecibir); // registramos la función callback
}
void loop() {
if (!mqtt.connected()) conectarMQTT();
mqtt.loop(); // imprescindible: aquí PubSubClient procesa los mensajes entrantes
}Cómo ejecutar la simulación
Sin instalar nada, en el navegador
- Abre wokwi.com y crea un proyecto nuevo de la placa que pide el ejercicio.
- Pega la solución (o tu intento) en la pestaña
sketch.ino. - Abre la pestaña
diagram.json, borra lo que haya y pega el circuito de aquí abajo. - Dale al play. Verás el circuito funcionando: LEDs, pantallas y monitor serie.
Circuito del ejercicio (diagram.json)
{
"version": 1,
"author": "ESIBot",
"editor": "wokwi",
"parts": [
{ "type": "board-esp32-devkit-c-v4", "id": "esp", "top": 0, "left": 0, "attrs": {} }
],
"connections": [],
"dependencies": {}
}En VS Code, con el repo del curso
El paquete de simulación de este ejercicio está en practica/10-esp32-iot-mqtt/wokwi/ej02/. Con la extensión Wokwi instalada y la licencia activada (lo cuenta el onboarding del curso):
make sim EJ=10-esp32-iot-mqtt/ej02y después, en VS Code, Ctrl+Shift+P → Wokwi: Start Simulator. La extensión no compila: simula un firmware ya compilado, y de compilarlo se encarga ese make.
Cómo usarlo
Con placa: abre el .ino en el Arduino IDE, elige placa y puerto y pulsa Subir. Sin placa: simulalo en Wokwi con cualquiera de las dos vias de arriba. Antes de mirar la solucion, intenta completar tu el esqueleto: es la mejor forma de aprender.