低功耗蓝牙开发实践

概述

蓝牙(Bluetooth)是一种无线通信技术,被广泛应用于各种电子设备之间的短距离数据传输和连接。而低功耗蓝牙(Bluetooth Low Energy,简称 BLE)是一种能够在低功耗情况下进行通信的蓝牙技术。与传统蓝牙相比,BLE的功耗更低,适用于需要长时间运行的低功耗设备,如智能手表、健康监测设备、智能家居等。

本文适用于低功耗蓝牙通信相关的开发,主要介绍了基于BLE进行蓝牙扫描管理、蓝牙连接状态管理、蓝牙设备特征值同步三个场景,并分别从服务端和客户端描述其相关实现。

实现原理

实现低功耗蓝牙通信的能力主要步骤如下:

首先服务端需要创建实例,成功创建后,需要添加对应服务,才能开启BLE广播,向客户端传递相关信息。当服务端开启BLE广播后,还需要订阅BLE连接状态,即对连接状态进行监听。当连接状态变化时,如断开连接,则此时需要关闭BLE广播。在服务端接收到客户端发起的连接请求和发起的特征值订阅请求后,才能向客户端传递特征值信息。

而客户端同样需要创建实例,并获取相关服务,才能通过蓝牙扫描到服务端,并发起连接请求和特征值订阅请求。客户端也需要监听连接状态,当断开连接时,关闭对应的监听能力。

关键技术

实现低功耗蓝牙通信能力需要使用@ohos.bluetooth.ble (蓝牙ble模块)中提供的API能力,服务端使用的API主要包括创建服务器实例发送BLE广播特征值变化通知等;客户端使用的API主要包括蓝牙连接订阅蓝牙低功耗设备的特征值变化订阅蓝牙低功耗设备的连接状态变化事件等。

开发流程

在需要使用低功耗蓝牙进行通信的场景中:

服务端

  1. 调用createGattServer()接口创建GattServer实例,即创建服务端。
  2. 调用addService()接口在服务端添加服务。
  3. 调用startAdvertising()接口开始发送BLE广播,向连接的客户端设备提供数据。
  4. 调用on('connectionStateChange')方法在服务端订阅BLE连接状态变化事件。
  5. 调用notifyCharacteristicChanged()接口,在服务端特征值发生变化时,主动通知已连接的客户端设备。

客户端

  1. 调用createGattClientDevice()接口创建GattClientDevice实例,即创建客户端。
  2. 调用connect()接口发起连接远端蓝牙低功耗设备。
  3. 调用setCharacteristicChangeNotification()接口向服务端发送设置通知此特征值请求。
  4. 调用on('BLECharacteristicChange')接口订阅蓝牙低功耗设备的特征值变化事件。需要先调用setCharacteristicChangeNotification接口或setCharacteristicChangeIndication接口才能接收服务端的通知。
  5. 调用on('BLEConnectionStateChange')接口订阅蓝牙低功耗设备的连接状态变化事件。

蓝牙扫描管理

场景描述

BLE扫描主要分为被动扫描和主动扫描两种模式。被动扫描是指设备监听周围的广播信息,不主动发起连接请求;而主动扫描则会在接收到广播的同时,向广播设备发送扫描请求,以获取更详尽的设备信息。

蓝牙扫描管理是在主动扫描中,对扫描出的结果进行管理。主动扫描时,不仅可以设置扫描参数,还能处理扫描结果。如扫描时设置扫描时间间隔、扫描持续时间等;扫描出结果后,可对设备列表更新,并过滤相关设备信息。

实现原理

蓝牙扫描步骤如下:

首先服务端需要检查蓝牙状态,如未开启蓝牙,则需要向用户申请开启蓝牙;如已开启,则开启广播。然后客户端也需要检查蓝牙状态,如未开启蓝牙,则需要向用户申请开启蓝牙;如已开启,则设置扫描参数,并启动蓝牙扫描。根据扫描结果来处理相关设备信息并展示,然后主动发起蓝牙连接请求,连接远端设备。

