找回密码
 立即注册
首页 业界区 业界 Android | Kotlin实现Ble低功耗蓝牙设备连接

Android | Kotlin实现Ble低功耗蓝牙设备连接

崔瑜然 2026-1-22 17:35:02
一、引言

本文记录在之前进行的仪表类多ble设备采集项目开发中,使用到的低功耗蓝牙连接技术的总结。
二、概念

(一) 低功耗蓝牙介绍

低功耗蓝牙是4.0版本起支持的蓝牙协议,主要特点是低功耗,传输速度快,传输数据量小的特点。
工作在2.4GHz 频段,使用调频扩频实现抗干扰。
支持广播+点对点快速连接。
(二) GATT (Generic Attribute Profile通用属性配置文件)

Gatt是立在 ATT(Attribute Protocol,属性协议) 之上的用于结构化数据交换的标准方式。
Gatt架构中,有明确的角色划分,分别是服务端和客户端。
Gatt Server(服务器)数据的提供者,提供服务和特征值。
Gatt Client(客户端) 访问服务器数据的设备,发起请求
1. GATT 层次结构:层级模型

GATT 使用一种树状结构来组织数据,从大到小依次为:
  1. Device(设备)
  2. └─── Service(服务)
  3.      └─── Characteristic(特征值)
  4.           ├── Value(值)
  5.           └─── 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. 权限声明清单
  1. [/code][size=6]四、具体实现[/size]
  2. [size=5](一) GATT 工作流程[/size]
  3. [size=4]1. [b]CCCD[/b][/size]
  4. [b]CCCD[/b]([b]C[/b]lient [b]C[/b]haracteristic [b]C[/b]onfiguration [b]D[/b]escriptor):
  5. 特殊的描述符,控制某个特征值是否通知Notify,即通知开关。
  6. [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。
  7. [size=4]2. [b]BluetoothAdapter[/b][/size]
  8. 注:从Android 4.3(API18)开始,使用BluetoothManager来获取adapter。
  9. 核心功能:
  10. [list]
  11. [*]控制蓝牙开关(Android 12后需要手动确认)
  12. [*]设备扫描(经典蓝牙和ble的api不同,ble需要通过其子对象 BluetoothLeScanner扫描)
  13. [*]获取蓝牙信息(name,mac)
  14. [*]通过地址获取设备引用(getRemoteDevice(address))
  15. [*]通过广播监听状态变化
  16. [/list][size=4]3. [b]BluetoothGatt[/b][/size]
  17. BluetoothGatt 是Android与ble外设通信的桥梁和控制中心,是ble客户端的抽象,并不是设备本身,而是与设备建立连接后获取的一个通信句柄。
  18. 核心功能:
  19. [list]
  20. [*]创建连接(connectGatt,会返回BluetoothGatt实例)
  21. [*]发现服务(discoverServices,发起服务发现流程)
  22. [*]读写特征值(readCharacteristic/writeCharacteristic)
  23. [*]开启本地通知监听(setCharacteristicNotification)
  24. [*]写描述符(如CCCD)
  25. [*]请求MTU扩展
  26. [*]设置连接优先级
  27. [*]断开连接(disconnect)
  28. [*]释放资源(close)
  29. [/list][size=4]4. [b]BluetoothGattCallback[/b][/size]
  30. 调用BluetoothGatt connectGatt(android.content.Context context, boolean autoConnect, android.bluetooth.BluetoothGattCallback callback, int transport)
  31. 需要传入一个关键参数,BluetoothGattCallback是一个抽象类,在进行具体实现时,需要继承这个抽象类实现所有的抽象回调方法,如果把BluetoothGatt比作电话,BluetoothGattCallback更像是一个听筒。
  32. 注意,每个设备和Gatt只能持有自己的gattCallBack
  33. BluetoothGattCallback包含如下重要的回调方法:
  34. [list]
  35. [*]onConnectionStateChange        连接状态回调
  36. [*]onServicesDiscovered                服务发现回调
  37. [*]onCharacteristicRead                特征值读回调
  38. [*]onCharacteristicWrite                特征值写入回调
  39. [*]onCharacteristicChanged        特征值变化通知回调(对应设置了CCCD的通知)
  40. [*]onDescriptorWrite                        特征值描述写入回调
  41. [*]onMtuChanged                        Mtu变化回调
  42. [/list][size=4]5. [b]工作流程:[/b][/size]
  43. App 启动 BLE 扫描 → 发现目标设备 → 自动连接 → 建立通信通道 → 准备好读写操作。
  44. 这里注意是采用特征值通知的方式读取数据,还有一种方式是上位机直接进行特征值读取。
  45. [align=center][img]https://cdn.nlark.com/yuque/__mermaid_v3/431f390567d85cd0bd3f71acfd618948.svg[/img][/align]
  46. [size=4]6. [b]注意点:[/b][/size]
  47. [list=1]
  48. [*]链路连接成功是onConnectionStateChange回调触发,但并不能直接进行通讯,真正的[b]连接成功[/b]定义在onServicesDiscovered成功之后;
  49. [/list]
  50. [list]
  51. [*]必须在onConnectionStateChange之中手动调用discoverServices
  52. [*]app主动获取低功耗蓝牙模块制定特征值的数据需要记录对应模块的READ UUID,通过调用readCharacteristic来进行对应特征值的读取,适用于app端主动获取数据的场景。
  53. [/list][size=5](二) 代码实现[/size]
  54. [code]/**
  55. * BLE 设备封装类 —— 实现连接、通信与事件分发
  56. */
  57. class BleDevice(
  58.     private val deviceName: String,
  59.     private val deviceAddress: String,
  60.     private var nativeDevice: BluetoothDevice? = null
  61. ) {
  62.     companion object {
  63.         private const val TAG = "BleDevice"
  64.         // 服务与特征值 UUID(请根据实际设备修改)
  65.         private val SERVICE_UUID = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb")
  66.         private val NOTIFY_CHARACTERISTIC_UUID = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb")
  67.         private val WRITE_CHARACTERISTIC_UUID = UUID.fromString("0000fff2-0000-1000-8000-00805f9b34fb")
  68.         // CCCD UUID(标准定义)
  69.         private val CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
  70.     }
  71.     // 当前连接状态
  72.     @Volatile
  73.     var isConnected: Boolean = false
  74.         private set
  75.     // 回调接口
  76.     var stateListener: ((device: BleDevice, state: ConnectionState) -> Unit)? = null
  77.     var dataListener: ((device: BleDevice, data: ByteArray) -> Unit)? = null
  78.     private var bluetoothGatt: BluetoothGatt? = null
  79.     private var writeCharacteristic: BluetoothGattCharacteristic? = null
  80.     private var notifyCharacteristicUuid: UUID? = null
  81.     // 主线程 Handler,用于回调 UI
  82.     private val mainHandler = Handler(Looper.getMainLooper())
  83.     /**
  84.      * 连接设备
  85.      */
  86.     fun connect(context: Context) {
  87.         if (isConnected) {
  88.             Log.w(TAG, "Already connected to $deviceName")
  89.             return
  90.         }
  91.         // 清理旧连接
  92.         closeOldConnection()
  93.         realConnect(context.applicationContext)
  94.     }
  95.     private fun closeOldConnection() {
  96.         if (bluetoothGatt != null) {
  97.             Log.d(TAG, "Closing previous GATT instance to avoid errors...")
  98.             try {
  99.                 bluetoothGatt?.close()
  100.             } catch (e: Exception) {
  101.                 Log.e(TAG, "Error closing old GATT", e)
  102.             }
  103.             bluetoothGatt = null
  104.             // 延迟重连,避免频繁操作导致 Status 133
  105.             mainHandler.postDelayed(this::realConnectWithAppContext, 200)
  106.         }
  107.     }
  108.     private val realConnectWithAppContext: () -> Unit = {
  109.         // 在延时任务中重新获取 context(需外部传入)
  110.     }
  111.     private fun realConnect(context: Context) {
  112.         val adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
  113.             ?: run {
  114.                 stateListener?.invoke(this, ConnectionState.Error("Bluetooth not available"))
  115.                 return
  116.             }
  117.         if (!adapter.isEnabled) {
  118.             stateListener?.invoke(this, ConnectionState.Error("Bluetooth is disabled"))
  119.             return
  120.         }
  121.         val device = nativeDevice ?: run {
  122.             val d = adapter.getRemoteDevice(deviceAddress)
  123.             nativeDevice = d
  124.             d
  125.         }
  126.         try {
  127.             bluetoothGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  128.                 device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
  129.             } else {
  130.                 device.connectGatt(context, false, gattCallback)
  131.             }
  132.         } catch (e: IllegalArgumentException) {
  133.             Log.e(TAG, "Invalid device address: $deviceAddress", e)
  134.             stateListener?.invoke(this, ConnectionState.Error("Invalid address"))
  135.         }
  136.     }
  137.     /**
  138.      * 断开连接
  139.      */
  140.     fun disconnect() {
  141.         bluetoothGatt?.disconnect()
  142.     }
  143.     /**
  144.      * 发送数据
  145.      */
  146.     fun sendData(data: ByteArray): Boolean {
  147.         val char = writeCharacteristic ?: return false
  148.         return try {
  149.             char.value = data
  150.             char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
  151.             bluetoothGatt?.writeCharacteristic(char) == true
  152.         } catch (e: Exception) {
  153.             Log.e(TAG, "Failed to write characteristic", e)
  154.             false
  155.         }
  156.     }
  157.     /**
  158.      * 主动读取特征值(可选)
  159.      */
  160.     fun readData() {
  161.         val char = bluetoothGatt?.getService(SERVICE_UUID)
  162.             ?.getCharacteristic(NOTIFY_CHARACTERISTIC_UUID)
  163.             ?: return
  164.         bluetoothGatt?.readCharacteristic(char)
  165.     }
  166.     // MARK: - GATT Callback
  167.     private val gattCallback = object : BluetoothGattCallback() {
  168.         override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
  169.             when (newState) {
  170.                 BluetoothProfile.STATE_CONNECTED -> {
  171.                     Log.i(TAG, "Connected to $deviceName")
  172.                     isConnected = true
  173.                     mainHandler.post { stateListener?.invoke(this@BleDevice, ConnectionState.Connected) }
  174.                     // 请求高优先级连接参数
  175.                     gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
  176.                     // 开始服务发现
  177.                     gatt.discoverServices()
  178.                 }
  179.                 BluetoothProfile.STATE_DISCONNECTED -> {
  180.                     Log.i(TAG, "Disconnected from $deviceName")
  181.                     isConnected = false
  182.                     writeCharacteristic = null
  183.                     notifyCharacteristicUuid = null
  184.                     gatt.close()
  185.                     bluetoothGatt = null
  186.                     mainHandler.post { stateListener?.invoke(this@BleDevice, ConnectionState.Disconnected) }
  187.                 }
  188.             }
  189.         }
  190.         override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
  191.             if (status != BluetoothGatt.GATT_SUCCESS) {
  192.                 Log.e(TAG, "Service discovery failed: $status")
  193.                 mainHandler.post {
  194.                     stateListener?.invoke(this@BleDevice, ConnectionState.Error("Service discovery failed"))
  195.                 }
  196.                 return
  197.             }
  198.             val targetService = gatt.getService(SERVICE_UUID)
  199.             if (targetService == null) {
  200.                 Log.e(TAG, "Target service not found")
  201.                 printAllServices(gatt.services)
  202.                 return
  203.             }
  204.             for (char in targetService.characteristics) {
  205.                 when (char.uuid) {
  206.                     WRITE_CHARACTERISTIC_UUID -> {
  207.                         writeCharacteristic = char.apply {
  208.                             writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
  209.                         }
  210.                         Log.d(TAG, "Write characteristic found: ${char.uuid}")
  211.                     }
  212.                     NOTIFY_CHARACTERISTIC_UUID -> {
  213.                         Log.d(TAG, "Notify characteristic found: ${char.uuid}")
  214.                         notifyCharacteristicUuid = char.uuid
  215.                         enableNotification(gatt, char)
  216.                     }
  217.                 }
  218.             }
  219.         }
  220.         // 接收被动通知的数据(服务器主动推送)
  221.         override fun onCharacteristicChanged(
  222.             gatt: BluetoothGatt,
  223.             characteristic: BluetoothGattCharacteristic
  224.         ) {
  225.             handleReceivedData(characteristic.value ?: byteArrayOf())
  226.         }
  227.         // 主动读取返回的数据
  228.         override fun onCharacteristicRead(
  229.             gatt: BluetoothGatt,
  230.             characteristic: BluetoothGattCharacteristic,
  231.             status: Int
  232.         ) {
  233.             if (status == BluetoothGatt.GATT_SUCCESS) {
  234.                 Log.d(TAG, "Read success: ${characteristic.uuid}")
  235.                 handleReceivedData(characteristic.value ?: byteArrayOf())
  236.             }
  237.         }
  238.         override fun onDescriptorWrite(
  239.             gatt: BluetoothGatt,
  240.             descriptor: BluetoothGattDescriptor,
  241.             status: Int
  242.         ) {
  243.             if (status == BluetoothGatt.GATT_SUCCESS) {
  244.                 Log.d(TAG, "CCCD enabled: ${descriptor.characteristic.uuid}")
  245.             } else {
  246.                 Log.e(TAG, "Failed to write descriptor: $status")
  247.             }
  248.         }
  249.     }
  250.     private fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
  251.         if (!gatt.setCharacteristicNotification(characteristic, true)) {
  252.             Log.e(TAG, "Failed to set characteristic notification")
  253.             return
  254.         }
  255.         val cccd = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG)
  256.         if (cccd != null) {
  257.             cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
  258.             gatt.writeDescriptor(cccd)
  259.         } else {
  260.             Log.w(TAG, "CCCD not found for characteristic ${characteristic.uuid}. Trying fallback...")
  261.             // 尝试写入第一个可用描述符(部分设备非标准实现)
  262.             characteristic.descriptors.firstOrNull()?.let { desc ->
  263.                 desc.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
  264.                 gatt.writeDescriptor(desc)
  265.             }
  266.         }
  267.     }
  268.     private fun handleReceivedData(value: ByteArray) {
  269.         Log.d(TAG, "Received data: ${value.toHexString()}")
  270.         mainHandler.post { dataListener?.invoke(this, value) }
  271.     }
  272.     private fun printAllServices(services: List<BluetoothGattService>) {
  273.         Log.d(TAG, "Discovered services:")
  274.         services.forEach { service ->
  275.             Log.d(TAG, "  Service: ${service.uuid}")
  276.             service.characteristics.forEach { char ->
  277.                 Log.d(TAG, "    Char: ${char.uuid} | Props: ${char.properties}")
  278.             }
  279.         }
  280.     }
  281. }
  282. // MARK: - 状态枚举
  283. sealed class ConnectionState {
  284.     object Connected : ConnectionState()
  285.     object Disconnected : ConnectionState()
  286.     data class Error(val message: String) : ConnectionState()
  287. }
  288. // MARK: - 工具扩展
  289. private fun ByteArray.toHexString(): String =
  290.     this.joinToString(separator = " ") { "%02X".format(it) }
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册