HarmonyOS技术精讲-Connectivity Kit:蓝牙基础——经典蓝牙与BLE入门
2026/7/25 11:24:36 网站建设 项目流程

HarmonyOS技术精讲-Connectivity Kit:蓝牙基础——经典蓝牙与BLE入门

HarmonyOS NEXT 在蓝牙模块上做了比较大的重构,把之前的@ohos.bluetooth拆成了两个独立的Manager:classicBluetoothManagerbleManager。这个改动本身是合理的——经典蓝牙和BLE的使用场景、API设计差别很大,拆开之后接口更加清晰。但很多初学者第一次接触时会困惑:到底该用哪个Manager?同一个设备能不能同时用两种模式?

这篇文章从实际开发角度出发,先把这两种模式讲清楚,然后分别给出BLE扫描连接和经典蓝牙文件发送的示例代码。代码可以直接跑,重点会放在生命周期管理和状态同步上,这是最容易出问题的地方。

蓝牙BR/EDR与BLE:先搞清楚用哪个

蓝牙本身有两种工作模式:BR/EDR(经典蓝牙)BLE(低功耗蓝牙)。在HarmonyOS中,它们被封装为两个独立的模块,API风格完全不同。

特性BLE (bleManager)经典蓝牙 (classicBluetoothManager)
功耗极低,纽扣电池可运行数年较高,适合持续数据传输
数据传输速度慢(约1Mbps),适合小数据包速度快(可达2-3Mbps),适合文件/音频
连接方式广播-扫描-连接-服务发现配对-连接-RFCOMM通道
典型场景传感器、心率计、设备发现耳机、音箱、文件传输
硬件要求所有蓝牙4.0+设备都支持必须支持BR/EDR(很多耳机/音箱仅支持此模式)
HarmonyOS API@kit.ConnectivityKitbleManager@kit.ConnectivityKitclassicBluetoothManager

实际开发中,必须先确定业务场景,再选Manager。如果一个设备需要同时支持两种模式(比如既做数据采集,又做文件传输),需要分别初始化两个Manager,它们之间没有依赖关系。

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上 目标设备:Mate 40 Pro (HarmonyOS NEXT)

核心实现:BLE扫描与连接

Step 1:权限声明与动态申请

蓝牙操作需要两个关键权限:ohos.permission.BLUETOOTH(蓝牙基础操作)和ohos.permission.DISCOVER_BLUETOOTH(扫描发现设备)。在HarmonyOS NEXT中,这两个权限都需要在module.json5中声明,并且在运行时动态申请。

// module.json5"requestPermissions":[{"name":"ohos.permission.BLUETOOTH","reason":"$string:app_name"},{"name":"ohos.permission.DISCOVER_BLUETOOTH","reason":"$string:app_name"}]

动态申请代码:

