Smart BLE 测试清单
测试环境
- 平台: macOS / iOS / Android / uni-app
- 测试设备: 各种 BLE 设备(iPhone、ESP32、nRF52等)
- 测试日期: _____
1. 设备扫描 (Device Scanning)
1.1 基本扫描
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 开始扫描 | 点击"扫描"按钮 | 按钮变为"停止",状态显示"扫描中" | ⬜ | |
| 停止扫描 | 点击"停止"按钮 | 按钮变回"扫描",停止发现新设备 | ⬜ | |
| 蓝牙关闭时扫描 | 关闭蓝牙后点击扫描 | 提示蓝牙未准备好 | ⬜ | |
| 自动停止 | 启动扫描后等待10秒 | 自动停止扫描 | ⬜ | 可配置 autoStopScanDuration |
注意事项:
- 设备必须立即显示,不能有节流延迟
- 使用
CBCentralManagerScanOptionAllowDuplicatesKey: true允许重复发现以更新RSSI - 每次发现设备后立即调用
applyFilters()更新UI
1.2 设备去重与更新
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 同一设备更新 | 保持扫描,观察同一设备 | RSSI实时更新,设备不重复显示 | ⬜ | |
| 设备数量限制 | 扫描超过100台设备 | 只显示最新的100台 | ⬜ | maxDeviceCount |
2. 设备过滤 (Device Filtering)
2.1 信号强度过滤
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 默认不过滤 | 默认状态(-100) | 显示所有设备 | ⬜ | |
| 过滤弱信号 | 滑块调至-70 | 只显示RSSI >= -70的设备 | ⬜ | |
| 过滤强信号 | 滑块调至-30 | 只显示RSSI >= -30的设备 | ⬜ |
代码注意:
swift
// 只有当 filterRSSI > -100 时才过滤
if filterRSSI > -100 && device.rssi < filterRSSI {
return false
}2.2 名称前缀过滤
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 前缀匹配 | 输入"BLE" | 只显示名称以"BLE"开头的设备 | ⬜ | 大小写不敏感 |
| 清空前缀 | 清空输入框 | 显示所有设备 | ⬜ |
2.3 隐藏无名设备
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 启用开关 | 开启"隐藏无名设备" | 不显示"Unknown Device" | ⬜ | |
| 禁用开关 | 关闭"隐藏无名设备" | 显示所有设备包括无名设备 | ⬜ |
2.4 过滤面板
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 展开/收起 | 点击过滤按钮 | 面板展开/收起有动画 | ⬜ | |
| 实时过滤 | 调整任何过滤条件 | 设备列表立即更新 | ⬜ | 使用 onChange |
3. 设备列表 (Device List)
3.1 设备卡片显示
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 设备名称 | 显示设备名称 | 正确显示,无名显示"Unknown Device" | ⬜ | |
| 设备ID | 显示前8位UUID | 格式正确,带"..." | ⬜ | |
| 信号强度条 | 1-4格绿色条 | 根据RSSI正确显示 | ⬜ | |
| RSSI数值 | 显示"xx dBm" | 数值正确 | ⬜ | |
| 设备类型标签 | 名称含BLE/BT | 显示蓝色标签 | ⬜ | |
| 服务UUID | 有服务UUID时显示 | 最多显示3个 | ⬜ |
3.2 设备排序
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 按信号排序 | 观察设备列表 | 信号强的排在前面 | ⬜ | 使用 sort { $0.rssi > $1.rssi } |
3.3 点击事件
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 点击卡片 | 点击任何设备卡片 | 弹出广播数据详情窗口 | ⬜ | |
| 窗口内容 | 查看弹窗 | 显示完整设备信息和广播数据 | ⬜ |
4. 广播数据弹窗 (Advertisement Modal)
4.1 信息显示
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 设备ID | 查看弹窗 | 显示完整UUID | ⬜ | |
| RSSI | 查看弹窗 | 显示信号强度和评级(极佳/良好/一般/微弱) | ⬜ | |
| 服务UUIDs | 有服务UUID时 | 列出所有服务UUID | ⬜ | |
| 厂商数据 | 有厂商数据时 | 显示十六进制格式 | ⬜ |
4.2 操作功能
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 连接按钮 | 点击"连接" | 开始连接设备 | ⬜ | |
| 复制设备信息 | 点击"复制" | 设备信息复制到剪贴板 | ⬜ | macOS: NSPasteboard |
| 复制广播数据 | 点击广播区的"复制" | 广播数据复制到剪贴板 | ⬜ | |
| 关闭窗口 | 点击"关闭" | 窗口关闭 | ⬜ |
5. 设备连接 (Device Connection)
5.1 连接流程
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 开始连接 | 点击"连接" | 按钮显示"连接中..." | ⬜ | |
| 连接成功 | 设备响应 | 自动切换到"设备"Tab | ⬜ | 使用 onChange(of: connectionState) |
| 自动发现服务 | 连接成功后 | 自动调用 discoverServices() | ⬜ | 在 DeviceDetailView 的 onAppear 中 |
| 停止扫描 | 开始连接时 | 自动停止扫描 | ⬜ | 在 connect() 中调用 stopScan() |
5.2 断开连接
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 主动断开 | 点击"断开" | 设备断开,服务列表清空 | ⬜ | |
| 清空状态 | 断开后 | Notify状态清空 | ⬜ | 调用 clearNotifyStates() |
6. 设备详情页 (Device Detail)
6.1 服务列表
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 服务展开 | 点击服务 | 展开显示特征值列表 | ⬜ | |
| 服务收起 | 再次点击 | 收起特征值列表 | ⬜ | |
| 箭头指示 | 展开/收起 | 箭头方向正确 | ⬜ | ▶/▼ |
6.2 特征值操作
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 读取特征值 | 点击"读取" | 发送读取请求,日志显示 | ⬜ | |
| 写入特征值 | 点击"写入" | 弹出写入对话框 | ⬜ | |
| Notify开关 | 点击"通知"/"停止通知" | 状态切换,正确显示当前状态 | ⬜ | 必须持久化状态 |
关键代码:
swift
// Notify状态必须持久化追踪
private var notifyingCharacteristics: Set<String> = []
func isNotifying(serviceUUID: String, characteristicUUID: String) -> Bool {
let key = "\(serviceUUID):\(characteristicUUID)"
return notifyingCharacteristics.contains(key)
}
// 连接时清空
func clearNotifyStates() {
notifyingCharacteristics.removeAll()
}7. 数据读写 (Read/Write)
7.1 读取
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| ASCII读取 | 读取有ASCII值的特征值 | 显示字符串 | ⬜ | |
| Hex读取 | 读取二进制数据 | 显示十六进制 | ⬜ |
7.2 写入
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| ASCII写入 | 选择TEXT,输入文本 | 写入成功,日志显示 | ⬜ | |
| Hex写入 | 选择HEX,输入"FF 00 AA" | 写入成功,日志显示 | ⬜ | 必须支持 |
| 空输入 | 不输入数据点击写入 | 按钮禁用 | ⬜ |
Hex转换代码:
swift
extension Data {
init(hex: String) {
let cleanHex = hex
self.init()
var index = cleanHex.startIndex
while index < cleanHex.endIndex {
let nextIndex = cleanHex.index(index, offsetBy: 2)
if let byte = UInt8(cleanHex[index..<nextIndex], radix: 16) {
append(byte)
}
index = nextIndex
}
}
}7.3 Notify
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 开启Notify | 点击"通知" | 按钮变为红色"停止通知" | ⬜ | |
| 接收数据 | 设备发送数据 | 日志显示接收到的数据 | ⬜ | |
| 关闭Notify | 点击"停止通知" | 按钮变回蓝色"通知" | ⬜ |
8. 广播功能 (Broadcast/Peripheral)
8.1 广播设置
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 输入名称 | 输入广播名称 | 可以输入 | ⬜ | |
| 输入UUID | 输入服务UUID | 可以输入 | ⬜ | |
| 广播中禁用 | 开始广播后 | 名称和UUID输入框禁用 | ⬜ |
8.2 广播控制
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 开始广播 | 点击"开始广播" | 状态灯变绿,显示"正在广播" | ⬜ | 使用 bleManager.isAdvertising |
| 停止广播 | 点击"停止广播" | 状态灯变灰,显示"未广播" | ⬜ | |
| 其他设备可见 | 用其他设备扫描 | 可以发现此广播 | ⬜ | 重点测试 |
状态同步:
swift
// 广播页必须使用 Manager 的状态
@Published var isAdvertising = false // BLEManager
// 不要使用本地 @State9. 操作日志 (Logs)
9.1 日志显示
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 时间戳 | 查看日志 | 每条都有时间 | ⬜ | |
| 日志类型 | 查看 | 系统读取/写入/接收/错误颜色区分 | ⬜ | |
| 自动滚动 | 新日志到来 | 自动滚动到底部 | ⬜ |
9.2 日志操作
| 测试项 | 步骤 | 预期结果 | 状态 | 备注 |
|---|---|---|---|---|
| 清除日志 | 点击"清除" | 所有日志清空 | ⬜ |
跨平台实现注意事项
macOS/iOS (CoreBluetooth)
- @MainActor: Manager必须标记为@MainActor
- NSPasteboard: 复制功能使用
NSPasteboard.general - TabView切换: 使用
onChange(of: connectionState)自动切换 - 过滤条件: 默认值
-100时不过滤,检查filterRSSI > -100
uni-app
- 权限: 微信小程序需要定位权限
- 写入格式: 已经支持HEX写入(
prepareData函数) - Notify状态: 在characteristic对象上存储
notifying属性
Android (BluetoothAdapter/BluetoothLeScanner)
- 需要运行时权限请求(BLUETOOTH_SCAN, BLUETOOTH_CONNECT)
- 扫描回调在后台线程,需要切换到主线程更新UI
- ClipboardManager用于复制功能
已知问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 设备显示延迟 | 节流机制 | 移除节流,发现后立即更新UI |
| 过滤后无设备 | RSSI默认值问题 | filterRSSI > -100 时才过滤 |
| 广播后其他设备看不到 | 状态同步问题 | 使用 bleManager.isAdvertising 而非本地状态 |
| Notify状态不正确 | 未持久化 | 使用Set存储notifying的key |
测试签名
- 测试人员: _________
- 测试日期: _________
- 平台版本: _________