77 lines
4.2 KiB
Markdown
77 lines
4.2 KiB
Markdown
## 目标
|
||||
|
|
把电机 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 终态判定(之前加的)继续生效
|