Files
ESP32_COMM/.zcode/plans/plan-sess_62703d36-b353-46c4-b5fb-4f137f524819.md
T
小龙虾 76782349a9 添加了SPI链接V2支持文件,重构OTA协调器的存储准备流程,
统一WiFi/BLE传输路径的收尾处理逻辑。
feat(ota): 优化OTA性能并增强系统监控功能

- 修改RSSI警告阈值从-75dBm调整为-80dBm,更新相关文档说明
- 增加HTTP服务器任务栈大小至10240字节,解决栈溢出问题
- 将OTA块大小从4096字节降至2048字节以降低峰值堆内存占用
- 实现按需擦除机制替代预擦除,在GD32 OTA中按扇区动态擦除
- 添加任务栈水位主动巡检功能,监控栈使用情况并告警
- 集成FreeRTOS栈溢出钩子,提供栈溢出检测和告警
- 优化ESP32 OTA实现使用顺序写入模式,消除长时间擦除阻塞
- 改进堆内存不足告警机制,添加限流避免刷屏
- 移除不必要的调试日志输出,精简健康监控模块
2026-07-13 03:12:21 +08:00

4.8 KiB
Raw Blame History

修复:WiFi 上传阻塞 HTTP 主线程导致日志挤压/内存爆满

根本原因

esp_http_server(IDF v6.0)是单线程 select 模型:一个主线程在 select() 循环里依次处理所有 socket。ota_upload_handler 在 HTTP 主线程里同步阻塞执行整个上传(GD32 含 Flash 擦除可达数十秒,ESP32 同样)。期间主线程不回 select(),导致:

  1. 前端 pausePollingForUpload() 仍以 2.5–3s 发 /system/status、/logs,但 HTTP 主线程无法 accept/处理 → 连接在 lwIP 侧堆积,pbuf 占用堆内存 → 内存爆满
  2. /logs 无法被 drain,日志在 ring buffer(64×256,有上限但前端看不到)→ 体现为"LOG挤压"
  3. max_open_sockets=4 + lru_purge 也无效:线程不回 select(),LRU 清理不触发

方案:异步上传 handler

用 IDF v6.0 的 httpd_req_async_handler_begin/complete API(已确认可用,examples/protocols/http_server/async_handlers 即此模式),把上传移到独立 worker 任务。HTTP 主线程立即返回 select() 循环,继续服务 /logs、/system/status。

改动文件

1. main/server/http_handlers.h(新增声明)

  • 新增 esp_err_t ota_upload_handler_async(httpd_req_t *req);(新的上传入口,注册替代原 ota_upload_handler)
  • 保留原 ota_upload_handler 的实现,但改为 static 内部函数,仅供 async worker 调用

2. main/server/http_handlers.cpp(核心改动)

  • 新增 async worker 基础设施(文件内 static):
    • static QueueHandle_t s_uploadQueue(长度 1,元素为 httpd_req_t*)
    • static SemaphoreHandle_t s_workerReady(binary,初值 0)
    • static TaskHandle_t s_uploadWorker(栈 8192,优先级 5,固定 core 0 与现有任务一致)
    • static void uploadWorkerTask(void*):循环 xSemaphoreGive(s_workerReady) → xQueueReceive → 调用原上传逻辑(用 async req 句柄)→ httpd_req_async_handler_complete()
    • static esp_err_t ensureUploadWorker():懒启动 worker(首次上传请求时创建,幂等)
  • 新增 ota_upload_handler_async(httpd_req_t *req):
    1. ensureUploadWorker()
    2. httpd_req_t *async_req; httpd_req_async_handler_begin(req, &async_req) —— 把 socket 标记 for_async_req,主线程不再处理它
    3. if (xSemaphoreTake(s_workerReady, 0) == false) —— worker 忙(已有上传在进行),立即 httpd_req_async_handler_complete(async_req) 并返回 503:
      httpd_resp_set_status(req, "503 Service Unavailable");
      httpd_resp_sendstr(req, "{\"status\":\"error\",\"message\":\"upload in progress\"}");
      return ESP_OK;
      
      注意:503 要发在原 req 上(begin 之后 async req 才能发响应,但此处用原 req 发错误更简单——实际上 begin 后原 req 的 remaining_len 被清零,发响应需用 async_req。所以错误路径用 async_req 发)。
    4. xQueueSend(s_uploadQueue, &async_req, portMAX_DELAY)
    5. 返回 ESP_OK(主线程已完成投递,立即回 select)
  • 把原 ota_upload_handler 改名 static esp_err_t ota_upload_handler_impl(httpd_req_t *req),逻辑不变(它内部已用 httpd_req_recv/httpd_resp_send,async 句柄完全兼容)
  • 保留 CORS 头设置:http_set_cors_headers 在 ota_upload_handler_async 里对原 req 设置(begin 之前),async 路径的响应头也需在 worker 里设置——实际上 httpd_req_async_handler_begin 会拷贝 resp_hdrs,所以 CORS 头在 begin 前对原 req 设置即可被子拷贝

3. main/server/wifi_ota.cpp(路由注册)

  • kHttpRoutes[] 中 /ota/upload 的 handler 从 ota_upload_handler 改为 ota_upload_handler_async
  • OPTIONS 预检不变(仍走 http_options_handler)

不改动的部分

  • 前端 app.js 不改:pausePollingForUpload 仍降速轮询,但现在能真正拿到 /logs 和 /system/status 响应(主线程不再阻塞)
  • 日志系统不改:ring buffer 已有上限,异步化后 /logs 能被正常 drain,不再积压
  • OtaCoordinator 不改:已有 ESP_ERR_INVALID_STATE 拒绝并发会话,worker 的 503 是更早的防线
  • OTA_BLOCK_SIZE、栈大小不改

内存影响

  • 新增 1 个 worker 任务栈 8192 字节(从现有 free_heap 出,ESP32-C3 内部堆约 200KB+,可接受)
  • 队列 1×sizeof(httpd_req_t*)=4 字节,信号量 ~80 字节,忽略不计
  • 收益:上传期间不再堆积 lwIP pbuf(每个未 accept 的连接可达数 KB),消除内存爆满主因

验证方式

  1. 编译通过(idf.py build)
  2. 功能验证:WiFi 上传 GD32/ESP32 固件,上传期间网页 /logs 持续刷新、/system/status 能返回(不再超时)
  3. 内存验证:/system/status 的 free_heap 在上传期间不再持续下降
  4. 并发验证:上传进行中再发一个 /ota/upload,立即返回 503 而非阻塞