关键技术

服务端:

  1. 调用access.getState()接口,查询设备的蓝牙状态。根据返回结果BluetoothState的值来判断蓝牙是否开启。
  2. 调用startAdvertising()接口,开启广播。只有服务端先开启广播,客户端才能搜索到设备。
  3. 当完成广播时,调用stopAdvertising()接口,停止广播。

客户端:

  1. 与服务端步骤1相同。
  2. 调用startBLEScan接口,可通过options参数传入设置的扫描参数,进行扫描。
  3. 调用on('BLEDeviceFind')方法订阅BLE设备发现上报事件,并处理扫描到的结果。
  4. 调用connect()接口,连接远端蓝牙低功耗设备。
  5. 当断开连接时,调用stopBLEScan()接口,停止蓝牙扫描。

实现流程

服务端

  1. 导入ble、access模块。

    import { ble } from '@kit.ConnectivityKit';
    import { access } from '@kit.ConnectivityKit';

  2. 开启广播前,需要先查询蓝牙是否可用,根据返回结果BluetoothState的值来判断蓝牙是否开启,当取值是STATE_ON 或STATE_TURNING_ON返回true,表示蓝牙已开启。

    isBluetoothEnabled(): boolean {
      const state: access.BluetoothState = access.getState();
      if (state === access.BluetoothState.STATE_ON || state === access.BluetoothState.STATE_TURNING_ON) {
        return true;
      }
      return false;
    }

  3. 如果蓝牙未开启,服务端设备需要调用access.enableBluetooth()接口开启蓝牙。

    access.enableBluetooth();

  4. 调用createGattServer()创建一个Gatt服务的实例,并调用addservice()接口,添加服务。服务添加成功后,调用startAdvertising()接口开始广播。

    startAdvertiser(): boolean {
      if (!this.isBluetoothEnabled()) {
        this.enableBluetooth();
        promptAction.showToast({
          message: 'Bluetooth enabled, please wait...',
          duration: 2000
        })
        return false;
      }
      // Create a GattServer instance
      this.mGattServer = ble.createGattServer();
    
      // ...
    
      // Define the heart rate beating service
      const service: ble.GattService = {
        serviceUuid: '0000180D-0000-1000-8000-00805F9B34FB',
        isPrimary: true,
        characteristics: characteristics,
        includeServices: []
      }
    
      // Add a service
      this.mGattServer.addService(service);
    
      try {
        // The status of the subscription connection service
        this.onConnectStateChange();
        // ...
    
        // Start advertising
        ble.startAdvertising(setting, advData, advResponse);
        return true;
      } catch (err) {
        Logger.error(`startAdvertiser: err = ${JSON.stringify(err)}`);
      }
      return false
    }

