Android低功耗蓝牙开发总结

基础使用

权限申请

蓝牙权限在各个版本中略有不同

  • Android 12 及以上版本,如果不需要通过蓝牙来推断位置的话,蓝牙扫描不需要开启位置权
  • Android 11 及以下版本,蓝牙扫描必须开启位置权限
  • Android 9 及以下版本,蓝牙扫描可开启粗略位置权限
xml 复制代码
<!-- Android 12 及以上版本 -->
<!-- 如果明确不需要蓝牙推断位置的话,可以通过标记 usesPermissionFlags="neverForLocation" --> 
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
  android:usesPermissionFlags="neverForLocation"
  tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

<!-- Android 11 及以下版本 -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>

<!-- Android 9 及以下版本 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28"/>

开启扫描/停止扫描

kotlin 复制代码
//获取蓝牙适配器
val bleAdapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter

//监听返回数据
private val bleScanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult?) {
        if (result != null){
            Log.e("bleLog", "startScanResult = $result")
        }
    }
}

/**
 * 开启扫描
 */
bleAdapter.bluetoothLeScanner.startScan(bleScanCallback)

/**
 * 结束扫描
 */
bleAdapter.bluetoothLeScanner.stopScan(bleScanCallback)

开始连接/断开连接

kotlin 复制代码
private var mBleGatt : BluetoothGatt? = null

//连接过程与数据接收回调
private val bleGattCallback = object : BluetoothGattCallback() {

  //连接状态变更
  override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
    if (newState == BluetoothProfile.STATE_CONNECTED){
      //已连接
      //发现服务
      mBleGatt?.discoverServices()
    }else if (newState == BluetoothProfile.STATE_DISCONNECTED){
      //已断开连接
    }
  }

  //发现服务回调
  override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
    // 调用 mBleGatt?.discoverServices() 时触发该回调
    if (status != BluetoothGatt.GATT_SUCCESS){
      //失败
      return
    }
    //获取指定GATT服务,UUID 由远程设备提供
    val bleGattService = mBleGatt?.getService(UUID.fromString("8888888"))
    //获取指定GATT特征,UUID 由远程设备提供
    val bleGattCharacteristic = bleGattService?.getCharacteristic(UUID.fromString("777777"))
    //启用特征通知,如果远程设备修改了特征,则会触发 onCharacteristicChange() 回调
    mBleGatt?.setCharacteristicNotification(bleGattCharacteristic, true)
    //启用客户端特征配置【固定写法】
    val bleGattDescriptor = bleGattCharacteristic?.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
    bleGattDescriptor?.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
    mBleGatt?.writeDescriptor(bleGattDescriptor)
  }

  //启用客户端特征配置结果回调
  override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) {
    if (status == BluetoothGatt.GATT_SUCCESS ){
      //此时蓝牙设备连接才算真正连接成功,即具备读写数据的能力
    }
  }

  //App修改特征回调,即 App 给设备发送数据结果回调
  override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int
                                          ) {
    if (status == BluetoothGatt.GATT_SUCCESS){
      //数据写入完成
      // 调用 characteristic?.value 得到的 ByteArray 与 发送数据一样
    }
  }

  //远程设备修改特征描述回调,即设备给 App 发送数据
  override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?
      ) {
    //调用 characteristic?.value 获取远程设备发送过来的数据
  }
}

/**
 * 开始连接
 * @param deviceMac 设备Mac地址
 */
val bleDevice = bleAdapter.getRemoteDevice(deviceMac)
mBleGatt = bleDevice.connectGatt(context, false, bleGattCallback, BluetoothDevice.TRANSPORT_LE)

/**
 * 断开连接
 */
mBleGatt?.disconnect()
mBleGatt?.close()

写入数据

kotlin 复制代码
mBleGattCharacteristic?.value = data
mBleGatt?.writeCharacteristic(mBleGattCharacteristic)

完整链路

总结

记住一个核心:蓝牙传输非常不稳定,指不定啥时候就没响应或丢包了。

连接过程

用户体验

  • Android 12 以下版本蓝牙扫描需要开启定位+授权才能使用,所以在扫描前要申请蓝牙&定位权限+判断是否开启蓝牙&定位。
  • 使用过程中,用户可能误操作关闭蓝牙,所以要监听蓝牙开关状态。
  • 蓝牙扫描添加超时机制,超时自动停止扫描。
  • 如果用列表按照信号强度展示扫描结果,建议扫描结束后再让用户选择设备,防止列表频繁跳动,导致用户误选。
  • 关于蓝牙的UI界面或操作,都需要判断当前蓝牙是否已连接。

