查看: 95|回复: 2

开发一款BLE低功耗蓝牙调试助手(一)连接BLE服务端

[复制链接]

1

主题

7

回帖

17

积分

新手上路

积分
17
发表于 2025-4-1 08:44:45 | 显示全部楼层 |阅读模式
1 简介

万物互联生态中的各种互联设备,依靠多种通信技术建立连接,如星闪NearLink、蓝牙。BLE(Bluetooth Low Energy,低功耗蓝牙)是常用的短距通信技术之一,应用场景广泛,如智能手表、健康监测设备、智能家居等。BLE是一种能够在低功耗情况下进行通信的蓝牙技术,与传统蓝牙相比,BLE的功耗更低,适用于需要长时间运行的低功耗设备。
本篇将介绍如何使用OpenHarmony原生能力开发一个BLE调试助手(HarmonyOS也支持)。效果为:
    支持扫描BLE设备
      能扫描周围的BLE设备,列出设备名称以及MAC地址。
    支持连接、订阅、发送BLE消息:
      在扫描列表中选择期望连接的设备,点击连接按钮即可与BLE设备建立连接。支持订阅、写入特定BLE服务(9011)的特征值(9012)。

目前APP作为客户端,调试的BLE设备作为服务端(手机也可以做服务端,后续文章进行解答),APP效果如下:

2 环境搭建

我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。
软件要求

    DevEco Studio版本:DevEco Studio NEXT Developer Preview2及以上。
  • HarmonyOS SDK版本:HarmonyOS NEXT Developer Preview2 SDK及以上。
硬件要求

    设备类型:华为手机。HarmonyOS系统:HarmonyOS NEXT Developer Preview2及以上。
环境搭建

    安装DevEco Studio,详情请参考下载和安装软件。设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,详情请参考配置开发环境。开发者可以参考以下链接,完成设备调试的相关配置:
      使用真机进行调试

3 代码结构解读

