Files
ESP32_COMM/.zcode/plans/plan-sess_d0b3f2a2-ff3f-4a86-9a4d-b7534d876929.md
小龙虾 770ae14156
SPI Link v2 Self-Test / selftest (push) Has been cancelled
feat(spi_link): 添加SPI Link v2 PC自测框架并优化协议稳定性
添加SPI Link v2的PC端自测功能,包含交叉校验基准测试,覆盖HELLO/OTA/CAN/PING等
多种协议场景。同时重构了health monitor中的多字段读取逻辑,消除了volatile变量
的撕裂读问题,并增强了BLE OTA的数据传输可靠性。

refactor(health_monitor): 修复heap hook多字段撕裂读问题

采用portMUX_CRITICAL临界区实现多字段一致快照读取,消除任务上下文与heap hook
之间的数据竞争。修改了HookSnap结构体设计,通过一次性拷贝volatile变量避免
跨字段组合时的数据不一致。JSON输出中明确标注estimated值的置信度。

fix(ble_ota): 实现DATA_IN丢包负反馈机制

当malloc失败或队列满时发送缺口ACK通知前端按completed_bytes续传,避免
静默丢包导致的8秒超时重传风暴。增加节流机制防止负ACK在重传叠加时打爆
NimBLE mbuf池,提升低内存环境下的OTA传输稳定性。

refactor(spi_master): 从堆分配改为固定Job池管理

将SPI worker的动态堆分配Job改为固定大小的静态池管理,避免频繁的
calloc/free操作影响实时性。新增acquireJobSlot/releaseJobSlot机制
管理池槽生命周期,提升SPI通信的可靠性和内存使用效率。
2026-07-21 01:16:43 +08:00

