三亚中国检科院生物安全中心门户网站建设seo的作用主要有
【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.mDevice
是ble.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))
至此功能讲解完毕,有问题欢迎沟通。