【鸿蒙开发实战】蓝牙功能的开发
前言
断更许久,也是因为没有什么好的素材可以写,最近学了一点鸿蒙开发,因为手头上有一块蓝牙模块(JDY-31),就想着说结合stm32,还有手机app实现一个类似智能家居的系统。说干就干,32的代码比较容易写,很快就完成了,但是鸿蒙蓝牙调试软件的开发让我犯了难。起初,我想要去应用商店找一下有没有现成的软件可以用,找了很久发现要么不好用,没有我想要的功能,要么就是半成品软件,空有其表。于是,我就打算开发一个自己的蓝牙调试器,在开发过程中,我也遇到了许多问题。所有我就写下了这篇文章来跟大家分享一下我的经验,如果有写的不好的地方还请各位大佬批评指正。
正文
首先我们需要清楚在鸿蒙当中怎样去实现蓝牙的功能
1.申请蓝牙权限
这里贴上文档地址:向用户申请授权-申请应用权限-应用权限管控-程序访问控制-安全-系统 - 华为HarmonyOS开发者
通过查看华为官方文档我们可以知道申请蓝牙权限有这些步骤:
首先,我们需要在配置文件当中申请蓝牙权限
如图,我们需要在module.json5
配置文件中加入这么一个配置
"requestPermissions": [{"name" : "ohos.permission.ACCESS_BLUETOOTH","reason": "$string:blooth_reason","usedScene": {"abilities": ["FormAbility"],"when":"inuse"}}],
其中reason一项很容易让人搞不懂鼠标移动到reason后,按住control,鼠标左键单击
单击后我们会发现跳转到了string.json,然后在这里仿造格式写上原因就行啦,注意这里name属性对应的就是上面那个配置当中$string:blooth_reason
的blooth_reason,这里随意起名字就好,但是要规范,自己能看得懂
在配置文件声明完权限后还需要添加向用户申请权限的代码,代码如下
// 用户申请权限async reqPermissionsFromUser(): Promise<number[]> {let context = getContext() as common.UIAbilityContext;let atManager = abilityAccessCtrl.createAtManager();let grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.ACCESS_BLUETOOTH']);return grantStatus.authResults;}// 用户申请蓝牙权限async requestBlueToothPermission() {let grantStatus = await this.reqPermissionsFromUser();for (let i = 0; i < grantStatus.length; i++) {if (grantStatus[i] === 0) {// 用户授权,可以继续访问目标操作}}}
需要 申请权限的时候就调用一下 requestBlueToothPermission
,这里要注意因为 requestBlueToothPermission
是异步函数,所以要用await来进行调用,像这样:
await this.requestBlueToothPermission()
这里给点小建议,可以在页面初始化的时候调用这个函数,这样就可以直接获取授权了
在这里我定义了一个init函数
代码如下
async init() {try {await this.requestBlueToothPermission()} catch (err) {promptAction.showToast({message: 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message//弹窗内容});console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}
我们可以在页面顶端调用该函数,如图所示:
如果一切顺利打开软件到调用init函数所在页面就会跳出授权弹窗
至此授权的过程也就完成了,如果用户选择拒绝授权还需要涉及二次授权,因为博主也没有二次授权的相关经验,还请大家自行参考官方文档进行调试
这里贴上二次授权的文档地址: 二次向用户申请授权-申请应用权限-应用权限管控-程序访问控制-安全-系统 - 华为HarmonyOS开发者
2.蓝牙相关操作开发
这里我定义了一个模型,用来存储扫描到的蓝牙设备数据,方便在连接界面展示
GlobalBlueTooth.ets
export interface BlueToothItem {mac: string,name: string
}@ObservedV2
export class GlobalBlueTooth {@Trace blueList: BlueToothItem[] = [] //蓝牙设备数据列表//重置数据方法rest(){this.blueList = [] //蓝牙设备数据列表}
}
1.蓝牙开与关
BlueToothManager.ets
//开启蓝牙
async blueToothOn()
{try {await this.requestBlueToothPermission()await access.enableBluetooth();} catch (err) {promptAction.showToast({message: 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message//弹窗内容});console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}
}//关闭蓝牙
async blueToothOff()
{try {await this.requestBlueToothPermission()await access.disableBluetooth();} catch (err) {promptAction.showToast({message: 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message//弹窗内容});console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}
}
2.蓝牙设备的搜索
BlueToothManager.ets
//检查扫描到的设备mac是否重复
//重复则不添加到表中
blueToothDeviceChecker(data: Array<string>)
{for(let i = 0; i < this.blueToothData.blueList.length; i++){if(this.blueToothData.blueList[i].mac == data[0]){//当检查到重复则跳出循环返回0return 0;}}//当循环结束还没有找到重复则返回1return 1;
}//蓝牙搜索方法
// 定义扫描结果上报回调函数
onReceiveEvent = (data: Array<string>) => {let deviceName = connection.getRemoteDeviceName(data[0])console.info('bluetooth device: '+ JSON.stringify(data));if(this.blueToothDeviceChecker(data)){//当没有扫描到重复时添加数据if(deviceName == ''){console.info('bluetooth device name: 未知设备');this.blueToothData.blueList.unshift({mac: data[0],name: "未知设备"}) //在共享的设备数据末位添加一条扫描到的设备数据}else{console.info('bluetooth device name: '+ deviceName);this.blueToothData.blueList.unshift({mac: data[0],name: deviceName})}}
};public startDiscovery() {try {connection.on('bluetoothDeviceFind', this.onReceiveEvent);} catch (err) {console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}try {// 判断本机设备是否正在进行扫描let scan = connection.isBluetoothDiscovering();if (!scan) {// 若当前不处于扫描过程,则开始扫描设备connection.startBluetoothDiscovery();}} catch (err) {console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}
}public stopDiscovery() {try {// 判断本机设备是否正在进行扫描let scan = connection.isBluetoothDiscovering();if (scan) {// 若当前处于扫描过程,则停止扫描设备connection.stopBluetoothDiscovery();}// 若不再需要使用扫描,可以取消订阅扫描上报结果connection.off('bluetoothDeviceFind', this.onReceiveEvent);} catch (err) {console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}
}
3.获取本机的扫描模式
BlueToothManager.ets
public setScanMode() {try {// 获取当前本机的扫描模式let scanMode: connection.ScanMode = connection.getBluetoothScanMode();console.info('scanMode: ' + scanMode);if (scanMode != connection.ScanMode.SCAN_MODE_CONNECTABLE_GENERAL_DISCOVERABLE) {// 将本机设备的扫描模式设为可被发现和可被连接connection.setBluetoothScanMode(connection.ScanMode.SCAN_MODE_CONNECTABLE_GENERAL_DISCOVERABLE, 0);}} catch (err) {console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}
}
4.获取已经配对的设备信息
public getPairedDevices() {try {// 获取已配对设备信息let devices = connection.getPairedDevices();console.info('pairedDevices: ' + JSON.stringify(devices));// 若已知道设备地址,可主动查询该设备是否是已配对的if (devices.length > 0) {let pairState = connection.getPairState(devices[0]);console.info('device: '+ devices[0] + ' pairState is ' + pairState);}} catch (err) {console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}
}
5.与设备连接
这里采用的是spp连接的方式,我定义了一个sppClientManager的类来进行管理
SppClientManager.ets
import { socket } from '@kit.ConnectivityKit'
import { BusinessError } from '@kit.BasicServicesKit';
import { GlobalType } from '../models/globalType';
import { AppStorageV2 } from '@kit.ArkUI';
import { connection } from '@kit.NetworkKit';class SppClientManager {// 定义客户端的socket idclientNumber: number = -1;//共享连接数据typeData: GlobalType = AppStorageV2.connect(GlobalType, 'TYPE_KEY', () => new GlobalType())!// 发起连接public startConnect(peerDevice: string, deviceName: string): void {// 配置连接参数let option: socket.SppOptions = {uuid: '00001101-0000-1000-8000-00805F9B34FB', // 需要连接的服务端UUID服务,确保服务端支持secure: false,type: socket.SppType.SPP_RFCOMM};console.info('startConnect ' + peerDevice);socket.sppConnect(peerDevice, option, (err, num: number) => {if (err) {console.error('startConnect errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);} else {//连接成功this.typeData.connectType = 1; //修改显示的连接状态this.typeData.connectedDeviceName = deviceName; //修改显示的连接设备名称console.info('startConnect clientNumber: ' + num);this.clientNumber = num;}});console.info('startConnect after ' + peerDevice);}// 发送数据public sendData(sendData: number) {console.info('sendData ' + this.clientNumber);let arrayBuffer = new ArrayBuffer(1);let data = new Uint8Array(arrayBuffer);data[0] = sendData;try {socket.sppWrite(this.clientNumber, arrayBuffer);} catch (err) {console.error('sppWrite errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);return -1; //发现异常,也就是有可能断开连接就返回-1}return 1; //正常发送数据代表还在连接就返回1}// 定义接收数据的回调函数read = (dataBuffer: ArrayBuffer) => {let data = new Uint8Array(dataBuffer);console.info('client data: ' + JSON.stringify(data));};// 接收数据public readData() {try {// 发起订阅socket.on('sppRead', this.clientNumber, this.read);} catch (err) {console.error('readData errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 断开连接public stopConnect() {console.info('closeSppClient ' + this.clientNumber);try {// 取消接收数据订阅socket.off('sppRead', this.clientNumber, this.read);} catch (err) {console.error('off sppRead errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}try {// 从client端断开连接socket.sppCloseClientSocket(this.clientNumber);} catch (err) {console.error('stopConnect errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}
}export const sppClientManager: SppClientManager = new SppClientManager()
BlueToothManager.ets
public connectDevice(device: string) {let pairState = connection.getPairState(device); //查询设备是否配对if(pairState == 2){//设备已配对let deviceName = connection.getRemoteDeviceName(device); //获取设备名称sppClientManager.startConnect(device, deviceName) //发起连接}else{//设备没有配对try {// 发起配对connection.pairDevice(device).then(() => {console.info('pairDevice');}, (error: BusinessError) => {console.error('pairDevice: errCode:' + error.code + ',errMessage' + error.message);});} catch (err) {console.error('startPair: errCode:' + err.code + ',errMessage' + err.message);console.info('mac:' + device);}}
}
当连接设备的时候调用connectDevice函数,传入连接设备的地址一般为 XX:XX:XX:XX:XX:XX
,该地址可以在步骤二蓝牙设备的搜索当中获取
connectDevice函数会先检查该地址设备是否已配对,如果没有配对则会开始配对设备,如果已配对就会调用SppClientManager
类中的startConnect开始连接设备,连接成功后就可以开始收发数据了
6.数据的收发
SppClientManager.ets
// 发送数据public sendData(sendData: number) {console.info('sendData ' + this.clientNumber);let arrayBuffer = new ArrayBuffer(1);let data = new Uint8Array(arrayBuffer);data[0] = sendData;try {socket.sppWrite(this.clientNumber, arrayBuffer);} catch (err) {console.error('sppWrite errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);return -1; //发现异常,也就是有可能断开连接就返回-1}return 1; //正常发送数据代表还在连接就返回1}// 定义接收数据的回调函数read = (dataBuffer: ArrayBuffer) => {let data = new Uint8Array(dataBuffer);console.info('client data: ' + JSON.stringify(data));};// 接收数据public readData() {try {// 发起订阅socket.on('sppRead', this.clientNumber, this.read);} catch (err) {console.error('readData errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}
在这里我定义的sendData函数只能发送数字数据,需要发送其他数据的小伙伴请自行修改
结尾
相信看到这里你已经掌握了鸿蒙蓝牙的常规开发,学会搜索,配对,连接设备,并且收发数据。很感谢你能看到这里,如果觉得我的文章对你有帮助的话请给一个点赞还有收藏谢谢,如果有什么意见欢迎在评论区留言,我们下期再见!