La logique de reconnexion MQTT ESP32 est ce qui sépare une démo qui marche sur votre bureau d'un appareil qui tourne pendant des mois. Dans ce guide, vous allez remplacer la boucle reconnect() bloquante présente dans la plupart des exemples par une machine à états non bloquante qui gère séparément le Wi-Fi et MQTT, espace ses tentatives avec un backoff exponentiel et du jitter, se réabonne automatiquement et met les mesures en tampon pendant les coupures. Pour finir, vous la vérifierez de la manière la plus honnête qui soit : en redémarrant votre routeur pendant que l'ESP32 est connecté à broker.connectmq.com.
Pourquoi la boucle de reconnexion classique échoue
Presque tous les exemples PubSubClient contiennent une variante de ce code :
void reconnect() {
while (!client.connected()) {
if (client.connect("esp32")) {
client.subscribe("cmd");
} else {
delay(5000);
}
}
}
Il fonctionne jusqu'à ce que le réseau disparaisse. Plusieurs problèmes apparaissent alors en même temps :
| Problème | Ce qui se passe en pratique |
|---|---|
Boucle while infinie |
Plus rien d'autre ne s'exécute : boutons, capteurs, écrans et commande moteur restent figés jusqu'au retour du broker |
delay(5000) |
Même effet, et le task watchdog peut se déclencher dans certaines configurations |
| Aucune vérification du Wi-Fi | Le sketch enchaîne les connexions MQTT alors que le Wi-Fi lui-même est coupé, chacune attendant un timeout DNS ou TCP |
| Intervalle de tentative fixe | Des centaines d'appareils qui se remettent de la même panne se reconnectent au même rythme et frappent le broker au même instant |
| Client ID codé en dur | Deux cartes avec le même firmware s'éjectent mutuellement à l'infini |
La solution consiste à traiter la connectivité comme une tâche de fond. loop() doit s'exécuter toutes les quelques millisecondes quoi qu'il arrive, et les tentatives de connexion ne doivent avoir lieu que lorsqu'elles sont dues.
Comprendre les codes d'état de PubSubClient
Avant d'améliorer quoi que ce soit, journalisez client.state() à chaque échec. Les codes indiquent si le problème vient du réseau, du broker ou de vos identifiants.
| Code | Constante | Signification | Cause typique |
|---|---|---|---|
| -4 | MQTT_CONNECTION_TIMEOUT |
Pas de réponse dans le délai de keep-alive | Réseau bloqué, Wi-Fi faible |
| -3 | MQTT_CONNECTION_LOST |
Connexion perdue | Redémarrage du routeur ou du broker, client ID en double |
| -2 | MQTT_CONNECT_FAILED |
Échec de la connexion TCP | Mauvais hôte ou port, échec DNS, pas d'Internet |
| -1 | MQTT_DISCONNECTED |
Déconnexion propre | Vous avez appelé disconnect() |
| 1 | MQTT_CONNECT_BAD_PROTOCOL |
Protocole non pris en charge | Rare avec les brokers 3.1.1 |
| 2 | MQTT_CONNECT_BAD_CLIENT_ID |
Client ID refusé | ID vide ou invalide |
| 3 | MQTT_CONNECT_UNAVAILABLE |
Serveur indisponible | Broker surchargé ou en cours de redémarrage |
| 4 | MQTT_CONNECT_BAD_CREDENTIALS |
Nom d'utilisateur ou mot de passe incorrect | Faute de frappe, mot de passe renouvelé |
| 5 | MQTT_CONNECT_UNAUTHORIZED |
Non autorisé | Identifiants non autorisés pour ce client ID |
Les codes 4 et 5 ne sont pas transitoires. Réessayer toutes les quelques secondes ne les corrigera pas et ne fera qu'accumuler des connexions refusées. De nombreux brokers, dont ConnectMQ, bloquent les adresses IP après des échecs d'authentification répétés : un appareil qui boucle sur de mauvais identifiants peut donc se bloquer lui-même. Le firmware ci-dessous espace beaucoup plus fortement les tentatives sur ces codes. Si vous obtenez -2 dès la toute première tentative, le problème n'est pas du tout la reconnexion ; parcourez d'abord pourquoi un ESP32 ne parvient pas à se connecter au broker MQTT.
Conception d'une reconnexion MQTT ESP32 non bloquante
Le firmware sépare trois couches :
- Wi-Fi. Laissez la reconnexion automatique de l'ESP32 gérer la radio et écoutez les événements Wi-Fi pour savoir quand le lien est actif. N'appelez pas
WiFi.begin()dans une boucle serrée. - MQTT. Ne tentez une connexion que lorsque le Wi-Fi dispose d'une adresse IP et que le délai de backoff est écoulé. En cas de succès, réinitialisez le backoff, publiez un statut
onlineet réabonnez-vous. En cas d'échec, doublez le délai jusqu'à un plafond et ajoutez un jitter aléatoire. - Application. Lisez les capteurs et pilotez les actionneurs à chaque boucle, quelle que soit la connectivité. Publiez lorsque vous êtes connecté ; sinon, placez les mesures dans un petit tampon circulaire et videz-le après la reconnexion.
Le backoff exponentiel avec jitter est le schéma standard pour réessayer face à un service partagé. Le jitter compte plus qu'on ne le pense : après une panne régionale, il étale des milliers de reconnexions sur une fenêtre de temps au lieu d'un pic unique.
Le sketch complet
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
String clientId; // construit à partir de l'adresse MAC
String base; // connectmq/demo/<clientId>
WiFiClient net;
PubSubClient mqtt(net);
// ---- État du backoff ----
const uint32_t BACKOFF_MIN = 1000; // 1 s
const uint32_t BACKOFF_MAX = 60000; // 60 s
const uint32_t BACKOFF_AUTH = 300000; // 5 min pour les erreurs d'identifiants
uint32_t backoff = BACKOFF_MIN;
unsigned long nextAttempt = 0;
volatile bool wifiUp = false;
uint32_t reconnects = 0;
// ---- Tampon circulaire hors ligne ----
const int BUF_SIZE = 20;
String pending[BUF_SIZE];
int head = 0, count = 0;
void onWiFiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
wifiUp = true;
Serial.printf("Wi-Fi up, IP %s\n", WiFi.localIP().toString().c_str());
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
if (wifiUp) Serial.println("Wi-Fi lost");
wifiUp = false;
break;
default:
break;
}
}
void onMessage(char* topic, byte* payload, unsigned int length) {
Serial.printf("cmd on %s: %.*s\n", topic, (int)length, (const char*)payload);
}
void subscribeAll() {
// Appelée après CHAQUE connexion réussie : les abonnements ne survivent pas
// à une reconnexion en clean session.
mqtt.subscribe((base + "/cmd/#").c_str(), 1);
}
void flushPending() {
while (count > 0 && mqtt.connected()) {
int tail = (head - count + BUF_SIZE) % BUF_SIZE;
if (!mqtt.publish((base + "/telemetry").c_str(), pending[tail].c_str())) break;
count--;
mqtt.loop();
}
}
void scheduleRetry(int state) {
bool authError = (state == MQTT_CONNECT_BAD_CREDENTIALS || state == MQTT_CONNECT_UNAUTHORIZED);
uint32_t wait = authError ? BACKOFF_AUTH : backoff;
uint32_t jitter = random(0, wait / 4 + 1);
nextAttempt = millis() + wait + jitter;
if (!authError) backoff = min(backoff * 2, BACKOFF_MAX);
Serial.printf("MQTT connect failed (state %d), retry in %lu ms\n", state, (unsigned long)(wait + jitter));
}
void maintainMqtt() {
if (mqtt.connected()) {
mqtt.loop();
return;
}
if (!wifiUp || (long)(millis() - nextAttempt) < 0) return;
String status = base + "/status";
bool ok = mqtt.connect(clientId.c_str(), nullptr, nullptr,
status.c_str(), 1, true, "offline");
if (!ok) {
scheduleRetry(mqtt.state());
return;
}
reconnects++;
backoff = BACKOFF_MIN;
Serial.printf("MQTT connected (#%lu)\n", (unsigned long)reconnects);
mqtt.publish(status.c_str(), "online", true);
subscribeAll();
flushPending();
}
void publishReading(const String& json) {
if (mqtt.connected() && mqtt.publish((base + "/telemetry").c_str(), json.c_str())) return;
pending[head] = json; // mise en tampon hors ligne
head = (head + 1) % BUF_SIZE;
if (count < BUF_SIZE) count++; // l'entrée la plus ancienne est écrasée quand le tampon est plein
}
void setup() {
Serial.begin(115200);
uint8_t mac[6];
WiFi.mode(WIFI_STA);
WiFi.macAddress(mac);
char id[24];
snprintf(id, sizeof(id), "esp32-%02x%02x%02x", mac[3], mac[4], mac[5]);
clientId = id;
base = "connectmq/demo/" + clientId;
randomSeed(esp_random());
WiFi.onEvent(onWiFiEvent);
WiFi.setAutoReconnect(true);
WiFi.persistent(false);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
mqtt.setSocketTimeout(5); // limite la durée de blocage d'une connexion qui stagne
}
unsigned long lastSample = 0;
void loop() {
maintainMqtt();
// Le travail applicatif s'exécute toujours, connecté ou non
if (millis() - lastSample >= 10000) {
lastSample = millis();
String json = "{\"uptime\":" + String(millis() / 1000) +
",\"rssi\":" + String(WiFi.RSSI()) +
",\"reconnects\":" + String(reconnects) + "}";
publishReading(json);
}
}
Remarques sur les lignes importantes
- Client ID dérivé de l'adresse MAC. Chaque carte obtient un ID stable et unique sans modifier le sketch. Les ID en double sont la première cause des mystérieuses boucles
-3, car le broker déconnecte l'ancienne session chaque fois que la nouvelle se connecte. Avec des identifiants du portail, utilisez plutôt le client ID fourni par le portail, puisque ConnectMQ associe les client ID aux identifiants. subscribeAll()après chaque connexion. PubSubClient se connecte en clean session : le broker oublie donc les abonnements quand la connexion tombe. Oublier de se réabonner, c'est le bug classique du « ça se reconnecte, mais ne reçoit plus les commandes ».- Statut
online/offlineen retained message. Le Last Will annonce à tous que l'appareil a décroché ; leonlineretenu corrige l'information après le rétablissement. Le guide ESP32 MQTT Last Will détaille ce schéma. setSocketTimeout(5). Même une conception non bloquante bloque pendant l'appel de connexion proprement dit, car la résolution DNS, la poignée de main TCP et l'attente du CONNACK sont synchrones dans PubSubClient. Un timeout court limite cette pause.- Minuterie résistante au débordement.
(long)(millis() - nextAttempt) < 0continue de fonctionner lorsquemillis()repasse à zéro après environ 49 jours.
Testez-la : redémarrez le routeur
Une stratégie de reconnexion que vous n'avez pas testée est un espoir, pas une stratégie. Voici un plan de test qui prend dix minutes :
- Flashez le sketch et ouvrez le moniteur série.
- Sur votre ordinateur, abonnez-vous aux topics de l'appareil (remplacez l'ID par celui affiché sur le moniteur série) :
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-test01 -v -t "connectmq/demo/esp32-a1b2c3/#"
- Vérifiez que vous voyez
status onlineet un message de télémétrie toutes les dix secondes. - Coupez puis rallumez votre routeur. Votre ordinateur perd lui aussi sa connexion : surveillez donc plutôt le moniteur série.
- Dans un délai d'environ 1,5 fois le keep-alive (45 secondes ici), le broker publie le Last Will. Vous le verrez dès que votre portable se sera reconnecté.
- Observez le journal série :
Wi-Fi lost, puis plus rien pendant que le routeur démarre (aucune tentative MQTT échouée, puisque le Wi-Fi est coupé), puisWi-Fi up, puisMQTT connected (#2). - Côté abonné, vérifiez que
status onlinerevient, que la télémétrie mise en tampon arrive d'un coup et quereconnectsa augmenté de un. - Envoyez une commande sur
connectmq/demo/esp32-a1b2c3/cmd/testet confirmez que l'appareil la reçoit toujours. Cela prouve que le réabonnement fonctionne.
Refaites le test avec le broker injoignable mais le Wi-Fi actif, par exemple en remplaçant temporairement MQTT_HOST par un nom inutilisé. Vous devriez voir les intervalles entre tentatives passer d'environ une seconde au plafond de 60 secondes, avec des valeurs légèrement différentes à chaque fois grâce au jitter.
Dépanner les problèmes de reconnexion
- Reconnexion toutes les quelques secondes avec l'état -3. Un autre appareil ou un client de test oublié utilise le même client ID.
- Connecté, mais les commandes n'arrivent plus après une reconnexion. Le réabonnement manque.
- État -4 avec un signal faible. Augmentez modérément le keep-alive, améliorez le placement de l'antenne et journalisez le RSSI comme montré.
- L'appareil ne se rétablit jamais après une longue coupure sans redémarrage. Quelque chose bloque indéfiniment, souvent un appel HTTP ou une bibliothèque de capteur. Ajoutez le task watchdog, comme décrit dans task watchdog ESP32 avec MQTT.
- Une rafale d'anciens messages surprend le tableau de bord. Ajoutez un horodatage aux mesures mises en tampon pour que les consommateurs distinguent les données en retard des données en direct.
Quand le côté broker compte
Un bon firmware part du principe que le broker peut être injoignable, mais la fréquence de ces situations dépend de l'endroit où tourne le broker. Une instance Mosquitto sur un serveur domestique tombe avec la coupure de courant, la mise à jour de l'OS et le certificat expiré. Un broker MQTT managé confie ces sujets à une équipe qui exploite le service pour vous. ConnectMQ expose également $SYS/broker/connection/{clientId}/state, ce qui permet à un backend de surveiller l'état de connexion d'un appareil, et l'explorateur de topics en direct du portail vous laisse voir les topics de statut retenus basculer entre online et offline pendant vos tests. Pour comprendre le keep-alive, les sessions et le Last Will, la présentation de ce que fait un broker MQTT est un complément utile. Le serveur MQTT public gratuit convient parfaitement à cet exercice ; le même firmware passe à des identifiants privés et au TLS en changeant trois constantes et la classe du client, comme le montre ESP32 MQTT sur SSL/TLS. Pour le parcours ESP32 dans son ensemble, consultez le tutoriel IoT ESP32.
Démarrez avec ConnectMQ
Créez gratuitement un broker Serverless sur app.connectmq.com, sans carte bancaire. Vous obtenez des identifiants dédiés, le TLS sur le port 8883 et un portail qui indique quand chaque appareil se connecte et se déconnecte, exactement ce qu'il vous faut pour régler le comportement de reconnexion.
Questions fréquentes
À quelle fréquence un ESP32 doit-il retenter une connexion MQTT ?
Commencez autour d'une seconde et doublez l'attente après chaque échec jusqu'à un plafond d'environ une minute, en ajoutant un jitter aléatoire. Pour les erreurs d'identifiants, attendez beaucoup plus longtemps ou arrêtez-vous et signalez le problème, car les nouvelles tentatives ne peuvent pas réussir.
Faut-il se réabonner après une reconnexion MQTT ESP32 ?
Avec PubSubClient, oui. Il utilise une clean session : le broker supprime donc les abonnements à la fin de la connexion. Appelez votre fonction d'abonnement après chaque connexion réussie.
Faut-il rappeler WiFi.begin() quand le Wi-Fi tombe ?
En général, non. Avec WiFi.setAutoReconnect(true), l'ESP32 se reconnecte tout seul. Appeler WiFi.begin() à répétition peut interrompre une association déjà en cours.
Pourquoi mon ESP32 se déconnecte-t-il sans cesse avec l'état -3 ?
La cause la plus fréquente est deux clients partageant un même client ID ; le broker déconnecte l'ancien chaque fois que l'autre se connecte. Un Wi-Fi faible et un keep-alive plus long que le délai d'inactivité d'un NAT ou d'un pare-feu sont les suspects suivants.



