- 启用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命令常量定义
317 lines
12 KiB
Markdown
317 lines
12 KiB
Markdown
# 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 | 回归 + 性能 + 长期稳定性 |
|