【HarmonyOS】鸿蒙应用低功耗蓝牙BLE的使用心得(一)
一、前言
鸿蒙官网文档中蓝牙部分,对于之前没有开发过蓝牙的同学,使用和查阅起来不是很方便。因为只是API的调用说明。并没有蓝牙整个调用流程的讲解,所以看起来会云里雾里。特别是针对低功耗蓝牙来说,对于普通蓝牙,开发步骤较少还好。
在开发蓝牙和低功耗蓝牙之前,我们最好对蓝牙开发有一定的基础认识,这样开发起来才能整体框架,知道自己在做什么,只是根据文档API的调用,很容易漏处理。
本章主要讲解低功耗蓝牙BLE的开发调用,普通蓝牙在之前的章节已经系统的讲过了。可以参见,【HarmonyOS】鸿蒙应用蓝牙功能实现 (一,二,三)系列。
二、BLE低功耗蓝牙的基础知识
1.低功耗蓝牙是什么?
低功耗蓝牙(Bluetooth Low Energy,简称BLE)是蓝牙技术的一种变体,也被称为蓝牙4.0。BLE技术通过一系列的技术和优化措施,如减少广播频段和广播时射频开启时间、采用深度睡眠状态(Duty-Cycle)、优化连接机制等,显著降低了设备的功耗。这使得BLE设备在长时间运行下,电池寿命得到大大延长。比如智能穿戴设备、智能家电、传感器等
2.低功耗蓝牙与普通蓝牙的区别是什么?
见名知意,在功耗上前者更为优秀,并且易维护,非常适合对能耗敏感的场景。相比之下,普通蓝牙的功耗较高。一旦激活,它就会始终保持连接,比较耗能。因此,它主要应用在对功耗要求不高的设备上,如无线耳机、音箱、游戏手柄等。
在广播信道上,前者为3,这有助于减少网络干扰并降低功耗。后者为32,虽然提供了更多的选择,但相应地也增加了功耗和复杂性。
数据传输速率和包长度,前者都更短些。
3.低功耗蓝牙相关专有名词解释:
配置文件 (Profile)
Profile 是被蓝牙标准预先定义的一些 Service 的集合,并不真实存在于蓝牙设备中。如果蓝牙设备之间要相互兼容,它们只要支持相同的 Profile 即可。一个蓝牙设备可以支持多个 Profile。
服务
Service 是蓝牙设备对外提供的服务,一个设备可以提供多个服务,比如电量信息服务、系统信息服务等。每个服务由一个 UUID 唯一标识。
特征
每个 Service 包含 0 至多个 Characteristic。比如,电量信息服务就会有个 Characteristic 表示电量数据。Characteristic 包含一个值 (value)和 0 至多个描述符 (Descriptor) 组成。在与蓝牙设备通信时,主要就是通过读写 Characteristic 的 value 完成。 每个 Characteristic 由一个 UUID 唯一标识。
描述符
Descriptor 是描述特征值的已定义属性。例如,Descriptor 可指定人类可读的描述、特征值的取值范围或特定于特征值的度量单位。每个 Descriptor 由一个 UUID 唯一标识。
GATT-Generic Attribute Profile
GATT 配置文件是关于通过 BLE 链路发送和接收一小段数据(称为“属性”)的一般规范。当前的所有 BLE 应用配置文件都基于 GATT。
Bluetooth SIG 为 BLE 设备定义了许多配置文件。配置文件是规定设备如何在特定应用中工作的规范。请注意,一个设备可以实现多个配置文件。例如,设备可能包含心率监测器和电池电量检测器。
GATT 是基于属性协议 (ATT) 构建的。这也称为 GATT/ATT。ATT 经过优化,可在 BLE 设备上运行。为此,它会尽可能减少使用的字节数量。每个属性均由通用唯一标识符 (UUID) 进行唯一标识。UUID 是一种标准化的 128 位格式,用于对信息进行唯一标识的字符串 ID。由 ATT 传输的特性会采用“特征”和服务的格式。
中央设备Central和外围设备Peripheral
这是低功耗蓝牙中十分重要的概念。通过低功耗蓝牙链接的两个设备,一个为中央设备(获取信息的信息使用方),一个为外围设备(产出信息的信息提供方)。例如手机和电子温度计通过蓝牙链接,前者就是中央设备,后者就是外围设备。
中央设备Central
中心设备可以扫描外围设备,并在发现有外围设备存在后与之建立连接,之后就可以使用外围设备提供的服务(Service)。一般而言,手机会担任中心设备的角色,利用外围设备提供的数据进行处理或展示等等。小程序提供低功耗蓝牙接口是默认设定手机为中心设备的。
外围设备Peripheral
外围设备一直处于广播状态,等待被中心设备搜索和连接,不能主动发起搜索。例如智能手环、传感器等设备。如果外围设备广播时被设置为不可连接的状态,也被称为广播模式 (Broadcaster),常见的例子是蓝牙信标 (Beacon) 设备。
三、BLE低功耗蓝牙的使用流程:
该流程分为两个部分,中央设备的角度和外围设备的角度。
首先作为中央设备,需要:
1.初始化蓝牙模块
2.扫描并发现蓝牙外围设备
3.连接设备
4.获取蓝牙外围设备的服务
5. 读写服务的特征值
6. 断开连接和关闭蓝牙适配器
之后作为外围设别,需要:
1.初始化蓝牙模块
2.添加服务,写入特征值和描述
3.发送广播,设置广播各种参数
DEMO示例:
中央设备操作步骤函数:
import { ble } from '@kit.ConnectivityKit';
import { constant } from '@kit.ConnectivityKit';
import { AsyncCallback, BusinessError } from '@kit.BasicServicesKit';const TAG: string = 'GattClientManager';export class GattClientManager {device: string = undefined;gattClient: ble.GattClientDevice = undefined;connectState: ble.ProfileConnectionState = constant.ProfileConnectionState.STATE_DISCONNECTED;myServiceUuid: string = '00001810-0000-1000-8000-00805F9B34FB';myCharacteristicUuid: string = '00001820-0000-1000-8000-00805F9B34FB';myFirstDescriptorUuid: string = '00002902-0000-1000-8000-00805F9B34FB'; // 2902一般用于notification或者indicationmySecondDescriptorUuid: string = '00002903-0000-1000-8000-00805F9B34FB';found: boolean = false;// 构造BLEDescriptorprivate initDescriptor(des: string, value: ArrayBuffer): ble.BLEDescriptor {let descriptor: ble.BLEDescriptor = {serviceUuid: this.myServiceUuid,characteristicUuid: this.myCharacteristicUuid,descriptorUuid: des,descriptorValue: value};return descriptor;}// 构造BLECharacteristicprivate initCharacteristic(): ble.BLECharacteristic {let descriptors: Array<ble.BLEDescriptor> = [];let descBuffer = new ArrayBuffer(2);let descValue = new Uint8Array(descBuffer);descValue[0] = 11;descValue[1] = 12;descriptors[0] = this.initDescriptor(this.myFirstDescriptorUuid, new ArrayBuffer(2));descriptors[1] = this.initDescriptor(this.mySecondDescriptorUuid, descBuffer);let charBuffer = new ArrayBuffer(2);let charValue = new Uint8Array(charBuffer);charValue[0] = 1;charValue[1] = 2;let characteristic: ble.BLECharacteristic = {serviceUuid: this.myServiceUuid,characteristicUuid: this.myCharacteristicUuid,characteristicValue: charBuffer,descriptors: descriptors};return characteristic;}private logCharacteristic(char: ble.BLECharacteristic) {let message = 'logCharacteristic uuid:' + char.characteristicUuid + '\n';let value = new Uint8Array(char.characteristicValue);message += 'logCharacteristic value: ';for (let i = 0; i < char.characteristicValue.byteLength; i++) {message += value[i] + ' ';}console.info(TAG, message);}private logDescriptor(des: ble.BLEDescriptor) {let message = 'logDescriptor uuid:' + des.descriptorUuid + '\n';let value = new Uint8Array(des.descriptorValue);message += 'logDescriptor value: ';for (let i = 0; i < des.descriptorValue.byteLength; i++) {message += value[i] + ' ';}console.info(TAG, message);}private checkService(services: Array<ble.GattService>): boolean {for (let i = 0; i < services.length; i++) {if (services[i].serviceUuid != this.myServiceUuid) {continue;}for (let j = 0; j < services[i].characteristics.length; j++) {if (services[i].characteristics[j].characteristicUuid != this.myCharacteristicUuid) {continue;}for (let k = 0; k < services[i].characteristics[j].descriptors.length; k++) {if (services[i].characteristics[j].descriptors[k].descriptorUuid == this.myFirstDescriptorUuid) {console.info(TAG, 'find expected service from server');return true;}}}}console.error(TAG, 'no expected service from server');return false;}// 1. 订阅连接状态变化事件public onGattClientStateChange() {if (!this.gattClient) {console.error(TAG, 'no gattClient');return;}try {this.gattClient.on('BLEConnectionStateChange', (stateInfo: ble.BLEConnectionChangeState) => {let state = '';switch (stateInfo.state) {case 0:state = 'DISCONNECTED';break;case 1:state = 'CONNECTING';break;case 2:state = 'CONNECTED';break;case 3:state = 'DISCONNECTING';break;default:state = 'undefined';break;}console.info(TAG, 'onGattClientStateChange: device=' + stateInfo.deviceId + ', state=' + state);if (stateInfo.deviceId == this.device) {this.connectState = stateInfo.state;}});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 2. client端主动连接时调用public startConnect(peerDevice: string) { // 对端设备一般通过ble scan获取到if (this.connectState != constant.ProfileConnectionState.STATE_DISCONNECTED) {console.error(TAG, 'startConnect failed');return;}console.info(TAG, 'startConnect ' + peerDevice);this.device = peerDevice;// 2.1 使用device构造gattClient,后续的交互都需要使用该实例this.gattClient = ble.createGattClientDevice(peerDevice);try {this.onGattClientStateChange(); // 2.2 订阅连接状态this.gattClient.connect(); // 2.3 发起连接} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 3. client端连接成功后,需要进行服务发现public discoverServices() {if (!this.gattClient) {console.info(TAG, 'no gattClient');return;}console.info(TAG, 'discoverServices');try {this.gattClient.getServices().then((result: Array<ble.GattService>) => {console.info(TAG, 'getServices success: ' + JSON.stringify(result));this.found = this.checkService(result); // 要确保server端的服务内容有业务所需要的服务});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 4. 在确保拿到了server端的服务结果后,读取server端特定服务的特征值时调用public readCharacteristicValue() {if (!this.gattClient || this.connectState != constant.ProfileConnectionState.STATE_CONNECTED) {console.error(TAG, 'no gattClient or not connected');return;}if (!this.found) { // 要确保server端有对应的characteristicconsole.error(TAG, 'no characteristic from server');return;}let characteristic = this.initCharacteristic();console.info(TAG, 'readCharacteristicValue');try {this.gattClient.readCharacteristicValue(characteristic).then((outData: ble.BLECharacteristic) => {this.logCharacteristic(outData);})} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 5. 在确保拿到了server端的服务结果后,写入server端特定服务的特征值时调用public writeCharacteristicValue() {if (!this.gattClient || this.connectState != constant.ProfileConnectionState.STATE_CONNECTED) {console.error(TAG, 'no gattClient or not connected');return;}if (!this.found) { // 要确保server端有对应的characteristicconsole.error(TAG, 'no characteristic from server');return;}let characteristic = this.initCharacteristic();console.info(TAG, 'writeCharacteristicValue');try {this.gattClient.writeCharacteristicValue(characteristic, ble.GattWriteType.WRITE, (err) => {if (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);return;}console.info(TAG, 'writeCharacteristicValue success');});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 6. 在确保拿到了server端的服务结果后,读取server端特定服务的描述符时调用public readDescriptorValue() {if (!this.gattClient || this.connectState != constant.ProfileConnectionState.STATE_CONNECTED) {console.error(TAG, 'no gattClient or not connected');return;}if (!this.found) { // 要确保server端有对应的descriptorconsole.error(TAG, 'no descriptor from server');return;}let descBuffer = new ArrayBuffer(0);let descriptor = this.initDescriptor(this.mySecondDescriptorUuid, descBuffer);console.info(TAG, 'readDescriptorValue');try {this.gattClient.readDescriptorValue(descriptor).then((outData: ble.BLEDescriptor) => {this.logDescriptor(outData);});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 7. 在确保拿到了server端的服务结果后,写入server端特定服务的描述符时调用public writeDescriptorValue() {if (!this.gattClient || this.connectState != constant.ProfileConnectionState.STATE_CONNECTED) {console.error(TAG, 'no gattClient or not connected');return;}if (!this.found) { // 要确保server端有对应的descriptorconsole.error(TAG, 'no descriptor from server');return;}let descBuffer = new ArrayBuffer(2);let descValue = new Uint8Array(descBuffer);descValue[0] = 11;descValue[1] = 12;let descriptor = this.initDescriptor(this.mySecondDescriptorUuid, descBuffer);console.info(TAG, 'writeDescriptorValue');try {this.gattClient.writeDescriptorValue(descriptor, (err) => {if (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);return;}console.info(TAG, 'writeDescriptorValue success');});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 8.client端主动断开时调用public stopConnect() {if (!this.gattClient || this.connectState != constant.ProfileConnectionState.STATE_CONNECTED) {console.error(TAG, 'no gattClient or not connected');return;}console.info(TAG, 'stopConnect ' + this.device);try {this.gattClient.disconnect(); // 8.1 断开连接this.gattClient.off('BLEConnectionStateChange', (stateInfo: ble.BLEConnectionChangeState) => {});this.gattClient.close() // 8.2 如果不再使用此gattClient,则需要close} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}
}let gattClientManager = new GattClientManager();
export default gattClientManager as GattClientManager;
外围设备操作步骤函数:
import { ble } from '@kit.ConnectivityKit';
import { constant } from '@kit.ConnectivityKit';
import { AsyncCallback, BusinessError } from '@kit.BasicServicesKit';const TAG: string = 'GattServerManager';export class GattServerManager {gattServer: ble.GattServer = undefined;connectState: ble.ProfileConnectionState = constant.ProfileConnectionState.STATE_DISCONNECTED;myServiceUuid: string = '00001810-0000-1000-8000-00805F9B34FB';myCharacteristicUuid: string = '00001820-0000-1000-8000-00805F9B34FB';myFirstDescriptorUuid: string = '00002902-0000-1000-8000-00805F9B34FB'; // 2902一般用于notification或者indicationmySecondDescriptorUuid: string = '00002903-0000-1000-8000-00805F9B34FB';// 构造BLEDescriptorprivate initDescriptor(des: string, value: ArrayBuffer): ble.BLEDescriptor {let descriptor: ble.BLEDescriptor = {serviceUuid: this.myServiceUuid,characteristicUuid: this.myCharacteristicUuid,descriptorUuid: des,descriptorValue: value};return descriptor;}// 构造BLECharacteristicprivate initCharacteristic(): ble.BLECharacteristic {let descriptors: Array<ble.BLEDescriptor> = [];let descBuffer = new ArrayBuffer(2);let descValue = new Uint8Array(descBuffer);descValue[0] = 31;descValue[1] = 32;descriptors[0] = this.initDescriptor(this.myFirstDescriptorUuid, new ArrayBuffer(2));descriptors[1] = this.initDescriptor(this.mySecondDescriptorUuid, descBuffer);let charBuffer = new ArrayBuffer(2);let charValue = new Uint8Array(charBuffer);charValue[0] = 21;charValue[1] = 22;let characteristic: ble.BLECharacteristic = {serviceUuid: this.myServiceUuid,characteristicUuid: this.myCharacteristicUuid,characteristicValue: charBuffer,descriptors: descriptors};return characteristic;}// 1. 订阅连接状态变化事件public onGattServerStateChange() {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}try {this.gattServer.on('connectionStateChange', (stateInfo: ble.BLEConnectionChangeState) => {let state = '';switch (stateInfo.state) {case 0:state = 'DISCONNECTED';break;case 1:state = 'CONNECTING';break;case 2:state = 'CONNECTED';break;case 3:state = 'DISCONNECTING';break;default:state = 'undefined';break;}console.info(TAG, 'onGattServerStateChange: device=' + stateInfo.deviceId + ', state=' + state);});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 2. server端注册服务时调用public registerServer() {let characteristics: Array<ble.BLECharacteristic> = [];let characteristic = this.initCharacteristic();characteristics.push(characteristic);let gattService: ble.GattService = {serviceUuid: this.myServiceUuid,isPrimary: true,characteristics: characteristics};console.info(TAG, 'registerServer ' + this.myServiceUuid);try {this.gattServer = ble.createGattServer(); // 2.1 构造gattServer,后续的交互都需要使用该实例this.onGattServerStateChange(); // 2.2 订阅连接状态this.gattServer.addService(gattService);} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 3. 订阅来自gattClient的读取特征值请求时调用public onCharacteristicRead() {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}console.info(TAG, 'onCharacteristicRead');try {this.gattServer.on('characteristicRead', (charReq: ble.CharacteristicReadRequest) => {let deviceId: string = charReq.deviceId;let transId: number = charReq.transId;let offset: number = charReq.offset;console.info(TAG, 'receive characteristicRead');let rspBuffer = new ArrayBuffer(2);let rspValue = new Uint8Array(rspBuffer);rspValue[0] = 21;rspValue[1] = 22;let serverResponse: ble.ServerResponse = {deviceId: deviceId,transId: transId,status: 0, // 0表示成功offset: offset,value: rspBuffer};try {this.gattServer.sendResponse(serverResponse);} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 4. 订阅来自gattClient的写入特征值请求时调用public onCharacteristicWrite() {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}console.info(TAG, 'onCharacteristicWrite');try {this.gattServer.on('characteristicWrite', (charReq: ble.CharacteristicWriteRequest) => {let deviceId: string = charReq.deviceId;let transId: number = charReq.transId;let offset: number = charReq.offset;console.info(TAG, 'receive characteristicWrite: needRsp=' + charReq.needRsp);if (!charReq.needRsp) {return;}let rspBuffer = new ArrayBuffer(0);let serverResponse: ble.ServerResponse = {deviceId: deviceId,transId: transId,status: 0, // 0表示成功offset: offset,value: rspBuffer};try {this.gattServer.sendResponse(serverResponse);} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 5. 订阅来自gattClient的读取描述符请求时调用public onDescriptorRead() {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}console.info(TAG, 'onDescriptorRead');try {this.gattServer.on('descriptorRead', (desReq: ble.DescriptorReadRequest) => {let deviceId: string = desReq.deviceId;let transId: number = desReq.transId;let offset: number = desReq.offset;console.info(TAG, 'receive descriptorRead');let rspBuffer = new ArrayBuffer(2);let rspValue = new Uint8Array(rspBuffer);rspValue[0] = 31;rspValue[1] = 32;let serverResponse: ble.ServerResponse = {deviceId: deviceId,transId: transId,status: 0, // 0表示成功offset: offset,value: rspBuffer};try {this.gattServer.sendResponse(serverResponse);} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 6. 订阅来自gattClient的写入描述符请求时调用public onDescriptorWrite() {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}console.info(TAG, 'onDescriptorWrite');try {this.gattServer.on('descriptorWrite', (desReq: ble.DescriptorWriteRequest) => {let deviceId: string = desReq.deviceId;let transId: number = desReq.transId;let offset: number = desReq.offset;console.info(TAG, 'receive descriptorWrite: needRsp=' + desReq.needRsp);if (!desReq.needRsp) {return;}let rspBuffer = new ArrayBuffer(0);let serverResponse: ble.ServerResponse = {deviceId: deviceId,transId: transId,status: 0, // 0表示成功offset: offset,value: rspBuffer};try {this.gattServer.sendResponse(serverResponse);} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}});} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}// 7. server端删除服务,不再使用时调用public unRegisterServer() {if (!this.gattServer) {console.error(TAG, 'no gattServer');return;}console.info(TAG, 'unRegisterServer ' + this.myServiceUuid);try {this.gattServer.removeService(this.myServiceUuid); // 7.1 删除服务this.gattServer.off('connectionStateChange', (stateInfo: ble.BLEConnectionChangeState) => { // 7.2 取消订阅连接状态});this.gattServer.close() // 7.3 如果不再使用此gattServer,则需要close} catch (err) {console.error(TAG, 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);}}
}let gattServerManager = new GattServerManager();
export default gattServerManager as GattServerManager;