## 方案:/can/feedback 改为请求驱动架构 ### 目标 - ESP32 不再被动缓存 GD32 的 0xC1 推送,改为前端请求时主动向 GD32 查询 - GD32 停止主动推送 0xC1(降低 SPI 资源占用) - 删除 `s_latest[3]`/`s_work[3]` 常驻 BSS(省 ~7.8KB)+ handler 栈上 `results[3]`(省 ~3.9KB 栈) ### 依据 - GD32 `handleFeedback` 已支持 0xC1 请求查询(`[channel, page]` → ACK + 页数据),无需新增协议 - ESP32 `transact(0xC1, ...)` 是现成请求/响应机制,`canFdSend`/`canFdSetRxMode` 已在用 - `canFdParseFeedbackPage` 可直接复用(只解析 payload 字节,不关心来源) - 0xC4/0xC5/0xC6/0xD2 推送独立,不受影响 ### 改动清单 #### 1. ESP32 侧 — `main/transport/canfd_bridge.cpp` / `.h` 新增 `canFdQueryFeedback(channel, page, can_feedback_result_t *out, uint8_t *entryIdx)`: ```cpp int canFdQueryFeedback(uint8_t channel, uint8_t page, can_feedback_result_t *out, uint8_t *entryIdx); ``` - 发 `transact(0xC1, {channel, page}, ...)`,超时 200ms - 成功(resp_type==ACK)→ 调 `canFdParseFeedbackPage(resp_buf, resp_len, out, entryIdx)` - 返回 0=成功,-1=SPI/参数失败,-2=GD32 拒绝(ERROR) #### 2. ESP32 侧 — `main/server/http_handlers.cpp` `can_feedback_handler` - 删除 `results[CAN_CHANNEL_MAX]` 栈数组 + `getLatestCanFeedback` 循环 - 改为:对每个使能通道,调 `canFdQueryFeedback(ch, 0, &result, &entryIdx)` + `canFdQueryFeedback(ch, 1, &result, &entryIdx)`,在栈上用单个 `can_feedback_result_t result`(1.3KB,用完即释放) - lite 路径:只需 page 0(统计字段在 page0 header),1 次 SPI 查询即可 - full 路径:page 0 + page 1,2 次查询,然后序列化 - 查询失败 → 该通道 `valid=false`(与原来无快照时行为一致) - `pauseCanFeedbackTask` 调用保留(send_batch 仍需独占 SPI) #### 3. ESP32 侧 — `can_mode_handler` GET - 原来 `getLatestCanFeedback(0, &fb)` 读缓存 mode → 改为 `canFdQueryFeedback(0, 0, &fb, &entryIdx)` 取 page0 的 mode 字段 - 原来的 fallback `canFdSetRxMode` 保留(查询也失败时兜底) #### 4. ESP32 侧 — `main/monitor/can_feedback_task.cpp` - **删除** `s_latest[3]`、`s_valid[3]`、`s_work[3]`、`s_workIdx[3]`、`s_lastLoggedError[3]`、`publish()`、`getLatestCanFeedback()` - **删除** taskLoop 里的 0xC1 分支(`canFdParseFeedbackPage` + `publish`) - **保留** 0xC4/0xC5/0xC6/0xD2 事件分发(这些仍走推送) - **保留** `pauseCanFeedbackTask`(send_batch / OTA 仍需暂停事件消费) - `s_mutex` 删除(仅保护 s_latest/s_valid,不再需要) #### 5. ESP32 侧 — `main/monitor/can_feedback_task.h` - 删除 `getLatestCanFeedback` 声明 - 保留 `startCanFeedbackTask`、`pauseCanFeedbackTask` #### 6. GD32 侧 — `app_spi_cmd_can_feedback.cpp` - `canFeedbackStartRound()`:改为空函数(或加 `s_pushEnabled=false` 永久禁用推送) - `canFeedbackPushStep()`:开头加 `if (!s_pushEnabled) return;` 或直接 return - **保留** `handleFeedback`(请求查询处理程序不动) - **保留** `buildFeedbackPayload`(查询和推送共用) #### 7. GD32 侧 — `task_runner.cpp` - `canFeedbackStartRound()` 调用可保留(变成空函数无副作用)或删除 ### 内存收益 | 项 | 改前 | 改后 | 省 | |---|---|---|---| | `s_latest[3]` BSS | 3.9KB | 0 | 3.9KB | | `s_work[3]` BSS | 3.9KB | 0 | 3.9KB | | handler 栈 `results[3]` | 3.9KB | 1.3KB(单通道) | 2.6KB 栈 | | **合计常驻 BSS** | | | **~7.8KB** | ### 性能影响 - 原来读缓存:<1ms - 改后查询:lite 路径 1 次 SPI 往返 ~2-4ms;full 路径 6 次往返(3ch×2page)~12-24ms - 前端 1Hz 轮询,24ms 完全可接受(httpd 单线程最坏阻塞 <50ms) - GD32 不再 1Hz 推送 6 帧,省下 SPI 带宽和 PB6 中断 ### 风险与缓解 - **SPI 查询失败**:GD32 不响应时 `transact` 超时返回,handler 返回 valid=false,前端显示"--"(与原无快照行为一致) - **OTA 期间查询**:send_batch/OTA 已 `pauseCanFeedbackTask`,且 SPI worker 串行化,查询不会与 OTA 数据流冲突 - **can_monitor.html full 路径延迟**:6 次 SPI 查询 ~24ms + JSON 序列化,总 <50ms,远小于 1s 轮询间隔 ### 不改动的部分 - 0xC4 ADC、0xC5 motor state、0xC6 motor version、0xD2 motor OTA 推送机制不变 - `canFdParseFeedbackPage` 解析逻辑不变 - GD32 `handleFeedback` 查询处理程序不变 - 前端 JS 不变(API 响应格式不变)