MQTT gestionado en la nube

Conecta un ESP32 a un broker MQTT en la nube por TLS

Guía paso a paso para migrar un proyecto ESP32 de un broker Mosquitto local a un broker MQTT en la nube por TLS, con usuario, contraseña y un certificado validado.

Ilustración de un ESP32 y un broker MQTT en la nube con una placa ESP32 que envía telemetría cifrada por TLS a un broker en la nube
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

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 offline en 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

  1. Guarda las credenciales en NVS o aprovisiónalas en fábrica, no en el control de versiones.
  2. Da a cada dispositivo su propio client ID.
  3. Conserva la CA raíz en lugar de un certificado hoja.
  4. Usa backoff con jitter en la reconexión.
  5. Publica un estado retenido con un Last Will.
  6. Mantén los payloads pequeños y estructurados.
  7. 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.

Artículos relacionados

Broker MQTT BYOC: el modelo Bring Your Own Cloud explicado

Un broker MQTT BYOC se ejecuta en tu cuenta cloud, pero lo opera un proveedor. Descubre cómo funciona el modelo, cómo se reparten las responsabilidades y cómo preparar tu cuenta.

9 min de lectura

Alternativa a Google Cloud IoT Core: opciones y migración tras su cierre

Google retiró Cloud IoT Core en agosto de 2023. Esta guía compara las alternativas realistas, traduce los conceptos de IoT Core a MQTT estándar y muestra cómo mover los dispositivos a un broker MQTT gestionado sin dejar de enviar datos a Google Cloud.

9 min de lectura

Mensajes MQTT por segundo: cómo estimar y medir el rendimiento

Planificar el rendimiento de MQTT se reduce a unas pocas fórmulas y una medición honesta. Aprende a estimar los mensajes entrantes y salientes por segundo, a tener en cuenta los picos y a trasladar el resultado a un plan de broker.

8 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.