Android蓝牙通用数据传输之二(BLE)



一、引言

Android蓝牙中涉及通用数据传输协议的有两种:

  • SPP协议
  • BLE(Bluetooth low energy)协议

SPP协议是Android 2.0引入的API,是通过Socket的形式来实现数据传输及交互,有分客户端和服务端,手机一般以客户端的角色主动连接SPP协议设备。
BLE协议是Android 4.3引入的API,但手机厂商大部份在Android 4.4上才支持BLE,即低功耗蓝牙,一般我们开发的话是使用中央(BluetoothGatt)或者外围(BluetoothGattServer)来进行开发的,手机正常情况下当作中央设备来接收信息,而蓝牙模块当作是外围设备发送数据。

二、BLE(Bluetooth low energy)协议

几个关键类:

  • BluetoothAdapter:代表本地蓝牙适配器,是所有蓝牙交互的入口,对蓝牙的操作都需要用到它,很重要,使用一个已知的MAC地址来实例化一个BluetoothDevice
  • BluetoothDevice:代表一个远程蓝牙设备,使用这个来请求一个与远程设备连接,或者查询关于设备名称、地址、类和连接状态等设备信息
  • BluetoothGatt:作为中央来使用和处理数据,重新连接蓝牙设备,发现蓝牙设备的 Service 等等。使用时有一个回调方法BluetoothGattCallback返回中央的状态和周边提供的数据
  • BluetoothCattService:作为周边来提供数据,通过这个类的 getCharacteristic(UUID uuid) 进一步获取 Characteristic 实现蓝牙数据的双向传输
  • BluetoothCattCharacteristic:蓝牙设备的特征,通过这个类定义需要往外围设备写入的数据和读取外围设备发送过来的数据

看着有点乱,BLE接口确定比SPP多且复杂,但网上有人把BLE连接比喻的非常好,如:BluetoothDevice为学校,BluetoothGatt为学校到达某一个班级的通道,BluetoothCattService为学校的某一个班级,BluetoothCattCharacteristic为班级中的某一个学生。那么蓝牙连接通信的过程就是这样,BluetoothAdapter先找到学校(就是连接目的设备),再通过通道找到目标班级,最后从班级中找到目标学生,这个学生就是我们设备之间通信的中介,很重要,学校有唯一的MAC地址,班级有唯一的serviceUUID,学生有唯一的charactersticUUID(相当于学号),所以就是在一所学校找一个学生的问题。



Service

一个低功耗蓝牙设备可以定义许多 Service, Service 可以理解为一个功能的集合。设备中每一个不同的 Service 都有一个 128 bit 的 UUID 作为这个 Service 的独立标志。蓝牙核心规范制定了两种不同的UUID,一种是基本的UUID,一种是代替基本UUID的16位UUID。所有的蓝牙技术联盟定义UUID共用了一个基本的UUID:0x0000xxxx-0000-1000-8000-00805F9B34FB,为了进一步简化基本UUID,每一个蓝牙技术联盟定义的属性有一个唯一的16位UUID,以代替上面的基本UUID的‘x’部分。例如,心率测量特性使用0X2A37作为它的16位UUID,因此它完整的128位UUID为:0x00002A37-0000-1000-8000-00805F9B34FB。

Characteristic

在 Service 下面,又包括了许多的独立数据项,我们把这些独立的数据项称作 Characteristic。同样的,每一个 Characteristic 也有一个唯一的 UUID 作为标识符。在 Android 开发中,建立蓝牙连接后,我们说的通过蓝牙发送数据给外围设备就是往这些 Characteristic 中的 Value 字段写入数据;外围设备发送数据给手机就是监听这些 Charateristic 中的 Value 字段有没有变化,如果发生了变化,手机的 BLE API 就会收到一个监听的回调。

UUID

BLE分为三部分Service、Characteristic、Descriptor,这三部分都由UUID作为唯一标示符。一个蓝牙4.0的终端可以包含多个Service,一个Service可以包含多个Characteristic,一个Characteristic包含一个Value和多个Descriptor,一个Descriptor包含一个Value。蓝牙就是通过这几个UUID识别交换数据的,这几个很重要。

三、BLE连接

BLE连接流程

BLE设备与手机的通讯过程大概是:BLE设备发出广播并提供用于通讯的服务和特征字–>手机开启蓝牙进行扫描扫描到BLE设备之后发起连接–>连接完成之后通过BLE设备提供的用于通讯的服务和特征字开始收发数据。具体有:

  • 申请权限
  • 蓝牙初始化
  • 搜索设备,查看扫描后的回调
  • 连接蓝牙设备
  • 数据读写
  • 断开连接

申请权限

