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 :
- Le topic et les données ne sont pas terminés par un caractère nul. Utilisez toujours
topic_lenetdata_len, comme le fait le format%.*sci-dessus. - 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énementsMQTT_EVENT_DATA. Vérifiezevent->current_data_offsetetevent->total_data_len, et réassemblez. - 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
- Erreurs de compilation sur
.uriou.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. - Fonctions MQTT 5 non définies :
CONFIG_MQTT_PROTOCOL_5n'est pas défini. Supprimezsdkconfigaprès avoir ajoutésdkconfig.defaults, ou activez-le dansidf.py menuconfig. MQTT_ERROR_TYPE_TCP_TRANSPORTjuste 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).- 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. - Payloads tronqués : augmentez
.buffer.sizeou réassemblez les événementsMQTT_EVENT_DATAfragmentés. - 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é.



