HarmonyOS技术精讲-Connectivity Kit:蓝牙进阶——GATT服务与音频通道

发布时间:2026/7/19 3:30:10
HarmonyOS技术精讲-Connectivity Kit:蓝牙进阶——GATT服务与音频通道 这个场景你很可能遇到过HarmonyOS NEXT 的蓝牙 API 覆盖了 BLE 和经典蓝牙两个方向但大多数官方示例只展示了最简单的扫描和连接。真到了要读写自定义 GATT 特征值或者控制蓝牙音箱播放/暂停时你就会发现很多细节没有文档说明——比如连接后的状态同步、特征值写入时机、音频焦点冲突等等。这篇文章会直接给你两段可复用的代码一个是连接自定义 BLE 温湿度服务模拟数据的客户端另一个是通过 A2DP Source 控制音箱播放/暂停。代码完全基于 ArkTS 实现在真机上验证过。如果你正在做一个需要和蓝牙外设交互的 App或者想给智能音箱加个遥控功能这几段逻辑可以直接拿过去改。它解决什么问题BLE GATT 服务和经典蓝牙音频通道是 HarmonyOSConnectivity Kit短距通信服务里的两个独立能力。它们解决的是两种完全不同的场景场景使用协议典型设备数据量实时性要求传感器数据读取BLE GATT温湿度计、血压仪小几十字节低秒级音频播放控制经典蓝牙 A2DP蓝牙音箱、耳机大流媒体高毫秒级GATT 适用于自定义服务比如给你的硬件设备定义温度、湿度、电量等特征值App 通过读写这些特征值来获取数据或下发指令。而 A2DP Source 则让你能控制远程音箱的播放状态播放/暂停/下一曲相当于给手机 or 平板装了一个遥控器。不适合的场景不要用 GATT 传大文件那是 L2CAP 或 SPP 的活不要用 A2DP 做实时语音交互那是 HFP 的活。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机 / 平板需支持 BLE 和经典蓝牙核心实现一、BLE 温湿度客户端读取/写入自定义特征值1. 扫描并连接设备假设你的温湿度外设广播了特定的 Service UUID0000FFE0-0000-1000-8000-00805F9B34FB其中包含两个特征值温度UUID:0000FFE1-...和湿度UUID:0000FFE2-...。客户端需要先扫描匹配设备然后建立 GATT 连接。// BLEGattClient.etsimport{ble,connectedDevice}fromkit.ConnectivityKit;constSERVICE_UUID0000FFE0-0000-1000-8000-00805F9B34FB;constTEMP_CHAR_UUID0000FFE1-0000-1000-8000-00805F9B34FB;constHUMID_CHAR_UUID0000FFE2-0000-1000-8000-00805F9B34FB;exportclassBLEGattClient{privatedeviceId:string;privatetempValue:number25.0;privatehumidValue:number60.0;// 扫描并连接asyncconnectToDevice():Promisevoid{// 先扫描constscanFilter:ble.ScanFilter{serviceUuid:SERVICE_UUID};constscanOptions:ble.ScanOptions{interval:500,dutyMode:ble.ScanDuty.SCAN_MODE_LOW_POWER};ble.on(BLEDeviceFind,(data:Arrayble.ScanResult){if(data.length0){constdevicedata[0];this.deviceIddevice.deviceId;ble.stopBLEScan();// 找到后停止扫描this.connectGatt();}});ble.startBLEScan(scanFilter,scanOptions);}privateasyncconnectGatt():Promisevoid{if(!this.deviceId){console.error(No device to connect);return;}try{// 发起 GATT 连接awaitble.gattClient.connect(this.deviceId);console.info(Connected to device:,this.deviceId);awaitthis.discoverServices();}catch(err){console.error(GATT connect failed:,err);}}privateasyncdiscoverServices():Promisevoid{try{// 发现服务constservicesawaitble.gattClient.getServices();consttargetServiceservices.find(ss.serviceUuidSERVICE_UUID);if(!targetService){console.error(Target service not found);return;}// 此时可以开始读写特征值awaitthis.readCharacteristics();awaitthis.writeCharacteristic(0000FFE3-...,newUint8Array([0x01]));// 模拟写数据}catch(err){console.error(Discover services failed:,err);}}}注意ble.gattClient.connect是异步操作连接成功后系统会自动触发ble.gattClient.on(BLEConnectionStateChange)回调。建议在回调中再调用discoverServices这样更安全。上面为了简化直接连后在connectGatt后面直接discoverServices实际项目建议用状态机管理。2. 读取特征值模拟温湿度数据由于外设是模拟数据我们可以在客户端里模拟一个周期性读取并更新 UI 的逻辑。// 读取温度和湿度privateasyncreadCharacteristics():Promisevoid{try{consttempawaitble.gattClient.readCharacteristicValue(TEMP_CHAR_UUID);consthumidawaitble.gattClient.readCharacteristicValue(HUMID_CHAR_UUID);// 将字节数组解析为数值外设按约定格式两个字节表示温度两个字节表示湿度if(temptemp.length2){this.tempValue(temp[0]8|temp[1])/100.0;}if(humidhumid.length2){this.humidValue(humid[0]8|humid[1])/100.0;}console.info(Temp:${this.tempValue}°C, Humidity:${this.humidValue}%);}catch(err){console.error(Read characteristic failed:,err);}}3. 写入特征值控制或配置比如给外设发送一个开启 LED 的指令。publicasyncwriteCharacteristic(charUuid:string,data:Uint8Array):Promisevoid{try{awaitble.gattClient.writeCharacteristicValue(charUuid,data.buffer);console.info(Write success);}catch(err){console.error(Write failed:,err);}}完整代码需要处理writeType带响应 vs 不带响应HarmonyOS 默认WRITE_WITH_RESPONSE。如果外设支持无响应写需要显式设置writeType: ble.WriteType.WRITE_WITHOUT_RESPONSE。二、控制蓝牙音箱播放/暂停A2DP Source经典蓝牙的音频通道控制HarmonyOS 提供了a2dpSource模块可以扮演音源角色连接音箱后下发 AV/C 指令如播放、暂停。// A2dpController.etsimport{a2dpSource,bluetooth}fromkit.ConnectivityKit;exportclassA2dpController{privatesinkDeviceId:string;// 扫描并连接音箱需要是经典蓝牙设备通常已经配对asyncconnectSpeaker(deviceId:string):Promisevoid{try{// 先确认设备是否已配对constpairedDevicesawaitbluetooth.getPairedDeviceInfo();consttargetpairedDevices.find(dd.deviceIddeviceId);if(!target){console.error(Device not paired);return;}this.sinkDeviceIddeviceId;// 建立 A2DP 连接awaita2dpSource.connect(this.sinkDeviceId);console.info(A2DP connected);}catch(err){console.error(A2DP connect failed:,err);}}// 播放asyncplay():Promisevoid{try{awaita2dpSource.play(this.sinkDeviceId);console.info(Play command sent);}catch(err){console.error(Play failed:,err);}}// 暂停asyncpause():Promisevoid{try{awaita2dpSource.pause(this.sinkDeviceId);console.info(Pause command sent);}catch(err){console.error(Pause failed:,err);}}// 断开asyncdisconnect():Promisevoid{try{awaita2dpSource.disconnect(this.sinkDeviceId);console.info(A2DP disconnected);}catch(err){console.error(Disconnect failed:,err);}}}使用示例constcontrollernewA2dpController();controller.connectSpeaker(XX:XX:XX:XX:XX:XX).then((){controller.play();});注意a2dpSource.play/pause要求已建立 A2DP 连接并且音箱支持 AV/C 控制指令。部分音箱可能不支持暂停只支持播放/停止实际行为需要测试。踩坑记录坑1GATT 连接成功后立即发现服务返回空列表现象在ble.gattClient.connect的回调里马上调用getServices()返回空数组。原因connect仅表示物理链路建立但服务发现需要等待 ATT 层的 MTU 交换和服务搜索完成。系统内部是异步的直接调用getServices()时服务列表尚未填充。解决方案监听ble.gattClient.on(BLEConnectionStateChange)事件在连接状态变为true后等待一个很短的延迟比如 100ms再调用getServices()或者使用官方推荐的方式在on(BLEConnectionStateChange)回调中启动服务发现。更稳妥的做法是通过ble.gattClient.discoverServices()显式触发发现然后在发现成功回调中获取服务。ble.gattClient.on(BLEConnectionStateChange,(state){if(state.isConnected){// 等一小会儿setTimeout(async(){awaitthis.discoverServices();},200);}});坑2写入特征值后外设无响应但 API 返回成功现象调用writeCharacteristicValue没有抛异常但外设未执行任何动作。原因HarmonyOS 的writeCharacteristicValue默认采用WRITE_WITH_RESPONSE带响应的写入。如果外设不支持带响应写只支持无响应写系统会返回成功但实际数据可能未送达或者外设的写回调未被触发。类似地如果外设特征值属性只支持Write Without Response用带响应写可能被拒绝。解决方案明确指定writeType。在调用前先通过getCharacteristicByUuid获取特征值对象检查其writeType属性或直接尝试两种写入方式。推荐写法asyncwriteWithType(charUuid:string,data:ArrayBuffer):Promisevoid{try{// 尝试无响应写awaitble.gattClient.writeCharacteristicValue(charUuid,data,ble.WriteType.WRITE_WITHOUT_RESPONSE);}catch(e){// 如果失败再试带响应写awaitble.gattClient.writeCharacteristicValue(charUuid,data,ble.WriteType.WRITE_WITH_RESPONSE);}}最佳实践不要在build()中频繁创建蓝牙对象蓝牙连接是一个长时间运行的任务如果每次页面刷新都重新创建BLEGattClient实例会导致重复注册事件监听甚至内存泄漏。建议在aboutToAppear中初始化页面销毁时移除监听。使用状态机管理连接生命周期BLE 连接的状态变化待连接、正在连接、已连接、已断开需要同步到 UI。推荐用一个State变量标记连接状态connect调用后更新为connectingBLEConnectionStateChange回调中更新为connected或disconnected。避免在异步回调外直接修改 UI 可能导致的State不一致。经典蓝牙音频控制前检查焦点调用a2dpSource.play前如果设备正在播放其他音频如来电可能会被系统拦截。建议先申请音频焦点audio.AudioManager的requestAudioFocus确保自己有控制权。实际生产代码需要处理权限申请ohos.permission.ACCESS_BLUETOOTH、设备选择UI、错误回退等。FAQQ1为什么真机连接 BLE 正常模拟器上总是失败A模拟器不支持蓝牙硬件蓝牙功能无法正常工作。必须在真机上调试。部分模拟器可能支持 BLE 仿真但不支持经典蓝牙。Q2写入特征值后立即读取拿到的仍然是旧值A写入可能未完成尤其是无响应写或者写入后外设需要时间更新。建议在写入后等待一个适当的时间如 200ms再读取或者通过外设的 Notify 特性接收数据变更。Q3a2dpSource.play调用后音箱没有反应A检查音箱是否处于非连接状态或者音箱的 AVRCP 协议版本较低。部分旧款音箱仅支持 A2DP Sink 角色不支持 AV/C 控制指令。可以尝试手动点按音箱上的播放键测试如果音箱本身不支持遥控播放则play指令无效。这篇文章是对 HarmonyOS 蓝牙开发中 GATT 与 A2DP 两个常见需求的实战总结。如果你还在纠结特征值读写时机或者音箱控制无效可以重点检查连接状态同步和写入类型。毕竟官方文档只告诉你 API 长什么样但实际怎么用还得靠多测。