Android Bluetooth Development (3) -- Low Power Bluetooth Development

We have already learned about classic Bluetooth development, learned about Bluetooth pairing connection and communication, and configured the A2DP file to realize the connection between the mobile phone and the Bluetooth speaker and play voice.

In this article, we will learn the last chapter of Bluetooth development, Bluetooth Low Energy BLE, which is what we often call Bluetooth 4.0.
The results to be completed today are as follows:

Central equipment peripheral equipment

1. Introduction

Different from traditional Bluetooth, low-power blue is mainly to reduce device power consumption and support communication with devices with lower power consumption (such as heart rate monitors, fitness equipment).

Android has built-in low-power Bluetooth after 4.3 (API 18) and provides the corresponding API to facilitate application discovery of devices. Query services and transfer information

1.1 Related concepts

Bluetooth low energy has two roles, namely central device and peripheral device

  • Peripheral devices: Refers to devices with lower power consumption that will continuously send out broadcasts until connected to the central device.
  • Central device: can scan, find peripheral broadcasts, and get data from the broadcasts

Generally, our mobile phone will act as a central device to search for broadcasts from surrounding peripheral devices, such as health devices. Then the health device is a peripheral device and keeps broadcasting until the central device is connected. After Android 5.0, the phone can also act as a peripheral device.

1.2 Key terms

The key terms regarding BLE are as follows:

  • General Attribute Profile (GATT): The GATT profile is a general specification. The content is mainly aimed at short data fragments when reading and writing BLE communications. Currently, BLE communications are based on GATT.
  • Attribute Protocol (ATT): ATT is the basis of GATT, which transmits attributes and feature services. These attributes have a specific UUID as a unique identifier and serve as the basis for communication.
  • GATT Service: Usually when the central device and the peripheral device need to communicate, they must first know the UUID of the service and establish communication with it, and then pass the FeaturesCarry out data communication with descriptor, etc. We will understand these later

2. Permission configuration

First, you need to use the BLUETOOTH permission and, considering that LE beacons are usually associated with location, also declare the ACCESS_FINE_LOCATION permission. Without this permission, the scan will return no results.

Note: If your app targets Android 9 (API level 28) or lower, you can declare the ACCESS_COARSE_LOCATION permission instead of the ACCESS_FINE_LOCATION permission.

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

<!-- If your app targets Android 9 or lower, you can declare
     ACCESS_COARSE_LOCATION instead. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Notice! Android 10 requires you to turn on gps, otherwise bluetooth will not be available

If you want your device to only support BLE, you can also have the following gods:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

<!-- If your app targets Android 9 or lower, you can declare
     ACCESS_COARSE_LOCATION instead. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

If required="false" is set, you can also determine BLE availability at runtime using PackageManager.hasSystemFeature():

private fun PackageManager.missingSystemFeature(name: String): Boolean = !hasSystemFeature(name)
...

packageManager.takeIf {
    
     it.missingSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) }?.also {
    
    
    Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show()
    finish()
}

3. Find BLE devices

For information about turning on Bluetooth, please refer to Android Bluetooth Development (1) – Traditional Bluetooth Chat Room

To find BLE devices, prior to 5.0, use the startLeScan() method, which returns the broadcast data for the current device and peripheral. However, after 5.0, use startScan() to scan. In order to facilitate the mobile phone to act as a peripheral device, the method after 5.0 is used uniformly.

Scanning is time-consuming. We should stop immediately after scanning the desired device or stop within the specified time. The scanning code is as follows:

fun scanDev(callback: BleDevListener) {
    
    
        devCallback = callback
        if (isScanning) {
    
    
            return
        }

        //扫描设置

        val builder = ScanSettings.Builder()
            /**
             * 三种模式
             * - SCAN_MODE_LOW_POWER : 低功耗模式,默认此模式,如果应用不在前台,则强制此模式
             * - SCAN_MODE_BALANCED : 平衡模式,一定频率下返回结果
             * - SCAN_MODE_LOW_LATENCY 高功耗模式,建议应用在前台才使用此模式
             */
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)//高功耗,应用在前台

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    
    
            /**
             * 三种回调模式
             * - CALLBACK_TYPE_ALL_MATCHED : 寻找符合过滤条件的广播,如果没有,则返回全部广播
             * - CALLBACK_TYPE_FIRST_MATCH : 仅筛选匹配第一个广播包出发结果回调的
             * - CALLBACK_TYPE_MATCH_LOST : 这个看英文文档吧,不满足第一个条件的时候,不好解释
             */
            builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
        }

        //判断手机蓝牙芯片是否支持皮批处理扫描
        if (bluetoothAdapter.isOffloadedFilteringSupported) {
    
    
            builder.setReportDelay(0L)
        }



        isScanning = true
        //扫描是很耗电的,所以,我们不能持续扫描
        handler.postDelayed({
    
    

            bluetoothAdapter.bluetoothLeScanner?.stopScan(scanListener)
            isScanning = false;
        }, 3000)
        bluetoothAdapter.bluetoothLeScanner?.startScan(null, builder.build(), scanListener)
        //过滤特定的 UUID 设备
        //bluetoothAdapter?.bluetoothLeScanner?.startScan()
    }

    }