注意点

  • 连接过程会有很多中间过程(触发连接 -> 连接回调成功后 -> 发现服务 -> ...),当获取为 null 或者返回失败时,要做异常返回,防止进度卡死。
  • 同上,连接中间过程较多,防止远端设备偶现无响应,在连接过程中设置超时机制,超时判定连接失败。
  • 当存在多个 GATT 特征时,可能需要调用多次 setCharacteristicNotification() + writeDescriptor(),注意此操作不能连续调用,正确姿势:gatt1 调用完成,待 onDescriptorWrite() 回调后,gatt2 再调用。

数据收发过程

背景:我手里的远端设备是一款实时操作系统的智能穿戴设备。该设备有一个特点:只能处理一条指令,处理完成后等待下一条,如果同时来多条,则只能处理第一条。

注意点

  • 因为远端设备只能处理单条指令,所以需要维护一个优先级队列
  • 蓝牙传输有最大传输单元限制(MTU),默认最大 23 个字节,可用的只有 20 个字节,[ 23 byte(ATT) =1 byte(Opcode) + 2 byte(Handler) + 20 byte(BATT) ],所以在发送指令时要做分包处理。
  • MTU 可通过调用 requestMtu() 调整大小,具体调整多大需和远端设备协定,调用后会回调 gattCallback#onMtuChanged(),注意:发现服务的调用要在该回调中,不能在连接状态回调中。
  • 单一指令发送和回包,需要加超时机制。即调用发送指令时开始超时倒计时,当触发 onCharacteristicChanged() 时并判断为指令回包,则移除倒计时。如果 onCharacteristicWrite() 返回失败或超时未回包,则移除倒计时并返回失败。
  • 单一指令发送并伴随多条回包,需要加 watchDog 机制。即调用发送指令时开始"养狗",当有远端设备回包时"喂狗",回包全部完成时"杀狗",如果 onCharacteristicWrite() 返回失败或到时间没有"喂狗",则"杀狗"并返回失败。

可能用到的知识

进制转换

Android Studio 打印日志或断点时,会自动将 16 进制 转成 10 进制进行显示。

十进制 与 16进制
十进制 -> 16进制:

十进制数 除以 16 取余,然后从低往上输出。例如:1758 = 0x6DE

16进制转 -> 十进制:

位数指向的数 * 16^位数 相加之和。例如 0x2A7F = 10879

十进制 与 二进制
十进制 -> 二进制:

记住常用数转化:例如, 45 = (32 + 8 + 4 + 1) = 101101

十进制 二进制
1 (2^0) 01
2 (2^1) 10
4 (2^2) 100
8 (2^3) 1000
16 (2^4) 10000
32 (2^5) 100000
64 (2^6) 1000000
二进制 -> 十进制:

位数指向的数 * 2^位数 相加之和。例如 10010 = 18

16进制 与 二进制
16进制 -> 二进制:

按每位数单独转二进制。例如: 0x6DA2 = 110110110100010

二进制 -> 16进制:

每四位一组,每组转 16 进制,然后拼接。例如: 101010110 = 0x156

位运算
& (与)

都为 1 时才是1

|(或)

**只要有 1 **时就是 1

^ (异或)

**只有一个 1 **时才是 1

~ (取反)

1 变 0, 0 变 1

>> (右移)

除以2^右移位数。例如: 75 >> 3 = 9

<< (左移)

乘以 2^ 左移位数。例如: 75 << 3 = 600

推荐阅读

Android 12 中的新蓝牙权限
蓝牙概览 | Connectivity | Android Developers
蓝牙智能设备数据采集平台化方案 | 京东云技术团队 - 掘金
BLE低功耗蓝牙技术详解
Android蓝牙通信机制详解 - 掘金


Hi,我是"青杉",您可以通过如下方式关注我:

相关推荐
拭心11 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王13 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡13 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道14 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库14 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道15 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe15 小时前
Android Hook - 动态加载so库
android
居居飒16 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He19 小时前
桌面列表小部件不能点击的问题分析
android
jiang_bluetooth19 小时前
GFPS扩展技术原理(七)-音频切换消息流
蓝牙·gfps·fast pair·谷歌快速连接