ESP32 et Arduino

Utiliser les fonctionnalités MQTT 5 d'esp-mqtt sur ESP32 avec ESP-IDF

Un projet ESP-IDF pratique qui active MQTT 5.0 dans esp-mqtt et utilise les user properties, l'expiration des messages, les topic aliases, l'expiration de session et les response topics avec un vrai broker MQTT 5.

Carte ESP32 envoyant des paquets MQTT 5 avec user properties et en-têtes d'expiration, illustrant esp-mqtt mqtt 5 dans ESP-IDF
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

La prise en charge de MQTT 5 par esp-mqtt dans ESP-IDF permet à un ESP32 d'associer des métadonnées aux messages, de définir des durées d'expiration, de raccourcir les topics grâce aux alias et de construire des échanges requête/réponse sans inventer ses propres conventions. Dans ce tutoriel, vous allez activer MQTT 5.0 dans un projet ESP-IDF, vous connecter à broker.connectmq.com et utiliser chaque fonctionnalité dans du code C fonctionnel que vous pouvez flasher dès aujourd'hui.

Pourquoi MQTT 5 sur un microcontrôleur

MQTT 3.1.1 est parfaitement utilisable sur un ESP32, et la plupart des bibliothèques Arduino s'arrêtent là. MQTT 5.0 ajoute des fonctionnalités qui résolvent de vrais problèmes côté appareil :

Fonctionnalité Problème résolu sur un appareil
User properties Transmettre version de firmware, unités ou version de schéma sans alourdir le payload JSON
Intervalle d'expiration des messages Empêcher que des commandes ou mesures périmées soient livrées des heures plus tard
Topic alias Remplacer une longue chaîne de topic par un nombre de 2 octets lors des publications répétées
Intervalle d'expiration de session Conserver les abonnements lors de courtes coupures Wi-Fi sans les garder indéfiniment
Response topic et correlation data Requête/réponse standard, pour qu'un backend puisse interroger un appareil et associer la réponse
Reason codes Savoir précisément pourquoi une connexion, une publication ou un abonnement a été refusé

esp-mqtt, le client MQTT fourni avec ESP-IDF, implémente tout cela derrière une option de compilation. Il vous faut aussi un broker qui parle MQTT 5.0. ConnectMQ prend en charge 3.1.1 et 5.0 sur tous ses listeners : le même endpoint fonctionne donc pour les anciens comme pour les nouveaux firmwares. Pour le contexte protocolaire, les fonctionnalités de MQTT 5 expliquées détaille chaque propriété.

Prérequis

  • ESP-IDF 5.x installé, avec idf.py fonctionnel dans votre terminal.
  • N'importe quelle carte de la famille ESP32 (ESP32, S3, C3, C6).
  • Les clients Mosquitto 2.x sur votre ordinateur ; mosquitto_sub sait afficher les propriétés MQTT 5.
  • Une connaissance d'un projet MQTT ESP-IDF de base. Si vous n'en avez jamais construit, commencez par l'exemple MQTT ESP-IDF puis revenez.

Configuration du projet et activation de MQTT 5.0

Créez un dossier de projet avec cette organisation :

mqtt5_connectmq/
  CMakeLists.txt
  sdkconfig.defaults
  main/
    CMakeLists.txt
    main.c

Le CMakeLists.txt de premier niveau, qui intègre l'assistant Wi-Fi utilisé par les exemples officiels :

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 active les chemins de code MQTT 5 dans esp-mqtt. Sans cette option, les fonctions esp_mqtt5_* ne sont pas compilées :

CONFIG_MQTT_PROTOCOL_5=y

Renseignez ensuite vos identifiants Wi-Fi avec idf.py menuconfig, sous Example Connection Configuration. Si votre version d'ESP-IDF n'embarque plus le composant mqtt dans l'arborescence, ajoutez-le avec idf.py add-dependency espressif/mqtt.

Le code d'exemple esp-mqtt MQTT 5

Remplacez la partie aléatoire de PREFIX et le client ID par vos propres valeurs.

#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;

