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

【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))

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

相关推荐
fatiaozhang95272 小时前
晶晨S905L3A(B)-安卓9.0-开启ADB和ROOT-支持IPTV6-支持外置游戏系统-支持多种无线芯片-支持救砖-完美通刷线刷固件包
android·游戏·adb·华为·电视盒子·机顶盒rom·魔百盒固件
用户545748341772 小时前
Harmonyos5应用开发实战——地图组件集成与定位功能实现(part1)
harmonyos
用户545748341772 小时前
Harmonyos5应用开发实战——订单页面开发(part2)
harmonyos
用户545748341772 小时前
Harmonyos5应用开发实战——地图组件集成与定位功能实现(part2)
harmonyos
用户545748341773 小时前
HarmonyOS Next应用开发实战——登录页面实现(part1)
harmonyos
用户545748341773 小时前
HarmonyOS Next应用开发实战——底部弹框组件的实现(part1)
harmonyos
用户545748341773 小时前
HarmonyOS Next应用开发实战——底部弹框组件的实现(part2)
harmonyos
用户545748341773 小时前
HarmonyOS Next应用开发实战——多功能页面组件构建(part1)
harmonyos
用户545748341773 小时前
HarmonyOS Next应用开发实战——多功能页面组件构建(part2)
harmonyos
星释3 小时前
鸿蒙Flutter实战:18-组合而非替换,现有插件快速鸿蒙化
flutter·华为·harmonyos