Ejercicio 01 — Conectar el ESP32 a WiFi y PUBLICAR un contador a un topic MQTT cada…

Dificultad: verde · Módulo 10 (ESP32 IoT (MQTT / API))

Enunciado

Conectar el ESP32 a WiFi y PUBLICAR un contador a un topic MQTT cada 2 s.

Montaje (ESP32 DevKit)

Ninguno (solo la placa)

Cómo se resuelve

El primer contacto con MQTT, y el ejercicio en el que se ve el “ajá” del módulo: un número sale de la placa y aparece en otra pantalla, sin que la placa sepa quién lo está mirando.

El programa no monta nada: solo el ESP32. Todo el trabajo es de red, y se apoya en dos librerías. WiFi.h viene con el core del ESP32 y ya la conoces del módulo anterior; PubSubClient.h hay que instalarla aparte (en Wokwi está declarada en wokwi/ej01/libraries.txt, en el IDE se busca en el Gestor de Librerías).

La configuración está toda arriba, en constantes con nombre, que es como debe estar:

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/contador";

La red Wokwi-GUEST sin contraseña es la red simulada del simulador, y aunque sea simulada tiene salida real a internet: por eso un broker público funciona desde la simulación igual que funcionaría desde una placa de verdad. Con hardware físico, aquí pondrías el nombre y la clave de tu red de 2.4 GHz.

El broker es broker.hivemq.com en el puerto 1883, que es el puerto de MQTT sin cifrar. Conviene tener muy claro lo que eso significa: es un broker público de pruebas, sin credenciales, y cualquier persona del mundo puede suscribirse a esibot/demo/contador y leer lo que publicas, o publicar ahí ella misma y hacer que tú leas lo suyo. Para aprender es perfecto, precisamente porque puedes abrir la consola web de HiveMQ en el navegador y ver llegar tus mensajes sin montar nada. Para cualquier cosa real, no: ahí van credenciales, TLS en el puerto 8883, y los secretos fuera del sketch.

Luego vienen las dos líneas que arman el cliente:

WiFiClient   espClient;
PubSubClient mqtt(espClient);

PubSubClient no sabe conectarse al WiFi; solo sabe hablar el protocolo MQTT por encima de un socket que alguien le dé. Ese socket es el WiFiClient. Por eso el orden importa: primero WiFi, y solo después MQTT.

conectarWiFi() es lo del módulo 09: modo estación, WiFi.begin, y un bucle que espera con delay(250) imprimiendo un punto hasta que WiFi.status() devuelve WL_CONNECTED. Aquí sí se puede bloquear, porque todavía no hay nada que atender.

conectarMQTT() tiene el detalle más interesante del ejercicio:

String clientId = "esibot-esp32-" + String(random(0xffff), HEX);

El clientId es el nombre con el que el broker te identifica, y tiene que ser único. Si dos placas se presentan con el mismo, el broker desconecta a la primera cuando entra la segunda, y acabas con dos placas echándose la una a la otra en bucle sin que ninguna llegue a publicar. En un broker público, donde hay gente que no conoces conectada, un nombre fijo tipo esp32 es casi garantía de choque. El sufijo aleatorio en hexadecimal lo evita.

El resto de la función es un while (!mqtt.connected()) que reintenta: si mqtt.connect() devuelve falso, imprime mqtt.state() (el código numérico de error de PubSubClient, útil para distinguir “no llego al broker” de “el broker me rechaza”) y espera delay(2000) antes de volver a intentarlo.

En setup() solo hay tres cosas: arrancar el serie a 115200 baudios, conectar el WiFi y decirle al cliente a dónde apuntar con mqtt.setServer(MQTT_BROKER, MQTT_PORT). Fíjate en que setServer no conecta: solo guarda la dirección. La conexión se intenta en el loop(), y eso es deliberado: así el mismo código sirve para conectar la primera vez y para reconectar cuando se caiga.

El loop() tiene tres bloques y ninguno sobra:

if (!mqtt.connected()) conectarMQTT();
mqtt.loop();
 
if (millis() - ultimo >= 2000) { ... }

El primero se ocupa de que siempre haya conexión. El segundo, mqtt.loop(), es el que casi todo el mundo olvida: PubSubClient no tiene hilos ni interrupciones, no hace absolutamente nada por su cuenta, y es dentro de esa llamada donde lee del socket y responde a los pings de mantenimiento del broker. Si falta, a los pocos segundos el broker considera que has desaparecido y te desconecta. Tiene que ejecutarse en todas las vueltas, también en las que no toca publicar.

El tercero es el patrón millis() del módulo 02, y aquí se entiende de verdad por qué no vale delay(2000): durante esos dos segundos de bloqueo, mqtt.loop() no se ejecutaría y la conexión se caería. Con millis() la placa da miles de vueltas entre publicación y publicación, atendiendo el protocolo, y solo entra en el bloque cuando han pasado 2000 ms desde ultimo.

Dentro, lo que se publica:

char payload[16];
snprintf(payload, sizeof(payload), "%d", contador);
mqtt.publish(TOPIC, payload);

MQTT transporta bytes, no enteros: el número hay que convertirlo a texto antes. snprintf lo formatea en un buffer de 16 caracteres, de sobra para cualquier int, y el sizeof garantiza que no se desborde. El payload que viaja es literalmente 1, luego 2, luego 3, uno cada dos segundos, y el Serial.print de después te deja comprobar en el monitor serie lo mismo que deberías estar viendo llegar al broker.

