【HarmonyOS Next】三天撸一个BLE调试精灵

发布于:2025-03-26 ⋅ 阅读:(29) ⋅ 点赞:(0)

【HarmonyOS Next】三天撸一个BLE调试精灵

一、功能介绍

BLE调试精灵APP属于工具类APP,在用户使用的过程中,负责调试BLE设备从机端,比如蓝牙耳机、低功耗设备、带有BLE的空调等设备,可以在页面中清晰看到设备的厂商,拥有扫描设备、连接设备、发送测试数据等主要的功能。当通过BLE调试精灵APP调试时,可以方便快捷的查看设备的属性。

本APP包含以下功能:

  • 扫描BLE从机设备
  • 区分从机设备的厂商
  • 广播包解析展示
  • 连接设备
  • 展示服务和特征值
  • 特征值的读、写、通知
  • 根据MTU分包大数据发送

在这里插入图片描述

二、基本知识

在实现BLE调试APP之前,需要对BLE有基本的了解。

  • BLE是低功耗蓝牙,用于可穿戴设备,IoT智能设备等众多物联网设备,功耗低、带宽也低。不同于经典蓝牙,经典蓝牙功耗高、带宽高。
  • BLE分为主机和从机,主动连接其它设备的是主机,比如手机是主机,可穿戴设备等是从机
  • 在有些平台下需要先扫描才能进行连接。
  • 在纯血鸿蒙平台下,从机的MAC地址无法获取,而是被包装成了deviceId,类似于某水果平台。
  • 广播中厂商信息、UUID有一定的规范,厂商可对应具体的厂家,由蓝牙技术联盟分配,UUID有比如获取电量等服务。
  • 连接的过程中通常会自定义超时时间、重连次数。
  • 下发数据时通常会根据MTU进行数据包的分割。
  • 基本的字节操作。

三、技术解析

1. 侧边栏容器

SideBarContainer 组件是鸿蒙的内置组件,配合状态管理,可以很轻松的实现侧边栏展示与隐藏的效果。
用内置属性controlButton展示不同的按钮,用@State tabShow控制侧边栏展示与隐藏的状态,用背景颜色达到蒙版的效果。

 SideBarContainer(SideBarContainerType.Overlay) {
        Column() {
          DrawerTab();
        }
        .height('100%')

        Column() {
          MainPage({ tabShow: this.tabShow })
        }
        .onClick(() => {
          animateTo({
            duration: 500,
            curve: Curve.EaseOut,
            playMode: PlayMode.Normal,
          }, () => {
            this.tabShow = false;
          })
        })
        .width('100%')
        .height('100%')
        .backgroundColor(this.tabShow ? '#c1c2c4' : '')
      }
      .showSideBar(this.tabShow)
      .controlButton({
        left: 6,
        top: 6,
        height: 40,
        width: 40,
        icons: {
          shown: $r("app.media.tab_change_back"),
          hidden: $r("app.media.tab_change"),
          switching: $r("app.media.tab_change")
        }
      })
      .onChange((value: boolean) => {
        this.tabShow = value;
      })
2. BLE扫描

startBLEScan 方法进行扫描,使用ScanFilter进行扫描过滤,使用ScanOptions可传入扫描的配置,比如用最快速的响应扫描所有的设备。
BLEDeviceFind 监听该事件接收扫描的结果回调。

      ble.on("BLEDeviceFind", this.onReceiveEvent);
      let scanFilter: ble.ScanFilter = {
        //name: scanName,
      };
      let scanOptions: ble.ScanOptions = {
        interval: 0,
        dutyMode: ble.ScanDuty.SCAN_MODE_LOW_LATENCY,
        matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE
      }
      ble.startBLEScan([scanFilter], scanOptions);

当扫描到一定时间时,停止扫描。

       // 取消上一次定时器
      if (this.mScanTimerId != 0) {
        clearTimeout(this.mScanTimerId)
      }
      this.mScanTimerId = setTimeout(() => {
        BleLogger.debug(TAG, "setTimeout")
        this.stopBLEScan();
        if (this.mCallback) {
          this.mCallback.scanFinish();
        }
      }, scanTime);

