El soporte de esp-mqtt MQTT 5 en ESP-IDF permite que un ESP32 adjunte metadatos a los mensajes, fije tiempos de caducidad, acorte topics con aliases y construya flujos de request/response sin inventar convenciones propias. En este tutorial activarás MQTT 5.0 en un proyecto de ESP-IDF, te conectarás a broker.connectmq.com y usarás cada función con código C funcional que puedes grabar hoy mismo.
Por qué MQTT 5 en un microcontrolador
MQTT 3.1.1 es perfectamente utilizable en un ESP32, y la mayoría de las librerías de Arduino se quedan ahí. MQTT 5.0 añade funciones que resuelven problemas reales de los dispositivos:
| Función | Problema que resuelve en un dispositivo |
|---|---|
| User properties | Enviar la versión de firmware, las unidades o la versión del esquema sin engordar el payload JSON |
| Message expiry interval | Evitar que comandos o lecturas obsoletos se entreguen horas después |
| Topic alias | Sustituir un topic largo por un número de 2 bytes en publicaciones repetidas |
| Session expiry interval | Mantener las suscripciones durante cortes breves de Wi-Fi sin conservarlas para siempre |
| Response topic y correlation data | Request/response estándar, para que un backend pueda llamar a un dispositivo y emparejar la respuesta |
| Reason codes | Saber exactamente por qué se rechazó una conexión, publicación o suscripción |
esp-mqtt, el cliente MQTT incluido en ESP-IDF, implementa todo esto tras una opción de compilación. También necesitas un broker que hable MQTT 5.0. ConnectMQ admite tanto 3.1.1 como 5.0 en todos sus listeners, así que el mismo endpoint sirve para firmware antiguo y nuevo. Para el contexto del protocolo, las funciones de MQTT 5 explicadas cubre cada propiedad en profundidad.
Requisitos previos
- ESP-IDF 5.x instalado, con
idf.pyfuncionando en tu terminal. - Cualquier placa de la familia ESP32 (ESP32, S3, C3, C6).
- Clientes Mosquitto 2.x en tu equipo;
mosquitto_subpuede imprimir propiedades de MQTT 5. - Familiaridad con un proyecto MQTT básico de ESP-IDF. Si aún no has creado uno, empieza por el ejemplo de MQTT con ESP-IDF y vuelve después.
Configuración del proyecto y activación de MQTT 5.0
Crea una carpeta de proyecto con esta estructura:
mqtt5_connectmq/
CMakeLists.txt
sdkconfig.defaults
main/
CMakeLists.txt
main.c
El CMakeLists.txt de nivel superior, que incorpora el asistente de Wi-Fi que usan los ejemplos oficiales:
cmake_minimum_required(VERSION 3.16)
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(mqtt5_connectmq)
main/CMakeLists.txt:
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
PRIV_REQUIRES mqtt nvs_flash esp_netif esp_event protocol_examples_common)
sdkconfig.defaults activa las rutas de código de MQTT 5 en esp-mqtt. Sin esta opción, las funciones esp_mqtt5_* no se compilan:
CONFIG_MQTT_PROTOCOL_5=y
Después configura tus credenciales Wi-Fi con idf.py menuconfig en Example Connection Configuration. Si tu versión de ESP-IDF ya no incluye el componente mqtt en el árbol, añádelo con idf.py add-dependency espressif/mqtt.
El código de ejemplo de esp-mqtt MQTT 5
Sustituye la parte aleatoria de PREFIX y el client ID por tus propios valores.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_log.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#include "esp_random.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "protocol_examples_common.h"
#include "mqtt_client.h"
#define PREFIX "connectmq/demo/idf5m7"
#define CLIENT_ID "idf5-demo-m7q2"
static const char *TAG = "mqtt5";
static esp_mqtt_client_handle_t client;
/* Propiedades enviadas una vez en el CONNECT */
static esp_mqtt5_connection_property_config_t connect_property = {
.session_expiry_interval = 600, /* conserva la sesión 10 min tras desconectar */
.maximum_packet_size = 4096,
.receive_maximum = 16,
.topic_alias_maximum = 4, /* aliases que aceptamos del broker */
.request_resp_info = true,
.request_problem_info = true,
.will_delay_interval = 15, /* espera 15 s antes de publicar el will */
.payload_format_indicator = true,
.message_expiry_interval = 3600,
.content_type = "text/plain",
};
static esp_mqtt5_user_property_item_t connect_user_props[] = {
{"model", "esp32-devkit"},
{"fw", "1.4.2"},
};
static esp_mqtt5_user_property_item_t telemetry_user_props[] = {
{"schema", "telemetry-v2"},
{"unit", "celsius"},
};
static void print_user_properties(mqtt5_user_property_handle_t props)
{
if (!props) return;
uint8_t count = esp_mqtt5_client_get_user_property_count(props);
if (!count) return;
esp_mqtt5_user_property_item_t *items = malloc(count * sizeof(*items));
if (items && esp_mqtt5_client_get_user_property(props, items, &count) == ESP_OK) {
for (int i = 0; i < count; i++) {
ESP_LOGI(TAG, " user property %s = %s", items[i].key, items[i].value);
free((char *)items[i].key);
free((char *)items[i].value);
}
}
free(items);
}
static void publish_telemetry(float temperature, int seq)
{
char payload[96];
int len = snprintf(payload, sizeof(payload),
"{\"seq\":%d,\"temperature\":%.2f}", seq, temperature);
esp_mqtt5_publish_property_config_t pub_prop = {
.payload_format_indicator = 1, /* payload UTF-8 */
.message_expiry_interval = 60, /* inútil pasado un minuto */
.topic_alias = 1, /* asocia este topic al alias 1 */
.content_type = "application/json",
};
esp_mqtt5_client_set_user_property(&pub_prop.user_property, telemetry_user_props,
sizeof(telemetry_user_props) / sizeof(telemetry_user_props[0]));
esp_mqtt5_client_set_publish_property(client, &pub_prop);
int msg_id = esp_mqtt_client_publish(client, PREFIX "/telemetry", payload, len, 1, 0);
ESP_LOGI(TAG, "published seq %d, msg_id %d", seq, msg_id);
esp_mqtt5_client_delete_user_property(pub_prop.user_property);
}
static void reply_to_request(esp_mqtt_event_handle_t event)
{
char response_topic[128];
int n = event->property->response_topic_len;
if (n <= 0 || n >= (int)sizeof(response_topic)) return;
memcpy(response_topic, event->property->response_topic, n);
response_topic[n] = '\0';
esp_mqtt5_publish_property_config_t pub_prop = {
.correlation_data = event->property->correlation_data,
.correlation_data_len = event->property->correlation_data_len,
.message_expiry_interval = 30,
};
esp_mqtt5_client_set_publish_property(client, &pub_prop);
char body[96];
int len = snprintf(body, sizeof(body), "{\"ok\":true,\"freeHeap\":%lu}",
(unsigned long)esp_get_free_heap_size());
esp_mqtt_client_publish(client, response_topic, body, len, 1, 0);
ESP_LOGI(TAG, "replied on %s", response_topic);
}
static void mqtt5_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, session present: %d", event->session_present);
esp_mqtt_client_publish(client, PREFIX "/status", "online", 0, 1, 1);
esp_mqtt5_subscribe_property_config_t sub_prop = {
.subscribe_id = 7,
.no_local_flag = true, /* no recibir el eco de nuestras publicaciones */
};
esp_mqtt5_client_set_subscribe_property(client, &sub_prop);
esp_mqtt_client_subscribe(client, PREFIX "/request", 1);
break;
}
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "data on %.*s: %.*s", event->topic_len, event->topic,
event->data_len, event->data);
print_user_properties(event->property->user_property);
if (event->property->response_topic_len > 0) {
reply_to_request(event);
}
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "disconnected");
print_user_properties(event->property->user_property);
break;
case MQTT_EVENT_ERROR:
if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
ESP_LOGE(TAG, "connection refused, reason code 0x%02x",
event->error_handle->connect_return_code);
}
break;
default:
break;
}
}
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());
esp_mqtt_client_config_t cfg = {
.broker.address.uri = "mqtt://broker.connectmq.com:1883",
.session.protocol_ver = MQTT_PROTOCOL_V_5,
.credentials.client_id = CLIENT_ID,
.session.last_will = {
.topic = PREFIX "/status",
.msg = "offline",
.msg_len = 7,
.qos = 1,
.retain = 1,
},
};
client = esp_mqtt_client_init(&cfg);
esp_mqtt5_client_set_user_property(&connect_property.user_property, connect_user_props,
sizeof(connect_user_props) / sizeof(connect_user_props[0]));
esp_mqtt5_client_set_connect_property(client, &connect_property);
esp_mqtt5_client_delete_user_property(connect_property.user_property);
connect_property.user_property = NULL;
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt5_event_handler, NULL);
esp_mqtt_client_start(client);
for (int seq = 0;; seq++) {
vTaskDelay(pdMS_TO_TICKS(5000));
float temperature = 20.0f + (esp_random() % 500) / 100.0f;
publish_telemetry(temperature, seq);
}
}
Compila, graba y monitoriza:
idf.py set-target esp32
idf.py build flash monitor
Ver cómo llegan las propiedades
Mosquitto 2.x imprime las propiedades de MQTT 5 cuando te suscribes con la versión de protocolo 5 y salida detallada:
mosquitto_sub -h broker.connectmq.com -p 1883 -V mqttv5 -i viewer-idf5m7 \
-t "connectmq/demo/idf5m7/#" -F "%t %P %p"
El token de formato %P imprime las user properties, así que cada línea de telemetría muestra schema:telemetry-v2 unit:celsius junto al JSON. Ahora prueba request/response. En una terminal escucha la respuesta y en otra envía una petición con un response topic y correlation data:
mosquitto_sub -h broker.connectmq.com -p 1883 -V mqttv5 -i reply-idf5m7 \
-t "connectmq/demo/idf5m7/reply/laptop" -v
mosquitto_pub -h broker.connectmq.com -p 1883 -V mqttv5 -i req-idf5m7 \
-t "connectmq/demo/idf5m7/request" -m '{"cmd":"health"}' \
-D publish response-topic "connectmq/demo/idf5m7/reply/laptop" \
-D publish correlation-data "req-001"
El ESP32 registra la petición, copia la correlation data y publica la respuesta en el topic que eligió el solicitante. Es el mismo patrón que usa un servicio backend para llamar a miles de dispositivos sin fijar en el código los topics de respuesta. El área pública es compartida y cualquiera puede leerla, así que úsala solo para experimentos; la página del servidor MQTT público gratuito explica sus límites, como la caducidad de sesión limitada a 30 minutos.
Cómo se comporta cada función
User properties
Las user properties son pares clave/valor de texto en la cabecera del paquete. Son ideales para metadatos que los consumidores pueden filtrar sin analizar el payload. Mantenlas cortas: se envían con cada mensaje y cuentan para el tamaño del paquete.
Message expiry interval
Un mensaje de telemetría con una caducidad de 60 segundos que queda en cola para un suscriptor desconectado es descartado por el broker cuando tiene más de un minuto. Cuando se entrega, se transmite el tiempo de vida restante. Usa caducidades cortas para comandos como "abrir válvula" que nunca deben ejecutarse tarde.
Topic alias
El cliente envía el topic una vez con el alias 1 y después puede referirse a él por su número. esp-mqtt mantiene su propia tabla de aliases para ello. El CONNACK del broker anuncia cuántos aliases acepta; nunca superes esa cifra. El ahorro importa sobre todo con topics largos al estilo ISA-95 en enlaces lentos o de pago por consumo.
Session expiry y will delay
Con session_expiry_interval = 600, el broker conserva tus suscripciones y los mensajes QoS 1 en cola durante diez minutos tras una desconexión, y session_present en el evento CONNECTED te indica si lo hizo. El will delay de 15 segundos implica que un microcorte de Wi-Fi seguido de una reconexión no muestra un estado offline en todos los dashboards. La guía de Last Will MQTT en ESP32 lo compara con el comportamiento de MQTT 3.1.1.
Response topic y correlation data
El solicitante decide adónde va la respuesta y adjunta un valor de correlación opaco, y el dispositivo simplemente devuelve ambos. Así el firmware se mantiene genérico: nunca necesita saber qué servicio o usuario preguntó. Da a cada solicitante su propio topic de respuesta y mantén corta la correlation data, como un contador o un UUID, porque el dispositivo la copia en la respuesta.
Reason codes
MQTT 5 sustituye el puñado de códigos de retorno de 3.1.1 por una lista detallada que también cubre publicación, suscripción y desconexión. Regístralos en los logs. Un dispositivo que informa de "0x87 not authorized" o "0x97 quota exceeded" te ahorra horas frente a un bucle de reconexión silencioso, y te permite distinguir errores de configuración de problemas de red.
Lista de comprobación para resolver problemas
- Errores de enlazado en las funciones
esp_mqtt5_*.CONFIG_MQTT_PROTOCOL_5no está activado. Borrasdkconfig, conservasdkconfig.defaultsy recompila. - Conexión rechazada con reason code 0x85. Identificador de cliente no válido. Usa un ID no vacío y, en un broker de producción, el client ID exacto emitido con tus credenciales.
- Reason code 0x97 tras muchas publicaciones. Cuota superada. Reduce el ritmo: el área pública permite 10 mensajes por segundo por cliente.
- Errores de topic alias y desconexiones. Has superado el Topic Alias Maximum del broker. Usa menos aliases o pon
topic_aliasa 0. - Basura al imprimir los topics.
event->topicyevent->datano terminan en nulo; imprímelos siempre con%.*sy los campos de longitud.
Pasar a TLS con tus propias credenciales
Para producción, cambia la URI a mqtts://broker.connectmq.com:8883, añade .broker.verification.crt_bundle_attach = esp_crt_bundle_attach (incluye esp_crt_bundle.h) y configura .credentials.username = "YOUR_USERNAME", .credentials.authentication.password = "YOUR_PASSWORD" y .credentials.client_id = "YOUR_CLIENT_ID". El paquete de certificados ya contiene la raíz de Let's Encrypt, así que no hace falta ningún archivo PEM. La lista completa de opciones está en la documentación de MQTT de ESP-IDF.
Ejecuta dispositivos MQTT 5 en ConnectMQ
Las propiedades solo sirven si el broker las respeta de extremo a extremo. ConnectMQ es un broker MQTT gestionado con session expiry, message expiry, topic aliases, response topics y reason codes claros de MQTT 5.0, además de suscripciones compartidas en el plan Serverless de pago para escalar los consumidores del backend. Crea un broker Serverless gratuito en app.connectmq.com sin tarjeta de crédito y apunta este proyecto hacia él. Para más recetas con dispositivos, el tutorial de IoT con ESP32 reúne la serie completa.
Preguntas frecuentes
¿esp-mqtt admite MQTT 5 en todos los chips ESP32?
Sí. El soporte de MQTT 5 forma parte del componente esp-mqtt, no del chip, así que funciona en el ESP32, S2, S3, C3, C6 y otros targets compatibles con tu versión de ESP-IDF una vez activado CONFIG_MQTT_PROTOCOL_5.
¿Puedo mezclar clientes MQTT 5 y MQTT 3.1.1 en el mismo broker?
Sí. Un broker que admite ambas versiones entrega mensajes entre ellos. Los suscriptores MQTT 3.1.1 simplemente no ven las propiedades de MQTT 5.
¿Tengo que configurar las propiedades antes de cada publicación?
Las propiedades de publicación se aplican a la siguiente llamada de publicación, así que configúralas cada vez que las necesites. Las propiedades de conexión se configuran una sola vez antes de arrancar el cliente.
¿Hay alguna librería de Arduino compatible con MQTT 5?
La mayoría de las librerías MQTT populares de Arduino solo implementan MQTT 3.1.1. Si necesitas MQTT 5 en un ESP32, ESP-IDF con esp-mqtt es la opción más completa, y también puedes usarlo desde proyectos con Arduino como componente.



