esp-mqtt es el cliente MQTT que incluye el framework ESP-IDF de Espressif. Se ejecuta en su propia tarea de FreeRTOS, se reconecta automáticamente, soporta MQTT 3.1.1 y MQTT 5, y habla TCP, TLS, WebSocket y WebSocket seguro con una sola URI. Lo configuras con una única estructura esp_mqtt_client_config_t, registras un manejador de eventos y llamas a esp_mqtt_client_start().
Este tutorial construye paso a paso una aplicación completa para ESP32: primero TCP sin cifrar, luego eventos, publicación, suscripciones, un Last Will y, por último, TLS en el puerto 8883. En cada etapa verificas el resultado desde el navegador, así sabes que el problema está en el dispositivo y no en tu prueba cuando algo falla.
Preparar el proyecto
Necesitas ESP-IDF 5.x instalado (idf.py --version debe funcionar en tu terminal). Parte del helper de Wi-Fi que usan los ejemplos oficiales para que el tutorial se centre en MQTT:
idf.py create-project mqtt_demo
cd mqtt_demo
idf.py set-target esp32Haz visible el componente Wi-Fi de ejemplo en el CMakeLists.txt de la raíz (antes de la línea include(...project.cmake)) y declara las dependencias de main. El componente mqtt forma parte de ESP-IDF, así que #include "mqtt_client.h" funciona sin descargas extra en las versiones 5.x.
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
PRIV_REQUIRES mqtt esp_wifi esp_netif nvs_flash protocol_examples_common)Después ejecuta idf.py menuconfig, abre Example Connection Configuration e introduce el SSID y la contraseña de tu Wi-Fi.
La configuración del cliente
La configuración mínima es una URI del broker. El esquema elige el transporte:
| URI | Transporte | Puerto |
|---|---|---|
mqtt://broker.connectmq.com:1883 | TCP | 1883 |
mqtts://broker.connectmq.com:8883 | TLS | 8883 |
ws://broker.connectmq.com:8083/mqtt | WebSocket | 8083 |
wss://broker.connectmq.com:8084/mqtt | WebSocket seguro | 8084 |
Una configuración realista también define un client ID, credenciales, keep alive y un Last Will:
#include "esp_log.h"
#include "mqtt_client.h"
static const char *TAG = "mqtt_demo";
static esp_mqtt_client_handle_t client;
#define BASE_TOPIC "testmqtt/espidf/dev01"
static esp_mqtt_client_handle_t mqtt_create(void)
{
const esp_mqtt_client_config_t cfg = {
.broker.address.uri = "mqtt://broker.connectmq.com:1883",
.credentials.client_id = "espidf-dev01",
.credentials.username = "username",
.credentials.authentication.password = "password",
.session.keepalive = 30,
.session.last_will = {
.topic = BASE_TOPIC "/status",
.msg = "offline",
.qos = 1,
.retain = 1,
},
.network.reconnect_timeout_ms = 5000,
};
return esp_mqtt_client_init(&cfg);
}Cada cliente necesita un client ID único: dos dispositivos con el mismo ID se expulsan mutuamente del broker en un bucle infinito. En producción derívalo de la dirección MAC, o genera uno con el generador de client ID mientras pruebas. Mantén las credenciales fuera del control de versiones moviéndolas a opciones de Kconfig.projbuild (CONFIG_MQTT_USER, CONFIG_MQTT_PASS) y definiéndolas en menuconfig o en sdkconfig.defaults.
Gestionar los eventos
esp-mqtt informa de todo a través del event loop de ESP. Un único manejador recibe todos los eventos; event_data apunta a un esp_mqtt_event_t:
static void mqtt_event_handler(void *args, esp_event_base_t base,
int32_t event_id, void *event_data)
{
esp_mqtt_event_handle_t event = event_data;
switch ((esp_mqtt_event_id_t)event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "connected");
esp_mqtt_client_publish(client, BASE_TOPIC "/status", "online", 0, 1, 1);
esp_mqtt_client_subscribe(client, BASE_TOPIC "/cmd/#", 1);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "disconnected, will retry");
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "subscribed, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(TAG, "broker acked msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
// topic y data NO terminan en nulo
ESP_LOGI(TAG, "%.*s = %.*s", event->topic_len, event->topic,
event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
ESP_LOGE(TAG, "transport error: esp-tls 0x%x, tls stack 0x%x, errno %d",
event->error_handle->esp_tls_last_esp_err,
event->error_handle->esp_tls_stack_err,
event->error_handle->esp_transport_sock_errno);
} else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
ESP_LOGE(TAG, "refused, CONNACK code %d",
event->error_handle->connect_return_code);
}
break;
default:
break;
}
}Tres detalles hacen tropezar a casi todo el mundo:
- Suscríbete en
MQTT_EVENT_CONNECTED, no después destart(). El cliente puede reconectarse muchas veces; suscribirte en cada conexión restaura tus suscripciones aunque el broker no haya conservado la sesión. topicydatano son cadenas C. Usa siempre los campos_len. Los mensajes grandes llegan en varios eventosMQTT_EVENT_DATA;event->current_data_offsetyevent->total_data_lente permiten reensamblarlos, o puedes aumentar el buffer con.buffer.size.- El manejador se ejecuta en la tarea MQTT. No lo bloquees. Pasa el trabajo largo a otra tarea mediante una cola de FreeRTOS.
Arranca todo desde app_main:
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "protocol_examples_common.h"
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect()); // bloquea hasta que el Wi-Fi tiene IP
client = mqtt_create();
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
esp_mqtt_client_start(client);
}Flashea y mira el log con idf.py flash monitor. Deberías ver connected seguido de subscribed.
Publicar telemetría
esp_mqtt_client_publish(client, topic, data, len, qos, retain) devuelve un ID de mensaje (0 con QoS 0, -1 si hay error). Pasa len = 0 para que la librería llame a strlen() sobre un payload de texto. Con QoS 1 y 2 la llamada escribe desde la tarea que la invoca y puede esperar a la red; esp_mqtt_client_enqueue() recibe los mismos argumentos más un flag store y deja el envío a la tarea MQTT:
static void telemetry_task(void *arg)
{
char payload[64];
for (int i = 0;; i++) {
int len = snprintf(payload, sizeof payload,
"{\"seq\":%d,\"heap\":%lu}", i,
(unsigned long)esp_get_free_heap_size());
int id = esp_mqtt_client_enqueue(client, BASE_TOPIC "/telemetry",
payload, len, 1, 0, true);
if (id < 0) ESP_LOGW(TAG, "outbox full or not connected");
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
// en app_main, después de esp_mqtt_client_start():
// xTaskCreate(telemetry_task, "telemetry", 3072, NULL, 5, NULL);Los mensajes encolados mientras el cliente está desconectado se quedan en el outbox (limitados por CONFIG_MQTT_OUTBOX_EXPIRED_TIMEOUT_MS y el heap disponible) y se envían tras reconectar. Elige el QoS a conciencia: consulta niveles de QoS en MQTT para ver lo que cuesta cada nivel.
Verificar desde el navegador
Abre el cliente MQTT online, conéctate a broker.connectmq.com (WebSocket 8084, ruta /mqtt) y suscríbete a testmqtt/espidf/dev01/#. Deberías ver:
testmqtt/espidf/dev01/status=online, marcado como retained.- Un mensaje de telemetría cada cinco segundos.
Ahora publica reboot en testmqtt/espidf/dev01/cmd/system desde el navegador: el log del ESP32 imprime el topic y el payload. Por último, desconecta el cable de alimentación. Tras aproximadamente 1,5 × keep alive (45 segundos aquí) el broker publica el Last Will y el estado cambia a offline. Esa única prueba demuestra que la conexión, el publish, el subscribe y el will funcionan. Cómo funciona el Last Will explica los tiempos.
Observa tu ESP32 en el cliente online
Añadir TLS en el puerto 8883
Pasar a TLS son dos cambios: una URI mqtts:// y una forma de verificar el certificado del servidor. Lo más sencillo es el ESP x509 certificate bundle, que contiene las CA raíz públicas habituales y está activado por defecto (CONFIG_MBEDTLS_CERTIFICATE_BUNDLE):
#include "esp_crt_bundle.h"
const esp_mqtt_client_config_t cfg = {
.broker.address.uri = "mqtts://broker.connectmq.com:8883",
.broker.verification.crt_bundle_attach = esp_crt_bundle_attach,
.credentials.client_id = "espidf-dev01",
.credentials.username = "username",
.credentials.authentication.password = "password",
};Si tu broker usa una CA privada o quieres fijar una raíz concreta, incrusta el archivo PEM:
idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
PRIV_REQUIRES mqtt esp_wifi esp_netif nvs_flash protocol_examples_common
EMBED_TXTFILES "ca.pem")extern const uint8_t ca_pem_start[] asm("_binary_ca_pem_start");
const esp_mqtt_client_config_t cfg = {
.broker.address.uri = "mqtts://broker.connectmq.com:8883",
.broker.verification.certificate = (const char *)ca_pem_start,
};TLS necesita la hora correcta para comprobar la validez del certificado y unos 30–40 KB de heap adicional durante el handshake; en un ESP32 cargado es lo primero que hay que revisar cuando fallan las conexiones. Nuestra guía de TLS en ESP32 cubre los certificados de cliente (.credentials.authentication.certificate y .key) para TLS mutuo.
Errores comunes de esp-mqtt
| Línea de log | Causa | Solución |
|---|---|---|
esp-tls: couldn't get hostname for :broker.connectmq.com: | Falló el DNS, normalmente porque el Wi-Fi aún no está conectado | Arranca MQTT solo cuando tengas IP |
mbedtls_ssl_handshake returned -0x2700 | Falló la verificación del certificado | Usa el bundle o la CA correcta; revisa la hora del sistema |
mbedtls_ssl_setup returned -0x7F00 | Sin memoria para TLS | Libera heap, reduce otros buffers |
Error transport connect | Puerto bloqueado o par esquema/puerto incorrecto | Comprueba mqtts:// con 8883 y mqtt:// con 1883 |
MQTT_ERROR_TYPE_CONNECTION_REFUSED, código 5 | No autorizado | Revisa usuario, contraseña y ACL |
Los códigos numéricos de CONNACK y los reason codes de MQTT 5 están en el buscador de reason codes. Para dispositivos que se conectan y se caen cada pocos minutos, lee desconexiones de Wi-Fi y MQTT en ESP32.
Activar MQTT 5
Activa CONFIG_MQTT_PROTOCOL_5 en menuconfig (Component config → ESP-MQTT Configurations) y define .session.protocol_ver = MQTT_PROTOCOL_V_5. Las llamadas básicas de publish y subscribe no cambian; los extras de MQTT 5 como user properties, message expiry y response topics se configuran con funciones como esp_mqtt5_client_set_publish_property() antes del publish. Las conexiones rechazadas informan entonces de reason codes precisos en lugar de los seis códigos de retorno de MQTT 3.1.1, lo que facilita mucho depurar la autenticación.
esp-mqtt te da un cliente de nivel de producción con muy poco código: una estructura de configuración, un manejador de eventos y el transporte elegido por la URI. Haz que funcione sobre TCP sin cifrar, demuestra el publish, el subscribe y el will desde el cliente online, pasa luego a mqtts:// y conserva las mismas pruebas como test de regresión antes de cada versión de firmware.
Preguntas frecuentes
¿Necesito una librería adicional para MQTT en ESP-IDF?
En la mayoría de proyectos, no. El cliente esp-mqtt viene con ESP-IDF (cabecera mqtt_client.h). Algunas versiones recientes de ESP-IDF lo distribuyen a través del component registry como espressif/mqtt, así que revisa la documentación de tu versión si no encuentra la cabecera.
¿Por qué se bloquea esp_mqtt_client_publish?
Con QoS 1 y 2 escribe en el socket desde la tarea que lo llama y puede esperar a la red. Usa esp_mqtt_client_enqueue para dejar el mensaje en el outbox y que lo envíe la tarea MQTT, algo más seguro desde código con requisitos de tiempo.
¿esp-mqtt soporta MQTT 5?
Sí. Activa MQTT 5 en menuconfig (Component config → ESP-MQTT Configurations) y pon session.protocol_ver a MQTT_PROTOCOL_V_5. Así obtienes reason codes, user properties y el resto de extras de MQTT 5 mediante APIs específicas.