You can see that after 5.0, some devices can be scanned through ScanSettings, such as setting the scan mode setScanMode. In startScan(), you can also filter your own UUID, thus saving some time. Then in the scan callback, call back the device whose name can be obtained to recyclerview.

 private val scanListener = object : ScanCallback() {
    
    
        override fun onScanResult(callbackType: Int, result: ScanResult?) {
    
    
            super.onScanResult(callbackType, result)
            //不断回调,所以不建议做复杂的动作
            result ?: return
            result.device.name ?: return

            val bean = BleData(result.device, result.scanRecord.toString())
            devCallback?.let {
    
    
                it(bean)
            }
        }

The effect is as follows:
Insert image description here

4. The mobile phone acts as a peripheral device (server)

As mentioned above, after Android 5.0, mobile phones can also serve as peripheral devices. Let’s practice it here;

First, for Android to complete a peripheral device, it needs to complete the following steps:

  1. Write broadcast settings, such as sending practice, sending power, etc.
  2. Write broadcast data, this is needed, you need to set the uuid of the service, or display the name, etc.
  3. Write a scan broadcast (optional). This broadcast is a broadcast that data can be accepted when the central device scans. Usually we will write some manufacturer data here.
  4. Add Gatt service to communicate with the central device

4.1 Broadcast settings

Before sending a broadcast, we can configure the broadcast first:

 /**
   * GAP广播数据最长只能31个字节,包含两中: 广播数据和扫描回复
   * - 广播数据是必须的,外设需要不断发送广播,让中心设备知道
   * - 扫描回复是可选的,当中心设备扫描到才会扫描回复
   * 广播间隔越长,越省电
   */

  //广播设置
  val advSetting = AdvertiseSettings.Builder()
      //低延时,高功率,不使用后台
      .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
      // 高的发送功率
      .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
      // 可连接
      .setConnectable(true)
      //广播时限。最多180000毫秒。值为0将禁用时间限制。(不设置则为无限广播时长)
      .setTimeout(0)
      .build()

As you can see, the broadcast is set here to be connectable, and the broadcast mode is set to SCAN_MODE_LOW_LATENCY high power consumption mode. There are three modes:

  • SCAN_MODE_LOW_POWER: Low power consumption mode. This mode is the default. If the application is not in the foreground, this mode is forced.
  • SCAN_MODE_BALANCED: Balanced mode, returns results at a certain frequency
  • SCAN_MODE_LOW_LATENCY high power consumption mode, it is recommended that the application only use this mode in the foreground

Transmit power is also optional:

  • Advertise using high TX power level: AdvertiseSettings#ADVERTISE_TX_POWER_HIGH
  • Advertise using low TX power level: AdvertiseSettings#ADVERTISE_TX_POWER_LOW
  • Advertise using medium TX power level: AdvertiseSettings#ADVERTISE_TX_POWER_MEDIUM
  • Advertise using the lowest transmit (TX) power level: AdvertiseSettings#ADVERTISE_TX_POWER_ULTRA_LOW

4.2 Configure sending broadcast data

Next, is the broadcast packet:

 //设置广播包,这个是必须要设置的
 val advData = AdvertiseData.Builder()
   .setIncludeDeviceName(true) //显示名字
   .setIncludeTxPowerLevel(true)//设置功率
   .addServiceUuid(ParcelUuid(BleBlueImpl.UUID_SERVICE)) //设置 UUID 服务的 uuid
   .build()

It is easier to understand. Let the broadcast display the Bluetooth name of the mobile phone and set the UUID of the service.

4.3 Configure scanning broadcast (optional)

Scanning broadcast is a broadcast that can be displayed when the central device is scanning. It can add some necessary data, such as manufacturer data, service data, etc. Attention! Like the broadcast above, cannot exceed 31 bytes.

//测试 31bit
        val byteData = byteArrayOf(-65, 2, 3, 6, 4, 23, 23, 9, 9,
            9,1, 2, 3, 6, 4, 23, 23, 9, 9, 8,23,23,23)

        //扫描广播数据(可不写,客户端扫描才发送)
        val scanResponse = AdvertiseData.Builder()
            //设置厂商数据
            .addManufacturerData(0x19, byteData)
            .build()

Finally, use startAdvertising() to start sending broadcasts:

val bluetoothLeAdvertiser = bluetoothAdapter?.bluetoothLeAdvertiser
        //开启广播,这个外设就开始发送广播了
        bluetoothLeAdvertiser?.startAdvertising(
            advSetting,
            advData,
            scanResponse,
            advertiseCallback
        )

Use to monitor whether the broadcast is enabled successfully or not:

    private val advertiseCallback = object : AdvertiseCallback() {
    
    
        override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
    
    
            super.onStartSuccess(settingsInEffect)
            logInfo("服务准备就绪,请搜索广播")
        }

        override fun onStartFailure(errorCode: Int) {
    
    
            super.onStartFailure(errorCode)
            if (errorCode == ADVERTISE_FAILED_DATA_TOO_LARGE) {
    
    
                logInfo("广播数据超过31个字节了 !")
            } else {
    
    
                logInfo("服务启动失败: $errorCode")
            }
        }
    }

