> 技术文档 > 开源 Arkts 鸿蒙应用 开发(十八)通讯--Ble低功耗蓝牙服务器

开源 Arkts 鸿蒙应用 开发(十八)通讯--Ble低功耗蓝牙服务器

 文章的目的为了记录使用Arkts 进行Harmony app 开发学习的经历。本职为嵌入式软件开发,公司安排开发app,临时学习,完成app的开发。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。

 相关链接:

开源 Arkts 鸿蒙应用 开发(一)工程文件分析-CSDN博客

开源 Arkts 鸿蒙应用 开发(二)封装库.har制作和应用-CSDN博客

开源 Arkts 鸿蒙应用 开发(三)Arkts的介绍-CSDN博客

开源 Arkts 鸿蒙应用 开发(四)布局和常用控件-CSDN博客

开源 Arkts 鸿蒙应用 开发(五)控件组成和复杂控件-CSDN博客

开源 Arkts 鸿蒙应用 开发(六)数据持久--文件和首选项存储-CSDN博客

开源 Arkts 鸿蒙应用 开发(七)数据持久--sqlite关系数据库-CSDN博客

开源 Arkts 鸿蒙应用 开发(八)多媒体--相册和相机-CSDN博客

开源 Arkts 鸿蒙应用 开发(九)通讯--tcp客户端-CSDN博客

开源 Arkts 鸿蒙应用 开发(十)通讯--Http-CSDN博客

开源 Arkts 鸿蒙应用 开发(十一)证书和包名修改-CSDN博客

开源 Arkts 鸿蒙应用 开发(十二)传感器的使用-CSDN博客

开源 Arkts 鸿蒙应用 开发(十三)音频--MP3播放_arkts avplayer播放音频 mp3-CSDN博客

开源 Arkts 鸿蒙应用 开发(十四)线程--任务池(taskpool)-CSDN博客

开源 Arkts 鸿蒙应用 开发(十五)自定义绘图控件--仪表盘-CSDN博客

开源 Arkts 鸿蒙应用 开发(十六)自定义绘图控件--波形图-CSDN博客

开源 Arkts 鸿蒙应用 开发(十七)通讯--http多文件下载-CSDN博客

开源 Arkts 鸿蒙应用 开发(十八)通讯--Ble低功耗蓝牙服务器-CSDN博客

 推荐链接:

开源 java android app 开发(一)开发环境的搭建-CSDN博客

开源 java android app 开发(二)工程文件结构-CSDN博客

开源 java android app 开发(三)GUI界面布局和常用组件-CSDN博客

开源 java android app 开发(四)GUI界面重要组件-CSDN博客

开源 java android app 开发(五)文件和数据库存储-CSDN博客

开源 java android app 开发(六)多媒体使用-CSDN博客

开源 java android app 开发(七)通讯之Tcp和Http-CSDN博客

开源 java android app 开发(八)通讯之Mqtt和Ble-CSDN博客

开源 java android app 开发(九)后台之线程和服务-CSDN博客

开源 java android app 开发(十)广播机制-CSDN博客

开源 java android app 开发(十一)调试、发布-CSDN博客

开源 java android app 开发(十二)封库.aar-CSDN博客

推荐链接:

开源C# .net mvc 开发(一)WEB搭建_c#部署web程序-CSDN博客

开源 C# .net mvc 开发(二)网站快速搭建_c#网站开发-CSDN博客

开源 C# .net mvc 开发(三)WEB内外网访问(VS发布、IIS配置网站、花生壳外网穿刺访问)_c# mvc 域名下不可訪問內網,內網下可以訪問域名-CSDN博客

开源 C# .net mvc 开发(四)工程结构、页面提交以及显示_c#工程结构-CSDN博客

开源 C# .net mvc 开发(五)常用代码快速开发_c# mvc开发-CSDN博客

本章内容主要演示了蓝牙广播调试应用,主要功能是通过BLE广播发送包含设备ID的心率数据。

1.工程结构

2.源码解析

3.演示效果

