自己封装的BLE库(5.0以上)

这里不记录具体代码规则,后面会给出参考文章,别人已经写很详细了,我就单纯记录下踩过的坑吧;

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:00WifiInfo.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?) {          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设备往其他设备发送消息时,也需要添加该属性:

  1. Android和iOS使用同一套BLE协议,因此可以通讯,如果是wifi direct的话,就不行了;
  2. 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 falseval headPackage = ByteArray(20)relayChar.value = headPackage        relayChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSEval result = gatt.writeCharacteristic(relayChar)

2.5 发送超过20字节数据

扩展阅读
BLE默认单次传输长度为20字节, 对于超过该长度的数据,有两种方式进行处理:

  1. 修改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();        }    }}
  1. 对数据进行分包操作,添加控制信息


    蓝牙数据分包.png

分为三部分,每个分包固定20字节:
a. head包,包含一些控制信息,如传送的数据长度,用于整合数据包
b. 用户要传送的数据内容(可加密);
c. tail包,所有数据发送完成后,发送一个结束信息(主要是避免head包发送失败时,接收方一直在等待发送结束,当然,若是tail包也发送失败,则需要通过接收超时机制来控制)
P.S. 跟iOS的同学交流后发现,iOS设备间单次最大也只是能发送512字节,因此应该也有分包的需求;

2.6 分包发送时间间隔过长的问题

stack overflow
连续通过characteristic写入数据时,相邻分包之间需要间隔一下,之前测试发现100ms失败率比较大,200ms就比较ok,但是也有一定概率失败,而且,单包20字节
,我要传输的数据基本都要400字节左右,总耗时(包括连接等)就可能达到5s以上,感觉时间还是太长,两种方式来避免:

  1. 修改 requestConnectionPriority() 值为 BluetoothGatt.CONNECTION_PRIORITY_HIGH
    这样设定后,分包之间设置为20ms就没再发现有出问题过(至少我手头的机型没出错过)
private var mGattCallback: BluetoothGattCallback? = nullmGattCallback = 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()        }    }}
  1. 添加错误重传机制,重传时间间隔增加
    发送分包时不可避免可能出错,若默认分包间隔为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 来查看:

  1. 查看hci日志文件路径
// 我使用nexus 6p 7.1.1系统,配置文件位于如下位置:adb shell cat /etc/bluetooth/bt_stack.conf// 文件中有一条配置信息,指示了log文件所在路径BtSnoopFileName=/sdcard/btsnoop_hci.log
  1. 抓取/导出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. 参考资料

  1. BLE 官方文档
  2. android ble常见问题收集
  3. BLE开发的各种坑
  4. ble address动态变化
  5. wireshark bluetooth简要描述
  6. Debugging Bluetooth With An Android App
    介绍了款测试软件,使用了,觉得不错...
  7. Android BLE中传输数据的最大长度怎么破
    看完这篇才知道为啥单个分包20字节,Android传iOS单次最多可用512字节....,注意:需要在设备连接成功后再来设置,最大512,但是即使设置成功也没法直接发送,需要在回调 onMtuChanged() 显示成功后,再写数据即可;
  8. Android BLE MTU调整
  9. 低功耗蓝牙介绍
    介绍了hci日志中的 host / controller 含义,以及协议帧结构

更多相关文章

  1. Android的HTTP基础与之使用HttpClient和HttpURLConnection
  2. Android中JSON解析
  3. 转:基于 Android(安卓)NDK 的学习之旅-----JNI 数据类型
  4. android 问题汇总系列之七
  5. android 数据存储之 SharedPreference
  6. Android:浅谈 mvp-clean 架构
  7. 【Android】实现登录、注册、数据库操作(极简洁)
  8. Android学习路线(二十七)键值对(SharedPreferences)存储
  9. android设备与蓝牙模块之间交互(蓝牙命令,收发)的两种方式,附DEMO下

随机推荐

  1. 可以显示九天天气情况的天气预报哦-LINUX
  2. Android使用KSOAP2调用WebService及正确
  3. android 弹出Dialog的时候播放声音!
  4. [转]五大布局对象---FrameLayout,LinearL
  5. [置顶] android 耳机按钮深层理解
  6. [android] Proguard代码混淆器如何排除指
  7. Android培训班(45)
  8. Android引入第三方jar包的方法
  9. Android学习10-----Android组件通信 (8)
  10. 桌面便签程序的实现详解和源码 (上)