查看: 242|回复: 3

开发一款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 | 显示全部楼层
请问这个代码后续会开源吗,我看仓库里没有了

0

主题

391

回帖

810

积分

高级会员

积分
810
发表于 2025-8-5 09:51:52 | 显示全部楼层
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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