Android Bluetooth 之 BLE 低功耗蓝牙

618 阅读5分钟

Android Bluetooth 之 BLE 低功耗蓝牙

  • Bluetooth Low Energy 低功耗蓝牙,旨在显著降低功耗(优化数据传输效率,通过最小化字节使用实现低功耗),适用于可穿戴设备、智能家居、传感器设备、心率监测器和车载系统等场景,支持短距离(通常在 100 米以内)间歇性数据传输,BLE 搜索、连接的速度更快,不过传输的速度慢,传输的数据量也很小
  • 基于 GATT 协议,所有数据通过 Attributes 属性(属性就代表一小段数据)进行传输,属性结构包含 Service 服务、Characteristic 特征值和 Descriptor 描述符三层,每个属性均由 128 位 UUID 通用唯一标识符进行标识,规定每次最多有效传输 20 个字节,超过了就需要分批(分包)多次进行传输了
  • 基于客户端-服务端架构,分为 Central 中央设备与 Peripheral 外围设备,中央设备作为 GATT 客户端(主设备,比如安卓手机设备),外围设备作为 GATT 服务端(从设备,比如心率监测器),如果两个设备都仅支持中央角色,则无法相互通信,如果两个设备都仅支持外围角色,也无法相互通信,另外通信都是由客户端主动发起并接收服务端的响应的
    • 中央设备:主动进行扫描、查找、接收广播的设备(比如安卓手机设备)
    • 外围设备:发送广播的设备(比如智能手环)
  • 一个外围设备同时只能被一个中央设备连接,一旦这个外围设备被连接就会马上停止广播,对其他设备就不可见了,而当这个外围设备断开后又开始广播,另外如果想要让两个外围设备能通信,只能通过中心设备进行中转
  • 可以同时使用传统蓝牙和低功耗蓝牙,但是不能同时扫描(搜索、发现、查询)低功耗蓝牙设备和传统蓝牙设备,相对于传统蓝牙设备需要先配对,而 BLE 无需配对

GATT

  • GATT:Generic Attribute Profile 通用属性配置文件,基于 ATT 属性协议实现应用级数据结构(比如服务、特征值等)和数据交互规范,实现设备间复杂数据传输
  • ATT:Attribute Protocol 属性协议,定义了 Attribute 属性的读写、通知等操作规则,直接负责数据的传输
  • GAP:Generic Access Profile 通用访问配置文件,定义设备发现、连接、断开、设备角色和安全配置等基础规则,控制设备广播与扫描,使得设备之间能够通过 ATT 和 GATT 进行通信

Attribute

  • Service:是一组相关的功能或数据,包含一个或者多个 Characteristic 特征值
  • Characteristic:是数据传输的最小基本单位,是 Service 服务包含的具体数据项
  • Descriptor:特征值补充描述属性(对特征值的额外说明,比如通知配置)

MTU

  • Maximum Transmission Unit 最大传输单元
  • BLE 传输速率较低,所以推荐单次传输数据建议不超过 20 字节(MTU 默认是 23 个字节,其中只有 20 个有效字节)
//建立连接之后调用
bluetoothGatt.requestMtu(247) //MTU 协商,实际生效不会超过设备的最大支持数

基本流程

  • 1 权限声明、权限申请、硬件检测与蓝牙启用
  • 2 BluetoothLeScanner#startScan 扫描 BLE 设备
  • 3 BluetoothDevice#connectGatt 连接 BLE 设备
  • 4 BluetoothGatt#discoverServices 发现服务
  • 5 特征值的读写与通知订阅
  • 6 断开连接与释放清理资源

1 权限声明、权限申请、硬件检测与蓝牙启用

权限相关

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<!-- 高版本额外需要位置权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- 可选硬件声明,声明设备支持 BLE,防止不支持的设备安装 -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

检查硬件支持情况

//使用 PackageManager 来检查设备是否支持 BLE 功能
packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)

蓝牙启用

val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter //获取蓝牙适配器
if (bluetoothAdapter?.isEnabled != true) {
    //询问用户开启蓝牙
    Log.e(TAG, "请先开启蓝牙")
    val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
    startActivityForResult(enableBtIntent, REQUEST_CODE_ENABLE_BT)
    return
}

2 扫描 BLE 设备