/* Propriétés envoyées une fois dans le CONNECT */
static esp_mqtt5_connection_property_config_t connect_property = {
    .session_expiry_interval = 600,      /* conserver la session 10 min après déconnexion */
    .maximum_packet_size = 4096,
    .receive_maximum = 16,
    .topic_alias_maximum = 4,            /* alias acceptés depuis le broker */
    .request_resp_info = true,
    .request_problem_info = true,
    .will_delay_interval = 15,           /* attendre 15 s avant de publier le 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,    /* inutile au-delà d'une minute */
        .topic_alias = 1,                 /* associer ce topic à l'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,        /* ne pas recevoir nos propres publications */
        };
        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);
    }
}

Compilez, flashez et surveillez :

idf.py set-target esp32
idf.py build flash monitor

Voir arriver les propriétés

Mosquitto 2.x affiche les propriétés MQTT 5 lorsque vous vous abonnez avec la version 5 du protocole et une sortie détaillée :

mosquitto_sub -h broker.connectmq.com -p 1883 -V mqttv5 -i viewer-idf5m7 \
  -t "connectmq/demo/idf5m7/#" -F "%t %P %p"

Le jeton de format %P affiche les user properties : chaque ligne de télémétrie montre donc schema:telemetry-v2 unit:celsius à côté du JSON. Testez maintenant la requête/réponse. Dans un terminal, écoutez la réponse, et dans un autre envoyez une requête avec un response topic et des 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"

L'ESP32 journalise la requête, copie les correlation data et publie la réponse sur le topic choisi par le demandeur. C'est exactement le modèle qu'utilise un service backend pour interroger des milliers d'appareils sans coder en dur les topics de réponse. La zone publique est partagée et lisible par tous : réservez-la aux expérimentations. La page du serveur MQTT public gratuit en explique les limites, comme l'expiration de session plafonnée à 30 minutes.

Comment se comporte chaque fonctionnalité

User properties

Les user properties sont des paires clé/valeur de chaînes placées dans l'en-tête du paquet. Elles sont idéales pour des métadonnées que les consommateurs peuvent filtrer sans analyser le payload. Gardez-les courtes : elles sont envoyées avec chaque message et comptent dans la taille du paquet.

Intervalle d'expiration des messages

Un message de télémétrie avec une expiration de 60 secondes, mis en attente pour un abonné hors ligne, est supprimé par le broker dès qu'il a plus d'une minute. Lorsqu'il est livré, la durée de vie restante est transmise. Utilisez des expirations courtes pour des commandes comme « ouvrir la vanne » qui ne doivent jamais s'exécuter en retard.

Topic alias

Le client envoie le topic une seule fois avec l'alias 1, puis peut y faire référence par son numéro. esp-mqtt maintient pour cela sa propre table d'alias. Le CONNACK du broker annonce combien d'alias il accepte ; ne dépassez jamais ce nombre. Le gain est le plus sensible pour les longs topics de style ISA-95 sur des liaisons lentes ou facturées au volume.

Expiration de session et délai du will

Avec session_expiry_interval = 600, le broker conserve vos abonnements et les messages QoS 1 en attente pendant dix minutes après une déconnexion, et session_present dans l'événement CONNECTED vous indique s'il l'a fait. Le délai de will de 15 secondes signifie qu'une brève coupure Wi-Fi suivie d'une reconnexion ne fait pas clignoter un statut offline sur tous les tableaux de bord. Le guide ESP32 MQTT Last Will compare ce fonctionnement avec celui de MQTT 3.1.1.

Response topic et correlation data

Le demandeur décide où va la réponse et joint une valeur de corrélation opaque, et l'appareil se contente de renvoyer les deux. Le firmware reste ainsi générique : il n'a jamais besoin de savoir quel service ou quel utilisateur a posé la question. Donnez à chaque demandeur son propre topic de réponse, et gardez les correlation data courtes, comme un compteur ou un UUID, car l'appareil les recopie dans la réponse.

Reason codes

MQTT 5 remplace la poignée de codes de retour de 3.1.1 par une liste détaillée qui couvre aussi la publication, l'abonnement et la déconnexion. Journalisez-les. Un appareil qui indique « 0x87 not authorized » ou « 0x97 quota exceeded » dans ses logs vous fait gagner des heures par rapport à une boucle de reconnexion silencieuse, et vous permet de distinguer les erreurs de configuration des problèmes réseau.

