ESP32 et Arduino

Corriger les resets du task watchdog ESP32 dans les projets MQTT

Pourquoi les sketches ESP32 MQTT déclenchent des resets du task watchdog, comment lire le message task_wdt et comment restructurer boucles de reconnexion, callbacks et code bloquant pour que le watchdog reste silencieux.

Moniteur série ESP32 affichant une erreur task_wdt dans un projet task watchdog ESP32 MQTT
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

Un reset du task watchdog ESP32 dans un projet MQTT signifie presque toujours qu'un morceau de code bloque une tâche plus longtemps que le watchdog ne le permet. Cet article montre comment lire le message task_wdt, identifie les coupables habituels dans les sketches MQTT et vous donne des schémas non bloquants qui maintiennent l'appareil connecté sans resets.

Ce que fait réellement le task watchdog

L'ESP32 exécute FreeRTOS. Même dans un sketch Arduino, setup() et loop() tournent dans une tâche FreeRTOS appelée loopTask, le Wi-Fi et la pile TCP/IP ont leurs propres tâches, et chaque cœur dispose d'une tâche idle qui s'exécute quand rien d'autre n'a besoin du CPU.

Le Task Watchdog Timer (TWDT) surveille un ensemble de tâches inscrites. Chaque tâche inscrite doit « nourrir » le watchdog périodiquement. Si l'une d'elles n'y parvient pas dans le délai imparti, le TWDT affiche une erreur et, selon la configuration, déclenche un panic et redémarre la puce.

Les tâches surveillées dépendent de votre environnement :

Environnement Surveillé par défaut Timeout typique
ESP-IDF Tâches idle des deux cœurs (configurable dans menuconfig) 5 s
Arduino-ESP32 Tâche idle du cœur 0 ; loopTask uniquement si vous appelez enableLoopWDT() 5 s
AsyncTCP (utilisé par AsyncMqttClient) Sa tâche async_tcp, lorsqu'il est compilé avec son option watchdog activée Hérite du timeout du TWDT

La tâche idle est surveillée pour une raison indirecte : si elle n'arrive jamais à s'exécuter, c'est qu'une autre tâche sur ce cœur monopolise le CPU sans jamais céder la main. Un message « IDLE0 » ne signifie donc pas que la tâche idle est cassée, mais qu'autre chose tourne en boucle sur le cœur 0.

Il existe aussi un Interrupt Watchdog distinct, qui se déclenche lorsque les interruptions restent désactivées trop longtemps ou qu'une ISR dure trop longtemps. Son message mentionne « Interrupt wdt timeout » et ses causes sont différentes, le plus souvent dans le code des ISR. Cet article traite du task watchdog.

Lire le message d'erreur

Un crash typique ressemble à ceci :

E (25391) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (25391) task_wdt:  - IDLE0 (CPU 0)
E (25391) task_wdt: Tasks currently running:
E (25391) task_wdt: CPU 0: mqttTask
E (25391) task_wdt: CPU 1: loopTask
E (25391) task_wdt: Aborting.

Lisez-le en deux parties. La première liste indique qui a été affamé : ici, la tâche idle du cœur 0. La liste « currently running » indique qui occupait le CPU à ce moment-là : une tâche appelée mqttTask sur le cœur 0. C'est votre suspect. Si la tâche affamée est async_tcp, le problème vient du code exécuté dans un callback AsyncTCP ou AsyncMqttClient. S'il s'agit de loopTask, votre loop() a bloqué alors que le watchdog de la boucle était activé.

Activez le décodeur d'exceptions dans votre moniteur série (dans PlatformIO, monitor_filters = esp32_exception_decoder) pour transformer les adresses du backtrace en noms de fichiers et numéros de ligne.

Les coupables habituels des crashs task watchdog ESP32 MQTT

Boucles d'attente active sans yield

Le coupable classique est une boucle d'attente vide :

