OpenHarmony开发者论坛

标题: 开发一款BLE低功耗蓝牙调试助手(一)连接BLE服务端 [打印本页]

作者: hellokun    时间: 2024-9-18 17:35
标题: 开发一款BLE低功耗蓝牙调试助手(一)连接BLE服务端
[md]# 1 简介

万物互联生态中的各种互联设备,依靠多种通信技术建立连接,如星闪NearLink、蓝牙。BLE(Bluetooth Low Energy,低功耗蓝牙)是常用的短距通信技术之一,应用场景广泛,如智能手表、健康监测设备、智能家居等。BLE是一种能够在低功耗情况下进行通信的蓝牙技术,与传统蓝牙相比,BLE的功耗更低,适用于需要长时间运行的低功耗设备。

本篇将介绍如何使用OpenHarmony原生能力开发一个BLE调试助手(HarmonyOS也支持)。效果为:

- **支持扫描BLE设备**
  - 能扫描周围的BLE设备,列出设备名称以及MAC地址。
- **支持连接、订阅、发送BLE消息**:
  - 在扫描列表中选择期望连接的设备,点击连接按钮即可与BLE设备建立连接。
  - 支持订阅、写入特定BLE服务(9011)的特征值(9012)。

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

![微信图片_20240816110133.jpg](https://dl-harmonyos.51cto.com/i ... rocess=image/resize,w_820,h_1852)

# 2 环境搭建

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

## 软件要求

- DevEco Studio版本:DevEco Studio NEXT Developer Preview2及以上。
- HarmonyOS SDK版本:HarmonyOS NEXT Developer Preview2 SDK及以上。
  ![img](https://alliance-communityfile-d ... edInitFileName=true)

## 硬件要求

- 设备类型:华为手机。
- HarmonyOS系统:HarmonyOS NEXT Developer Preview2及以上。

## 环境搭建

1. 安装DevEco Studio,详情请参考[下载](https://developer.huawei.com/con ... ad-0000001507075446)和[安装软件](https://developer.huawei.com/con ... ll-0000001558013317)。
2. 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,详情请参考[配置开发环境](https://developer.huawei.com/con ... ig-0000001507213638)。
3. 开发者可以参考以下链接,完成设备调试的相关配置:
   - [使用真机进行调试](https://developer.huawei.com/con ... ce-0000001053822404)

# 3 代码结构解读

本篇文档只对核心代码进行讲解,对于完整代码,开源后提供下载链接。

```c
. entry/src
|-- common // 常量以及工具库
|   |-- CommonConstants.ets
|   |-- Logger.ets        
|   `-- PermissionUtil.ets
|-- entryability
|   `-- EntryAbility.ets
|-- pages
|   |-- Index.ets // 主页
`-- servers
    |-- BLEScanManager.ets // BLE 设备扫描实现
            // 开启扫描
            public startScan()
                   // 关闭扫描
        public stopScan()  
            // 回调的形式返回扫描结果
        public onScanResultCallback(data: Callback<ble.ScanResult>)
    |-- BLEGattClientManager.ets// BLE 特征值管理实现
            //写入server端服务的特征值时调用
        public writeCharacteristicValue(Value:string)   
            // client端主动连接时调用
        public startConnect(peerDevice: string)   
            // 订阅指定的BLE服务特征数据
        public GetMsg(callback: Callback<string>)
```

# 4 构建应用主界面

调试助手主要有一个功能页面,将扫描设备列表、数据展示区、功能按钮区依次由至下布局。为了方便后续扩展更多BLE调试功能,本文将APP客户端连接BLE服务端设备的实现放在一个子组件中。Index整体的布局框架如下:

```js
@Entry
@Component
struct Index {
  build() {
    Column({space:20}) {
      Text(this.message)
        .id('Ble Test')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      // APP客户端连接BLE服务端实现子组件
      BleScan({deviceId:this.deviceId,deviceName:this.deviceName})

    }.justifyContent(FlexAlign.Center)
    .height('100%')
    .width('100%')
  }
}
```

下面介绍BleScan子组件的实现,主要满足的功能需求以及实现如下:

- 下拉BLE设备列表触底后能刷新扫描列表:List onReachEnd方法实现;
- 能突出显示被连接的BLE设备:判断被选择的List Item与渲染的index是否一致;
- 能修改需要发送到BLE设备的内容:使用TextArea

```js
// APP客户端连接BLE服务端实现子组件
@Component
struct BleScan {
  build() {
    Column({space:10}){
      // 扫描到的设备列表
      List({ space: 10 }) {
        ForEach(this.bleDevicesArray, (data: ble.ScanResult, index: number) => {
          ListItem() {
             ....
              }.justifyContent(FlexAlign.Start).width('100%')
              .onClick(() => { // 点击对应设备,高亮并获取设备ID与设备名,用于后续连接
                this.deviceId = data.deviceId;
                this.deviceName = data.deviceName;
                this.clickNum = index;
              })
              Divider()
            }
          } // 被选择的BLE设备高亮显示。
          .backgroundColor(this.clickNum==index?'#32C5FF':Color.Transparent)
        })
      }
      .onReachEnd(()=>{ // 滑动到列表底部后刷新扫描列表
        bleScanManager.onScanResultCallback(this.BleScanCallback);
      })
      // 收到、发送的数据编辑区
      Row()
      {
         ... // 展示收到的数据
      }
      // 连接、断开、订阅、发送按钮
      Row({space:25})
      {
          ... // 实现功能按钮
      }
    }.height('70%')
  }
}
```

# 5 BLE功能开发

## 5.0 调试助手实现思路

调试助手需要具备扫描发现、连接、订阅、读写特征值等功能。调试助手扮演两个角色:BLE客户端、BLE服务端。本文讲解的是BLE客户端的实现,即实现与BLE服务端设备(例如鼠标或者Hi2821/Hi3863等有BLE的模组开发板)连接。调试助手实现思路如下:
![ble调试助手思路.png](https://dl-harmonyos.51cto.com/i ... rocess=image/resize,w_820,h_1913)

无论APP作为BLE客户端还是服务端,都需要设置手机蓝牙、扫描蓝牙设备,BLE详细参考见:[@ohos.bluetooth.ble (蓝牙ble模块)-ArkTS API-Connectivity Kit(短距通信服务)-网络-系统 - 华为HarmonyOS开发者 (huawei.com)](https://developer.huawei.com/con ... is-bluetooth-ble-V5)

## 5.1 扫描BLE设备

在文件BLEScanManager.ets 中详细介绍了BLE 设备扫描的实现,扫描设备之前需要完成以下步骤:

- 1.申请ohos.permission.ACCESS_BLUETOOTH权限
- 2.引入bel库: import { ble } from '@kit.ConnectivityKit',导出的扫描实例,便于后续使用。

  ```js
  import { ble } from '@kit.ConnectivityKit';
  export class BleScanManager {
    private static instance: BleScanManager;
    // 获取扫描实例的接口
    public static getInstance(): BleScanManager {
      if (!BleScanManager.instance) {
        BleScanManager.instance = new BleScanManager();
      }
      return BleScanManager.instance;
    }
  ....
  }
  export const bleScanManager = BleScanManager.getInstance();
  ```
- 3.开启扫描ble.startBLEScan([scanFilter], scanOptions);

  - 根据需求设置扫描过滤,如设备ID、设备服务UUID等。、
  - 构造扫描参数
  - 开启扫描

  ```js
  // 2 开启扫描
    public startScan() {
      // 2.1 构造扫描过滤器,需要能够匹配预期的广播包内容
      let scanFilter: ble.ScanFilter = { // 根据业务实际情况定义过滤器
        manufactureId: manufactureId,
        manufactureData: manufactureData.buffer,
        manufactureDataMask: manufactureDataMask.buffer
      };
      // 2.2 构造扫描参数
      let scanOptions: ble.ScanOptions = {
        interval: 0,
        dutyMode: ble.ScanDuty.SCAN_MODE_LOW_POWER,
        matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE
      }
      try {
        ble.startBLEScan([scanFilter], scanOptions);
      } catch (err) {
      }
    }
  ```
- 4.获取扫描结果  ble.on('BLEDeviceFind', (data: Array<ble.ScanResult>)

  ```js
    // 1 订阅扫描结果 回调的形式返回扫描结果,方便外部调用
    public onScanResultCallback(dataCallback: Callback<ble.ScanResult>) {
      ble.on('BLEDeviceFind', (data: Array<ble.ScanResult>) => {
        if (data.length > 0) {
          console.info(TAG, `BLE scan result = ${JSON.stringify(data)}`);
          dataCallback(data[0]);
        }
      });
    }
  ```
- 5. 关闭扫描

  ```js
    // 3 关闭扫描
    public stopScan() {
      try {
        ble.off('BLEDeviceFind', (data: Array<ble.ScanResult>) => { // 取消订阅扫描结果
        });
        ble.stopBLEScan();
      } catch (err) {
      }
  ```

在Index.ets的APP客户端连接BLE服务端实现子组件@Component struct BleScan中,我们在页面渲染之前就开启扫描以及订阅扫描结果,实现启动APP即扫描BLE设备。

```js
// Index.ets 页面渲染之前开启扫描并订阅扫描结果
aboutToAppear(): void {
    bleScanManager.onScanResultCallback(this.BleScanCallback);
    bleScanManager.startScan();
  }
// 订阅扫描回调
private BleScanCallback: Callback<ble.ScanResult> = (data: ble.ScanResult) => {
    if ((!this.bleDevicesArray.some(item => item.deviceId === data.deviceId)) && data.deviceName !== '') {
      this.bleDevicesArray.push(data)
    }
  };
```

![扫描结果.png](https://dl-harmonyos.51cto.com/i ... rocess=image/resize,w_663,h_462)

## 5.2 连接与订阅BLE服务

扫描到设备后,下一步需要连接选择的BLE设备,在文件BLEScanManager.ets 中详细介绍了BLE 设备扫描的实现,关键的实现步骤如下:

- 1.获取GATT Client实例、主动连接目标设备

  - 使用获取实例时要传入待连接的设备ID,即使用5.1节扫描到的BLE设备ID。
  - Client实例调用.connect()接口连接BLE设备。
  - 同时订阅连接状态,便于后续数据收发时辨别连接状态。、

```js
  // client端主动连接时调用
  public startConnect(peerDevice: string) { // 对端设备一般通过ble scan获取到
    ...
    console.info(TAG, 'startConnect ' + peerDevice);
    // 使用peerDevice构造gattClient,后续的交互都需要使用该实例
    this.gattClient = ble.createGattClientDevice(peerDevice);
    try {
      this.gattClient.connect(); // 2.3 发起连接
      this.onGattClientStateChange(); // 2.2 订阅连接状态
    } catch (err) {
                        ...
    }
  }
```

- 2.发现自定义服务

  订阅BLE设备服务之前,需要明确服务信息、筛选服务。

  - 定义需要处理的服务(UUID 9011)、特征(UUID 9012)以及特征描述(UUID 2902)可自行修改。

```js
myServiceUuid: string = '00009011-0000-1000-8000-00805F9B34FB';
myCharacteristicUuid: string = '00009012-0000-1000-8000-00805F9B34FB';
// 2902一般用于notification或者indication
myFirstDescriptorUuid: string = '00002902-0000-1000-8000-00805F9B34FB';
```

- 对连接设备的服务进行扫描,this.found确认是否有需要的服务以及特征。

```js
// client端连接成功后,需要进行服务发现
public discoverServices() {
  ...
  try {
    this.gattClient.getServices().then((result: Array<ble.GattService>) => {
      console.info(TAG, 'getServices success: ' + JSON.stringify(result));
      // 要确保server端的服务内容有业务所需要的服务
      this.found = this.checkService(result);
    });
  } catch (err) {
      ...
  }
}
// 检测扫描到的服务
private checkService(services: Array<ble.GattService>): boolean {
  for (let i = 0; i < services.length; i++) {
       // 对比服务UUID
    if (services.serviceUuid != this.myServiceUuid) {
      continue;
    }
    for (let j = 0; j < services.characteristics.length; j++) {
       // 对比特征UUID
      if (services.characteristics[j].characteristicUuid != this.myCharacteristicUuid) {
        continue;
      }
      for (let k = 0; k < services.characteristics[j].descriptors.length; k++) {
       // 对比特征描述UUID 2902
        if (services.characteristics[j].descriptors[k].descriptorUuid == this.myFirstDescriptorUuid) {
          console.info(TAG, 'find expected service from server');
          return true;
        }
      }
    }
  }
  console.error(TAG, 'no expected service from server');
  return false;
}
```

- 3.订阅特征值变化 on('BLECharacteristicChange',...)

  - 订阅前先构造需要操作的特征及其描述

```js
  // 构造BLECharacteristic
  private initCharacteristic(): ble.BLECharacteristic {
    let descriptors: Array<ble.BLEDescriptor> = [];
    let descBuffer = new ArrayBuffer(2);
    let descValue = new Uint8Array(descBuffer);
    descValue[0] = 11;
    descValue[1] = 12;
    descriptors[0] = this.initDescriptor(this.myFirstDescriptorUuid, new ArrayBuffer(2));
    let charBuffer = new ArrayBuffer(2);
    let charValue = new Uint8Array(charBuffer);
    charValue[0] = 1;
    charValue[1] = 2;
    let characteristic: ble.BLECharacteristic = {
      serviceUuid: this.myServiceUuid,
      characteristicUuid: this.myCharacteristicUuid,
      characteristicValue: charBuffer,
      descriptors: descriptors
    };
    return characteristic;
  }
```

- 扫描到需要的服务后this.found==true:

  - 此时可以使用setCharacteristicChangeNotification订阅特征值变化,
  - 当.on('BLECharacteristicChange')时可接收到订阅特征值characteristicChangeReq.characteristicValue

```js
// 扫描、连接到对应的BLE server后,订阅指定的BLE服务特征数据
public GetMsg(callback: Callback<string>)
{
  try {
        this.gattClient.setCharacteristicChangeNotification(this.initCharacteristic(), true).then(() => {
              console.info(TAG, `get ble server notify success`);
              try {
   this.gattClient!.on('BLECharacteristicChange', (characteristicChangeReq: ble.BLECharacteristic) => {
    let value: Uint8Array = new Uint8Array(characteristicChangeReq.characteristicValue);
    let textDecoder = util.TextDecoder.create('utf-8');
     // 接收BLE Server消息,Uint8Array 转为string
    let retStr = textDecoder.decodeWithStream(value);
                              ...
                  callback(retStr);
                });
              } catch (err) {
                  ...
```


上述步骤1在点击连接按钮时触发;步骤2、3在点击订阅按钮时触发。

## 5.3 发送数据(写入BLE服务的特征值)

实质上,通用属性协议是GATT(Generic Attribute)定义了一套通用的属性和服务框架,通过GATT协议,蓝牙设备可以向其他设备提供服务,也可以从其他设备获取服务。调试助手APP作为客户端时也可以对服务端提供的特征值进行写入(前提是服务支持写入权限),APP向BLE设备发送数据其实就是对特征值的写入过程。主要步骤如下:

- 1. 点击发送按钮时先确认在连接状态,传入需要发送的内容到发送接口.writeCharacteristicValue("data")

```js
   Button('发 送').onClick(()=>{
          // 1.要确保server端的服务内容有业务所需要的服务
          // 2.在确保拿到了server端的服务结果后,读取到了需要的server端特定服务的特征
          // 3.连接到对应的BLE server后,对指定的BLE服务特征进行写入
          if(this.BleConnectState==constant.ProfileConnectionState.STATE_CONNECTED)
          {
            gattClientManager.writeCharacteristicValue(this.writeCharacteristicValue)
          }
        })
```

- 2.写入特征值

  - 与订阅一样需要构建特征及其描述
  - 将需要传输的数据转换为Uint8Array(如string转为Uint8Array)
  - WRITE_NO_RESPONSE对指定服务特征进行写入操作

```js
  // 5. 在确保拿到了server端的服务结果后,写入server端特定服务的特征值时调用
  public writeCharacteristicValue(Value:string) {
      ...
    if (!this.found) { // 要确保server端有对应的characteristic
      console.error(TAG, 'no characteristic from server');
      return;
    }
    // 构造服务和特征
     ...
    // 转换需要写入的数据
    let charBuffer = new ArrayBuffer(Value.length);
    let charValue = new Uint8Array(charBuffer);
    for(let i=0; i<Value.length;i++)
    {
      charValue = Value.charCodeAt(i);  // 字符转换为unicode
    }
    let characteristic: ble.BLECharacteristic = {
      serviceUuid: this.myServiceUuid,
      characteristicUuid: this.myCharacteristicUuid,
      characteristicValue: charBuffer,
      descriptors: descriptors
    };
    console.info(TAG, 'writeCharacteristicValue');
    try { // WRITE_NO_RESPONSE方式写入BLE设备的特征
      this.gattClient.writeCharacteristicValue(characteristic, ble.GattWriteType.WRITE_NO_RESPONSE, (err) => {
       ...
```

# 6 BLE调试助手测试

配置测试设备:

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

开启后启动调试助手,扫描到设备,点击连接即可。
编辑需要发送的数据,点击发送,可以在BLE服务端(海思Hi2821)收到了数据内容。
![e4861b0f8d125069e418 middlehorizontal.gif](https://dl-harmonyos.51cto.com/i ... 53a1377223a7d7e.gif)
[/md]




欢迎光临 OpenHarmony开发者论坛 (https://forums.openharmony.cn/) Powered by Discuz! X3.5