import{abilityAccessCtrl,common,Permissions}from'@kit.AbilityKit';asyncfunctionrequestBlePermissions(context:common.UIAbilityContext):Promise<void>{constatManager=abilityAccessCtrl.createAtManager();constpermissions:Array<Permissions>=['ohos.permission.BLUETOOTH','ohos.permission.DISCOVER_BLUETOOTH'];try{for(constpermofpermissions){constresult=awaitatManager.requestPermissionsFromUser(context,[perm]);if(result.authResults[0]!==abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED){console.error('权限被拒绝:',perm);// 这里建议弹窗提示用户手动授权}}}catch(err){console.error('权限请求失败:',err.message);}}

注意:权限申请必须放在aboutToAppear或者页面初始化阶段,不要在build()里申请,否则会导致连续弹窗。另外,requestPermissionsFromUser是异步的,调用startBLEScan之前必须确保权限已经获得。

Step 2:初始化BLE Manager并开始扫描

bleManager是单例对象,直接通过import导入即可使用,不需要getInstance之类的方法。

import{ble}from'@kit.ConnectivityKit';letscanResults:ble.ScanResult[]=[];letscanning=false;functionstartBleScan():void{if(scanning){console.warn('扫描正在进行中,无需重复启动');return;}constfilters:ble.ScanFilter[]=[];// 可以根据需要设置过滤条件,比如只扫描特定服务UUID的设备// filters.push({ serviceUuid: '0000180f-0000-1000-8000-00805f9b34fb' });constoptions:ble.ScanOptions={interval:500,// 扫描间隔,单位msdutyMode:ble.ScanDuty.SCAN_MODE_LOW_POWER,// 功耗模式phyType:ble.PhyType.PHY_LE_1M// 物理层类型};try{ble.startBLEScan(filters,options);scanning=true;console.info('BLE扫描已启动');}catch(err){console.error('startBLEScan失败:',err.message);}}// 注册扫描结果监听ble.on('BLEDeviceFind',(data:Array<ble.ScanResult>)=>{data.forEach(device=>{constexist=scanResults.some(item=>item.deviceId===device.deviceId);if(!exist){scanResults.push(device);console.info('发现新设备:',device.deviceName,device.deviceId);}});});// 停止扫描functionstopBleScan():void{if(!scanning)return;try{ble.stopBLEScan();scanning=false;console.info('BLE扫描已停止');}catch(err){console.error('stopBLEScan失败:',err.message);}}

几个关键点

  1. startBLEScanfilters参数可以传空数组,表示扫描所有设备。如果加了过滤,发现速度会变慢,但能减少无效回调。
  2. on('BLEDeviceFind')的回调会在每次发现设备时触发,而且同一个设备可能被多次发现。上面代码里用deviceId去重,避免UI列表重复渲染。
  3. 扫描间隔interval默认是200ms,实测发现设为500ms以上能明显减少设备间的干扰,适合在信号较差的场景。

Step 3:连接设备并监听GATT连接状态

扫描到设备后,调用connect进行连接。注意:BLE连接是异步的,需要通过回调监听连接状态。

import{connectedAble,gatt}from'@kit.ConnectivityKit';// 注意:bleManager也提供了连接方法,但推荐使用connectedAble,它暴露了更详细的连接状态letdeviceId:string;// 从扫描结果中获取// 连接设备functionconnectBleDevice(targetId:string):void{deviceId=targetId;// 先注册连接状态变化监听ble.on('BLEConnectionStateChange',(state:ble.BLEConnectionChangeState)=>{console.info('连接状态变化: deviceId=',state.deviceId,'state=',state.state);// state.state: 0=已断开, 1=正在连接, 2=已连接if(state.state===2){console.info('设备已连接,开始发现服务');discoverServices();}elseif(state.state===0){console.warn('设备已断开');// 这里建议触发重连逻辑,或者更新UI状态}});try{ble.connect(deviceId);}catch(err){console.error('连接失败:',err.message);}}// 发现服务functiondiscoverServices():void{try{constservices=ble.getServices(deviceId);// 返回GattService数组services.forEach(service=>{console.info('发现服务:',service.serviceUuid);// 可以在这里遍历characteristics,找到需要的特征});}catch(err){console.error('getServices失败:',err.message);}}// 断开连接(页面销毁时一定要调用)functiondisconnectBleDevice():void{try{ble.disconnect(deviceId);// 同时注销监听ble.off('BLEConnectionStateChange');}catch(err){console.error('断开连接失败:',err.message);}}

很多人会忽略off操作。如果页面销毁时不注销监听,下次进入页面会重复注册,导致回调被多次触发,出现状态错乱。

核心实现:经典蓝牙配对与文件发送

经典蓝牙的场景通常是设备配对后,通过RFCOMM通道传输文件。HarmonyOS的classicBluetoothManager提供了createBond进行配对,以及listener('bondStateChange')监听配对状态。

Step 1:初始化并打开蓝牙

import{classicBluetooth,baseProfile}from'@kit.ConnectivityKit';// 获取经典蓝牙实例constbtManager=classicBluetooth.getClassicBluetoothManager();// 检查并打开蓝牙functionenableClassicBt():void{conststate=btManager.getState();if(state===baseProfile.BluetoothState.STATE_OFF){btManager.enableBluetooth();// 注意:这个是异步的,需要轮询状态确认console.info('正在打开经典蓝牙...');}else{console.info('经典蓝牙已开启');}}

经典蓝牙的enableBluetooth不会立即生效,建议通过on('stateChange')监听状态变化:

btManager.on('stateChange',(data:baseProfile.StateChangeParam)=>{if(data.state===baseProfile.BluetoothState.STATE_ON){console.info('经典蓝牙已就绪');// 可以开始扫描startClassicScan();}});

Step 2:扫描经典蓝牙设备并配对

经典蓝牙的扫描方法和BLE不同,它通过startBluetoothDiscovery发现设备,然后通过createBond建立配对。

letclassicDevices:classicBluetooth.BondDevice[]=[];// 开始扫描functionstartClassicScan():void{try{btManager.startBluetoothDiscovery();// 扫描附近的经典蓝牙设备}catch(err){console.error('扫描启动失败:',err.message);}}// 监听发现设备btManager.on('deviceFind',(device:classicBluetooth.BondDevice)=>{constexist=classicDevices.some(d=>d.deviceId===device.deviceId);if(!exist){classicDevices.push(device);console.info('发现经典蓝牙设备:',device.deviceName);// 可以在这里自动发起配对,或等待用户点击// bondWithDevice(device.deviceId);}});// 发起配对functionbondWithDevice(deviceId:string):void{try{btManager.createBond(deviceId);console.info('正在发起配对...');}catch(err){console.error('createBond失败:',err.message);}}// 监听配对状态btManager.on('pinRequired',(deviceId:string,pinCode:number)=>{console.info('需要输入PIN码:',pinCode);// 通常需要显示一个UI弹窗让用户输入,或者自动确认btManager.setDevicePin(deviceId,pinCode.toString());// 自动配对});btManager.on('bondStateChange',(data:baseProfile.BondStateParam)=>{if(data.state===baseProfile.BondState.BONDED){console.info('配对成功:',data.deviceId);// 配对成功后可以建立RFCOMM连接}elseif(data.state===baseProfile.BondState.BONDING){console.info('正在配对中...');}elseif(data.state===baseProfile.BondState.UNBONDED){console.info('配对失败或已解除:',data.deviceId);}});

Step 3:RFCOMM数据传输示例

经典蓝牙的文件传输通常使用RFCOMM(串行端口协议)。下面是一个发送文本数据的示例:

import{socket}from'@kit.ConnectivityKit';import{fileIo}from'@kit.CoreFileKit';// 建立RFCOMM连接asyncfunctionconnectRfcomm(deviceId:string,channel:number):Promise<void>{constrfcommSocket=socket.constructRfcommSocketInstance();// 监听连接状态rfcommSocket.on('connect',()=>{console.info('RFCOMM连接成功');sendFileViaRfcomm(rfcommSocket);});rfcommSocket.on('disconnect',(err)=>{console.warn('RFCOMM连接断开:',err.message);});try{awaitrfcommSocket.connect({deviceId,channel});}catch(err){console.error('RFCOMM连接失败:',err.message);}}// 发送文件asyncfunctionsendFileViaRfcomm(socket:socket.RfcommSocket):Promise<void>{constfilePath='/data/storage/el2/base/haps/entry/files/test.txt';// 模拟一个文件路径try{constfile=awaitfileIo.open(filePath,fileIo.OpenMode.READ_ONLY);constbuf=newArrayBuffer(1024);lettotalBytes=0;// 分批读取并发送while(true){constreadResult=awaitfileIo.read(file.fd,buf);if(readResult.bytesRead===0)break;// 读取完毕constsent=socket.send(newUint8Array(buf.slice(0,readResult.bytesRead)));totalBytes+=sent;}awaitfileIo.close(file.fd);console.info('文件发送完成, 共发送字节:',totalBytes);}catch(err){console.error('文件发送失败:',err.message);}}

注意:RFCOMM的channel参数需要提前知道,或者通过SDP(服务发现协议)查询。实际项目中,通常需要双方约定一个固定的channel编号。

常见问题

问题1:权限声明后仍然报错 “Permission denied”

现象:在module.json5中添加了权限,运行时startBLEScan依然抛异常。

原因:HarmonyOS NEXT 对敏感权限要求必须同时满足两个条件:

  1. module.json5中声明
  2. 在运行时通过abilityAccessCtrl动态申请

如果忘记动态申请,或者用户在权限弹窗中点击了"拒绝",后续接口会持续报错。

解决方案

  • aboutToAppear中调用权限申请方法,并在回调中处理拒绝情况
  • 如果用户拒绝,引导用户去系统设置中手动开启

问题2:BLE扫描发现设备后,立即连接失败

现象:扫描日志显示找到了设备,但调用ble.connect后回调状态为 “断开”。

原因:BLE设备在广播时处于可连接状态,但如果设备已经与另一个主机建立了连接,或者设备本身处于非连接广播模式(如仅广播数据),则连接会失败。此外,扫描停止后立即连接的成功率更高,因为扫描会占用蓝牙资源。

解决方案

  • 在连接前先调用ble.stopBLEScan,释放扫描资源
  • 添加重连逻辑:第一次失败后,等待1-2秒再重试一次

问题3:经典蓝牙配对成功后,RFCOMM连接超时

现象createBond回调了BONDED,但调用rfcommSocket.connect时一直超时。

原因:配对成功只代表设备建立了信任关系,但不代表RFCOMM服务已经就绪。有些设备需要先建立连接(socket),配对才会正式完成。此外,channel号如果不正确,连接也会失败。

解决方案

  • 确保channel参数使用的是对方设备支持的RFCOMM通道号
  • 配对成功后,等待500ms-1s再发起RFCOMM连接,给设备足够的准备时间

最佳实践

  1. 不要在build()中直接调用startBLEScan。ArkUI的build()方法会多次执行,每次都会启动新的扫描,导致回调混乱。应该把扫描逻辑放在aboutToAppear或者按钮点击事件里。

  2. 页面销毁时必须注销所有监听on('BLEDeviceFind')on('BLEConnectionStateChange')等监听如果不注销,当页面跳转后,回调节点依然存在,可能会操作已经被销毁的UI组件,导致undefined错误。

  3. BLE扫描的生命周期管理。如果应用进入后台,系统会暂停BLE扫描。建议在onPageHide中停止扫描,在onPageShow中重新启动。这样可以节省电量,同时避免在后台无效扫描。

FAQ

Q:为什么真机正常,模拟器不生效?

A:HarmonyOS模拟器对蓝牙硬件的模拟不完整,特别是BLE的广播和扫描功能。建议所有蓝牙相关功能都在真机上测试。

Q:为什么页面返回后,再次进入发现设备列表清空了?

A:因为scanResults是组件内的局部变量,页面销毁后重新创建,数据自然丢失。建议把扫描结果存储在AppStorage或者全局单例中,实现跨页面持久化。

Q:第一次配对成功后,再次连接需要重新配对吗?

A:不需要。配对信息保存在系统蓝牙配置中,再次连接时直接调用ble.connectrfcommSocket.connect即可,系统会自动使用已保存的配对凭证。但注意,如果用户在系统蓝牙设置中手动删除了配对,则需要重新createBond

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询