OpenHarmony BLE低功耗蓝牙 原创 精华

NL_AIDC_XJS
发布于 2022-11-15 16:18
浏览
8收藏

作者:徐金生
目标:实现BLE蓝牙设备与DAYU200设备之间数据交互,即中心设备接收外围设备的通知数据,OpenHarmony社区提供了详细的API文档,可以移步到:蓝牙

之前在HarmonyOS系统上实现了BLE蓝牙的连接与数据传输,《HarmonyOS BLE蓝牙通信开发总结》,现在需要在OpenHarmony上也实现BLE蓝牙的通信。

设备与环境

设备:BLE蓝牙设备、DAYU200设备
系统:OpenHarmony 3.2 beta1
SDK:9

先看下效果

查看视频
OpenHarmony BLE低功耗蓝牙-鸿蒙开发者社区

说在前面的话

如果你需要了解蓝牙的基础知识,可以查看《HarmonyOS BLE蓝牙通信开发总结》这篇文章的“蓝牙介绍”部分。

前置步骤

创建项目

说明:创建项目的过程比较简单,注意在选择SDK 9的版本,使用Stage模型,如下:
OpenHarmony BLE低功耗蓝牙-鸿蒙开发者社区OpenHarmony BLE低功耗蓝牙-鸿蒙开发者社区

业务逻辑梳理

1、权限问题,首先需要注册蓝牙相关权限;
2、搜索蓝牙,应用启动后可以手动的开启和关闭蓝牙扫描;
3、连接蓝牙,根据蓝牙的mac地址,调用connect进行连接;
4、遍历蓝牙特征,在蓝牙连接成功后,获取蓝牙的服务特征,设置指定GATT特征通知;
5、通知数据,将数据通过蓝牙服务中的通知属性发送;
6、接受通知,中心设备通过characteristicChangedEvent接收通知数据,并显示在屏幕上;
7、断开蓝牙,根据需要断开连接的蓝牙;
8、关闭蓝牙,在应用退出,需要结束扫描,释放资源。

开发实践

1、申请权限

开发之前,通过API文档可以指导,需要实现目标需要获得以下权限:

  • ohos.permission.USE_BLUETOOTH // 允许应用查看蓝牙的配置。
  • ohos.permission.DISCOVER_BLUETOOTH // 允许应用配置本地蓝牙,查找远端设备且与之配对连接。
  • ohos.permission.LOCATION // 允许应用获取设备位置信息。
  • ohos.permission.MANAGE_BLUETOOTH // 允许应用配对蓝牙设备,并对设备的电话簿或消息进行访问。

以上权限中ohos.permission.MANAGE_BLUETOOTH级别是system_basic,此权限在应用打包签名时需要在UnsgnedReleasedProfileTemplate.json文件中的acls字段下添加此权限,否则安装时会出现:Failed due to grant request permissions failed,具体原因可以查看:#夏日挑战赛#OpenHarmony 应用安装报权限错误,如下代码:

"acls": {
    "allowed-acls": [
      "ohos.permission.MANAGE_BLUETOOTH"
    ]
  }

如下图:
OpenHarmony BLE低功耗蓝牙-鸿蒙开发者社区

应用开发时,将需要申请的权限在modele.json5文件中声明,权限相关的说明可以查看:应用权限列表

