《HarmonyOS技术精讲-蓝牙》BLE进阶:广播、多连接与功耗优化

《HarmonyOS技术精讲-蓝牙》BLE进阶:广播、多连接与功耗优化 HarmonyOS技术精讲-蓝牙BLE进阶广播、多连接与功耗优化一个常见的开发误区HarmonyOS 的 BLE 开发很多人的第一印象是“官方示例能跑”。但实际把广播、多连接、功耗优化这些能力组合在一起时就会发现 API 组合里的坑比想象的多。官方文档把基础流程讲得很清楚开启蓝牙、扫描、连接、服务发现、读写。但当你需要同时连接两个体温计、一个心率带还要让 App 在后台保持低功耗扫描时文档里没有直接告诉你如何管理这些并发状态。这篇文章会从三个实际使用场景出发自定义广播数据包、多设备并发连接管理、扫描窗口与功耗平衡。每个场景都会给完整代码并且标记出那些容易被忽略的边界情况。本文解决的核心问题问题官方文档覆盖程度实际开发难点广播自定义UUID与厂商数据有基础示例广播包长度限制、周期广播时序同时连接多个设备有基本API连接池管理、回调分发、断连重连扫描功耗优化有参数说明不同业务场景的参数组合策略安全配对与绑定有回调说明绑定状态持久化、设备删除环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机手表蓝牙外设支持GATT的BLE设备核心实现分步骤构建BLE多设备管理框架第一步广播配置与自定义数据广播不仅仅是让设备可见。在实际项目中广播数据里通常需要携带厂商身份标识、服务UUID、设备名称摘要等。HarmonyOS 的BleAdvertiserSettings提供了对广播参数的细粒度控制。先看广播端的完整实现// BleAdvertiserService.etsimport{ble,conform}fromkit.ConnectivityKit;classBleAdvertiserManager{privateadvertiser:ble.BLEAdvertiser|nullnull;privateadvertiserId:number-1;publicstartAdvertising(customData:Uint8Array):void{// 获取BLE广播器实例if(!this.advertiser){this.advertiserble.getBleAdvertiser();}// 配置广播参数constsettings:ble.BleAdvertiserSettings{// 广播间隔单位0.625ms这里设置100msinterval:160,// 发射功率中等txPower:ble.GattTxpwer.TX_POWER_MEDIUM,// 可连接广播connectable:true,// 是否包含设备名称includeDeviceName:false,// 是否包含发射功率建议包含方便外设侧做信号滤波includeTxPowerLevel:true,};// 构造广播数据// Manufacturer Specific Data的格式Company ID (2字节) 自定义数据constcompanyId:Uint8ArraynewUint8Array([0x00,0x01]);constcombinedDatanewUint8Array(companyId.lengthcustomData.length);combinedData.set(companyId,0);combinedData.set(customData,companyId.length);constadvertiseData:ble.AdvertiseData{// 完整的128位服务UUIDserviceUuids:[0000180F-0000-1000-8000-00805F9B34FB],// 制造商自定义数据manufacturerData:[{manufacturerId:0x0001,manufacturerSpecificData:combinedData,}],};// 启动广播this.advertiser.startAdvertising(this.advertiserId,settings,advertiseData,({data,rssi}){console.info(广播回调监听当前RSSI:${rssi});});}publicstopAdvertising():void{if(this.advertiserthis.advertiserId0){this.advertiser.stopAdvertising(this.advertiserId);this.advertiserId-1;}}}这个实现有几个地方值得注意includeDeviceName我设置了false。如果设备名称较长超过8字节加上serviceUuids和manufacturerData广播包很容易超过31字节的上限。实际项目里建议把名称放入扫描响应包scan response里。interval160对应100ms160×0.625ms。更短的间隔比如20ms会让设备更容易被发现但功耗会明显上升。startAdvertising的回调用于接收广播状态的反馈可以用来检测广播是否异常中断。第二步构建多设备连接管理器多连接管理的核心问题不是API调用而是连接状态的跟踪和回调分发。如果让每个页面的组件独立管理连接页面返回后连接状态会丢失而且回调会触发一个已销毁的组件。推荐的做法是用一个单例管理所有GATT连接// GattConnectionManager.etsimport{ble}fromkit.ConnectivityKit;interfaceDeviceConnectionInfo{deviceId:string;deviceName:string;connection:ble.GattConnection|null;state:disconnected|connecting|connected|disconnecting;services:ble.GattService[];}classGattConnectionManager{privatestaticinstance:GattConnectionManager;privateconnections:Mapstring,DeviceConnectionInfonewMap();// 最大同时连接数根据业务调整privatemaxConnections:number3;publicstaticgetInstance():GattConnectionManager{if(!GattConnectionManager.instance){GattConnectionManager.instancenewGattConnectionManager();}returnGattConnectionManager.instance;}publicconnect(device:ble.BLEDevice):void{// 检查连接数上限if(this.getConnectedCount()this.maxConnections){console.error(已达最大连接数(${this.maxConnections}));return;}constinfo:DeviceConnectionInfo{deviceId:device.deviceId,deviceName:device.deviceName||,connection:null,state:connecting,services:[]};this.connections.set(device.deviceId,info);// 建立GATT连接constconnble.gatt.createGattConnection(device.deviceId);conn.on(connect,(deviceId:string){console.info(设备连接成功:${deviceId});constconnInfothis.connections.get(deviceId);if(connInfo){connInfo.stateconnected;connInfo.connectionconn;// 发现服务this.discoverServices(deviceId);}});conn.on(disconnect,(deviceId:string){console.info(设备断开连接:${deviceId});constconnInfothis.connections.get(deviceId);if(connInfo){connInfo.statedisconnected;connInfo.connectionnull;}});conn.connect();}// 分别对每个设备执行读写操作publicasyncreadCharacteristic(deviceId:string,charUUID:string):PromiseUint8Array|null{constconnInfothis.connections.get(deviceId);if(!connInfo||!connInfo.connection){console.error(设备未连接:${deviceId});returnnull;}constconnconnInfo.connection;// 遍历服务查找指定特征for(constserviceofconnInfo.services){constcharacteristicservice.serviceUuid0000180F-0000-1000-8000-00805F9B34FB?service.characteristics.find(cc.characteristicUuidcharUUID):null;if(characteristic){try{constvalueawaitconn.readCharacteristicValue(service,characteristic);console.info(读取成功:${deviceId}, value:${value});returnvalue;}catch(err){console.error(读取失败:${deviceId}, error:${err.message});returnnull;}}}console.warn(未找到特征:${charUUID});returnnull;}privategetConnectedCount():number{letcount0;this.connections.forEach((info){if(info.stateconnected)count;});returncount;}privatediscoverServices(deviceId:string):void{constconnInfothis.connections.get(deviceId);if(!connInfo||!connInfo.connection)return;constconnconnInfo.connection;conn.discoverServices().then((services:ble.GattService[]){connInfo.servicesservices;console.info(设备${deviceId}服务发现完成服务数量:${services.length});});}}exportdefaultGattConnectionManager;这个管理器的设计要点连接池控制maxConnections上限防止同时打开过多连接导致系统资源耗尽。状态机每个设备有独立的state断开、重建、重试的逻辑都有判断依据。回调隔离每个 GATT 连接实例的回调只处理对应 deviceId 的事件不会出现 A 设备的数据跑到 B 设备的回调里。第三步扫描参数调优与功耗平衡扫描是功耗大户。HarmonyOS 的setScanFilter可以在频率层做过滤避免所有广播包都被唤醒 App。但 filter 设得太严格又会漏掉目标设备。下面是一个兼顾功耗和发现成功率的扫描策略// BleScanner.etsimport{ble}fromkit.ConnectivityKit;classBleScanner{privatescanResultCb:ble.BleScanCallback|nullnull;publicstartLowPowerScan(targetServiceUuids:string[]):void{// 扫描过滤器只监听包含指定Service UUID的设备constfilterstargetServiceUuids.map(uuid({serviceUUID:uuid,type:0// 精确匹配}));constsettings:ble.BleScanSettings{// 扫描窗口30ms扫描间隔200ms// 这意味着每200ms中只扫描30ms其余时间休眠scanInterval:400,// 单位0.625ms实际200msscanWindow:60,// 单位0.625ms实际37.5msscanMode:ble.BleScanMode.SCAN_MODE_LOW_POWER,};// 设置过滤器ble.setScanFilter(filters);// 注册扫描结果回调this.scanResultCb{onBleScanResult:(result:ble.BleScanResult){console.info(发现设备:${result.deviceId}, RSSI:${result.rssi});// 只在发现目标服务UUID时才处理if(result.serviceUuids.some(uuidtargetServiceUuids.includes(uuid))){this.handleTargetDevice(result);}}};ble.startBleScan(filters,settings,this.scanResultCb);console.info(低功耗扫描启动scanWindow: 37.5msscanInterval: 200ms);}publicstopScan():void{if(this.scanResultCb){ble.stopBleScan(this.scanResultCb);this.scanResultCbnull;}}privatehandleTargetDevice(result:ble.BleScanResult):void{// 将设备信息传递到UI层或连接管理器console.info(目标设备到达准备连接:${result.deviceName});}}参数选择逻辑业务场景scanInterval(ms)scanWindow(ms)功耗等级发现延迟持续监控如找车10050中1s后台发现如步数同步200050低2-5s快速配对如点击搜索5030高0.5sscanWindow占scanInterval的比例建议控制在10%-20%。比例过高功耗和扫描差异不大比例过低发现延迟会很不可控。常见问题与深层原因分析问题1绑定状态回调bleBondStateChange触发后设备重启需要重新配对现象首次配对成功App退出再打开或设备断电重启后再次连接会要求重新输入配对码。原因绑定信息存储在蓝牙协议栈中但App没有主动保存bondState。HarmonyOS 的bleBondStateChange回调仅在绑定状态发生变化时触发不会在App启动时主动同步已经绑定的设备列表。解法收到BONDED状态时将设备地址和名称写入本地Preferences或数据库。App启动时主动调用ble.getBondedDevices()获取已绑定设备列表然后尝试恢复连接。// 绑定状态回调处理ble.on(bleBondStateChange,(data:{deviceId:string,state:BondState}){if(data.stateBondState.BONDED){// 持久化绑定信息this.saveBondedDevice(data.deviceId);}elseif(data.stateBondState.BOND_NONE){// 绑定解除清理本地记录this.removeBondedDevice(data.deviceId);}});// App启动时检查constbondedDevicesble.getBondedDevices();bondedDevices.forEach(device{// 检查本地是否已连接未连接则自动发起连接if(!this.connectionManager.isConnected(device.deviceId)){this.connectionManager.connect(device);}});问题2广播数据长度超过31字节导致广播启动失败现象startAdvertising调用成功但其他设备扫描不到广播包。或者广播能启动但持续几秒后自动停止。原因BLE广播包ADV_IND的有效载荷最大31字节。如果 serviceUuids16字节/个、deviceName最多8字节超过会截断、manufacturerData 加起来超过31字节系统会静默放弃广播或只发送部分内容。解法使用扫描响应包Scan Response分担数据。广播包放 serviceUUID 和必要标识scan response 放 deviceName 和额外的厂商数据。// 广播数据可以拆分constadvertiseData:ble.AdvertiseData{// 广播包只放一个UUID和制造商IDserviceUuids:[0000180F-0000-1000-8000-00805F9B34FB],manufacturerData:[{manufacturerId:0x0001,manufacturerSpecificData:newUint8Array([0x00,0x01,0x02]),// 3字节标识}],};// Scan Response 放设备名称和扩展信息constscanResponseData:ble.ScanResponseData{deviceName:MyBLEDevice_X001,// 完整名称放这里manufacturerData:[{manufacturerId:0x0001,manufacturerSpecificData:newUint8Array([0x10,0x20,0x30,0x40,0x50]),// 额外数据}],};this.advertiser.startAdvertising(this.advertiserId,settings,advertiseData,scanResponseData,callback);最佳实践总结不要在主线程中处理蓝牙数据。无论是onBleScanResult回调还是readCharacteristicValue返回数据长度可能从几字节到几百字节不等。如果直接用于 UI 更新ArkUI 重新渲染时会造成明显的卡顿。推荐先拷贝数据到共享变量再通过State触发 UI 更新。在页面onPageHide或aboutToDisappear中主动断开未使用的连接。用户离开连接页面后GATT 连接仍然维持。长时间不用的连接不仅耗电还会占用蓝牙协议栈的连接槽位导致新设备无法连接。扫描参数要根据当前页面状态动态调整。用户主动点击“搜索”时用高频扫描window50ms, interval100msApp 后台保活时用低频扫描window20ms, interval1000ms。用一个全局状态变量控制扫描参数的生命周期而不是每次搜索都调用startBleScan。Demo 入口文件// Index.ets完整的页面入口importGattConnectionManagerfrom./GattConnectionManager;import{BleScanner}from./BleScanner;import{BleAdvertiserManager}from./BleAdvertiserService;EntryComponentstruct Index{StatedeviceList:string[][];privatescanner:BleScannernewBleScanner();privateconnectionManager:GattConnectionManagerGattConnectionManager.getInstance();build(){Column(){Button(开始扫描).onClick((){this.scanner.startLowPowerScan([0000180F-0000-1000-8000-00805F9B34FB]);})Button(连接设备).onClick((){// 模拟连接第一个发现的设备if(this.deviceList.length0){constdeviceIdthis.deviceList[0];// 假设已从扫描结果中构造了BLEDevice// this.connectionManager.connect(bleDevice);}})List(){ForEach(this.deviceList,(device:string){ListItem(){Text(device).fontSize(16);}})}}.width(100%).height(100%).onPageHide((){this.scanner.stopScan();})}}FAQQ为什么我的设备扫描到了但连接总是失败A最常见的原因是设备端没有处于可连接广播状态。检查广播参数里connectable是否为 true以及广播间隔是否太短导致设备端来不及处理连接请求。另外部分设备的 GAP 层限制只能接受已配对的设备连接需要先调用ble.pairDevice。Q同时连接3个设备后新设备扫描不到了A蓝牙协议栈有连接数上限不同设备厂商的配置不同。HarmonyOS 基于高通或海思芯片的方案通常支持 7-8 个 BLE 连接。如果扫描不到新设备先检查当前连接数是否已接近上限然后主动断开不活跃的连接。Q为什么 setScanFilter 设置了但还有大量垃圾数据进来AsetScanFilter在部分芯片上有实现差异。有些情况下filter 只作用在应用层物理层仍然会接收所有广播包CPU 仍然会被唤醒。可以考虑在回调里再做一次业务层过滤只处理 serviceUuids 匹配的设备。目前还没有 API 能完全关闭未匹配设备的底层中断。