【HarmonyOS】鸿蒙应用低功耗蓝牙BLE的使用心得 (二)

server/2024/12/2 14:50:34/

BLE__0">【HarmonyOS】鸿蒙应用低功耗蓝牙BLE的使用心得 (二)

一、前言

目前鸿蒙应用的实现逻辑,基本都是参考和移植Android端来实现。针对BLE低功耗蓝牙来说,在鸿蒙化的实现过程中。我们发现了,鸿蒙独有的优秀点,也发现了不一致带来的问题。

并且因为鸿蒙系统还在迭代中,难免有一些bug的存在。目前BLE低功耗蓝牙主流程环节是没有问题的。

在这里插入图片描述

鸿蒙整体的实现流程与Android基本是一致。但是在BLE中有一个点需要特别注意。

当周边设备(外围设备)开启广播时,其实没有前置条件,不需要先开启GATT服务器和注册相关服务。但是有些业务逻辑是如此,导致我们开发的时候,惯性思维。实际上鸿蒙BLE广播流程设计上,并不需要前置条件。

当周边设备持续发送广播,例如智能腕表发送广播,这时候中心设备(中央设备),例如手机才能通过BLE扫描去去识别到该设备。然后才是连接和信息传递的过程。

二、目前已知鸿蒙和Android不一致的实现点梳理:

1.安卓中设置低功耗蓝牙广播,其中有参数可以设置超时时间:
AdvertiseSettings.Builder.SetTimeout()。
但是对标鸿蒙中设置广播,文档中并没有该参数。鸿蒙的低功耗蓝牙广播,无法设置超时时间。

2.鸿蒙中的低功耗蓝牙广播设置对象,ble.AdvertiseSetting中只能设置txPower广播强度。设置广播的模式,对标Android的三个广播模式:

在均衡电源模式下执行蓝牙LE广播:
AdvertiseSettings#ADVERTISE_MODE_BALANCED

在低延迟,高功率模式下执行蓝牙LE广播:
AdvertiseSettings#ADVERTISE_MODE_LOW_LATENCY

在低功耗模式下执行蓝牙LE广播
:AdvertiseSettings#ADVERTISE_MODE_LOW_POWER

鸿蒙这边可以使用interval参数来设置广播频率
ADVERTISE_MODE_LOW_LATENCY(低延迟,广播间隔100ms)对应interval的值为 160
ADVERTISE_MODE_BALANCED(均衡模式,广播间隔250ms)对应interval的值为 400
ADVERTISE_MODE_LOW_POWER(低功耗,广播间隔1s)对应interval的值为 1600

BLEDEMO_34">三、BLE低功耗蓝牙DEMO项目示例参考:

此项目实际上是从官方文档基础上进行了扩展。文档所示例的样本代码比较割裂,只是针对API功能的讲解,在不知道BLE完成业务流程的基础上,我们是不清楚如何调用的。所以我们开玩笑的角度说,基本上当你会这个技术点了,官方文档也就能看懂了。

项目整个框架采用管理对象进行BLE功能的封装:

GattServermanager
作为GATT服务器(周边设备)的管理类,实现服务器的初始化和服务的注册。

GattClientManager
作为周边设备客户端管理类,实现链接和数据读写传递。

BleScanManager
作为BLE扫描管理类,实现扫描和结果的处理。

BleAdvertisingManager
作为BLE广播类,实现广播的处理,特征值和描述符的生成。

BLEMgr
作为总管理类,业务逻辑的感知层,业务直接调用BLEMgr实现低功耗蓝牙接口的调用。

在这里插入图片描述
Index.ets 启动页

import { CommonTextModifier } from '../common/CommonTextModifier'
import { BLEMgr } from '../mgr/BLEMgr';
import { promptAction } from '@kit.ArkUI';
import { PermissionsUtil } from '../utils/PermissionsUtil';

