Files
ESP32_COMM/.zcode/plans/plan-sess_be551324-a74a-4d44-b63e-8e00c515c2d5.md
T
小龙虾 2138a572ee feat(esp32): 添加多电机OTA中继功能并优化调试日志系统
- 新增多电机状态、版本和OTA中继监控模块(monitor/motor_state.cpp,
  monitor/motor_version.cpp, monitor/motor_ota_relay.cpp)
- 在CAN反馈任务中添加0xC5/0xC6/0xD2类型消息处理,支持多电机状态推送
- 重构调试日志系统,将一次性JSON分配改为512B栈缓冲流式发送,降低内存峰值
- 调试UART波特率从115200提升至30000
- 修改版本管理方式,从自动生version.h改为手动维护
- 更新VSCode设置以支持ESP-IDF开发环境配置
- 在OTA协调器中添加电机OTA互斥机制,防止并发冲突
- 完善中继状态机异常处理,添加远程取消通知机制
2026-07-26 20:40:16 +08:00

77 lines
4.2 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.
## 目标
把电机 OTA 从"收固件+触发喂数据"一步式,改成 BLE 刷 GD32 那样的两段式:**先收完固件写 flash,再单独触发 SPI START + 喂数据**。拆分后收固件阶段不碰 SPI,避免与 GD32 按铃推送竞态;触发阶段只发一个短小的 SPI 0xD0。
## 改动点(共 4 个文件)
### 1. `main/server/http_handlers.cpp` — 拆分上传 + 新增 relay 触发
**`motor_ota_upload_handler`(line 2057)改:**
- 保留:互斥检查、`beginWriteSession`、流式收固件写 flash(`flashWriteChunk` 循环)、`beginWriteSession` 复位
- **删除**:末尾的 `motorOtaStart(...)` 调用(line 2128)
- 收完后写 meta:`metaSave({total_size=content_len, stored_size=received})`,让 relay 端点能确认固件完整
- 返回改为 `{"status":"stored","message":"call POST /motor/ota/relay?ch=X to trigger","channel":ch,"size":received}`
**新增 `motor_ota_relay_handler`:**
- 从 query 读 `ch`(和上传端点一致)
- `Gd32FlashStore::metaLoad` 确认 `stored_size >= total_size`,否则返回 400 `firmware not fully stored`
- `motorOtaIsActive()` 检查,已在跑则 409
- 调 `motorOtaStart(meta.total_size, ch)`:
- 0 → `{"status":"ok","message":"OTA started"}`
- -2 → 409 `already in progress`
- -3 → 500 `SPI start failed`(保留原日志 DEBUG_ERR)
- 成功触发后**清 meta**(`metaClear`),固件已交给 feedTask,避免残留
### 2. `main/server/wifi_ota.cpp:482` — 注册新路由
路由表加一行:
```cpp
{"/motor/ota/relay", HTTP_POST, motor_ota_relay_handler},
```
(紧挨现有三个 motor/ota 路由,CORS preflight 复用现有循环自动处理)
### 3. `main/server/http_handlers.h` — 声明新 handler
加 `esp_err_t motor_ota_relay_handler(httpd_req_t *req);`
### 4. `tools/motor.html` — 前端两段式调用
**`startOta`(line 2986-3001)改:**
```
// 步骤2:POST /motor/ota 只收固件
const upResp = await fetch("/motor/ota?ch=${ch}", { POST, body: ota.bytes, timeout 15s });
const upJson = await upResp.json();
if (!upResp.ok) throw new Error(upJson.error);
otaLog("[上传] 成功,触发 GD32 转发…");
// 步3:POST /motor/ota/relay 触发喂数据
const rResp = await fetch("/motor/ota/relay?ch=${ch}", { POST, timeout 5000 });
const rJson = await rResp.json();
if (!rResp.ok) throw new Error(rJson.error);
otaLog("[转发] GD32 开始握手…");
startOtaPoll(); // 轮询不变
```
超时:上传 15s(48KB),触发 5s(一个 SPI 事务)。
**`cancelOta`:** 不用改。`/motor/ota/cancel` 两阶段都有效:触发前 motorOtaCancel 是空操作(feedTask 没跑),但仍清 GD32 状态;可顺便让 cancel handler 也清 meta(见下)。
### 5.(附带)`motor_ota_cancel_handler` 加清 meta
cancel 时 `motorOtaCancel()` 后补一行 `Gd32FlashStore::instance().metaClear()`,覆盖"上传完未触发就取消"场景,清掉残留固件 meta。触发后取消也清 meta 无害(feedTask 已停)。
## 状态机与轮询(不变)
- `/motor/ota/status` 复用,返回 GD32 pushStatus 的 state/packets_acked
- 触发前 status = IDLE;触发后进 HANDSHAKE/TRANSFER/FINISHED/ERROR
- 前端在 relay 请求成功后才 `startOtaPoll`,避免触发前空轮询
## 关键收益
- **消除竞态**:收固件纯 HTTP 流式写 flash,不碰 SPI/MISO 缓冲,不与 GD32 按铃推送争抢;触发阶段只发一个短 SPI 0xD0,窗口极小
- **语义清晰**:和 BLE 刷 GD32 的 `prepare+收流 → /ota/relay` 两段式对齐
- **改动最小**:feedTask、GD32 侧、状态轮询全部不动;只拆 HTTP 端点和前端调用顺序
## 风险
- 前端两次请求间若用户取消:cancel 已覆盖两阶段
- 上传成功但触发失败:meta 仍存,用户可重试 relay(relay 端点先检查 `motorOtaIsActive` 再触发);或 cancel 清 meta 重来
- 固件占 flash 未触发:metaClear 在 cancel/relay 成功后执行,残留风险低
## 验证
- 编译 ESP32_COMM(idf.py build)
- 手动测:选固件→开始,看日志先"[上传] 成功"再"[转发] GD32 开始握手",确认两次 HTTP 分开
- 观察 SPI 按铃竞态是否消失(原"SPI start failed"概率应降到极低)
- feedTask 终态判定(之前加的)继续生效