扫描到结果ble.ScanResult对象,包含deviceId、广播包等数据,deviceId相当于本次扫描过程中的设备的唯一标识,可在后续的流程中用于连接;广播包一般是31个字节,在BLE5.0及以上可超出31个字节,使用拓展广播包,广播包由LTV格式构成,可以在LTV格式中解析出厂商代码,厂商代码可对应成具体厂家。

解析LTV格式:

/**
 * LTV格式数据
 */
export class LtvInfo {
  length: number;
  tag: number;
  value: Uint8Array;

  constructor(length: number, tag: number, value: Uint8Array) {
    this.length = length;
    this.tag = tag;
    this.value = value;
  }
}

export class BleBeaconUtil2 {
  /**
   * 解析 BLE 广播数据
   * @param advData 广播数据字节数组
   * @returns 解析结果的 Map,键是数据类型,值是对应的数据内容
   */
  public static parseData(advData: Uint8Array): Array<LtvInfo> {
    let result: Array<LtvInfo> = []; // 存放解析后的结果
    let index = 0; // 用于遍历数据

    while (index < advData.length) {
      let length = advData[index]; // 获取当前数据单元的长度
      index++;

      if (length === 0) {
        break; // 长度为 0 时结束解析
      }

      const type = advData[index]; // 获取数据类型
      index++;

      const data = advData.slice(index, index + length - 1); // 获取实际数据
      index += length - 1; // 更新索引

      // 根据不同类型解析数据
      let ltvInfo: LtvInfo = BleBeaconUtil2.parseDataType(length, type, data);
      result.push(ltvInfo);
    }

    return result; // 返回解析后的数据
  }

  /**
   * 根据广播数据的类型解析具体的数据
   * @param type 数据类型
   * @param data 对应类型的数据
   * @param result 存放解析结果的对象
   */
  private static parseDataType(length: number, type: number, data: Uint8Array): LtvInfo {
    return new LtvInfo(length, type, data);
  }
}

获取厂商代码,厂商代码和厂家对应信息应构成Map<number, string>数据结构,具体厂商代码在该网址下进行获取
https://bitbucket.org/bluetooth-SIG/public/raw/HEAD/assigned_numbers/company_identifiers/company_identifiers.yaml

 /**
   * 获取厂商代码
   * @returns
   */
  public getManufacturerData(ltvArray: Array<LtvInfo>): number {
    let manufacturerData = new Uint8Array(2);
    if (ltvArray.length == 0) {
      return 0;
    }
    for (const ltvInfo of ltvArray) {
      if (ltvInfo.tag === 0xff) {
        if (ltvInfo.value && ltvInfo.value.length > 2) {
          manufacturerData[0] = ltvInfo.value[1];
          manufacturerData[1] = ltvInfo.value[0];
          return ByteUtils.byteToShortBig(manufacturerData);
        }
      }
    }
    return 0;
  }
3. BLE连接

GattClientDevice.connect 传入deviceId用于连接,可自定义超时逻辑。

  /**
   * 开始链接
   * @param deviceId
   */
  private connect(deviceId: string) {
    this.notifyConnectStart(deviceId);
    this.mDevice = ble.createGattClientDevice(deviceId);
    this.mDevice.on('BLEConnectionStateChange', this.ConnectStateChanged.bind(this));
    try {
      this.mDevice.connect();
    } catch (e) {
      // todo 比如,蓝牙突然关闭时的错误
      this.notifyConnectError(BleException.ERROR_CODE_CONNECT_10001, this.mDeviceId);
      return;
    }
    this.cancelTimeoutRunnable();
    this.startTimeoutRunnable(this.mConnectTimeout);
  }

在核心回调中处理连接成功或失败的状态,抛给业务层。值得注意的是,并不是连接成功之后就算业务上的连接成功,还需要发现服务,如需进行数据交互,还需要启用通知->设置MTU操作,都完成之后,才是业务层面的连接成功。

4. 获取服务和特征值

