Android BLE开发小记

自己封装的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: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?) {
          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 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字节, 对于超过该长度的数据,有两种方式进行处理:

  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. 对数据进行分包操作,添加控制信息


    Android BLE开发小记_第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? = 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()
        }
    }
}
  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 含义,以及协议帧结构

你可能感兴趣的:(Android BLE开发小记)