84 lines
7.7 KiB
Markdown
Raw Permalink 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.
## 目标
把 BLE OTA DATA_IN 路径的 OOM/队列满从“静默丢包”变为“可恢复协议反馈”,让前端在一个窗口内收到缺口反馈并按 `completed_bytes` 续传退避,避免整窗口 8s 超时重发风暴。仅做方案 A(负 ACK 反馈),不上固定缓冲池;节流复用现有窗口节流思路。
## 现状核对(已核实)
- 平台 ESP32-C3,无 PSRAM,堆紧张;稳态堆水位需实测后才能评估是否上池,故本计划不涉及池。
- `ble_ota_data.cpp:597` `malloc(recv_len)` 失败仅 `DEBUG_ERR`,回调仍返回 0(ATT 成功)。
- `ble_ota_data.cpp:602` `xQueueSend` 失败同样静默:`free(heap_buf)` + `DEBUG_WARN("...丢...等重传")`,无协议反馈。两条路径都需修,不只 malloc 路径。
- 客户端窗口内 seq≥1 用 `writeValueWithoutResponse`(即发即忘),**收不到 ATT Error Response**;只有 seq=0 用 `writeValueWithResponse`。因此“返回 ATT 错误码”对窗口内丢包无效,必须靠 DATA_OUT 通知通道发窗口 ACK。原报告“返回 ATT 错误”建议在窗口路径不成立,本计划以通知 ACK 为主。
- 客户端 `transfer.js` 已支持按 `completed_bytes` 续传(`resumeSeq = ceil(lastAckBytes / payloadSize)`,最多 3 次重试,窗口 64 包),所以设备端多发一个缺口 ACK 不会破坏客户端语义,只会让续传更早开始。
- 现有 `sendWindowAck(cmd, seq, completed_bytes)` 在 `ble_ota_data.cpp:440` 已封装好,seq 取 `s_reorder.expected - 1`(最近连续落盘序号),负 ACK 直接复用即可,无需新协议字段、无需改客户端。
- 现有节流是“进度触发 + 每 `BLE_OTA_ACK_WINDOW`(64) 包边界”(见 process_esp32_packet:691-712),不是时间间隔。丢包路径发生在写入前、无进度,无法复用边界判定,需为负 ACK 引入轻量节流(避免重传叠加时 ACK 通知打爆 NimBLE mbuf 池)。
## 改动点(集中在 `main/server/ble_ota_data.cpp`)
### 1. 新增丢包计数(可观测性,满足“池耗尽/丢包有计数”验收)
- 在文件静态区新增:
- `static uint32_t s_ota_drop_malloc_fail = 0;` // malloc 失败累计
- `static uint32_t s_ota_drop_queue_full = 0;` // 队列满累计
- `static uint32_t s_neg_ack_sent = 0;` // 负 ACK 发送累计
- 在 `ble_ota_session_reset()`(已有复位点,约 line 414)中清零,与 reorder 状态同生命周期。
- 通过现有 health monitor 或 DEBUG_DBG 暴露(按现有日志风格,不新增上报通道)。
### 2. 新增负 ACK 节流状态
- 复用“每 64 包/最小间隔”思路,但丢包路径无 `completedPackets`,故用包计数 + 最小时间间隔双约束:
- `static uint32_t s_neg_ack_count = 0;` // 自上次发负 ACK 以来累计丢包数
- `static int64_t s_last_neg_ack_us = 0;` // 上次负 ACK 时刻(`esp_timer_get_time()`)
- 节流规则(在丢包路径触发):
- 累计丢包 ≥ `BLE_OTA_ACK_WINDOW`(64) **或** 距上次 ≥ 200ms 时,发一次负 ACK 并复位计数/时间。
- 否则只计数、不发,避免重传风暴期间 ACK 通知打爆 mbuf 池。
- 常量定义在文件内 `static constexpr`(与 `BLE_OTA_BUF_SIZE` 同位置,约 line 580),不污染 app_config.h。
- 时间用 `esp_timer_get_time()`(已是项目内惯用 API,见 reorder/health 用法)。
### 3. 新增负 ACK 发送辅助函数
- 在 `sendWindowAck`(line 460 后)下方新增:
```cpp
// 丢包路径专用:发缺口 ACK 让前端按 completed_bytes 续传退避,而非等 8s 超时。
// 节流:每 BLE_OTA_ACK_WINDOW 个丢包或 200ms 最多一次,防止 ACK 通知打爆 mbuf 池。
static void sendNegativeAckIfDue(uint8_t cmd);
```
- 实现:
- 读取 `s_reorder.expected`:>0 时 `ackSeq = expected - 1`,`completed = Esp32OtaCore::getReceived()`;expected==0(首包都没落盘)时 ackSeq=0、completed=0(等价于让前端从头续发,客户端已支持此语义)。
- 节流判定通过后调用 `sendWindowAck(cmd, ackSeq, completed)` 并 `s_neg_ack_sent++`。
- GD32 路径同样适用(`sendWindowAck` 已按 cmd 区分),但 GD32 的 expected 状态在 `s_ble_gd32_meta`,需在函数内按 cmd 分支取 expected/received;为简单起见,先实现 ESP32 路径,GD32 路径仅计数不发 ACK(GD32 是中继转发,缺口由 GD32 侧自行反馈),并在注释中标注。
### 4. 改造 `ble_ota_data_in_access` 丢包路径(line 596-605)
- malloc 失败分支:`s_ota_drop_malloc_fail++` → `sendNegativeAckIfDue(cmd)`(cmd 从 batch 头解析,见下)→ 保留 `DEBUG_ERR`。
- 队列满分支:`s_ota_drop_queue_full++` → `sendNegativeAckIfDue(cmd)` → 保留 `DEBUG_WARN`。
- **cmd 解析问题**:当前 line 566-596 的回调只把 mbuf 拷到 `buf`,未解析 cmd。负 ACK 需要 cmd 字节。两种做法:
- (a) 回调里解析前 7 字节头取 cmd(与 worker 中 `ble_ota_process_data_batch` 解析逻辑一致,可抽一个小 helper `parse_cmd_from_batch(buf, recv_len)`)。
- (b) 不解析,统一用 `BLE_OTA_CMD_ESP32_DATA`(0x01)(C3 自身 OTA 是主路径,GD32 经 BLE 中继较少)。
- **选 (a)**:抽 helper,正确性优先,避免误报 cmd 让前端续传走错分支。helper 放在 `ble_ota_data.cpp` 内 static,不暴露到头文件。
### 5. 不改动的部分
- 回调返回值仍为 0(窗口内 write-without-response 返回错误对端也收不到,且会污染 ATT 层;负 ACK 走通知通道才是正反馈)。
- 不引入固定缓冲池(C3 无 PSRAM,池规模需实测,本计划范围内不做)。
- 不改客户端 `tools/js/ble/*`(现有续传语义已兼容)。
- 不改协议字段/UUID/通知格式。
## 风险与权衡
- **负 ACK 抖动**:极端低堆时丢包密集,节流会让部分丢包延迟反馈,客户端可能仍等部分超时。权衡已用 200ms 兜底,可接受;若实测仍风暴,再调常量。
- **首包未落盘时负 ACK**:expected==0 时发 `ackSeq=0, completed=0`,等价“一切重来”。客户端 seq=0 用 write-with-response,本就等首包 ACK;此时负 ACK 不会误导。
- **GD32 路径**:本计划 GD32 仅计数不发负 ACK,避免改动 GD32 中继反馈语义;如需对称可后续扩展。
- **节流状态并发**:`s_neg_ack_*` 仅在 NimBLE host task 回调中读写(单线程),worker task 不写,无需锁。
## 验收
1. 构造低堆注入:临时在 malloc 前插 `if (s_inject_fail) { heap_buf=nullptr; }` 开关(测试桩,提交前移除),或用 `heap_caps_set_minimum_size` 压低堆水位。
2. 触发 OTA 上传,观察:
- 日志出现 `DATA_IN 堆内存不足` / `DATA_IN满` + `sendNegativeAckIfDue` 发送记录。
- 客户端在窗口内(≤200ms~8s)收到缺口 ACK,`lastAckBytes` 推进,从 `resumeSeq` 续传,**不出现整窗口 8s 超时**。
- `s_ota_drop_*` 计数器增长,负 ACK 后传输恢复,无永久卡死。
3. 正常路径回归:不注入失败时 ACK 行为、传输耗时、收尾 SHA256 不变。
4. 池耗尽有计数:`s_ota_drop_queue_full` / `s_ota_drop_malloc_fail` 在 health monitor/日志可查。
## 实施顺序
1. 加丢包计数 + `ble_ota_session_reset` 清零。
2. 加负 ACK 节流状态 + `sendNegativeAckIfDue`(ESP32 路径)。
3. 抽 `parse_cmd_from_batch` helper。
4. 改造 `ble_ota_data_in_access` 两条丢包路径接入负 ACK。
5. 低堆注入测试验收,移除注入桩。
6. (可选)GD32 路径对称负 ACK,视后续需要。
## 涉及文件
- `main/server/ble_ota_data.cpp`(主要改动:line 414 reset、line 460 后新增函数、line 596-605 丢包路径、line 580 附近常量)
- 不改:`ble_ota_priv.h`、`ble_ota.cpp`、`app_config.h`、客户端 JS。