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,我是"青杉",您可以通过如下方式关注我:

相关推荐
雨白8 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
漫步企鹅8 小时前
【蓝牙】Linux Qt4查看已经配对的蓝牙信息
linux·qt·蓝牙·配对
kk爱闹9 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空11 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭12 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日12 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安13 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑13 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟17 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡18 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio