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

news2025/3/29 14:46:58

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2322382.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java 批量下载doc\excle\pdf

指定图片集合 下载到指定文件夹 import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.util.Arrays; import java.util.List;public class OfficeFileDownloader {/*** 需要下载的Office文档URL列表*/private static final List<Strin…

软件性能效率测试工具有哪些?专业第三方软件检测机构推荐

在软件开发的新时代&#xff0c;软件性能效率测试已经成为每个企业不可或缺的一部分。无论是在竞争激烈的市场中&#xff0c;还是在追求卓越用户体验的过程中&#xff0c;都需要进行有效的性能测试。 一、软件性能效率测试的目标   1、响应时间&#xff1a;确保用户请求的响…

使用flask_restful快速构建接口

Flask-RESTful 是一个用于快速构建 RESTful API 的 Flask 扩展。它简化了创建、管理和文档化 REST API 的过程。利用 Flask-RESTful&#xff0c;你可以更容易地将你的 Flask 应用程序组织成 RESTful 原则的风格 安装包 pip install flask_restful 快速构建接口 from flask im…

centos 7 部署FTP 服务用shell 搭建脚本,使用时稍微修改自己所需需求

#!/bin/bash # 检查是否为 root 用户 if [ "$(id -u)" ! "0" ]; then echo "此脚本需要以 root 用户身份运行。" exit 1 fi # 安装 vsftpd yum install vsftpd -y # 备份原始配置文件 cp /etc/vsftpd/vsftpd.conf /etc/vsftpd/vsftpd…

Hadoop集群搭建(hdfs、yarn)

Hadoop 是 Apache 软件基金会旗下的一个开源项目&#xff0c;是用于处理大数据的分布式系统基础架构&#xff0c;被广泛应用于大数据存储、处理和分析等场景。 一、核心组件 1、Hadoop 分布式文件系统&#xff08;HDFS&#xff09; 具有高容错性&#xff0c;能在低成本硬件上…

Keepalived 实现高可用方案

Keepalived简介 ‌Keepalived‌ 是一个基于 ‌VRRP&#xff08;Virtual Router Redundancy Protocol&#xff09;协议‌的高可用性解决方案&#xff0c;主要用于实现‌服务故障自动切换&#xff08;Failover&#xff09;和负载均衡‌。通过管理虚拟 IP&#xff08;VIP&#xf…

医学图像分割数据集肺分割数据labelme格式6299张2类别

数据集格式&#xff1a;labelme格式(不包含mask文件&#xff0c;仅仅包含jpg图片和对应的json文件) 图像分辨率&#xff1a;1024x1024 图片数量(jpg文件个数)&#xff1a;6299 标注数量(json文件个数)&#xff1a;6299 标注类别数&#xff1a;2 标注类别名称:["leftl…

C语言复习笔记--函数递归

在学习了函数之后,函数递归是我们必然会接触到的课题,下面就让我们看下函数递归相关的知识. 递归是什么&#xff1f; 递归这个词看着就不那么好理解,那么什么是递归呢?递归其实是⼀种解决问题的⽅法,在C语⾔中,递归就是函数自己调用自己. 写⼀个史上最简单的C语⾔递归代码: …

husky的简介以及如果想要放飞自我的解决方案

husky 是一个 Git Hooks 管理工具&#xff0c;它的主要作用是 在 Git 提交&#xff08;commit&#xff09;、推送&#xff08;push&#xff09;等操作时执行自定义脚本&#xff0c;比如代码检查&#xff08;Lint&#xff09;、单元测试&#xff08;Test&#xff09;、格式化代码…

侯捷 C++ 课程学习笔记:现代 C++ 中的移动语义与完美转发深度解析

1. 前言&#xff1a;为什么我们需要移动语义&#xff1f; 在侯捷老师的《C11/14/17 新特性详解》课程中&#xff0c;移动语义&#xff08;Move Semantics&#xff09;被称作"C近十年来最重要的革新"。传统C中饱受诟病的深拷贝性能问题&#xff0c;在现代C中通过移动语…

23种设计模式-结构型模式-适配器

文章目录 简介场景问题解决方案建立中间转换层关键收益 总结 简介 使接口不兼容的类实现协同工作&#xff0c;通过引入中间层实现客户端接口和服务端接口的兼容。典型场景比如整合第三方类库或遗留系统时保持代码兼容。 场景 假设你正在开发一个股票监控程序。这个程序会下…

美亚科技业绩波动明显:现金流为负,四起未决诉讼涉金额1700万

《港湾商业观察》施子夫 近期&#xff0c;广东美亚旅游科技集团股份有限公司&#xff08;以下简称&#xff0c;美亚科技&#xff09;披露第二轮审核问询函的回复。从两轮问询函监管层提出的问题来看&#xff0c;有关美亚科技业绩增长的合理性、募投项目的必要性及合理性、经营…

PyTorch 深度学习实战(21):元强化学习与 MAML 算法

一、元强化学习原理 1. 元学习核心思想 元强化学习&#xff08;Meta-RL&#xff09;旨在让智能体快速适应新任务&#xff0c;其核心是通过任务分布学习共享知识。与传统强化学习的区别在于&#xff1a; 对比维度传统强化学习元强化学习目标解决单一任务快速适应任务分布中的…

23中设计模式-迭代器(Iterator)设计模式

迭代器设计模式 &#x1f6a9;什么是迭代器设计模式&#xff1f;&#x1f6a9;迭代器设计模式的特点&#x1f6a9;迭代器设计模式的结构&#x1f6a9;迭代器设计模式的优缺点&#x1f6a9;迭代器设计模式的Java实现&#x1f6a9;代码总结&#x1f6a9;总结 &#x1f6a9;什么是…

Word中公式自动标号带章节编号

&#xff08;1&#xff09;插入一行三列的表格&#xff0c;设置宽度分别为0.5&#xff0c;13.39和1.5&#xff0c;设置纵向居中&#xff0c;中间列居中对齐&#xff0c;最右侧列靠右对齐&#xff0c;设置段落如下 &#xff08;2&#xff09;插入域代码 【Word】利用域代码快速实…

【Spring AI】基于专属知识库的RAG智能问答小程序开发——功能优化:用户鉴权主体功能开发

系列文章目录 【Spring AI】基于专属知识库的RAG智能问答小程序开发——完整项目&#xff08;含完整前端后端代码&#xff09;【Spring AI】基于专属知识库的RAG智能问答小程序开发——代码逐行精讲&#xff1a;核心ChatClient对象相关构造函数【Spring AI】基于专属知识库的R…

[7-01-03].SpringBoot3集成MinIo

MinIO学习大纲 一、Spingboot整合MinIo 第1步&#xff1a;搭建SpringBoot项目&#xff1a; 第2步&#xff1a;引入minio依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi&q…

ISIS-3 LSDB链路状态数据库同步

上一章我们介绍了ISIS的邻居建立关系以及ISIS的路由器角色有哪些,在不同的网络类型当中建立邻居关系有什么不同,并且以实验案例抓包的形式给大家进一步介绍了建立的过程。 这一章我们来介绍ISIS中是如何实现链路状态数据库同步的,与OSPF的链路状态同步有什么不同,在不同网络类…

快速入手-基于Django的Form和ModelForm操作(七)

1、Form组件 2、ModelForm操作 3、给前端表单里在django里添加class相关属性值 4、前端 5、后端form 新增数据处理 6、更新数据处理

Springboot集成Debezium监听postgresql变更

1.创建springboot项目引入pom <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>io.debezium</groupI…