Files
ESP32_COMM/docs/test_ble_ota_fix.md
T
小龙虾 2caf10b9de feat(ota): 添加OTA回滚保护和固件完整性校验功能
- 启用CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE和CONFIG_APP_ROLLBACK_ENABLE配置选项
- 在main.cpp中添加esp_ota_mark_app_valid_cancel_rollback()调用,防止意外回滚
- 实现SHA-256固件完整性校验,包括计算、存储和验证功能
- 添加ota_mutex.h头文件用于ESP32 OTA全局互斥锁管理

feat(health_monitor): 实现线程安全的健康状态管理

- 将HealthMonitor类的健康状态改为私有成员并通过mutex保护
- 添加getState()和setState()方法提供线程安全的状态访问
- 修复多任务并发访问健康状态时的竞争条件问题

feat(ota_relay): 增强中继模块的线程安全性和可靠性

- 为OtaRelay类添加静态mutex保护状态变量访问
- 实现StateLockGuard RAII锁管理器确保异常安全
- 添加全局超时机制防止OTA过程无限等待
- 改进flash写入逻辑支持非4字节对齐数据处理

refactor(app_config): 更新版本信息和配置常量

- 更新BUILD_TIME至最新编译时间
- 更新GIT_HASH至最新提交
- 添加BLE_OTA_CTRL_VERIFY_SHA256命令常量定义
2026-06-14 17:10:24 +08:00

