Este tutorial de ESP32 MQTT te lleva de un sketch de Arduino vacío a una placa que publica telemetría en JSON, escucha comandos y enciende o apaga su LED integrado. Primero te conectarás a broker.connectmq.com, el servidor MQTT público gratuito de ConnectMQ, y después pasarás el mismo sketch a TLS en el puerto 8883 con credenciales del portal de ConnectMQ. Cada paso se verifica con mosquitto_sub y mosquitto_pub desde tu ordenador.
Qué vas a construir
El dispositivo terminado hace tres cosas:
- Se conecta al Wi-Fi y a un broker MQTT, con reconexión automática.
- Publica un mensaje JSON como
{"temperature":23.41,"uptime":125}cada cinco segundos. - Se suscribe a un topic de comandos y enciende o apaga el LED integrado cuando recibe
ON,OFFoTOGGLE.
La estructura de topics mantiene todo bajo un único prefijo:
| Topic | Dirección | Payload de ejemplo |
|---|---|---|
connectmq/demo/<deviceId>/telemetry |
El ESP32 publica | {"temperature":23.41,"uptime":125} |
connectmq/demo/<deviceId>/led/set |
El ESP32 se suscribe | ON, OFF, TOGGLE |
connectmq/demo/<deviceId>/led/state |
El ESP32 publica (retenido) | ON u OFF |
Si términos de MQTT como topic, QoS o retained message son nuevos para ti, lee primero ¿Qué es un broker MQTT?.
Requisitos previos
- Una placa de desarrollo ESP32 (cualquier ESP32 DevKit sirve; la mayoría tiene un LED azul en el GPIO 2).
- Arduino IDE 2.x con el paquete de placas esp32 instalado desde el Gestor de placas.
- La librería PubSubClient de Nick O'Leary, instalada desde el Gestor de librerías.
- Una red Wi-Fi de 2,4 GHz (el ESP32 no admite 5 GHz).
- Opcional: los clientes de línea de comandos de Mosquitto (
mosquitto_sub,mosquitto_pub) en tu ordenador para hacer pruebas. - Opcional: un sensor DHT22. Por defecto el sketch simula la temperatura, así que no necesitas hardware adicional.
Endpoints del broker para ESP32 MQTT
broker.connectmq.com expone cuatro listeners. El ESP32 usa los dos primeros; los puertos WebSocket son para navegadores.
| Protocolo | Puerto | Uso en este tutorial |
|---|---|---|
| MQTT (TCP) | 1883 | Parte 1, área pública anónima |
| MQTT sobre TLS | 8883 | Parte 2, credenciales del portal |
| WebSocket | 8083 | No se usa (clientes de navegador) |
| WebSocket seguro | 8084 | No se usa (clientes de navegador) |
El área pública no necesita cuenta: conéctate sin usuario ni contraseña y con un client ID único y no vacío. Ten en cuenta que todo lo que se publica allí puede leerlo cualquiera, así que envía solo datos de demostración. El área pública permite 64 KB por mensaje, 10 mensajes por segundo y 10 suscripciones por cliente, y hasta 8 niveles de topic, más que suficiente para este proyecto. El servidor público es solo para pruebas y prototipos, no para datos de producción.
Parte 1: ESP32 MQTT sobre TCP sin cifrar (puerto 1883)
Genera un client ID único a partir de la dirección MAC
Los client IDs deben ser únicos: si dos clientes se conectan con el mismo ID, el broker desconecta al más antiguo. Cada ESP32 tiene una dirección MAC programada de fábrica, que es la fuente perfecta para un ID estable y único. El sketch construye algo como esp32-a1b2c3d4e5f6 y lo reutiliza en el prefijo de los topics.
El sketch completo
Crea un sketch nuevo, pega el código siguiente y configura el SSID y la contraseña de tu Wi-Fi.
#include <WiFi.h>
#include <PubSubClient.h>
// ---- Wi-Fi ----
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
// ---- MQTT ----
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const int LED_PIN = 2; // LED integrado en la mayoría de ESP32 DevKit
const unsigned long PUBLISH_INTERVAL_MS = 5000;
WiFiClient netClient;
PubSubClient mqtt(netClient);
String deviceId;
String topicTelemetry, topicLedSet, topicLedState;
unsigned long lastPublish = 0;
unsigned long lastReconnectAttempt = 0;
bool ledOn = false;
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print("\nWi-Fi connected, IP: ");
Serial.println(WiFi.localIP());
}
String makeDeviceId() {
uint8_t mac[6];
WiFi.macAddress(mac);
char buf[20];
snprintf(buf, sizeof(buf), "esp32-%02x%02x%02x%02x%02x%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return String(buf);
}
void publishLedState() {
mqtt.publish(topicLedState.c_str(), ledOn ? "ON" : "OFF", true); // retenido
}
void onMessage(char* topic, byte* payload, unsigned int length) {
String msg;
for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
msg.trim();
Serial.printf("Message on %s: %s\n", topic, msg.c_str());
if (String(topic) != topicLedSet) return;
if (msg.equalsIgnoreCase("ON")) ledOn = true;
else if (msg.equalsIgnoreCase("OFF")) ledOn = false;
else if (msg.equalsIgnoreCase("TOGGLE")) ledOn = !ledOn;
else return; // ignorar cualquier otra cosa
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
publishLedState();
}
bool connectMqtt() {
Serial.printf("Connecting to MQTT as %s ... ", deviceId.c_str());
if (mqtt.connect(deviceId.c_str())) { // sin usuario/contraseña: área pública
Serial.println("connected");
mqtt.subscribe(topicLedSet.c_str(), 1);
publishLedState();
return true;
}
Serial.printf("failed, state=%d\n", mqtt.state());
return false;
}
float readTemperature() {
// Sensor simulado: 22-25 °C con un poco de ruido
return 23.5 + 1.5 * sin(millis() / 60000.0) + random(-20, 20) / 100.0;
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
connectWiFi();
deviceId = makeDeviceId();
String base = "connectmq/demo/" + deviceId;
topicTelemetry = base + "/telemetry";
topicLedSet = base + "/led/set";
topicLedState = base + "/led/state";
Serial.println("Topic prefix: " + base);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setBufferSize(512); // el valor por defecto es 256 bytes, ver más abajo
mqtt.setKeepAlive(30);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
}
if (!mqtt.connected()) {
unsigned long now = millis();
if (now - lastReconnectAttempt > 5000) { // reintento no bloqueante cada 5 s
lastReconnectAttempt = now;
if (connectMqtt()) lastReconnectAttempt = 0;
}
return;
}
mqtt.loop(); // procesar mensajes entrantes y keepalive
if (millis() - lastPublish >= PUBLISH_INTERVAL_MS) {
lastPublish = millis();
char json[96];
snprintf(json, sizeof(json), "{\"temperature\":%.2f,\"uptime\":%lu}",
readTemperature(), millis() / 1000);
mqtt.publish(topicTelemetry.c_str(), json);
Serial.printf("Published %s\n", json);
}
}
Selecciona tu placa y tu puerto, sube el sketch y abre el Monitor Serie a 115200 baudios. Deberías ver la dirección IP, el prefijo de topics y una línea Published cada cinco segundos. Copia el ID del dispositivo que aparece en el prefijo; lo necesitarás en el siguiente paso.
Cómo funciona la lógica de reconexión
El sketch nunca se queda bloqueado indefinidamente esperando al broker. Si la conexión se cae, loop() lo vuelve a intentar cada cinco segundos mientras el resto del programa sigue funcionando. Hay dos detalles importantes:
mqtt.loop()debe ejecutarse a menudo. Lee los paquetes entrantes y envía los pings de keepalive; si llamas adelay(10000)en tu bucle, el broker dará el cliente por muerto.- Tras reconectarse, el sketch vuelve a suscribirse. Con la clean session por defecto, las suscripciones no sobreviven a una desconexión, así que volver a suscribirse en
connectMqtt()es imprescindible.
mqtt.state() devuelve un código numérico cuando algo falla: -2 suele indicar un problema de red (host incorrecto, sin internet), mientras que los valores positivos proceden del CONNACK del broker, por ejemplo 2 cuando se rechaza el identificador.
Usar un sensor DHT22 real
Para usar datos reales, instala la «DHT sensor library» de Adafruit, conecta el pin de datos del sensor al GPIO 4 y sustituye readTemperature():
#include <DHT.h>
DHT dht(4, DHT22); // llama a dht.begin() en setup()
float readTemperature() {
float t = dht.readTemperature();
return isnan(t) ? -127.0 : t; // enviar un valor centinela en lugar de NaN
}
Verifica con mosquitto_sub y mosquitto_pub
Abre una terminal y suscríbete a todo lo que cuelga del prefijo de tu dispositivo. Sustituye esp32-a1b2c3d4e5f6 por el ID que aparece en el Monitor Serie.
mosquitto_sub -h broker.connectmq.com -p 1883 -i laptop-watch-81f3 \
-t "connectmq/demo/esp32-a1b2c3d4e5f6/#" -v
La salida será como esta:
connectmq/demo/esp32-a1b2c3d4e5f6/led/state OFF
connectmq/demo/esp32-a1b2c3d4e5f6/telemetry {"temperature":23.41,"uptime":125}
connectmq/demo/esp32-a1b2c3d4e5f6/telemetry {"temperature":23.47,"uptime":130}
Ahora cambia el LED desde una segunda terminal:
mosquitto_pub -h broker.connectmq.com -p 1883 -i laptop-cmd-81f3 \
-t "connectmq/demo/esp32-a1b2c3d4e5f6/led/set" -q 1 -m "TOGGLE"
El LED cambia y el suscriptor muestra el nuevo led/state retenido. Como el estado es retenido, cualquier panel que se suscriba más tarde recibe al instante el valor actual. La suscripción usa QoS 1 para que el broker confirme el comando; Niveles de QoS en MQTT explicados analiza los pros y contras.
También puedes ver cómo el dispositivo se conecta y desconecta: suscríbete a $SYS/broker/connection/esp32-a1b2c3d4e5f6/state y verás 1 cuando se conecta y 0 cuando se desconecta.
El tamaño del búfer de PubSubClient
PubSubClient reserva un único búfer para los paquetes entrantes y salientes, y por defecto solo tiene 256 bytes. Ese búfer debe contener el paquete MQTT completo: cabecera fija, nombre del topic y payload. Un topic largo más un documento JSON de tamaño moderado lo superan enseguida y, cuando eso ocurre, publish() devuelve false en silencio y los mensajes entrantes que no caben se descartan.
Llama a setBufferSize() antes de conectarte y comprueba el valor de retorno de publish():
| Tamaño del payload (aprox.) | setBufferSize() recomendado |
|---|---|
| Hasta ~150 bytes | 256 (por defecto) |
| Hasta ~400 bytes | 512 |
| Hasta ~1.800 bytes | 2048 |
| Documentos más grandes | Divide el mensaje o envía solo los campos que cambian |
Cada byte adicional sale de la RAM del ESP32, así que dimensiónalo según tus mensajes reales y no según el máximo del broker.
Parte 2: ESP32 MQTT seguro con TLS en el puerto 8883
El área pública es perfecta para aprender, pero los dispositivos de producción deben usar TLS y sus propias credenciales. Inicia sesión en https://app.connectmq.com, crea un broker MQTT gestionado y copia el usuario, la contraseña y el client ID generados. Los clientes autenticados llegan a tu propio inquilino aislado en lugar de al área pública.
En las conexiones autenticadas, los client IDs están restringidos al ID emitido y a sus variantes _suffix. Da a cada dispositivo su propio sufijo, por ejemplo YOUR_CLIENT_ID_esp32a, y otro distinto a tu portátil.
Cambios en el sketch
Solo cambian el cliente de red, el puerto, las credenciales y los topics. Descarga el certificado ISRG Root X1 (isrgrootx1.pem) de Let's Encrypt; la cadena del certificado del broker llega hasta él. Pega su contenido en el sketch:
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <time.h>
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 8883;
const char* MQTT_USER = "YOUR_USERNAME";
const char* MQTT_PASS = "YOUR_PASSWORD";
const char* MQTT_CLIENT_ID = "YOUR_CLIENT_ID_esp32a";
// ISRG Root X1 (pega el PEM completo de isrgrootx1.pem)
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the certificate body here...
-----END CERTIFICATE-----
)EOF";
WiFiClientSecure netClient;
PubSubClient mqtt(netClient);
void syncClock() {
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
time_t now = time(nullptr);
while (now < 1700000000) { delay(250); now = time(nullptr); }
}
// en setup(), después de connectWiFi():
// syncClock();
// netClient.setCACert(ROOT_CA);
// mqtt.setServer(MQTT_HOST, MQTT_PORT);
// base de topics: "plant1/" + String(MQTT_CLIENT_ID) en lugar de "connectmq/demo/..."
bool connectMqtt() {
if (mqtt.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS)) {
mqtt.subscribe(topicLedSet.c_str(), 1);
publishLedState();
return true;
}
return false;
}
Algunas notas:
setCACert()hace que el ESP32 verifique que realmente está hablando conbroker.connectmq.com. Sincronizar antes el reloj por NTP permite a la pila TLS comprobar las fechas de validez del certificado.setInsecure()se salta por completo la verificación. Es práctico para una prueba de cinco minutos, pero elimina la protección frente a ataques de intermediario, así que nunca lo incluyas en un producto.- TLS consume bastante más RAM y el primer handshake tarda un momento. Es normal en el ESP32.
- Mantén las credenciales fuera de tu repositorio Git, por ejemplo en un
secrets.hseparado e incluido en.gitignore. La lista de seguridad MQTT trata el almacenamiento de secretos en dispositivos y otras medidas de refuerzo.
Verifica la conexión segura
Suscríbete con tus propias credenciales y un sufijo distinto. En Linux puedes usar el almacén de CA del sistema con --capath /etc/ssl/certs; en cualquier sistema operativo puedes pasar el certificado raíz descargado:
mosquitto_sub -h broker.connectmq.com -p 8883 --cafile isrgrootx1.pem \
-u YOUR_USERNAME -P YOUR_PASSWORD -i YOUR_CLIENT_ID_laptop \
-t "plant1/#" -v
Envía un comando de la misma forma:
mosquitto_pub -h broker.connectmq.com -p 8883 --cafile isrgrootx1.pem \
-u YOUR_USERNAME -P YOUR_PASSWORD -i YOUR_CLIENT_ID_cli \
-t "plant1/YOUR_CLIENT_ID_esp32a/led/set" -q 1 -m "ON"
También puedes abrir el cliente MQTT en el navegador o el explorador de topics en tiempo real del portal de ConnectMQ para ver la telemetría sin instalar nada.
Solución de problemas
| Síntoma | Causa probable | Solución |
|---|---|---|
state=-2 en bucle |
Sin internet, host incorrecto o puerto bloqueado | Revisa el Wi-Fi, prueba el puerto 1883, revisa el firewall |
state=2 |
Client ID rechazado | Usa un ID único y no vacío; con credenciales, usa el ID emitido o una variante _suffix |
state=4 o 5 |
Credenciales incorrectas | Vuelve a copiar el usuario y la contraseña del portal |
| Las publicaciones se detienen sin mostrar error | Mensaje mayor que el búfer | Aumenta setBufferSize(), comprueba el valor de retorno de publish() |
| Desconexiones constantes | mqtt.loop() no se llama con frecuencia, u otro cliente usa el mismo ID |
Elimina las llamadas largas a delay(), da a cada cliente su propio ID |
| TLS falla y TCP sin cifrar funciona | CA incorrecta, reloj sin configurar | Usa ISRG Root X1 y sincroniza la hora por NTP antes de conectarte |
Empieza con ConnectMQ
Ya tienes un ESP32 que envía telemetría y recibe comandos por MQTT, primero en el área pública y después por TLS con sus propias credenciales. Cuando quieras ir más allá de los datos de demostración, crea un broker con el plan Serverless gratuito en https://app.connectmq.com, sin tarjeta de crédito, y usa el panel, la vista de dispositivos y las alertas por umbral del portal para vigilar tu flota.
Preguntas frecuentes
¿Qué librería MQTT debo usar en el ESP32 con Arduino IDE?
PubSubClient es la opción más extendida: es pequeña, estable y funciona tanto con WiFiClient como con WiFiClientSecure. Admite MQTT 3.1.1 con publicación en QoS 0 y suscripciones en QoS 0 o 1, lo que cubre la mayoría de los proyectos con sensores.
¿Por qué falla la publicación de mi ESP32 sin mostrar ningún error?
Lo más habitual es que el paquete supere el búfer por defecto de 256 bytes de PubSubClient. Llama a mqtt.setBufferSize(512) o a un valor mayor antes de conectarte y comprueba el booleano que devuelve publish().
¿Necesito una cuenta para probar ESP32 MQTT con broker.connectmq.com?
No. Conéctate al puerto 1883 u 8883 sin usuario y con un client ID único para usar el área pública. Para datos reales, crea una cuenta gratuita y usa las credenciales generadas.
¿Puedo usar setInsecure() con WiFiClientSecure?
Solo para pruebas rápidas. Cifra el tráfico pero se salta la verificación del servidor, así que un atacante podría suplantar al broker. Usa setCACert() con ISRG Root X1 en todo lo que despliegues.
¿Cómo doy a varias placas ESP32 su propio client ID en un mismo broker?
Añade un sufijo al client ID emitido en el portal, como YOUR_CLIENT_ID_esp32a y YOUR_CLIENT_ID_esp32b. ConnectMQ acepta el ID emitido y sus variantes _suffix en las conexiones autenticadas.