Liste de vérification pour le dépannage

  1. Erreurs d'édition de liens sur les fonctions esp_mqtt5_*. CONFIG_MQTT_PROTOCOL_5 n'est pas défini. Supprimez sdkconfig, conservez sdkconfig.defaults et recompilez.
  2. Connexion refusée avec le reason code 0x85. Identifiant client invalide. Utilisez un ID non vide et, sur un broker de production, le client ID exact émis avec vos identifiants.
  3. Reason code 0x97 après de nombreuses publications. Quota dépassé. Ralentissez : la zone publique autorise 10 messages par seconde et par client.
  4. Erreurs de topic alias et déconnexions. Vous avez dépassé le Topic Alias Maximum du broker. Utilisez moins d'alias ou mettez topic_alias à 0.
  5. Caractères parasites à l'affichage des topics. event->topic et event->data ne sont pas terminés par un caractère nul ; affichez-les toujours avec %.*s et les champs de longueur.

Passer à TLS avec vos propres identifiants

Pour la production, remplacez l'URI par mqtts://broker.connectmq.com:8883, ajoutez .broker.verification.crt_bundle_attach = esp_crt_bundle_attach (en incluant esp_crt_bundle.h), et définissez .credentials.username = "YOUR_USERNAME", .credentials.authentication.password = "YOUR_PASSWORD" et .credentials.client_id = "YOUR_CLIENT_ID". Le bundle de certificats contient déjà la racine Let's Encrypt : aucun fichier PEM n'est nécessaire. La liste complète des options figure dans la documentation MQTT d'ESP-IDF.

Faites tourner vos appareils MQTT 5 sur ConnectMQ

Les propriétés ne servent que si le broker les respecte de bout en bout. ConnectMQ est un broker MQTT managé qui prend en charge l'expiration de session MQTT 5.0, l'expiration des messages, les topic aliases, les response topics et des reason codes clairs, ainsi que les abonnements partagés sur l'offre Serverless payante pour faire monter en charge les consommateurs backend. Créez un broker Serverless gratuit sur app.connectmq.com, sans carte bancaire, et pointez ce projet dessus. Pour d'autres recettes côté appareil, le tutoriel IoT ESP32 rassemble toute la série.

Questions fréquentes

esp-mqtt prend-il en charge MQTT 5 sur toutes les puces ESP32 ?

Oui. La prise en charge de MQTT 5 fait partie du composant esp-mqtt, pas de la puce : elle fonctionne donc sur l'ESP32, le S2, le S3, le C3, le C6 et les autres cibles prises en charge par votre version d'ESP-IDF dès que CONFIG_MQTT_PROTOCOL_5 est activé.

Puis-je mélanger des clients MQTT 5 et MQTT 3.1.1 sur le même broker ?

Oui. Un broker qui prend en charge les deux versions achemine les messages entre eux. Les abonnés MQTT 3.1.1 ne voient simplement pas les propriétés MQTT 5.

Dois-je définir les propriétés avant chaque publication ?

Les propriétés de publication s'appliquent à l'appel de publication suivant : définissez-les donc chaque fois que vous en avez besoin. Les propriétés de connexion sont définies une seule fois avant le démarrage du client.

Existe-t-il une bibliothèque Arduino compatible MQTT 5 ?

La plupart des bibliothèques MQTT Arduino populaires n'implémentent que MQTT 3.1.1. Si vous avez besoin de MQTT 5 sur un ESP32, ESP-IDF avec esp-mqtt est l'option la plus complète, et vous pouvez aussi l'appeler depuis des projets Arduino-as-a-component.

Articles liés

Exemple MQTT ESP-IDF : construire un client esp-mqtt à partir de zéro

Construisez un projet ESP-IDF à partir d'un dossier vide avec le composant esp-mqtt : configuration du Wi-Fi en mode station, gestionnaire d'événements MQTT, publication et abonnement, propriétés MQTT 5 et TLS avec le bundle de certificats.

9 min de lecture

Horodatage NTP ESP32 MQTT : ajouter une heure précise à chaque message

Synchronisez l'horloge de l'ESP32 avec NTP, formatez des horodatages ISO 8601 et epoch en millisecondes, et joignez-les aux payloads MQTT pour que les mesures conservent leur véritable heure de capture même lorsqu'elles sont publiées en retard.

8 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.