At this point, if you search, you can find the Bluetooth name of your phone and the corresponding broadcast data.

4.4 Gatt Service

But if the peripheral device wants to communicate with the central device, it needs to start the Gatt service. As mentioned above, when starting the Service, we need to configureCharacteristic and Descriptor Descriptor, here we will explain the following.

4.3 Characteristic

Characteristic is the smallest logical unit of Gatt communication. A characteristic contains a single value variable and 0-n descriptors used to describe the characteristic variable. Similar to service, each characteristic is identified by a 16-bit or 32-bit uuid. In actual communication, read and write communication is also performed through Characteristic.

So in order to facilitate communication, here we need to add the Characteristic of reading and writing.

//添加读+通知的 GattCharacteristic
  val readCharacteristic = BluetoothGattCharacteristic(
      BleBlueImpl.UUID_READ_NOTIFY,
      BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_NOTIFY,
      BluetoothGattCharacteristic.PERMISSION_READ
  )
  //添加写的 GattCharacteristic
  val writeCharacteristic = BluetoothGattCharacteristic(
      BleBlueImpl.UUID_WRITE,
      BluetoothGattCharacteristic.PROPERTY_WRITE,
      BluetoothGattCharacteristic.PERMISSION_WRITE
  )

Descriptor

Its definition is to describe the defined attributes of GattCharacteristic values, such as specifying readable attributes, acceptable ranges, etc., such as adding descriptors for written characteristics:

 //添加 Descriptor 描述符
        val descriptor =
            BluetoothGattDescriptor(
                BleBlueImpl.UUID_DESCRIBE,
                BluetoothGattDescriptor.PERMISSION_WRITE
            )

        //为特征值添加描述
        writeCharacteristic.addDescriptor(descriptor)

Next, add the characteristics to the service and use openGattServer() to open the Gatt service:

/**
 * 添加 Gatt service 用来通信
 */

//开启广播service,这样才能通信,包含一个或多个 characteristic ,每个service 都有一个 uuid
val gattService =
    BluetoothGattService(
        BleBlueImpl.UUID_SERVICE,
        BluetoothGattService.SERVICE_TYPE_PRIMARY
    )
gattService.addCharacteristic(readCharacteristic)
gattService.addCharacteristic(writeCharacteristic)


val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
//打开 GATT 服务,方便客户端连接
mBluetoothGattServer = bluetoothManager.openGattServer(this, gattServiceCallbak)
mBluetoothGattServer?.addService(gattService)

The code is relatively simple, and then you can use gattServiceCallbak to monitor data success and read and write data:

    private val gattServiceCallbak = object : BluetoothGattServerCallback() {
    
    
        override fun onConnectionStateChange(device: BluetoothDevice?, status: Int, newState: Int) {
    
    
            super.onConnectionStateChange(device, status, newState)
            device ?: return
            Log.d(TAG, "zsr onConnectionStateChange: ")
            if (status == BluetoothGatt.GATT_SUCCESS && newState == 2) {
    
    
                logInfo("连接到中心设备: ${device?.name}")
            } else {
    
    
                logInfo("与: ${device?.name} 断开连接失败!")
            }
        }
        ...

5. Central equipment connects to peripherals (client)

The server code has been configured above. Then, use the connectGatt() method of BluetoothDevice to connect to the GATT service through the scanned broadcast:

 override fun onItemClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) {
    
    
      //连接之前先关闭连接
      closeConnect()
      val bleData = mData[position]
      blueGatt = bleData.dev.connectGatt(this, false, blueGattListener)
      logInfo("开始与 ${bleData.dev.name} 连接.... $blueGatt")
  }