客户端

  1. 导入ble模块。

    import { ble } from '@kit.ConnectivityKit';

  2. 客户端设备使用蓝牙功能时,同样需要开启蓝牙,开启方法与服务端中一致。

    开启蓝牙后,调用startBLEScan()接口发起BLE扫描流程。
    startBLEScan(): boolean {
      if (!this.isBluetoothEnabled()) {
        this.enableBluetooth();
        promptAction.showToast({
          message: 'Bluetooth enabled, please wait...',
          duration: 2000
        })
        return false;
      }
      this.onBLEDeviceFind();
      const ret = this.startBLEScanInner();
      return ret;
    }
    onBLEDeviceFind方法中通过ble.on('BLEDeviceFind')订阅BLE设备发现上报事件,并将扫描到的设备存放到AppStorage中。
    onBLEDeviceFind() {
      try {
        ble.on('BLEDeviceFind', (arr: Array<ble.ScanResult>) => {
          let deviceId: string = arr[0].deviceId;
          let deviceName: string = arr[0].deviceName;
          let device = this.mAvailableDevices.find((availableDevice => {
            return availableDevice.deviceId === deviceId;
          }))
          if (!device) {
            let data: ArrayBuffer = arr[0].data;
            deviceName = util.TextDecoder.create('utf-8', {
              ignoreBOM: true
            }).decodeToString(new Uint8Array(data), { stream: false });
            this.mAvailableDevices = [];
            this.mAvailableDevices.push(this.getDevice(deviceId, deviceName));
            AppStorage.setOrCreate('availableDevices', this.mAvailableDevices);
          }
        })
      } catch (err) {
        Logger.info(`onBLEDeviceFind: err = ${JSON.stringify(err)}`);
      }
    }

  3. 扫描到设备后,用户选择对应的蓝牙设备进行连接。

    connectInner(gattClientDevice: ble.GattClientDevice): boolean {
      try {
        if (!gattClientDevice) {
          return false;
        }
        // The subscription connection status changes
        this.onBLEConnectionStateChange();
        // Subscription feature value changes
        this.onBLECharacteristicChange();
        // Start connecting
        gattClientDevice.connect();
        this.mConnectBluetoothDevice.connectionState = ConnectionState.STATE_CONNECTING;
        AppStorage.setOrCreate('connectBluetoothDevice', this.mConnectBluetoothDevice);
        return true;
      } catch (err) {
        Logger.error(`connectInner: err = ${JSON.stringify(err)}`);
      }
      return false;
    }

蓝牙连接状态管理

蓝牙连接状态管理可实时监测蓝牙连接的质量,包括信号强度、数据传输速率、误码率等指标。在蓝牙设备连接后,经常会遇到服务端、客户端连接状态改变的情况,例如客户端连接后,服务端关闭广播导致连接断开。针对此问题,在进行蓝牙设备开发的时候,需要对蓝牙的连接进行管理。

实现原理

在服务端与客户端进行连接的相关操作时,注册对应的监听事件。如监听蓝牙设备开关状态、监听蓝牙状态变化等。当对应状态发生变化时,即可通过回调函数来处理相应逻辑。

关键技术

服务端:

1.调用on('stateChange')方法订阅蓝牙设备开关状态事件。

2.调用on('connectionStateChange')方法订阅蓝牙状态变化事件。

客户端:

1.订阅蓝牙设备开关状态事件,与服务端步骤1一致。

2.调用on('BLEConnectionStateChange')订阅蓝牙低功耗设备的连接状态变化事件。

实现流程

服务端

  1. 导入ble、access模块。

    import { ble } from '@kit.ConnectivityKit';
    import { access } from '@kit.ConnectivityKit';

  2. 调用access.on('stateChange')订阅蓝牙设备开关状态事件。

    (1)根据蓝牙的状态BluetoothState进行处理,如果当前蓝牙开启,AppStorage存储当前蓝牙状态为true,表示蓝牙可用,并开启广播。

    (2)如果当前蓝牙关闭,则当前蓝牙不可用,AppStorage存储当前蓝牙状态为false,表示蓝牙不可用。
    onBTStateChange() {
      try {
        access.on('stateChange', (data: access.BluetoothState) => {
          if (data === access.BluetoothState.STATE_ON) {
            AppStorage.setOrCreate('bluetoothEnable', true);
          } else if (data === access.BluetoothState.STATE_OFF) {
            AppStorage.setOrCreate('bluetoothEnable', false);
          }
        })
      } catch (err) {
        Logger.error(`onBTSateChange: err = ${JSON.stringify(err)}`);
      }
    }

  3. 调用on('connectionStateChange')订阅蓝牙连接变化事件。

    (1)根据连接状态设置连接设备ID。

    (2)如果断连,则将连接设备ID设置为空。
    onConnectStateChange() {
      if (!this.mGattServer) {
        return;
      }
    
      try {
        this.mGattServer.on('connectionStateChange', (data: ble.BLEConnectionChangeState) => {
          if (data) {
            if (data.state === constant.ProfileConnectionState.STATE_CONNECTED) {
              let deviceId = data.deviceId;
              AppStorage.setOrCreate('deviceId', deviceId);
            } else if (data.state === constant.ProfileConnectionState.STATE_DISCONNECTED) {
              AppStorage.setOrCreate('deviceId', '');
              this.stopAdvertiser();
            }
          }
        })
      } catch (err) {
        Logger.error(`connectInner: err = ${JSON.stringify(err)}`);
      }
    }

客户端

  1. 导入ble、access模块。

    import { ble } from '@kit.ConnectivityKit';
    import { access } from '@kit.ConnectivityKit';

  2. 调用access.on('stateChange')订阅蓝牙设备开关状态事件。

    (1)根据蓝牙的状态BluetoothState进行处理,如果当前蓝牙开启,则发起BLE扫描流程。

    (2)如果当前蓝牙关闭,即当前蓝牙不可用,此时需要将当前可连接设备设置为空数组。
    onBTStateChange() {
      try {
        access.on('stateChange', (data: access.BluetoothState) => {
          if (data === access.BluetoothState.STATE_ON) {
            this.startBLEScan();
          } else if (data === access.BluetoothState.STATE_OFF) {
            this.mAvailableDevices = [];
            AppStorage.setOrCreate('availableDevices', this.mAvailableDevices);
          }
        })
      } catch (err) {
        Logger.error(`onBTSateChange: err = ${JSON.stringify(err)}`);
      }
    }

  3. 使用on('BLEConnectionStateChange')订阅连接状态变化。

    onBLEConnectionStateChange() {
      if (!this.mGattClientDevice) {
        return;
      }
    
      try {
        this.mGattClientDevice.on('BLEConnectionStateChange', async (data: ble.BLEConnectionChangeState) => {
    
          let state: constant.ProfileConnectionState = data.state;
          if (data) {
            if (state === constant.ProfileConnectionState.STATE_CONNECTED) {
              this.mConnectBluetoothDevice.connectionState = ConnectionState.STATE_CONNECTED;
              AppStorage.setOrCreate('availableDevices', this.mAvailableDevices);
              // The connection is successful, and the service is obtained
              let services: Array<ble.GattService> | undefined = await this.mGattClientDevice?.getServices();
              let service: ble.GattService | undefined =
                services?.find(item => item.serviceUuid === '0000180D-0000-1000-8000-00805F9B34FB')
              let characteristics: Array<ble.BLECharacteristic> | undefined = service?.characteristics;
              let characteristic: ble.BLECharacteristic | undefined =
                characteristics?.find(item => item.characteristicUuid === '00002A37-0000-1000-8000-00805F9B34FB');
              // Sends a setup notification request for this feature value to the server
              this.mGattClientDevice?.setCharacteristicChangeNotification(characteristic, true);
              let descriptors: Array<ble.BLEDescriptor> | undefined = characteristic?.descriptors;
              let descriptor = descriptors?.find(item => item.descriptorUuid === '00002902-0000-1000-8000-00805F9B34FB')
              let descriptorValue = ArrayBufferUtils.byteArray2ArrayBuffer([0x01, 0x00]);
              let descriptorObj: BLEDescriptor = {
                serviceUuid: descriptor?.serviceUuid,
                characteristicUuid: descriptor?.characteristicUuid,
                descriptorValue: descriptorValue,
                descriptorUuid: descriptor?.descriptorUuid
              }
              // Writes binary data to a Bluetooth Low energy device-specific descriptor
              this.mGattClientDevice?.writeDescriptorValue(descriptorObj);
            } else if (state === constant.ProfileConnectionState.STATE_DISCONNECTED) {
              this.mConnectBluetoothDevice.connectionState = ConnectionState.STATE_DISCONNECTED;
              AppStorage.setOrCreate('availableDevices', []);
              if (this.getConnectionState() === ConnectionState.STATE_CONNECTING) {
                this.close();
              } else if (this.getConnectionState() === ConnectionState.STATE_CONNECTED ||
                this.getConnectionState() === ConnectionState.STATE_DISCONNECTING) {
                this.close();
              }
            }
          }
        })
      } catch (err) {
        Logger.error(`onBLEConnectionStateChange: err = ${JSON.stringify(err)}`);
      }
    }

