You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ESPAsyncWebServer中串口查询慢回调致ESP32崩溃的解决方案咨询

ESPAsyncWebServer中处理Serial异步查询的解决方案

问题场景

已知在ESPAsyncWebServer库的回调函数中延迟或yield是不可取的,但回调函数需通过Serial端口查询另一设备,该过程耗时超1秒,会导致ESP32崩溃。示例代码如下:

void getDeviceConfig(AsyncWebServerRequest *request) {
  AsyncResponseStream *response =
      request->beginResponseStream("application/json");
  StaticJsonDocument<1024> doc;
  JsonArray array = doc.createNestedArray("get");

  for (size_t i = 0; i < request->params(); i++)
    array.add(request->getParam(i)->value());

  serializeJson(doc, Serial);
  /* At this point, the remote device determines what is being asked for 
     and builds a response. This can take fair bit of time depending on 
     what is being asked (>1sec) */

  response->print(Serial.readStringUntil('\n'));
  request->send(response);
}

曾尝试构建响应回调,但无法提前知晓远程设备生成的数据量;也试过使用分块响应,但无法告知调用方仍有数据未接收,只能返回0终止回调,求其他可行方案?


解决方案:异步Serial处理 + 请求上下文缓存

核心思路是把Web请求的上下文保存下来,将Serial的读写操作从Web回调中剥离,放到主循环或独立任务中处理,彻底避免阻塞Web服务器的事件循环。

1. 定义请求上下文结构体

用来保存Web请求的关键信息,包括请求对象、响应流、接收状态和缓冲区:

struct PendingRequest {
  AsyncWebServerRequest* req;
  AsyncResponseStream* resp;
  bool isWaitingForResponse;
  String serialBuffer;
};

// 单并发场景用单变量保存,多并发需改用FreeRTOS队列
PendingRequest currentPendingReq = {nullptr, nullptr, false, ""};

2. 修改Web回调函数

回调中仅完成查询指令发送和上下文保存,不等待Serial响应:

void getDeviceConfig(AsyncWebServerRequest *request) {
  // 避免并发冲突,单并发场景直接检查状态
  if (currentPendingReq.isWaitingForResponse) {
    request->send(503, "text/plain", "Busy processing another request");
    return;
  }

  // 初始化响应流并生成查询指令
  AsyncResponseStream *response = request->beginResponseStream("application/json");
  StaticJsonDocument<1024> doc;
  JsonArray array = doc.createNestedArray("get");

  for (size_t i = 0; i < request->params(); i++)
    array.add(request->getParam(i)->value());

  // 发送查询指令到Serial
  serializeJson(doc, Serial);
  Serial.println(); // 确保远程设备识别结束符

  // 保存请求上下文,标记为等待状态
  currentPendingReq.req = request;
  currentPendingReq.resp = response;
  currentPendingReq.isWaitingForResponse = true;
  currentPendingReq.serialBuffer.clear();
}

3. 在主循环中处理Serial响应

在loop()中监听Serial输入,收到完整响应后完成Web请求的发送:

void loop() {
  // 处理待响应的请求
  if (currentPendingReq.isWaitingForResponse && Serial.available()) {
    while (Serial.available()) {
      char c = Serial.read();
      if (c == '\n') { // 假设响应以换行符为结束标记
        // 将缓冲区内容写入响应流并发送
        currentPendingReq.resp->print(currentPendingReq.serialBuffer);
        currentPendingReq.req->send(currentPendingReq.resp);
        
        // 清理上下文
        currentPendingReq.isWaitingForResponse = false;
        currentPendingReq.req = nullptr;
        currentPendingReq.resp = nullptr;
        currentPendingReq.serialBuffer.clear();
        break;
      } else {
        currentPendingReq.serialBuffer += c;
      }
    }
  }

  // 其他主循环任务...
}

4. 多并发场景优化

如果需要支持多个并发请求,将单变量替换为FreeRTOS队列:

QueueHandle_t pendingRequestsQueue;

// 在setup()中初始化队列
void setup() {
  // ...其他初始化代码
  pendingRequestsQueue = xQueueCreate(5, sizeof(PendingRequest));
}

// 修改回调中的上下文保存逻辑
void getDeviceConfig(AsyncWebServerRequest *request) {
  PendingRequest newReq;
  newReq.req = request;
  newReq.resp = request->beginResponseStream("application/json");
  newReq.isWaitingForResponse = true;
  newReq.serialBuffer.clear();

  if (xQueueSend(pendingRequestsQueue, &newReq, 0) != pdPASS) {
    request->send(503, "text/plain", "Too many pending requests");
    return;
  }

  // 发送查询指令到Serial...
}

// 修改loop()中的Serial处理逻辑
void loop() {
  PendingRequest tempReq;
  if (xQueueReceive(pendingRequestsQueue, &tempReq, 0) == pdPASS && tempReq.isWaitingForResponse) {
    // 复用单场景的Serial接收逻辑,完成后发送响应
  }
}

补充说明

  • 该方案完全遵循ESPAsyncWebServer的异步设计原则,不会阻塞事件循环
  • 如果远程设备无固定响应结束符,可添加超时机制(比如2秒超时后发送已接收内容并结束请求)
  • 也可以给Serial添加中断处理,进一步提升响应实时性,主循环轮询已能覆盖大部分场景需求

内容的提问来源于stack exchange,提问作者Zapho

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 13:25:33