基础使用
权限申请
蓝牙权限在各个版本中略有不同
- Android 12 及以上版本,如果不需要通过蓝牙来推断位置的话,蓝牙扫描不需要开启位置权
- Android 11 及以下版本,蓝牙扫描必须开启位置权限
- Android 9 及以下版本,蓝牙扫描可开启粗略位置权限
<!-- 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"/>
开启扫描/停止扫描
//获取蓝牙适配器
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)
开始连接/断开连接
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()
写入数据
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,我是“青杉”,您可以通过如下方式关注我: