MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin蓝牙低能耗设备交互方案

2023-02-278.0k 阅读

Kotlin 与蓝牙低能耗设备交互基础

蓝牙低能耗(BLE)简介

蓝牙低能耗(Bluetooth Low Energy,BLE),也被称为蓝牙 4.0 或蓝牙 Smart,是一种短距离、低功耗的无线通信技术。与传统蓝牙相比,BLE 旨在显著降低功耗,同时仍保持一定的数据传输速率,适用于对功耗敏感的设备,如健身追踪器、智能手表、传感器等。BLE 设备通常工作在 2.4GHz 的 ISM 频段,采用时分复用(TDM)的方式进行通信。

BLE 设备角色主要分为中心设备(Central)和外围设备(Peripheral)。中心设备负责发起连接并读取或写入外围设备的数据,而外围设备则提供数据供中心设备访问。例如,智能手表可以作为外围设备,向作为中心设备的手机传输心率数据。

Kotlin 与 BLE 交互的优势

Kotlin 作为一种现代编程语言,与 BLE 交互具有诸多优势。首先,Kotlin 简洁的语法可以减少代码量,使 BLE 交互代码更易读和维护。例如,在处理 BLE 连接状态变化时,Kotlin 的简洁语法能更清晰地表达逻辑。其次,Kotlin 与 Java 兼容,这意味着可以直接使用大量现有的 Java BLE 库,同时利用 Kotlin 的特性进行优化。另外,Kotlin 的空安全特性在处理 BLE 交互中可能出现的空指针异常时非常有用,因为 BLE 操作中涉及到许多可能为空的对象,如蓝牙设备、服务、特征等。

安卓开发环境配置

  1. SDK 版本:确保你的安卓项目的 minSdkVersion 至少为 18,因为蓝牙低能耗支持从安卓 4.3(API 级别 18)开始。在 build.gradle 文件中,可以看到类似如下配置:
android {
    compileSdkVersion 33
    defaultConfig {
        applicationId "com.example.bleapp"
        minSdkVersion 18
        targetSdkVersion 33
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}
  1. 权限配置:在 AndroidManifest.xml 文件中,需要添加蓝牙相关权限:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

ACCESS_FINE_LOCATION 权限在安卓 6.0(API 级别 23)及以上是必需的,因为 BLE 扫描需要获取设备位置信息。同时,还需要在运行时动态请求权限:

private fun requestLocationPermission() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), LOCATION_PERMISSION_REQUEST_CODE)
    }
}

onRequestPermissionsResult 方法中处理权限请求结果:

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
        if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 权限授予,可进行 BLE 扫描
        } else {
            // 权限拒绝处理
        }
    }
}

蓝牙设备的扫描与连接

获取蓝牙适配器

在 Kotlin 中,首先要获取系统的蓝牙适配器。蓝牙适配器是与蓝牙设备进行交互的入口点。可以通过 BluetoothManager 来获取蓝牙适配器:

val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter
if (bluetoothAdapter == null ||!bluetoothAdapter.isEnabled) {
    val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}

上述代码中,先通过 BluetoothManager 获取 BluetoothAdapter,如果蓝牙适配器为空或蓝牙未启用,则弹出一个请求启用蓝牙的对话框。REQUEST_ENABLE_BT 是一个自定义的请求码,用于在 onActivityResult 方法中处理蓝牙启用结果。

扫描 BLE 设备

安卓提供了 BluetoothLeScanner 来扫描 BLE 设备。以下是一个简单的扫描示例:

private val bluetoothLeScanner: BluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
private val scanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        super.onScanResult(callbackType, result)
        val bluetoothDevice = result.device
        // 处理扫描到的设备,例如添加到列表中显示
    }
    override fun onBatchScanResults(results: MutableList<ScanResult>) {
        super.onBatchScanResults(results)
        for (result in results) {
            val bluetoothDevice = result.device
            // 处理批量扫描到的设备
        }
    }
    override fun onScanFailed(errorCode: Int) {
        super.onScanFailed(errorCode)
        // 处理扫描失败情况
    }
}
fun startScan() {
    val scanSettings = ScanSettings.Builder()
      .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
      .build()
    val scanFilters = mutableListOf<ScanFilter>()
    bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback)
}
fun stopScan() {
    bluetoothLeScanner.stopScan(scanCallback)
}

在上述代码中,startScan 方法配置了扫描设置,包括扫描模式为低功耗模式,并启动扫描。scanCallback 是一个 ScanCallback 的实例,用于处理扫描结果、批量扫描结果以及扫描失败的情况。stopScan 方法用于停止扫描。

连接 BLE 设备

一旦扫描到目标 BLE 设备,就可以尝试连接它。连接过程通过 BluetoothGatt 来实现。以下是连接设备的代码示例:

