作者:徐金生
目标:实现BLE蓝牙设备与DAYU200设备之间数据交互,即中心设备接收外围设备的通知数据,OpenHarmony社区提供了详细的API文档,可以移步到:蓝牙
之前在HarmonyOS系统上实现了BLE蓝牙的连接与数据传输,《HarmonyOS BLE蓝牙通信开发总结》,现在需要在OpenHarmony上也实现BLE蓝牙的通信。
设备与环境
设备:BLE蓝牙设备、DAYU200设备
系统:OpenHarmony 3.2 beta1
SDK:9
先看下效果
查看视频

说在前面的话
如果你需要了解蓝牙的基础知识,可以查看《HarmonyOS BLE蓝牙通信开发总结》这篇文章的“蓝牙介绍”部分。
前置步骤
创建项目
说明:创建项目的过程比较简单,注意在选择SDK 9的版本,使用Stage模型,如下:


业务逻辑梳理
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 应用安装报权限错误,如下代码:
如下图:

应用开发时,将需要申请的权限在modele.json5文件中声明,权限相关的说明可以查看:应用权限列表
2、打开并搜索蓝牙
2.1、打开蓝牙
2.2、bluetooth.getState()
点击蓝牙开关,进行开启和关闭蓝牙操作,程序启动后会先自动检测系统蓝牙是否开启,如果开启则打开蓝牙开关,默认状态下关闭蓝牙。蓝牙被开启后会进入蓝牙扫描。目前主要针对BLE低功耗蓝牙进行操作,所以这里只开启BLE蓝牙扫描,下面说到的蓝牙相关操作,都是指BLE蓝牙。
接口 |
说明 |
返回值 |
bluetooth.getState |
获取蓝牙开关状态。 |
BluetoothState 蓝牙开关状态。 |
2.3、启动蓝牙扫描,并注册发现设备监听器
启动BLE蓝牙扫描,并注册“BLEDeviceFiind”蓝牙监听器,用于接收扫描到蓝牙,为了方便调试我这里只将需要的蓝牙设备过滤出来。扫描出来的蓝牙状态默认为:未连接
参数名 |
类型 |
必填 |
说明 |
filters |
Array<ScanFilter> |
是 |
表示扫描结果过滤策略集合,如果不使用过滤的方式,该参数设置为null。 |
options |
ScanOptions |
否 |
表示扫描的参数配置,可选参数。 |
参数名 |
类型 |
必填 |
说明 |
type |
string |
是 |
填写"BLEDeviceFind"字符串,表示BLE设备发现事件。 |
callback |
Callback<Array<ScanResult>> |
是 |
表示回调函数的入参,发现的设备集合。回调函数由用户创建通过该接口注册。 |
抛出问题
问题1:在bluetooth.BLE.startBLEScan()接口中传递需要过滤的deviceId,但是无效,问题已向社区反馈,如果有兴趣可以关注相关 issues
3、连接蓝牙
点击列表中的蓝牙信息,根据当前的状态发起蓝牙连接,涉及的接口:GattClientDevice.connect()
返回值:
类型 |
说明 |
boolean |
连接操作成功返回true,操作失败返回false。 |
4、获取蓝牙服务,遍历蓝牙特征
通过GattClientDevice.on(type: “BLEConnectionStateChange”, callback: Callback<BLEConnectChangedState>)注册蓝牙连接状态变化监听器,获取蓝牙连接状态,当蓝牙连接成功,则通过GattClientDevice.getServices() 获取蓝牙支持的服务,这里提醒一句,获取服务需要耗时3秒左右,通过蓝牙服务设置
readCharacteristicValue()、writeCharacteristicValue()、setNotifyCharacteristicChanged()、on(‘BLECharacteristicChange’) 来完成对蓝牙的读、写、监听特征值变化的操作。
- 通过GattClientDevice.getServices()
5、向低功耗蓝牙设备写入特定的特征值
通过步骤4可以获取到BLECharacteristic,调用:GattClientDevice.writeCharacteristicValue() 就可以向低功耗蓝牙设备写入特定的特征值。
6、接受通知
向服务端发送设置通知此特征值请求:setNotifyCharacteristicChanged(characteristic: BLECharacteristic, enable: boolean)
- 订阅蓝牙低功耗设备的特征值变化事件:GattClientDevice.on(type: “BLECharacteristicChange”, callback: Callback<BLECharacteristic>)
抛出问题
问题1:监听多服务通道特征通知会导致异常,相关issues
7、断开蓝牙
蓝牙连接成功后,点击蓝牙列表中的蓝牙信息,弹窗窗口提示用户需要断开蓝牙,点击"确定"则断开蓝牙,涉及的接口:GattClientDevice.disconnect()
8、关闭蓝牙
关闭蓝牙后,会通知再2.1中蓝牙关闭状态的回调
到此BLE低功耗蓝牙的整体流程就介绍完毕,如果有什么问题,可以在评论区留言。
问题与思考
1、BLE蓝牙创建加密通信通道时需要进行绑定,目前SDK9的版本上还不支持,只能使用不绑定的方式进行通信。相关 issues
补充代码
UI
数据转换工具: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, '')
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
字符工具:TextUtils
感谢
如果您能看到最后,还希望您能动动手指点个赞,一个人能走多远关键在于与谁同行,我用跨越山海的一路相伴,希望得到您的点赞。
前排学习,顺便贴下视频链接:https://ost.51cto.com/show/19300
终于到OpenHarmony了
看过了演示,整个扫码流程很流畅
很详细的蓝牙开发指导
请问有源码或demo吗?谢谢
有一些私有协议,暂时没整demo,后期提供
不错不错,蓝牙BLE现在很流行了
请问一下,扫描结果中的广播数据不断发生变化是怎么回事?第一次发现是一种结果,第二次返现又是一种结果,使用蓝牙助手测试的时候,ble设备发送的广播数据是不变化的,这个问题怎么破?
把你收到的数据原值打印出来对比下,再确认下你的解析方式。
本人是跨专业来学鸿蒙开发的,我有几个看起来很简单的问题想向博主请教一下,我现在已经能够连接上BLE设备了,同时可以给BLE写入数据,但是在读取BLE的通知时,通知信息无法读取出来,我看了一下日志发现this.onBLECharacteristicChange()无法执行,也就是注册的监听无法执行
(如果执行了那么在第三行会执行下面语句)
我把blueSetNotifyCharacteristicChanged()放到了一个按钮的点击事件里执行,但是我只知道把这些函数放到事件中执行,不知道其他方法,希望博主可以赐教一二
最后博主的代码有不完整部分,作为一个小白来说阅读起来十分吃力,恳求博主可以早日整理出源码或者demo,感激不尽
谢谢你的支持,非常抱歉,因为原项目中代码有些私有协议暂时没有剥离,所以没办法直接给你提供demo。
你使用的blueSetNotifyCharacteristicChanged操作没问题,首先我们这边介绍的是BLE蓝牙,所以你先确认蓝牙类型是否正确,在@ohos.bluetooth接口文件中提供了两种不同的方式,因为我不知道你具体的设备类型和是否有一些私有的设置导致无法收到信息,你确认下你的蓝牙是否需要配对认证,我之前开的时候发现,如果是需要配对认证,也就是如果蓝牙消息是需要加密传输的是无法获取到传输的数据,需要取消认证,直接连接才能进行数据交互。
嗯嗯,我使用的就是正点原子的BLE蓝牙模块,是不需要配对认证的(当我使用其他BLE调试软件时,无需配对,并且可以正常手法信息),现在我觉得很有可能就是BLE特征值没有改变导致监听无法启动,但是我用BLE调试软件时可以接收到的,私有设置方面(我的理解是服务的uuid)应该都没问题,因为可以正常发送数据
博主我又发现他可以监听到特征值变化,但是返回的data却是一个null
博主你好,请问startBLEScan,Blutooth.BLE.on('BLEDeviceFind',data),是在打开蓝牙的时候调用一次就可以了吗,需要重复调用吗
启动扫描后,在未关闭之前扫描蓝牙设备只要调用一次,等待监听结果就可以。
好嘞,谢谢博主!
博主您好,SERVICE_UUID、 NOTIFY_UUID、 WRITE_UUID 这些常量的定义是?“serviceUuid” 还是 "00001800-0000-1000-8000-00805F9B34FB?
蓝牙连接getService报错,请问如何解决