BLE丛机包含多个服务,每个服务都有一个UUID,主服务下可包含多个子服务,子服务下可以包含多个特征值。

连接成功之后得到services: Array<ble.GattService>,从中循环取出服务和特征值并展示。

 List() {
        ForEach(this.deviceInfo.services, (item: ble.GattService) => {
          ListItem() {
            /* item view */
            ServiceItemView({ item: item, deviceId: this.deviceInfo.deviceId })
          }
        })
      }
      

 List() {
          ForEach(this.item.characteristics, (itemChild: ble.BLECharacteristic) => {
            ListItem() {
              Column() {
                Text(this.getName(itemChild.characteristicUuid))
                  .fontSize($r('app.float.font_normal'))
                  .fontWeight(FontWeight.Bold)
                BlockView({ block: 2 })
                Text('UUID:' + itemChild.characteristicUuid).fontSize(12)
                BlockView({ block: 2 })
                Text('可操作属性')
                ServiceItemButtonView({ itemChild: itemChild, deviceId: this.deviceId })

                // 子服务-描述符
                if (itemChild.descriptors) {
                  ForEach(itemChild.descriptors, (itemChildDes: ble.BLEDescriptor) => {
                    ListItem() {
                      Column() {
                        Text('Descriptors')
                          .fontSize($r('app.float.font_normal'))
                          .fontWeight(FontWeight.Bold)
                        BlockView({ block: 2 })
                        Text('UUID:' + itemChildDes.descriptorUuid).fontSize(12)
                      }.alignItems(HorizontalAlign.Start)
                    }
                  })
                }
              }
              .alignItems(HorizontalAlign.Start)
              .width('100%')
              .padding({
                left: 30,
                right: 10,
                top: 10,
                bottom: 10
              })
            }
          })
        }.divider({
          strokeWidth: 1,
          color: '#cccccc'
        }).backgroundColor('#eeeeee')
5. 特征值

特征值一共有五个。

  • Read:可读取数据(如设备名称、电量)
  • Write:可写入数据(如配置参数)
  • Notify:丛机主动通知主机(无需确认)
  • Indicate:丛机通知主机(需确认,比Notify多了一个确认)
  • Write Without Response:写入无需回复(低延迟,如控制指令)

用最常见的数据通信举例,主机给丛机发送一个文件数据,丛机给主机回复收到。

this.mDeviceble.GattClientDevice对象,在连接时根据createGattClientDevice获取。

首先请求设置MTU,拿到MTU之后,再对数据进行分包

private mtuChangeCallback = (mtu: number) => {
    BleLogger.debug(TAG, 'set mtu change:' + mtu)
    if (!this.mDeviceId) {
      this.notifyError(BleException.ERROR_CODE_CONNECT_10009, this.mDeviceId);
      return;
    }
    let bluetoothGatt = BleConnectionList.getInstance().get(this.mDeviceId);
    if (!bluetoothGatt) {
      this.notifyError(BleException.ERROR_CODE_CONNECT_10009, this.mDeviceId);
      return;
    }
    bluetoothGatt.off('BLEMtuChange', this.mtuChangeCallback);
    BLESdkConfig.getInstance().setBleMaxMtu(mtu);
    this.notifySuccess(mtu, this.mDeviceId);
  }

将分隔的组成一个队列,队列中每个数据的长度是MTU

let queuePacket: Queue<Uint8Array> =
      BleDataUtils.splitByte(requestPacket, BLESdkConfig.getInstance().getBleMaxMtu());

将数据依次取出,然后发送数据

let data: Uint8Array = this.mDataQueue.pop();
let bluetoothGatt = BleConnectionList.getInstance().get(this.mDeviceId!);
// 携带数据
this.mBleCharacteristic.characteristicValue = this.typedArrayToBuffer(data);
bluetoothGatt.writeCharacteristicValue(this.mBleCharacteristic, this.mWriteType,
this.writeCharacteristicValueCallBack.bind(this))

至此功能讲解完毕,有问题欢迎沟通。


网站公告

今日签到

点亮在社区的每一天
去签到