Este ejemplo de ESP-IDF MQTT construye un cliente esp-mqtt funcional desde una carpeta vacía: un Wi-Fi en modo estación, un manejador MQTT orientado a eventos, publicación y suscripción, propiedades de conexión de MQTT 5 y, por último, TLS. Al terminar tendrás un firmware que habla con broker.connectmq.com y una idea clara de cómo está estructurado el componente esp-mqtt.
Por qué usar ESP-IDF en lugar de Arduino
Las librerías de Arduino como PubSubClient permiten empezar rápido, pero solo soportan MQTT 3.1.1 y son mayoritariamente bloqueantes. El componente esp-mqtt de ESP-IDF lo mantiene Espressif, se ejecuta en su propia tarea de FreeRTOS, soporta MQTT 3.1, 3.1.1 y 5.0, y gestiona los transportes TCP, TLS, WebSocket y WebSocket seguro a través de una única URI. La reconexión, el keepalive y la gestión del outbox vienen integrados, y tu código reacciona a eventos en lugar de sondear.
La contrapartida es más configuración: CMake, menuconfig e inicialización explícita del Wi-Fi. Esta guía reduce esa configuración al mínimo. Está pensada para ESP-IDF 5.x, donde la estructura de configuración usa campos anidados como .broker.address.uri. El código escrito para ESP-IDF 4.x usaba campos planos y no compilará sin cambios. La referencia oficial de la API es la documentación de MQTT de ESP-IDF.
Estructura del proyecto
Crea esta estructura:
connectmq-idf/
CMakeLists.txt
sdkconfig.defaults
main/
CMakeLists.txt
main.c
El CMakeLists.txt de nivel superior:
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(connectmq_idf)
main/CMakeLists.txt:
idf_component_register(SRCS "main.c" INCLUDE_DIRS ".")
El componente main depende automáticamente de todos los componentes del framework, así que no necesitas listar explícitamente mqtt, esp_wifi ni nvs_flash. En ESP-IDF 5.x, esp-mqtt viene incluido en el framework. Si tu versión de ESP-IDF lo ha trasladado al registro de componentes, añádelo con idf.py add-dependency "espressif/mqtt".
sdkconfig.defaults activa el soporte de MQTT 5, que viene desactivado por defecto:
CONFIG_MQTT_PROTOCOL_5=y
El ejemplo completo de ESP-IDF MQTT
Este es el main/main.c completo. Se conecta al Wi-Fi, arranca el cliente MQTT contra el área pública de broker.connectmq.com, se suscribe a un topic de comandos y publica un contador cada cinco segundos.
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#include "mqtt_client.h"
#define WIFI_SSID "YOUR_WIFI"
#define WIFI_PASS "YOUR_WIFI_PASSWORD"
#define BROKER_URI "mqtt://broker.connectmq.com:1883"
#define TOPIC_PREFIX "connectmq/demo/c19d44"
static const char *TAG = "connectmq";
static EventGroupHandle_t wifi_events;
#define WIFI_CONNECTED_BIT BIT0
static esp_mqtt_client_handle_t client;
static bool mqtt_connected = false;
static void wifi_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
xEventGroupClearBits(wifi_events, WIFI_CONNECTED_BIT);
ESP_LOGW(TAG, "Wi-Fi lost, reconnecting");
esp_wifi_connect();
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *ev = (ip_event_got_ip_t *)data;
ESP_LOGI(TAG, "Got IP " IPSTR, IP2STR(&ev->ip_info.ip));
xEventGroupSetBits(wifi_events, WIFI_CONNECTED_BIT);
}
}
static void wifi_init(void)
{
wifi_events = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_handler, NULL));
wifi_config_t wc = { 0 };
strncpy((char *)wc.sta.ssid, WIFI_SSID, sizeof(wc.sta.ssid));
strncpy((char *)wc.sta.password, WIFI_PASS, sizeof(wc.sta.password));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wc));
ESP_ERROR_CHECK(esp_wifi_start());
xEventGroupWaitBits(wifi_events, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
}
static void mqtt_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)data;
switch ((esp_mqtt_event_id_t)id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT connected");
mqtt_connected = true;
esp_mqtt_client_subscribe(client, TOPIC_PREFIX "/cmd", 1);
esp_mqtt_client_publish(client, TOPIC_PREFIX "/status", "online", 0, 1, 1);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "MQTT disconnected");
mqtt_connected = false;
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "Subscribed, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "Topic %.*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, errno=%d",
event->error_handle->esp_transport_sock_errno);
} else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
ESP_LOGE(TAG, "Connection refused, code=0x%x",
event->error_handle->connect_return_code);
}
break;
default:
break;
}
}
static void mqtt_start(void)
{
static char client_id[32];
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
snprintf(client_id, sizeof(client_id), "idf-%02x%02x%02x", mac[3], mac[4], mac[5]);
esp_mqtt_client_config_t cfg = {
.broker.address.uri = BROKER_URI,
.credentials.client_id = client_id,
.session.protocol_ver = MQTT_PROTOCOL_V_5,
.session.keepalive = 30,
.session.last_will = {
.topic = TOPIC_PREFIX "/status",
.msg = "offline",
.qos = 1,
.retain = 1,
},
};
client = esp_mqtt_client_init(&cfg);
esp_mqtt5_connection_property_config_t conn_props = {
.session_expiry_interval = 600, /* segundos */
};
esp_mqtt5_client_set_connect_property(client, &conn_props);
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_handler, NULL);
esp_mqtt_client_start(client);
}
void app_main(void)
{
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
wifi_init();
mqtt_start();
int counter = 0;
char payload[64];
while (true) {
if (mqtt_connected) {
int len = snprintf(payload, sizeof(payload), "{\"count\":%d}", counter++);
esp_mqtt_client_publish(client, TOPIC_PREFIX "/telemetry", payload, len, 0, 0);
}
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
Compila, flashea y abre el monitor:
idf.py set-target esp32
idf.py build flash monitor
Después observa el tráfico desde tu equipo:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-c19d44 -t 'connectmq/demo/c19d44/#' -v
mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-c19d44-pub -t connectmq/demo/c19d44/cmd -m hello
El log del ESP32 muestra el comando hello llegando a través de MQTT_EVENT_DATA.
Cómo funciona el modelo de eventos de esp-mqtt
El cliente se ejecuta en su propia tarea, creada por esp_mqtt_client_start(). Todo lo que quiere comunicarte llega como un evento al manejador que registraste:
| Evento | Cuándo se dispara | Acción típica |
|---|---|---|
MQTT_EVENT_CONNECTED |
Se recibe el CONNACK | Suscribirse, publicar el mensaje de nacimiento |
MQTT_EVENT_DISCONNECTED |
Se pierde la conexión | Actualizar el estado; el cliente se reconecta automáticamente |
MQTT_EVENT_SUBSCRIBED |
Se recibe el SUBACK | Confirmar las suscripciones |
MQTT_EVENT_PUBLISHED |
PUBACK/PUBCOMP para QoS 1/2 | Seguir la entrega |
MQTT_EVENT_DATA |
Se recibe un mensaje | Copiar y despachar el payload |
MQTT_EVENT_ERROR |
Error de transporte o de protocolo | Registrar el tipo de error y los códigos |
Tres detalles suelen tomar por sorpresa a los desarrolladores:
- El topic y los datos no terminan en nulo. Usa siempre
topic_lenydata_len, como hace el formato%.*sde arriba. - Los mensajes grandes llegan troceados. Si un payload es mayor que el búfer del cliente (
.buffer.size, 1024 bytes por defecto), recibes varios eventosMQTT_EVENT_DATA. Compruebaevent->current_data_offsetyevent->total_data_leny vuelve a ensamblarlo. - Suscríbete en
MQTT_EVENT_CONNECTED. Con clean session, las suscripciones desaparecen tras una reconexión. Hacerlas en el manejador de conexión las restablece cada vez.
El manejador se ejecuta en la tarea MQTT, así que mantenlo corto. El trabajo pesado corresponde a una tarea aparte alimentada por una cola, como se describe en la guía de ESP32 FreeRTOS MQTT.
Publicar frente a encolar
esp_mqtt_client_publish() es bloqueante: envía el paquete desde la tarea que la llama y, para QoS 1 y 2, lo mantiene en el outbox hasta que se confirma. Llamarla desde dentro del manejador de eventos no da problemas. Si publicas desde una tarea con requisitos de tiempo estrictos, esp_mqtt_client_enqueue(client, topic, data, len, qos, retain, true) coloca el mensaje en el outbox y retorna de inmediato; la tarea MQTT lo envía más tarde. Ambas devuelven un ID de mensaje, o un valor negativo si fallan, así que compruébalo.
Usar las funciones de MQTT 5
Configurar .session.protocol_ver = MQTT_PROTOCOL_V_5 y activar CONFIG_MQTT_PROTOCOL_5 pasa el cliente a MQTT 5. El ejemplo ya establece un intervalo de expiración de sesión mediante esp_mqtt5_client_set_connect_property(). En el área pública de broker.connectmq.com, la expiración de sesión puede ser de hasta 30 minutos; los 10 minutos usados aquí encajan con holgura.
Las propiedades de publicación se establecen antes de la llamada de publicación:
esp_mqtt5_publish_property_config_t pub_props = {
.message_expiry_interval = 60,
.content_type = "application/json",
};
esp_mqtt5_client_set_publish_property(client, &pub_props);
esp_mqtt_client_publish(client, TOPIC_PREFIX "/telemetry", payload, len, 1, 0);
La expiración de mensajes evita que se entreguen lecturas obsoletas a un suscriptor que vuelve mucho más tarde. Para un recorrido más profundo por los response topics, las user properties y los reason codes en este componente, consulta esp-mqtt y MQTT 5, y para el lado del protocolo, las funciones de MQTT 5.
Elegir QoS, retain y keepalive
El ejemplo usa a propósito tres configuraciones de publicación distintas. La telemetría rutinaria sale con QoS 0 y sin retain: si se pierde una lectura, la siguiente llega cinco segundos después, y nada espera una confirmación. El mensaje de estado usa QoS 1 con retain, así que el broker lo confirma y cualquier dashboard que se suscriba más tarde ve si el dispositivo está en línea. El Last Will refleja ese topic de estado con offline, que el broker publica si el dispositivo desaparece sin una desconexión limpia.
El keepalive de 30 segundos es un término medio razonable. esp-mqtt envía los paquetes PINGREQ por su cuenta, así que, a diferencia de las librerías de Arduino, no necesitas llamar a una función loop para mantener viva la sesión. Un keepalive más corto detecta antes los dispositivos caídos pero cuesta más tiempo de radio; uno más largo es más amable con los nodos con batería. Si las garantías de entrega te resultan nuevas, los niveles de QoS de MQTT explicados repasa lo que cuesta cada nivel en la red.
Pasar a TLS en el puerto 8883
Para cualquier cosa que vaya más allá de una demo, cifra la conexión. ESP-IDF incluye un bundle de certificados con las CA raíz más comunes, incluida la raíz de Let's Encrypt que usa ConnectMQ, así que no necesitas pegar un archivo PEM. Cambia el esquema de la URI y adjunta el bundle:
#include "esp_crt_bundle.h"
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 = "YOUR_CLIENT_ID",
.credentials.username = "YOUR_USERNAME",
.credentials.authentication.password = "YOUR_PASSWORD",
.session.protocol_ver = MQTT_PROTOCOL_V_5,
};
Asegúrate de que CONFIG_MBEDTLS_CERTIFICATE_BUNDLE está activado (lo está por defecto). Usa el client ID, el usuario y la contraseña generados para tu broker en el portal; el client ID está vinculado a esas credenciales, así que un ID inventado se rechaza. El mismo enfoque de URI funciona con los transportes WebSocket: ws://broker.connectmq.com:8083/mqtt y wss://broker.connectmq.com:8084/mqtt, si CONFIG_MQTT_TRANSPORT_WEBSOCKET está activado.
Lista de comprobación para resolver problemas
- Errores de compilación sobre
.urio.host: estás mezclando la sintaxis de configuración de ESP-IDF 4.x y 5.x. Usa los campos anidados que se muestran aquí. - Funciones de MQTT 5 sin definir:
CONFIG_MQTT_PROTOCOL_5no está activado. Borrasdkconfigdespués de añadirsdkconfig.defaults, o actívalo enidf.py menuconfig. MQTT_ERROR_TYPE_TCP_TRANSPORTjusto tras arrancar: el Wi-Fi aún no está activo, falló el DNS o el esquema y el puerto no coinciden (mqtts://necesita el 8883).- Conexión rechazada con credenciales: comprueba el client ID exacto del portal y que el campo de la contraseña es
.credentials.authentication.password. - Payloads cortados: aumenta
.buffer.sizeo vuelve a ensamblar los eventosMQTT_EVENT_DATAtroceados. - Desbordamiento de pila en la tarea MQTT: aumenta
.task.stack_size, sobre todo con TLS y logs detallados.
Si estás lidiando con fallos de conexión en general, la guía sobre qué hacer cuando un ESP32 no puede conectarse a un broker MQTT cubre comprobaciones a nivel de red que también se aplican a ESP-IDF.
Lleva tu firmware esp-mqtt a producción con ConnectMQ
Acabas de ejecutar este ejemplo de ESP-IDF MQTT contra el servidor MQTT público gratuito, pensado para pruebas y prototipos, no para datos de producción. Cuando estés listo para usar credenciales privadas, ConnectMQ te ofrece un broker MQTT gestionado con MQTT 5, TLS en el 8883 y un certificado de Let's Encrypt en el que el bundle de ESP-IDF ya confía. Crea uno en el plan gratuito Serverless en app.connectmq.com, sin tarjeta de crédito, y cambia solo la URI y las credenciales en tu configuración. Para una visión más amplia de los proyectos de dispositivos, consulta el tutorial de IoT con ESP32.
Preguntas frecuentes
¿Se reconecta esp-mqtt automáticamente?
Sí. Tras una desconexión, el cliente reintenta por su cuenta usando .network.reconnect_timeout_ms. Vuelve a suscribirte en MQTT_EVENT_CONNECTED para que las suscripciones regresen con la conexión.
¿Qué versión de ESP-IDF necesita este ejemplo?
Está pensado para ESP-IDF 5.x, donde la configuración del cliente usa campos anidados como .broker.address.uri. ESP-IDF 4.x usaba una estructura plana y necesita pequeños cambios.
¿Puedo usar MQTT 5 con el plan gratuito de ConnectMQ?
Sí. Todos los brokers de la plataforma soportan MQTT 3.1.1 y 5.0. El plan gratuito Serverless soporta QoS hasta 1, así que usa QoS 0 o 1 en tus publicaciones allí.
¿Por qué el topic recibido se imprime con basura al final?
Los punteros de topic y datos en MQTT_EVENT_DATA no terminan en nulo. Imprímelos con %.*s y los campos de longitud, o cópialos en un búfer terminado en nulo.