"requestPermissions": [
      {
        "name": "ohos.permission.USE_BLUETOOTH",
        "reason": "$string:grant_use_bluetooth",
        "usedScene": {
          "abilities": [
            "MainAbility"
          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.DISCOVER_BLUETOOTH",
        "reason": "$string:grant_discovery_bluetooth",
        "usedScene": {
          "abilities": [
            "MainAbility"
          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:grant_location",
        "usedScene": {
          "abilities": [
            "MainAbility"
          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.MANAGE_BLUETOOTH",
        "reason": "$string:grant_manage_bluetooth",
        "usedScene": {
          "abilities": [
            "MainAbility"
          ],
          "when": "inuse"
        }
      }
    ]
2、打开并搜索蓝牙
2.1、打开蓝牙
  • 打开蓝牙并监听蓝牙状态

let result:boolean = bluetooth.enableBluetooth() // 开启蓝牙

  • 监听蓝牙状态(开启或关闭)
  /***
   * 添加监听器
   */
  addBlueListener() {
    // 订阅蓝牙连接状态改变事件。
    let self = this
    bluetooth.on('stateChange', (data) => {
      logger.info(TAG, `enter on stateChange`)
      // 蓝牙打开
      if (data === bluetooth.BluetoothState.STATE_ON) {
        logger.info(TAG, `enter BluetoothState.STATE_ON`)
        self.startBleScan()
      }
      // 蓝牙关闭
      if (data === bluetooth.BluetoothState.STATE_OFF) {
        logger.info(TAG, `enter BluetoothState.STATE_OFF`)
        self.disconnect()
        self.stopBleScan()
        bluetooth.stopBluetoothDiscovery() // 关闭蓝牙扫描
        self.mDeviceName = ''
        self.mDeviceRower = ''
        self.discoveryList = []
      }
      logger.info(TAG, `BluetoothState = ${JSON.stringify(data)}`)
    })
  }
2.2、bluetooth.getState()

点击蓝牙开关,进行开启和关闭蓝牙操作,程序启动后会先自动检测系统蓝牙是否开启,如果开启则打开蓝牙开关,默认状态下关闭蓝牙。蓝牙被开启后会进入蓝牙扫描。目前主要针对BLE低功耗蓝牙进行操作,所以这里只开启BLE蓝牙扫描,下面说到的蓝牙相关操作,都是指BLE蓝牙。

  • 获取蓝牙状态
接口 说明 返回值
bluetooth.getState 获取蓝牙开关状态。 BluetoothState 蓝牙开关状态。
import bluetooth from '@ohos.bluetooth';

let state = bluetooth.getState()
    if (state === bluetooth.BluetoothState.STATE_ON) {
      this.isOn = true
      // 针对BLE蓝牙
      this.startBleScan() // 发现BLE蓝牙
    }
    if (state === bluetooth.BluetoothState.STATE_OFF) {
      this.isOn = false
    }

2.3、启动蓝牙扫描,并注册发现设备监听器

启动BLE蓝牙扫描,并注册“BLEDeviceFiind”蓝牙监听器,用于接收扫描到蓝牙,为了方便调试我这里只将需要的蓝牙设备过滤出来。扫描出来的蓝牙状态默认为:未连接

参数名 类型 必填 说明
filters Array<ScanFilter> 表示扫描结果过滤策略集合,如果不使用过滤的方式,该参数设置为null。
options ScanOptions 表示扫描的参数配置,可选参数。
参数名 类型 必填 说明
type string 填写"BLEDeviceFind"字符串,表示BLE设备发现事件。
callback Callback<Array<ScanResult>> 表示回调函数的入参,发现的设备集合。回调函数由用户创建通过该接口注册。

  /**
   * 开启BLE蓝牙扫描
   * @param data
   */
  startBleScan() {
    logger.info(TAG, `startBleScan`)
    bluetooth.BLE.on('BLEDeviceFind', this.onBLEDeviceFind)
    bluetooth.BLE.startBLEScan([{
                                  deviceId: DEVICE_MAC
                                }])
  }
  onBLEDeviceFind = (scanResult: Array<bluetooth.ScanResult>) => {
    let self = this
    //    logger.info(TAG, `BLE scan device find result= ${JSON.stringify(scanResult)}`)
    if (typeof (scanResult) === 'undefined' || scanResult.length <= 0) {
      return
    }
    for (let result of scanResult) {
      let temp: string = result.deviceId
      //  只过滤需要的设备
      if (DEVICE_MAC !== temp) {
        break
      }
      if (!self.isExistDevice(self.discoveryList, temp)) {
        self.createBleBlueInfo(temp, result.rssi).then((info) => {
          logger.info(TAG, `BLE scan device find,add = ${JSON.stringify(info)}`)
          // 连续两次发送相同的广播,时间间隔5ms,过滤极短时间发现的设备
          let curTime: number = Date.now()
          let diff: number = curTime - self.mLastFindDeviceTime
          logger.info(TAG, `BLE scan device find,Time diff = ${diff} curTime=${curTime}  mLastFindDeviceTime=${self.mLastFindDeviceTime}`)
          if (diff > 5) {
            logger.info(TAG, `BLE scan device find,Time is less than 5 ms, so back`)
            self.discoveryList.push(info)
          }
          this.mLastFindDeviceTime = curTime
        });
      } else {
        let curBlueInfo: baseInfo = self.getBlueInfoByDeviceId(self.discoveryList, temp)
        if (curBlueInfo !== null) {
          curBlueInfo.deviceRssi = result.rssi
          logger.info(TAG, `BLE scan device find,update rssi = ${curBlueInfo.deviceRssi}`)
          let index: number = self.getIndexByDeviceId(self.discoveryList, temp)
          if (index >= 0) {
            self.discoveryList.splice(index, 1)
            logger.info(TAG, `BLE scan device find,delete index= ${index}`)
            self.discoveryList.push(curBlueInfo)
            logger.info(TAG, `BLE scan device find,add new info = ${JSON.stringify(curBlueInfo)}`)
          }
        }
      }
    }
  }

抛出问题

问题1:在bluetooth.BLE.startBLEScan()接口中传递需要过滤的deviceId,但是无效,问题已向社区反馈,如果有兴趣可以关注相关 issues

3、连接蓝牙

点击列表中的蓝牙信息,根据当前的状态发起蓝牙连接,涉及的接口:GattClientDevice.connect()

返回值:

类型 说明
boolean 连接操作成功返回true,操作失败返回false。
this.mGattClientDevice = bluetooth.BLE.createGattClientDevice(deviceId)
let connectStatus = this.mGattClientDevice.connect()
logger.info(TAG, `BLE Device connect = ${connectStatus}`)
4、获取蓝牙服务,遍历蓝牙特征

通过GattClientDevice.on(type: “BLEConnectionStateChange”, callback: Callback<BLEConnectChangedState>)注册蓝牙连接状态变化监听器,获取蓝牙连接状态,当蓝牙连接成功,则通过GattClientDevice.getServices() 获取蓝牙支持的服务,这里提醒一句,获取服务需要耗时3秒左右,通过蓝牙服务设置
readCharacteristicValue()、writeCharacteristicValue()、setNotifyCharacteristicChanged()、on(‘BLECharacteristicChange’) 来完成对蓝牙的读、写、监听特征值变化的操作。

  • 通过GattClientDevice.getServices()

        // BLE蓝牙连接成功,获取当前BLE蓝牙的服务
        this.mGattClientDevice.getServices().then((result: Array<bluetooth.GattService>) => {
          logger.info(TAG, `BLE Device getServices successfully`)
          for (let blueService of result) {
            logger.info(TAG, `BLE Device blue ${connectDeviceId} Service uuid=${blueService.serviceUuid}`)
            if (SERVICE_UUID === blueService.serviceUuid) {
              let curCharacteristics = blueService.characteristics
              for (let characteristic of curCharacteristics) {
                logger.info(TAG, `BLE Device characteristic= ${JSON.stringify(characteristic)}`)
                if (NOTIFY_UUID === characteristic.characteristicUuid) {
                  self.mNotifyCharacteristic = characteristic
                  self.blueSetNotifyCharacteristicChanged()
                } else if (WRITE_UUID === characteristic.characteristicUuid) {
                  self.mWriteCharacteristic = characteristic
                  logger.info(TAG, `BLE Device blue send : 获得WriteCharacteristic `)
                  // 延迟500ms再向蓝牙设备发送获取设备信息消息,防止未注册消息通知导致无法收到
                  setTimeout(() => {
                    self.getDeviceInfo()
                  }, 100)
                }
              }
            }
            // todo 使用ReadCharacteristic 时会导致WriteCharacteristic、NotifyCharacteristic失败,这里先注释
            //            else if (SERVICE_BATTERY_UUID === blueService.serviceUuid) {
            //              // 监听BLE设备主动发送的消息
            //              let curCharacteristics = blueService.characteristics
            //              for (let characteristic of curCharacteristics) {
            //                logger.info(TAG, `BLE Device characteristic2= ${JSON.stringify(characteristic)}`)
            //                if (LEVEL_BATTERY_UUID === characteristic.characteristicUuid) {
            //                  self.mReadCharacteristic = characteristic
            //                  self.blueSetReadCharacteristic()
            //                }
            //              }
            //            }
          }
        });
      } else if (connectState === bluetooth.ProfileConnectionState.STATE_DISCONNECTED) {
        // 断开连接
        this.refreshBleConnect(connectDeviceId, STATE.DISCONNECT)
        this.mDeviceName = ''
        this.mDeviceRower = ''
      } else if (connectState === bluetooth.ProfileConnectionState.STATE_CONNECTING) {
        // 连接中
        this.refreshBleConnect(connectDeviceId, STATE.CONNECTING)
        this.mDeviceName = ''
        this.mDeviceRower = ''
      }
    })

5、向低功耗蓝牙设备写入特定的特征值

通过步骤4可以获取到BLECharacteristic,调用:GattClientDevice.writeCharacteristicValue() 就可以向低功耗蓝牙设备写入特定的特征值。

  blueWriteCharacteristicValue(str: string) {
    if (this.mWriteCharacteristic === null) {
      return
    }
    let strUpper: string = str.toUpperCase()
    let temp: string = HexUtil.stringToHex(strUpper)
    let dataPacket: string = HexUtil.getWriteDataPacket(temp)
    let data: Uint8Array = HexUtil.hexStringToBytes2(dataPacket)
    this.mWriteCharacteristic.characteristicValue = data.buffer
    let success: boolean = this.mGattClientDevice.writeCharacteristicValue(this.mWriteCharacteristic)
    logger.info(TAG, `BLE Device WriteCharacteristicValue  success =${success}`)
  }
6、接受通知

向服务端发送设置通知此特征值请求:setNotifyCharacteristicChanged(characteristic: BLECharacteristic, enable: boolean)


  blueSetNotifyCharacteristicChanged() {
    logger.info(TAG, `BLE Device SetNotifyCharacteristicChanged`)
    let success1: boolean = this.mGattClientDevice.setNotifyCharacteristicChanged(this.mNotifyCharacteristic, true)
    logger.info(TAG, `BLE Device SetNotifyCharacteristicChanged success1=${success1}`)
    logger.info(TAG, `BLE Device blue send :  注册通知监听器 success1 =${success1}`)
    if (success1) {
      this.onBLECharacteristicChange()
      let descriptor: bluetooth.BLEDescriptor = this.getDescriptor(this.mNotifyCharacteristic, this.mNotifyCharacteristic.characteristicUuid);
      if (descriptor != null) {
        descriptor.descriptorValue = new Uint8Array(this.ENABLE_NOTIFICATION_VALUE).buffer
        let success2: boolean = this.mGattClientDevice.writeDescriptorValue(descriptor)
        logger.info(TAG, `BLE Device SetNotifyCharacteristicChanged success2=${success2}`)
      } else {
        logger.info(TAG, `BLE Device SetNotifyCharacteristicChanged descriptor is null`)
      }
    }
  }

  • 订阅蓝牙低功耗设备的特征值变化事件:GattClientDevice.on(type: “BLECharacteristicChange”, callback: Callback<BLECharacteristic>)
  /**
   *订阅蓝牙低功耗设备的特征值变化事件。
   * 需要先调用setNotifyCharacteristicChanged接口才能接收server端的通知。
   * @param deviceId
   */
  onBLECharacteristicChange() {
    this.mGattClientDevice.on('BLECharacteristicChange', (data: bluetooth.BLECharacteristic) => {
      let serviceUuid = data.serviceUuid;
      let characteristicUuid = data.characteristicUuid;
      logger.info(TAG, `BLE Device data on BLECharacteristicChange ${serviceUuid}, ${characteristicUuid}`)
      let characteristicValue:ArrayBuffer = data.characteristicValue
      if (characteristicValue !== null && typeof(characteristicValue) === 'undefined') {
        return
      }
      let valueTemp = HexUtil.ab2hex(characteristicValue);
      let value = HexUtil.filterValue(valueTemp)
      let temp: string = HexUtil.hexToString(value)
      let tempStrs: string[] = temp.split(',')
      let count: number = tempStrs.length
      let curStr: string = tempStrs[count - 1] + '\n'
      // 解析设备基础信息
      this.analysisDeviceInfo(curStr)
      // 解析设备电量信息
      this.analysisRower(curStr)
      if (this.filterResult(curStr)) {
        return
        }
      this.mScanStr = curStr + '' + this.mScanStr
    })
  }
抛出问题

问题1:监听多服务通道特征通知会导致异常,相关issues

7、断开蓝牙

蓝牙连接成功后,点击蓝牙列表中的蓝牙信息,弹窗窗口提示用户需要断开蓝牙,点击"确定"则断开蓝牙,涉及的接口:GattClientDevice.disconnect()

  • 提示弹窗
AlertDialog.show({
          title: $r('app.string.disconnect'),
          message: '此操作将会断开您与以下设备的连接:' + ((typeof (blue.deviceName) !== 'undefined') ? blue.deviceName : blue.deviceId),
          primaryButton: {
            value: $r('app.string.cancel'),
            action: () => {
            }
          },
          secondaryButton: {
            value: $r('app.string.confirm'),
            action: () => {
              this.disconnectBlue(blue)
            }
          }
        })

  • 断开蓝牙
  /**
   * 断开蓝牙
   * @param blue
   */
  disconnectBlue(blue: baseInfo) {
    logger.info(TAG, `disconnectBlue info = ${JSON.stringify(blue)}`)
    if (blue === null || typeof (this.mGattClientDevice) === 'undefined') {
      return
    }
    let deviceId: string = blue.deviceId
    let ret = this.mGattClientDevice.disconnect()
    logger.info(TAG, `BLE Device ${deviceId} disconnect Status = ${ret}`)
    if (ret === true) {
      this.removeBlueDevice(deviceId)
      this.mGattClientDevice = null
    } else {
      this.showToast('断开蓝牙失败')
    }
  }
8、关闭蓝牙

关闭蓝牙后,会通知再2.1中蓝牙关闭状态的回调


let result:boolean = bluetooth.disableBluetooth()

到此BLE低功耗蓝牙的整体流程就介绍完毕,如果有什么问题,可以在评论区留言。

问题与思考

1、BLE蓝牙创建加密通信通道时需要进行绑定,目前SDK9的版本上还不支持,只能使用不绑定的方式进行通信。相关 issues

补充代码

UI
 build() {
    Column() {
      Stack({ alignContent: Alignment.Top }) {
        Image($r('app.media.top_bg'))
          .objectFit(ImageFit.Cover)
        Row() {
          Text(this.mDeviceName)
            .width('30%')
            .fontColor('#71fbfd')
            .align(Alignment.Center)
            .textAlign(TextAlign.Center)
            .fontSize(32)
            .margin({
              bottom: '15%'
            })
          Image($r('app.media.project_hr22'))
            .objectFit(ImageFit.Contain)
            .width('30%')
            .onClick(() => {
              this.onClickDevice()
            })
          Text(`电量 ${this.mDeviceRower}%`)
            .width('30%')
            .margin({
              top: '20%'
            })
            .fontColor('#71fbfd')
            .align(Alignment.Center)
            .textAlign(TextAlign.Center)
            .fontSize(24)
        }
        .alignItems(VerticalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .width('100%')
        .height('100%')
      }
      .width('100%')
      .height('40%')

      Divider()
        .vertical(false)
        .color('#fc3811')
        .strokeWidth(5)

      Column() {
        Row() {
          Column() {
            Text($r('app.string.bluetooth'))
              .fontSize(30)
              .alignSelf(ItemAlign.Start)
            if (true === this.isOn) {
              Row() {
                Text($r('app.string.discovery'))
                  .fontSize(20)
                  .alignSelf(ItemAlign.Start)
                  .margin({
                    top: '10'
                  })

                LoadingProgress()
                  .color(Color.Grey)
                  .width(30)
                  .height(30)
                  .align(Alignment.Center)
                  .margin({
                    left: 10
                  })
              }
              .height(40)
            }
          }.margin({ top: 20, bottom: 20 })

          Blank() // 空白填充

          Column() {
            Toggle({ type: ToggleType.Switch, isOn: this.isOn })
              .selectedColor('#ff2982ea')
              .key('toggleBtn')
              .onChange((isOn: boolean) => {
                if (isOn) {
                  this.isOn = true
                  let result:boolean = bluetooth.enableBluetooth() // 开启蓝牙
                  logger.info(TAG, `enable Bluetooth ${result}`)
                } else {
                  this.isOn = false
                  let result:boolean = bluetooth.disableBluetooth()
                  logger.info(TAG, `disable Bluetooth ${result}`)
                }
              })
          }
        }.width('100%')

        if (this.isOn) {
          Divider()
            .width('100%')
            .vertical(false)
            .color('#a3a4a7')
            .strokeWidth(1)
            .margin({
              bottom: '1%'
            })
        }


        Column() {
          ForEach(this.discoveryList, (item: baseInfo) => {
            Row() {
              Image(item.state === STATE.CONNECTED ? $r('app.media.blue_connect') :
                (item.state === STATE.CONNECTING ? $r('app.media.blue_connecting') : $r('app.media.blue_disconnect')))
                .width(45)
                .height(45)
                .objectFit(ImageFit.Cover)
                .margin({
                  right: '1%'
                })

              Column() {
                Text(item.deviceName)
                  .width('100%')
                  .fontSize(22)
                  .margin({
                    top: '2%',
                    bottom: '2%'
                  })
                Row() {
                  Text(item.deviceId)
                    .fontSize(16)
                  Text(item.deviceRssi.toString())
                    .fontSize(16)
                    .margin({
                      left: '5%'
                    })
                }
                .width('100%')
                .margin({
                  bottom: '2%'
                })
              }
            }
            .justifyContent(FlexAlign.Start)
            .alignSelf(ItemAlign.Start)
            .width('100%')
            .key('pairedDevice')
            .onClick(() => {
              this.onClickBlueItem(item)
            })
          })
        }
      }
      .width('90%')
      .padding({ top: 10, left: 30, right: 30, bottom: 10 })
      .margin({ top: 20, bottom: 20 })
      .backgroundColor(Color.White)
      .borderRadius(20)
      .borderWidth(1)
      .borderColor('#a3a4a7')

      Column() {
        Scroll(this.scroller) {
          Column() {
            Text(this.mScanStr)
              .width('100%')
              .fontSize(18)
              .lineHeight(30)
              .align(Alignment.Start)
          }
          .width('100%')
          .padding({
            left: '20',
            right: '20'
          })
          .alignSelf(ItemAlign.Start)
          .justifyContent(FlexAlign.Start)
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBarColor(Color.Gray)
        .scrollBar(BarState.Auto)
        .scrollBarWidth(10)
        .width('90%')
        .height('20%')
      }
      .padding({
        left: '20',
        right: '20',
        top: '20',
        bottom: '20'
      })
      .margin({
        bottom: '20'
      })
      .border({
        width: 1,
        color: '#a3a4a7'
      })
      .borderRadius(30)
      .backgroundColor(Color.White)


      Button('清除')
        .width('90%')
        .height(60)
        .backgroundColor('#fc3811')
        .fontSize(24)
        .onClick(() => {
          this.mScanStr = ''
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#dcdcdc')
  }
数据转换工具:HexUtil
import TextUtils from './TextUtils'

type char = string;
type byte = number;

export default class HexUtil {
  private static readonly DIGITS_LOWER: char[] = ['0', '1', '2', '3', '4', '5',
  '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
  private static readonly DIGITS_UPPER: char[] = ['0', '1', '2', '3', '4', '5',
  '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
  private static readonly UPDATE_HEAD: string = 'xxxxxxxx';
  private static readonly WRITE_PACKET_DATE_START: string = "xxxxxxxxx"; //发送数据固定起始数据域格式
  public static readonly WRITE_PACKET_DATE_END: string = "xxxx"; // 发送数据固定结束数据域格式
  public static readonly NOTIFY_DATE_START: string = "xxxxxxxx"; // 通知数据头
  public static readonly NOTIFY_DATE_END: string = "xxxxxxxxx"; // 通知数据尾

  public static encodeHex(data: byte[], toLowerCase: boolean = true): char[] {
    return HexUtil.encodeHexInner(data, toLowerCase ? HexUtil.DIGITS_LOWER : HexUtil.DIGITS_UPPER);
  }

  protected static encodeHexInner(data: byte[], toDigits: char[]): char[] {
    if (!data)
    return null;
    let l: number = data.length;
    let out: char[] = new Array(l << 1);
    let index:number = 0
    for(let item of data) {
      index += 1
      out[index] = toDigits[(0xF0 & item) >>> 4]
      index += 1
      out[index] = toDigits[0x0F & item]
    }
    return out;
  }

  protected static encodeHexInner2(data: Uint8Array, toDigits: char[]): char[] {
    if (!data)
    return null;
    let l: number = data.length;
    let out: char[] = new Array(l << 1);
    let index:number = 0
    for(let item of data) {
      index += 1
      out[index] = toDigits[(0xF0 & item) >>> 4]
      index += 1
      out[index] = toDigits[0x0F & item]
    }
    return out;
  }

  private static byteToString(data: char[]): string {
    let str = '';
    for(let item of data) {
      str += item
    }
    return str;
  }

  public static encodeHexStr(data: byte[], toLowerCase: boolean = true): string{
    return HexUtil.encodeHexStrInner(data, toLowerCase ? HexUtil.DIGITS_LOWER : HexUtil.DIGITS_UPPER);
  }

  protected static encodeHexStrInner(data: byte[], toDigits: char[]): string {
    return HexUtil.byteToString(HexUtil.encodeHexInner(data, toDigits));
  }

  public static encodeHexStr2(data: Uint8Array, toLowerCase: boolean = true): string{
    return HexUtil.encodeHexStrInner2(data, toLowerCase ? HexUtil.DIGITS_LOWER : HexUtil.DIGITS_UPPER);
  }

  protected static encodeHexStrInner2(data: Uint8Array, toDigits: char[]): string {
    return HexUtil.byteToString(HexUtil.encodeHexInner2(data, toDigits));
  }

  public static formatHexString(data: Uint8Array, addSpace: boolean = false): string {
    if (!data || data.length < 1)
    return null;
    let sb: string = '';
    for (let item of data) {
      let hex: String = (item & 0xFF).toString(16);
      if (hex.length == 1) {
        hex = '0' + hex;
      }
      sb = sb + hex;
      if (addSpace)
      sb = sb + " ";
    }
    return sb;
  }

  public static decodeHex(data: char[]): byte[] {
    let len: number = data.length;

    if ((len & 0x01) != 0) {
      throw new Error("Odd number of characters.");
    }

    let out: byte[] = new Array(len >> 1);
    let i:number = 0
    let j:number = 0
    while(j < len) {
      let f : number = HexUtil.toDigit(data[j], j) << 4
      j += 1
      f = f | HexUtil.toDigit(data[j], j)
      j += 1
      out[i] = (f & 0xFF)
    }
    return out;
  }

  protected static toDigit(ch: char, index: number): number {
    let digit: number = HexUtil.charToByte(ch.toUpperCase()); //Character.digit(ch, 16);
    if (digit == -1) {
      throw new Error("Illegal hexadecimal character " + ch
      + " at index " + index);
    }
    return digit;
  }

  public static hexStringToBytes(hexString: string): Uint8Array {
    if (TextUtils.isEmpty(hexString)) {
      return null;
    }
    hexString = hexString.trim();
    hexString = hexString.toUpperCase();
    let length: number = hexString.length / 2;
    let hexChars: char[] = TextUtils.toCharArray(hexString);
    let d: byte[] = new Array(length);
    let index = 0
    while (index < length) {
      let pos = index * 2;
      d[index] = (HexUtil.charToByte(hexChars[pos]) << 4 | HexUtil.charToByte(hexChars[pos + 1]));
      index += 1
    }
    return new Uint8Array(d);
  }

  public static hexStringToBytes2(hexString: string): Uint8Array {
    if (TextUtils.isEmpty(hexString)) {
      return null;
    }
    hexString = hexString.trim();
    hexString = hexString.toUpperCase();
    let length: number = hexString.length / 2;
    let hexChars: char[] = TextUtils.toCharArray(hexString);
    let d: byte[] = new Array(length);
    let index = 0
    while (index < length) {
      let pos = index * 2;
      d[index] = (HexUtil.charToByte(hexChars[pos]) << 4 | HexUtil.charToByte(hexChars[pos + 1]));
      index += 1
    }
    return new Uint8Array(d);
  }

  public static charToByte(c: char): byte {
    return "0123456789ABCDEF".indexOf(c);
  }

  public static extractData(data: Uint8Array, position: number): String {
    return HexUtil.formatHexString(new Uint8Array([data[position]]));
  }

  public static getWriteDataPacket(hexString: string): string {
    if (TextUtils.isEmpty(hexString) || hexString.length % 2 !== 0) {
      return ''
    }

    let dataField: string = ''
    if (hexString.startsWith(HexUtil.UPDATE_HEAD)) {
      dataField = hexString.replace(HexUtil.UPDATE_HEAD, '')
    } else {
      dataField = HexUtil.WRITE_PACKET_DATE_START.concat(hexString, HexUtil.WRITE_PACKET_DATE_END)
    }
    return dataField
  }

  public static stringToHex(s: string): string {
    let str: string = ''
    let len: number = s.length
    let index: number = 0
    while (index < len) {
      let ch: number = s.charCodeAt(index)
      let s4: string = ch.toString(16)
      str = str + s4
      index += 1
    }
    return str
  }

  public static hexToString(data:string):string {
    let val : string = ''
    let arr:string[] = data.split(',')
    let index:number = 0
    while(index < arr.length) {
      val += String.fromCharCode(parseInt(arr[index], 16))
      index += 1
    }
    let b:string = decodeURIComponent(val)
    console.log('hexToString b' + b)
    return b
  }

  public static  ab2hex(buffer:ArrayBuffer):string {
    var hexArr = Array.prototype.map.call(
      new Uint8Array(buffer),
      function (bit) {
        return ('00' + bit.toString(16)).slice(-2)
      }
    )
    return hexArr.join(',');
  }

  /**
   * 过滤通知消息头和消息尾
   * @param data
   */
  public static filterValue(data:string) : string {
    if (data === null) {
      return ''
    }
    return data.replace(this.NOTIFY_DATE_START, '').replace(this.NOTIFY_DATE_END, '')
  }
}

字符工具:TextUtils
export default class TextUtils{
    public static isEmpty(text: string): boolean {
        return text == null || text == undefined || text.length==0;
    }

    public static toCharArray(text: string): Array<string> {
        let arr: string[] = new Array(text.length);
        let index = 0
        while(index < text.length) {
            arr[index] = text.charAt(index);
            index += 1
        }
        return arr;
    }
}

感谢

如果您能看到最后,还希望您能动动手指点个赞,一个人能走多远关键在于与谁同行,我用跨越山海的一路相伴,希望得到您的点赞。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2022-11-22 10:55:22修改
14
收藏 8
回复
举报
18条回复
按时间正序
/
按时间倒序
红叶亦知秋
红叶亦知秋

前排学习,顺便贴下视频链接:​​https://ost.51cto.com/show/19300​

回复
2022-11-15 16:44:24
冰淇淋爱我
冰淇淋爱我

终于到OpenHarmony了

回复
2022-11-16 18:12:50
麻辣香锅配馒头
麻辣香锅配馒头

看过了演示,整个扫码流程很流畅

回复
2022-11-17 10:53:32
hmyxd
hmyxd

很详细的蓝牙开发指导

回复
2022-11-18 15:33:38
wx5850cc9853ec8
wx5850cc9853ec8

请问有源码或demo吗?谢谢

回复
2022-12-19 21:52:05
NL_AIDC_XJS
NL_AIDC_XJS 回复了 wx5850cc9853ec8
请问有源码或demo吗?谢谢

有一些私有协议,暂时没整demo,后期提供

回复
2022-12-20 08:46:13
物联风景
物联风景

不错不错,蓝牙BLE现在很流行了

1
回复
2022-12-20 16:51:36
wx61297db958c46
wx61297db958c46

请问一下,扫描结果中的广播数据不断发生变化是怎么回事?第一次发现是一种结果,第二次返现又是一种结果,使用蓝牙助手测试的时候,ble设备发送的广播数据是不变化的,这个问题怎么破?

回复
2023-3-1 17:10:04
NL_AIDC_XJS
NL_AIDC_XJS 回复了 wx61297db958c46
请问一下,扫描结果中的广播数据不断发生变化是怎么回事?第一次发现是一种结果,第二次返现又是一种结果,使用蓝牙助手测试的时候,ble设备发送的广播数据是不变化的,这个问题怎么破?

把你收到的数据原值打印出来对比下,再确认下你的解析方式。

回复
2023-3-1 18:03:58
不会抛削的板混
不会抛削的板混

本人是跨专业来学鸿蒙开发的,我有几个看起来很简单的问题想向博主请教一下,我现在已经能够连接上BLE设备了,同时可以给BLE写入数据,但是在读取BLE的通知时,通知信息无法读取出来,我看了一下日志发现this.onBLECharacteristicChange()无法执行,也就是注册的监听无法执行

(如果执行了那么在第三行会执行下面语句)

logger.info(TAG, `BLE Device data on BLECharacteristicChange ${serviceUuid}, ${characteristicUuid}`)

我把blueSetNotifyCharacteristicChanged()放到了一个按钮的点击事件里执行,但是我只知道把这些函数放到事件中执行,不知道其他方法,希望博主可以赐教一二

最后博主的代码有不完整部分,作为一个小白来说阅读起来十分吃力,恳求博主可以早日整理出源码或者demo,感激不尽

已于2023-3-13 00:02:19修改
回复
2023-3-12 23:44:21
NL_AIDC_XJS
NL_AIDC_XJS

谢谢你的支持,非常抱歉,因为原项目中代码有些私有协议暂时没有剥离,所以没办法直接给你提供demo。

你使用的blueSetNotifyCharacteristicChanged操作没问题,首先我们这边介绍的是BLE蓝牙,所以你先确认蓝牙类型是否正确,在@ohos.bluetooth接口文件中提供了两种不同的方式,因为我不知道你具体的设备类型和是否有一些私有的设置导致无法收到信息,你确认下你的蓝牙是否需要配对认证,我之前开的时候发现,如果是需要配对认证,也就是如果蓝牙消息是需要加密传输的是无法获取到传输的数据,需要取消认证,直接连接才能进行数据交互。


已于2023-3-13 09:28:17修改
回复
2023-3-13 09:27:27
不会抛削的板混
不会抛削的板混 回复了 NL_AIDC_XJS
谢谢你的支持,非常抱歉,因为原项目中代码有些私有协议暂时没有剥离,所以没办法直接给你提供demo。你使用的blueSetNotifyCharacteristicChanged操作没问题,首先我们这边介绍的是BLE蓝牙,所以你先确认蓝牙类型是否正确,在@ohos.bluetooth接口文件中提供了两种不同的方式,因为我不知道你具体的设备类型和是否有一些私有的设置导致无法收到信息,你确认下你的蓝牙是否需要配对认证,我之前开的时候发现,如果是需要...

嗯嗯,我使用的就是正点原子的BLE蓝牙模块,是不需要配对认证的(当我使用其他BLE调试软件时,无需配对,并且可以正常手法信息),现在我觉得很有可能就是BLE特征值没有改变导致监听无法启动,但是我用BLE调试软件时可以接收到的,私有设置方面(我的理解是服务的uuid)应该都没问题,因为可以正常发送数据


已于2023-3-13 12:12:31修改
回复
2023-3-13 12:10:56
不会抛削的板混
不会抛削的板混

博主我又发现他可以监听到特征值变化,但是返回的data却是一个null

回复
2023-3-13 16:35:43
如果雨停了
如果雨停了

博主你好,请问startBLEScan,Blutooth.BLE.on('BLEDeviceFind',data),是在打开蓝牙的时候调用一次就可以了吗,需要重复调用吗


回复
2023-4-4 11:48:39
NL_AIDC_XJS
NL_AIDC_XJS 回复了 如果雨停了
博主你好,请问startBLEScan,Blutooth.BLE.on('BLEDeviceFind',data),是在打开蓝牙的时候调用一次就可以了吗,需要重复调用吗

启动扫描后,在未关闭之前扫描蓝牙设备只要调用一次,等待监听结果就可以。

回复
2023-4-4 15:05:59
如果雨停了
如果雨停了 回复了 NL_AIDC_XJS
启动扫描后,在未关闭之前扫描蓝牙设备只要调用一次,等待监听结果就可以。

好嘞,谢谢博主!

回复
2023-4-6 15:54:30
wx5fc8ebc8b10a6
wx5fc8ebc8b10a6

博主您好,SERVICE_UUID、 NOTIFY_UUID、 WRITE_UUID 这些常量的定义是?“serviceUuid” 还是 "00001800-0000-1000-8000-00805F9B34FB?

回复
2024-1-17 09:57:49
FuCen
FuCen

蓝牙连接getService报错,请问如何解决


回复
2024-4-10 16:57:00
回复
    相关推荐