while (WiFi.status() != WL_CONNECTED) { }   // ne cède jamais la main
while (!mqtt.connect(clientId)) { }          // réessaie à pleine vitesse

Dans loop() sur le cœur 1, cela passe souvent sans reset, car le core Arduino ne surveille pas la tâche idle 1 par défaut. Déplacez le même code dans une tâche épinglée au cœur 0, ou activez le watchdog de la boucle, et la carte redémarre au bout de cinq secondes. Un delay(10) ou un vTaskDelay() dans la boucle corrige la famine mais pas la conception : l'appareil ne peut toujours rien faire d'autre pendant qu'il attend.

Appels de connexion bloquants plus longs que le timeout

Le connect() de PubSubClient est bloquant. Si le broker est injoignable, la connexion TCP sous-jacente et l'attente du CONNACK MQTT peuvent ensemble dépasser cinq secondes, surtout avec le TLS, où le handshake seul peut prendre plusieurs secondes avec un signal faible. Une fonction de reconnexion qui appelle connect() plusieurs fois d'affilée multiplie ce délai. Réduisez le timeout de socket avec mqtt.setSocketTimeout(4) et ne faites qu'une tentative par appel.

Travail lourd dans les callbacks MQTT

Avec AsyncMqttClient, onMessage s'exécute dans la tâche async_tcp. Y analyser un gros document JSON, écrire en flash, piloter un écran ou appeler delay() bloque la pile réseau et peut déclencher le watchdog de cette tâche. Avec PubSubClient, le callback s'exécute dans mqtt.loop() : un travail lourd y bloque donc plutôt la boucle. Dans les deux cas, copiez le payload, positionnez un flag ou poussez dans une file, et faites le travail ailleurs.

Lectures de capteur et écritures flash longues et bloquantes

Certaines bibliothèques de capteurs interrogent en attente active, et un téléchargement OTA ou une grosse écriture flash peut durer plusieurs secondes. Ce n'est pas un problème dans leur propre tâche qui nourrit le watchdog, mais ça l'est dans la tâche qui gère MQTT.

Tempêtes de reconnexion après une perte réseau

Quand le Wi-Fi tombe, un sketch naïf appelle WiFi.begin() et mqtt.connect() l'un après l'autre dans une boucle serrée. Chaque appel bloque un peu, la boucle ne marque jamais de pause, et le watchdog finit par se déclencher. Le backoff exponentiel résout à la fois le problème du watchdog et la charge sur le broker.

Schéma MQTT non bloquant pour loop()

Ce sketch garde loop() courte en permanence. Chaque tentative de connexion a lieu au plus une fois par intervalle de backoff, le callback se contente de copier les données, et le traitement s'exécute ensuite dans loop(). Il se connecte à la zone publique de broker.connectmq.com.

#include <WiFi.h>
#include <PubSubClient.h>

const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* CMD_TOPIC = "connectmq/demo/b52e07/cmd";
const char* TELEMETRY_TOPIC = "connectmq/demo/b52e07/telemetry";

WiFiClient net;
PubSubClient mqtt(net);
String clientId;

volatile bool commandPending = false;
char commandBuf[128];

unsigned long nextMqttAttempt = 0;
unsigned long mqttBackoff = 1000;
unsigned long lastPublish = 0;

void onMessage(char* topic, byte* payload, unsigned int length) {
  size_t n = min((size_t)length, sizeof(commandBuf) - 1);
  memcpy(commandBuf, payload, n);
  commandBuf[n] = '\0';
  commandPending = true;          // aucun travail lourd ici
}

void handleCommand(const char* cmd) {
  Serial.printf("Handling command: %s\n", cmd);
  // un travail lent est acceptable ici, tant qu'il reste sous le timeout du watchdog
}

