目录
即将迈入新阶段,贼开心,总结下。如果有误,麻烦指出。
先看下最终效果(图中的“刷新”只是方便测试,表明从硬件接收到了新的数据包而已):
1. 功能部分
1.1 BLE简介
核心功能使用的是Android官方提供的BLE SDK。在BLE协议中,有两个角色——周边和中央。周边用来提供数据,中央用来使用数据。在我的应用中,周边即BLE模块,中央即安卓手机。
先介绍2个关键类:
(1)BluetoothGattServer作为周边来提供数据;BluetoothGattServerCallback返回周边的状态。
(2)BluetoothGatt作为中央来使用和处理数据;BluetoothGattCallback返回中央的状态和周边提供的数据。
我们要做的是什么呢?是拿到这个BluetoothGatt!拿到了它,调用API中提供的方法,就可以通过BluetoothGattCallback和周边BluetoothGattServer交互了。
协议、服务(Service)、特征(Characteristic)等基本概念参考
1.2. 详细流程
我把Android蓝牙开发分为3个大步骤:扫描、连接和交互。
(1).声明BLE相关权限。
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
(2).判断设备是否支持BLE,若支持,则获取到设备的蓝牙适配器。
//检查设备是否支持BLE
if(!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)){
Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
finish();
}
//蓝牙管理器,主要用于获取蓝牙适配器和管理所有和蓝牙相关的东西。
final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
//用蓝牙管理器获得蓝牙适配器,每一台支持蓝牙功能的手机都有一个蓝牙适配器。
mBluetoothAdapter = bluetoothManager.getAdapter();
//检查设备是否支持BLE
if(mBluetoothAdapter == null){
Toast.makeText(this, R.string.bluetooth_not_supported, Toast.LENGTH_SHORT).show();
finish();
return;
}
//andoird 6.0需要开启定位请求
mayRequestLocation();
(3).判断当前手机是否已经开启了蓝牙功能,若未开启,提示用户开启蓝牙功能。(跟共享单车APP里的步骤一样一样的)
//如果当前手机蓝牙未开启,弹出dialog提示用户开启。
if(!mBluetoothAdapter.isEnabled()){
if(!mBluetoothAdapter.isEnabled()){
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
}
(4).扫描周边BLE设备,获取扫描到的设备。
mBluetoothAdapter.startLeScan(mLeScanCallback);//扫描很费电,要预设扫描周期,扫描一定周期就停止扫描
//作为BLE扫描结果的接口,实现如下。
private BluetoothAdapter.LeScanCallback mLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
...
}
};
(5).调用连接到GATT服务端的方法;
mBluetoothGatt = device.connectGatt(this, false, mGattCallBack);//这个mBluetoothGatt很关键,后面要调用这个类的方法。
到此为止,已经有了一个成果——已经拿到了BluetoothGatt!!!
(6).连接到GATT服务端成功后,发现服务;发现服务并成功获取服务后,获取特征值,得到属性为Write和Notify的特征值,获取到特征值即代表手机与蓝牙设备连接成功;然后使能属性为Noitfy的特征值,只有使能特征值之后,特征值发生改变,才会马上回调到onCharacteristicChanged()中。
private final BluetoothGattCallback mGattCallBack = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { //连接状态改变时候回调到此处
if(newState == BluetoothProfile.STATE_CONNECTED){
//连接上之后发现服务
mBluetoothGatt.discoverServices();
...
}
//发现到服务之后回调到此处
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//优先根据xxx蓝牙模块的服务UUID来查找服务
BluetoothGattService service = gatt.getService(UUID.fromString(BleGattAttributes.BLE_Service));
if(service != null){
//根据xxx蓝牙模块的特征值UUID来查找特征值
mWriteCharacteristic = service.getCharacteristic(UUID.fromString(BleGattAttributes.BLE_Write_Characteristic));
mNotifyCharacteristic = service.getCharacteristic(UUID.fromString(BleGattAttributes.BLE_Notify_Characteristic));
}
if(mNotifyCharacteristic != null){
//找到特征值才代表连接成功
//使能通知,使能属性为Notify的特征值之后,以后特征值改变时候,就会回调到onChracteristicChanged()中。
setCharacteristicNotification(mNotifyCharacteristic, true);
}
}
//读取特征值后回调到此处
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
...
}
//特征值发生改变时回调到此处
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
...
}
//写入特征值后回调到此处
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
...
}
};
(7).接下来手机就可与蓝牙模块交互了。即调用发送函数,向蓝牙模块发送数据包;有数据包返回时候,会回调到onCharactericChanged()中。
//向蓝牙设备发送数据
mWriteCharacteristic.setValue(data);
mBluetoothGatt.writeCharacteristic(mWriteCharacteristic);
(8).按照协议对收到的数据帧进行解析并显示。我的处理是把解析出来的数据存放到全局变量中,便于在Fragment中使用,然后适配器负责把这些数据与界面联接起来。
(9).数据需要定时更新的情况下,按照一定的周期重复步骤7、8。
1.3 几个注意点:
(1)BLE要求每包数据最多20字节,如果数据量大于20字节,难免要分包发送再拼接下,拼接后再处理。所以BLE更适合小数据量的应用,用来传递音视频的话,可能心会比较累……
(2)我是对拼接后的数据包处理了一下,判断了下拼接后的数据包的正误才处理的(不知道是否必要,不过加了更安全),有空贴出来我的写法。
2. 界面部分
整体采用界面效果跟微信相似,底部导航栏+ViewPager实现了滑动切换Fragment的效果,某些需要显示信息量大的页面采用ListView。
ListView的使用:其中第3、4个页面用的是列表,以第3个页面为例说明ListView的用法。第3个页面中的ListView中要装入的是单体电压数据,但是数组cellVoltage中的数据是无法直接传给ListView的,需要借助适配器来实现。我选择的适配器是ArrayAdapter(因为它可以通过泛型来指定要适配的数据类型),在构造适配器对象时候,将要用的列表样式和数据源作为参数传递进去。然后,将实例化出来的列表对象和构造出来的适配器关联在一起。Ok啦。
ViewPager的使用:ViewPager是一个页面切换的组件,可以往里面填充多个View,通过触摸屏左右滑动来切换不同的View。和ListView一样,需要一个适配器(适配器的作用:将复杂的数据填充在指定视图界上,数据源多种多样,而ListView等组件显示数据的格式是有要求的,适配器就是桥梁),将要显示的View和我们的ViewPager进行绑定,而ViewPager有自己特定的Adapter—PagerAdapter。ViewPager实现
3. 遇到的坑:
- 功能部分,上下位机数据交互时候(上位机指手机,下位机指带有BLE模块的硬件系统),读到的数据比较混乱,没有按照请求的包的顺序来收发。经过debug单步调试,找到了原因:在上下位机数据交互时,数据的载体是一个叫做“特征值”的东西(基本概念的介绍有空再加上),上下发送数据时候,都会改动这个特征值,两方同时操作一个特征值,不出现混乱才奇怪呢。解决方法是:当收到了下位机回复的第i包数据时候,上再向下要第i+1包数据,具体来说是这样的顺序:(假设一次数据更新分为了5包数据)上向下发送命令包1——>下向上回复数据包1——>上向下发送命令包2——>下向上回复数据包2——>…——>上向下发送命令包5——>下向上回复数据包5。严格控制这样的顺序,保证了每个时刻,上位机和下位机只有一个在操作特征值。
- 界面部分,左右滑动来切换页面的功能最初用了一个库,但是实验过程中发现,当数据刷新频率很快且左右滑动频率很快时候,边缘的页面(最靠右边的或者倒数第二个)就会崩溃。原因我认为在于库中对ViewPager的缓存机制用得不好。(ViewPager的缓存机制:ViewPager不仅会缓存当前页、还会缓存前一页和后一页,比如当前位于第2页,就会缓存第1、2、3页;比如当前位于第4页时候,缓存的是3和4)。后来我就索性自己实现了下这个功能。
4. 推荐学习资料
Android:
《第一行代码》郭霖,适合安卓新手学习。pdf下载
BLE:
https://blog.youkuaiyun.com/jimoduwu/article/details/21604215
https://www.jianshu.com/p/c7bb4e8f9fe6
https://developer.android.com/guide/topics/connectivity/bluetooth-le#terms //Andoid BLE开发官方文档
https://blog.youkuaiyun.com/shb2058/article/details/51279731 //Andoid BLE开发官方文档中文翻译