private val REQUEST_CODE_ENABLE_BT = 1
private val SCAN_TIMEOUT = 10_000L //扫描超时时间
private var bluetoothLeScanner: BluetoothLeScanner? = null
private var bluetoothGatt: BluetoothGatt? = null
//
private val handler = Handler(Looper.getMainLooper())
//BLE 扫描回调 android.bluetooth.le.ScanCallback
private val leScanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        val bluetoothDevice = result.device
        //处理扫描到的设备(比如展示在列表中供选择,存入列表之前可能需要先判断去重)
        Log.e(TAG, "onScanResult: name:${bluetoothDevice.name} address:${bluetoothDevice.address}")
    }
    override fun onScanFailed(errorCode: Int) {
        Log.e(TAG, "onScanFailed 扫描失败,错误码:$errorCode")
    }
}
private fun startBleScan() {
    val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    val bluetoothAdapter = bluetoothManager.adapter //获取蓝牙适配器
    if (bluetoothAdapter?.isEnabled != true) {
        //开启蓝牙(需用户确认)
        Log.e(TAG, "startBleScan: 请先开启蓝牙")
        val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        startActivityForResult(enableBtIntent, REQUEST_CODE_ENABLE_BT)
        return
    }
    //配置扫描过滤
    val scanFilter = ScanFilter.Builder()
        //.setDeviceName("deviceName")
        .build()
    //配置扫描参数
    val scanSettings = ScanSettings.Builder()
        //扫描模式:SCAN_MODE_LOW_POWER 省电模式(默认)、SCAN_MODE_BALANCED 平衡模式
        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) //低延迟模式
        //回调类型:CALLBACK_TYPE_FIRST_MATCH 仅接收第一个匹配的
        .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
        .build()
    //获取 BluetoothLeScanner 扫描器
    bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
    //启动扫描
    bluetoothLeScanner?.startScan(listOf(scanFilter), scanSettings, leScanCallback)
    //定时停止扫描,推荐设置合适的扫描时间间隔,避免长时间持续扫描,在不需要的时候要及时调用停止扫描,以节省功耗
    handler.postDelayed({ stopBleScan() }, SCAN_TIMEOUT)
}
//停止扫描
private fun stopBleScan() {
    bluetoothLeScanner?.stopScan(leScanCallback)
}

3 连接 BLE 设备、4 发现服务、5 特征值的读写与通知订阅

private fun connectBleDevice(macAddress: String) {
    val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                //与设备 GATT 服务端建立连接成功
                gatt.discoverServices() //连接成功后进行发现服务
            } else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
                gatt.close()
            }
        }
        override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                //服务发现成功
                val gattServiceList = gatt.services
                for (gattService in gattServiceList) {
                    for (characteristic in gattService.characteristics) {
                        Log.e(TAG, "onServicesDiscovered: ${characteristic.value}")
                    }
                }
                //
                val gattService = gatt.getService(UUID.fromString("UUID"))
                val characteristic = gattService.getCharacteristic(UUID.fromString("UUID"))
                //读取特征值,触发 BluetoothGattCallback#onCharacteristicRead 回调
                gatt.readCharacteristic(characteristic)
                //写入特征值,触发 BluetoothGattCallback#onCharacteristicWrite 回调
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    gatt.writeCharacteristic(characteristic, "data", 0)
                } else {
                    characteristic.setValue("data")
                    gatt.writeCharacteristic(characteristic)
                }
                //为某个特征值启用通知(监听数据变化),触发 BluetoothGattCallback#onCharacteristicChanged 回调
                gatt.setCharacteristicNotification(characteristic, true)
                //
                val descriptor = characteristic.getDescriptor(UUID.fromString("UUID"))
                descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
                gatt.writeDescriptor(descriptor) //触发 BluetoothGattCallback.onDescriptorWrite 回调
            }
        }
        override fun onCharacteristicRead(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            value: ByteArray,
            status: Int
        ) {
            if (status === BluetoothGatt.GATT_SUCCESS) {
                Log.d(TAG, "读取特征值 value: ${String(value)}")
            }
        }
        override fun onCharacteristicWrite(
            gatt: BluetoothGatt?,
            characteristic: BluetoothGattCharacteristic?,
            status: Int
        ) {
            if (status === BluetoothGatt.GATT_SUCCESS) {
                Log.d(TAG, "写入特征值成功!")
            }
        }
        override fun onCharacteristicChanged(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            value: ByteArray
        ) {
            //接收特征值变化
        }
    }
    val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    val bluetoothAdapter = bluetoothManager.adapter //获取蓝牙适配器
    val bluetoothDevice = bluetoothAdapter?.getRemoteDevice(macAddress)
    //通过 connectGatt 方法与设备 GATT 服务端建立连接
    //bluetoothGatt = bluetoothDevice?.connectGatt(this, false, gattCallback)
    bluetoothGatt = bluetoothDevice?.connectGatt(this, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
}

6 断开连接与释放清理资源

fun releaseGatt() {
    bluetoothGatt?.disconnect()
    bluetoothGatt?.close()
    bluetoothGatt = null
}

总结

  • BLE 基于 GATT 协议,设备之间通过 Service 和 Characteristic 等 Attribute 属性进行数据通信(设备暴露服务和特征值供读写和通知),每个 Service 可以包含一个或多个 Characteristic
  • 可以从 Characteristic 读取数据,也可以往 Characteristic 写入数据,从而实现了双向的通信