一、引言本文记录在之前进行的仪表类多ble设备采集项目开发中使用到的低功耗蓝牙连接技术的总结。二、概念(一) 低功耗蓝牙介绍低功耗蓝牙是4.0版本起支持的蓝牙协议主要特点是低功耗传输速度快传输数据量小的特点。工作在2.4GHz 频段使用调频扩频实现抗干扰。支持广播点对点快速连接。(二) GATT Generic Attribute Profile通用属性配置文件Gatt是立在 ATTAttribute Protocol属性协议 之上的用于结构化数据交换的标准方式。Gatt架构中有明确的角色划分分别是服务端和客户端。Gatt Server服务器数据的提供者提供服务和特征值。Gatt Client客户端 访问服务器数据的设备发起请求1. GATT 层次结构层级模型GATT 使用一种树状结构来组织数据从大到小依次为Device设备└─── Service服务└─── Characteristic特征值├── Value值└─── Descriptor描述符可选1.1 Service服务表示一类功能或数据集合。每个 Service 包含一个或多个 Characteristic。Service 的组成UUIDUniversally 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 ~ 11API 23–30必须获取 ACCESS_FINE_LOCATION用户可在设置中关闭即使 App 不需要定位也必须申请位置权限2. Android 12API ≥ 31不再需要位置权限使用两个新权限BLUETOOTH_SCAN用于扫描BLUETOOTH_CONNECT用于连接已有设备3. 权限声明清单android:nameandroid.permission.ACCESS_FINE_LOCATIONandroid:maxSdkVersion30 /android:nameandroid.permission.ACCESS_COARSE_LOCATIONandroid:maxSdkVersion30 /四、具体实现(一) GATT 工作流程1. CCCDCCCDClient 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. BluetoothGattBluetoothGatt 是Android与ble外设通信的桥梁和控制中心是ble客户端的抽象并不是设备本身而是与设备建立连接后获取的一个通信句柄。核心功能创建连接connectGatt会返回BluetoothGatt实例发现服务discoverServices发起服务发现流程读写特征值readCharacteristic/writeCharacteristic开启本地通知监听setCharacteristicNotification写描述符如CCCD)请求MTU扩展设置连接优先级断开连接disconnect释放资源close4. BluetoothGattCallback调用BluetoothGatt connectGatt(android.content.Context context, boolean autoConnect, android.bluetooth.BluetoothGattCallback callback, int transport)需要传入一个关键参数BluetoothGattCallback是一个抽象类在进行具体实现时需要继承这个抽象类实现所有的抽象回调方法如果把BluetoothGatt比作电话BluetoothGattCallback更像是一个听筒。注意每个设备和Gatt只能持有自己的gattCallBackBluetoothGattCallback包含如下重要的回调方法onConnectionStateChange 连接状态回调onServicesDiscovered 服务发现回调onCharacteristicRead 特征值读回调onCharacteristicWrite 特征值写入回调onCharacteristicChanged 特征值变化通知回调对应设置了CCCD的通知onDescriptorWrite 特征值描述写入回调onMtuChanged Mtu变化回调5. 工作流程App 启动 BLE 扫描 → 发现目标设备 → 自动连接 → 建立通信通道 → 准备好读写操作。这里注意是采用特征值通知的方式读取数据还有一种方式是上位机直接进行特征值读取。img6. 注意点链路连接成功是onConnectionStateChange回调触发但并不能直接进行通讯真正的连接成功定义在onServicesDiscovered成功之后必须在onConnectionStateChange之中手动调用discoverServicesapp主动获取低功耗蓝牙模块制定特征值的数据需要记录对应模块的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)}// 当前连接状态Volatilevar isConnected: Boolean falseprivate set// 回调接口var stateListener: ((device: BleDevice, state: ConnectionState) - Unit)? nullvar dataListener: ((device: BleDevice, data: ByteArray) - Unit)? nullprivate var bluetoothGatt: BluetoothGatt? nullprivate var writeCharacteristic: BluetoothGattCharacteristic? nullprivate var notifyCharacteristicUuid: UUID? null// 主线程 Handler用于回调 UIprivate 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 133mainHandler.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 dd}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 falsereturn try {char.value datachar.writeType BluetoothGattCharacteristic.WRITE_TYPE_DEFAULTbluetoothGatt?.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)?: returnbluetoothGatt?.readCharacteristic(char)}// MARK: - GATT Callbackprivate 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 truemainHandler.post { stateListener?.invoke(thisBleDevice, ConnectionState.Connected) }// 请求高优先级连接参数gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)// 开始服务发现gatt.discoverServices()}BluetoothProfile.STATE_DISCONNECTED - {Log.i(TAG, Disconnected from $deviceName)isConnected falsewriteCharacteristic nullnotifyCharacteristicUuid nullgatt.close()bluetoothGatt nullmainHandler.post { stateListener?.invoke(thisBleDevice, 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(thisBleDevice, 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.uuidenableNotification(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_VALUEgatt.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_VALUEgatt.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) {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) }倚凳挠蚀