《HarmonyOS技术精讲-蓝牙》综合实战:构建智能家居控制应用

📅 2026/7/11 2:01:00
《HarmonyOS技术精讲-蓝牙》综合实战:构建智能家居控制应用
HarmonyOS技术精讲从零构建智能家居蓝牙控制应用HarmonyOS NEXT 开发中蓝牙相关 API 经常被同时使用但开发者容易混淆。一个是传统蓝牙Classic Bluetooth用于高带宽场景如音频传输另一个是低功耗蓝牙BLE用于小数据量、低功耗的设备控制。很多人在实际开发中会面临一个典型问题如何在一个应用里同时管理两种蓝牙协议并处理它们复杂的重连和异常逻辑蓝牙音箱的播放和智能灯的控制看似是两个独立功能但在一个智能家居场景里它们需要协同工作。比如检测到温度过高时自动播放报警提示音。这个功能本身不复杂真正的难点在于生命周期管理、状态同步以及异常重连。官方示例大多只演示了单一功能的连接和通信没有解释在真实项目中当页面销毁、蓝牙连接断开、权限变化时如何保证整个系统稳定运行。本文目标很明确通过一个完整的智能家居控制 Demo体验如何在一个应用中融合传统蓝牙和 BLE实现音箱播报和灯控、温度采集的联动。代码会完整贴出并解释每一步的设计取舍。它解决什么问题蓝牙 API 主要分为两类场景场景协议特点适合场景不适合场景音频播放传统蓝牙 (BR)高带宽支持 A2DP/HSP/HFP播放音乐、通话、语音播报低功耗、小数据量设备控制设备控制BLE低功耗支持 GATT 协议服务与特征智能开关、传感器、穿戴设备需要持续高带宽音频流高带宽数据传统蓝牙 (SPP)串口模拟双向通信数据传输、外设控制低功耗、广播在智能家居场景中一套音响系统通常使用传统蓝牙A2DP播放音频而智能灯和温度传感器则基于 BLE 的 GATT 服务进行读写和控制。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机 / 平板需支持蓝牙核心实现项目结构如下SmartHomeDemo/ ├── entry/ │ ├── src/main/ │ │ ├── ets/ │ │ │ ├── Service/ │ │ │ │ ├── BluetoothService.ets // 蓝牙封装核心类 │ │ │ │ ├── SpeakerConnector.ets // 音箱连接器(传统蓝牙) │ │ │ │ └── LightConnector.ets // 灯控连接器(BLE) │ │ │ ├── pages/ │ │ │ │ └── Index.ets // 主页面 │ │ │ └── model/ │ │ │ └── DeviceInfo.ets // 设备数据类型 │ │ └── resources/ │ │ └── base/ │ │ └── profile/ │ │ └── main_pages.json1. 权限配置在module.json5中添加蓝牙和位置权限requestPermissions:[{name:ohos.permission.USE_BLUETOOTH},{name:ohos.permission.DISCOVER_BLUETOOTH},{name:ohos.permission.ACCESS_BLUETOOTH}]这里需要注意ACCESS_FINE_LOCATION在新版本 API 中已不再强制要求但若需要扫描 BLE 设备仍需确保位置服务开启。2. 蓝牙基础封装我们创建一个单例BluetoothService管理全局蓝牙开关、配对、连接等核心生命周期。// BluetoothService.etsimport{bluetooth}fromkit.ConnectivityKit;classBluetoothService{privatestaticinstance:BluetoothService;privatestate:bluetooth.BluetoothStatebluetooth.BluetoothState.STATE_OFF;privateconnectedDevice:string;privatecentral:bluetooth.BLE|nullnull;staticgetInstance():BluetoothService{if(!BluetoothService.instance){BluetoothService.instancenewBluetoothService();}returnBluetoothService.instance;}constructor(){this.init();}privateinit(){// 监听蓝牙状态变化bluetooth.on(stateChange,(data:bluetooth.BluetoothState){console.info(蓝牙状态变化:${data});this.statedata;});}// 开启蓝牙asyncenableBluetooth():Promiseboolean{returnnewPromise((resolve,reject){bluetooth.enableBluetooth().then((){console.info(蓝牙已开启);resolve(true);}).catch((err:Error){console.error(开启蓝牙失败: JSON.stringify(err));reject(false);});});}// 检查蓝牙是否开启isBluetoothEnabled():boolean{returnthis.statebluetooth.BluetoothState.STATE_ON;}// 获取已配对设备列表getPairedDevices():Arraybluetooth.BondState{returnbluetooth.getPairedDevices();}}exportdefaultBluetoothService;设计意图BluetoothService作为全局状态管家任何页面都能获取到蓝牙状态。为什么不写在页面Component里因为蓝牙生命周期贯穿整个应用页面销毁后蓝牙连接需要继续保持。3. 传统蓝牙连接音箱播放传统蓝牙连接喇叭这里使用 A2DP profile。完整连接流程是先配对 → 再连接 A2DP。// SpeakerConnector.etsimport{bluetooth}fromkit.ConnectivityKit;importBluetoothServicefrom./BluetoothService;classSpeakerConnector{privatea2dp:bluetooth.A2dpSourceProfile|nullnull;asyncconnectToSpeaker(deviceId:string):Promiseboolean{// 1. 获取 A2DP profilereturnnewPromise((resolve,reject){bluetooth.createProfileInstance(bluetooth.ProfileId.PROFILE_A2DP_SOURCE).then((profile:bluetooth.A2dpSourceProfile){this.a2dpprofile;returnthis.a2dp.connect(deviceId);}).then((){console.info(音箱连接成功);resolve(true);}).catch((err:Error){console.error(连接音箱失败: JSON.stringify(err));reject(false);});});}// 断开连接disconnect(){this.a2dp?.disconnect();}}exportdefaultSpeakerConnector;为什么选择 A2DP 而不是 HSPA2DP 用于高质量的立体声音频流适合播放提示音、音乐。HSP/HFP 主要用于通话音质较低。在智能家居场景里显然是 A2DP 更合适。4. BLE连接智能灯与传感器BLE 连接核心是 GATT 通信。我们需要发现服务、然后读写特征。// LightConnector.etsimport{bluetooth}fromkit.ConnectivityKit;classLightConnector{privateble:bluetooth.BLE;privatedevice:bluetooth.BLEDevice|nullnull;privategattClient:bluetooth.GattClientDevice|nullnull;constructor(){this.blebluetooth.getProfileInstance();}// 扫描并连接asyncconnectToLight(deviceId:string):Promiseboolean{returnnewPromise((resolve,reject){this.ble.on(BLEDeviceFind,(data:Arraybluetooth.ScanResult){constdevicedata.find((item)item.deviceIddeviceId);if(device){this.ble.stopScan();this.devicedevice;// 连接 GATTbluetooth.connectGattClientDevice(device.deviceId).then((client:bluetooth.GattClientDevice){this.gattClientclient;returnthis.gattClient.on(BLEConnectionStateChange,(state){console.info(BLE 连接状态变化: JSON.stringify(state));});}).then(()resolve(true)).catch((err:Error)reject(err));}});this.ble.startScan();});}// 写入特征控制灯开关asyncwriteCharacteristic(serviceUuid:string,characteristicUuid:string,data:ArrayBuffer):Promisevoid{if(!this.gattClient){thrownewError(BLE 未连接);}returnthis.gattClient.writeCharacteristicValue(serviceUuid,characteristicUuid,data);}// 读取特征获取温度传感器数据asyncreadCharacteristic(serviceUuid:string,characteristicUuid:string):PromiseArrayBuffer{if(!this.gattClient){thrownewError(BLE 未连接);}returnthis.gattClient.readCharacteristicValue(serviceUuid,characteristicUuid);}// 断开连接disconnect(){this.gattClient?.disconnect();}}exportdefaultLightConnector;细节说明startScan需要在前置条件蓝牙开启、位置权限、位置服务开启下才能生效。很多人在真机上发现扫描不到设备往往是因为位置服务没开。写特征前需要先发现服务否则 GATT 读写会失败。这个步骤在connectGattClientDevice内部会自动完成但在某些设备上可能有延迟建议在连接状态回调完成后再进行读写。5. 联动逻辑温度过高自动播报这个功能在主页面的Component中实现。我们会在 BLE 读取温度后根据阈值调用音箱播放音频。// Index.etsEntryComponentstruct Index{StateisLightOn:booleanfalse;Statetemperature:number25.0;StatespeakerConnected:booleanfalse;privatespeakerConnector:SpeakerConnectornewSpeakerConnector();privatelightConnector:LightConnectornewLightConnector();build(){Column(){// 蓝牙状态显示Text(音箱状态:${this.speakerConnected?已连接:未连接})Button(连接音箱).onClick((){this.speakerConnector.connectToSpeaker(音箱设备MAC地址).then((){this.speakerConnectedtrue;}).catch((){// 连接失败处理});})// 灯控开关Button(this.isLightOn?关闭灯:打开灯).onClick((){constdatanewUint8Array([this.isLightOn?0x00:0x01]).buffer;this.lightConnector.writeCharacteristic(灯的Service UUID,开关特征UUID,data).then((){this.isLightOn!this.isLightOn;}).catch((){// 写入失败提醒});})// 温度显示与联动Text(当前温度:${this.temperature}°C)Button(读取温度).onClick((){this.lightConnector.readCharacteristic(温度Service UUID,温度特征UUID).then((buffer){this.temperaturenewFloat32Array(buffer)[0];// 联动逻辑温度 30播放警报if(this.temperature30this.speakerConnected){this.playAlarmHint();}});})}.padding(16).onPageShow((){// 页面显示时检查蓝牙状态})}asyncplayAlarmHint(){// 这里可以调用系统媒体播放器播放预设音频// 或者通过 A2DP 传输音频流console.info(播放警报提示音);}}设计取舍为什么把蓝牙状态放在页面组件因为 UI 交互需要实时绑定状态。但蓝牙连接的生命周期由BluetoothService管理页面销毁后不影响蓝牙后台运行。为什么读取温度后直接联动这属于业务逻辑放在页面层方便管理但要注意避免在异步回调里频繁更新State导致性能问题。实际项目中最好将联动逻辑抽成独立 Service。常见问题 1传统蓝牙与 BLE 同时连接时的冲突现象当同时连接音箱传统蓝牙和智能灯BLE时音箱播放音频后 BLE 连接自动断开。原因在某些设备上传统蓝牙的音频流A2DP和 BLE 共存时可能存在带宽竞争。尤其是音箱在播放高码率音频时会占用大量蓝牙带宽导致 BLE 连接不稳定。解决方案在播放音频前先确保 BLE 连接不在传输高负载数据。使用bluetooth.getBluetoothState()检查蓝牙状态若正在播放音频可考虑降低音频码率或调整 BLE 通信间隔。在BluetoothService中维护一个isAudioStreaming状态Ble 写入特征前检查该状态。常见问题 2页面返回后蓝牙状态丢失现象用户从主页面跳转到其他页面再返回时蓝牙连接显示断开但实际上蓝牙还连着。原因页面组件的State在页面销毁时重置重新创建时变量恢复默认值。蓝牙 Service 虽然是单例但页面并没有自动同步。解决方案使用Link或全局 Store 共享蓝牙状态。推荐在BluetoothService中增加一个State引用页面通过Watch订阅变化。// 在 BluetoothService 中新增private_stateEmitter:(state:boolean)void(){};setStateChangeCallback(cb:(state:boolean)void){this._stateEmittercb;}// 在 Index.ets 中aboutToAppear(){BluetoothService.getInstance().setStateChangeCallback((connected:boolean){this.speakerConnectedconnected;});}最佳实践管理蓝牙连接生命周期不要在Component的build()中频繁调用connect/disconnect应该使用单例 Service 统一管理。页面仅做 UI 和事件触发。封装异步回调蓝牙 API 大量使用回调容易造成回调地狱。推荐使用Promiseasync/await包装。但要注意 Promise 的错误处理尤其是连接超时场景。在connect调用上建议增加超时处理。异常重连机制蓝牙连接时常不稳定。可以在BluetoothService中实现重连策略断开后等待 3 秒尝试重连。连续失败 3 次后暂停。重连次数过多时通知用户检查设备。FAQQ真机上扫描不到 BLE 设备但模拟器可以A模拟器通常不支持 BLE 广播需要真机。真机扫描失败的原因主要是没有开启位置服务蓝牙扫描需要精细位置权限。蓝牙未开启。设备处于不可发现模式。Q为什么写入 BLE 特征后 UI 没更新AUI 更新需要通过State变量触发。如果写入成功回调里直接赋值注意上下文中的this。使用箭头函数或bind(this)。Q传统蓝牙断开后如何自动重连A在bluetooth.on(stateChange)或bluetooth.on(bluetoothDeviceFind)回调中加入重连逻辑。但注意不要无限重试应设定最大重试次数和间隔。结语在 HarmonyOS 中同时管理传统蓝牙和 BLE核心挑战不是调用 API而是理解生命周期和设计稳定的状态同步机制。把蓝牙状态集中到一个 Service 里处理页面只关注 UI 展示和事件触发这样你的应用才能在各种异常场景下连续工作。官方文档对蓝牙 API 描述得比较抽象建议结合真机调试一步一步验证。如果你也遇到类似的问题可以重点检查是否遗漏了位置服务权限或状态同步逻辑。