Conectar un ESP32 a un broker MQTT en la nube por TLS es el paso que convierte un prototipo de banco de pruebas en algo que puedes desplegar fuera de la red de tu casa. En este tutorial migrarás un sketch de ESP32 de un broker Mosquitto local en el puerto 1883 a un broker Serverless de ConnectMQ en el puerto 8883, con una validación de certificados correcta, credenciales, un topic de estado y reconexiones fiables.
Qué cambia cuando dejas el broker local
La mayoría de los proyectos con ESP32 empiezan con Mosquitto en una Raspberry Pi o en un portátil: TCP plano en el 1883, sin autenticación y accesible solo en tu LAN. Pasar a un broker MQTT en la nube cambia cuatro cosas:
| Aspecto | Broker local | Broker en la nube |
|---|---|---|
| Host | 192.168.1.20 |
broker.connectmq.com |
| Puerto | 1883 (TCP plano) | 8883 (TLS) |
| Autenticación | A menudo ninguna | Usuario, contraseña, client ID |
| Cliente de red | WiFiClient |
WiFiClientSecure |
| Certificado | Ninguno | Certificado del servidor validado contra una CA |
| Alcance | Solo la LAN | Cualquier lugar con internet |
La lógica MQTT en sí, es decir, topics, publicaciones y suscripciones, sigue siendo la misma. Si nunca has conectado un ESP32 a MQTT, empieza por el tutorial de ESP32 y MQTT y vuelve aquí después.
Requisitos previos
- Una placa de desarrollo ESP32 y un cable USB.
- Arduino IDE 2.x o PlatformIO con el core de Arduino para ESP32 instalado (consulta la documentación oficial del core Arduino de Espressif).
- La biblioteca PubSubClient de Nick O'Leary.
- Un broker Serverless gratuito. Regístrate en el portal, elige el plan Serverless (Free), crea un broker y copia el usuario, la contraseña y el client ID generados.
Paso 1: obtén el certificado de la CA raíz
El broker sirve un certificado de Let's Encrypt. El ESP32 debe confiar en la raíz que firmó la cadena. La cadena de Let's Encrypt llega hasta ISRG Root X1, así que ese es el certificado que debes incrustar. Puedes confirmar la cadena tú mismo desde cualquier máquina con OpenSSL:
openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -showcerts </dev/null
Fíjate en las líneas issuer al principio de la salida. Después descarga ISRG Root X1 en formato PEM desde la página de certificados de Let's Encrypt y pégalo en tu sketch como cadena raw. Incrustar la raíz de larga duración en lugar del propio certificado del servidor significa que no tienes que volver a flashear los dispositivos cuando el certificado del servidor se renueva cada pocos meses.
Paso 2: el sketch completo
Este sketch se conecta a la Wi-Fi, sincroniza la hora (la validación de certificados TLS necesita un reloj correcto), se conecta al broker por TLS con credenciales, publica un estado online retenido con un Last Will, se suscribe a un topic de comandos y publica telemetría cada diez segundos.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <time.h>
// ---- Wi-Fi ----
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
// ---- ConnectMQ ----
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";
const char* TOPIC_TELEMETRY = "devices/esp32-01/telemetry";
const char* TOPIC_STATUS = "devices/esp32-01/status";
const char* TOPIC_COMMAND = "devices/esp32-01/cmd";
// ISRG Root X1 (pega aquí el PEM completo de Let's Encrypt)
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
PASTE_ISRG_ROOT_X1_PEM_CONTENT_HERE
-----END CERTIFICATE-----
)EOF";
WiFiClientSecure net;
PubSubClient mqtt(net);
unsigned long lastPublish = 0;
unsigned long lastReconnectAttempt = 0;
unsigned long reconnectDelay = 1000;
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nWi-Fi connected, IP %s\n", WiFi.localIP().toString().c_str());
}
void syncTime() {
configTime(0, 0, "pool.ntp.org", "time.google.com");
Serial.print("Syncing time");
time_t now = time(nullptr);
while (now < 1700000000) {
delay(500);
Serial.print(".");
now = time(nullptr);
}
Serial.println(" done");
}
void onMessage(char* topic, byte* payload, unsigned int length) {
String msg;
for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
Serial.printf("Command on %s: %s\n", topic, msg.c_str());
if (msg == "led:on") digitalWrite(LED_BUILTIN, HIGH);
if (msg == "led:off") digitalWrite(LED_BUILTIN, LOW);
}
bool connectMqtt() {
Serial.print("Connecting to ConnectMQ... ");
bool ok = mqtt.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS,
TOPIC_STATUS, 1, true, "offline");
if (!ok) {
Serial.printf("failed, state=%d\n", mqtt.state());
return false;
}
Serial.println("connected");
mqtt.publish(TOPIC_STATUS, "online", true);
mqtt.subscribe(TOPIC_COMMAND, 1);
return true;
}
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
connectWiFi();
syncTime();
net.setCACert(ROOT_CA);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
mqtt.setBufferSize(512);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
}
if (!mqtt.connected()) {
unsigned long now = millis();
if (now - lastReconnectAttempt > reconnectDelay) {
lastReconnectAttempt = now;
if (connectMqtt()) {
reconnectDelay = 1000;
} else {
reconnectDelay = min(reconnectDelay * 2, 60000UL);
}
}
return;
}
mqtt.loop();
if (millis() - lastPublish > 10000) {
lastPublish = millis();
char payload[128];
snprintf(payload, sizeof(payload),
"{\"uptime_s\":%lu,\"rssi\":%d,\"heap\":%u}",
millis() / 1000, WiFi.RSSI(), ESP.getFreeHeap());
mqtt.publish(TOPIC_TELEMETRY, payload);
Serial.println(payload);
}
}
Notas sobre las decisiones tomadas:
- Estado retenido más Last Will. El broker publica
offlineen tu nombre si el ESP32 se cae sin una desconexión limpia, y los nuevos suscriptores siempre ven el último estado. Más sobre este patrón en Last Will de MQTT en ESP32. - Backoff no bloqueante. Los intentos de reconexión se duplican desde un segundo hasta un minuto, así que una flota no satura el broker tras una caída.
- Tamaño del buffer. El buffer por defecto de 256 bytes de PubSubClient descarta en silencio los mensajes más grandes; 512 bytes dan margen.
- Algunas placas no tienen
LED_BUILTIN. Defínelo como el pin del LED de tu placa, a menudo el GPIO 2.
Paso 3: verifica desde tu computadora
Abre el cliente del navegador del portal o el explorador de topics y suscríbete a devices/#. Deberías ver telemetría cada diez segundos y online retenido en el topic de estado. Para enviar un comando desde la línea de comandos:
mosquitto_pub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs \
-u YOUR_USERNAME -P YOUR_PASSWORD -i YOUR_CLIENT_ID-cli \
-t devices/esp32-01/cmd -m 'led:on' -q 1
Usa un client ID distinto del del ESP32. Si tu plan vincula un único client ID a las credenciales, usa en su lugar el cliente del navegador del portal o crea credenciales adicionales para las herramientas.
Pruebas sin cuenta
Para probar el sketch antes de registrarte, usa el servidor MQTT público gratuito, pensado para pruebas y prototipos y nunca para datos de producción: mantén el puerto 8883 y la misma CA raíz, pasa usuario y contraseña vacíos (mqtt.connect(clientId)), elige cualquier client ID único y no vacío y antepón a cada topic un prefijo único como connectmq/demo/x8q2m/esp32-01/.... El área pública puede leerla cualquiera, así que no publiques nada sensible, y ten en cuenta sus límites: 64 KB por mensaje y 10 mensajes por segundo por cliente.
Solución de problemas
| Síntoma | Causa probable | Solución |
|---|---|---|
state=-2 de inmediato |
Falló el handshake TLS | Revisa la CA raíz, la sincronización horaria y el puerto 8883 |
| Se queda colgado en la sincronización horaria | NTP bloqueado | Permite UDP 123 o usa un servidor NTP accesible |
state=4 |
Usuario o contraseña incorrectos | Vuelve a copiar las credenciales del portal |
state=5 |
No autorizado | El client ID no coincide con las credenciales emitidas |
| Se conecta y se cae en bucle | Client ID duplicado | Asegúrate de que cada dispositivo y herramienta usa su propio ID |
| Los mensajes grandes nunca llegan | Buffer de PubSubClient demasiado pequeño | Aumenta setBufferSize |
| Funciona en casa, falla en el sitio | Puerto 8883 saliente bloqueado | Pide a TI que abra el 8883 o usa WSS en el 8084 con un cliente MQTT sobre WebSocket |
Solo durante el desarrollo, net.setInsecure() omite la validación del certificado y ayuda a aislar si la CA es el problema. Nunca publiques firmware con eso. Para problemas de TLS más profundos, la guía de SSL en MQTT con ESP32 va más allá.
Memoria, energía y ciclo de vida de los certificados
TLS en un ESP32 es llevadero, pero no gratuito, y algunas decisiones de diseño evitan problemas más adelante.
Uso del heap. Una sesión TLS necesita un trozo considerable de RAM para buffers y para el handshake. En un ESP32 normal eso no es problema para una conexión, pero si tu sketch también ejecuta un servidor web, una cámara o documentos JSON grandes, vigila ESP.getFreeHeap() en el payload de telemetría anterior. Un valor que baja de forma constante suele apuntar a una fuga en tu propio código, como concatenar String dentro de un bucle, más que en la pila TLS.
Mantén la conexión abierta. El handshake es la parte cara. Conectarse, publicar un mensaje y desconectarse cada pocos segundos cuesta mucha más energía y tiempo que mantener una sesión abierta con un keep alive de 30 a 60 segundos.
Dispositivos con batería. Si la placa entra en deep sleep entre lecturas, cada despertar paga la asociación Wi-Fi, NTP y un handshake TLS. Reduce el coste despertando con menos frecuencia y agrupando varias lecturas en una sola publicación, y considera guardar la última hora NTP en la memoria RTC para poder saltarte una sincronización completa en sueños cortos.
Caducidad del certificado raíz. Incrustar la CA raíz en lugar del certificado hoja del servidor significa que las renovaciones rutinarias nunca afectan a tus dispositivos. Aun así, las raíces acaban cambiando, así que conserva una forma de actualizar el certificado en campo, ya sea mediante actualizaciones de firmware OTA o guardando el PEM en flash, donde un flujo de aprovisionamiento pueda sustituirlo.
Checklist para producción
- Guarda las credenciales en NVS o aprovisiónalas en fábrica, no en el control de versiones.
- Da a cada dispositivo su propio client ID.
- Conserva la CA raíz en lugar de un certificado hoja.
- Usa backoff con jitter en la reconexión.
- Publica un estado retenido con un Last Will.
- Mantén los payloads pequeños y estructurados.
- Vigila los dispositivos en el panel del portal y define reglas de alerta por umbral para las métricas clave.
La visión de seguridad más amplia está en buenas prácticas de seguridad en MQTT.
Conecta tu ESP32 a ConnectMQ
Crea un broker Serverless gratuito en app.connectmq.com sin tarjeta de crédito, pega las credenciales en el sketch anterior y tu ESP32 estará hablando con la nube por TLS. ConnectMQ es un broker MQTT gestionado, así que los certificados, los parches y la capacidad se gestionan por ti mientras te centras en el firmware.
Preguntas frecuentes
¿Por qué mi ESP32 necesita la hora correcta para TLS?
La validación de certificados comprueba las fechas de validez. Sin NTP, el reloj del ESP32 arranca en 1970, todos los certificados parecen no válidos y el handshake falla.
¿Puedo usar AsyncMqttClient en lugar de PubSubClient?
Sí. AsyncMqttClient no es bloqueante y admite TLS según tu core y tus opciones de compilación, mientras que PubSubClient con WiFiClientSecure es la configuración más sencilla y extendida.
¿Qué certificado raíz debo incrustar para ConnectMQ?
ISRG Root X1, la raíz de Let's Encrypt. Es de larga duración, así que las renovaciones del certificado del servidor no requieren actualizar el firmware.
¿TLS ralentiza notablemente el ESP32?
El handshake tarda un momento y usa RAM adicional, pero una vez conectado la sobrecarga por mensaje es pequeña. Mantén la conexión abierta en lugar de reconectarte para cada publicación.



