HarmonyOS7 蓝牙通信:BLE 连接智能硬件的完整流程
文章目录前言BLE 基础概念权限声明扫描设备连接与发现服务读写特征值断开与重连写在最后前言去年做一个智能体脂秤的 App第一次搞 BLE 蓝牙通信整个人被折磨得够呛。扫描扫不到设备、连上了读不出数据、写命令没反应——每个环节都是坑。文档也帮不了太多因为 BLE 的概念太多GATT、Service、Characteristic、Descriptor 这堆术语把人绕晕了。折腾了两周终于跑通了全流程现在回头看其实就那几步。今天把 BLE 通信的完整流程从头到尾讲一遍帮你少走弯路。BLE 基础概念先搞清楚几个核心术语不然后面代码看着会懵术语说明类比BLE低功耗蓝牙Bluetooth Low Energy省电版蓝牙GATT通用属性协议定义数据传输规范数据库的表结构Service一组相关特征的集合数据库里的表Characteristic具体的数据值可读/可写/可通知表里的字段Descriptor特征的描述信息配置通知开关等字段的元数据UUID服务和特征的唯一标识表名和字段名|RSSI| 信号强度负值越小信号越强 | Wi-Fi 信号格数 |理解 GATT 结构是关键一个 BLE 设备 → 包含多个 Service → 每个 Service 包含多个 Characteristic → 读写操作都在 Characteristic 上进行。类比BLE 设备就是一个数据库Service 是表Characteristic 是字段。你要读数据就 SELECT要写就 UPDATE要监听变化就 WATCH。权限声明BLE 操作需要蓝牙权限在module.json5里配置{requestPermissions:[{name:ohos.permission.ACCESS_BLUETOOTH,reason:$string:bluetooth_reason,usedScene:{abilities:[EntryAbility],when:inuse}}]}ohos.permission.ACCESS_BLUETOOTH—— BLE 扫描、连接、读写都需要这个权限必须声明usedScene和reason否则审核过不了这个权限是用户授权的第一次调 BLE API 时系统会弹窗扫描设备BLE 通信的第一步是扫描周围的设备import{ble}fromkit.ConnectivityKit;import{BusinessError}fromkit.BasicServicesKit;EntryComponentstruct BLEScanDemo{StatedeviceList:ble.ScanResult[][];privatescanTimer:number0;startScan(){// 1. 监听扫描结果ble.on(BLEDeviceFind,(results:ble.ScanResult[]){// 去重同一个设备只保留信号最强的记录for(constdeviceofresults){constidxthis.deviceList.findIndex(dd.deviceIddevice.deviceId);if(idx-1){this.deviceList.push(device);}elseif(device.rssithis.deviceList[idx].rssi){this.deviceList[idx]device;// 更新为信号更强的记录}}});// 2. 配置扫描参数constscanOptions:ble.ScanOptions{interval:500,// 扫描间隔 msdutyMode:ble.ScanDuty.SCAN_MODE_LOW_POWER,// 低功耗模式matchMode:ble.MatchMode.MATCH_MODE_AGGRESSIVE,// 宽松匹配};// 3. 启动扫描try{ble.startBLEScan(null,scanOptions);}catch(err){console.error(扫描启动失败:${(errasBusinessError).message});}// 4. 设置超时自动停止重要别一直扫this.scanTimersetTimeout((){this.stopScan();},10000);// 10 秒后停止}stopScan(){ble.off(BLEDeviceFind);// 取消监听ble.stopBLEScan();// 停止扫描clearTimeout(this.scanTimer);}build(){Column(){Button(开始扫描).onClick(()this.startScan())Button(停止扫描).onClick(()this.stopScan())List(){ForEach(this.deviceList,(device:ble.ScanResult){ListItem(){Row(){Text(device.deviceName||未知设备).fontWeight(FontWeight.Bold)Text(RSSI:${device.rssi}).fontColor(#999999)}.width(100%).padding(12)}})}}}}关键代码讲解ble.on(BLEDeviceFind, callback)—— 注册扫描结果监听设备会被重复上报必须去重startBLEScan(null, scanOptions)—— 第一个参数是过滤条件null表示扫描所有设备想只扫特定设备可以传ScanFilterSCAN_MODE_LOW_POWER—— 低功耗扫描模式省电但速度慢。需要快速发现设备用SCAN_MODE_LOW_LATENCY10 秒超时自动停止—— 这是最容易忽略的不停止扫描会一直耗电用户手机发烫想只扫特定设备用 ScanFilter{ deviceId: XX:XX:XX:XX:XX:XX }或{ serviceUuid: 你的服务UUID }减少无关设备干扰。连接与发现服务扫描到目标设备后下一步是连接并发现服务import{ble}fromkit.ConnectivityKit;import{constant}fromkit.ConnectivityKit;privategattClient:ble.GattClientDevice|nullnull;privatetargetService:ble.GattService|nullnull;// 连接设备asyncconnectDevice(deviceId:string){try{// 1. 创建 GATT 客户端this.gattClientble.createGattClientDevice(deviceId);// 2. 监听连接状态this.gattClient.on(BLEConnectionStateChange,(state:ble.BLEConnectionChangeState){if(state.stateconstant.ProfileConnectionState.STATE_CONNECTED){console.info(设备连接成功);this.discoverServices();// 连接成功后立刻发现服务}elseif(state.stateconstant.ProfileConnectionState.STATE_DISCONNECTED){console.info(设备断开连接);}});// 3. 发起连接this.gattClient.connect();}catch(err){console.error(连接失败:${(errasBusinessError).message});}}// 发现服务asyncdiscoverServices(){if(!this.gattClient)return;try{constservicesawaitthis.gattClient.getServices();console.info(发现${services.length}个服务);for(constserviceofservices){console.info(Service UUID:${service.serviceUuid});for(constcharofservice.characteristics){console.info(Characteristic:${char.characteristicUuid});console.info(属性: 可读${char.properties.read}可写${char.properties.write}可通知${char.properties.notify});}}// 根据业务需要找到目标 Service// this.targetService services.find(s s.serviceUuid 你的UUID);}catch(err){console.error(发现服务失败:${(errasBusinessError).message});}}关键代码讲解createGattClientDevice(deviceId)—— 用设备 MAC 地址创建客户端一个设备只创建一个 clienton(BLEConnectionStateChange)—— 监听连接状态连接成功后才能发现服务getServices()—— 获取设备所有 Service这一步是异步的要用 awaitchar.properties—— 每个特征值有不同的属性read/write/notify读写前先确认属性支持连接 BLE 设备有个坑不是连上就完了还要getServices()发现服务、再找 Characteristic三步缺一不可。很多人连上了就读数据直接报错。读写特征值找到目标 Characteristic 后就可以读写数据了——这是 BLE 通信的核心。privatereadChar:ble.GattCharacteristic|nullnull;privatewriteChar:ble.GattCharacteristic|nullnull;// 读取数据asyncreadCharacteristic(char:ble.GattCharacteristic){if(!this.gattClient)return;try{constresultawaitthis.gattClient.readCharacteristicValue(char);// result.serviceValue.value 是 ArrayBufferconstdatanewUint8Array(result.serviceValue.value);console.info(读取到数据: [${data.join(, )}]);returndata;}catch(err){console.error(读取失败:${(errasBusinessError).message});}}// 写入数据向设备发送指令asyncwriteCharacteristic(char:ble.GattCharacteristic,data:ArrayBuffer){if(!this.gattClient)return;try{constcharValue:ble.GattCharacteristicValue{serviceUuid:char.serviceUuid,characteristicUuid:char.characteristicUuid,value:data,};awaitthis.gattClient.writeCharacteristicValue(charValue,ble.GattWriteType.WRITE);console.info(写入成功);}catch(err){console.error(写入失败:${(errasBusinessError).message});}}// 订阅通知设备主动推送数据enableNotification(char:ble.GattCharacteristic){if(!this.gattClient)return;// 1. 先订阅通知this.gattClient.on(BLECharacteristicChange,(charChange:ble.CharacteristicChange){constdatanewUint8Array(charChange.characteristicValue.value);console.info(收到通知: [${data.join(, )}]);});// 2. 写入 Descriptor 开启通知constdescriptorschar.descriptors;if(descriptorsdescriptors.length0){constcccDescriptordescriptors.find(dd.descriptorUuid00002902-0000-1000-8000-00805f9b34fb// CCC UUID);if(cccDescriptor){constenableValuenewArrayBuffer(2);newDataView(enableValue).setUint16(0,1,true);// 0x0001 开启通知this.gattClient.writeDescriptorValue({serviceUuid:char.serviceUuid,characteristicUuid:char.characteristicUuid,descriptorUuid:cccDescriptor.descriptorUuid,value:enableValue,});}}}关键代码讲解readCharacteristicValue—— 主动读一次数据适合拉取设备状态writeCharacteristicValue—— 向设备写指令第二个参数WRITE表示不需要响应要确认写入成功用WRITE_NO_RESPONSE改成WRITEBLECharacteristicChange——这是接收设备数据最重要的回调设备主动推送的数据全靠它00002902-0000-1000-8000-00805f9b34fb—— CCCClient Characteristic ConfigurationDescriptor 的标准 UUID写入 0x0001 开启通知0x0000 关闭通知大多数 BLE 硬件体脂秤、心率带、温湿度传感器都是用 Notify 推数据。设备测量完往 Characteristic 写值App 通过BLECharacteristicChange收到。比轮询读取高效得多。断开与重连BLE 连接不稳定断开是常态。处理好断开和重连很重要disconnect(){if(this.gattClient){this.gattClient.off(BLECharacteristicChange);this.gattClient.off(BLEConnectionStateChange);this.gattClient.disconnect();this.gattClient.close();// 释放资源this.gattClientnull;}}// 自动重连setupReconnect(deviceId:string){if(!this.gattClient)return;this.gattClient.on(BLEConnectionStateChange,(state:ble.BLEConnectionChangeState){if(state.stateconstant.ProfileConnectionState.STATE_DISCONNECTED){console.info(连接断开3秒后重连...);setTimeout((){this.connectDevice(deviceId);},3000);}});}几个注意点disconnect()只是断开连接close()才释放底层资源两个都要调断开后要off所有事件监听防止内存泄漏重连加个延迟别立刻重连——设备可能还在处理断开逻辑连续重连失败要设上限别无限重试耗电又没用写在最后BLE 通信的完整流程就五步扫描设备 → 连接设备 → 发现服务 → 读写特征值 → 断开/重连每一步都有坑但核心逻辑就这么清晰。最容易踩的三个坑扫描不停止—— 忘了调stopBLEScan手机发烫连上不发现服务—— 直接读写必然报错通知不开 Descriptor—— 只on了回调没写 CCC收不到数据把这三点记住了BLE 开发能省一半调试时间。有问题评论区聊。