添加了SPI链接V2支持文件,重构OTA协调器的存储准备流程,
统一WiFi/BLE传输路径的收尾处理逻辑。 feat(ota): 优化OTA性能并增强系统监控功能 - 修改RSSI警告阈值从-75dBm调整为-80dBm,更新相关文档说明 - 增加HTTP服务器任务栈大小至10240字节,解决栈溢出问题 - 将OTA块大小从4096字节降至2048字节以降低峰值堆内存占用 - 实现按需擦除机制替代预擦除,在GD32 OTA中按扇区动态擦除 - 添加任务栈水位主动巡检功能,监控栈使用情况并告警 - 集成FreeRTOS栈溢出钩子,提供栈溢出检测和告警 - 优化ESP32 OTA实现使用顺序写入模式,消除长时间擦除阻塞 - 改进堆内存不足告警机制,添加限流避免刷屏 - 移除不必要的调试日志输出,精简健康监控模块
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# 修复: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 而非阻塞
|
||||
@@ -0,0 +1,58 @@
|
||||
## 删除"断线后切换慢速轮询"机制
|
||||
|
||||
**文件:** `G:\Robot_v2\DCU\ESP32_COMM\tools\app.js`
|
||||
|
||||
**范围:** 只删除"降速到 10s"的行为,保留 5 次失败触发的网络诊断(diagnoseNetwork)和断线 UI 提示。删除后断线时保持 1s 轮询,仅显示"未连接"并按需做网络诊断。
|
||||
|
||||
### 改动点
|
||||
|
||||
1. **删除变量 `pollSlowMode`(1600 行)**
|
||||
- 移除 `var pollSlowMode = false; // 降速模式(10s 一次)`
|
||||
|
||||
2. **删除函数 `slowDownPolling()`(1671-1681 行)**
|
||||
- 整个函数移除
|
||||
|
||||
3. **删除函数 `restorePolling()`(1682-1695 行)**
|
||||
- 整个函数移除(保留 `startPolling`、`pausePollingForUpload`、`resumePollingAfterUpload`)
|
||||
|
||||
4. **简化 `startPolling()`(1662-1670 行)**
|
||||
- 删除 `pollSlowMode = false;` 这一行(其余保留)
|
||||
|
||||
5. **简化 `resumePollingAfterUpload()`(1714 行)**
|
||||
- `statusPollIntervalMs = pollSlowMode ? 10000 : 1000;` → `statusPollIntervalMs = 1000;`
|
||||
|
||||
6. **简化 `pollStatus()` 成功分支(1738-1747 行)**
|
||||
- 移除 `wasSlow` / `restorePolling()` 调用,仅保留 `consecutivePollFailures = 0;`(保留"恢复在线"的 toast 体验需要另判断;改为:若 `consecutivePollFailures > 0` 则提示已恢复并清零)
|
||||
|
||||
7. **简化 `pollStatus()` 失败分支的 5-fail 块(1789-1800 行)**
|
||||
- 删除 `!pollSlowMode` 条件判断中的 `pollSlowMode` 部分
|
||||
- 删除 `slowDownPolling();` 调用和"已切换为慢速轮询"日志
|
||||
- 保留 `diagnoseNetwork()` 诊断、`updateConnectionUi(false, '未连接')`、`applyDisconnectedRuntimeState()` 和诊断文本显示
|
||||
|
||||
8. **简化 `pollStatus()` 失败分支末尾(1828 行)**
|
||||
- `} else if (!pollSlowMode) {` → `} else {`(该 else 分支总是显示未连接状态)
|
||||
|
||||
9. **清理 `testConnection()`(817-820 行)**
|
||||
- 删除 `if (pollSlowMode) { addLog(...); restorePolling(); }` 块
|
||||
|
||||
10. **清理 `refreshSystemStatusNow()`(874 行)**
|
||||
- 删除 `if (pollSlowMode) restorePolling();`
|
||||
|
||||
11. **清理 `visibilitychange` 监听(1854-1855 行)**
|
||||
- 删除 `// 之前若在慢速模式...` 注释和 `if (pollSlowMode) restorePolling();`
|
||||
|
||||
12. **清理 IP 输入 change 监听(1887-1892 行)**
|
||||
- 简化为:IP 变化时清零失败计数并立即拉一次状态(不再依赖 restorePolling)
|
||||
- 或直接移除 `if (pollSlowMode) { ... restorePolling(); }` 块,保留监听器做基本的失败计数清零
|
||||
|
||||
### 不删除的部分
|
||||
- `consecutivePollFailures` 变量及其计数逻辑(诊断仍需要)
|
||||
- `diagnoseNetwork()` 网络诊断函数
|
||||
- `lastDiagTime` / `diagResult` 诊断状态变量
|
||||
- `applyDisconnectedRuntimeState()` 断线 UI 更新
|
||||
- `statusPollIntervalMs` 变量(被 upload 暂停/恢复机制复用,保持 1000 默认值)
|
||||
|
||||
### 验证
|
||||
- 浏览器打开 `tools/index.html`,确认页面加载无 JS 错误
|
||||
- 模拟 ESP32 断线:连续 5 次失败后应仍触发网络诊断并显示"未连接",但轮询间隔保持 1s
|
||||
- 恢复 ESP32:应清零失败计数并提示"恢复在线"
|
||||
Reference in New Issue
Block a user