一、引言
本文记录在之前进行的仪表类多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. 权限声明清单
- [/code][size=6]四、具体实现[/size]
- [size=5](一) GATT 工作流程[/size]
- [size=4]1. [b]CCCD[/b][/size]
- [b]CCCD[/b]([b]C[/b]lient [b]C[/b]haracteristic [b]C[/b]onfiguration [b]D[/b]escriptor):
- 特殊的描述符,控制某个特征值是否通知Notify,即通知开关。
- [table][tr][b]写入值(16位)[/b][b]含义[/b][b]用途[/b][/tr][tr][td]0x0000[/td][td]禁用通知和指示(默认值)[/td][td]初始状态,不接收推送[/td][/tr][tr][td]0x0001[/td][td]启用 [b]Notification(通知)[/b][/td][td]服务器可主动发送数据,无需确认[/td][/tr][tr][td]0x0002[/td][td]启用 [b]Indication(指示)[/b][/td][td]服务器发送数据后,必须等待客户端回复 ACK[/td][/tr][/table]如果没有配置[b]CCCD[/b],即便低功耗蓝牙设备具备通知功能,也无法进行通知。因此在需要开启消息自动通知时,需要配置CCCD。
- [size=4]2. [b]BluetoothAdapter[/b][/size]
- 注:从Android 4.3(API18)开始,使用BluetoothManager来获取adapter。
- 核心功能:
- [list]
- [*]控制蓝牙开关(Android 12后需要手动确认)
- [*]设备扫描(经典蓝牙和ble的api不同,ble需要通过其子对象 BluetoothLeScanner扫描)
- [*]获取蓝牙信息(name,mac)
- [*]通过地址获取设备引用(getRemoteDevice(address))
- [*]通过广播监听状态变化
- [/list][size=4]3. [b]BluetoothGatt[/b][/size]
- BluetoothGatt 是Android与ble外设通信的桥梁和控制中心,是ble客户端的抽象,并不是设备本身,而是与设备建立连接后获取的一个通信句柄。
- 核心功能:
- [list]
- [*]创建连接(connectGatt,会返回BluetoothGatt实例)
- [*]发现服务(discoverServices,发起服务发现流程)
- [*]读写特征值(readCharacteristic/writeCharacteristic)
- [*]开启本地通知监听(setCharacteristicNotification)
- [*]写描述符(如CCCD)
- [*]请求MTU扩展
- [*]设置连接优先级
- [*]断开连接(disconnect)
- [*]释放资源(close)
- [/list][size=4]4. [b]BluetoothGattCallback[/b][/size]
- 调用BluetoothGatt connectGatt(android.content.Context context, boolean autoConnect, android.bluetooth.BluetoothGattCallback callback, int transport)
- 需要传入一个关键参数,BluetoothGattCallback是一个抽象类,在进行具体实现时,需要继承这个抽象类实现所有的抽象回调方法,如果把BluetoothGatt比作电话,BluetoothGattCallback更像是一个听筒。
- 注意,每个设备和Gatt只能持有自己的gattCallBack
- BluetoothGattCallback包含如下重要的回调方法:
- [list]
- [*]onConnectionStateChange 连接状态回调
- [*]onServicesDiscovered 服务发现回调
- [*]onCharacteristicRead 特征值读回调
- [*]onCharacteristicWrite 特征值写入回调
- [*]onCharacteristicChanged 特征值变化通知回调(对应设置了CCCD的通知)
- [*]onDescriptorWrite 特征值描述写入回调
- [*]onMtuChanged Mtu变化回调
- [/list][size=4]5. [b]工作流程:[/b][/size]
- App 启动 BLE 扫描 → 发现目标设备 → 自动连接 → 建立通信通道 → 准备好读写操作。
- 这里注意是采用特征值通知的方式读取数据,还有一种方式是上位机直接进行特征值读取。
- [align=center][img]https://cdn.nlark.com/yuque/__mermaid_v3/431f390567d85cd0bd3f71acfd618948.svg[/img][/align]
- [size=4]6. [b]注意点:[/b][/size]
- [list=1]
- [*]链路连接成功是onConnectionStateChange回调触发,但并不能直接进行通讯,真正的[b]连接成功[/b]定义在onServicesDiscovered成功之后;
- [/list]
- [list]
- [*]必须在onConnectionStateChange之中手动调用discoverServices
- [*]app主动获取低功耗蓝牙模块制定特征值的数据需要记录对应模块的READ UUID,通过调用readCharacteristic来进行对应特征值的读取,适用于app端主动获取数据的场景。
- [/list][size=5](二) 代码实现[/size]
- [code]/**
- * 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) }
复制代码 来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |