蓝牙无线技术是一种全球通用的短距离无线技术,具有耗电量低、成本低、安全性、稳定性、易用性等优点,尤其在物联网设备上的占有率非常高,因此我们有必要对蓝牙做深入的了解。本文从蓝牙和Android手机的一次通信过程为引,讲解Android蓝牙通信上的一些问题和原理,以及调用方法。如有描述不当的地方还请指出。
例子
假设Android手机A要给蓝牙设备B发送一条“hello”的消息,然后B会给A回一条“world”。我们应该怎么做呢?
过程
过程可以大致分为三步:
- 寻找设备
- 连接
- 通信
寻找设备
A要怎么找到B呢,一般是由设备B按照一定的周期广播数据包,然后A和B指定好协议,看广播数据里有没有协议约定的数据,有的话则说明找到了B。
广播的数据包分为四种
- ADV_IND
- ADV_DIRECT_IND
- ADV_NONCONN_IND
- ADV_SCAN_IND
一般发送的是ADV_IND包(可参考蓝牙协议分析(5)_BLE广播通信相关的技术分析。
Android设备通过系统提供的API开始接受广播数据,
//要先停止上一次的scan,不然无法启动新的scan
bluetoothAdapter.getBluetoothLeScanner().stopScan(bleCallback);
bluetoothAdapter.getBluetoothLeScanner().startScan(getFilters(), getSettings(), bleCallback);
然后在callback里获取广播数据
private ScanCallback bleCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, final ScanResult result) {
}
}
ScanResult就是当前接收到的广播里数据,在5.0以及5.0以上的api,会帮我们解析广播的数据,但是5.0以下的话,只是会把整个广播数据传回来。下面大致讲解下广播协议数据。
广播协议分析:
官方文档:
https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile
广播的数据,按 len 1字节,TYPE1字节, data (len -1)字节的顺序组依次组织,key的含义在上面的表格中已经给出。 下面举个栗子
假设下面是设备B的广播数据
02 01 06 05 03 F6 FE F5 FE 0E 09 5B 52 4F 41 44 42 49 54 5D 00 00 00 00 0D FF AA 00 66 EE 06 66 1F FF FF 25 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
解析数据
02 01 06 //长度2,type 1 表示设备功能, 6表示110 -> 普通发现模式,且不支持BR/EDR
05 03 F6 FE F5 FE //长度5,type 03,表示16位uuid列表, F6FE和F5FE两组uuid
0E 09 5B 52 4F 41 44 42 49 54 5D 00 00 00 00 长度 0E,即14, type 9,即设备名称,后面试试字符串转16进制
0D FF AA 00 66 EE 06 66 1F FF FF 25 04 00 , FF表示自定义数据,即蓝牙设备的自定义的数据
另外,设备的广播频率是可以自己定义的,从几ms到几百ms不等,广播的频率决定了手机发现设备的快慢,
有个软件叫"nRF Connect",可以很方便的观看蓝牙设备的广播速度。下载地址
软件截图:
上图的Adv. Interval就是广播的间隔,上文讲到的所有东西都可以在这个软件上观察到。
连接过程
蓝牙分为5种工作状态,
- 准备(standby):就绪状态,准备转变为其他状态
- 广播(advertising):向外发送数据的状态
- 监听扫描(Scanning): 扫描状态的时候,在接受到ADV_IND包是,会发送SCAN_REQ包,可以获得更多的信息。
- 发起连接(Initiating):发起连接状态,在ADV_IND或者ADV_DIRECT_IND之后,会发送CONNECT_REQ包,从而建立连接。
- 已连接(Connected):根据连接时约定的参数,发送CONNECT_EVENT,保持连接不断开。
具体工作流程如下:
- 可被连接的设备(Advertiser),按照一定的周期广播ADV_IND或者ADV_DIRECT_IND包(可参考“蓝牙协议分析(5)_BLE广播通信相关的技术分析”)。
- 主动连接的设备(Initiator),在收到广播包之后,会回应一个CONNECT_REQ请求,该请求携带了可决定后续“通信时序”的参数,例如双方在哪一个时间点、哪一个Physical Channel收发数据,等等。
- Initiator在发出CONNECT_REQ数据包之后,自动转变为Connection状态,成为Master角色(注意:这是“自动”的,不需要等待另一方的回应)。同样,Advertiser在收到CONNECT_REQ请求之后,也自动转变为Connection状态,成为Slave角色。
- 此后,双方按照CONNECT_REQ参数所给出的约定,定时到切换到某一个Physical Channel上,按照Master->Slave然后Slave->Master的顺序,收发数据,直至连接断开。
在Android手机中,连接的代码非常简单,只有一个api可以调用,如下:
private BluetoothGatt connectGattCompat(BluetoothGattCallback bluetoothGattCallback, BluetoothDevice device, boolean autoConnect) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return device.connectGatt(context, autoConnect, bluetoothGattCallback, TRANSPORT_LE);
} else {
return device.connectGatt(context, autoConnect, bluetoothGattCallback);
}
}
之后在bluetoothGattCallback的onConnectionChange的回调里,就可以知道连接的结果。
通信
说道通信,就要讲到跳频。跳频的原理是
蓝牙这边在通信的时候,会约定一个随机的频率(也不是完全随机,有一个范围)。双方在这个频率上通信,这样通信更稳定。所以Ble在扫描和连接两个步骤中,可能耗时比较长,但是开始通信之后,速度和稳定性都会加快不少。
而跳频的参数,是在连接命令里发过去的。
蓝牙设备,都会注册service和characteristic,并且用uuid标识。
service 可以理解为一个服务,在BLE从机中有多个服务,电量信息,系统服务信息等,每一个service中包含了多个characteristic特征值,每一个具体的characteristic特征值才是BLE通信的主题。
characteristic特征值:BLE主机从机通信均是通过characteristic进行,可以将其理解为一个标签,通过该标签可以读取或写入相关信息。
大家可以理解为service就是java里的class,characteristic是class里的public方法。所以我们应该先找到class,再调用其方法。
再往下想,写和读肯定是两个方法,所以也会是两个characteristic。
service和characteristic的uuid都是通信之前双方约定好的。android端在上文说的discoverService之后,查看有没有对应的uuid,如果有,就可以发起通信了。
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
listener.onServicesDiscovered(status);
}
@Override
public void onServicesDiscovered(final int status) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
service = getGatt().getService(ServerUUID);
if (null != service) {
BluetoothGattCharacteristic read_characteristic = service.getCharacteristic(readDataUUID);
if (null != read_characteristic) {
int properties = read_characteristic.getProperties();
if ((properties | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
getGatt().setCharacteristicNotification(read_characteristic, true);
}
}
}
}
}
setCharacteristicNotification 成功后,就可以在onCharacteristicChanged方法收到回调了。
蓝牙通信的过程,就是往writeCharacteristic里写数据,然后监听readCharacteristic的返回。这两个操作保持有序进行,就可以通信了。
过程总结
针对上文说的例子, 画个图来总结一下
蓝牙采坑总结
scan过程
据测试,Android5.0以下的手机,是不支持128位的uuid的,只支持16位的。如果过滤的uuid填入128位的话,是搜索不到设备的。所以要分别判断。
部分手机(三星遇到过),在蓝牙关闭情况下,仍然可以使用ble功能,但是经常会有问题,所以请务必打开蓝牙再开始通信。
部分手机,如果1次scan不到,后续都不会scan到了,这时要停止scan,重新启动一次。
连接错误
国内Android手机的蓝牙不知道为何,非常不稳定。在连接的过程中,会有各种各样的错误,我用了github上总结的错误,
https://github.com/Twelvelines/AndroidMuseumBleManager/blob/ef5e866c0aa8955320bac7ee9884f60be5bbe1d5/bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/GattError.java
或者
https://blog.csdn.net/ocean20/article/details/65431478
这些错误会频繁的遇到,所以为了成功率更高,需要在连接的时候增加重试机制。
遇到这些错误,立刻断开连接,等待1-2秒后再次尝试连接。
具体见以下代码:
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
BleLog.d("onConnectionStateChange", "status: " + status + " newState: " + newState);
if (status != BluetoothGatt.GATT_SUCCESS) {
BleLog.e(TAG, "连接状态异常->" + GattError.parseConnectionError(status));
disconnectInternal(false);
onConnectError(status);
}
onConnectError的回调里,可以直接去重试连接。这样可以提高成功率。但是重新连接之前一定要close当前连接,否则会出现各种问题。
但是重试随之而来的问题是老的gatt对象可能仍然会回调(系统回调不知道什么时候回来),所以在接收回调的时候,最好判断一下当前的gatt对象是否是最新的。
然后还有就是通信问题,通信相对于连接来说,是稳定很多的,但是仍然会有一定的几率,收不到消息。所以硬件设备和手机的蓝牙代码,最好有一方有重试机制,一般来说手机重试就足够了。
参考文献
- 蜗窝科技 http://www.wowotech.net
- BluetoothKit作者博文 BluetoothKit
- 简书
后续会对android连接的源码分析一波