HarmonyOS技术精讲-Connectivity Kit蓝牙基础——经典蓝牙与BLE入门HarmonyOS NEXT 在蓝牙模块上做了比较大的重构把之前的ohos.bluetooth拆成了两个独立的ManagerclassicBluetoothManager和bleManager。这个改动本身是合理的——经典蓝牙和BLE的使用场景、API设计差别很大拆开之后接口更加清晰。但很多初学者第一次接触时会困惑到底该用哪个Manager同一个设备能不能同时用两种模式这篇文章从实际开发角度出发先把这两种模式讲清楚然后分别给出BLE扫描连接和经典蓝牙文件发送的示例代码。代码可以直接跑重点会放在生命周期管理和状态同步上这是最容易出问题的地方。蓝牙BR/EDR与BLE先搞清楚用哪个蓝牙本身有两种工作模式BR/EDR经典蓝牙和BLE低功耗蓝牙。在HarmonyOS中它们被封装为两个独立的模块API风格完全不同。特性BLE (bleManager)经典蓝牙 (classicBluetoothManager)功耗极低纽扣电池可运行数年较高适合持续数据传输数据传输速度慢约1Mbps适合小数据包速度快可达2-3Mbps适合文件/音频连接方式广播-扫描-连接-服务发现配对-连接-RFCOMM通道典型场景传感器、心率计、设备发现耳机、音箱、文件传输硬件要求所有蓝牙4.0设备都支持必须支持BR/EDR很多耳机/音箱仅支持此模式HarmonyOS APIkit.ConnectivityKit下bleManagerkit.ConnectivityKit下classicBluetoothManager实际开发中必须先确定业务场景再选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.json5requestPermissions:[{name:ohos.permission.BLUETOOTH,reason:$string:app_name},{name:ohos.permission.DISCOVER_BLUETOOTH,reason:$string:app_name}]动态申请代码import{abilityAccessCtrl,common,Permissions}fromkit.AbilityKit;asyncfunctionrequestBlePermissions(context:common.UIAbilityContext):Promisevoid{constatManagerabilityAccessCtrl.createAtManager();constpermissions:ArrayPermissions[ohos.permission.BLUETOOTH,ohos.permission.DISCOVER_BLUETOOTH];try{for(constpermofpermissions){constresultawaitatManager.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}fromkit.ConnectivityKit;letscanResults:ble.ScanResult[][];letscanningfalse;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);scanningtrue;console.info(BLE扫描已启动);}catch(err){console.error(startBLEScan失败:,err.message);}}// 注册扫描结果监听ble.on(BLEDeviceFind,(data:Arrayble.ScanResult){data.forEach(device{constexistscanResults.some(itemitem.deviceIddevice.deviceId);if(!exist){scanResults.push(device);console.info(发现新设备:,device.deviceName,device.deviceId);}});});// 停止扫描functionstopBleScan():void{if(!scanning)return;try{ble.stopBLEScan();scanningfalse;console.info(BLE扫描已停止);}catch(err){console.error(stopBLEScan失败:,err.message);}}几个关键点startBLEScan的filters参数可以传空数组表示扫描所有设备。如果加了过滤发现速度会变慢但能减少无效回调。on(BLEDeviceFind)的回调会在每次发现设备时触发而且同一个设备可能被多次发现。上面代码里用deviceId去重避免UI列表重复渲染。扫描间隔interval默认是200ms实测发现设为500ms以上能明显减少设备间的干扰适合在信号较差的场景。Step 3连接设备并监听GATT连接状态扫描到设备后调用connect进行连接。注意BLE连接是异步的需要通过回调监听连接状态。import{connectedAble,gatt}fromkit.ConnectivityKit;// 注意bleManager也提供了连接方法但推荐使用connectedAble它暴露了更详细的连接状态letdeviceId:string;// 从扫描结果中获取// 连接设备functionconnectBleDevice(targetId:string):void{deviceIdtargetId;// 先注册连接状态变化监听ble.on(BLEConnectionStateChange,(state:ble.BLEConnectionChangeState){console.info(连接状态变化: deviceId,state.deviceId,state,state.state);// state.state: 0已断开, 1正在连接, 2已连接if(state.state2){console.info(设备已连接开始发现服务);discoverServices();}elseif(state.state0){console.warn(设备已断开);// 这里建议触发重连逻辑或者更新UI状态}});try{ble.connect(deviceId);}catch(err){console.error(连接失败:,err.message);}}// 发现服务functiondiscoverServices():void{try{constservicesble.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}fromkit.ConnectivityKit;// 获取经典蓝牙实例constbtManagerclassicBluetooth.getClassicBluetoothManager();// 检查并打开蓝牙functionenableClassicBt():void{conststatebtManager.getState();if(statebaseProfile.BluetoothState.STATE_OFF){btManager.enableBluetooth();// 注意这个是异步的需要轮询状态确认console.info(正在打开经典蓝牙...);}else{console.info(经典蓝牙已开启);}}经典蓝牙的enableBluetooth不会立即生效建议通过on(stateChange)监听状态变化btManager.on(stateChange,(data:baseProfile.StateChangeParam){if(data.statebaseProfile.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){constexistclassicDevices.some(dd.deviceIddevice.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.statebaseProfile.BondState.BONDED){console.info(配对成功:,data.deviceId);// 配对成功后可以建立RFCOMM连接}elseif(data.statebaseProfile.BondState.BONDING){console.info(正在配对中...);}elseif(data.statebaseProfile.BondState.UNBONDED){console.info(配对失败或已解除:,data.deviceId);}});Step 3RFCOMM数据传输示例经典蓝牙的文件传输通常使用RFCOMM串行端口协议。下面是一个发送文本数据的示例import{socket}fromkit.ConnectivityKit;import{fileIo}fromkit.CoreFileKit;// 建立RFCOMM连接asyncfunctionconnectRfcomm(deviceId:string,channel:number):Promisevoid{constrfcommSocketsocket.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):Promisevoid{constfilePath/data/storage/el2/base/haps/entry/files/test.txt;// 模拟一个文件路径try{constfileawaitfileIo.open(filePath,fileIo.OpenMode.READ_ONLY);constbufnewArrayBuffer(1024);lettotalBytes0;// 分批读取并发送while(true){constreadResultawaitfileIo.read(file.fd,buf);if(readResult.bytesRead0)break;// 读取完毕constsentsocket.send(newUint8Array(buf.slice(0,readResult.bytesRead)));totalBytessent;}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 对敏感权限要求必须同时满足两个条件在module.json5中声明在运行时通过abilityAccessCtrl动态申请如果忘记动态申请或者用户在权限弹窗中点击了拒绝后续接口会持续报错。解决方案在aboutToAppear中调用权限申请方法并在回调中处理拒绝情况如果用户拒绝引导用户去系统设置中手动开启问题2BLE扫描发现设备后立即连接失败现象扫描日志显示找到了设备但调用ble.connect后回调状态为 “断开”。原因BLE设备在广播时处于可连接状态但如果设备已经与另一个主机建立了连接或者设备本身处于非连接广播模式如仅广播数据则连接会失败。此外扫描停止后立即连接的成功率更高因为扫描会占用蓝牙资源。解决方案在连接前先调用ble.stopBLEScan释放扫描资源添加重连逻辑第一次失败后等待1-2秒再重试一次问题3经典蓝牙配对成功后RFCOMM连接超时现象createBond回调了BONDED但调用rfcommSocket.connect时一直超时。原因配对成功只代表设备建立了信任关系但不代表RFCOMM服务已经就绪。有些设备需要先建立连接socket配对才会正式完成。此外channel号如果不正确连接也会失败。解决方案确保channel参数使用的是对方设备支持的RFCOMM通道号配对成功后等待500ms-1s再发起RFCOMM连接给设备足够的准备时间最佳实践不要在build()中直接调用startBLEScan。ArkUI的build()方法会多次执行每次都会启动新的扫描导致回调混乱。应该把扫描逻辑放在aboutToAppear或者按钮点击事件里。页面销毁时必须注销所有监听。on(BLEDeviceFind)、on(BLEConnectionStateChange)等监听如果不注销当页面跳转后回调节点依然存在可能会操作已经被销毁的UI组件导致undefined错误。BLE扫描的生命周期管理。如果应用进入后台系统会暂停BLE扫描。建议在onPageHide中停止扫描在onPageShow中重新启动。这样可以节省电量同时避免在后台无效扫描。FAQQ为什么真机正常模拟器不生效AHarmonyOS模拟器对蓝牙硬件的模拟不完整特别是BLE的广播和扫描功能。建议所有蓝牙相关功能都在真机上测试。Q为什么页面返回后再次进入发现设备列表清空了A因为scanResults是组件内的局部变量页面销毁后重新创建数据自然丢失。建议把扫描结果存储在AppStorage或者全局单例中实现跨页面持久化。Q第一次配对成功后再次连接需要重新配对吗A不需要。配对信息保存在系统蓝牙配置中再次连接时直接调用ble.connect或rfcommSocket.connect即可系统会自动使用已保存的配对凭证。但注意如果用户在系统蓝牙设置中手动删除了配对则需要重新createBond。