4.工程下载网址

一、工程结构:

BluetoothServer.ets - 主界面和业务逻辑

AdvertiseBluetoothViewModel.ets - 蓝牙广播和GATT服务管理

AdvData.ets - 广播数据构造

辅助工具类:ArrayBufferUtils, MathUtils, Logger
 

二、源码解析

2.1  BluetoothServer.ets主界面组件,主要功能:提供UI界面让用户输入ID,管理广播状态,处理权限请求,协调视图模型操作。

函数说明:

toggleAdvertiser() - 切换广播状态

toggleHeartRate() - 开始/停止心率模拟

stringCheck() - 验证用户输入的ID格式

import { abilityAccessCtrl, common, Permissions } from \'@kit.AbilityKit\';import { promptAction } from \'@kit.ArkUI\';import { util } from \'@kit.ArkTS\';import { BusinessError } from \'@kit.BasicServicesKit\';import { Logger } from \'../utils/Logger\';import advertiseBluetoothViewModel from \'../viewmodel/AdvertiseBluetoothViewModel\';import MathUtils from \'../utils/MathUtils\';// ble.tsconst MIN_HEART_RATE = 40;const MAX_HEART_RATE = 200;const PERMISSION_LIST: Array = [ \'ohos.permission.ACCESS_BLUETOOTH\'];function reqPermissionFromUser(permissions: Array, context: common.UIAbilityContext): void { const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); atManager.requestPermissionsFromUser(context, permissions).then((data) => { Logger.info(`data:${JSON.stringify(data)}`); }).catch((err: BusinessError) => { Logger.error(`requestPermissionsFromUser fail: err = ${JSON.stringify(err)}`); })}@Entry@Componentexport struct BluetoothServer { @StorageLink(\'deviceId\') @Watch(\'onDeviceIdChange\') deviceId: string = \'\'; @StorageLink(\'bluetoothEnable\') @Watch(\'onBluetoothEnableChange\') bluetoothEnable: boolean = false; @State startAdvertiserState: boolean = false; @State localName: string = \'\'; @State heartRate: number = -1; private mIntervalId: number = -1; @State myid: string = \'\' // ID private idArray: Uint8Array = new Uint8Array([0x00, 0x00, 0x00,0x0]); onDeviceIdChange(): void { Logger.info(`onDeviceIdChange: deviced = ${this.deviceId}`); } onBluetoothEnableChange(): void { if (this.bluetoothEnable) { this.toggleAdvertiser(); } else { advertiseBluetoothViewModel.stopAdvertiser(); this.toggleHeartRate(false); this.startAdvertiserState = false; promptAction.showToast({ message: $r(\'app.string.bluetooth_off_Stop_heart_rate_broadcast\'), duration: 2000 }); } } stringToBytes(val: string): number { let that = new util.TextEncoder(\'utf-8\'); let result = that.encodeInto(val); return result?.length ?? 0; } toggleAdvertiser(): void { if (this.startAdvertiserState) { advertiseBluetoothViewModel.stopAdvertiser(); this.toggleHeartRate(false); this.startAdvertiserState = false; promptAction.showToast({ message: $r(\'app.string.ble_heart_rate_broadcast_is_disabled\'), duration: 2000 }); } else { let BLEName: string = advertiseBluetoothViewModel.getLocalName(); if (this.stringToBytes(BLEName) > 22) { promptAction.showToast({ message: $r(\'app.string.change_bluetooth_name\'), duration: 2000 }); return; } let ret = advertiseBluetoothViewModel.startAdvertiser(this.idArray); if (ret) { this.localName = BLEName; this.toggleHeartRate(true); this.startAdvertiserState = true; promptAction.showToast({ message: $r(\'app.string.the_ble_heart_rate_broadcast_has_been_enabled\'), duration: 2000 }); } } } toggleHeartRate(open: boolean): void { clearInterval(this.mIntervalId); if (open) { this.mIntervalId = setInterval(() => { this.heartRate = MathUtils.getRandomInt(MIN_HEART_RATE, MAX_HEART_RATE); if (this.deviceId) { advertiseBluetoothViewModel.notifyCharacteristicChanged(this.deviceId, this.heartRate); } }, 1000) } } aboutToAppear(): void { const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; reqPermissionFromUser(PERMISSION_LIST, context); } aboutToDisappear(): void { advertiseBluetoothViewModel.stopAdvertiser(); } stringCheck():boolean { // 验证myid if (this.myid.length != 8) { promptAction.showToast({ message: \'请重新输入,ID为1个字节\', duration: 2000 }); return false; } return true; } build() { Column() { Text(\'BLE广播调试\') .fontSize(24) .fontWeight(FontWeight.Bold) .margin({ top: 20, bottom: 30 }) TextInput({ placeholder: \'ID\' ,text:\'12345678\'}) .width(\'90%\') .height(40) .margin({ bottom: 10 }) .onChange((value: string) => { this.myid = value }) // 按钮行 Row() { Button(\'发送\') .width(\'45%\') .height(50) .fontSize(18) .onClick(() => { if(!this.stringCheck()) {  return; } for (let i = 0; i  { this.toggleAdvertiser() // 停止按钮点击事件 console.log(\'停止按钮被点击\') }) } .width(\'90%\') .justifyContent(FlexAlign.SpaceBetween) } .width(\'100%\') .height(\'100%\') .padding(16) .backgroundColor(\'#F5F5F5\') }}

