安卓 蓝牙 android BLE 基础开发

(本文观点纯属个人观点,并非具有权威参考价值,如有错误,望看客指出。)

简介

开发之前,首先来了解一下蓝牙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中添加相关权限。

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
<!--当android:required为true的时候,app只能强制运行在支持BLE功能的设备商,为false的时候,可以运行在所有设备上,-->
<!--但某些方法需要手动检测,否则可能存在隐性BUG-->

新建一个服务类例如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<BluetoothDevice> boundDevices = mBluetoothAdapter.getBondedDevices();
    for(BluetoothDevice device : boundDevices){
        //对device进行其他操作,比如连接等。
    }
}

扫描的3种方法

//设备列表成员变量
private List<BluetoothDevice> 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<ScanResult> 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<BluetoothGattCharacteristic > 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<BluetoothGattDescriptor> 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 更新

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Key Terms And Concepts 关键术语和概念 Here is a summary of key B...
    Jaesoon阅读 2,426评论 0 5
  • 前言: 本文主要描述Android BLE的一些基础知识及相关操作流程,不牵扯具体的业务实现,其中提供了针对广播包...
    幻影宇寰阅读 5,310评论 6 19
  • 声明:转载请注明出处http://www.jianshu.com/p/54bc88207050 Android 4...
    蛇发女妖阅读 6,686评论 1 4
  • 背景 蓝牙历史说到蓝牙,就不得不说下蓝牙技术联盟(Bluetooth SIG),它负责蓝牙规范制定和推广的国际组织...
    徐正峰阅读 12,218评论 6 33
  • 蓝牙 蓝牙的波段为2400-2483.5MHz(包括防护频带)。这是全球范围内无需取得执照(但定不是无管制的)的工...
    苏永茂阅读 6,103评论 0 11