At this point, if your configuration is correct, you can connect to the device through the BluetoothGattCallback callback:

private val blueGattListener = object : BluetoothGattCallback() {
    
    
        override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
    
    
            super.onConnectionStateChange(gatt, status, newState)
            val device = gatt?.device
            if (newState == BluetoothProfile.STATE_CONNECTED){
    
    
                isConnected = true
                //开始发现服务,有个小延时,最后200ms后尝试发现服务
                handler.postDelayed({
    
    
                    gatt?.discoverServices()
                },300)

                device?.let{
    
    logInfo("与 ${it.name} 连接成功!!!")}
            }else if (newState == BluetoothProfile.STATE_DISCONNECTED){
    
    
                isConnected = false
                logInfo("无法与 ${device?.name} 连接: $status")
                closeConnect()
            }
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
    
    
            super.onServicesDiscovered(gatt, status)
           // Log.d(TAG, "zsr onServicesDiscovered: ${gatt?.device?.name}")
            val service = gatt?.getService(BleBlueImpl.UUID_SERVICE)
            mBluetoothGatt = gatt
            logInfo("已连接上 GATT 服务,可以通信! ")
        }

The code should be easy to understand, that is, when the newState in onConnectionStateChange() is BluetoothProfile.STATE_CONNECTED, it means that it has been connected. At this time, try to discover the service. If the onServicesDiscovered() method can also be called back, it proves that the GATT service has been successful at this time. Established and can communicate.

5.1 Reading data

At this time, you can read the data of the peripheral device. This data is read by the peripheral device to the central device. Therefore, the read callback of the peripheral device is as follows:

BluetoothGattServerCallback for peripherals

        override fun onDescriptorReadRequest(
            device: BluetoothDevice?,
            requestId: Int,
            offset: Int,
            descriptor: BluetoothGattDescriptor?
        ) {
    
    
            super.onDescriptorReadRequest(device, requestId, offset, descriptor)
            val data = "this is a test"
            mBluetoothGattServer?.sendResponse(
                device, requestId, BluetoothGatt.GATT_SUCCESS,
                offset, data.toByteArray()
            )
            logInfo("客户端读取 [descriptor ${descriptor?.uuid}] $data")
        }

It's very simple, just send a character transmission of "this is a test"

Central device reading

 /**
     * 读数据
     */
    fun readData(view: View) {
    
    
        //找到 gatt 服务
        val service = getGattService(BleBlueImpl.UUID_SERVICE)
        if (service != null) {
    
    
            val characteristic =
                service.getCharacteristic(BleBlueImpl.UUID_READ_NOTIFY) //通过UUID获取可读的Characteristic
            mBluetoothGatt?.readCharacteristic(characteristic)
        }
    }


    // 获取Gatt服务
    private fun getGattService(uuid: UUID): BluetoothGattService? {
    
    
        if (!isConnected) {
    
    
            Toast.makeText(this, "没有连接", Toast.LENGTH_SHORT).show()
            return null
        }
        val service = mBluetoothGatt?.getService(uuid)
        if (service == null) {
    
    
            Toast.makeText(this, "没有找到服务", Toast.LENGTH_SHORT).show()
        }
        return service
    }

If the GATT service is found, get the Characteristic, the smallest unit of GATT communication, through getCharacteristic(), and read the data through mBluetoothGatt?.readCharacteristic(characteristic), so that the data will be obtained in onCharacteristicRead of the BluetoothGattCallback callback:

override fun onCharacteristicRead(
      gatt: BluetoothGatt?,
      characteristic: BluetoothGattCharacteristic?,
      status: Int
  ) {
    
    
      super.onCharacteristicRead(gatt, characteristic, status)
      characteristic?.let {
    
    
          val data = String(it.value)
          logInfo("CharacteristicRead 数据: $data")
      }
  }

Write the same thing in the same way, so that our BLE low-power Bluetooth learning is over

Reference:
https://www.jianshu.com/p/d273e46f47b1
https://developer.android.google.cn/ guide/topics/connectivity/bluetooth-le

Reprint: https://blog.csdn.net/u011418943/article/details/108401011

Guess you like

Origin blog.csdn.net/gqg_guan/article/details/134332638
Recommended