这里不记录具体代码规则,后面会给出参考文章,别人已经写很详细了,我就单纯记录下踩过的坑吧;
1. 版本支持
Android 从 4.3(API Level 18) 开始支持低功耗蓝牙(Bluetooth low energy),但是只支持作为中心设备(Central)模式,这就意味着 Android 设备只能主动扫描和链接其他外围设备(Peripheral),从 Android 5.0(API Level 21) 开始两种模式都支持。
P.S. 不过也不是5.0以上就全部都支持,之前测试到魅族M2貌似就开不起peripheral模式,毕竟硬件相关,很难保证,我同事之前开发时候甚至碰到过某些设备会固定少发一个字节,也是坑啊...
2. 踩过的坑
2.1 开启peripheral模式
之前以为开启了手机蓝牙和gps功能, 手机就能被central设备搜索到, 那是经典蓝牙, 要想启用BLE功能并作为peripheral从机,需要使用 BluetoothLeAdvertiser
开启广播模式:
P.S. BLE链接不会弹出连接请求,比经典蓝牙方便,毕竟不打扰用户,另外,查到的资料说,BLE central大概最多同时链接7台设备左右;
/**
* 开启广播模式,用于本机被其他central设备搜索到
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
fun startAdvertising() {
if (isBluetoothEnable()
&& !isAdvertising
&& isSupportAdvertisement
&& mBluetoothLeAdvertiser != null
&& mGattServer != null) {
val success = mGattServerCallBack.setupServices(mGattServer)
Logger.d("startAdvertising result = $success ", TAG)
if (success) {
mBluetoothLeAdvertiser?.startAdvertising(createAdSettings(true, 0), createAdData(), mAdCallback)
}
} else {
Logger.d("startAdvertising fail", TAG)
}
}
2.2 蓝牙地址动态变化
参考这篇
Google在Android6.0上修改了获取设备标识信息功能:
// 以下方法固定返回: 02:00:00:00:00:00
WifiInfo.getMacAddress()
BluetoothAdapter.getAddress()
坑爹的是,假设central设备扫描得到peripheral的蓝牙地址记为: A , 连接同一台peripheral设备时获取的蓝牙地址记为B, A跟B还不一致,又动态变化了,真是坑啊:
之所以会想要记录设备蓝牙地址,是想作为唯一标识符,在转传信息时,不要再回传到数据来源方, 比如 A 发送数据给 B, B再往其他设备转传时,就不需要回传给A了,但是地址动态变化的话,我就没辙了,有解决方案的话麻烦告知我一下;
// 低功耗蓝牙扫描回调
var mLeScanCallback: ScanCallback? = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
// Logger.d("scan successful $result")
// 这里通过ScanResult获取到的蓝牙地址A,跟通过手机系统设置页面查看得到的蓝牙地址是不同的,而且每次重新开启peripheral模式后,同一台手机的蓝牙地址就又变化了
//
// 另外,同一台设备会在短时间内被扫描到很多次,因此不是需要对设备进行过滤判断
addBleDevice(result)
}
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
super.onBatchScanResults(results)
results?.forEach { addBleDevice(it) }
}
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
if (ScanCallback.SCAN_FAILED_ALREADY_STARTED != errorCode) {
isScanningBle = false
}
Logger.d("scan failed errorCode = $errorCode")
}
}
2.3 自定义characteristic UUID
之前以为只要符合uuid模式: 00000000-0000-0000-0000-000000000000
(8-4-4-4-12)随便定义即可, 后来看了 这篇 才发现不是这样的,能自定义的只是其中一部分,有兴趣的可以去研究下 BLE文档;
0000????-0000-1000-8000-00805f9b34fb
????就表示4个可以自定义16进制数
2.4 跟iOS通讯时循环写入数据失败
我们是通过 Characteristic
来写入的, 它有个属性来指明发送时不需要响应: BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
, 而我在跟iOS交互时,貌似这个字段双方设定不一致,导致发送后一直没收到响应,然后iOS就一直重发;
因此,需要在作为peripheral模式时,添加的characteristic需要设置为: BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
;
另外,作为central设备往其他设备发送消息时,也需要添加该属性:
- Android和iOS使用同一套BLE协议,因此可以通讯,如果是wifi direct的话,就不行了;
- Android 4.3虽然也支持central模式,但是查到的文章有说在跟iOS参数交互时有问题,而我使用4.3来搜索其他Android设备也经常找不到,因此就直接不考虑了,从5.0开始;
/**
* 接收数据时,通过本类回调处理
*/
class GattServerCallBack : BluetoothGattServerCallback() {
companion object {
private val TAG = "GattServerCallBack"
}
private var mGattServer: BluetoothGattServer? = null
/**
* 初始化需要用来转传数据的 service/characteristic
* */
private val mRelayService by lazy {
val service = BluetoothGattService(UUID.fromString(BleConstant.RELAY_SERVICE_UUID), BluetoothGattService.SERVICE_TYPE_PRIMARY)
val characteristic = BluetoothGattCharacteristic(
UUID.fromString(BleConstant.RELAY_CHARACTERISTIC_UUID),
BluetoothGattCharacteristic.PROPERTY_READ
or BluetoothGattCharacteristic.PROPERTY_WRITE
or BluetoothGattCharacteristic.PROPERTY_NOTIFY
or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, // 这里设定不需要回应,也可选择需要响应模式
BluetoothGattCharacteristic.PERMISSION_READ
or BluetoothGattCharacteristic.PERMISSION_WRITE)// 可写模式,不同ble设备间通过本characteristic来传输数据
characteristic.setValue(BlePara.adCharacteristicValue)
val addCharacteristic = service.addCharacteristic(characteristic)
Logger.d("addCharacteristic result = $addCharacteristic", TAG)
service
}
/**
* 广播开始后,设置一个用于接收消息的service
* 后续有数据传入时,会触发 [org.lynxz.ble_lib.callbacks.GattServerCallBack.onCharacteristicWriteRequest]
* */
fun setupServices(gattServer: BluetoothGattServer?): Boolean {
if (gattServer == null) {
return false
}
// 设置一个GattService以及BluetoothGattCharacteristic
mGattServer = gattServer
val service = mGattServer?.getService(UUID.fromString(BleConstant.RELAY_SERVICE_UUID))
if (service == null) {
val addResult = mGattServer?.addService(mRelayService)
Logger.d(" -> 添加自定义service...result = $addResult", TAG)
} else {
Logger.d(" -> 添加自定义service... service已存在,不用重复添加", TAG)
}
return true
}
override fun onCharacteristicWriteRequest(device: BluetoothDevice?, requestId: Int, characteristic: BluetoothGattCharacteristic?, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray?) {
super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value)
// 按需发送响应
var responseResult = true
if (responseNeeded) responseResult = mGattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null) ?: false
Logger.d("responseNeeded = $responseNeeded ,send response result = $responseResult , receive data length = ${value?.size}")
}
}
// 作为central设备,通过characteristic发送数据时
val service = gatt.getService(UUID.fromString("*********")) ?: return false
val relayChar = service.getCharacteristic(UUID.fromString("*********")) ?: return false
val headPackage = ByteArray(20)
relayChar.value = headPackage
relayChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
val result = gatt.writeCharacteristic(relayChar)
2.5 发送超过20字节数据
扩展阅读
BLE默认单次传输长度为20字节, 对于超过该长度的数据,有两种方式进行处理:
- 修改MTU值(最大为512字节)
在跟iOS交互的时候,发现它一次性可以往Android发送512字节(Android使用默认设定),后来才发现Android设备间也可以重新指定该值,不过使用这种方式的话,我测试到有这种现象: mtu设置回调成功,central设备发送数据也成功,但peripheral设备却不能完整接收到,比如我设置512字节,但收到的可能只有140字节,因此我没有采用这种方式:
mGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
val device = gatt.device
Logger.d("onConnectionStateChange newState = $newState ${device.address}")
if (BluetoothGatt.STATE_CONNECTED == newState) {
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
Logger.d("设置mtu结果 : ${gatt.requestMtu(BlePara.mtu)}"
} else if (BluetoothGatt.STATE_DISCONNECTED == newState) {
gatt.close()
}
}
// mtu设置成功后才去搜索service/characteristic,然后才可以传输数据
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)
Logger.d(" mtu = $mtu $status")
if (status == BluetoothGatt.GATT_SUCCESS) {
gatt.discoverServices();
}
}
}
-
对数据进行分包操作,添加控制信息
分为三部分,每个分包固定20字节:
a. head包,包含一些控制信息,如传送的数据长度,用于整合数据包
b. 用户要传送的数据内容(可加密);
c. tail包,所有数据发送完成后,发送一个结束信息(主要是避免head包发送失败时,接收方一直在等待发送结束,当然,若是tail包也发送失败,则需要通过接收超时机制来控制)
P.S. 跟iOS的同学交流后发现,iOS设备间单次最大也只是能发送512字节,因此应该也有分包的需求;
2.6 分包发送时间间隔过长的问题
stack overflow
连续通过characteristic写入数据时,相邻分包之间需要间隔一下,之前测试发现100ms失败率比较大,200ms就比较ok,但是也有一定概率失败,而且,单包20字节
,我要传输的数据基本都要400字节左右,总耗时(包括连接等)就可能达到5s以上,感觉时间还是太长,两种方式来避免:
- 修改
requestConnectionPriority()
值为BluetoothGatt.CONNECTION_PRIORITY_HIGH
这样设定后,分包之间设置为20ms就没再发现有出问题过(至少我手头的机型没出错过)
private var mGattCallback: BluetoothGattCallback? = null
mGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
val device = gatt.device
Logger.d("onConnectionStateChange newState = $newState ${device.address}")
if (BluetoothGatt.STATE_CONNECTED == newState) {
Logger.d("onConnectionStateChange STATE_CONNECTED = $newState ,gatt == mGatt? = ${gatt == mGatt}")
// 发送大数据时设置如此,有人建议发送完成后要设置成默认的: CONNECTION_PRIORITY_BALANCED
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
// REFACTOR: 17/06/2017 可以设置mtu大小,若启用此方式,则请在onMtuChanged()回调成功后再搜索及发送数据,但Android之间测试发现接收方有些只能收到152个字节,暂时不考虑,后续研究
// Logger.d("设置mtu结果 : ${gatt.requestMtu(BlePara.mtu)}"
// 连接成功,开始搜索service
gatt.discoverServices()
} else if (BluetoothGatt.STATE_DISCONNECTED == newState) {
// gatt连接断开
Logger.d("onConnectionStateChange STATE_DISCONNECTED = $newState")
gatt.close()
}
}
}
- 添加错误重传机制,重传时间间隔增加
发送分包时不可避免可能出错,若默认分包间隔为20ms,发送失败后,可尝试重传一次,重传时的时间间隔略微设定大些,如200ms,这样仍能有效减小总发送时间;
var result = true // 发送数据是否成功
val delay = 20 // 分包之间的延时,单位:毫秒
try {
// 注意,这里需要延时一下,不然测试发现,基本上只能收到其中几帧的数据,失败的概率比较大
Thread.sleep(delay.toLong())
var i = 0
while (i < size) {
var to = i + 20
if (to >= size) {
to = size
}
val slice = Arrays.copyOfRange(encryptedContentBytes, i, to)
relayChar.value = slice
var sliceResult = gatt.writeCharacteristic(relayChar)
Logger.d("传送第 $i ~ $to 块数据的结果: $sliceResult", TAG)
// 发送失败时,尝试重传一次就好
if (!sliceResult) {
Thread.sleep(200)
sliceResult = gatt.writeCharacteristic(relayChar)
Logger.d(" =>重传第 $i ~ $to 块数据的结果: $sliceResult", TAG)
}
result = result and sliceResult
i = to
Thread.sleep(delay.toLong())
// 由于只重传一次, 因此如果某个数据分包重传失败,则不必要再传后续数据,直接返回失败
if (!result) {
break
}
}
} catch (e: Exception) {
e.printStackTrace()
result = false
}
2.7 蓝牙抓包,日志查看
之前跟iOS交互出错后,app层回调可看到的信息比较少, 查到的资料 又都说有某个控制参数出错, 没发现characteristic设置有问题前,就想着要抓包看看具体的参数交互, 未找到实时抓包的简单方法, 倒是可以通过Android手机的hcidump功能来获取日志,然后通过 wireshark 来查看:
- 查看hci日志文件路径
// 我使用nexus 6p 7.1.1系统,配置文件位于如下位置:
adb shell cat /etc/bluetooth/bt_stack.conf
// 文件中有一条配置信息,指示了log文件所在路径
BtSnoopFileName=/sdcard/btsnoop_hci.log
- 抓取/导出hci日志
// 先清除原先的日志
adb shell rm /sdcard/btsnoop_hci.log
// 通过手机系统打开日志功能: settings-developer options -- enable bluetooth hci snoop log
// 抓取结束后,导出log文件到pc上
adb pull /sdcard/btsnoop_hci.log
不过, 一开始做ble没经验,可以先下载些软件来测试下ble功能,这里推荐一个 nRF24L01 , 具体请参考 这篇文章, 好用, 搜索/连接/发送数据等功能一应俱全, 写完 peripheral 模式后,用它测试下,确认ok了,再来做central模式;
3. 参考资料
- BLE 官方文档
- android ble常见问题收集
- BLE开发的各种坑
- ble address动态变化
- wireshark bluetooth简要描述
-
Debugging Bluetooth With An Android App
介绍了款测试软件,使用了,觉得不错... -
Android BLE中传输数据的最大长度怎么破
看完这篇才知道为啥单个分包20字节,Android传iOS单次最多可用512字节....,注意:需要在设备连接成功后再来设置,最大512,但是即使设置成功也没法直接发送,需要在回调 onMtuChanged() 显示成功后,再写数据即可; - Android BLE MTU调整
-
低功耗蓝牙介绍
介绍了hci日志中的 host / controller 含义,以及协议帧结构