《HarmonyOS技术精讲-蓝牙》BLE数据通信:GATT读写与通知

《HarmonyOS技术精讲-蓝牙》BLE数据通信:GATT读写与通知 从一个常见问题说起做 BLE 开发很多人上来就找writeCharacteristic和setNotifyCharacteristic的 API 调用。官方示例确实能跑起来扫描 - 连接 - 发现服务 - 读写。但放到实际项目里两个问题立刻冒出来状态管理混乱——GATT 回调是异步的多个操作串行执行时开发者经常搞不清当前处于哪个阶段。生命周期问题——页面销毁后蓝牙会话还在回调还在直接崩溃或者数据错乱。这篇内容就是针对这几个真实开发痛点把 GATT 通信的完整链路梳理清楚。它解决什么问题BLE GATTGeneric Attribute Profile是低功耗蓝牙的设备间数据交换协议。简单说它定义了设备和手机之间的数据组织方式和交互规则。核心概念就几个Service一组功能相关的数据集合。比如心率服务0x180D电池服务0x180F。Characteristic服务中的具体数据点有读、写、通知三种操作。Descriptor特征的描述信息常用的是通知开关0x2902。你的设备可能包含多个 Service每个 Service 含多个 Characteristic。应用的通信过程就是发现这些 Characteristic然后读写它们的数据或者订阅通知实时接收。适合的场景智能穿戴、传感器数据采集、遥控器、门锁等。不适合的场景大数据量传输BLE 带宽有限持续传输会发热掉电。环境说明DevEco Studio 版本DevEco Studio NEXT Developer Beta2 及以上 HarmonyOS SDK 版本HarmonyOS API 12 及以上 目标设备支持 BLE 的手机 / 平板 开发语言ArkTS核心实现假心率传感器客户端下面一步步实现一个完整的 BLE 数据交互流程。假设远端有一个模拟心率传感器设备提供心率0x2A37和体表温度0x2A6C两个特征并支持通知。1. 申请权限HarmonyOS 的 BLE 服务需要网络位置权限和蓝牙权限。在module.json5中添加requestPermissions:[{name:ohos.permission.ACCESS_BLUETOOTH},{name:ohos.permission.ACCESS_FINE_LOCATION},{name:ohos.permission.BLUETOOTH_SCAN},{name:ohos.permission.BLUETOOTH_ADVERTISING},{name:ohos.permission.BLUETOOTH_CONNECT}]注意从 API 12 开始ACCESS_BLUETOOTH和ACCESS_FINE_LOCATION是必要权限缺少会导致扫描不到设备或连接失败。2. 扫描并连接设备import{ble,ErrorCode}fromkit.ConnectivityKit// 定义假设备的服务 UUIDconstHEART_RATE_SERVICE_UUID0000180d-0000-1000-8000-00805f9b34fbconstHEART_RATE_CHAR_UUID00002a37-0000-1000-8000-00805f9b34fbconstTEMPERATURE_CHAR_UUID00002a6c-0000-1000-8000-00805f9b34fb// 设备 IDlettargetDeviceId:string// 开始扫描functionstartScan(){ble.startBLEScan({interval:500,window:250,success:(){console.info(BLE scan started)},fail:(err:Error){console.error(Scan start failed:,err.code,err.message)}})}// 监听扫描结果ble.on(BLEFound,(result:Arrayble.ScanResult){for(letdeviceofresult){// 过滤假心率设备这里假设设备名包含 HeartSensorif(device.deviceName?.includes(HeartSensor)){targetDeviceIddevice.deviceIdconsole.info(Found target device:,targetDeviceId)// 停止扫描开始连接ble.stopBLEScan()connectDevice()}}})这一步关键点startBLEScan的interval和window不要太小否则耗电且扫描不稳定。收到BLEFound事件时设备列表可能包含重复项建议用一个Set去重。必须确认targetDeviceId非空后再调用连接否则容易报-1错误。3. 连接并发现服务import{ble}fromkit.ConnectivityKitfunctionconnectDevice(){if(!targetDeviceId){console.error(No target device to connect)return}constdeviceble.createGattClientDevice(targetDeviceId)if(!device){console.error(Failed to create GATT client)return}// 连接device.connect().then((){console.info(Connected to device:,targetDeviceId)// 连接成功后发现服务returndevice.discoverServices()}).then((){console.info(Services discovered)// 读取特征值readHeartRate(device)readTemperature(device)// 注册通知subscribeNotifications(device)}).catch((err:Error){console.error(BLE session failed:,err.code,err.message)})}这里有一个常见陷阱connect()和discoverServices()都是异步操作。如果直接在 connect 的 then 里调 discover没问题。但如果你把 device 对象存起来反复使用必须确保同一个 device 实例。我见过不少人在页面切换时重新创建 device导致回调全失灵。4. 读写特征值asyncfunctionreadHeartRate(device:ble.GattClientDevice){try{constvalue:Uint8Arrayawaitdevice.readCharacteristicValue({serviceUuid:HEART_RATE_SERVICE_UUID,characteristicUuid:HEART_RATE_CHAR_UUID,})// 心率数据第一个字节是标志位第二个字节是8位心率值constheartRatevalue[1]console.info(Heart Rate:,heartRate)}catch(err){console.error(Read heart rate failed:,err.code,err.message)}}asyncfunctionreadTemperature(device:ble.GattClientDevice){try{constvalue:Uint8Arrayawaitdevice.readCharacteristicValue({serviceUuid:HEART_RATE_SERVICE_UUID,characteristicUuid:TEMPERATURE_CHAR_UUID,})// 温度数据32位 float小端序lettemp0// 这里做简单处理实际需要根据具体协议解析if(value.length2){temp(value[1]8|value[0])/100.0}console.info(Temperature:,temp)}catch(err){console.error(Read temperature failed:,err.code,err.message)}}标志位处理心率特征的数据格式在 BLE 规范中有明确定义第一个字节是标志位需要按位判断是否包含额外字段。字节序BLE 默认小端序Little-Endian这是最容易犯的错误。比如 16 位整数value[0]是低 8 位value[1]是高 8 位。5. 注册通知functionsubscribeNotifications(device:ble.GattClientDevice){// 先写通知开关DescriptorconstenableNotify:Uint8ArraynewUint8Array([0x01,0x00])// 开启通知device.writeCharacteristicValue({serviceUuid:HEART_RATE_SERVICE_UUID,characteristicUuid:HEART_RATE_CHAR_UUID,value:enableNotify,// 注意写通知开关实际上是写 Descriptor但 API 统一用 writeCharacteristicValue// 部分设备需要写 0x02 表示指示indicatewriteType:ble.GattWriteType.WRITE_REQUEST}).then((){console.info(Notification enabled)// 注册回调device.on(BLECharacteristicChange,(result:ble.BLECharacteristicChangeResult){constcharUuidresult.characteristicUuidconstdataresult.valueif(charUuidHEART_RATE_CHAR_UUID){constheartRatedata[1]console.info(Notify Heart Rate:,heartRate)// 这里通知 UI 更新}})}).catch((err:Error){console.error(Enable notify failed:,err.code,err.message)})}这个writeCharacteristicValue写的是 Client Characteristic Configuration Descriptor0x2902。官方文档在这里描述得不够细写通知开关时value不要传new Uint8Array([1])要传两个字节格式[0x01, 0x00]。只传[0x01]在某些设备上会失效。6. 完整入口文件// Index.etsimport{ble}fromkit.ConnectivityKitEntryComponentstruct Index{StateheartRate:number0Statetemperature:number0privatedevice:ble.GattClientDevice|nullnullbuild(){Column(){Text(心率: this.heartRate)Text(体温: this.temperature)Button(开始扫描).onClick((){this.startScan()})}.padding(20)}startScan(){// 复用前面的扫描代码// 在 this 上绑定 device}}常见问题问题 1createGattClientDevice后connect不触发任何回调现象connect()的 then 和 catch 都不执行。原因最常见的是设备已断开或设备正在被其他应用连接。HarmonyOS 的GattClientDevice对象本身生命周期短如果你在页面重建时重新 new 一个对象旧对象已失效。解决方案建议把device作为单例管理或者在onPageHide中主动调用disconnect()和destroy()。问题 2writeCharacteristicValue总是返回-1现象写通知开关时writeCharacteristicValue直接走 fail 回调错误码-1含义是参数错误。原因writeCharacteristicValue的参数中value的长度和格式必须严格匹配。写通知开关时value长度必须为 2 字节。另外某些特征不支持写操作需要先检查属性。解决方案用getCharacteristicValue获取特征属性确认Write标志位。问题 3一台设备重复触发BLEFound现象扫描时同一个设备会被多次回调。原因这是 BLE 标准行为。设备在不同信道广播手机收到多次。解决方案用Set或Map根据deviceId去重收到后立即stopBLEScan。最佳实践不要在 build() 中创建 GattClientDevice 对象——ArkUI 的 build 方法会频繁执行创建的对象会泄漏而且每次都是新实例回调会丢失。建议在aboutToAppear()中初始化单例管理。页面销毁前必须清理蓝牙资源——在onPageHide或aboutToDisappear()中调用device.disconnect()和device.destroy()。否则可能导致后续页面启动时BLE 回调还在执行状态错乱。通知回调中不要直接修改 State——ArkUI 的State更新必须在主线程。通知回调一般发生在子线程或专用线程中直接用可能会导致 UI 不刷新或崩溃。建议用runOnMainThread或emit事件转一发。import{basic}fromkit.ArkUI// 在通知回调中if(charUuidHEART_RATE_CHAR_UUID){basic.runOnMainThread((){this.heartRatedata[1]})}UUID 要用标准格式——HarmonyOS 的 BLE API 接受 16 位或 128 位 UUID。如果是 16 位如0x180D需要补全为标准 128 位格式否则会找不到服务。这一点官方文档写得比较隐晦。FAQQ为什么真机可以正常发现服务模拟器不行A模拟器不支持真实的蓝牙硬件和 BLE 广播。模拟器上的startBLEScan返回空列表是正常的。建议始终用真机调试。Q为什么页面返回后重新扫描连不上之前连接过的设备A很可能是因为旧GattClientDevice对象没有destroy。在你重新扫描时系统认为设备已被占用。确保在页面销毁前调用device.disconnect()和device.destroy()。Q为什么设置通知后设备却一直不发送数据A分两种情况设备需要收到特定的写命令后才开始发送数据比如写入一个启动命令。通知开关写错了。检查new Uint8Array([0x01, 0x00])如果设备期望的是指示Indicate应该用[0x02, 0x00]。Qon(BLECharacteristicChange)注册后没收到回调A检查两点确保setNotifyCharacteristic写通知开关已经成功执行。确保回调注册在setNotify的 then 中而不是之前。这篇内容覆盖了 BLE GATT 通信的完整链路扫描、连接、发现服务、读写特征、注册通知。如果你在实际项目中遇到具体的异常行为建议先检查日志中的错误码最典型的就是-1参数错误和-5连接断开。