ESP32 et Arduino

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.

Terminal exécutant idf.py monitor à côté d'un ESP32 dans un projet exemple MQTT 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

Cet exemple MQTT ESP-IDF construit un client esp-mqtt fonctionnel à partir d'un dossier vide : un Wi-Fi en mode station, un gestionnaire MQTT piloté par événements, la publication et l'abonnement, les propriétés de connexion MQTT 5 et enfin le TLS. À la fin, vous disposerez d'un firmware qui dialogue avec broker.connectmq.com et d'une vision claire de la structure du composant esp-mqtt.

Pourquoi utiliser ESP-IDF plutôt qu'Arduino

Les bibliothèques Arduino comme PubSubClient permettent de démarrer vite, mais elles se limitent à MQTT 3.1.1 et sont en grande partie bloquantes. Le composant esp-mqtt d'ESP-IDF est maintenu par Espressif, s'exécute dans sa propre tâche FreeRTOS, prend en charge MQTT 3.1, 3.1.1 et 5.0, et gère les transports TCP, TLS, WebSocket et WebSocket sécurisé au moyen d'une seule URI. La reconnexion, le keepalive et la gestion de l'outbox sont intégrés, et votre code réagit à des événements au lieu d'interroger en boucle.

La contrepartie, c'est une configuration plus lourde : CMake, menuconfig et une initialisation explicite du Wi-Fi. Ce guide réduit cette configuration au strict minimum. Il cible ESP-IDF 5.x, où la structure de configuration utilise des champs imbriqués comme .broker.address.uri. Le code écrit pour ESP-IDF 4.x utilisait des champs à plat et ne compilera pas tel quel. La référence de l'API qui fait foi est la documentation MQTT d'ESP-IDF.

Organisation du projet

Créez cette structure :

connectmq-idf/
  CMakeLists.txt
  sdkconfig.defaults
  main/
    CMakeLists.txt
    main.c

Le CMakeLists.txt de premier niveau :

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 ".")

Le composant main dépend automatiquement de tous les composants du framework : vous n'avez donc pas besoin de lister explicitement mqtt, esp_wifi ou nvs_flash. Dans ESP-IDF 5.x, esp-mqtt est livré avec le framework. Si votre version d'ESP-IDF l'a déplacé vers le registre de composants, ajoutez-le avec idf.py add-dependency "espressif/mqtt".

sdkconfig.defaults active la prise en charge de MQTT 5, désactivée par défaut :

CONFIG_MQTT_PROTOCOL_5=y

L'exemple MQTT ESP-IDF complet

Voici le fichier main/main.c complet. Il se connecte au Wi-Fi, démarre le client MQTT sur la zone publique de broker.connectmq.com, s'abonne à un topic de commande et publie un compteur toutes les cinq secondes.

#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,   /* secondes */
    };
    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));
    }
}

Compilez, flashez et ouvrez le moniteur :

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

Observez ensuite le trafic depuis votre ordinateur :

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

Le journal de l'ESP32 montre la commande hello qui arrive via MQTT_EVENT_DATA.

Comment fonctionne le modèle d'événements d'esp-mqtt

Le client s'exécute dans sa propre tâche, créée par esp_mqtt_client_start(). Tout ce qu'il a à vous dire arrive sous forme d'événement sur le gestionnaire que vous avez enregistré :

Événement Quand il se déclenche Action typique
MQTT_EVENT_CONNECTED CONNACK reçu S'abonner, publier un message de naissance
MQTT_EVENT_DISCONNECTED Connexion perdue Mettre à jour l'état ; le client se reconnecte automatiquement
MQTT_EVENT_SUBSCRIBED SUBACK reçu Confirmer les abonnements
MQTT_EVENT_PUBLISHED PUBACK/PUBCOMP pour la QoS 1/2 Suivre la livraison
MQTT_EVENT_DATA Message reçu Copier et répartir le payload
MQTT_EVENT_ERROR Erreur de transport ou de protocole Journaliser le type d'erreur et les codes

