(本文观点纯属个人观点,并非具有权威参考价值,如有错误,望看客指出。)
简介
开发之前,首先来了解一下蓝牙BLE和传统蓝牙有何区别,这将会有助于后面的开发。
在过去的蓝牙开发项目当中,我们曾经使用过BLUETOOTHSOCKET和BLUETOOTHSERVERSOCKET的方式来在手机与蓝牙外设之间进行通信,这种方式与TCP/IP通信有点像,手机与蓝牙外设只能其中一个作为SERVER角色,另外一个作为CLIENT角色,一般来说,谁优先发起链接请求的,谁就是CLIENT,另外一个就作为SERVER。在通过BLUETOOTHSOCKET或BLUETOOTHSERVERSOCKET相关API获得蓝牙通道(RFCOMM)后,方可利用此通道进行双端通信。
在BLE开发当中,同样存在两个角色,一个是中心角色(CENTRAL),一个是外设角色(PERIPHERAL);蓝牙设备或手机都可以单独作为CENTRAL或PERIPHERAL角色(较旧的版本当中手机无法作为外设提供数据)。外设角色的作用是为中心角色提供各种数据,中心角色可以扫描并接收多个外设角色数据,数据以服务(SERVICE)和特征(CHARACTERISTIC)的形式呈现。一个服务可以包含多个特征,如何更好的理解服务和特征?大家可以把服务当成一个包裹,特征当成包裹里面的物品,包裹有包裹的名字,物品有物品的名字,一个包裹可以包含多个物品,或为空。如此一来,服务和特征的特性关系应该更加清晰,我们的目标就是用手机来获取外设的服务,取的其中的特征来获得数据。
在非常简单的介绍过后,接下来我们来看看在BLE开发当中各种主要类和其作用。
- BluetoothDeivce:蓝牙设备,代表一个具体的蓝牙外设。
- BluetoothAdapter:蓝牙适配器,每一台支持蓝牙功能的手机都有一个蓝牙适配器,一般来说,只有一个。
- BluetoothManager:蓝牙管理器,主要用于获取蓝牙适配器和管理所有和蓝牙相关的东西。
- BluetoothGatt:通用属性协议, 定义了BLE通讯的基本规则,就是通过把数据包装成服务和特征的约定过程。
- BluetoothGattCallback:一个回调类,非常重要而且会频繁使用,用于回调GATT通信的各种状态和结果,后面会详细解释。
- BluetoothGattCharacteristic:特征,里面包含了一组或多组数据,是GATT通信中的最小数据单元。
- BluetoothGattService:服务,描述了一个BLE设备的一项基本功能,由零或多个特征组构成。
- BluetoothGattDescriptor:特征描述符,对特征的额外描述,包括但不仅限于特征的单位,属性等。
- BluetoothLeScanner:蓝牙适配器里面的扫描器,用于扫描BLE外设。
以上就是在BLE开发中一定会用到的一些基础类,实际使用当中,可能还会用到例如AdvertiseData,AdvertiseSettings,AdvertiseCallback等等,会在后续陆续更新。接下来需要先简单梳理各种类和角色之间是什么关系。
- 中心与外设角色:任何设备都可以单独作为中心或外设角色。一个没有被链接的外设角色,会向外界发出广播,这个时候可以被多个中心角色发现,一旦外设角色被某个中心角色链接后,外设角色就会停止广播,其他中心角色就无法在链接到这个外设角色。中心角色可以扫描外设角色,可以监听接收广播或主动链接,一个中心角色可以与多个外设同时链接。
- 协议,服务与特征:一份协议由一个或多个服务构成,一个服务由零个或多个特征构成,一个特征可以包含一组或多组值,可以包含零个或多个描述符。每一个服务与特征都有一个UUID作为唯一识别符,识别符有通用的,也可以自定义,也可以随机生成,固定格式00000000-0000-0000-0000-000000000000(8-4-4-4-12),一般来说自定义的UUID只有前8位有变化,后面的基本是固定的0000-1000-8000-00805f9b34fb,所以一个自定义的UUID一般看起来就像这样 “0000????-0000-1000-8000-00805f9b34fb” ????就表示4个16进制数。每一个特征都有其属性和权限(READ | WRITE | NOTIFY | INDICATE),特征根据属性可读可写。
- 适配器,扫描器,设备:每一台支持蓝牙的手机中都会有一个蓝牙适配器,由蓝牙管理器管理着,从其中获得蓝牙适配器。适配器中自带扫描器,使用扫描器可以扫描周边的蓝牙设备。
到这里,大体上已经对BLE相关基础内容有了初步印象,接下来会用代码方式演示如何通过协议与外设通信。
协议
首先我们来看看一个简单的BLE协议是怎么样定义的。在BLE开发过程中,硬件开发与软件开发之间需要互相协商定义一个双方都认可的BLE通信协议,包括所有服务,特征等的UUID,数据格式,和数据包定义等。先模拟一个简单的协议,名字叫XXXX服务:
XXXX Service
UUID(Hex) | Type(DEFINE) | Length | Property |
---|---|---|---|
0x018f | service uuid | - | - |
0x2a19 | xxxx characteristic | 1 | READ |
0x4e17 | xxxx characteristic | 1 | NOTIFY |
由表中看出,XXXX服务的服务UUID是018f (完整的表示 :0000018f - 0000-1000-8000-00805f9b34fb, 在IOS中,可以直接使用018f),其中包含2个特征,一个UUID是2a19,属性为READ可读,另外一个是4e17,属性为NOTIFY通知 。如何用代码表示一个已知的UUID?如下:
private UUID xxxxServiceUUID = UUID.fromString("0000018f-0000-1000-8000-00805f9b34fb");
需要注意的是,这里用的String字符串一定是完整的32位的,安卓中不能单独使用018f,否则会报错。如此一来,一个服务UUID就表示完毕了,特征也是一样。
在蓝牙设备与中心通信的数据交互中,一次传输最大只能传20个byte,超过20个byte,就需要进行分包处理了,那么协议中如何定义这20个byte当中包含的信息呢?举个例子,现在我们定义:
byte0-byte1分别表示总包号,分包号;
byte2-byte5表示UCT时间;
byte0 | byte1 | byte2 | byte3 | byte4 |
---|---|---|---|---|
0 | 1 | 18 | 62 | 6e |
byte5 | byte6 | byte7 | byte8 | byte9 |
80 | 0 | 0 | 0 | 0 |
byte10 | byte11 | byte12 | byte13 | byte14 |
0 | 0 | 0 | 0 | 0 |
byte15 | byte16 | byte17 | byte18 | byte19 |
0 | 0 | 0 | 0 | 0 |
最后,我们会收到一条数据:00 01 18 62 6e 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00
当然,不一定需要使用20个byte,如果6个就可以解决问题,那么我们为什么不用 00 01 18 62 6e 80 呢?
在中心接收到这条收据后,我们就可以知道这些数据表示:总包号为0,分包号为1,UCT时间为0x18626e80秒,得到这些原始数据后,我们就可以根据具体情况,转换或解析每一个数据表示什么,后面会讲到这些,现在只是举例说明协议是如何定义的。
这里只是举例了一个服务,一个设备是可以包含很多个服务的,所以,你最后大概会需要分析一大堆UUID和这些byte数组。
到此为止,我们已经非常简单的定义了一个可用的BLE协议了。
代码
在Androidmanifest.xml中添加相关权限。
新建一个服务类例如BleService继承于Service,在服务里完成对蓝牙的所有操作。
public class BleService extends Service {
private BleBinder mBleBinder;
private Handler mBleHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
public IBinder onBind(Intent intent) {
return mBleBinder;
}
public class BleBinder extends Binder {
//在此提供对外部的调用方法,当某活动绑定此服务后,获得返回mBleBinder对象,外部活动通过操作mBleBinder的方法来控制蓝牙设备。
public void startScan(){
//开始扫描......
}
public void stopScan(){
//停止扫描........
}
//更多方法........需要注意的是,某些方法为耗时操作,有必要时应该开启子线程去执行。
//而且蓝牙很多时候都是异步操作,需要使用许多回调方法。
//如果此服务为独立进程服务,并为其他app提供数据,需要注意方法同步。
}
}
判断设备上的蓝牙是否可用,是否开启。
//适配器与蓝牙管理器的成员变量。
private BluetoothAdapter mBluetoothAdapter;
private BluetoothManager mBluetoothManager;
//检查设备是否支持BLE功能。
private boolean checkIfSupportBle(){
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
}
//如果设备支持BLE,那么就可以获取蓝牙适配器。
private BluetoothAdapter getAdapter(){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = mBluetoothManager.getAdapter();
} else {
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
}
return mBluetoothAdapter;
}
//获取完适配器后,需要检测是否已经打开蓝牙功能,如果没有,就需要开启。
//开启蓝牙功能需要一小段时间,具体涉及的线程操作或同步对象不在此讨论,视实际情况按需编写。
private void enableBluetooth(){
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
}
//此方法用于获取在手机中已经获取并绑定了的设备
private void getBoundDevices(){
Set boundDevices = mBluetoothAdapter.getBondedDevices();
for(BluetoothDevice device : boundDevices){
//对device进行其他操作,比如连接等。
}
}
扫描的3种方法
//设备列表成员变量
private List mDevices;
//startDiscover() 和 startLeScan(),startBleScan() 都可以发现设备,但是startLeScan() 和startScan()只会发现那些支持BLE的设备。
//第一种方法
//startDiscover() 是通用扫描方法
private void startDiscover(){
mBluetoothAdapter.startDiscover();
//此过程大概持续10秒,当扫描到蓝牙设备后,会发出广播,只要在需要的地方注册接收广播,就可以获得扫描结果。
//这种方法可以扫描出所有蓝牙设备,包括BLE,但貌似不同手机有不同体验,各位实践出真知吧,扫不扫得到,看具体设备。
}
//注册此广播,监听BluetoothDevice.ACTION_FOUND,以接收系统消息取得扫描结果
private class DeviceReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if(BluetoothDevice.ACTION_FOUND.equals(action)){
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); //这个就是所获得的蓝牙设备。
mDevices.add(device );
}
}
}
//第二种方法
//startLeScan是一个已经被声明过时的方法,但此方法依然可用,而且很多设备上运行的还是这个方法。
//开始扫描
private void startLeScan() {
mBluetoothAdapter.startLeScan(mLeScanCallback);
}
//停止扫描
private void stopScan() {
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
//LeScanCallback 是蓝牙扫描返回结果的回调,可以通过回调获取扫描结果。
private BluetoothAdapter.LeScanCallback mLeScanCallback= new BluetoothAdapter.LeScanCallback(){
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
mDevices.add(bluetoothDevice);
//成功扫描到设备后,在这里获得bluetoothDevice。可以放进设备列表成员变量当中方便后续操作。
//也可以发广播通知activity发现了新设备,更新活动设备列表的显示等。
//这里需要注意一点,在onLeScan当中不能执行耗时操作,不宜执行复杂运算操作,切记,
//下面即将提到的onScanResult,onBatchScanResults同理。
}
};
//第三种方法
//代替已过时的startLeScan()方法的方法,但貌有时候扫不出目标设备。此方法API21以上可用。
//开始扫描
private void startBleScan(){
mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);
}
//停止扫描
private void stopBleScan(){
mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback);
}
//ScanCallback 是蓝牙扫描返回结果的回调,可以通过回调获取扫描结果。
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
//当发现一个外设时回调此方法,但本人在实际使用过程当中发现一些问题,
//此方法在一次扫描过程当中只会返回一台设备,也就是如果scan有结果返回后,
//就会一直返回被第一次扫描到的那个设备,无论等多久都一样,所以本人怀疑
//如果要使用此方法的话,可能需要间歇性多次调用startScan才能发现多个设备。
//但是不是这样,各位可以自己去试一试,因为本人在开发过程中依然使用了
//上面第二种过时的方法。ScanResult 可以获得扫描到的设备,可以保存到设备列表成员变量当中方便后续操作。
}
@Override
public void onBatchScanResults(List results) {
super.onBatchScanResults(results);
//在此返回一个包含所有扫描结果的列表集,包括以往扫描到的结果。
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
//扫描失败后的处理。
}
};
//这个方法意在告诉大家获取得到设备后,我们能获得什么信息,非主要函数,可以忽略。
private void showDetailOfDevice(){
//获得设备名称,多个设备可以有同一个名称。
String deviceName = mTargetDevice.getName();//获得设备名称,多个设备可以有同一个名称。
//获取设备物理地址,一个设备只能有一个物理地址,每个设备都有每个设备的物理地址,无法相同。
String deviceMacAddress = mTargetDevice.getAddress();
//绑定设备
mTargetDevice.createBond();
//更多的信息....
}
另外再补充一点,对于安卓6.0以上的基带,需要获取动态权限,具体如下
//android 6.0 以上的扫描结果获取动态权限的方法
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_attach_device);
//再onCreate方法当中加入以下代码,判断系统是否需要动态获取权限。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (this.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
}
}
}
//然后加入onRequestPermissionsResult这个方法,如果只是需要获取权限,那么如下就可以了。
//至于获取到权限后各位还有什么其他用处,就自由发挥吧。
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
利用以上方法,可以扫描到所需要的蓝牙设备,需要注意的是,同一时间只能有一种扫描方法起作用,不能在使用方法1的同时再使用方法2或3。可以把扫描结果存储在一个成员列表里,以便后续操作。还有一点需要注意,LeScanCallback和ScanCallback是两个不同的类,注意不要用混淆了。所有扫描方法都会一定程度影响主线程的流畅性,因此可以考虑把扫描操作放置子线程执行。
连接BLE蓝牙设备
//手机链接蓝牙设备,就需要获取与之相关的GATT链接,首先声明gatt
private BluetoothGatt mBluetoothGatt;
private BluetoothDevice mTargetDevice;//从扫描到的设备列表里选出目标设备。
//mBluetoothGattCallback 为所有蓝牙数据回调的处理者,也是整个蓝牙操作当中最为核心的一部分
private BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
//BluetoothGattCallback 里面有很多方法,但并非所有都需要在开发当中用到。
//这里列出来只是作为部分解析,需要哪个方法,就重写哪个方法,不需要的,直接去掉。
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
//当设备与中心连接状态发生改变时。
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
//当发现设备服务时,会回调到此处。
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
//读取特征后回调到此处。
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
//写入特征后回调到此处。
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
//当特征(值)发生变法时回调到此处。
}
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorRead(gatt, descriptor, status);
//读取描述符后回调到此处。
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
//写入描述符后回调到此处
}
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
super.onReliableWriteCompleted(gatt, status);
//暂时没有用过。
}
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
super.onReadRemoteRssi(gatt, rssi, status);
//Rssi表示设备与中心的信号强度,发生变化时回调到此处。
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
//暂时没有用过。
}
};
private void openGatt(){
mBluetoothGatt = mTargetDevice.connectGatt(BluetoothLeService.this, false, mBluetoothGattCallback);
//通过之前扫描所得设备,打开Gatt链接。
//连接可能需要等待。
//第一个参数是传Context,这个很好理解。
//第二个参数是控制是否自动链接,为true的时候,当设备进入中心范围,会进行自动连接,为false反之。
//第三个参数就是上面那个GattCallback了。
//此步骤执行之后,所有结果都会回调到GattCallback当中,接下来我们就需要对其进行操作了。
}
GattCallback有什么作用?
//特征列表集
private List mCharacteristics;
//在connectGatt()这个方法执行完毕后,GattCallback当中便会出现回调结果。
//首先在onConnectionStateChange()这个方法当中会,我们判断设备是否成功连接
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
switch (newState) {//newState顾名思义,表示当前最新状态。status可以获取之前的状态。
case BluetoothProfile.STATE_CONNECTED:
//这里表示已经成功连接,如果成功连接,我们就会执行discoverServices()方法去发现设备所包含的服务
onStateConnected(gatt);
break;
case BluetoothProfile.STATE_DISCONNECTED:
//表示gatt连接已经断开。
onStateDisconnected(gatt);
break;
}
}
private void onStateConnected(BluetoothGatt gatt) {
gatt.discoverServices();
Log.i("Ble Connection", "Start to discover services.");
}
private void onStateDisconnected(BluetoothGatt gatt) {
Log.i("Ble Connection", "Connection is broken.");
gatt.close();
}
//接下来,在执行discoverServices()后,外设就会告诉我们它能够为中心提供哪些服务
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
//gatt.getServices()可以获得外设的所有服务。
for (BluetoothGattService service : gatt.getServices()) {//接下来遍历所有服务
//每发现一个服务,我们再次遍历服务当中所包含的特征,service.getCharacteristics()可以获得当前服务所包含的所有特征
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
mCharacteristics.add(characteristic);//通常可以把所发现的特征放进一个列表当中以便后续操作。
Log.i("", characteristic.getUuid().toString());//打印特征的UUID。
}
}
}
//当方法执行完后,我们就获取了设备所有的特征了。
//如果你想知道每个特征都包含哪些描述符,很简单,再用一个循环去遍历每一个特征的getDescriptor()方法。
}
//当我们执行了readCharacteristic()方法后,结果会回调在此。
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
if(status == BluetoothGatt.GATT_SUCCESS) {
//如果程序执行到这里,证明特征的读取已经完成,我们可以在回调当中取出特征的值。
//特征所包含的值包含在一个byte数组内,我们可以定义一个临时变量来获取。
byte[] characteristicValueBytes = characteristic.getValue();
//如果这个特征返回的是一串字符串,那么可以直接获得其值
String bytesToString = new String(characteristicValueBytes );
//如果只需要取得其中的几个byte,可以直接指定获取特定的数组位置的byte值.
//例如协议当中定义了这串数据当中前2个byte表示特定一个数值,那么获取这个值,可以直接写成
byte[] aValueBytes = new byte[]{
characteristic.getValue()[0], characteristic.getValue()[1]
}
Log.i("c-u", "" + Integer.parseInt(UUIDS.bytesToHexString(characteristic.getValue()), 16));
//至于这个值时表示什么,十进制数值?或是一个字符串?还是翻开协议慢慢找吧。
//到这里为止,我们已经成功采用读的方式,获得了存在于特征当中的值。
//characteristic还能为我们提供什么东西呢?属性,权限等是比较常用的。
}
}
//当我们执行了gatt.setCharacteristicNotification或写入特征的时候,结果会回调在此
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
//当我们决定用通知的方式获取外设特征值的时候,每当特征值发生变化,程序就会回调到此处。
//在一个gatt链接当中,可以同时存在多个notify的回调,全部值都会回调到这里,那么我们如何区分这些值的来源?
//这个时候,我们就需要去判断回调回来的特征的UUID,因为UUID是唯一的,所以我们可以用UUID
//来确定,这些数据来自哪个特征。
//假设我们已经在BleService当中定了多个我们想要使用的静态UUID,前面已经说过如何表达一个UUID
//那么我们需要做的就是对比这些UUID,根据不同的UUID来分类这些数据,究竟应该交由哪个方法来处理
//所以,这么一来我们便会发现其实上面的onCharacteristicRead也会出现这种情况,
//因为我们不可能只读取一个特征,除非这个外设也只有这一个特征,
//究竟是谁在读取,读取的值来自于哪个特征等,都需要进行判断。
if(mUUID_ONE.equals(characteristic.getUuid())){
//do something.
} else if (mUUID_TWO.equals(characteristic.getUuid())){
//do something.
}
//我们会有更好的写法,我们应该抽象这些方法,这样一来我们可能会减少很多代码量。
}
/*从onCharacteristicChanged当中我们发现了一个问题,就是无论什么时候,我们都需要去判断一个回调特征
从哪里来到哪里去,因此我们应该采用一种方法来统一这种读取的操作*/
private void updateCharacteristic(BluetoothGattCharacteristic characteristic) {
//在onCharacteristicChanged和onCharacteristicRead方法中直接把characteristic交给updateCharacteristic处理。
//这样一来,程序可读性就会更加好了,思路也会更加清晰。
//这里处理所有的特征取值
if(mUUID_ONE.equals(characteristic.getUuid())){
handlerCharacteristic_One();
} else if (mUUID_TWO.equals(characteristic.getUuid())){
handlerCharacteristic_Two();
}
}
private void handlerCharacteristic_One(){
//相应解析方案
}
private void handlerCharacteristic_Two(){
//相应解析方案
}
/*在官方的写法中,采用了Broadcast的方式来让Service向其他组件发送蓝牙回调数据,
这么做当然是可以的,但是如果一个蓝牙设备包含了很多个服务而且服务当中存在大量
Notify方式读取的数据,那么Service收到这些数据后,又用Broadcast的方式广播数据,
系统中就会存在大量广播而导致app性能下降,如何解决这种问题,我们可以使用回调
的方式的方式来触发外部activity或fragment等组件取得数据更新UI或其他操作,例如我们
可以这么写:
*/
/*我们先定义一个回调接口,让外部Activity或Fragment实现,又或直接新建一个类实现。
我们在Activity绑定Service后获得Binder对象时,把实现了ICallback的回调接口类通过
setter方式传入到Service当中作为Service的成员变量,例如可在Service的内部Binder类
写一个方法setFragments或setActivities之类的。至于回调的工作方式,这里不做详尽介绍了。
*/
public interface ICallback {
//这就是我们定义的接口,让需要获得characteristic更新数据的外部组件实现回调接口。
void updateCharacteristic(BluetoothGattCharacteristic characteristic);
}
//在Service的GattCallback对象中,加入一个方法,如下:
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
updateCharacteristic('这里传入实现了ICallback接口的对象', characteristic);
}
private void updateCharacteristic(ICallback callback, BluetoothGattCharacteristic characteristic) {
callback.updateCharacteristic(characteristic);
}
//在外部activity或fragment等组件当中收到回调
@Override
public void updateCharacteristic(final BluetoothGattCharacteristic characteristic) {
/*这里已经回到Activity或Fragement了,我们可以获取characteristic进行我们想要的任何操作了,
但这里需要注意的是,回调执行的线程可能并非UI线程或不是对该Activity或Fragement当中的
view拥有操作权,可能会异常,我们需要使用activity或fragment当中的handler对象来处理*/
mHandler.post(new Runable(){
@Override
public void run(){
//在这里我们就可以放心执行UI更新,例如对characteristic进行getValue之类的。
}
});
}
/*采用回调的方式可以更加高效快捷地处理蓝牙发送过来的数据,让系统中少了一大堆广播,逻辑也更加
清晰了。另外也可以通过观察者模式来实现Service与外部组件交换数据,让Serviec作为一个可以被订阅
的对象(或拥有一个可被订阅的对象),让想获得特征更新的外部组件成为订阅者,这么做也是可以的,
但依然需要注意线程和使用handler机制。*/
上面介绍了最常用的2种获取外设特征值的回调函数,read方式与notify方式,一个相当于主动获取,一个相当于被动接收。上文中还列出来GattCallback的其他回调函数,根据实际需要自行添加即可(有时间会逐一补全)。
下面介绍如何进行对特征的读写操作, 要进行特征读写,首先需要知道该特征是否可以读,或是否可写。
//判断特征可读
private boolean ifCharacteristicReadable(BluetoothGattCharacteristic characteristic){
return ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0);
}
//判断特征可写
private boolean ifCharacteristicWritable(BluetoothGattCharacteristic characteristic){
return ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0 ||
(characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0));
}
//判断特征是否具备通知属性
private boolean ifCharacteristicNotifiable (BluetoothGattCharacteristic characteristic){
return ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0 ||
(characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0));
}
//当然,这只是在不知道此特征的读写性的情况下,才需要做判断,一般来说,协议中会明确指出特征的读写性,
//如果万一真的忘记写,我们也可以自己判断。
在知道了各个特征的读写性后,我们终于可以进行读写交互的动作了。
//读取特征,相当简单,一句话带过,读取结果会回调到mGattCallback中的onCharacteristicRead。
private void readCharacteristic(BluetoothGattCharacteristic characteristic){
mBluetoothGatt.readCharacteristic(characteristic);
}
//写入特征,也相当简单,一句话带过,读取结果会回调到mGattCallback中的onCharacteristicWrite
private void readCharacteristic(BluetoothGattCharacteristic characteristic){
characteristic.setValue(?);//参数可以是byte数组,字符串等。
mBluetoothGatt.writeCharacteristic(characteristic);
}
//设置通知,读取结果会回调到mGattCallback中的onCharacteristicChanged
private void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enable){
mBluetoothGatt.setCharacteristicNotification(characteristic, enable);
//以下的几句代码有人问可不可省略,这里建议写上。
//在明确知道当前特征的描述符前提下,可以直接使用描述符,不需要做判断,
//但如果不知道此特征是否具有描述符的情况下,没有以下几行代码可能会导致设置通知失败的情况发生。
List descriptorList = characteristic.getDescriptors();
if (descriptorList != null) {
for (BluetoothGattDescriptor descriptor : descriptorList) {
byte[] value = enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
descriptor.setValue(value);
mBluetoothGatt.writeDescriptor(descriptor);
}
}
}
到这里为止,基本上BLE的扫描,连接,读写都做了一番简单介绍了,这里提几点要点
- 第一:蓝牙的通信不能进行无间隔读写发送操作,如何理解?比如我要连续设置2个CharacteristicNotification为true,当代码这么写的时候:
//
private void method(){
setCharacteristicNotification(characteristicOne, true);
setCharacteristicNotification(characteristicTwo, true);
}
生效的只有characteristicTwo。那么如何解决这种问题?我们可以使用Hanlder的机制,采用PostDeley一个一个设置,也可以在onCharateristicNotify,onCharateristicRead等等Gatt回调后在回掉函数中用handler发message,再在handler中处理(可以把这些任务抽象成Runnable或Callable,在Gatt收到回调后再用handler的post方法执行下一个任务,可以考虑把Runnable或Callable放在Queue中,经实际应用后,个人比较推荐后者,就是在Gatt中收到回调后再执行下一个蓝牙写操作,而不是通过handler的PostDeley),么一来,我们就有充分时间让上一个设置生效后,我们再执行下一个设置。
- 第二:鉴于一台手机设备当中,对Gatt连接的资源做了限制的设定,一定要注意当程序不需要用到连接的时候,就需要关闭gatt的连接,让系统进行资源回收。
//断开连接
private void closeGatt(){
if(mBluetoothGatt != null){
mBluetoothGatt .disconnect();
}
}
在GattCallBack的onConnectionStateChange当中,关闭连接
......
case BluetoothProfile.STATE_DISCONNECTED:
mBluetoothGatt.close();
mBluetoothGatt = null;
break;
- 第三:蓝牙的相关操作,就以个人使用经历来说,还是放在子线程中执行比较稳妥,尤其是扫描,当然,蓝牙的读写,链接放在UI线程并无不妥。本人目前并没有阅读过BLE API的源码,不知道内部是如何处理线程的,但为保证UI线程的顺畅,最好还是把蓝牙操作放到子线程中执行。如日后有时间阅读BLE API的源码后确认再更新此说法。
好了到这里为止,我们的BleService服务基本完工,但这只是一个非常简洁的介绍,实际开发当中一定还会存在各种问题,希望大家可以开发顺利。
2017-1-11 15:30
2017-3-10 09:54 更新