在 Android 6.0 及以上,还需要打开位置权限。如果应用没有位置权限,蓝牙扫描功能不可用,即扫描不到BLE设备,但其它蓝牙操作例如连接蓝牙设备和写入数据不受影响。

<!-- 通用蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

<!-- 位置权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

或者动态申请权限

    // 所需的全部权限
    static final String[] PERMISSIONS = new String[]{
        Manifest.permission.ACCESS_COARSE_LOCATION,
        ......
    };

    if (checkDeniedPermissions(PERMISSIONS)) {
        requestPermissions(PERMISSIONS);
    }

    // 判断权限集合
    public boolean checkDeniedPermissions(String... permissions) {
        for (String permission : permissions) {
            if (checkDeniedPermission(permission)) {
                return true;
            }
        }
        return false;
    }

    // 判断是否缺少权限
    private boolean checkDeniedPermission(String permission) {
        return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_DENIED;
    }

    // 请求权限兼容低版本
    private void requestPermissions(String... permissions) {
        ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE);
    }

蓝牙初始化

蓝牙初始化工作:在建立蓝牙连接之前,获取本手机的Manager、适配器Adapter,需要确认设备支持 BLE。如果支持,再确认蓝牙是否开启。如果蓝牙没有开启,可以使用 BLuetoothAdapter 类来开启蓝牙。

    //判断是否支持蓝牙4.0,不过目前市场上的Android的手机都支持BLE,所以这条可以略过
    if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
        finish();
    }

    //获取蓝牙适配器
    final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    mBluetoothAdapter = bluetoothManager.getAdapter();

    //判断是否支持蓝牙
    if (mBluetoothAdapter == null) {
        //不支持
        finish();
    }else{
        //打开蓝牙
        if (!mBluetoothAdapter.isEnabled()) {//判断是否已经打开
            //方式一,强制打开
            mBluetoothAdapter.enable();
            //方式二,询问方式打开
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
        }
    }

搜索设备,查看扫描后的回调

在Android5.0(API 21)及之后,原先的mBluetoothAdapter.startLeScan及mBluetoothAdapter.stopLeScan不建议使用了,改成一个Scanner。 使用新接口BluetoothLeScanner.startScan(List, ScanSettings, ScanCallback)和BluetoothLeScanner.stopScan(ScanCallback)。

private void scanLeDevice() {
    Handler mHandler = new Handler();
    mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();

    mBluetoothLeScanner.startScan(mScanCallback);
    Toast.makeText(getApplicationContext(), "Scan 6s", Toast.LENGTH_SHORT).show();

    // Stops scanning after a pre-defined scan period.
    mHandler.postDelayed(new Runnable() {
        @Override
            public void run() {

            mBluetoothLeScanner.stopScan(mScanCallback);
        }
    }, SCAN_TIME);//SCAN_TIME=6000,即6秒后关闭扫描
}

有时候为了过滤不相关的设备,如匹配UUID或名称,可通过带过滤搜索接口:

List<ScanFilter> bleScanFilters = new ArrayList<>();
//添加过滤规则
bleScanFilters.add(new ScanFilter.Builder().setServiceUuid(new ParcelUuid(serviceUUID[0])).build());//UUID过滤
//bleScanFilters.add(new ScanFilter.Builder().setDeviceName(DEVICE_NAME_STRING).build()); //名称过滤
ScanSettings bleScanSettings = new ScanSettings.Builder().build();
mBluetoothAdapter.getBluetoothLeScanner().startScan(bleScanFilters, bleScanSettings, mLeScanCallback);

扫描回调

private ScanCallback mScanCallback = new ScanCallback() {
    boolean flag = true;

    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        if (result != null) {
            BluetoothDevice device = result.getDevice();
            if ((device != null) && (devList != null)) {
                for (BluetoothDevice de : devList) {
                    for (BluetoothDevice de : devList) {
                        if (device.getName().equals(de.getName())) {
                            flag = false;
                            break;
                        } else {
                            flag = true;
                        }
                    }

                    if (true == flag) {
                        devList.add(device);
                        Intent dataListIntent = new Intent(Constants.ACTION_LEDATA_SEND);
                        Bundle mBundle = new Bundle();
                        mBundle.putParcelable("BT_DEVICE", device);
                        dataListIntent.putExtras(mBundle);
                        sendBroadcast(dataListIntent);
                    }
                }
            }
        }
    }
}

当执行上面的代码之后,一旦发现蓝牙设备,LeScanCallback 就会被回调,直到 stopLeScan 被调用。需要注意的是,传入的回调必须是开启蓝牙扫描时传入的回调,否则蓝牙扫描不会停止。

连接蓝牙设备

BluetoothGatt常规用到的几个操作示例:

  • connect() :连接远程设备
  • discoverServices() : 搜索连接设备所支持的service
  • disconnect():断开与远程设备的GATT连接
  • close():关闭GATTClient端
  • readCharacteristic(characteristic) :读取指定的characteristic
  • setCharacteristicNotification(characteristic, enabled):设置当指定characteristic值变化时,发出通知
  • getServices() :获取远程设备所支持的services
connect()

连接蓝牙设备可以通过 BluetoothDevice#ConnectGatt 方法连接,也可以通过 BluetoothGatt#connect 方法进行重新连接。

mBluetoothDevice.connectGatt(context,false,mGattCallbask);

第二个参数表示是否需要自动连接。如果设置为 true, 表示如果设备断开了,会不断的尝试自动连接。设置为 false 表示只进行一次连接尝试。

mGatt = mLastBluetoothDevice.connectGatt(getApplicationContext(), true, mGattCallback);//真正的连接

当调用蓝牙的连接方法之后,蓝牙会异步执行蓝牙连接的操作,如果连接成功会回调 BluetoothGattCalbackl#onConnectionStateChange 方法。这个方法运行的线程是一个 Binder 线程,所以不建议直接在这个线程处理耗时的任务,因为这可能导致蓝牙相关的线程被阻塞。

    private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        //连接状态改变的回调
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                                int newState) {
            //第二个参数代表是否成功执行了连接操作,如果为 BluetoothGatt.GATT_SUCCESS 表示成功执行连接操作,第三个参数才有效,否则说明这次连接尝试不成功
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                mGatt.discoverServices(); //执行到这里其实蓝牙已经连接成功
                Log.e(TAG, "Connected to GATT server.");
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                if (mBluetoothDevice != null) {
                    Log.e(TAG, "重新连接");
                    mGatt.close(); // 防止出现status 133
                    mGatt.connect();
                } else {
                    Log.e(TAG, "Disconnected from GATT server.");
                }
            }
        }

        //发现服务的回调
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            //成功发现服务后可以调用相应方法得到该BLE设备的所有服务,并且打印每一个服务的UUID和每个服务下各个特征的UUID
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.e(TAG, "GATT_connect_succeed");
                //得到所有Service
                List<BluetoothGattService> supportedGattServices = gatt.getServices();
                 for (BluetoothGattService gattService : supportedGattServices) {
                    //TODO
                 }

                service = mGatt.getService(UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb"));
                if (service != null) {
                    BluetoothGattCharacteristic characteristicRead = service.getCharacteristic(UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb"));
                    if (characteristicRead != null) {
                        gatt.setCharacteristicNotification(characteristicRead, true);
                        BluetoothGattDescriptor descriptor = characteristicRead.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
                        descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                        gatt.writeDescriptor(descriptor);
                    }
                    BluetoothGattCharacteristic characteristicWrite = service.getCharacteristic(UUID.fromString("0000fff2-0000-1000-8000-00805f9b34fb"));
                    if (characteristicWrite != null ) {
                        mGatt.setCharacteristicNotification(characteristicWrite, true);
                        characteristicWrite.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
                    }
                }
            }
        }

        //写操作的回调
        public void onCharacteristicWrite(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic, int status) {
            Log.e(TAG, "onCharacteristicWrite");
        }

        //读操作的回调
        public void onCharacteristicRead(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.e(TAG, "读取成功" +characteristic.getValue());
                //注意!!!!!!!!你如果是通知的话使用的是下面的那个回调函数
            }
        }

        //数据返回的回调(此处接收BLE设备返回数据)
        public void onCharacteristicChanged(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic) {
            try {
                    receiveData = new String(characteristic.getValue(),"gbk");

                    Intent receiveIntent = new Intent(Constants.ACTION_RECEIVELE_DATA);
                    receiveIntent.putExtra("BT_DATA_REV",receiveData);
                    sendBroadcast(receiveIntent);
                    Log.e(TAG, "读取成功" + new String(characteristic.getValue(), "gbk"));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }
        }

        //if(mtu>20) boolean ret = mBluetoothGatt.requestMtu(mtu); Android5.0及以上引入的
        //这个方法在联接成功的BluetoothGattCallback:onConnectionStateChange() 联接成功的状态下调用
        @Override
        public void onMtuChanged(BluetoothDevice device, int mtu) {
            super.onMtuChanged(device, mtu);
            logd(String.format("onMtuChanged:mtu = %s", mtu));
            MTU = mtu - 3;
        }
    }

数据读写