private var bluetoothGatt: BluetoothGatt? = null
fun connect(device: BluetoothDevice) {
    bluetoothGatt = device.connectGatt(this, false, object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            super.onConnectionStateChange(gatt, status, newState)
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                gatt.discoverServices()
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                // 处理断开连接情况
            }
        }
        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            super.onServicesDiscovered(gatt, status)
            // 处理发现服务的情况
        }
    })
}

connect 方法中,调用 device.connectGatt 方法来连接设备,并传入一个 BluetoothGattCallback 实例。在 BluetoothGattCallback 中,onConnectionStateChange 方法用于处理连接状态的变化,当连接成功(newState == BluetoothProfile.STATE_CONNECTED)时,调用 gatt.discoverServices() 方法来发现设备上的服务。onServicesDiscovered 方法在服务发现完成后被调用。

BLE 服务与特征交互

服务与特征概述

BLE 设备通过服务(Service)和特征(Characteristic)来提供数据和功能。服务是一组相关特征的集合,而特征则包含实际的数据值以及对该数据的操作权限。例如,一个心率监测设备可能有一个“心率服务”,该服务包含一个“心率测量特征”。每个特征可以有不同的属性,如可读、可写、可通知等。

发现服务与特征

在连接设备并调用 discoverServices 方法后,会在 onServicesDiscovered 方法中获取设备的服务和特征。以下是获取服务和特征的代码示例:

override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
    super.onServicesDiscovered(gatt, status)
    val services = gatt.services
    for (service in services) {
        val characteristics = service.characteristics
        for (characteristic in characteristics) {
            // 处理每个特征,例如根据 UUID 判断特征用途
            val uuid = characteristic.uuid
            if (uuid == UUID.fromString(SOME_CHARACTERISTIC_UUID)) {
                // 特定特征处理
            }
        }
    }
}

上述代码中,通过 gatt.services 获取设备的所有服务,然后遍历每个服务获取其特征。可以根据特征的 UUID 来判断特征的用途,并进行相应的处理。

读取特征值

读取特征值是与 BLE 设备交互的常见操作之一。以下是读取特征值的代码示例:

fun readCharacteristic(characteristic: BluetoothGattCharacteristic) {
    bluetoothGatt?.readCharacteristic(characteristic)
}
override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
    super.onCharacteristicRead(gatt, characteristic, status)
    if (status == BluetoothGatt.GATT_SUCCESS) {
        val value = characteristic.value
        // 处理读取到的特征值
    }
}

readCharacteristic 方法中,调用 bluetoothGatt?.readCharacteristic(characteristic) 来发起读取操作。在 onCharacteristicRead 方法中,当读取成功(status == BluetoothGatt.GATT_SUCCESS)时,获取特征值并进行处理。

写入特征值

写入特征值可以向 BLE 设备发送数据。例如,向智能灯发送控制指令。以下是写入特征值的代码示例:

fun writeCharacteristic(characteristic: BluetoothGattCharacteristic, value: ByteArray) {
    characteristic.value = value
    bluetoothGatt?.writeCharacteristic(characteristic)
}
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
    super.onCharacteristicWrite(gatt, characteristic, status)
    if (status == BluetoothGatt.GATT_SUCCESS) {
        // 处理写入成功情况
    }
}

writeCharacteristic 方法中,先设置特征的 value,然后调用 bluetoothGatt?.writeCharacteristic(characteristic) 进行写入操作。在 onCharacteristicWrite 方法中,处理写入成功的情况。

特征通知

特征通知允许 BLE 设备主动向中心设备发送数据,而无需中心设备不断读取。例如,心率监测设备可以通过特征通知实时向手机发送心率数据。以下是设置特征通知的代码示例:

fun setCharacteristicNotification(characteristic: BluetoothGattCharacteristic, enabled: Boolean) {
    bluetoothGatt?.setCharacteristicNotification(characteristic, enabled)
    val descriptor = characteristic.getDescriptor(UUID.fromString(BluetoothGattDescriptor.UUID_CLIENT_CHARACTERISTIC_CONFIG))
    descriptor.value = if (enabled) BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE else BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
    bluetoothGatt?.writeDescriptor(descriptor)
}
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
    super.onDescriptorWrite(gatt, descriptor, status)
    if (status == BluetoothGatt.GATT_SUCCESS) {
        // 处理描述符写入成功情况,通知设置生效
    }
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
    super.onCharacteristicChanged(gatt, characteristic)
    val value = characteristic.value
    // 处理通知数据
}

setCharacteristicNotification 方法中,先调用 bluetoothGatt?.setCharacteristicNotification(characteristic, enabled) 设置是否启用通知,然后获取特征的描述符并设置相应的值,最后调用 bluetoothGatt?.writeDescriptor(descriptor) 写入描述符。在 onDescriptorWrite 方法中处理描述符写入成功的情况。当设备有通知数据时,会调用 onCharacteristicChanged 方法,在该方法中处理通知数据。