Trois détails piègent souvent les développeurs :

  1. Le topic et les données ne sont pas terminés par un caractère nul. Utilisez toujours topic_len et data_len, comme le fait le format %.*s ci-dessus.
  2. Les gros messages arrivent par morceaux. Si un payload dépasse le tampon du client (.buffer.size, 1024 octets par défaut), vous recevez plusieurs événements MQTT_EVENT_DATA. Vérifiez event->current_data_offset et event->total_data_len, et réassemblez.
  3. Abonnez-vous dans MQTT_EVENT_CONNECTED. Avec une clean session, les abonnements disparaissent après une reconnexion. Les effectuer dans le gestionnaire de connexion les rétablit à chaque fois.

Le gestionnaire s'exécute dans la tâche MQTT : gardez-le donc court. Le travail lourd a sa place dans une tâche séparée alimentée par une file d'attente, comme décrit dans le guide ESP32 FreeRTOS MQTT.

Publish ou enqueue

esp_mqtt_client_publish() est bloquant : il envoie le paquet depuis la tâche appelante et, pour les QoS 1 et 2, le conserve dans l'outbox jusqu'à son acquittement. L'appeler depuis le gestionnaire d'événements ne pose pas de problème. Si vous publiez depuis une tâche critique en temps, esp_mqtt_client_enqueue(client, topic, data, len, qos, retain, true) place le message dans l'outbox et rend la main immédiatement ; la tâche MQTT l'envoie plus tard. Les deux renvoient un identifiant de message, ou une valeur négative en cas d'échec : vérifiez-le donc.

Utiliser les fonctionnalités de MQTT 5

Définir .session.protocol_ver = MQTT_PROTOCOL_V_5 et activer CONFIG_MQTT_PROTOCOL_5 fait passer le client en MQTT 5. L'exemple définit déjà un intervalle d'expiration de session via esp_mqtt5_client_set_connect_property(). Sur la zone publique de broker.connectmq.com, l'expiration de session peut aller jusqu'à 30 minutes ; les 10 minutes utilisées ici tiennent largement dans cette limite.

Les propriétés de publication se définissent avant l'appel de publication :

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

L'expiration des messages empêche que des mesures périmées soient livrées à un abonné qui revient bien plus tard. Pour un tour plus approfondi des response topics, des user properties et des reason codes dans ce composant, consultez esp-mqtt et MQTT 5, et pour le côté protocole, les fonctionnalités de MQTT 5.

Choisir la QoS, le retain et le keepalive

L'exemple utilise volontairement trois réglages de publication différents. La télémétrie de routine part en QoS 0 sans retain : si une mesure est perdue, la suivante arrive cinq secondes plus tard, et rien n'attend d'acquittement. Le message de statut utilise la QoS 1 avec retain : il est donc confirmé par le broker, et tout tableau de bord qui s'abonne plus tard voit si l'appareil est en ligne. Le Last Will reprend ce topic de statut avec offline, que le broker publie si l'appareil disparaît sans déconnexion propre.

Le keepalive de 30 secondes est un compromis raisonnable. esp-mqtt envoie lui-même les paquets PINGREQ : contrairement aux bibliothèques Arduino, vous n'avez donc pas besoin d'appeler une fonction de boucle pour maintenir la session. Un keepalive plus court détecte plus vite les appareils morts mais coûte plus de temps radio ; un keepalive plus long ménage les nœuds sur batterie. Si les garanties de livraison sont nouvelles pour vous, les niveaux de QoS MQTT expliqués détaille ce que coûte chaque niveau sur le réseau.

Passer au TLS sur le port 8883

Au-delà d'une démo, chiffrez la connexion. ESP-IDF fournit un bundle de certificats contenant les CA racines courantes, dont la racine Let's Encrypt utilisée par ConnectMQ : vous n'avez donc pas besoin de coller un fichier PEM. Changez le schéma de l'URI et attachez le 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,
};

