Tutoriels

MQTT avec ESP-IDF (esp-mqtt) sur ESP32 : configuration, événements et TLS

Utilisez le client esp-mqtt d'ESP-IDF sur ESP32 : esp_mqtt_client_config_t, gestionnaire d'événements, publish, subscribe, Last Will, TLS sur 8883 et tests.

Mis à jour · 9 min de lecture

MQTT avec ESP-IDF (esp-mqtt) sur ESP32 : configuration, événements et TLS

esp-mqtt est le client MQTT livré avec le framework ESP-IDF d'Espressif. Il tourne dans sa propre tâche FreeRTOS, se reconnecte automatiquement, prend en charge MQTT 3.1.1 et MQTT 5, et gère TCP, TLS, WebSocket et WebSocket sécurisé avec une seule URI. Vous le configurez avec une unique structure esp_mqtt_client_config_t, enregistrez un gestionnaire d'événements et appelez esp_mqtt_client_start().

Ce tutoriel construit pas à pas une application ESP32 complète : d'abord du TCP en clair, puis les événements, la publication, les abonnements, un Last Will et enfin TLS sur le port 8883. À chaque étape, vous vérifiez le résultat depuis un navigateur : quand quelque chose casse, vous savez que le problème vient de l'appareil et non de votre test.

Préparer le projet

Il vous faut ESP-IDF 5.x installé (idf.py --version doit fonctionner dans votre terminal). Partez de l'assistant Wi-Fi utilisé par les exemples officiels pour que le tutoriel se concentre sur MQTT :

Create the project
idf.py create-project mqtt_demo
cd mqtt_demo
idf.py set-target esp32

Rendez le composant Wi-Fi d'exemple visible dans le CMakeLists.txt racine (avant la ligne include(...project.cmake)) et déclarez les dépendances de main. Le composant mqtt fait partie d'ESP-IDF : #include "mqtt_client.h" fonctionne donc sans téléchargement supplémentaire sur les versions 5.x.

CMakeLists.txt (project root)
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
main/CMakeLists.txt
idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS "."
                       PRIV_REQUIRES mqtt esp_wifi esp_netif nvs_flash protocol_examples_common)

Lancez ensuite idf.py menuconfig, ouvrez Example Connection Configuration et saisissez le SSID et le mot de passe de votre Wi-Fi.

La configuration du client

La configuration minimale est une URI de broker. Le schéma choisit le transport :

URITransportPort
mqtt://broker.connectmq.com:1883TCP1883
mqtts://broker.connectmq.com:8883TLS8883
ws://broker.connectmq.com:8083/mqttWebSocket8083
wss://broker.connectmq.com:8084/mqttWebSocket sécurisé8084

Une configuration réaliste définit aussi un client ID, des identifiants, le keep alive et un Last Will :

main/main.c — configuration
#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);
}

Chaque client a besoin d'un client ID unique : deux appareils avec le même ID s'éjectent mutuellement du broker en boucle. En production, dérivez-le de l'adresse MAC, ou générez-en un avec le générateur de client ID pendant vos tests. Gardez les identifiants hors du contrôle de version en les déplaçant dans des options Kconfig.projbuild (CONFIG_MQTT_USER, CONFIG_MQTT_PASS) renseignées dans menuconfig ou sdkconfig.defaults.

Gérer les événements

esp-mqtt signale tout via l'event loop d'ESP. Un seul gestionnaire reçoit tous les événements ; event_data pointe vers un esp_mqtt_event_t :

main/main.c — event handler
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 et data ne sont PAS terminés par un caractère nul
        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;
    }
}

Trois détails piègent presque tout le monde :

  • Abonnez-vous dans MQTT_EVENT_CONNECTED, pas après start(). Le client peut se reconnecter de nombreuses fois ; s'abonner à chaque connexion restaure vos abonnements même si le broker n'a pas conservé la session.
  • topic et data ne sont pas des chaînes C. Utilisez toujours les champs _len. Les gros messages arrivent en plusieurs événements MQTT_EVENT_DATA ; event->current_data_offset et event->total_data_len permettent de les réassembler, sinon augmentez le buffer avec .buffer.size.
  • Le gestionnaire s'exécute dans la tâche MQTT. Ne le bloquez pas. Confiez les traitements longs à une autre tâche via une file FreeRTOS.

Démarrez le tout depuis app_main :

main/main.c — 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());          // bloque jusqu'à ce que le Wi-Fi ait une IP

    client = mqtt_create();
    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
    esp_mqtt_client_start(client);
}

Flashez et suivez le log avec idf.py flash monitor. Vous devriez voir connected suivi de subscribed.

Publier de la télémétrie

esp_mqtt_client_publish(client, topic, data, len, qos, retain) renvoie un ID de message (0 en QoS 0, -1 en cas d'erreur). Passez len = 0 pour que la bibliothèque appelle strlen() sur un payload texte. En QoS 1 et 2, l'appel écrit depuis la tâche appelante et peut attendre le réseau ; esp_mqtt_client_enqueue() prend les mêmes arguments plus un drapeau store et laisse l'envoi à la tâche MQTT :

Telemetry task
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));
    }
}
// dans app_main, après esp_mqtt_client_start() :
// xTaskCreate(telemetry_task, "telemetry", 3072, NULL, 5, NULL);

