# 统一 BLE 连接与控制流程(ED713 / ED719) ## 平台注入层(2026-09 迁移新增) BLE 运行时调用统一收敛:`createBleCore({ platform })` 接受平台对象(默认 Taro,见 `src/ble/adapter/index.ts`),单元测试可注入 mock;`createBluetoothDiscovery`(`src/ble/discovery.js`)原样保留全部能力(扫描过滤/去重/重启/Android 定位权限/连接/鉴权/WiFi/雷达),仅 3 处 uni.* 换为 Taro.*。React 页面通过 `useBle()`(`src/ble/hooks/useBle.ts`)消费——工厂回调转 state,方法原样透出。 ## 1. 标准连接流程图(文字版) 1. 初始化阶段 - `openBluetoothAdapter` - 注册监听:`onBluetoothAdapterStateChange`、`onBLEConnectionStateChange`、`onBLECharacteristicValueChange` 2. 搜索阶段 - 优先过滤扫描:`startBluetoothDevicesDiscovery({ services:[0x00F4] })` - 超时无结果则无过滤兜底扫描 - 选择目标设备 `deviceId` 3. 连接与发现阶段 - `createBLEConnection(deviceId)` - `getBLEDeviceServices`,定位服务 `0x00F4` - `getBLEDeviceCharacteristics`,识别 `F401/F402/F403/F501/F301/F302` - 订阅通知:`notify(F301)=true`、`notify(F302)=true` 4. 标准业务阶段 - 读取设备信息:`read(F501)` - 鉴权:`write(F402, A7分包key)`,监听 `F301` 状态码 - 当前项目约定:**已知密钥 = 设备 MAC 去分隔符后 + `0000`**,例如 `10:52:1C:81:64:F4` → `10521C8164F40000` - 鉴权成功后进行功能操作: - WiFi 配网:`write(F401, A7分包ssid|pass)` - 控制命令:`write(F403, [cmd])` - 雷达数据:`cmd=0xA1` 开始、`cmd=0xA2` 停止,解析 `F302` 5. 收尾阶段 - 停止扫描 / 断开连接 / 关闭适配器 - 清理监听器与状态 ## 2. 目录结构 ```text src/ble/ core/ bleCore.js # 蓝牙核心能力(连接/发现/读写/订阅/重连) controller.js # 标准流程编排(scan->connect->discover->auth) errors.js # 统一异常 eventBus.js # 回调分发 packet.js # A7 分包工具 retry.js # 重试工具 state.js # 状态模型 modules/ auth.js # 鉴权流程(读MSG + 写KEY) wifi.js # WiFi配置流程 radar.js # 雷达控制流程 parsers/ commonParser.js # MSG/STATE/雷达通用解析 index.js # 解析聚合出口 protocols/ profiles.js # ED713/ED719/UNKNOWN profile 差异配置 utils/ bytes.js # 字节/高低位工具 hex.js # hex/utf8 转换 uuid.js # UUID 标准化与比较 index.js # 统一出口 ``` ## 3. 公共方法列表 - 蓝牙核心 - `createBleCore(options)` - `openAdapter/getAdapterState/closeAdapter` - `startDiscovery/stopDiscovery` - `connect/disconnect` - `discoverServices/discoverCharacteristics` - `readCharacteristic/writeCharacteristic/notifyCharacteristic` - 流程编排 - `createUnifiedBleController(options)` - `standardConnectFlow({ deviceId, key })` - `scanAndPickDevice/connectAndDiscover/cleanup` - 业务模块 - `createAuthModule(ble).ensureAuthorized(key)` - `createAuthModule(ble).ensureAuthorizedByMac({ suffix: '0000' })` - `createWifiModule(ble).configureWifi(ssid, password)` - `createRadarModule(ble).startRadarStream()/stopRadarStream()/writeFallParams()` - 工具 - `buildA7Packets(payload, packetConfig)` - `bytesToHex/hexToBytes/utf8ToBytes/bytesToUtf8` - `splitToLowHigh/joinLowHigh/readUint16LE/readUint32LE` - `normalizeUuid/toFullUuid/uuidEquals` ## 4. 核心流程代码示例 ```js import { createUnifiedBleController, DEVICE_PROFILE } from '@/ble' const bleController = createUnifiedBleController({ profileId: DEVICE_PROFILE.UNKNOWN, reconnect: { enabled: true, retries: 2, delay: 1000 } }) async function runStandardFlow({ key, ssid, password }) { try { // 1) 连接 + 服务发现 + 自动订阅F301/F302 + 鉴权 const session = await bleController.standardConnectFlow({ key }) // 2) 读取设备信息(如需要可再次读取) const info = await bleController.auth.readDeviceInfo() // 3) WiFi 配置 const wifiResult = await bleController.wifi.configureWifi(ssid, password) // 4) 雷达控制 await bleController.radar.startRadarStream() return { session, info, wifiResult } } finally { // 页面退出或流程结束时清理 await bleController.cleanup() } } ``` ## 5. ED713/ED719 兼容策略 - 相同 UUID 与鉴权主流程统一抽象。 - 差异通过 `profiles.js` 配置: - `radar.notifySignature`: ED713=`0x7c`,ED719=`0x67` - `cmd.supportsNarrowMode`: ED713=true - `cmd.supportsFallParam67`: ED719=true - 未知设备先用 `UNKNOWN`,收到 `F302` 数据后按签名字节自动识别 profile。 ## 6. 修改记录 ### 2026-09-10 恢复过滤扫描优先 + 搜索排障日志 #### src/ble/discovery.js - 回退 2026-09-09 的"放弃过滤扫描改直接全扫"方案:真机验证该机型下无过滤扫描反而搜不出设备(修改前过滤扫描正常),存在机型兼容差异;恢复 `DISCOVERY_FILTER_SERVICE_UUIDS` 过滤扫描优先 + `fallbackToUnfilteredDiscoveryIfNeeded` 4s 兜底切全扫的原始策略,兜底切换日志(`[BLE][SCAN] filtered scan found nothing, fallback to unfiltered`)可在 vConsole 确认切换是否发生 - `upsertDevice` 返回布尔(是否新命中),事件通道(`device:found`)与轮询通道(`getBluetoothDevices`)均输出排障日志:`[BLE][SCAN] native devices=N newHits=M`(原生采集总量 vs JS 匹配命中量)、`[BLE][FOUND] event devices=N newHits=M`。真机排障按三级定位: - `native devices=0` → 原生层没采到任何设备(权限/适配器/机型问题) - `native devices>0, newHits=0` → 采到了但 JS 匹配规则全没命中(设备广播内容与关键词不符) - 无任何 `[BLE][SCAN]` 日志 → 扫描流程未走到(初始化/会话状态问题) - 启动扫描处输出 `[BLE][SCAN] filtered discovery started` / 过滤启动失败转全扫日志,均带 sessionId #### src/constants/bluetooth.js - 恢复 `DISCOVERY_FILTER_SERVICE_UUIDS` 导出(三个 128 位完整 UUID),供过滤扫描使用 ### 2026-09-09 Taro 迁移后交互卡顿修复 #### src/ble/discovery.js - `stopSearch`: `setSearching(false)` 提前到 `stopBluetoothDevicesDiscovery` 之前同步执行——部分机型原生停扫回调慢,等回调才翻转状态会让按钮长时间停在"搜索中"并串行阻塞连接等后续操作;`searchSessionId` 已递增、`acceptingDeviceEvents` 已关闭,先翻转状态安全 - `connectDevice`: 由无条件 `stopSearch(false)` 改为仅在 `isSearching` 时停搜索,避免每次连接都白调一次原生停扫接口 - 设备列表 UI 刷新加 300ms 节流(`emitDeviceListThrottled`):`allowDuplicatesKey=true` 下目标设备广播包每秒到达数次且 RSSI 逐包变化,签名判重只拦"完全相同"的包;不节流时每包都触发 `onDeviceListChange` → React setState → 全列表重渲染,搜到设备后渲染线程被打满、点击排队卡死(表现为"搜到设备就无法停搜索")。数据仍实时合入 `deviceMap`,仅通知 UI 频率受限;`resetDeviceMap`/`stopSearch` 绕过节流立即冲刷,`cleanupDiscovery` 补清节流定时器 - 放弃 services 过滤扫描,`startSearch` 直接无过滤全频段扫描:微信 `startBluetoothDevicesDiscovery` 的 services 参数只按 128 位完整 UUID 匹配,目标设备广播的是 16 位短 UUID(0x00F4 等),部分机型/基础库版本下过滤扫描收不到这些设备的广播(表现为一直搜不出,或等 4s 兜底切全扫后才出现)。事件通道(`onBluetoothDeviceFound`)+ 轮询通道(`getBluetoothDevices`)双路取数,过滤交给 JS 层 `matchTargetDevice`(服务 UUID/名称关键词/广播包关键词三路匹配)。删除 `fallbackToUnfilteredDiscoveryIfNeeded`/`FILTER_SCAN_TIMEOUT_MS`/`DISCOVERY_FILTER_SERVICE_UUIDS` #### src/pages/device/ble-config.tsx - 补 `useDidHide` 停搜索:迁移版原只有 useEffect 卸载清理,`navigateTo` 进配网页/切后台时组件不卸载,扫描持续空转(对齐旧版 onHide/onUnload 双触发) - `prepareWifiConfig` 包 60s `Promise.race` 超时兜底:`createBLEConnection` 部分机型失败后不回调、`discoverServices`/`notify` 无原生超时,不设兜底会导致 loading 永不消失、按钮永久禁用 - 连接失败分支补 `disconnectDevice()` 清 OS 层残留连接(对齐旧版 safeDisconnect),避免"已连接(10010)"卡死后续重试;成功分支补回"设备已连接,请填写WiFi信息"提示 ### 2026-04-30 对照协议文档第二轮修复 #### commonParser.js - `parseDeviceInfo(payload, profileId)`: 重写为按 ED713/ED719 分路解析 - ED713:MAC[0:6] + bindStatus[6] + 其它[7:10] + productId[10:12] + baseBoard[12] + coreBoard[13] + 其它[14:16] + deviceType[16] + wifiStatus[17] + cellularStatus[18] - ED719:布局与 ED713 一致(bindStatus/productId 偏移相同,文档仅省略标注),复用 `parseEd713DeviceInfo` - `parseEd719Radar`: 修正 `shieldNum` 从 bytes[27] → bytes[24](按文档结构体偏移累加 header[4]+event+people+num+nodata+height+mode+beeper+left+right+front+back+sensitive+nodata1+stateDelay=24) - 新增 `wallOption`、`peopleHaveSwitch`、`peopleActivitySwitch` 字段解析 #### radar.js - `setNarrowMode(enabled)`: 修正命令格式,从单字节 `0x7C`/`0x7D` 改为两字节 `[0x7C, flag]`,flag 0=关 1=开(符合协议文档 CMD=0x7C + 1byte参数) #### auth.js - `readDeviceInfo(options)`: 新增 `msgTimeout` 参数与 4 秒超时保护,防止 UUID_MSG 无回包时页面挂死 - `readDeviceInfo()`: 调用 `parseDeviceInfo` 时传入 `ble.getProfile().id` - `ensureAuthorized`/`ensureAuthorizedByMac`: 透传 `options` 到 `readDeviceInfo` - `writeKeyAndExpectStates`: `rejectOnUnexpected` 从 `true` 改为 `false`,避免设备中间态(-1/0)直接 reject 阻断鉴权 - `ensureAuthorizedWithDeviceInfo`: 移除 `bindStatus === null` 分支(ED719 实际布局与 ED713 一致,bindStatus 可正常读取) #### controller.js - 特征分发器 `createCharacteristicDispatcher` 中 `parseDeviceInfo` 调用传入 `profile.id` #### bleCore.js - `closeAdapter()`: 新增重置 `adapterStateHandler`/`connectionStateHandler`/`characteristicValueHandler`/`deviceFoundHandler` 为 `null`,确保 `openAdapter` 后 `init()` 可重新注册所有原生监听 #### pages/device/config.vue - `openWifiPanel`: `prepareWifiConfig` 超时从 `800000` 改为 `8000`,新增 `msgTimeout: 4000` - `wifiPanelVisible = true` 后加 `await this.$nextTick()` 确保 Vue DOM 更新后再调原生 toast #### hooks/useBluetoothDiscovery.js - 新增 `SCAN_DURATION_MS = 30000`,蓝牙搜索 30 秒后自动停止 - `startSearch`: 搜索开始前同步设置 `isSearching = true`(避免异步导致无法暂停);先 `stopDiscoverySilently()` 再 `resetDeviceMap()`(新搜索刷新数据);启动 30 秒自动停止定时器 - `startSearch`: `stopDiscoverySilently()` 后执行完整适配器重置(`unwatch*` 注销所有原生监听 → `closeAdapter` 关闭适配器 → `initialized = false`),由 `ensureInitialized` 重新打开并注册全部监听,确保每次搜索都从干净适配器状态开始,修复二次搜索找不到设备的问题 - `stopSearch`: 移除 `resetDeviceMap()` 调用,暂停搜索时保留已发现设备数据;清除自动停止定时器 ### 2026-04-29 对照协议文档修复 (初次修复,记录同上,已被 2026-04-30 条目覆盖)