Vérifiez que CONFIG_MBEDTLS_CERTIFICATE_BUNDLE est activé (il l'est par défaut). Utilisez le client ID, le nom d'utilisateur et le mot de passe générés pour votre broker dans le portail ; le client ID est lié à ces identifiants, si bien qu'un ID inventé est rejeté. La même approche par URI fonctionne pour les transports WebSocket : ws://broker.connectmq.com:8083/mqtt et wss://broker.connectmq.com:8084/mqtt, si CONFIG_MQTT_TRANSPORT_WEBSOCKET est activé.

Liste de contrôle pour le dépannage

  1. Erreurs de compilation sur .uri ou .host : vous mélangez les syntaxes de configuration d'ESP-IDF 4.x et 5.x. Utilisez les champs imbriqués présentés ici.
  2. Fonctions MQTT 5 non définies : CONFIG_MQTT_PROTOCOL_5 n'est pas défini. Supprimez sdkconfig après avoir ajouté sdkconfig.defaults, ou activez-le dans idf.py menuconfig.
  3. MQTT_ERROR_TYPE_TCP_TRANSPORT juste après le démarrage : le Wi-Fi n'est pas encore actif, le DNS a échoué, ou le schéma et le port ne correspondent pas (mqtts:// nécessite le 8883).
  4. Connexion refusée avec des identifiants : vérifiez que le client ID est exactement celui du portail et que le champ du mot de passe est .credentials.authentication.password.
  5. Payloads tronqués : augmentez .buffer.size ou réassemblez les événements MQTT_EVENT_DATA fragmentés.
  6. Débordement de pile dans la tâche MQTT : augmentez .task.stack_size, en particulier avec le TLS et des journaux verbeux.

Si vous traitez des échecs de connexion de manière générale, le guide sur que faire quand un ESP32 ne se connecte pas à un broker MQTT couvre des vérifications au niveau réseau qui s'appliquent aussi à ESP-IDF.

Passez votre firmware esp-mqtt en production avec ConnectMQ

Vous venez d'exécuter cet exemple MQTT ESP-IDF sur le serveur MQTT public gratuit, prévu pour les tests. Lorsque vous serez prêt pour des identifiants privés, ConnectMQ vous offre un broker MQTT managé avec MQTT 5, le TLS sur 8883 et un certificat Let's Encrypt auquel le bundle ESP-IDF fait déjà confiance. Créez-en un avec l'offre gratuite Serverless sur app.connectmq.com, sans carte bancaire, et ne changez que l'URI et les identifiants dans votre configuration. Pour une vue d'ensemble des projets d'appareils, consultez le tutoriel IoT ESP32.

Questions fréquentes

esp-mqtt se reconnecte-t-il automatiquement ?

Oui. Après une déconnexion, le client retente de lui-même en utilisant .network.reconnect_timeout_ms. Réabonnez-vous dans MQTT_EVENT_CONNECTED pour que les abonnements reviennent avec la connexion.

Quelle version d'ESP-IDF cet exemple nécessite-t-il ?

Il cible ESP-IDF 5.x, où la configuration du client utilise des champs imbriqués comme .broker.address.uri. ESP-IDF 4.x utilisait une structure à plat et nécessite de petites modifications.

Puis-je utiliser MQTT 5 avec l'offre gratuite de ConnectMQ ?

Oui. Tous les brokers de la plateforme prennent en charge MQTT 3.1.1 et 5.0. L'offre gratuite Serverless prend en charge la QoS jusqu'à 1 : utilisez donc la QoS 0 ou 1 dans vos publications.

Pourquoi le topic reçu s'affiche-t-il avec des caractères parasites à la fin ?

Les pointeurs de topic et de données dans MQTT_EVENT_DATA ne sont pas terminés par un caractère nul. Affichez-les avec %.*s et les champs de longueur, ou copiez-les dans un tampon terminé.

Articles liés

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.