Un client MQTT asynchrone sur ESP32 permet à votre firmware de continuer à lire des capteurs, piloter des écrans et servir des pages web pendant que le trafic MQTT est traité en arrière-plan. Dans ce guide, vous allez construire un sketch AsyncMqttClient complet avec des callbacks d'événements, des reconnexions automatiques et une publication en QoS 1 vers un vrai broker. Vous découvrirez aussi les pièges de la bibliothèque afin de savoir quand elle constitue le bon choix.
Pourquoi utiliser un client MQTT asynchrone
Les bibliothèques bloquantes comme PubSubClient fonctionnent bien, mais elles exigent que vous appeliez loop() fréquemment, et connect() peut figer votre sketch jusqu'à l'expiration du délai du socket lorsque le réseau est mauvais. Sur un appareil qui pilote aussi un moteur, rafraîchit un ruban LED ou héberge une page de configuration, ces pauses se voient.
AsyncMqttClient, de Marvin Roger, adopte une autre approche. Il repose sur la bibliothèque AsyncTCP, qui exécute la partie réseau dans sa propre tâche FreeRTOS. Votre code enregistre des callbacks pour les événements (connexion, déconnexion, message reçu, publication acquittée) et la bibliothèque les appelle lorsque quelque chose se produit. Il n'y a pas de loop() à appeler.
| Aspect | PubSubClient | AsyncMqttClient |
|---|---|---|
| Modèle de programmation | Scrutation, appel de loop() |
Callbacks d'événements |
Comportement de connect() |
Bloque jusqu'au CONNACK ou au timeout | Retourne immédiatement |
| QoS en publication | 0 | 0, 1 et 2 |
| QoS en abonnement | 0 et 1 | 0, 1 et 2 |
| Gros payloads entrants | Doivent tenir dans le buffer | Livrés par fragments |
| TLS sur ESP32 | Oui, via WiFiClientSecure |
Peu réaliste avec AsyncTCP standard |
| Dépendances | Aucune | AsyncTCP |
Cette dernière ligne compte. La prise en charge du TLS par AsyncMqttClient dépend d'une version d'AsyncTCP compilée avec SSL, qui n'existe pas dans la bibliothèque AsyncTCP standard pour ESP32. En pratique, cette bibliothèque convient surtout au MQTT en clair sur le port 1883 au sein d'un réseau de confiance, ou au prototypage. Si vous avez besoin du TLS avec une API événementielle, la bibliothèque espMqttClient, activement maintenue, propose un style de callbacks très proche avec prise en charge du TLS sur ESP32. Nous détaillons les compromis à la fin.
Installer les bibliothèques
- Installez le paquet de cartes « esp32 by Espressif Systems » dans l'Arduino IDE.
- Installez AsyncTCP pour ESP32. Plusieurs forks maintenus existent dans le gestionnaire de bibliothèques ; choisissez celui qui correspond à la version de votre core ESP32.
- Installez AsyncMqttClient. S'il n'apparaît pas dans le gestionnaire de bibliothèques, téléchargez le ZIP de la release depuis le dépôt du projet et utilisez Croquis → Inclure une bibliothèque → Ajouter la bibliothèque .ZIP.
- Avec PlatformIO, ajoutez les deux à
lib_depsdansplatformio.ini.
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
marvinroger/AsyncMqttClient
esp32async/AsyncTCP
Le sketch complet de client MQTT asynchrone pour ESP32
Cet exemple se connecte à broker.connectmq.com, le serveur MQTT public gratuit de ConnectMQ, réservé aux tests et aux prototypes, sur le port 1883. Il reconnecte le Wi-Fi et MQTT à l'aide de timers FreeRTOS, publie de la télémétrie en QoS 1 toutes les dix secondes et bascule la LED intégrée à la réception d'une commande.
#include <WiFi.h>
#include <AsyncMqttClient.h>
extern "C" {
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
}
#define WIFI_SSID "YOUR_WIFI"
#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD"
#define MQTT_HOST "broker.connectmq.com"
#define MQTT_PORT 1883
#define LED_PIN 2
static const char* DEVICE_ID = "async-4b81d2"; // rendez cet ID unique
String topicTelemetry = String("connectmq/demo/") + DEVICE_ID + "/telemetry";
String topicCommand = String("connectmq/demo/") + DEVICE_ID + "/led/set";
String topicStatus = String("connectmq/demo/") + DEVICE_ID + "/status";
AsyncMqttClient mqttClient;
TimerHandle_t mqttReconnectTimer;
TimerHandle_t wifiReconnectTimer;
unsigned long lastPublish = 0;
void connectToWifi() {
Serial.println("Connecting to Wi-Fi...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
void connectToMqtt() {
Serial.println("Connecting to MQTT...");
mqttClient.connect();
}
void onWifiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
Serial.print("Wi-Fi connected, IP ");
Serial.println(WiFi.localIP());
connectToMqtt();
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
Serial.println("Wi-Fi lost");
xTimerStop(mqttReconnectTimer, 0); // pas de MQTT tant que le Wi-Fi est coupé
xTimerStart(wifiReconnectTimer, 0);
break;
default:
break;
}
}
void onMqttConnect(bool sessionPresent) {
Serial.printf("MQTT connected, session present: %d\n", sessionPresent);
mqttClient.subscribe(topicCommand.c_str(), 1);
mqttClient.publish(topicStatus.c_str(), 1, true, "online");
}
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
Serial.printf("MQTT disconnected, reason %d\n", static_cast<int>(reason));
if (WiFi.isConnected()) {
xTimerStart(mqttReconnectTimer, 0);
}
}
void onMqttSubscribe(uint16_t packetId, uint8_t qos) {
Serial.printf("Subscribe ack, packetId %u, granted QoS %u\n", packetId, qos);
}
void onMqttPublish(uint16_t packetId) {
Serial.printf("Publish ack (PUBACK), packetId %u\n", packetId);
}
void onMqttMessage(char* topic, char* payload,
AsyncMqttClientMessageProperties properties,
size_t len, size_t index, size_t total) {
// payload n'est pas terminé par un zéro et peut n'être qu'un fragment d'un message plus grand
if (index != 0 || len != total) {
Serial.println("Fragmented message ignored in this demo");
return;
}
String msg;
msg.reserve(len);
for (size_t i = 0; i < len; i++) msg += payload[i];
Serial.printf("Message on %s (QoS %u): %s\n", topic, properties.qos, msg.c_str());
if (msg == "ON") digitalWrite(LED_PIN, HIGH);
if (msg == "OFF") digitalWrite(LED_PIN, LOW);
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE,
nullptr, reinterpret_cast<TimerCallbackFunction_t>(connectToMqtt));
wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE,
nullptr, reinterpret_cast<TimerCallbackFunction_t>(connectToWifi));
WiFi.onEvent(onWifiEvent);
mqttClient.onConnect(onMqttConnect);
mqttClient.onDisconnect(onMqttDisconnect);
mqttClient.onSubscribe(onMqttSubscribe);
mqttClient.onPublish(onMqttPublish);
mqttClient.onMessage(onMqttMessage);
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
mqttClient.setClientId(DEVICE_ID); // le pointeur doit rester valide
mqttClient.setKeepAlive(30);
mqttClient.setWill(topicStatus.c_str(), 1, true, "offline");
connectToWifi();
}
void loop() {
// Votre code applicatif s'exécute librement ici ; MQTT est géré en arrière-plan.
if (mqttClient.connected() && millis() - lastPublish > 10000) {
lastPublish = millis();
char payload[96];
snprintf(payload, sizeof(payload), "{\"uptime\":%lu,\"rssi\":%d,\"heap\":%u}",
millis() / 1000, WiFi.RSSI(), (unsigned)ESP.getFreeHeap());
uint16_t id = mqttClient.publish(topicTelemetry.c_str(), 1, false, payload);
Serial.printf("Queued publish, packetId %u\n", id);
}
}
Observez le trafic et envoyez des commandes depuis votre ordinateur :
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-4b81d2 -t "connectmq/demo/async-4b81d2/#" -v -q 1
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-4b81d2 -t "connectmq/demo/async-4b81d2/led/set" -m ON -q 1
Comment fonctionne le flux d'événements
Comprendre l'ordre des événements évite la plupart des bugs :
connectToWifi()lance la connexion Wi-Fi et retourne aussitôt.- Lorsque la station obtient une adresse IP,
onWifiEventappelleconnectToMqtt(). mqttClient.connect()retourne immédiatement. Plus tard, la tâche d'AsyncTCP reçoit le CONNACK et la bibliothèque appelleonMqttConnect.- Dans
onMqttConnect, vous vous abonnez et publiez votre statut « online ». C'est le bon endroit, car avec une session propre (clean session), les abonnements doivent être renouvelés après chaque reconnexion. - Si le broker ou le réseau vous déconnecte,
onMqttDisconnectprogramme un timer à déclenchement unique qui appelleconnectToMqtt()deux secondes plus tard.
publish() renvoie un identifiant de paquet pour les messages en QoS 1 et 2, 1 pour un message QoS 0 mis en file d'attente, et 0 lorsque le message n'a pas pu être envoyé. Le callback onMqttPublish(packetId) correspondant se déclenche lorsque le broker acquitte la livraison, ce qui vous permet de vérifier que les messages importants sont bien arrivés.
Les pièges qui font mal en production
Les callbacks s'exécutent dans une autre tâche
Vos callbacks s'exécutent dans la tâche AsyncTCP, pas dans loop(). N'y appelez pas delay() et n'y faites pas d'entrées/sorties lentes : cela bloque tout le trafic réseau et peut déclencher le watchdog de tâche. Copiez les données dans une variable ou une file d'attente et traitez-les dans loop(). Si loop() et un callback partagent une variable, protégez-la, par exemple avec une file FreeRTOS ou, dans les cas simples, un indicateur volatile.
Durée de vie des chaînes
setClientId(), setCredentials(), setServer() avec un nom d'hôte et setWill() stockent des pointeurs, pas des copies. Passer le c_str() d'une String temporaire fait envoyer des données aléatoires lors de la reconnexion. Utilisez des constantes globales ou des objets String globaux, comme dans l'exemple.
Payloads fragmentés
Les gros messages entrants arrivent en plusieurs appels à onMessage, avec un index croissant et le même total. Si vous attendez des payloads de plus de quelques centaines d'octets, accumulez les fragments dans un buffer de taille total et traitez le message lorsque index + len == total.
Pas de reconnexion intégrée
La bibliothèque ne se reconnecte pas toute seule. Le schéma à base de timers ci-dessus est l'approche standard. Arrêtez toujours le timer MQTT quand le Wi-Fi tombe, sinon vous multipliez des tentatives de connexion vouées à l'échec.
Sessions persistantes pour les commandes manquées
Par défaut, le client se connecte avec une session propre : les commandes envoyées pendant que l'appareil était hors ligne sont perdues. Appelez mqttClient.setCleanSession(false) avant de vous connecter et abonnez-vous en QoS 1 : le broker conserve alors la session et met en file les messages QoS 1 destinés à l'appareil jusqu'à son retour. La session ne dure pas éternellement ; les brokers l'expirent au bout d'un certain temps (sur le serveur MQTT public gratuit, les sessions expirent après 30 minutes au maximum). Vérifiez sessionPresent dans onMqttConnect pour savoir si le broker s'est souvenu de vous, et réabonnez-vous dans le cas contraire.
Les limites du broker s'appliquent toujours
Asynchrone ne veut pas dire illimité. Le serveur MQTT public gratuit autorise 10 messages par seconde par client, 10 abonnements par client et 64 Ko par message. Une boucle serrée qui appelle publish() atteindra vite ces limites.
Choisir entre client asynchrone et client bloquant
Utilisez cette liste pour trancher :
- Besoin du TLS sur le port 8883 ? Choisissez PubSubClient avec
WiFiClientSecure, ou le client sécurisé d'espMqttClient. AsyncMqttClient standard n'est pas adapté. - Besoin de publier en QoS 1 ou 2 depuis l'appareil ? AsyncMqttClient ou espMqttClient.
- Un serveur web (ESPAsyncWebServer) tourne sur le même appareil ? AsyncMqttClient partage parfaitement la pile AsyncTCP.
- Vous ciblez aussi l'ESP8266 ? AsyncMqttClient y fonctionne avec ESPAsyncTCP, même si la mémoire est plus limitée. Consultez le tutoriel ESP8266 MQTT.
- Vous voulez le code le plus simple pour un projet débutant ? Le tutoriel PubSubClient est un point de départ plus accessible.
Si vos besoins portent sur les garanties de livraison, lisez les niveaux de QoS MQTT expliqués avant de changer de bibliothèque uniquement pour le QoS 2 ; de nombreuses charges de travail de capteurs se contentent très bien du QoS 0 ou 1. Le tutoriel IoT ESP32, plus général, montre comment MQTT s'intègre dans un projet de capteurs complet.
Configuration de production avec ConnectMQ
Pour les déploiements hors de votre labo, connectez l'appareil à un broker MQTT dans le cloud dédié plutôt qu'au serveur MQTT public gratuit. Sur ConnectMQ, vous créez un broker dans le portail et recevez un nom d'utilisateur, un mot de passe et un client ID. Avec AsyncMqttClient, vous les définissez ainsi, en gardant à l'esprit que les client ID sont liés aux identifiants émis :
mqttClient.setServer("broker.connectmq.com", 1883);
mqttClient.setClientId("YOUR_CLIENT_ID");
mqttClient.setCredentials("YOUR_USERNAME", "YOUR_PASSWORD");
Comme les identifiants transitent en clair sur le port 1883, privilégiez une bibliothèque prenant en charge le TLS pour les appareils situés hors d'un réseau de confiance.
Démarrer avec ConnectMQ
ConnectMQ est un broker MQTT managé compatible MQTT 3.1.1 et 5.0, avec TLS, WebSockets, un explorateur de topics en direct et des alertes de seuil dans le portail, pour voir votre ESP32 publier en temps réel. Créez un broker Serverless gratuit sur app.connectmq.com sans carte bancaire et connectez votre premier appareil asynchrone en quelques minutes.
Questions fréquentes
AsyncMqttClient fonctionne-t-il avec le TLS sur ESP32 ?
Pas avec la bibliothèque AsyncTCP standard, qui n'existe pas en version SSL pour ESP32. Utilisez PubSubClient avec WiFiClientSecure ou une bibliothèque asynchrone maintenue comme espMqttClient lorsque vous avez besoin du port 8883.
Pourquoi ai-je besoin de timers FreeRTOS pour la reconnexion ?
AsyncMqttClient n'a pas de reconnexion automatique. Les timers à déclenchement unique vous permettent de réessayer après un court délai sans bloquer loop() ni la tâche réseau.
Puis-je appeler delay() dans onMqttMessage ?
Non. Les callbacks s'exécutent dans la tâche AsyncTCP, et y bloquer paralyse tout le trafic réseau. Stockez le message et traitez-le dans loop().
AsyncMqttClient est-il toujours maintenu ?
La bibliothèque d'origine n'a pas connu de développement actif depuis un certain temps, même si elle reste très utilisée. Vérifiez l'état du dépôt avant de lancer un nouveau produit à longue durée de vie et envisagez espMqttClient comme alternative maintenue.



