当前位置: 首页 > news >正文

【鸿蒙开发实战】蓝牙功能的开发

前言

断更许久,也是因为没有什么好的素材可以写,最近学了一点鸿蒙开发,因为手头上有一块蓝牙模块(JDY-31),就想着说结合stm32,还有手机app实现一个类似智能家居的系统。说干就干,32的代码比较容易写,很快就完成了,但是鸿蒙蓝牙调试软件的开发让我犯了难。起初,我想要去应用商店找一下有没有现成的软件可以用,找了很久发现要么不好用,没有我想要的功能,要么就是半成品软件,空有其表。于是,我就打算开发一个自己的蓝牙调试器,在开发过程中,我也遇到了许多问题。所有我就写下了这篇文章来跟大家分享一下我的经验,如果有写的不好的地方还请各位大佬批评指正。

正文

首先我们需要清楚在鸿蒙当中怎样去实现蓝牙的功能

1.申请蓝牙权限

这里贴上文档地址:向用户申请授权-申请应用权限-应用权限管控-程序访问控制-安全-系统 - 华为HarmonyOS开发者

通过查看华为官方文档我们可以知道申请蓝牙权限有这些步骤:

image-20250626224549706

首先,我们需要在配置文件当中申请蓝牙权限

如图,我们需要在module.json5配置文件中加入这么一个配置

"requestPermissions": [{"name" : "ohos.permission.ACCESS_BLUETOOTH","reason": "$string:blooth_reason","usedScene": {"abilities": ["FormAbility"],"when":"inuse"}}],

image-20250626225251815

其中reason一项很容易让人搞不懂鼠标移动到reason后,按住control,鼠标左键单击

image-20250626225139038

单击后我们会发现跳转到了string.json,然后在这里仿造格式写上原因就行啦,注意这里name属性对应的就是上面那个配置当中$string:blooth_reason的blooth_reason,这里随意起名字就好,但是要规范,自己能看得懂

image-20250626225204132

在配置文件声明完权限后还需要添加向用户申请权限的代码,代码如下

  // 用户申请权限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函数

image-20250626231123260

代码如下

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);}}

我们可以在页面顶端调用该函数,如图所示:

image-20250626231408151

如果一切顺利打开软件到调用init函数所在页面就会跳出授权弹窗f981d094b52d0e41a8ad1604d21c730

至此授权的过程也就完成了,如果用户选择拒绝授权还需要涉及二次授权,因为博主也没有二次授权的相关经验,还请大家自行参考官方文档进行调试

这里贴上二次授权的文档地址: 二次向用户申请授权-申请应用权限-应用权限管控-程序访问控制-安全-系统 - 华为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函数只能发送数字数据,需要发送其他数据的小伙伴请自行修改

结尾

相信看到这里你已经掌握了鸿蒙蓝牙的常规开发,学会搜索,配对,连接设备,并且收发数据。很感谢你能看到这里,如果觉得我的文章对你有帮助的话请给一个点赞还有收藏谢谢,如果有什么意见欢迎在评论区留言,我们下期再见!

http://www.lryc.cn/news/575600.html

相关文章:

  • Vue3 中 Axios 深度整合指南:从基础到高级实践引言总结
  • WPF Binding 的 Mode 属性
  • 12345政务热线系统:接诉即办,赋能智慧城市治理
  • 大数据赋能智慧城市:从数据洪流到科学规划的“智慧之匙”
  • 【机器学习深度学习】交互式线性回归 demo
  • Trae IDE 大师评测:驾驭 MCP Server - Figma AI Bridge 一键成就前端瑰宝
  • 【Excel数据分析】花垣县事业单位出成绩了,用Excel自带的M语言做一个数据分析
  • 高中成绩可视化平台开发笔记
  • 01【C++ 入门基础】命名空间/域
  • 基于定制开发开源AI智能名片S2B2C商城小程序源码的H5游戏开发模式创新研究
  • Solidity 从 0 到 1 |Web3 开发入门免费共学营
  • 60% 重构项目陷 “越改越烂” 泥潭!
  • 智慧农业app农场监控系统框架搭建
  • 【缓存技术】深入分析如果使用好缓存及注意事项
  • 光场操控新突破!3D 光学信息处理迎来通用 PSF 工程时代--《自然》子刊:无需复杂算法,这一技术让 3D 光学成像实现 “即拍即得”念日
  • 从零开始的云计算生活——第二十四天,重起航帆,初见MySQL数据库
  • Linux中部署Jenkins保姆间教程
  • 编写CSS的格式
  • React:利用计算属性名特点更新表单值
  • Spring Security 安全控制终极指南
  • ubuntu20.04如何给appImage创建快捷方式
  • 【thinkphp5】Session和Cache记录微信accesstoken
  • 【Docker基础】Docker容器管理:docker rm及其参数详解
  • 百度中年危机:一场艰难的突围战
  • 关于单片机的基础知识(一)
  • 苍穹外卖day3--公共字段填充+新增菜品
  • 【LLM安全】MCP(模型上下文协议)及其关键漏洞、技术细节
  • 解锁企业效率革命:Microsoft 365 Copilot 重塑办公新范式
  • 16.1 Python应用容器化终极指南:Dockerfile多阶段构建与安全优化实战
  • leetcode-2311.小于等于k的最长二进制子序列