2.2  AdvertiseBluetoothViewModel.ets组件实现:蓝牙状态管理,广播的启动和停止,GATT服务管理,连接状态监控

简单函数说明:

startAdvertiser() - 配置并启动BLE广播

stopAdvertiser() - 停止广播

notifyCharacteristicChanged() - 通知客户端特征值变化

 /** * 最佳实践:低功耗蓝牙开发实践 */// [Start access1]import { access, ble, connection, constant } from \'@kit.ConnectivityKit\';// [End access1]import { promptAction } from \'@kit.ArkUI\';import ArrayBufferUtils from \'../utils/ArrayBufferUtils\';import { Logger } from \'../utils/Logger\';import { BusinessError } from \'@kit.BasicServicesKit\';import advData from \'../viewmodel/AdvData\';const uiContext: UIContext | undefined = AppStorage.get(\'uiContext\');interface CharacteristicModel { serviceUuid: string, characteristicUuid: string, characteristicValue: ArrayBufferLike, descriptors: Array}interface NotifyCharacteristicModel { serviceUuid: string, characteristicUuid: string, characteristicValue: ArrayBufferLike, confirm: boolean}export class AdvertiseBluetoothViewModel { private mGattServer: ble.GattServer | undefined; private advHandle: number = 0xFF; // 初始的无效值 private stateChangeFunc = (data: access.BluetoothState): void => { if (data === access.BluetoothState.STATE_ON) { AppStorage.setOrCreate(\'bluetoothEnable\', true); } else if (data === access.BluetoothState.STATE_OFF) { AppStorage.setOrCreate(\'bluetoothEnable\', false); } } private connectionStateChangeFunc = (data: ble.BLEConnectionChangeState): void => { if (data) { if (data.state === constant.ProfileConnectionState.STATE_CONNECTED) { let deviceId = data.deviceId; AppStorage.setOrCreate(\'deviceId\', deviceId); } else if (data.state === constant.ProfileConnectionState.STATE_DISCONNECTED) { AppStorage.setOrCreate(\'deviceId\', \'\'); } } } isBluetoothEnabled(): boolean { const state: access.BluetoothState = access.getState(); Logger.info(`isBluetoothEnabled: state = ${state}`); if (state === access.BluetoothState.STATE_ON || state === access.BluetoothState.STATE_TURNING_ON) { return true; } return false; } enableBluetooth() { try { this.onBTStateChange(); access.enableBluetooth(); } catch (err) { Logger.error(`enableBluetooth: err = ${JSON.stringify(err)}`); } } disableBluetooth() { try { this.offBTStateChange(); access.disableBluetooth(); } catch (err) { Logger.error(`disableBluetooth: err = ${JSON.stringify(err)}`); } } getLocalName(): string { let localName = \'\'; try { localName = connection.getLocalName(); } catch (err) { Logger.error(`getLocalName: err = ${JSON.stringify(err)}`); } return localName; } // [Start tooth1] startAdvertiser(PhoneId: Uint8Array): boolean { if (!this.isBluetoothEnabled()) { this.enableBluetooth(); uiContext?.getPromptAction().showToast({ message: $r(\'app.string.bluetooth_enabled_please_wait\'), duration: 2000 }) return false; } /* // Create a GattServer instance this.mGattServer = ble.createGattServer(); // [StartExclude tooth1] let descriptors: Array = []; const arrayBuffer = ArrayBufferUtils.byteArray2ArrayBuffer([11]); const descriptor: ble.BLEDescriptor = { serviceUuid: \'0000180D-0000-1000-8000-00805F9B34FB\', characteristicUuid: \'00002A37-0000-1000-8000-00805F9B34FB\', descriptorUuid: \'00002902-0000-1000-8000-00805F9B34FB\', descriptorValue: arrayBuffer } descriptors[0] = descriptor; let characteristics: Array = []; const arrayBufferC = ArrayBufferUtils.byteArray2ArrayBuffer([1]); let characteristic: ble.BLECharacteristic = { serviceUuid: \'0000180D-0000-1000-8000-00805F9B34FB\', characteristicUuid: \'00002A37-0000-1000-8000-00805F9B34FB\', characteristicValue: arrayBufferC, descriptors: descriptors } characteristics[0] = characteristic; // [EndExclude tooth1] // Define the heart rate beating service const service: ble.GattService = { serviceUuid: \'0000180D-0000-1000-8000-00805F9B34FB\', isPrimary: true, characteristics: characteristics, includeServices: [] } try { // Add a service this.mGattServer.addService(service); } catch (err) { Logger.error(`addService: err = ${JSON.stringify(err)}`); } */ try { // The status of the subscription connection service this.onConnectStateChange(); // [StartExclude tooth1] let setting: ble.AdvertiseSetting = { interval: 160, txPower: 1, connectable: false } /* let advData: ble.AdvertiseData = { serviceUuids: [\'0000180D-0000-1000-8000-00805F9B34FB\'], manufactureData: [], serviceData: [], includeDeviceName: true } */ let recv = advData.CreateData(PhoneId); let manufactureValueBuffer: Uint8Array = new Uint8Array([ 0x0,0x0,0x0,0x0,0x0, 0x0,0x0,0x0,0x0,0x0, 0x0,0x0,0x0,0x0,0x0, 0x0,0x0,0x0,0x0,0x0, 0x0,0x0,0x0 ]);//比协议多1个字节的,设置不可连接后,长度不够 for (let i = 0; i < manufactureValueBuffer.length-1; i++) { manufactureValueBuffer[i+1] = recv[i]; } let manufactureDataUnit: ble.ManufactureData = { manufactureId: 0x0006, manufactureValue: manufactureValueBuffer.buffer }; let advPacket: ble.AdvertiseData = { serviceUuids: [], manufactureData: [manufactureDataUnit], serviceData: [], includeDeviceName: false // 表示是否携带设备名,可选参数。注意:带上设备名时,容易导致广播报文长度超出31个字节,使得广播启动失败 }; let advResponse: ble.AdvertiseData = { serviceUuids: [\'0000180D-0000-1000-8000-00805F9B34FB\'], manufactureData: [], serviceData: [] } // [EndExclude tooth1] ble.startAdvertising(setting, advPacket, advResponse); return true; } catch (err) { Logger.error(`startAdvertiser: err = ${JSON.stringify(err)}`); } return false } // [End tooth1] stopAdvertiser() { ble.stopAdvertising(); /* if (this.mGattServer) { try { this.offConnectStateChange(); ble.stopAdvertising(); this.disableBluetooth(); } catch (err) { Logger.error(`stopAdvertiser: err = ${JSON.stringify(err)}`); } } * */ } // [Start not_char] notifyCharacteristicChanged(deviceId: string, heartRate: number) { if (!deviceId) { return; } if (this.mGattServer) { try { let descriptors: Array = []; let arrayBuffer = ArrayBufferUtils.byteArray2ArrayBuffer([11]); let descriptor: ble.BLEDescriptor = { serviceUuid: \'0000180D-0000-1000-8000-00805F9B34FB\', characteristicUuid: \'00002A37-0000-1000-8000-00805F9B34FB\', descriptorUuid: \'00002902-0000-1000-8000-00805F9B34FB\', descriptorValue: arrayBuffer } descriptors[0] = descriptor; let arrayBufferC = ArrayBufferUtils.byteArray2ArrayBuffer([0x00, heartRate]); let characteristic: CharacteristicModel = { serviceUuid: \'0000180D-0000-1000-8000-00805F9B34FB\', characteristicUuid: \'00002A37-0000-1000-8000-00805F9B34FB\', characteristicValue: arrayBufferC, descriptors: descriptors } let notifyCharacteristic: NotifyCharacteristicModel = { serviceUuid: \'0000180D-0000-1000-8000-00805F9B34FB\', characteristicUuid: \'00002A37-0000-1000-8000-00805F9B34FB\', characteristicValue: characteristic.characteristicValue, confirm: false } this.mGattServer.notifyCharacteristicChanged(deviceId, notifyCharacteristic, (err: BusinessError) => { if (err) { Logger.error(`notifyCharacteristicChanged callback failed: err = ${JSON.stringify(err)}`); } else { Logger.info(\'notifyCharacteristicChanged callback success\') } }) } catch (err) { Logger.error(`notifyCharacteristicChanged: err = ${JSON.stringify(err)}`); } } } // [End not_char] // [Start on_bts] private onBTStateChange() { try { access.on(\'stateChange\', (data: access.BluetoothState) => { if (data === access.BluetoothState.STATE_ON) { AppStorage.setOrCreate(\'bluetoothEnable\', true); } else if (data === access.BluetoothState.STATE_OFF) { AppStorage.setOrCreate(\'bluetoothEnable\', false); } }) } catch (err) { Logger.error(`onBTSateChange: err = ${JSON.stringify(err)}`); } } // [End on_bts] private offBTStateChange() { try { access.off(\'stateChange\'); } catch (err) { Logger.error(`offBTSateChange: err = ${JSON.stringify(err)}`); } } // [Start change_State] private onConnectStateChange() { if (!this.mGattServer) { return; } try { this.mGattServer.on(\'connectionStateChange\', (data: ble.BLEConnectionChangeState) => { if (data) { if (data.state === constant.ProfileConnectionState.STATE_CONNECTED) { let deviceId = data.deviceId; AppStorage.setOrCreate(\'deviceId\', deviceId); } else if (data.state === constant.ProfileConnectionState.STATE_DISCONNECTED) { AppStorage.setOrCreate(\'deviceId\', \'\'); this.stopAdvertiser(); } } }) } catch (err) { Logger.error(`connectInner: err = ${JSON.stringify(err)}`); } } // [End change_State] private offConnectStateChange() { if (!this.mGattServer) { return; } try { this.mGattServer.off(\'connectionStateChange\'); } catch (err) { Logger.error(`offConnectStateChange: err = ${JSON.stringify(err)}`); } }}let advertiseBluetoothViewModel = new AdvertiseBluetoothViewModel();export default advertiseBluetoothViewModel as AdvertiseBluetoothViewModel;

