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

4.2 KiB
Raw Blame History

目标

把电机 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 — 注册新路由

路由表加一行:

{"/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 终态判定(之前加的)继续生效