处理连接状态与异常

连接状态管理

在与 BLE 设备交互过程中,连接状态可能会发生变化,如连接成功、连接失败、断开连接等。通过 BluetoothGattCallbackonConnectionStateChange 方法来管理连接状态:

override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
    super.onConnectionStateChange(gatt, status, newState)
    when (newState) {
        BluetoothProfile.STATE_CONNECTED -> {
            // 连接成功处理,例如发现服务
            gatt.discoverServices()
        }
        BluetoothProfile.STATE_CONNECTING -> {
            // 连接中处理
        }
        BluetoothProfile.STATE_DISCONNECTED -> {
            // 断开连接处理,例如尝试重新连接
            if (shouldAutoReconnect) {
                reconnect()
            }
        }
        BluetoothProfile.STATE_DISCONNECTING -> {
            // 正在断开连接处理
        }
    }
}

在上述代码中,根据 newState 的值进行不同的处理。连接成功时发现服务,断开连接时可根据需要尝试重新连接。

异常处理

在 BLE 交互中,可能会遇到各种异常情况,如扫描失败、连接失败、服务发现失败等。在 BluetoothGattCallback 的相应方法中处理异常:

override fun onScanFailed(errorCode: Int) {
    super.onScanFailed(errorCode)
    when (errorCode) {
        ScanSettings.SCAN_FAILED_ALREADY_STARTED -> {
            // 扫描已在进行中异常处理
        }
        ScanSettings.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> {
            // 应用注册失败异常处理
        }
        ScanSettings.SCAN_FAILED_FEATURE_UNSUPPORTED -> {
            // 功能不支持异常处理
        }
        ScanSettings.SCAN_FAILED_INTERNAL_ERROR -> {
            // 内部错误异常处理
        }
    }
}
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
    super.onConnectionStateChange(gatt, status, newState)
    if (status != BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_DISCONNECTED) {
        // 连接失败断开处理
    }
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
    super.onServicesDiscovered(gatt, status)
    if (status != BluetoothGatt.GATT_SUCCESS) {
        // 服务发现失败处理
    }
}

onScanFailed 方法中,根据不同的 errorCode 处理扫描失败异常。在 onConnectionStateChangeonServicesDiscovered 方法中,处理连接失败和服务发现失败的异常情况。

优化与最佳实践

功耗优化

由于 BLE 设备对功耗敏感,在与 BLE 设备交互时要注意功耗优化。

  1. 扫描策略:尽量使用低功耗扫描模式,如 ScanSettings.SCAN_MODE_LOW_POWER。同时,合理控制扫描时间,避免长时间持续扫描。可以设置一个扫描周期,例如每隔一段时间扫描一次,而不是一直扫描。
  2. 连接管理:在不需要与设备交互时,及时断开连接。避免长时间保持连接状态,因为即使没有数据传输,连接也会消耗一定的电量。可以在应用进入后台时断开连接,在回到前台时重新连接。
  3. 特征通知:如果设备支持特征通知,尽量使用通知来获取数据,而不是频繁读取特征值。通知方式可以减少设备间的数据传输次数,从而降低功耗。

代码结构优化

为了使代码更易读和维护,对 BLE 交互代码进行结构优化。

  1. 封装 BLE 操作:将 BLE 设备的扫描、连接、读写等操作封装成独立的方法或类。例如,可以创建一个 BluetoothLeManager 类,将所有 BLE 相关操作放在该类中,这样在主业务逻辑中调用起来更加清晰。
  2. 使用回调或 RxJava:使用回调机制或 RxJava 来处理 BLE 操作的结果和状态变化。回调机制可以使代码逻辑更清晰,而 RxJava 可以方便地处理异步操作和事件流,提高代码的可组合性和可维护性。例如,使用 RxJava 可以将扫描、连接、发现服务等操作组合成一个链式调用,使代码更加简洁。

兼容性处理

不同的安卓设备和 BLE 设备在实现上可能存在差异,需要进行兼容性处理。

  1. 设备测试:在开发过程中,尽可能在多种安卓设备和 BLE 设备上进行测试,确保应用在不同设备上都能正常工作。不同厂商的设备可能对 BLE 规范的实现存在细微差别,通过广泛测试可以发现并解决兼容性问题。
  2. 版本兼容性:考虑安卓系统版本的兼容性。虽然 BLE 从安卓 4.3 开始支持,但不同版本可能存在一些 API 差异或行为变化。例如,在安卓 6.0 及以上需要动态请求位置权限,在处理时要进行版本判断并采取相应的措施。

通过以上功耗优化、代码结构优化和兼容性处理等最佳实践,可以提高 Kotlin 与 BLE 设备交互的稳定性和性能,为用户提供更好的体验。在实际开发中,还需要根据具体的应用场景和需求,进一步优化和完善 BLE 交互方案。