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

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

相关推荐
Van_captain18 分钟前
rn_for_openharmony常用组件_Breadcrumb面包屑
javascript·开源·harmonyos
御承扬33 分钟前
鸿蒙原生系列之动画效果(帧动画)
c++·harmonyos·动画效果·ndk ui·鸿蒙原生
行者962 小时前
Flutter与OpenHarmony深度集成:数据导出组件的实战优化与性能提升
flutter·harmonyos·鸿蒙
小雨下雨的雨2 小时前
Flutter 框架跨平台鸿蒙开发 —— Row & Column 布局之轴线控制艺术
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨2 小时前
Flutter 框架跨平台鸿蒙开发 —— Center 控件之完美居中之道
flutter·ui·华为·harmonyos·鸿蒙
小雨下雨的雨3 小时前
Flutter 框架跨平台鸿蒙开发 —— Icon 控件之图标交互美学
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨3 小时前
Flutter 框架跨平台鸿蒙开发 —— Placeholder 控件之布局雏形美学
flutter·ui·华为·harmonyos·鸿蒙系统
行者964 小时前
OpenHarmony Flutter弹出菜单组件深度实践:从基础到高级的完整指南
flutter·harmonyos·鸿蒙
小雨下雨的雨4 小时前
Flutter 框架跨平台鸿蒙开发 —— Padding 控件之空间呼吸艺术
flutter·ui·华为·harmonyos·鸿蒙系统
行者965 小时前
Flutter到OpenHarmony:横竖屏自适应布局深度实践
flutter·harmonyos·鸿蒙