void maintainMqtt() {
  if (WiFi.status() != WL_CONNECTED) return;   // le Wi-Fi se reconnecte tout seul
  if (mqtt.connected()) { mqtt.loop(); return; }
  if (millis() < nextMqttAttempt) return;

  if (mqtt.connect(clientId.c_str())) {
    mqtt.subscribe(CMD_TOPIC);
    mqttBackoff = 1000;
  } else {
    nextMqttAttempt = millis() + mqttBackoff;
    mqttBackoff = min(mqttBackoff * 2, 30000UL);
  }
}

void setup() {
  Serial.begin(115200);
  enableLoopWDT();                // surveiller aussi loop(), pour voir les blocages pendant les tests
  WiFi.mode(WIFI_STA);
  WiFi.setAutoReconnect(true);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  clientId = "esp32-wdt-" + String((uint32_t)ESP.getEfuseMac(), HEX);
  mqtt.setServer("broker.connectmq.com", 1883);
  mqtt.setSocketTimeout(4);       // chaque connexion bloquante reste bien sous 5 s
  mqtt.setCallback(onMessage);
}

void loop() {
  maintainMqtt();

  if (commandPending) {
    commandPending = false;
    handleCommand(commandBuf);
  }

  if (mqtt.connected() && millis() - lastPublish > 10000) {
    lastPublish = millis();
    char payload[64];
    snprintf(payload, sizeof(payload), "{\"uptime\":%lu,\"heap\":%u}",
             millis() / 1000, ESP.getFreeHeap());
    mqtt.publish(TELEMETRY_TOPIC, payload);
  }
}

Le core Arduino nourrit automatiquement le watchdog de la boucle à chaque retour de loop() : tant qu'un passage dans loop() reste bien en dessous du timeout, l'activer ne coûte rien et détecte tôt les régressions. L'activer pendant le développement est un moyen peu coûteux de trouver l'appel bloquant avant qu'il n'arrive sur le terrain.

Exécuter MQTT dans sa propre tâche FreeRTOS

Si vous préférez une tâche MQTT dédiée, inscrivez-la au watchdog et réinitialisez-le à chaque itération. L'API esp_task_wdt d'ESP-IDF 5 (utilisé par Arduino-ESP32 3.x) se présente ainsi :

#include <esp_task_wdt.h>

void mqttTask(void* arg) {
  esp_task_wdt_add(NULL);         // surveiller cette tâche
  for (;;) {
    maintainMqtt();               // doit rendre la main rapidement
    esp_task_wdt_reset();         // nourrir le watchdog
    vTaskDelay(pdMS_TO_TICKS(10)); // toujours céder la main
  }
}

void setup() {
  // ... configuration Wi-Fi et mqtt comme ci-dessus ...
  xTaskCreatePinnedToCore(mqttTask, "mqttTask", 6144, NULL, 1, NULL, 1);
}

Gardez une priorité basse (1 est la même que loopTask) sauf raison de l'augmenter ; une tâche de haute priorité qui ne bloque jamais affame toutes celles qui sont en dessous. Épinglez-la au cœur 1 si vous voulez rester à l'écart du cœur où tourne le Wi-Fi. Si le client MQTT est utilisé depuis plusieurs tâches, protégez-le avec un mutex, car PubSubClient n'est pas thread-safe. Le guide ESP32 FreeRTOS MQTT détaille les files d'attente et la conception des tâches.

Faut-il augmenter le timeout du watchdog ?

Parfois, mais en dernier recours. Augmenter le timeout masque le code bloquant au lieu de le corriger, et l'appareil met plus de temps à se rétablir d'un vrai blocage. Les cas légitimes sont les opérations longues et inévitables, comme une écriture flash OTA. Avec ESP-IDF 5, vous pouvez reconfigurer le TWDT à l'exécution :

esp_task_wdt_config_t cfg = {
  .timeout_ms = 15000,
  .idle_core_mask = (1 << 0),     // continuer à surveiller IDLE0
  .trigger_panic = true,
};
esp_task_wdt_reconfigure(&cfg);