2.3  AdvData.ets文件负责构造广播数据包:

预定义了一个22字节的数据模板

SetPhoneId()方法将设备ID嵌入到指定位置

CreateData()生成最终的广播数据

// AdvData.tsexport class AdvData { private Fanal_DATA: Uint8Array = new Uint8Array([ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x0 ]); private SetPhoneId(bytes: Uint8Array): void { for (let i = 0; i < 4; i++) { this.Fanal_DATA[10 + i] = bytes[i]; } } private toHexString(byteArray: Uint8Array): string { if (byteArray === null || byteArray.length < 1) return \"\"; let hexString = \"\"; for (const byte of byteArray) { hexString += \" \"; if ((byte & 0xff) < 0x10) { hexString += \"0\"; } hexString += byte.toString(16); } return hexString.toLowerCase(); } public CreateData(PhoneId: Uint8Array,): Uint8Array { this.SetPhoneId(PhoneId); let mystr =\"\"; mystr = this.toHexString(this.Fanal_DATA); console.log(`Fanal_DATA_1: ${mystr}`); return this.Fanal_DATA; }}let advData = new AdvData();export default advData as AdvData;

2.4  MathUtils.ets
 

export default class MathUtils { static getRandomInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1) + min); }}

2.5  ArrayBufferUtils.ets