struct Index { isOpenBluetooth: boolean = false;private mBLEMgr: BLEMgr = new BLEMgr();txtModifier: CommonTextModifier = new CommonTextModifier()async aboutToAppear() {let isHave: boolean = await PermissionsUtil.requestPermission();if(isHave){this.isOpenBluetooth = this.mBLEMgr.getBluetoothState();}else{this.toSysSettingPage();}}private toSysSettingPage(){globalThis.sysContext.startAbility({bundleName: 'com.huawei.hmos.settings',abilityName: 'com.huawei.hmos.settings.MainAbility',// com.huawei.hmos.settings.AppInfoAbilityuri: 'application_info_entry', //application_settings   application_info_entryparameters: {pushParams: globalThis.sysContext.abilityInfo.bundleName // 应用包名com.example.tosettingdemo  'uiAbilityContext.abilityInfo.bundleName'}});}onClickStart = async ()=>{let isHave: boolean = await PermissionsUtil.requestPermission();if(isHave){this.mBLEMgr.startBluetooth((str: string)=>{let content: string = "";if (str == 'STATE_ON') {content = "蓝牙已开启";}else{content = "开启错误:" + str;}promptAction.showToast({message: content});});}else{this.toSysSettingPage();}}onClickClose = async ()=>{let isHave: boolean = await PermissionsUtil.requestPermission();if(isHave){this.mBLEMgr.closeBluetooth((str: string)=>{let content: string = "";if (str == 'STATE_OFF') {content = "蓝牙已关闭";}else{content = "关闭错误:" + str;}promptAction.showToast({message: content});});}else{this.toSysSettingPage();}}onClickStartAdv = ()=>{this.mBLEMgr.startAdvertising((advState: string)=>{let content: string = "";if(advState == "STARTED"){content = "广播已开启";}else{content = "广播错误:" + advState;}promptAction.showToast({message: content});});}onClickCloseAdv = ()=>{this.mBLEMgr.stopAdvertising((str: string)=>{promptAction.showToast({message: str});});}onClickStartScan = ()=>{this.mBLEMgr.startScan();}onClickCloseScan = ()=>{this.mBLEMgr.stopScan();}onClickStartServer = ()=>{this.mBLEMgr.registerServer((res: string)=>{promptAction.showToast({message: res});});}onClickCloseServer = ()=>{this.mBLEMgr.unRegisterServer((res: string)=>{promptAction.showToast({message: res});});} LineView(){Line().width("100%").height(px2vp(2)).backgroundColor(Color.Black).margin({top: px2vp(100)})}build() {Column() {Column(){Text(this.isOpenBluetooth ? "蓝牙状态: 已开启" : "蓝牙状态: 已关闭")Text("蓝牙设备名:" + this.mBLEMgr.getCurrentDeviceName())}Text("开启蓝牙").attributeModifier(this.txtModifier).onClick(this.onClickStart)Text("关闭蓝牙").attributeModifier(this.txtModifier).onClick(this.onClickClose)this.LineView()Text("启动服务").attributeModifier(this.txtModifier).onClick(this.onClickStartServer)Text("关闭服务").attributeModifier(this.txtModifier).onClick(this.onClickCloseServer)Text("开启广播").attributeModifier(this.txtModifier).onClick(this.onClickStartAdv)Text("关闭广播").attributeModifier(this.txtModifier).onClick(this.onClickCloseAdv)this.LineView()Text("开启扫描").attributeModifier(this.txtModifier).onClick(this.onClickStartScan)Text("关闭扫描").attributeModifier(this.txtModifier).onClick(this.onClickCloseScan)}.height('100%').width('100%')}
}

BLEMgr.ets

import bleAdvertisingManager from "./BleAdvertisingManager";
import bleScanManager from "./BleScanManager";
import { access } from '@kit.ConnectivityKit';
import gattServerManager from "./GattServerManager";
import { connection } from '@kit.ConnectivityKit';
import { BusinessError } from "@kit.BasicServicesKit";const TAG: string = "BLEMgr";export class BLEMgr {public getBluetoothState(): boolean {let state = access.getState();return this.getStateName(state) == "STATE_ON" ? true : false;}/*** 当前设备蓝牙设备名称*/public getCurrentDeviceName(){let localName: string = "";try {localName = connection.getLocalName();console.info(TAG, 'getCurrentDeviceName localName: ' + localName);} catch (err) {console.error(TAG, 'getCurrentDeviceName errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}return localName;}/*** 蓝牙广播状态* @param state* @returns*/public getAdvState(state: number){switch (state) {case 1:// 首次启动广播后的状态。return 'STARTED';case 2:// 临时启动广播后的状态。return 'ENABLED';case 3:// 临时停止广播后的状态。return 'DISABLED';case 4:// 完全停止广播后的状态。return 'STOPPED';default:return 'unknown status';}}/*** 蓝牙开启状态* @param state* @returns*/private getStateName(state: number): string {switch (state) {case 0:// 蓝牙已关闭。return 'STATE_OFF'; ;case 1:// 蓝牙正在打开。return 'STATE_TURNING_ON';case 2:// 蓝牙已打开。return 'STATE_ON';case 3:// 蓝牙正在关闭。return 'STATE_TURNING_OFF';case 4:// 蓝牙正在打开LE-only模式。return 'STATE_BLE_TURNING_ON';case 5:// 蓝牙正处于LE-only模式。return 'STATE_BLE_ON';case 6:// 蓝牙正在关闭LE-only模式。return 'STATE_BLE_TURNING_OFF';default:return 'unknown status';}}/*** 开启蓝牙*/public startBluetooth(callback: (str: string)=> void){try {access.enableBluetooth();} catch (err) {let errStr: string = JSON.stringify(err);console.info(TAG, 'startBluetooth enableBluetooth err: ' + errStr);callback(errStr);}access.on('stateChange', (data) => {let btStateMessage = this.getStateName(data);callback(btStateMessage);if (btStateMessage == 'STATE_ON') {access.off('stateChange');}console.info('bluetooth statues: ' + btStateMessage);});}/*** 关闭蓝牙*/public closeBluetooth(callback: (str: string)=> void){access.disableBluetooth();access.on('stateChange', (data) => {let btStateMessage = this.getStateName(data);callback(btStateMessage);if (btStateMessage == 'STATE_OFF') {access.off('stateChange');}console.info("bluetooth statues: " + btStateMessage);})}/*** 创建GATT服务器,注册服务*/public registerServer(callBack: (str: string)=> void){gattServerManager.registerServer(callBack);}/*** 删除服务,关闭GATT服务器*/public unRegisterServer(callBack: (str: string)=> void){gattServerManager.unRegisterServer(callBack);}/*** 开启广播*/public async startAdvertising(callBack: (state: string)=> void) {await bleAdvertisingManager.startAdvertising((state: number)=>{let advState: string = this.getAdvState(state);callBack(advState);});}/*** 关闭广播*/public async stopAdvertising(callBack: (str: string)=> void) {await bleAdvertisingManager.stopAdvertising(callBack);}/*** 开始扫描*/public startScan() {bleScanManager.startScan();}/*** 关闭扫描*/public stopScan() {bleScanManager.stopScan();}}

GattServermanager.ets

import { ble } from '@kit.ConnectivityKit';
import { constant } from '@kit.ConnectivityKit';
import { BusinessError } from '@kit.BasicServicesKit';const TAG: string = 'GattServerManager';export class GattServerManager {gattServer: ble.GattServer | undefined = undefined;connectState: ble.ProfileConnectionState = constant.ProfileConnectionState.STATE_DISCONNECTED;myServiceUuid: string = '00001810-0000-1000-8000-00805F9B34FB';myCharacteristicUuid: string = '00001820-0000-1000-8000-00805F9B34FB';myFirstDescriptorUuid: string = '00002902-0000-1000-8000-00805F9B34FB'; // 2902一般用于notification或者indicationmySecondDescriptorUuid: string = '00002903-0000-1000-8000-00805F9B34FB';// 构造BLEDescriptorprivate initDescriptor(des: string, value: ArrayBuffer): ble.BLEDescriptor {let descriptor: ble.BLEDescriptor = {serviceUuid: this.myServiceUuid,characteristicUuid: this.myCharacteristicUuid,descriptorUuid: des,descriptorValue: value};return descriptor;}// 构造BLECharacteristicprivate initCharacteristic(): ble.BLECharacteristic {let descriptors: Array<ble.BLEDescriptor> = [];let descBuffer = new ArrayBuffer(2);let descValue = new Uint8Array(descBuffer);descValue[0] = 31;descValue[1] = 32;descriptors[0] = this.initDescriptor(this.myFirstDescriptorUuid, new ArrayBuffer(2));descriptors[1] = this.initDescriptor(this.mySecondDescriptorUuid, descBuffer);let charBuffer = new ArrayBuffer(2);let charValue = new Uint8Array(charBuffer);charValue[0] = 21;charValue[1] = 22;let characteristic: ble.BLECharacteristic = {serviceUuid: this.myServiceUuid,characteristicUuid: this.myCharacteristicUuid,characteristicValue: charBuffer,descriptors: descriptors};return characteristic;}// 1. 订阅连接状态变化事件public onGattServerStateChange() {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}try {this.gattServer.on('connectionStateChange', (stateInfo: ble.BLEConnectionChangeState) => {let state = '';switch (stateInfo.state) {case 0:state = 'DISCONNECTED';break;case 1:state = 'CONNECTING';break;case 2:state = 'CONNECTED';break;case 3:state = 'DISCONNECTING';break;default:state = 'undefined';break;}console.info(TAG, 'onGattServerStateChange: device=' + stateInfo.deviceId + ', state=' + state);});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 2. server端注册服务时调用public registerServer(callBack: (str: string)=> void) {let characteristics: Array<ble.BLECharacteristic> = [];let characteristic = this.initCharacteristic();characteristics.push(characteristic);let gattService: ble.GattService = {serviceUuid: this.myServiceUuid,isPrimary: true,characteristics: characteristics};console.info(TAG, 'registerServer ' + this.myServiceUuid);try {this.gattServer = ble.createGattServer(); // 2.1 构造gattServer,后续的交互都需要使用该实例this.onGattServerStateChange(); // 2.2 订阅连接状态this.gattServer.addService(gattService);callBack("服务成功");} catch (err) {callBack("服务失败:" + JSON.stringify(err));console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 3. 订阅来自gattClient的读取特征值请求时调用public onCharacteristicRead() {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}console.info(TAG, 'onCharacteristicRead');try {this.gattServer.on('characteristicRead', (charReq: ble.CharacteristicReadRequest) => {let deviceId: string = charReq.deviceId;let transId: number = charReq.transId;let offset: number = charReq.offset;console.info(TAG, 'receive characteristicRead');let rspBuffer = new ArrayBuffer(2);let rspValue = new Uint8Array(rspBuffer);rspValue[0] = 21;rspValue[1] = 22;let serverResponse: ble.ServerResponse = {deviceId: deviceId,transId: transId,status: 0, // 0表示成功offset: offset,value: rspBuffer};try {this.gattServer?.sendResponse(serverResponse);} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 4. 订阅来自gattClient的写入特征值请求时调用public onCharacteristicWrite() {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}console.info(TAG, 'onCharacteristicWrite');try {this.gattServer.on('characteristicWrite', (charReq: ble.CharacteristicWriteRequest) => {let deviceId: string = charReq.deviceId;let transId: number = charReq.transId;let offset: number = charReq.offset;console.info(TAG, 'receive characteristicWrite: needRsp=' + charReq.needRsp);if (!charReq.needRsp) {return;}let rspBuffer = new ArrayBuffer(0);let serverResponse: ble.ServerResponse = {deviceId: deviceId,transId: transId,status: 0, // 0表示成功offset: offset,value: rspBuffer};try {this.gattServer?.sendResponse(serverResponse);} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 5. 订阅来自gattClient的读取描述符请求时调用public onDescriptorRead() {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}console.info(TAG, 'onDescriptorRead');try {this.gattServer.on('descriptorRead', (desReq: ble.DescriptorReadRequest) => {let deviceId: string = desReq.deviceId;let transId: number = desReq.transId;let offset: number = desReq.offset;console.info(TAG, 'receive descriptorRead');let rspBuffer = new ArrayBuffer(2);let rspValue = new Uint8Array(rspBuffer);rspValue[0] = 31;rspValue[1] = 32;let serverResponse: ble.ServerResponse = {deviceId: deviceId,transId: transId,status: 0, // 0表示成功offset: offset,value: rspBuffer};try {this.gattServer?.sendResponse(serverResponse);} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 6. 订阅来自gattClient的写入描述符请求时调用public onDescriptorWrite() {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}console.info(TAG, 'onDescriptorWrite');try {this.gattServer.on('descriptorWrite', (desReq: ble.DescriptorWriteRequest) => {let deviceId: string = desReq.deviceId;let transId: number = desReq.transId;let offset: number = desReq.offset;console.info(TAG, 'receive descriptorWrite: needRsp=' + desReq.needRsp);if (!desReq.needRsp) {return;}let rspBuffer = new ArrayBuffer(0);let serverResponse: ble.ServerResponse = {deviceId: deviceId,transId: transId,status: 0, // 0表示成功offset: offset,value: rspBuffer};try {this.gattServer?.sendResponse(serverResponse);} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 7. server端删除服务,不再使用时调用public unRegisterServer(callBack: (str: string)=> void) {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}console.info(TAG, 'unRegisterServer ' + this.myServiceUuid);try {this.gattServer.removeService(this.myServiceUuid); // 7.1 删除服务callBack("关闭服务");this.gattServer.off('connectionStateChange', (stateInfo: ble.BLEConnectionChangeState) => { // 7.2 取消订阅连接状态});this.gattServer.close() // 7.3 如果不再使用此gattServer,则需要close} catch (err) {callBack("关闭失败:" + JSON.stringify(err));console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}
}let gattServerManager = new GattServerManager();
export default gattServerManager as GattServerManager;

其余GattClientManager BleScanManager BleAdvertisingManager 类可以参考官方文档。


http://www.ppmy.cn/server/140591.html

相关文章

Linux上的各种查询

在Linux中&#xff0c;有许多命令可以用于查询系统信息、文件和进程等。以下是一些常用的查询命令及其简要介绍&#xff1a; ls: 用途&#xff1a;列出目录中的文件和子目录。示例&#xff1a;ls -l&#xff08;以详细格式显示&#xff09;或 ls -a&#xff08;包括隐藏文件&am…

LeetCode题练习与总结:字典序排数--386

一、题目描述 给你一个整数 n &#xff0c;按字典序返回范围 [1, n] 内所有整数。 你必须设计一个时间复杂度为 O(n) 且使用 O(1) 额外空间的算法。 示例 1&#xff1a; 输入&#xff1a;n 13 输出&#xff1a;[1,10,11,12,13,2,3,4,5,6,7,8,9]示例 2&#xff1a; 输入&am…

选项式api和组合式api

在 Vue 3 中&#xff0c;选项式 API&#xff08;Options API&#xff09;和 组合式 API&#xff08;Composition API&#xff09;是两种不同的编写组件的方式。Vue 3 引入了组合式 API&#xff0c;旨在改善 Vue 2.x 中的选项式 API 的一些限制&#xff0c;特别是在大型项目中&a…

【LeetCode】【算法】48. 旋转图像

LeetCode 48. 旋转图像 题目描述 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 思路 思路&#xff1a;再次拜见K神&#xf…

OpenAI大事记;GPT到ChatGPT参数量进化

目录 OpenAI大事记 GPT到ChatGPT参数量进化 OpenAI大事记 GPT到ChatGPT参数量进化 ChatGPT是从初代 GPT逐渐演变而来的。在进化的过程中,GPT系列模型的参数数量呈指数级增长,从初代GPT的1.17亿个参数,到GPT-2的15 亿个参数,再到 GPT-3的1750 亿个参数。模型越来越大,训练…

走进算法大门---双指针问题(一)

一.双指针算法介绍 概念&#xff1a;双指针是指在遍历数据结构&#xff08;如数组、链表等&#xff09;时使用两个指针&#xff0c;通过特定的移动规则来解决问题。这两个指针可以同向移动&#xff0c;也可以相向移动。 同向双指针&#xff1a;常用于解决需要两个位置信息的问…

线性代数:Matrix2x2和Matrix3x3

今天整理自己的框架代码&#xff0c;将Matrix2x2和Matrix3x3给扩展了一下&#xff0c;发现网上unity数学计算相关挺少的&#xff0c;所以记录一下。 首先扩展Matrix2x2&#xff1a; using System.Collections; using System.Collections.Generic; using Unity.Mathemati…

释放专利力量:Patently 如何利用向量搜索和 NLP 简化协作

作者&#xff1a;来自 Elastic Matt Scourfield, Andrew Crothers, Brian Lambert 组织依靠知识产权 (IP) 来推动创新、保持竞争优势并创造收入来源。对于希望将新产品推向市场的公司来说&#xff0c;弄清楚谁拥有哪些专利是一项必不可少的能力。搜索数百万项专利可能既困难又耗…