317 lines
12 KiB
Markdown
Raw 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 刷写修复测试计划
> 对应修复:问题1(偶发丢包检测错误)+ 问题2(通道占用延迟)
> 涉及文件:`ble_ota.h` / `ble_ota.cpp` / `ble_ota.html`
---
## 一、正常流程测试
### T-01 标准 ESP32 OTA 全流程(基线回归)
| 项目 | 说明 |
|------|------|
| 固件大小 | 使用常规固件(~200KB–1MB) |
| 操作步骤 | BLE 连接 → 选择文件 → 一键刷写 |
| 预期结果 | 刷写完成,设备重启,日志无报错 |
**验证要点:**
- 每个窗口 ACK 字节数与期望完全匹配
- 日志中出现 `已写 X/2035 包` 进度连续递增
- 日志出现 `[FINISH] ESP32 OTA 刷写完成,等待重启` + toast 成功提示
- 设备重启后 `readVersion` 返回新版本号
### T-02 标准 GD32 OTA 中继全流程(回归验证)
| 项目 | 说明 |
|------|------|
| 目标 | GD32(经 ESP32 中继 SPI 转发) |
| 操作步骤 | BLE 连接 → 选择 GD32 固件 → 一键刷写 |
| 预期结果 | ESP32 中继正常,GD32 收到完整固件 |
**验证要点:**
- ESP32 端去重逻辑仅作用于 `BLE_OTA_CMD_ESP32_DATA`(0x01),不影响 GD32 通道(0x02)
- GD32 窗口 ACK 正常返回
- 刷写完成后 CTRL 命令触发成功
### T-03 尾包非整窗口场景
| 项目 | 说明 |
|------|------|
| 条件 | `packetTotal % BLE_OTA_ACK_WINDOW != 0`,如固件 976752 字节 / 480 = 2035 包,最后一个窗口仅 19 包(seq 2016–2034) |
| 预期 | 最后一个窗口的 `endSeq = 2034`,ACK 正常返回 |
**验证要点:**
- `expectedBytes = Math.min((endSeq+1)*480, totalSize)` 正确截断
- 尾包 `data_len < BLE_OTA_CHUNK_PAYLOAD` 触发 `isLastPacket`
- 设备创建收尾任务,发送 `[FINISH] 即将执行 esp_ota_end()`
### T-04 多窗口连续刷写稳定性
| 项目 | 说明 |
|------|------|
| 条件 | 固件 > 500KB(至少 16 个窗口) |
| 操作 | 连续执行 3 次完整刷写 |
| 预期 | 每次均成功,无丢包报错 |
**验证要点:**
- `m_esp32ExpectedSeq` 在 `ble_esp32_ota_begin` 中正确重置为 0
- 旧 OTA 会话被先中止(日志出现 `检测到未完成的 ESP32 OTA,会先中止旧会话`)
- 每次刷写的 `m_esp32Received` 从 0 开始累加
---
## 二、边界条件测试
### T-05 固件大小恰好为窗口整数倍
| 项目 | 说明 |
|------|------|
| 条件 | 构造固件大小 = N × 32 × 480,如 15360 字节(1 窗口)或 491520 字节(32 窗口) |
| 预期 | 无尾包,最后一个窗口 `endSeq = packetTotal - 1`,`expectedBytes = totalSize` |
**验证要点:**
- 最后一个包 `data_len == BLE_OTA_CHUNK_PAYLOAD`,不触发 `isLastPacket`
- 完成条件走 `m_esp32Received >= total_size` 分支
### T-06 固件极小(单包/小于窗口)
| 项目 | 说明 |
|------|------|
| 条件 | 固件 < 480 字节(仅 1 包)或 480–15360 字节(1 窗口内) |
| 预期 | 单包即触发尾包检测或窗口 ACK |
**验证要点:**
- 单包场景:`seq=0` 同时满足 `(seq+1)%32==0` 否、但 `isLastPacket` 是
- 设备发送 ACK,`m_esp32Received` 等于实际固件大小
- `expectedBytes` 正确等于 `totalSize`
### T-07 固件极大(接近分区上限)
| 项目 | 说明 |
|------|------|
| 条件 | 固件大小接近 OTA 分区 size(如 ~1.5MB) |
| 预期 | 完整刷写成功,约 50+ 个窗口全部通过 |
**验证要点:**
- `m_esp32ExpectedSeq` 跨窗口持续递增,不会溢出 `uint16_t`(最大值 65535)
- `ble_esp32_ota_begin` 中 `total_size > check_partition->size` 检查仍正确拒绝超大固件
### T-08 payloadSize 非默认值
| 项目 | 说明 |
|------|------|
| 条件 | 在页面修改 payload 大小为 256 / 400 / 498 |
| 预期 | 包总数自动调整,每窗口 ACK 字节校验正确 |
**验证要点:**
- 设备端 `BLE_OTA_CHUNK_PAYLOAD = 480` 是固定值
- 若 Web 端 payload < 480,设备端 `remaining_after_header < BLE_OTA_CHUNK_PAYLOAD + 4` 会走 `data_len = remaining - 4` 分支
- 若 Web 端 payload > 480,设备端会按 480 解析,剩余数据被当作下一包处理(可能出错)
- **注意:** payload 大小必须与设备端 `BLE_OTA_CHUNK_PAYLOAD` 一致,此为已知约束
### T-09 BLE 断开重连后重新刷写
| 项目 | 说明 |
|------|------|
| 操作 | 刷写到一半 → 手动断开 BLE → 重新连接 → 再次刷写 |
| 预期 | 新刷写从 `ble_esp32_ota_begin` 重新开始 |
**验证要点:**
- `m_esp32ExpectedSeq` 在 `ble_esp32_ota_begin` 中重置为 0
- 旧 OTA 会话被中止,`m_esp32OtaActive` 被清除
- 全局互斥 `esp32_ota_mutex_release` 被正确调用
---
## 三、故障注入测试
### T-10 BLE 链路层偶发丢包自动恢复(核心修复验证)
| 项目 | 说明 |
|------|------|
| 注入方式 | 在 BLE 信号较弱的环境中刷写(距离远/有干扰),或临时将包间延迟移除后快速发送 |
| 预期 | 检测到丢包 → 自动重发窗口 → 恢复成功 |
**验证要点(日志检查):**
- 出现 `丢包检测: seq=X 丢失Y字节(Z包),第1次重发整个窗口` 警告日志
- 重发后 ACK 字节数匹配,继续下一窗口
- 设备端日志出现 `ESP32 OTA 重复包,跳过: seq=X expected=Y`
- 最终刷写成功,无数据损坏
### T-11 单窗口连续 2 次丢包(重试用尽)
| 项目 | 说明 |
|------|------|
| 注入方式 | 极端信号劣化,使一个窗口连续 3 次发送都有丢包 |
| 预期 | 2 次重试用尽后抛出错误 |
**验证要点:**
- 日志出现第 1 次、第 2 次重发记录
- 最终报错:`BLE 丢包检测: seq=X 期望Y字节 实际Z字节 丢失W字节(V包) 已重试2次`
- `uploadFirmware` 返回 `false`,toast 显示失败
- `state.lastUploadStatus = 'failed'`
- 用户手动重新刷写可成功
### T-12 通道占用 → 立即失败(核心修复验证)
| 项目 | 说明 |
|------|------|
| 注入方式 | 先通过 WiFi 通道启动 ESP32 OTA(占用互斥锁),然后通过 BLE 发起刷写 |
| 预期 | BLE 刷写在 < 1 秒内失败,而非等待 10 秒超时 |
**验证要点(日志检查):**
- 设备端日志:`ESP32 OTA 被其他通道占用,拒绝 BLE OTA begin`
- Web 端日志按顺序出现:
1. `[FINISH] ESP32 OTA 被其他通道占用,互斥拒绝`
2. Toast 弹出 `ESP32 OTA 被其他通道占用,请稍后重试`
3. `写入失败: 设备端 FINISH 错误: stage=0x83 value=0`
4. `[AUTO] 一键刷写中止`
- **关键时序**:从 FINISH 通知到写入失败之间 < 500ms(此前需要 10 秒)
- 不应出现 `等待 ACK 超时` 日志
### T-13 通道占用后重试成功
| 项目 | 说明 |
|------|------|
| 操作 | T-12 失败后 → 等待 WiFi OTA 完成 → 重新 BLE 刷写 |
| 预期 | 互斥锁已释放,BLE 刷写成功 |
**验证要点:**
- `esp32_ota_mutex_release` 在 WiFi OTA 完成后被调用
- `g_esp32OtaOccupied` 被清除为 `false`
- BLE 刷写正常走完全流程
### T-14 esp_ota_end / set_boot_partition 失败快速中断
| 项目 | 说明 |
|------|------|
| 注入方式 | 使用损坏的固件文件(无有效 ESP32 镜像头),使 `esp_ota_end` 返回错误 |
| 预期 | 设备发送 `FINISH stage=0x81`,Web 端立即报错 |
**验证要点:**
- Web 端收到 FINISH 0x81,toast `ESP32 OTA 刷写失败`
- `ackWaiter` 被 reject(如果此时有等待中的 ACK)
- `uploadFirmware` 快速返回 `false`
- 设备端 `esp_ota_abort` 被调用,互斥锁释放
### T-15 刷写过程中用户中止
| 项目 | 说明 |
|------|------|
| 操作 | 刷写进行中点击「中止」按钮 |
| 预期 | 当前窗口完成后抛出 `用户中止` 错误 |
**验证要点:**
- `state.abortRequested = true` 在窗口循环开头被检测
- `throw new Error('用户中止')` 被触发
- `finally` 块清理 `ackWaiter`、设置 `state.uploading = false`
- 设备端 OTA 会话在下次刷写时被 `ble_esp32_ota_begin` 清理
### T-16 窗口末尾丢包恢复(最常见场景)
| 项目 | 说明 |
|------|------|
| 场景 | 窗口 0(seq 0–31),其中 seq=31 在首次发送时丢失 |
| 预期 | 重发整个窗口后,seq=0–30 被跳过(重复包),seq=31 写入成功 |
**验证要点:**
- 首次发送:seq=0–30 写入成功,`m_esp32ExpectedSeq = 31`
- seq=31 丢失,设备未收到,不发送 ACK(因为 ACK 触发条件在 seq=31)
- 主机 waitForAck 超时 → 触发窗口重发
- 重发时 seq=0 到 seq=30:`seq < m_esp32ExpectedSeq(31)` → 重复包跳过
- 重发时 seq=31:`seq == expectedSeq(31)` → 写入成功,`expectedSeq` 变为 32
- 触发 ACK,`m_esp32Received = 32 × 480 = 15360`,匹配期望值
- 设备日志出现 `ESP32 OTA 重复包,跳过: seq=X expected=Y`
### T-17 窗口中间丢包恢复(严格顺序检测验证)
| 项目 | 说明 |
|------|------|
| 场景 | 窗口 0(seq 0–31),其中 seq=15 在首次发送时丢失,seq=16–31 正常到达 |
| 预期 | 乱序包被拒绝,重发后缺失包被填补 |
**验证要点:**
- 首次发送:seq=0–14 写入成功,`m_esp32ExpectedSeq = 15`
- seq=15 丢失
- seq=16 到达:`seq(16) > expectedSeq(15)` → **乱序包跳过**,设备日志出现 `ESP32 OTA 乱序包,跳过: seq=16 expected=15 (前序包丢失)`
- seq=17–31 同样被跳过(乱序)
- seq=31 被跳过但仍触发 ACK(因 `(seq+1)%32==0`),ACK 回报 `m_esp32Received = 15 × 480 = 7200`
- 主机检测到 7200 < 15360,触发重发
- 重发:seq=0–14 跳过(重复),seq=15 写入(`expectedSeq` 变为 16),seq=16–31 写入
- 最终 `m_esp32Received = 15360`,匹配期望值
- 最终刷写成功,固件数据完整
### T-18 窗口内多包丢失跨轮次恢复
| 项目 | 说明 |
|------|------|
| 场景 | 窗口内 seq=5 和 seq=20 同时丢失 |
| 预期 | 第 1 次重发填补 seq=5,第 2 次重发填补 seq=20 |
**验证要点:**
- 首次:seq=0–4 写入,seq=5 丢失,seq=6–31 被拒绝(乱序),ACK 报 2400 字节
- 第 1 次重发:seq=0–4 跳过,seq=5 写入,seq=6–19 写入,seq=20 丢失,seq=21–31 拒绝,ACK 报 9600 字节
- 第 2 次重发:seq=0–19 跳过,seq=20 写入,seq=21–31 写入,ACK 报 15360 字节
- 2 次重试内完成(`MAX_WINDOW_RETRIES = 2`),刷写成功
---
## 四、性能与回归测试
### T-19 包间延迟对总刷写时间的影响
| 项目 | 说明 |
|------|------|
| 测量 | 记录完整刷写耗时(修改前 vs 修改后) |
| 预期 | 每窗口增加 ~62ms(31 × 2ms),64 窗口约增加 ~4 秒,可接受 |
**参考计算:**
- 2035 包 ÷ 32 包/窗口 ≈ 64 窗口
- 每窗口 31 个间隔 × 2ms = 62ms
- 总额外延迟 ≈ 64 × 62ms ≈ 3.97 秒
- 总刷写时间预计从 ~30 秒增加到 ~34 秒
### T-20 连续多次刷写无内存泄漏
| 项目 | 说明 |
|------|------|
| 操作 | 连续刷写 5 次(每次完整流程) |
| 预期 | 设备 free heap 无明显下降 |
**验证要点:**
- `ble_esp32_ota_begin` 中旧会话清理完整(abort + mutex release + 状态重置)
- `m_esp32ExpectedSeq` 每次重置,不会累积
- 收尾任务(`ble_esp32_finish_and_reboot_task`)正常删除
### T-21 刷写完成后版本验证
| 项目 | 说明 |
|------|------|
| 操作 | 刷写完成 → 设备重启 → 重新 BLE 连接 → 读取版本号 |
| 预期 | 版本号与新固件匹配 |
**验证要点:**
- `esp_ota_set_boot_partition` 指向新分区
- 设备从新分区启动
- `readVersion` 返回 `version.h` 中定义的版本号
---
## 五、修改文件清单
| 文件 | 修改内容 |
|------|----------|
| `main/server/ble_ota.h` | 新增 `m_esp32ExpectedSeq` 字段,用于跟踪下一个期望的包序号 |
| `main/server/ble_ota.cpp` | `ble_esp32_ota_begin` 重置期望序号;DATA_IN 回调添加严格顺序检测(拒绝重复包 + 拒绝乱序包) |
| `tools/ble_ota.html` | 包间 2ms 延迟;整窗口重传重试(最多 2 次,50ms 重发前等待);FINISH 错误事件立即 reject ackWaiter |
## 六、测试优先级
| 优先级 | 用例 | 说明 |
|--------|------|------|
| P0 | T-01, T-10, T-12, T-16, T-17 | 核心功能回归 + 两个修复的直接验证 + 丢包恢复场景 |
| P1 | T-03, T-09, T-13, T-15, T-18 | 边界场景 + 恢复路径 + 多包丢失 |
| P2 | T-05, T-06, T-07, T-11, T-14 | 极端边界 + 重试耗尽 |
| P3 | T-02, T-04, T-08, T-19, T-20, T-21 | 回归 + 性能 + 长期稳定性 |