Pour une seule opération longue, une approche plus propre consiste à nourrir le watchdog depuis l'intérieur de celle-ci, par exemple depuis un callback de progression OTA, et à laisser le timeout global inchangé. Ne désactivez jamais complètement le watchdog en production. C'est la seule chose qui redémarre un appareil bloqué dans un interblocage sur le terrain.

Liste de contrôle pour le dépannage

  1. Lisez quelle tâche a été affamée et quelle tâche s'exécutait ; cette paire vous indique où chercher.
  2. Décodez le backtrace avec le décodeur d'exceptions pour obtenir la ligne exacte.
  3. Cherchez dans le code les boucles while sans delay(), yield() ni vTaskDelay().
  4. Réglez mqtt.setSocketTimeout() à quelques secondes et supprimez les boucles de tentatives autour de connect().
  5. Sortez l'analyse JSON, les écritures flash et les mises à jour d'écran des callbacks MQTT.
  6. Ajoutez un backoff exponentiel aux reconnexions Wi-Fi et MQTT.
  7. Activez enableLoopWDT() pendant les tests pour que les blocages dans loop() apparaissent tôt.
  8. Surveillez l'uptime de l'appareil via MQTT : un compteur qui revient sans cesse à zéro trahit un redémarrage par watchdog ou par brownout.

Publier uptime et esp_reset_reason() après chaque démarrage est une habitude simple qui transforme des resets invisibles en données. Beaucoup de problèmes de connexion qui ressemblent à des soucis de watchdog sont en réalité des problèmes réseau sous-jacents ; si l'appareil ne se connecte jamais, commencez par pourquoi un ESP32 ne se connecte pas à un broker MQTT, et pour la stratégie de reconnexion, consultez le guide de reconnexion MQTT ESP32.

Surveillez vos appareils sur ConnectMQ

Le sketch de test ci-dessus tourne sur le serveur MQTT public gratuit de ConnectMQ, idéal pour reproduire un reset sur votre bureau, mais réservé aux tests. Pour une flotte, un broker MQTT managé privé sur ConnectMQ vous permet de suivre la télémétrie de chaque appareil dans l'explorateur de topics en direct et de définir des règles d'alerte à seuil sur des valeurs comme le heap libre, si bien qu'un appareil qui commence à redémarrer se repère rapidement. Vous pouvez en créer un avec l'offre gratuite Serverless sur app.connectmq.com, sans carte bancaire. Si vous hésitez encore sur l'hébergement de votre broker, le guide du broker MQTT cloud compare les options, et le tutoriel IoT ESP32 présente de bout en bout un projet d'appareil non bloquant complet.

Questions fréquentes

Que signifie « Task watchdog got triggered » avec IDLE0 ?

Une tâche s'exécutant sur le cœur 0 a occupé le CPU plus longtemps que le timeout sans céder la main, si bien que la tâche idle n'a jamais pu s'exécuter. La ligne « Tasks currently running » désigne le coupable probable.

delay() réinitialise-t-il le watchdog ?

delay() cède la main aux autres tâches, ce qui laisse la tâche idle s'exécuter : il empêche donc la famine d'IDLE. Il ne nourrit pas une tâche que vous avez inscrite avec esp_task_wdt_add() ; cette tâche doit appeler elle-même esp_task_wdt_reset().

Pourquoi AsyncMqttClient déclenche-t-il le watchdog async_tcp ?

Ses callbacks s'exécutent dans la tâche async_tcp. Un traitement long y bloque la tâche : gardez donc des callbacks courts et transmettez le payload à loop() ou à une tâche séparée via un flag ou une file d'attente.

Peut-on désactiver le task watchdog sans risque ?

Pas en production. C'est le mécanisme qui sort un appareil d'un blocage. Corrigez le code bloquant, et n'augmentez le timeout que pour des opérations réellement longues.

Articles liés

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.

9 min de lecture

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.