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

62 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 修复: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 而非阻塞