ESP32异步WebServer触发Task Watchdog重启问题排查与解决求助
解决ESP32 AsyncWebServer中同步HTTPS请求触发Task Watchdog重启的问题
看起来你踩了ESP32 AsyncWebServer的一个经典坑——异步服务器的回调函数是运行在async_tcp任务上下文里的,这个任务要求绝对不能有长时间的阻塞操作!你的getPin()函数里的HTTPS GET请求是同步阻塞的,耗时数秒,直接导致async_tcp任务无法及时喂看门狗,最终触发了重启。
核心问题拆解
从串口输出的错误信息就能明确看到问题根源:
E (137906) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (137906) task_wdt: - async_tcp (CPU 0/1)
async_tcp任务负责处理所有AsyncWebServer的请求,它的看门狗超时时间通常在几秒以内,而你的HTTPS请求刚好超过了这个阈值,所以触发了强制重启。
可行解决方法
方案1:把阻塞的HTTPS请求移到独立FreeRTOS任务中(推荐生产环境使用)
AsyncWebServer的设计初衷就是非阻塞处理HTTP请求,所以我们需要把长耗时的HTTPS请求逻辑放到独立任务里,同时用异步流式响应的方式给客户端返回结果。这样既不会阻塞async_tcp任务,又能给用户友好的加载提示。
修改后的完整代码如下:
#include <heltec.h> #include "WiFi.h" #include "ESPAsyncWebServer.h" #include <WiFiClientSecure.h> #include <HTTPClient.h> const char* ssid = "MyWiFiSSID"; const char* password = "MyWiFiPW"; AsyncWebServer server(80); // 结构体用来传递任务所需的上下文参数 struct RequestContext { AsyncResponseStream *responseStream; String targetIp; }; void setup() { Heltec.begin(true, false, true, true, 470E6); WiFi.softAP(ssid, password); IPAddress IP = WiFi.softAPIP(); Serial.print("AccessPoint IP address: "); Serial.println(IP); server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/html", "<!DOCTYPE html><html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" charset=\"UTF-8\"><link rel=\"icon\" href=\"data:,\"><style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}.button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}</style></head><body><h1>Welcome to the Landing Page of the Web Server</h1><p><a href=\"/get_unlock_pin\"><button class=\"button\">Click Me</button></a></p></body></html>"); }); server.on("/get_unlock_pin", HTTP_GET, [](AsyncWebServerRequest *request){ // 创建异步响应流,先返回加载提示页面 AsyncResponseStream *response = request->beginResponseStream("text/html"); response->print("<!DOCTYPE html><html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" charset=\"UTF-8\"><link rel=\"icon\" href=\"data:,\"><style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}</style></head><body><h2>正在获取Pin码,请稍候...</h2></body></html>"); // 封装任务所需的参数 RequestContext *ctx = new RequestContext; ctx->responseStream = response; ctx->targetIp = "192.168.4.101"; // 创建独立任务执行HTTPS请求,绑定到CPU1避免和async_tcp任务冲突 xTaskCreatePinnedToCore( getPinTask, // 任务函数 "GetPinTask", // 任务名称 8192, // 栈大小,可根据实际调整 ctx, // 传递的参数 1, // 任务优先级 NULL, // 任务句柄(不需要可以留空) 1 // 运行在CPU1 ); // 发送响应头,开始流式传输 request->send(response); }); server.begin(); } void loop() { } // 独立任务:负责执行HTTPS请求并返回结果到响应流 void getPinTask(void *parameter) { RequestContext *ctx = (RequestContext*)parameter; String receivedPin = "获取失败"; Serial.println("\nStarting connection to server..."); WiFiClientSecure wificlient; // 栈上分配对象,比new更高效 HTTPClient https; https.setAuthorization("MyUserName", "MyPassword"); String path = "https://" + ctx->targetIp + "/api/unlock/generate_pin"; Serial.print("[HTTPS] begin... Path: " + path + "\n"); if (https.begin(wificlient, path)) { Serial.print("[HTTPS] GET...\n"); int httpCode = https.GET(); if (httpCode > 0) { Serial.printf("[HTTPS] GET... code: %d\n", httpCode); if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { String payload = https.getString(); Serial.println(payload); // 提取Pin码(建议后续换成ArduinoJSON库解析,更健壮) String tmp = payload.substring(payload.indexOf(':'), payload.indexOf('}')); String tmp2 = tmp.substring(tmp.indexOf('"')+1,tmp.lastIndexOf('"')); receivedPin = (tmp2.startsWith("-")) ? "-" : tmp2; } else { receivedPin = "请求失败,错误码:" + String(httpCode); } } else { Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str()); receivedPin = "请求失败:" + https.errorToString(httpCode); } https.end(); } else { Serial.printf("[HTTPS] Unable to connect\n"); receivedPin = "无法连接到目标服务器"; } // 用JS动态替换页面内容,给用户展示结果 ctx->responseStream->print("<script>document.body.innerHTML = '<h2>Received Pin: </h2><h2 style=\"color: #FF0000\">"); ctx->responseStream->print(receivedPin); ctx->responseStream->print("</h2></body></html>';</script>"); // 结束响应流,通知客户端请求完成 ctx->responseStream->close(); // 释放上下文内存 delete ctx; // 任务完成,自行销毁 vTaskDelete(NULL); }
方案2:临时禁用Task Watchdog(仅用于测试,不推荐生产环境)
如果你只是临时测试不想改架构,可以在HTTPS请求前后禁用和重新启用看门狗,但这会让系统失去对无响应任务的监控能力,风险较高:
修改你的getPin()函数:
String getPin(String ip){ // 禁用两个核心的看门狗 disableCore0WDT(); disableCore1WDT(); Serial.println("\nStarting connection to server..."); WiFiClientSecure *wificlient = new WiFiClientSecure; HTTPClient https; https.setAuthorization("MyUserName", "MyPassword"); String path = "https://" + ip + "/api/unlock/generate_pin"; Serial.print("[HTTPS] begin... Path: " + path + "\n"); // ... 原有逻辑保持不变 ... // 重新启用看门狗 enableCore0WDT(); enableCore1WDT(); // 记得返回默认值,避免函数无返回 return "获取失败"; }
额外优化建议
- JSON解析优化:当前用字符串截取Pin码的方式非常脆弱,只要返回的JSON格式稍有变化就会出错,建议使用
ArduinoJSON库来解析JSON,代码会更健壮。 - 资源复用:避免每次请求都创建新的
WiFiClientSecure对象,可以创建一个全局对象复用,减少内存开销和连接建立时间。 - 栈大小调整:如果运行时出现栈溢出错误,可以适当增大
getPinTask的栈大小(比如调整到10240字节)。
内容的提问来源于stack exchange,提问作者Sam