读取数据:当我们发现服务之后就可以通过 BluetoothGatt#getService 获取 BluetoothGattService,接着通过 BluetoothGattService#getCharactristic 获取 BluetoothGattCharactristic。通过 BluetoothGattCharactristic#readCharacteristic 方法可以通知系统去读取特定的数据。如果系统读取到了蓝牙设备发送过来的数据就会调用 BluetoothGattCallback#onCharacteristicRead 方法。通过 BluetoothGattCharacteristic#getValue 可以读取到蓝牙设备的数据。

// 读取数据
BluetoothGattService service = gattt.getService(SERVICE_UUID);
BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(CHARACTER_UUID);
gatt.readCharacteristic();

写入数据:和读取数据一样,在执行写入数据前需要获取到 BluetoothGattCharactristic。接着执行一下步骤:

  • 调用 BluetoothGattCharactristic#setValue 传入需要写入的数据(蓝牙最多单次1支持 20 个字节数据的传输,如果需要传输的数据大于这一个字节则需要分包传输,Android 5.0之后可以设置更长)
  • 调用 BluetoothGattCharactristic#writeCharacteristic 方法通知系统异步往设备写入数据
  • 系统回调 BluetoothGattCallback#onCharacteristicWrite 方法通知数据已经完成写入

在我们需要发送数据的时候,通过UUID找到Characteristic,去设置其值,最后通过writeCharacteristic(characteristic)方法发送数据。

//往蓝牙数据通道的写入数据
BluetoothGattService service = gattt.getService(SERVICE_UUID);
BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(CHARACTER_UUID);
characteristic.setValue(sendValue);
gatt.writeCharacteristic(characteristic);

断开连接

当与设备完成通信之后之后一定要断开与设备的连接。调用以下方法断开与设备的连接:

mBluetoothGatt.disconnect();
mBluetoothGatt.close();

通俗的理解:获取到MAC地址对应的蓝牙设备mDevice = adapter.getRemoteDevice(mac)或者通过队列中取得的mDevice,这个mDevice就是我们的学校,学校有了,那就要找到通道,bluetoothGatt = mDevice.connectGatt(this,false,new BluetoothGattCallback() ),bluetoothGatt就是通道,BluetoothGattCallback()是回调类,里面有很多方法,寻找班级和学生就在它的方法里面去实现。里面有几个比较重要的方法,分别是onConnectionStateChange(),onServiceDiscovered(),onCharacteristicRead(),onCharacteristicChanged(),几个方法之间是独立的,同时又有联系的,过程是这样:连接通道时最先触发的是onConnectionStateChange()方法,在该方法里面启动服务发现后会触发onConnectionStateChange(),此方就是寻找班级和学生的重点方法;在通道通过UUID找到班级,在班级通过UUID找到学生,每个设备有多个班级UUID,每个班级对应多个学生UUID,所以你可以先打印出来这些信息,获取有用的班级UUID和学生UUID,找到了学生,就可以发出通知了,即把学生通知出去,上面说了学生只是一个通信的中介,此时会触发onCharacteristicChanged()方法,在该方法里面就可以获取到设备那边传过来的数据包了,另外onCharacteristicRead()方法被触发的前提是要在onServiceDiscovered()里面执行readCharacteristic()方法,然后就可以在onCharacteristicRead()里面做自己想要的操作了,整个过程就是这样的。

四、蓝牙操作的踩坑之路

  • Android 5.0 即API21及以上可以支持MoreData功能,支持发送大于20字节的数据而不需要分包,接口BluetoothGatt#requestMtu (int mtu)
  • Android 6.0 及以上需要定位权限,否则搜不到BLE设备,即android.permission.ACCESS_COARSE_LOCATION
  • 蓝牙的写入操作( 包括 Descriptor 的写入操作), 读取操作必须序列化进行,写入数据和读取数据是不能同时进行的, 如果调用了写入数据的方法, 马上又调用写入数据或者读取数据的方法,第二次调用的方法会立即返回 false, 代表当前无法进行操作
  • Android 连接外围设备的数量有限,当不需要连接蓝牙设备的时候,必须调用 BluetoothGatt#close 方法释放资源
  • 所有的蓝牙操作使用 Handler 固定在一条线程操作,这样能省去很多因为线程不同步导致的麻烦
  • 传递对象而不传递地址,起初采用Intent传递Bluetooth.getAddress()方式传递蓝牙的Mac地址,在ManagerActivity调用mBluetoothAdapter#getRemoteDevice()获取设备。实际使用中发现此函数比较耗时,影响效率。解决办法是使用Intent传递BluetoothDevice对象。BluetoothDevice实现了Parcelable接口,可以使用Intent直接传递
  • 手机端打开蓝牙、关闭蓝牙、开启搜索和停止搜索,数据发送等操作都需要一定的时间来完成,因此在执行以上操作后最好加上一段延时,等待对应操作完成后再去执行下一步

参考