Um reset do task watchdog do ESP32 em um projeto MQTT quase sempre significa que algum trecho de código está bloqueando uma task por mais tempo do que o watchdog permite. Este artigo mostra como ler a mensagem task_wdt, identifica os culpados de sempre em sketches MQTT e traz padrões não bloqueantes que mantêm o dispositivo conectado sem resets.
O que o task watchdog realmente faz
O ESP32 roda FreeRTOS. Mesmo em um sketch Arduino, setup() e loop() rodam dentro de uma task FreeRTOS chamada loopTask, o Wi-Fi e a pilha TCP/IP rodam em tasks próprias, e cada núcleo da CPU tem uma idle task que roda quando nada mais precisa da CPU.
O Task Watchdog Timer (TWDT) monitora um conjunto de tasks inscritas. Cada task inscrita precisa "alimentar" o watchdog periodicamente. Se uma delas deixar de fazer isso dentro do timeout, o TWDT imprime um erro e, dependendo da configuração, entra em pânico e reinicia o chip.
Quais tasks são vigiadas depende do seu ambiente:
| Ambiente | Vigiadas por padrão | Timeout típico |
|---|---|---|
| ESP-IDF | Idle tasks dos dois núcleos (configurável no menuconfig) | 5 s |
| Arduino-ESP32 | Idle task do núcleo 0; loopTask só se você chamar enableLoopWDT() |
5 s |
| AsyncTCP (usado pelo AsyncMqttClient) | Sua task async_tcp, quando compilado com a opção de watchdog ativada |
Herda o timeout do TWDT |
A idle task é vigiada por um motivo indireto: se ela nunca consegue rodar, alguma outra task naquele núcleo está monopolizando a CPU sem ceder. Então uma mensagem com "IDLE0" não significa que a idle task está quebrada, e sim que outra coisa no núcleo 0 está girando sem parar.
Existe ainda um Interrupt Watchdog separado, que dispara quando as interrupções ficam desativadas por tempo demais ou uma ISR demora demais. A mensagem dele menciona "Interrupt wdt timeout" e as causas são outras, quase sempre em código de ISR. Este artigo trata do task watchdog.
Lendo a mensagem de erro
Um crash típico se parece com isto:
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.
Leia em duas partes. A primeira lista diz quem ficou sem CPU: aqui, a idle task do núcleo 0. A lista "currently running" diz quem estava segurando a CPU naquele momento: uma task chamada mqttTask no núcleo 0. Essa é a sua suspeita. Se a task que ficou sem CPU for async_tcp, o problema é código rodando dentro de um callback do AsyncTCP ou do AsyncMqttClient. Se for loopTask, o seu loop() bloqueou com o loop watchdog ativado.
Ative o exception decoder no monitor serial (no PlatformIO, monitor_filters = esp32_exception_decoder) para transformar os endereços do backtrace em nomes de arquivo e números de linha.
Os culpados de sempre nos crashes de task watchdog do ESP32 com MQTT
Loops de espera ativa sem ceder a CPU
O infrator clássico é um loop de espera vazio:
while (WiFi.status() != WL_CONNECTED) { } // never yields
while (!mqtt.connect(clientId)) { } // retries at full speed
No loop(), no núcleo 1, isso muitas vezes escapa sem reset, porque o core Arduino não vigia a idle task 1 por padrão. Mova o mesmo código para uma task fixada no núcleo 0, ou ative o loop watchdog, e ele reinicia após cinco segundos. Um delay(10) ou vTaskDelay() dentro do loop resolve a falta de CPU, mas não o design: o dispositivo continua sem conseguir fazer mais nada enquanto espera.
Chamadas de conexão bloqueantes mais longas que o timeout
O connect() do PubSubClient é bloqueante. Se o broker estiver inacessível, a conexão TCP subjacente e a espera pelo CONNACK do MQTT podem, juntas, levar mais de cinco segundos, principalmente com TLS, em que só o handshake pode levar segundos com sinal fraco. Uma função de reconexão que chama connect() várias vezes seguidas multiplica isso. Reduza o timeout do socket com mqtt.setSocketTimeout(4) e tente apenas uma vez por chamada.
Trabalho pesado dentro dos callbacks MQTT
Com o AsyncMqttClient, o onMessage roda dentro da task async_tcp. Analisar um documento JSON grande, gravar na flash, atualizar um display ou chamar delay() ali bloqueia a pilha de rede e pode disparar o watchdog dessa task. Com o PubSubClient, o callback roda dentro de mqtt.loop(), então trabalho pesado ali bloqueia o loop. Nos dois casos, copie o payload, marque uma flag ou coloque em uma fila e faça o trabalho em outro lugar.
Leituras de sensor e gravações na flash longas e bloqueantes
Algumas bibliotecas de sensores fazem polling com espera ativa, e um download OTA ou uma gravação grande na flash pode durar segundos. Isso não é problema em uma task própria que alimenta o watchdog, mas sim na mesma task que atende o MQTT.
Tempestades de reconexão após perda de rede
Quando o Wi-Fi cai, um sketch ingênuo chama WiFi.begin() e mqtt.connect() em sequência em um loop apertado. Cada chamada bloqueia um pouco, o loop nunca pausa e, mais cedo ou mais tarde, o watchdog dispara. O backoff exponencial resolve tanto o problema do watchdog quanto a carga sobre o broker.
Padrão MQTT não bloqueante para o loop()
Este sketch mantém o loop() curto o tempo todo. Cada tentativa de conexão acontece no máximo uma vez por intervalo de backoff, o callback só copia os dados e o trabalho roda depois, no loop(). Ele se conecta à área pública do 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; // no heavy work here
}
void handleCommand(const char* cmd) {
Serial.printf("Handling command: %s\n", cmd);
// slow work is fine here, as long as it stays under the watchdog timeout
}
void maintainMqtt() {
if (WiFi.status() != WL_CONNECTED) return; // Wi-Fi reconnects on its own
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(); // watch loop() too, so blocking shows up in testing
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); // keep each blocking connect well under 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);
}
}
O core Arduino alimenta o loop watchdog automaticamente toda vez que o loop() retorna, então, enquanto cada passagem pelo loop() ficar bem abaixo do timeout, ativá-lo não custa nada e pega regressões cedo. Ligá-lo durante o desenvolvimento é um jeito barato de encontrar a chamada bloqueante antes que ela chegue ao campo.
Rodando o MQTT em uma task FreeRTOS própria
Se você prefere uma task dedicada ao MQTT, inscreva-a no watchdog e alimente-o a cada iteração. A API esp_task_wdt no ESP-IDF 5 (usado pelo Arduino-ESP32 3.x) fica assim:
#include <esp_task_wdt.h>
void mqttTask(void* arg) {
esp_task_wdt_add(NULL); // watch this task
for (;;) {
maintainMqtt(); // must return quickly
esp_task_wdt_reset(); // feed the watchdog
vTaskDelay(pdMS_TO_TICKS(10)); // always yield
}
}
void setup() {
// ... Wi-Fi and mqtt setup as above ...
xTaskCreatePinnedToCore(mqttTask, "mqttTask", 6144, NULL, 1, NULL, 1);
}
Mantenha a prioridade baixa (1 é a mesma do loopTask), a menos que tenha um motivo para aumentá-la; uma task de alta prioridade que nunca bloqueia deixa sem CPU tudo o que está abaixo dela. Fixe-a no núcleo 1 se quiser ficar longe do núcleo onde o Wi-Fi roda. Se o cliente MQTT for usado por mais de uma task, proteja-o com um mutex, porque o PubSubClient não é thread-safe. O guia de ESP32 com FreeRTOS e MQTT aborda filas e o design de tasks com mais detalhes.
Devo aumentar o timeout do watchdog?
Às vezes, mas trate isso como último recurso. Aumentar o timeout esconde o código bloqueante em vez de corrigi-lo, e o dispositivo passa a demorar mais para se recuperar de um travamento real. Casos legítimos são operações longas e inevitáveis, como uma gravação OTA na flash. No ESP-IDF 5 você pode reconfigurar o TWDT em tempo de execução:
esp_task_wdt_config_t cfg = {
.timeout_ms = 15000,
.idle_core_mask = (1 << 0), // keep watching IDLE0
.trigger_panic = true,
};
esp_task_wdt_reconfigure(&cfg);
Uma abordagem mais limpa para uma única operação longa é alimentar o watchdog de dentro dela, por exemplo em um callback de progresso do OTA, e deixar o timeout global em paz. Nunca desative o watchdog por completo em produção. Ele é a única coisa que reinicia um dispositivo travado em deadlock no campo.
Checklist de solução de problemas
- Leia qual task ficou sem CPU e qual estava rodando; esse par diz onde procurar.
- Decodifique o backtrace com o exception decoder para obter a linha exata.
- Procure no código loops
whilesemdelay(),yield()ouvTaskDelay(). - Defina
mqtt.setSocketTimeout()em alguns segundos e remova loops de novas tentativas em volta deconnect(). - Tire a análise de JSON, as gravações na flash e as atualizações de display de dentro dos callbacks MQTT.
- Adicione backoff exponencial às reconexões de Wi-Fi e MQTT.
- Ative
enableLoopWDT()durante os testes para que bloqueios noloop()apareçam cedo. - Acompanhe o uptime do dispositivo via MQTT: um contador que vive zerando indica reboot por watchdog ou brownout.
Publicar uptime e esp_reset_reason() após cada boot é um hábito simples que transforma resets invisíveis em dados. Muitos problemas de conexão que parecem ser de watchdog são, no fundo, problemas de rede; se o dispositivo nunca chega a conectar, resolva primeiro por que o ESP32 não conecta ao broker MQTT e, para a estratégia de reconexão, veja o guia de reconexão MQTT no ESP32.
Monitore seus dispositivos no ConnectMQ
O sketch de teste acima roda contra o servidor MQTT público (gratuito) do ConnectMQ, perfeito para reproduzir um reset na sua bancada. Para uma frota, um broker MQTT gerenciado privado do ConnectMQ permite acompanhar a telemetria de cada dispositivo no explorador de tópicos ao vivo e definir regras de alerta por limite em valores como heap livre, para que um dispositivo que começa a reiniciar se destaque rapidamente. Você pode criar um no plano Serverless gratuito em app.connectmq.com, sem cartão de crédito. Se ainda está decidindo onde hospedar o broker, o guia de broker MQTT na nuvem compara as opções, e o tutorial de IoT com ESP32 mostra um projeto de dispositivo não bloqueante completo, de ponta a ponta.
Perguntas frequentes
O que significa "Task watchdog got triggered" com IDLE0?
Uma task rodando no núcleo 0 manteve a CPU ocupada por mais tempo que o timeout sem ceder, então a idle task nunca rodou. A linha "Tasks currently running" aponta o provável culpado.
O delay() reinicia o watchdog?
O delay() cede a CPU a outras tasks, o que deixa a idle task rodar, então evita a falta de CPU da IDLE. Ele não alimenta uma task que você inscreveu com esp_task_wdt_add(); essa task precisa chamar esp_task_wdt_reset() por conta própria.
Por que o AsyncMqttClient dispara o watchdog do async_tcp?
Os callbacks dele rodam dentro da task async_tcp. Trabalho demorado ali bloqueia a task, então mantenha os callbacks curtos e passe o payload para o loop() ou para outra task por meio de uma flag ou fila.
É seguro desativar o task watchdog?
Não em produção. Ele é o mecanismo que recupera um dispositivo de um travamento. Corrija o código bloqueante e só aumente o timeout para operações genuinamente longas.



