当前位置: 首页 > news >正文

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.1Service(服务)
  • 表示一类功能或数据集合。
  • 每个 Service 包含一个或多个 Characteristic。

Service 的组成:

  • UUID(Universally Unique Identifier):唯一标识符,用来区分不同服务。

  • 标准服务使用16位 UUID(由 Bluetooth SIG 定义)

  • 自定义服务使用128位 UUID,避免冲突。

  • Handle(句柄):内部索引号,用于快速定位。

  • 包含关系:一个服务可以“包含”另一个服务(较少见)。

1.2Characteristic(特征值)

这是 GATT 中最核心的数据单元。

  • 特征值代表一个具体的数据项,比如“当前心率”、“开关状态”、“温度值”。
  • 每个特征值属于某个服务。
  • 包括三部分:
  1. Value(值):实际的数据内容(如 byte 数组)。
  2. Properties(属性):说明该特征值支持哪些操作。
  3. Descriptors(描述符,可选):对值的补充说明(如单位、用户描述)。

特征值的 Properties

这些属性决定了客户端能对该特征值做什么操作:

属性功能对应操作
Read可读Client 可读取其值
Write可写Client 可写入新值
Notify通知Server 主动向 Client 发送更新(无需回复)
Indicate指示类似 Notify,但要求 Client 回 ACK(确认收到)
Broadcast广播向所有监听设备发送(不常用)
Write Without Response无响应写入快速写入,不等待确认(适合高频数据)
1.3Descriptor(描述符)

是对特征值的元数据说明,是可选组件。

三、权限适配

在进行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

CCCDClientCharacteristicConfigurationDescriptor):

特殊的描述符,控制某个特征值是否通知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.注意点:

  1. 链路连接成功是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) }
http://www.jsqmd.com/news/576362/

相关文章:

  • Win10自带应用太多?3分钟教你用PowerShell精准卸载(附常用应用命令大全)
  • 四川区域专业混凝土仿树皮栏杆优质厂家推荐 - 优质品牌商家
  • Qt QML 模块化进阶:qmldir 配置的实战避坑指南
  • QMCFLAC2MP3终极指南:一键解锁QQ音乐格式限制的完整解决方案
  • 2026 年电动观光车品牌价值榜行业深度报告 - 深度智识库
  • seo软文标题怎么写
  • CSS 嵌套的最佳实践:编写优雅的样式代码
  • 智能客服VS语音转写:不同场景下语音识别评估指标的选择指南
  • 2026年张掖艺考生文化课冲刺指南:五大集训品牌深度解析 - 2026年企业推荐榜
  • YOLO26镜像小白教程:5分钟搭建训练环境,轻松上手AI检测
  • 手把手排查 DeepSpeed CPUAdam 报错:从 AttributeError 到成功编译 Op 的完整日志分析
  • 2026天津新车月供避坑清单:3个硬指标必看 - 精选优质企业推荐榜
  • 如何用AI招聘系统,让AI主动去找人才?
  • 2026年洗涤设备厂家推荐:工业洗涤设备/布草洗涤设备厂家/洗涤设备价格/洗脱一体机/洗衣房设备厂家/选择指南 - 优质品牌商家
  • 从数据到诊断:深度学习驱动下的多模态抑郁症识别技术全景
  • Pixel Couplet Gen部署教程:Docker Multi-stage构建最小化镜像(<180MB)
  • 告别繁琐配置:用快马AI一键生成企业级gstack项目脚手架,效率提升300%
  • 如何在不支持的设备上安装Windows 11:绕过硬件限制的实用指南
  • 2026天津捷途汽车选型指南:3个硬指标定高配低配 - 精选优质企业推荐榜
  • 如何快速打造现代化Windows提示界面:ModernFlyouts终极指南
  • 快速原型:用快马一键生成win11右键菜单传统样式恢复工具
  • 暗黑破坏神2存档编辑器:3步解决角色培养与装备管理难题
  • 从零实现相机标定:OpenCV实战内外参数与畸变矫正
  • 从智能音箱到TWS耳机:拆解INMP441如何成为消费电子产品的“隐形功臣”
  • 告别系统臃肿:Win11Debloat三步配置流程让Windows运行效率提升51%
  • 立车采购避坑指南:三大专业平台,帮你选机更省心更靠谱 - 品牌推荐大师
  • 2026什么牌子排插质量好?安全与实用性兼具的选择 - 品牌排行榜
  • 暗黑破坏神2存档编辑器:解密游戏数据,重塑角色命运
  • 告别单调闪烁!用GD32F303的TIMER高级功能玩转PWM:实现S形曲线呼吸灯与多灯同步效果
  • 告别环境依赖!用PyInstaller打包你的PyTorch模型为独立EXE(含.pth权重文件)