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 中最核心的数据单元。

  • 特征值代表一个具体的数据项,比如“当前心率”、“开关状态”、“温度值”。
  • 每个特征值属于某个服务。
  • 包括三部分:
  1. Value(值):实际的数据内容(如 byte 数组)。
  2. Properties(属性):说明该特征值支持哪些操作。
  3. 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

CCCDClient 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 特征值描述写入回调
  • onMtuChanged Mtu变化回调

5. 工作流程:

App 启动 BLE 扫描 → 发现目标设备 → 自动连接 → 建立通信通道 → 准备好读写操作。

这里注意是采用特征值通知的方式读取数据,还有一种方式是上位机直接进行特征值读取。

img

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) }
posted @ 2026-01-22 16:45  来一碗糖醋锦鲤  阅读(35)  评论(0)    收藏  举报