84 lines
7.7 KiB
Markdown
84 lines
7.7 KiB
Markdown
## 目标
|
||||
|
|
把 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。
|