export default class ArrayBufferUtils { public static byteArray2ArrayBuffer(byteArr: Array): ArrayBufferLike { return new Uint8Array(byteArr).buffer; } public static arrayBuffer2ByteArray(arrayBuffer: ArrayBuffer): Array { return [...new Uint8Array(arrayBuffer)]; }}

2.6   Logger.ets

import { hilog } from \'@kit.PerformanceAnalysisKit\';export class Logger { private static domain: number = 0xFF00; private static prefix: string = \'BluetoothLowEnergy\'; private static format: string = \'%{public}s\'; static debug(...args: string[]): void { hilog.debug(Logger.domain, Logger.prefix, Logger.format, args); } static info(...args: string[]): void { hilog.info(Logger.domain, Logger.prefix, Logger.format, args); } static warn(...args: string[]): void { hilog.warn(Logger.domain, Logger.prefix, Logger.format, args); } static error(...args: string[]): void { hilog.error(Logger.domain, Logger.prefix, Logger.format, args); }}

2.7  module.json5权限文件

{ \"module\": { \"name\": \"entry\", \"type\": \"entry\", \"description\": \"$string:module_desc\", \"mainElement\": \"EntryAbility\", \"deviceTypes\": [ \"phone\" ], \"deliveryWithInstall\": true, \"installationFree\": false, \"pages\": \"$profile:main_pages\", \"abilities\": [ { \"name\": \"EntryAbility\", \"srcEntry\": \"./ets/entryability/EntryAbility.ets\", \"description\": \"$string:EntryAbility_desc\", \"icon\": \"$media:layered_image\", \"label\": \"$string:EntryAbility_label\", \"startWindowIcon\": \"$media:startIcon\", \"startWindowBackground\": \"$color:start_window_background\", \"exported\": true, \"skills\": [ { \"entities\": [  \"entity.system.home\" ], \"actions\": [  \"action.system.home\" ] } ] } ], \"extensionAbilities\": [ { \"name\": \"EntryBackupAbility\", \"srcEntry\": \"./ets/entrybackupability/EntryBackupAbility.ets\", \"type\": \"backup\", \"exported\": false, \"metadata\": [ { \"name\": \"ohos.extension.backup\", \"resource\": \"$profile:backup_config\" } ], } ], \"requestPermissions\": [ { \"name\": \'ohos.permission.ACCESS_BLUETOOTH\', \"reason\": \'$string:reason\', \"usedScene\": { \"abilities\": [ \"EntryAbility\" ], \"when\": \"always\" } } ] }}

三、演示效果

使用方法:

用户输入ID -> 转换为字节数组 -> 嵌入广播数据 -> 开始广播-> 通过GATT通知发送给客户端

华为 HarmonyNextOS 系统APP界面

使用安卓手机nrf Connect的App来检查数据,名字无法查看,rssi在30左右,点开可以看到数据

四、工程下载网址:https://download.csdn.net/download/ajassi2000/91685808?spm=1001.2014.3001.5503