蓝牙设备特征值同步

在低功耗蓝牙通信中,特征值是数据的基本单元,它是一种用于存储或传输特定类型数据的实体。许多蓝牙设备需要实时传输数据,如运动传感器将加速度、陀螺仪等数据传输到手机应用。如果特征值不同步,数据可能会出现延迟、丢失或者错误的顺序,导致接收端(如手机应用)无法准确地对运动状态进行分析,像在运动步数统计中,可能会少计或多计步数。

实现原理

在服务端与客户端连接成功后,服务端通过调用相关接口,主动通知客户端特征值发生变化;而客户端需要调用接口向服务端来订阅特征值变化,当特征值变化时,客户端即可接收到相关变化信息。

关键技术

服务端:

1.调用notifyCharacteristicChanged方法主动通知已连接的客户端设备,特征值变化。

客户端:

1.调用setCharacteristicChangeNotification接口向服务端发送设置通知此特征值请求。

2.调用on('BLECharacteristicChange')订阅蓝牙低功耗设备的特征值变化事件。

(1)根据UUID对服务进行过滤,获取服务特征值配置。

(2)调用setCharacteristicChangeNotification,向服务端发送设置通知此特征值请求。

(3)调用writeDescriptorValue,向低功耗蓝牙设备特定的描述符写入二进制数据。

实现流程

服务端

  1. 导入ble模块。

    import { ble } from '@kit.ConnectivityKit';

  2. 调用notifyCharacteristicChanged,特征值发生变化时,主动通知已连接的客户端设备。

    notifyCharacteristicChanged(deviceId: string, heartRate: number) {
      if (!deviceId) {
        return;
      }
      if (this.mGattServer) {
        try {
          let descriptors: Array<ble.BLEDescriptor> = [];
          let arrayBuffer = ArrayBufferUtils.byteArray2ArrayBuffer([11]);
          // Define descriptors
          let descriptor: ble.BLEDescriptor = {
            serviceUuid: '0000180D-0000-1000-8000-00805F9B34FB',
            characteristicUuid: '00002A37-0000-1000-8000-00805F9B34FB',
            descriptorUuid: '00002902-0000-1000-8000-00805F9B34FB',
            descriptorValue: arrayBuffer
          }
          descriptors[0] = descriptor;
    
          let arrayBufferC = ArrayBufferUtils.byteArray2ArrayBuffer([0x00, heartRate]);
          // Define characteristic
          let characteristic: CharacteristicModel = {
            serviceUuid: '0000180D-0000-1000-8000-00805F9B34FB',
            characteristicUuid: '00002A37-0000-1000-8000-00805F9B34FB',
            characteristicValue: arrayBufferC,
            descriptors: descriptors
          }
    
          // Define notifyCharacteristic
          let notifyCharacteristic: NotifyCharacteristicModel = {
            serviceUuid: '0000180D-0000-1000-8000-00805F9B34FB',
            characteristicUuid: '00002A37-0000-1000-8000-00805F9B34FB',
            characteristicValue: characteristic.characteristicValue,
            confirm: false
          }
    
          // Notifies the connected client of the change of the feature value
          this.mGattServer.notifyCharacteristicChanged(deviceId, notifyCharacteristic, (err: BusinessError) => {
            if (err) {
              Logger.error(`notifyCharacteristicChanged callback failed: err = ${JSON.stringify(err)}`);
            } else {
              Logger.info('notifyCharacteristicChanged callback success')
            }
          })
        } catch (err) {
          Logger.error(`notifyCharacteristicChanged: err = ${JSON.stringify(err)}`);
        }
      }
    }