Para cerrar el círculo, abre la consola web de HiveMQ (o usa mosquitto_sub -h broker.hivemq.com -t esibot/demo/contador), suscríbete a ese topic y déjalo mirando. Los números aparecen ahí sin que la placa haya sabido nunca de tu existencia: eso es el desacoplamiento del que habla la teoría.

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 01 — PRÁCTICA (rellena los TODO)
 * Enunciado: Conectar el ESP32 a WiFi y PUBLICAR un contador a un topic MQTT cada 2 s.
 * Dificultad: verde
 * Placa: ESP32 DevKit    Montaje: ninguno (solo la placa)
 * Correr: Arduino IDE / Wokwi (wokwi/ej01) / arduino-cli (ver README)
 * NOTA: en Wokwi la red es SSID "Wokwi-GUEST" SIN contraseña.
 *       Broker MQTT público de pruebas: broker.hivemq.com puerto 1883 (sin TLS).
 *       Librería necesaria: PubSubClient (ver wokwi/ej01/libraries.txt).
 */
#include <WiFi.h>
// TODO: incluye la librería MQTT -> #include <PubSubClient.h>
 
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/contador";
 
WiFiClient espClient;
// TODO: crea el cliente MQTT sobre espClient -> PubSubClient mqtt(espClient);
 
int   contador      = 0;
unsigned long ultimo = 0;
 
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() {
    // TODO: bucle while (!mqtt.connected()) que intente mqtt.connect(clientId)
    //       usa un clientId único, p.ej. "esibot-esp32-" + String(random(0xffff), HEX)
    //       si falla, delay(2000) y reintenta
}
 
void setup() {
    Serial.begin(115200);
    delay(100);
    conectarWiFi();
    // TODO: configura el broker -> mqtt.setServer(MQTT_BROKER, MQTT_PORT)
}
 
void loop() {
    // TODO: si !mqtt.connected() llama a conectarMQTT()
    // TODO: llama a mqtt.loop() para mantener viva la conexión
 
    if (millis() - ultimo >= 2000) {
        ultimo = millis();
        contador++;
        char payload[16];
        snprintf(payload, sizeof(payload), "%d", contador);
        // TODO: publica el contador -> mqtt.publish(TOPIC, payload)
        Serial.println(payload);
    }
}

Solución — cópiala y ejecútala

/*
 * Curso de Arduino — Módulo 10: ESP32 IoT (MQTT / API)
 * Ejercicio 01 — MODELO (resuelto)
 * Enunciado: Conectar el ESP32 a WiFi y PUBLICAR un contador a un topic MQTT cada 2 s.
 * Dificultad: verde
 * Placa: ESP32 DevKit    Montaje: ninguno (solo la placa)
 * Correr: Arduino IDE / Wokwi (wokwi/ej01) / arduino-cli (ver README)
 * NOTA: en Wokwi la red es SSID "Wokwi-GUEST" SIN contraseña.
 *       Broker MQTT público de pruebas: broker.hivemq.com puerto 1883 (sin TLS).
 *       Librería necesaria: PubSubClient (ver wokwi/ej01/libraries.txt).
 */
#include <WiFi.h>                          // WiFi del core ESP32
#include <PubSubClient.h>                  // cliente MQTT ligero
 
const char* SSID     = "Wokwi-GUEST";      // red WiFi (Wokwi sin contraseña)
const char* PASSWORD = "";
 
const char* MQTT_BROKER = "broker.hivemq.com";  // broker público de pruebas
const int   MQTT_PORT   = 1883;                  // puerto MQTT sin cifrar
const char* TOPIC       = "esibot/demo/contador";  // donde publicamos
 
WiFiClient   espClient;                    // socket TCP que usará MQTT
PubSubClient mqtt(espClient);              // cliente MQTT montado sobre ese socket
 
int   contador      = 0;
unsigned long ultimo = 0;                  // marca de tiempo del último envío
 
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()) {            // reintentar hasta enganchar al broker
        // el clientID debe ser único en el broker: añadimos un número aleatorio
        String clientId = "esibot-esp32-" + String(random(0xffff), HEX);
        Serial.print("Conectando MQTT como ");
        Serial.print(clientId);
        if (mqtt.connect(clientId.c_str())) {
            Serial.println(" ... conectado.");
        } else {
            Serial.print(" fallo rc=");
            Serial.print(mqtt.state());    // código de error de PubSubClient
            Serial.println(", reintento en 2 s");
            delay(2000);
        }
    }
}
 
void setup() {
    Serial.begin(115200);
    delay(100);
    conectarWiFi();
    mqtt.setServer(MQTT_BROKER, MQTT_PORT);  // a qué broker apuntamos
}
 
void loop() {
    if (!mqtt.connected()) conectarMQTT();   // si se cayó, reconectar
    mqtt.loop();                             // mantiene viva la conexión MQTT
 
    if (millis() - ultimo >= 2000) {         // cada 2 s, sin bloquear con delay
        ultimo = millis();
        contador++;
        char payload[16];
        snprintf(payload, sizeof(payload), "%d", contador);  // número -> texto
        mqtt.publish(TOPIC, payload);        // PUBLISH al topic
        Serial.print("Publicado en ");
        Serial.print(TOPIC);
        Serial.print(": ");
        Serial.println(payload);
    }
}

Cómo ejecutar la simulación

Sin instalar nada, en el navegador

  1. Abre wokwi.com y crea un proyecto nuevo de la placa que pide el ejercicio.
  2. Pega la solución (o tu intento) en la pestaña sketch.ino.
  3. Abre la pestaña diagram.json, borra lo que haya y pega el circuito de aquí abajo.
  4. 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/ej01/. Con la extensión Wokwi instalada y la licencia activada (lo cuenta el onboarding del curso):

make sim EJ=10-esp32-iot-mqtt/ej01

y después, en VS Code, Ctrl+Shift+PWokwi: 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.

Conexiones