Les messages mis en file pendant que le client est hors ligne restent dans l'outbox (limités par CONFIG_MQTT_OUTBOX_EXPIRED_TIMEOUT_MS et le heap disponible) et sont envoyés après la reconnexion. Choisissez le QoS en connaissance de cause : voyez les niveaux de QoS MQTT pour le coût de chaque niveau.

Vérifier depuis le navigateur

Ouvrez le client MQTT en ligne, connectez-vous à broker.connectmq.com (WebSocket 8084, chemin /mqtt) et abonnez-vous à testmqtt/espidf/dev01/#. Vous devriez voir :

  1. testmqtt/espidf/dev01/status = online, marqué comme retained.
  2. Un message de télémétrie toutes les cinq secondes.

Publiez maintenant reboot sur testmqtt/espidf/dev01/cmd/system depuis le navigateur : le log de l'ESP32 affiche le topic et le payload. Enfin, débranchez l'alimentation. Après environ 1,5 × keep alive (45 secondes ici), le broker publie le Last Will et le statut passe à offline. Ce seul test prouve que la connexion, le publish, le subscribe et le will fonctionnent. Le fonctionnement du Last Will détaille les délais.

Observer votre ESP32 dans le client en ligne

Ajouter TLS sur le port 8883

Passer à TLS demande deux changements : une URI mqtts:// et un moyen de vérifier le certificat du serveur. Le plus simple est l'ESP x509 certificate bundle, qui contient les CA racines publiques courantes et est activé par défaut (CONFIG_MBEDTLS_CERTIFICATE_BUNDLE) :

TLS with the 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 votre broker utilise une CA privée ou si vous voulez épingler une racine précise, intégrez plutôt le fichier PEM :

main/CMakeLists.txt
idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
                       PRIV_REQUIRES mqtt esp_wifi esp_netif nvs_flash protocol_examples_common
                       EMBED_TXTFILES "ca.pem")
Embedded CA certificate
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 a besoin de l'heure exacte pour contrôler la validité du certificat et d'environ 30 à 40 Ko de heap supplémentaire pendant le handshake ; sur un ESP32 chargé, c'est le premier point à examiner quand les connexions échouent. Notre guide TLS pour ESP32 couvre les certificats client (.credentials.authentication.certificate et .key) pour le TLS mutuel.

Erreurs esp-mqtt courantes

Ligne de logCauseCorrection
esp-tls: couldn't get hostname for :broker.connectmq.com:Échec DNS, généralement Wi-Fi pas encore connectéNe démarrer MQTT qu'une fois l'IP obtenue
mbedtls_ssl_handshake returned -0x2700Échec de vérification du certificatUtiliser le bundle ou la bonne CA ; vérifier l'heure système
mbedtls_ssl_setup returned -0x7F00Mémoire insuffisante pour TLSLibérer du heap, réduire d'autres buffers
Error transport connectPort bloqué ou mauvaise paire schéma/portVérifier mqtts:// avec 8883, mqtt:// avec 1883
MQTT_ERROR_TYPE_CONNECTION_REFUSED, code 5Non autoriséVérifier utilisateur, mot de passe et ACL

Les codes CONNACK numériques et les reason codes MQTT 5 sont répertoriés dans l'outil de recherche de reason codes. Pour les appareils qui se connectent puis décrochent toutes les quelques minutes, lisez déconnexions Wi-Fi et MQTT sur ESP32.

Activer MQTT 5

Activez CONFIG_MQTT_PROTOCOL_5 dans menuconfig (Component config → ESP-MQTT Configurations) et définissez .session.protocol_ver = MQTT_PROTOCOL_V_5. Les appels de base pour publish et subscribe ne changent pas ; les apports de MQTT 5 comme les user properties, le message expiry et les response topics se règlent avec des fonctions telles que esp_mqtt5_client_set_publish_property() avant le publish. Les connexions refusées renvoient alors des reason codes précis au lieu des six codes de retour de MQTT 3.1.1, ce qui facilite beaucoup le débogage de l'authentification.

esp-mqtt vous offre un client de niveau production avec très peu de code : une structure de configuration, un gestionnaire d'événements et un transport choisi par l'URI. Faites-le fonctionner en TCP clair, prouvez le publish, le subscribe et le will depuis le client en ligne, passez ensuite à mqtts:// et conservez les mêmes tests comme contrôle de non-régression avant chaque version du firmware.

Questions fréquentes

Faut-il une bibliothèque supplémentaire pour MQTT dans ESP-IDF ?

Pas pour la plupart des projets. Le client esp-mqtt est fourni avec ESP-IDF (en-tête mqtt_client.h). Certaines versions récentes d'ESP-IDF le distribuent via le component registry sous le nom espressif/mqtt : consultez la documentation de votre version si l'en-tête est introuvable.

Pourquoi esp_mqtt_client_publish bloque-t-il ?

En QoS 1 et 2, il écrit sur le socket depuis la tâche appelante et peut attendre le réseau. Utilisez esp_mqtt_client_enqueue pour placer le message dans l'outbox et laisser la tâche MQTT l'envoyer, ce qui est plus sûr dans du code soumis à des contraintes de temps.

esp-mqtt prend-il en charge MQTT 5 ?

Oui. Activez MQTT 5 dans menuconfig (Component config → ESP-MQTT Configurations) et réglez session.protocol_ver sur MQTT_PROTOCOL_V_5. Vous obtenez alors les reason codes, les user properties et les autres apports de MQTT 5 via des API dédiées.