客户端

  1. 导入ble模块。

    import { ble } from '@kit.ConnectivityKit';

  2. 调用setCharacteristicChangeNotification接口,向服务端发送设置通知此特征值请求。

    onBLEConnectionStateChange() {
      if (!this.mGattClientDevice) {
        return;
      }
    
      try {
        this.mGattClientDevice.on('BLEConnectionStateChange', async (data: ble.BLEConnectionChangeState) => {
    
          let state: constant.ProfileConnectionState = data.state;
          if (data) {
            if (state === constant.ProfileConnectionState.STATE_CONNECTED) {
              this.mConnectBluetoothDevice.connectionState = ConnectionState.STATE_CONNECTED;
              AppStorage.setOrCreate('availableDevices', this.mAvailableDevices);
              // The connection is successful, and the service is obtained
              let services: Array<ble.GattService> | undefined = await this.mGattClientDevice?.getServices();
              let service: ble.GattService | undefined =
                services?.find(item => item.serviceUuid === '0000180D-0000-1000-8000-00805F9B34FB')
              let characteristics: Array<ble.BLECharacteristic> | undefined = service?.characteristics;
              let characteristic: ble.BLECharacteristic | undefined =
                characteristics?.find(item => item.characteristicUuid === '00002A37-0000-1000-8000-00805F9B34FB');
              // Sends a setup notification request for this feature value to the server
              this.mGattClientDevice?.setCharacteristicChangeNotification(characteristic, true);
              let descriptors: Array<ble.BLEDescriptor> | undefined = characteristic?.descriptors;
              let descriptor = descriptors?.find(item => item.descriptorUuid === '00002902-0000-1000-8000-00805F9B34FB')
              let descriptorValue = ArrayBufferUtils.byteArray2ArrayBuffer([0x01, 0x00]);
              let descriptorObj: BLEDescriptor = {
                serviceUuid: descriptor?.serviceUuid,
                characteristicUuid: descriptor?.characteristicUuid,
                descriptorValue: descriptorValue,
                descriptorUuid: descriptor?.descriptorUuid
              }
              // Writes binary data to a Bluetooth Low energy device-specific descriptor
              this.mGattClientDevice?.writeDescriptorValue(descriptorObj);
            } else if (state === constant.ProfileConnectionState.STATE_DISCONNECTED) {
              this.mConnectBluetoothDevice.connectionState = ConnectionState.STATE_DISCONNECTED;
              AppStorage.setOrCreate('availableDevices', []);
              if (this.getConnectionState() === ConnectionState.STATE_CONNECTING) {
                this.close();
              } else if (this.getConnectionState() === ConnectionState.STATE_CONNECTED ||
                this.getConnectionState() === ConnectionState.STATE_DISCONNECTING) {
                this.close();
              }
            }
          }
        })
      } catch (err) {
        Logger.error(`onBLEConnectionStateChange: err = ${JSON.stringify(err)}`);
      }
    }

  3. 订阅蓝牙低功耗设备的特征值变化事件BLECharacteristicChange。需要先调用setCharacteristicChangeNotification接口或setCharacteristicChangeIndication接口才能接收server端的通知

    onBLECharacteristicChange() {
      if (!this.mGattClientDevice) {
        return;
      }
      this.mGattClientDevice.on('BLECharacteristicChange', (data: ble.BLECharacteristic) => {
        let characteristicValue: ArrayBuffer = data.characteristicValue;
        let byteArr = ArrayBufferUtils.arrayBuffer2ByteArray(characteristicValue);
        let hearRate = byteArr[1];
        AppStorage.setOrCreate('heartRate', hearRate);
      })
    }

常见问题

设备连接后调用getServices()获取不到数据

通过on('BLEConnectionStateChange')接口订阅连接状态,连接状态订阅中返回状态值为2的时候,表示连接成功,之后再调用getServices()获取数据。

扫描获取的deviceId与真实地址不同

基于信息安全考虑,此处获取的设备地址为随机MAC地址。配对成功后,该地址不会变更;已配对设备取消配对后重新扫描或蓝牙服务下电时,该随机地址会变更。

搜索
请输入您想要搜索的关键词