文章的目的为了记录使用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.ts
const MIN_HEART_RATE = 40;
const MAX_HEART_RATE = 200;
const PERMISSION_LIST: Array<Permissions> = [
'ohos.permission.ACCESS_BLUETOOTH'
];
function reqPermissionFromUser(permissions: Array<Permissions>, 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
@Component
export 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 < 8; i += 2) {
let byteStr = this.myid.substr(i, 2); // 截取两位
this.idArray[i / 2] = parseInt(byteStr, 16); // 按十六进制解析
}
this.toggleAdvertiser()
// 发送按钮点击事件
console.log('发送按钮被点击')
})
Button('停止')
.width('45%')
.height(50)
.fontSize(18)
.margin({ left: '10%' })
.onClick(() => {
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<ble.BLEDescriptor>
}
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<ble.BLEDescriptor> = [];
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<ble.BLECharacteristic> = [];
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<ble.BLEDescriptor> = [];
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.ts
export 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<number>): ArrayBufferLike {
return new Uint8Array(byteArr).buffer;
}
public static arrayBuffer2ByteArray(arrayBuffer: ArrayBuffer): Array<number> {
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