Android | Kotlin实现Ble低功耗蓝牙设备连接
一、引言
本文记录在之前进行的仪表类多ble设备采集项目开发中,使用到的低功耗蓝牙连接技术的总结。
二、概念
(一) 低功耗蓝牙介绍
低功耗蓝牙是4.0版本起支持的蓝牙协议,主要特点是低功耗,传输速度快,传输数据量小的特点。
工作在2.4GHz 频段,使用调频扩频实现抗干扰。
支持广播+点对点快速连接。
(二) GATT (Generic Attribute Profile通用属性配置文件)
Gatt是立在 ATT(Attribute Protocol,属性协议) 之上的用于结构化数据交换的标准方式。
Gatt架构中,有明确的角色划分,分别是服务端和客户端。
Gatt Server(服务器)数据的提供者,提供服务和特征值。
Gatt Client(客户端) 访问服务器数据的设备,发起请求
1. GATT 层次结构:层级模型
GATT 使用一种树状结构来组织数据,从大到小依次为:
Device(设备)
└─── Service(服务)
└─── Characteristic(特征值)
├── Value(值)
└─── Descriptor(描述符,可选)
1.1 Service(服务)
- 表示一类功能或数据集合。
- 每个 Service 包含一个或多个 Characteristic。
Service 的组成:
-
UUID(Universally Unique Identifier):唯一标识符,用来区分不同服务。
-
标准服务使用 16位 UUID(由 Bluetooth SIG 定义)
-
自定义服务使用 128位 UUID,避免冲突。
-
Handle(句柄):内部索引号,用于快速定位。
-
包含关系:一个服务可以“包含”另一个服务(较少见)。
1.2 Characteristic(特征值)
这是 GATT 中最核心的数据单元。
- 特征值代表一个具体的数据项,比如“当前心率”、“开关状态”、“温度值”。
- 每个特征值属于某个服务。
- 包括三部分:
- Value(值):实际的数据内容(如 byte 数组)。
- Properties(属性):说明该特征值支持哪些操作。
- Descriptors(描述符,可选):对值的补充说明(如单位、用户描述)。
特征值的 Properties
这些属性决定了客户端能对该特征值做什么操作:
| 属性 | 功能 | 对应操作 |
|---|---|---|
Read |
可读 | Client 可读取其值 |
Write |
可写 | Client 可写入新值 |
Notify |
通知 | Server 主动向 Client 发送更新(无需回复) |
Indicate |
指示 | 类似 Notify,但要求 Client 回 ACK(确认收到) |
Broadcast |
广播 | 向所有监听设备发送(不常用) |
Write Without Response |
无响应写入 | 快速写入,不等待确认(适合高频数据) |
1.3 Descriptor(描述符)
是对特征值的元数据说明,是可选组件。
三、权限适配
在进行ble设备操作前,必须进行权限的相关配置。
1. Android 6.0 ~ 11(API 23–30)
- 必须获取
ACCESS_FINE_LOCATION - 用户可在设置中关闭
- 即使 App 不需要定位,也必须申请位置权限
2. Android 12+(API ≥ 31)
-
不再需要位置权限
-
使用两个新权限:
-
BLUETOOTH_SCAN:用于扫描 -
BLUETOOTH_CONNECT:用于连接已有设备
3. 权限声明清单
<!-- 必须:使用蓝牙功能 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- 可发现性(仅旧版需要) -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- API 23+:扫描和连接需要位置权限(API 31 以下) -->
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="30" />
<!-- API 31+:新蓝牙权限(取代位置权限) -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- 可选:后台扫描(需特殊声明) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
四、具体实现
(一) GATT 工作流程
1. CCCD
CCCD(Client Characteristic Configuration Descriptor):
特殊的描述符,控制某个特征值是否通知Notify,即通知开关。
| 写入值(16位) | 含义 | 用途 |
|---|---|---|
0x0000 |
禁用通知和指示(默认值) | 初始状态,不接收推送 |
0x0001 |
启用 Notification(通知) | 服务器可主动发送数据,无需确认 |
0x0002 |
启用 Indication(指示) | 服务器发送数据后,必须等待客户端回复 ACK |
如果没有配置CCCD,即便低功耗蓝牙设备具备通知功能,也无法进行通知。因此在需要开启消息自动通知时,需要配置CCCD。
2. BluetoothAdapter
注:从Android 4.3(API18)开始,使用BluetoothManager来获取adapter。
核心功能:
- 控制蓝牙开关(Android 12后需要手动确认)
- 设备扫描(经典蓝牙和ble的api不同,ble需要通过其子对象
BluetoothLeScanner扫描) - 获取蓝牙信息(
name,mac) - 通过地址获取设备引用(
getRemoteDevice(address)) - 通过广播监听状态变化
3. BluetoothGatt
BluetoothGatt 是Android与ble外设通信的桥梁和控制中心,是ble客户端的抽象,并不是设备本身,而是与设备建立连接后获取的一个通信句柄。
核心功能:
- 创建连接(
connectGatt,会返回BluetoothGatt实例) - 发现服务(
discoverServices,发起服务发现流程) - 读写特征值(
readCharacteristic/writeCharacteristic) - 开启本地通知监听(
setCharacteristicNotification) - 写描述符(如
CCCD) - 请求
MTU扩展 - 设置连接优先级
- 断开连接(
disconnect) - 释放资源(
close)
4. BluetoothGattCallback
调用BluetoothGatt connectGatt(android.content.Context context, boolean autoConnect, android.bluetooth.BluetoothGattCallback callback, int transport)
需要传入一个关键参数,BluetoothGattCallback是一个抽象类,在进行具体实现时,需要继承这个抽象类实现所有的抽象回调方法,如果把BluetoothGatt比作电话,BluetoothGattCallback更像是一个听筒。
注意,每个设备和Gatt只能持有自己的gattCallBack
BluetoothGattCallback包含如下重要的回调方法:
onConnectionStateChange连接状态回调onServicesDiscovered服务发现回调onCharacteristicRead特征值读回调onCharacteristicWrite特征值写入回调onCharacteristicChanged特征值变化通知回调(对应设置了CCCD的通知)onDescriptorWrite特征值描述写入回调onMtuChangedMtu变化回调
5. 工作流程:
App 启动 BLE 扫描 → 发现目标设备 → 自动连接 → 建立通信通道 → 准备好读写操作。
这里注意是采用特征值通知的方式读取数据,还有一种方式是上位机直接进行特征值读取。
6. 注意点:
- 链路连接成功是
onConnectionStateChange回调触发,但并不能直接进行通讯,真正的连接成功定义在onServicesDiscovered成功之后;
- 必须在
onConnectionStateChange之中手动调用discoverServices - app主动获取低功耗蓝牙模块制定特征值的数据需要记录对应模块的READ UUID,通过调用
readCharacteristic来进行对应特征值的读取,适用于app端主动获取数据的场景。
(二) 代码实现
/**
* BLE 设备封装类 —— 实现连接、通信与事件分发
*/
class BleDevice(
private val deviceName: String,
private val deviceAddress: String,
private var nativeDevice: BluetoothDevice? = null
) {
companion object {
private const val TAG = "BleDevice"
// 服务与特征值 UUID(请根据实际设备修改)
private val SERVICE_UUID = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb")
private val NOTIFY_CHARACTERISTIC_UUID = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb")
private val WRITE_CHARACTERISTIC_UUID = UUID.fromString("0000fff2-0000-1000-8000-00805f9b34fb")
// CCCD UUID(标准定义)
private val CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
}
// 当前连接状态
@Volatile
var isConnected: Boolean = false
private set
// 回调接口
var stateListener: ((device: BleDevice, state: ConnectionState) -> Unit)? = null
var dataListener: ((device: BleDevice, data: ByteArray) -> Unit)? = null
private var bluetoothGatt: BluetoothGatt? = null
private var writeCharacteristic: BluetoothGattCharacteristic? = null
private var notifyCharacteristicUuid: UUID? = null
// 主线程 Handler,用于回调 UI
private val mainHandler = Handler(Looper.getMainLooper())
/**
* 连接设备
*/
fun connect(context: Context) {
if (isConnected) {
Log.w(TAG, "Already connected to $deviceName")
return
}
// 清理旧连接
closeOldConnection()
realConnect(context.applicationContext)
}
private fun closeOldConnection() {
if (bluetoothGatt != null) {
Log.d(TAG, "Closing previous GATT instance to avoid errors...")
try {
bluetoothGatt?.close()
} catch (e: Exception) {
Log.e(TAG, "Error closing old GATT", e)
}
bluetoothGatt = null
// 延迟重连,避免频繁操作导致 Status 133
mainHandler.postDelayed(this::realConnectWithAppContext, 200)
}
}
private val realConnectWithAppContext: () -> Unit = {
// 在延时任务中重新获取 context(需外部传入)
}
private fun realConnect(context: Context) {
val adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
?: run {
stateListener?.invoke(this, ConnectionState.Error("Bluetooth not available"))
return
}
if (!adapter.isEnabled) {
stateListener?.invoke(this, ConnectionState.Error("Bluetooth is disabled"))
return
}
val device = nativeDevice ?: run {
val d = adapter.getRemoteDevice(deviceAddress)
nativeDevice = d
d
}
try {
bluetoothGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
} else {
device.connectGatt(context, false, gattCallback)
}
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Invalid device address: $deviceAddress", e)
stateListener?.invoke(this, ConnectionState.Error("Invalid address"))
}
}
/**
* 断开连接
*/
fun disconnect() {
bluetoothGatt?.disconnect()
}
/**
* 发送数据
*/
fun sendData(data: ByteArray): Boolean {
val char = writeCharacteristic ?: return false
return try {
char.value = data
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
bluetoothGatt?.writeCharacteristic(char) == true
} catch (e: Exception) {
Log.e(TAG, "Failed to write characteristic", e)
false
}
}
/**
* 主动读取特征值(可选)
*/
fun readData() {
val char = bluetoothGatt?.getService(SERVICE_UUID)
?.getCharacteristic(NOTIFY_CHARACTERISTIC_UUID)
?: return
bluetoothGatt?.readCharacteristic(char)
}
// MARK: - GATT Callback
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
Log.i(TAG, "Connected to $deviceName")
isConnected = true
mainHandler.post { stateListener?.invoke(this@BleDevice, ConnectionState.Connected) }
// 请求高优先级连接参数
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
// 开始服务发现
gatt.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
Log.i(TAG, "Disconnected from $deviceName")
isConnected = false
writeCharacteristic = null
notifyCharacteristicUuid = null
gatt.close()
bluetoothGatt = null
mainHandler.post { stateListener?.invoke(this@BleDevice, ConnectionState.Disconnected) }
}
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "Service discovery failed: $status")
mainHandler.post {
stateListener?.invoke(this@BleDevice, ConnectionState.Error("Service discovery failed"))
}
return
}
val targetService = gatt.getService(SERVICE_UUID)
if (targetService == null) {
Log.e(TAG, "Target service not found")
printAllServices(gatt.services)
return
}
for (char in targetService.characteristics) {
when (char.uuid) {
WRITE_CHARACTERISTIC_UUID -> {
writeCharacteristic = char.apply {
writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
}
Log.d(TAG, "Write characteristic found: ${char.uuid}")
}
NOTIFY_CHARACTERISTIC_UUID -> {
Log.d(TAG, "Notify characteristic found: ${char.uuid}")
notifyCharacteristicUuid = char.uuid
enableNotification(gatt, char)
}
}
}
}
// 接收被动通知的数据(服务器主动推送)
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
handleReceivedData(characteristic.value ?: byteArrayOf())
}
// 主动读取返回的数据
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d(TAG, "Read success: ${characteristic.uuid}")
handleReceivedData(characteristic.value ?: byteArrayOf())
}
}
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d(TAG, "CCCD enabled: ${descriptor.characteristic.uuid}")
} else {
Log.e(TAG, "Failed to write descriptor: $status")
}
}
}
private fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
if (!gatt.setCharacteristicNotification(characteristic, true)) {
Log.e(TAG, "Failed to set characteristic notification")
return
}
val cccd = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG)
if (cccd != null) {
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(cccd)
} else {
Log.w(TAG, "CCCD not found for characteristic ${characteristic.uuid}. Trying fallback...")
// 尝试写入第一个可用描述符(部分设备非标准实现)
characteristic.descriptors.firstOrNull()?.let { desc ->
desc.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(desc)
}
}
}
private fun handleReceivedData(value: ByteArray) {
Log.d(TAG, "Received data: ${value.toHexString()}")
mainHandler.post { dataListener?.invoke(this, value) }
}
private fun printAllServices(services: List<BluetoothGattService>) {
Log.d(TAG, "Discovered services:")
services.forEach { service ->
Log.d(TAG, " Service: ${service.uuid}")
service.characteristics.forEach { char ->
Log.d(TAG, " Char: ${char.uuid} | Props: ${char.properties}")
}
}
}
}
// MARK: - 状态枚举
sealed class ConnectionState {
object Connected : ConnectionState()
object Disconnected : ConnectionState()
data class Error(val message: String) : ConnectionState()
}
// MARK: - 工具扩展
private fun ByteArray.toHexString(): String =
this.joinToString(separator = " ") { "%02X".format(it) }
浙公网安备 33010602011771号