本篇文档只对核心代码进行讲解,对于完整代码,开源后提供下载链接。
  1. . entry/src
  2. |-- common // 常量以及工具库
  3. |   |-- CommonConstants.ets
  4. |   |-- Logger.ets        
  5. |   `-- PermissionUtil.ets
  6. |-- entryability
  7. |   `-- EntryAbility.ets
  8. |-- pages
  9. |   |-- Index.ets // 主页
  10. `-- servers
  11.     |-- BLEScanManager.ets // BLE 设备扫描实现
  12.         // 开启扫描
  13.         public startScan()
  14.         // 关闭扫描
  15.         public stopScan()  
  16.         // 回调的形式返回扫描结果
  17.         public onScanResultCallback(data: Callback)
  18.     |-- BLEGattClientManager.ets// BLE 特征值管理实现
  19.         //写入server端服务的特征值时调用
  20.         public writeCharacteristicValue(Value:string)   
  21.         // client端主动连接时调用
  22.         public startConnect(peerDevice: string)   
  23.         // 订阅指定的BLE服务特征数据
  24.         public GetMsg(callback: Callback)
复制代码
4 构建应用主界面

调试助手主要有一个功能页面,将扫描设备列表、数据展示区、功能按钮区依次由至下布局。为了方便后续扩展更多BLE调试功能,本文将APP客户端连接BLE服务端设备的实现放在一个子组件中。Index整体的布局框架如下:
  1. @Entry
  2. @Component
  3. struct Index {
  4.   build() {
  5.     Column({space:20}) {
  6.       Text(this.message)
  7.         .id('Ble Test')
  8.         .fontSize(20)
  9.         .fontWeight(FontWeight.Bold)
  10.       // APP客户端连接BLE服务端实现子组件
  11.       BleScan({deviceId:this.deviceId,deviceName:this.deviceName})
  12.     }.justifyContent(FlexAlign.Center)
  13.     .height('100%')
  14.     .width('100%')
  15.   }
  16. }
复制代码
下面介绍BleScan子组件的实现,主要满足的功能需求以及实现如下:
    下拉BLE设备列表触底后能刷新扫描列表:List onReachEnd方法实现;能突出显示被连接的BLE设备:判断被选择的List Item与渲染的index是否一致;能修改需要发送到BLE设备的内容:使用TextArea
  1. // APP客户端连接BLE服务端实现子组件
  2. @Component
  3. struct BleScan {
  4.   build() {
  5.     Column({space:10}){
  6.       // 扫描到的设备列表
  7.       List({ space: 10 }) {
  8.         ForEach(this.bleDevicesArray, (data: ble.ScanResult, index: number) => {
  9.           ListItem() {
  10.              ....
  11.               }.justifyContent(FlexAlign.Start).width('100%')
  12.               .onClick(() => { // 点击对应设备,高亮并获取设备ID与设备名,用于后续连接
  13.                 this.deviceId = data.deviceId;
  14.                 this.deviceName = data.deviceName;
  15.                 this.clickNum = index;
  16.               })
  17.               Divider()
  18.             }
  19.           } // 被选择的BLE设备高亮显示。
  20.           .backgroundColor(this.clickNum==index?'#32C5FF':Color.Transparent)
  21.         })
  22.       }
  23.       .onReachEnd(()=>{ // 滑动到列表底部后刷新扫描列表
  24.         bleScanManager.onScanResultCallback(this.BleScanCallback);
  25.       })
  26.       // 收到、发送的数据编辑区
  27.       Row()
  28.       {
  29.          ... // 展示收到的数据
  30.       }
  31.       // 连接、断开、订阅、发送按钮
  32.       Row({space:25})
  33.       {
  34.           ... // 实现功能按钮
  35.       }
  36.     }.height('70%')
  37.   }
  38. }
复制代码
5 BLE功能开发

5.0 调试助手实现思路

调试助手需要具备扫描发现、连接、订阅、读写特征值等功能。调试助手扮演两个角色:BLE客户端、BLE服务端。本文讲解的是BLE客户端的实现,即实现与BLE服务端设备(例如鼠标或者Hi2821/Hi3863等有BLE的模组开发板)连接。调试助手实现思路如下:
无论APP作为BLE客户端还是服务端,都需要设置手机蓝牙、扫描蓝牙设备,BLE详细参考见:@ohos.bluetooth.ble (蓝牙ble模块)-ArkTS API-Connectivity Kit(短距通信服务)-网络-系统 - 华为HarmonyOS开发者 (huawei.com)
5.1 扫描BLE设备

在文件BLEScanManager.ets 中详细介绍了BLE 设备扫描的实现,扫描设备之前需要完成以下步骤:
    1.申请ohos.permission.ACCESS_BLUETOOTH权限
  • 2.引入bel库: import { ble } from '@kit.ConnectivityKit',导出的扫描实例,便于后续使用。
    1. import { ble } from '@kit.ConnectivityKit';
    2. export class BleScanManager {
    3.   private static instance: BleScanManager;
    4.   // 获取扫描实例的接口
    5.   public static getInstance(): BleScanManager {
    6.     if (!BleScanManager.instance) {
    7.       BleScanManager.instance = new BleScanManager();
    8.     }
    9.     return BleScanManager.instance;
    10.   }
    11. ....
    12. }
    13. export const bleScanManager = BleScanManager.getInstance();
    复制代码
    3.开启扫描ble.startBLEScan([scanFilter], scanOptions);
      根据需求设置扫描过滤,如设备ID、设备服务UUID等。、构造扫描参数开启扫描
    1. // 2 开启扫描
    2.   public startScan() {
    3.     // 2.1 构造扫描过滤器,需要能够匹配预期的广播包内容
    4.     let scanFilter: ble.ScanFilter = { // 根据业务实际情况定义过滤器
    5.       manufactureId: manufactureId,
    6.       manufactureData: manufactureData.buffer,
    7.       manufactureDataMask: manufactureDataMask.buffer
    8.     };
    9.     // 2.2 构造扫描参数
    10.     let scanOptions: ble.ScanOptions = {
    11.       interval: 0,
    12.       dutyMode: ble.ScanDuty.SCAN_MODE_LOW_POWER,
    13.       matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE
    14.     }
    15.     try {
    16.       ble.startBLEScan([scanFilter], scanOptions);
    17.     } catch (err) {
    18.     }
    19.   }
    复制代码
  • 4.获取扫描结果  ble.on('BLEDeviceFind', (data: Array)
    1.   // 1 订阅扫描结果 回调的形式返回扫描结果,方便外部调用
    2.   public onScanResultCallback(dataCallback: Callback) {
    3.     ble.on('BLEDeviceFind', (data: Array) => {
    4.       if (data.length > 0) {
    5.         console.info(TAG, `BLE scan result = ${JSON.stringify(data)}`);
    6.         dataCallback(data[0]);
    7.       }
    8.     });
    9.   }
    复制代码
      关闭扫描
    1.   // 3 关闭扫描
    2.   public stopScan() {
    3.     try {
    4.       ble.off('BLEDeviceFind', (data: Array) => { // 取消订阅扫描结果
    5.       });
    6.       ble.stopBLEScan();
    7.     } catch (err) {
    8.     }
    复制代码
在Index.ets的APP客户端连接BLE服务端实现子组件@Component struct BleScan中,我们在页面渲染之前就开启扫描以及订阅扫描结果,实现启动APP即扫描BLE设备。
  1. // Index.ets 页面渲染之前开启扫描并订阅扫描结果
  2. aboutToAppear(): void {
  3.     bleScanManager.onScanResultCallback(this.BleScanCallback);
  4.     bleScanManager.startScan();
  5.   }
  6. // 订阅扫描回调
  7. private BleScanCallback: Callback = (data: ble.ScanResult) => {
  8.     if ((!this.bleDevicesArray.some(item => item.deviceId === data.deviceId)) && data.deviceName !== '') {
  9.       this.bleDevicesArray.push(data)
  10.     }
  11.   };
复制代码
5.2 连接与订阅BLE服务

扫描到设备后,下一步需要连接选择的BLE设备,在文件BLEScanManager.ets 中详细介绍了BLE 设备扫描的实现,关键的实现步骤如下:
    1.获取GATT Client实例、主动连接目标设备
      使用获取实例时要传入待连接的设备ID,即使用5.1节扫描到的BLE设备ID。Client实例调用.connect()接口连接BLE设备。同时订阅连接状态,便于后续数据收发时辨别连接状态。、

  1.   // client端主动连接时调用
  2.   public startConnect(peerDevice: string) { // 对端设备一般通过ble scan获取到
  3.     ...
  4.     console.info(TAG, 'startConnect ' + peerDevice);
  5.     // 使用peerDevice构造gattClient,后续的交互都需要使用该实例
  6.     this.gattClient = ble.createGattClientDevice(peerDevice);
  7.     try {
  8.       this.gattClient.connect(); // 2.3 发起连接
  9.       this.onGattClientStateChange(); // 2.2 订阅连接状态
  10.     } catch (err) {
  11.             ...
  12.     }
  13.   }
复制代码
    2.发现自定义服务
    订阅BLE设备服务之前,需要明确服务信息、筛选服务。
      定义需要处理的服务(UUID 9011)、特征(UUID 9012)以及特征描述(UUID 2902)可自行修改。

  1. myServiceUuid: string = '00009011-0000-1000-8000-00805F9B34FB';
  2. myCharacteristicUuid: string = '00009012-0000-1000-8000-00805F9B34FB';
  3. // 2902一般用于notification或者indication
  4. myFirstDescriptorUuid: string = '00002902-0000-1000-8000-00805F9B34FB';
复制代码
    对连接设备的服务进行扫描,this.found确认是否有需要的服务以及特征。
  1. // client端连接成功后,需要进行服务发现
  2. public discoverServices() {
  3.   ...
  4.   try {
  5.     this.gattClient.getServices().then((result: Array) => {
  6.       console.info(TAG, 'getServices success: ' + JSON.stringify(result));
  7.       // 要确保server端的服务内容有业务所需要的服务
  8.       this.found = this.checkService(result);
  9.     });
  10.   } catch (err) {
  11.       ...
  12.   }
  13. }
  14. // 检测扫描到的服务
  15. private checkService(services: Array): boolean {
  16.   for (let i = 0; i < services.length; i++) {
  17.        // 对比服务UUID
  18.     if (services[i].serviceUuid != this.myServiceUuid) {
  19.       continue;
  20.     }
  21.     for (let j = 0; j < services[i].characteristics.length; j++) {
  22.        // 对比特征UUID
  23.       if (services[i].characteristics[j].characteristicUuid != this.myCharacteristicUuid) {
  24.         continue;
  25.       }
  26.       for (let k = 0; k < services[i].characteristics[j].descriptors.length; k++) {
  27.        // 对比特征描述UUID 2902
  28.         if (services[i].characteristics[j].descriptors[k].descriptorUuid == this.myFirstDescriptorUuid) {
  29.           console.info(TAG, 'find expected service from server');
  30.           return true;
  31.         }
  32.       }
  33.     }
  34.   }
  35.   console.error(TAG, 'no expected service from server');
  36.   return false;
  37. }
复制代码
    3.订阅特征值变化 on('BLECharacteristicChange',...)
      订阅前先构造需要操作的特征及其描述

  1.   // 构造BLECharacteristic
  2.   private initCharacteristic(): ble.BLECharacteristic {
  3.     let descriptors: Array = [];
  4.     let descBuffer = new ArrayBuffer(2);
  5.     let descValue = new Uint8Array(descBuffer);
  6.     descValue[0] = 11;
  7.     descValue[1] = 12;
  8.     descriptors[0] = this.initDescriptor(this.myFirstDescriptorUuid, new ArrayBuffer(2));
  9.     let charBuffer = new ArrayBuffer(2);
  10.     let charValue = new Uint8Array(charBuffer);
  11.     charValue[0] = 1;
  12.     charValue[1] = 2;
  13.     let characteristic: ble.BLECharacteristic = {
  14.       serviceUuid: this.myServiceUuid,
  15.       characteristicUuid: this.myCharacteristicUuid,
  16.       characteristicValue: charBuffer,
  17.       descriptors: descriptors
  18.     };
  19.     return characteristic;
  20.   }
复制代码
    扫描到需要的服务后this.found==true:
      此时可以使用setCharacteristicChangeNotification订阅特征值变化,当.on('BLECharacteristicChange')时可接收到订阅特征值characteristicChangeReq.characteristicValue

  1. // 扫描、连接到对应的BLE server后,订阅指定的BLE服务特征数据
  2. public GetMsg(callback: Callback)
  3. {
  4.   try {
  5.         this.gattClient.setCharacteristicChangeNotification(this.initCharacteristic(), true).then(() => {
  6.               console.info(TAG, `get ble server notify success`);
  7.               try {
  8.    this.gattClient!.on('BLECharacteristicChange', (characteristicChangeReq: ble.BLECharacteristic) => {
  9.     let value: Uint8Array = new Uint8Array(characteristicChangeReq.characteristicValue);
  10.     let textDecoder = util.TextDecoder.create('utf-8');
  11.      // 接收BLE Server消息,Uint8Array 转为string
  12.     let retStr = textDecoder.decodeWithStream(value);
  13.                 ...
  14.                   callback(retStr);
  15.                 });
  16.               } catch (err) {
  17.                   ...
复制代码
上述步骤1在点击连接按钮时触发;步骤2、3在点击订阅按钮时触发。
5.3 发送数据(写入BLE服务的特征值)

实质上,通用属性协议是GATT(Generic Attribute)定义了一套通用的属性和服务框架,通过GATT协议,蓝牙设备可以向其他设备提供服务,也可以从其他设备获取服务。调试助手APP作为客户端时也可以对服务端提供的特征值进行写入(前提是服务支持写入权限),APP向BLE设备发送数据其实就是对特征值的写入过程。主要步骤如下:
      点击发送按钮时先确认在连接状态,传入需要发送的内容到发送接口.writeCharacteristicValue("data")

  1.    Button('发 送').onClick(()=>{
  2.           // 1.要确保server端的服务内容有业务所需要的服务
  3.           // 2.在确保拿到了server端的服务结果后,读取到了需要的server端特定服务的特征
  4.           // 3.连接到对应的BLE server后,对指定的BLE服务特征进行写入
  5.           if(this.BleConnectState==constant.ProfileConnectionState.STATE_CONNECTED)
  6.           {
  7.             gattClientManager.writeCharacteristicValue(this.writeCharacteristicValue)
  8.           }
  9.         })
复制代码
    2.写入特征值
      与订阅一样需要构建特征及其描述将需要传输的数据转换为Uint8Array(如string转为Uint8Array)WRITE_NO_RESPONSE对指定服务特征进行写入操作

  1.   // 5. 在确保拿到了server端的服务结果后,写入server端特定服务的特征值时调用
  2.   public writeCharacteristicValue(Value:string) {
  3.       ...
  4.     if (!this.found) { // 要确保server端有对应的characteristic
  5.       console.error(TAG, 'no characteristic from server');
  6.       return;
  7.     }
  8.     // 构造服务和特征
  9.      ...
  10.     // 转换需要写入的数据
  11.     let charBuffer = new ArrayBuffer(Value.length);
  12.     let charValue = new Uint8Array(charBuffer);
  13.     for(let i=0; i {
  14.        ...
复制代码
6 BLE调试助手测试

配置测试设备:
    设备名BLE_UART_SERVER服务UUID配置为9011BLE设备定义:
      9011服务的,其特征UUID为90129012特征提供读Read、无返回的写Write No Respond和通知Notify权限。

开启后启动调试助手,扫描到设备,点击连接即可。编辑需要发送的数据,点击发送,可以在BLE服务端(海思Hi2821)收到了数据内容。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

0

主题

3

回帖

10

积分

新手上路

积分
10
发表于 2025-4-1 08:45:16 | 显示全部楼层
您好,请问这个代码会开源吗

0

主题

2

回帖

10

积分

新手上路

积分
10
发表于 2025-4-1 08:45:37 | 显示全部楼层
请问这个代码后续会开源吗,我看仓库里没有了
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表