You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动