Android BLE蓝牙通讯学习

Android BLE蓝牙通讯学习

app应用的开发过程中,一般和蓝牙接触的不多,但是随着智能穿戴设备的发展,穿戴设备和手机关联的app越来越多,之前也是没怎么接触过这一块的东西,正好最近需要做一个和蓝牙有关的app,所以研究学习下,把应用的东西总结一下。项目源码已经上传github。

介绍

BLEBluetooth Low Energy的缩写,又叫蓝牙4.0,区别于蓝牙3.0和之前的技术。BLE前身是NOKIA开发的Wibree技术,主要用于实现移动智能终端与周边配件之间的持续连接,是功耗极低的短距离无线通信技术,并且有效传输距离被提升到了100米以上,同时只需要一颗纽扣电池就可以工作数年之久。

BLE是在蓝牙技术的基础上发展起来的,既同于蓝牙,又区别于传统蓝牙。BLE设备分单模和双模两种,双模简称BR,商标为Bluetooth Smart Ready,单模简称BLE或者LE,商标为Bluetooth SmartAndroid是在4.3后才支持BLE,这可以解释不是所有蓝牙手机都支持BLE,而且支持BLE的蓝牙手机一般是双模的。

BLE和普通蓝牙之间是有区别的:

  • 优点:

    功耗更低

    连接速度更快

  • 缺点:

    每次发送的数据比较小

Android和BLE

一些概念

ATT

ATT(Attribute Protocol)协议是基础协议,ATT针对BLE设备进行了特别的优化,它的基础是属性,使用一个UUID来定义属性的类型。

扫描二维码关注公众号,回复: 1791314 查看本文章

GATT

GATT(Generic Attribute Profile)是所有BLE顶层协议的基础,它定义了怎么把一堆ATT属性分组成为有意义的服务。

services

服务,基础是UUID的值为0x2800的属性。所有跟在这个属性后面的属性都属于这个属性定义的服务,直到另一个0x2800属性出现。一个BLE设备可以有多个服务。

characteristics

特征,每一个服务都可以包含有多个特征,特征存储了有用的值以及权限。其中蓝牙模块和app进行通讯主要是通过它来进行。

descriptor

特征的描述,又叫描述符,在一个特征中会有多个描述符。GATT协议已经定义了大多数的标准描述符,这其中有一个特别重要的描述符是:client characteristic configurationUUID是0x2902,具有一个16bit的可读写值。被用来定义通知和暗示,通过设置可以能够让设备发送通知,并且被主机端接收到。

Notification

通知,BLE模块向空中发送消息,可以被主机端的蓝牙模块接收到。包含在characteristics中,但是需要权限打开。

BLE透传

透传模式下,所有的串口数据都被看做用户数据,模块会将这些数据通过蓝牙发送给主机,即是蓝牙模块和手机或者其他控制设备之间的通讯(其实还有另外一种命令模式,但是透传模式更快速和简单。),透传模式即是通过servicescharacteristics进行。一般厂家或者工程师会给出相应的说明,例如下图:

BLE透传说明

角色和职责

Android设备和BLE设备交互有两组角色:

中心设备(Central)和外围设备(Periphery)。外围设备是数据提供者,中央是数据使用/处理者。一个中央设备可以同时连接多个外围设备,但是一个外围设备同时只能连接一个中央设备。

Android 使用

准备权限

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

准备BLE

  • 获取BluetoothAdapter:
        BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        bluetoothAdapter = bluetoothManager.getAdapter();
        //打开蓝牙 方法一
        if (!bluetoothAdapter.isEnabled()) {
            bluetoothAdapter.enable();
        }
        //方法二 推荐
        Intent enabler = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enabler, REQUEST_ENABLE);
  • 判断是否支持BLE:
    private boolean checkBluetooth() {
        if (!getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_BLUETOOTH_LE)) {
            return false;
        }
        return true;
    }
  • 开始扫描设备:
bluetoothAdapter.startLeScan(mLeScanCallback);

BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    /*
                    显示了一个列表,点击进入具体设备页面 BLEDeviceTestActivity               
                    并且将设备的名称和地址传递过去
                     intent.putExtra(DeviceControlActivity.EXTRAS_DEVICE_NAME, device.getName());
                     intent.putExtra(DeviceControlActivity.EXTRAS_DEVICE_ADDRESS, device.getAddress());
                    */
                    adapter.add("name : " + device.getName() + "\n address : " + device.getAddress());
                    bluetoothDevices.add(device);
                }
            });
        }
    };

连接设备

BLEDeviceTestActivity中获取设备,并且连接设备:

//根据地址获取设备
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(addressStr);
//获取链接 这个时候需要实现BluetoothGattCallback
BluetoothGatt bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback);

有关device.connectGatt(this, false, bluetoothGattCallback);方法,可以看到它是通过设备建立一个GATT链接,任何有关GATT链接的操作都将触发回调。而BluetoothGattCallback会异步处理这些回调结果。

    /**
     * Connect to GATT Server hosted by this device. Caller acts as GATT client.
     * The callback is used to deliver results to Caller, such as connection status as well
     * as any further GATT client operations.
     * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
     * GATT client operations.
     * @param callback GATT callback handler that will receive asynchronous callbacks.
     * @param autoConnect Whether to directly connect to the remote device (false)
     *                    or to automatically connect as soon as the remote
     *                    device becomes available (true).
     * @throws IllegalArgumentException if callback is null
     */
    public BluetoothGatt connectGatt(Context context, boolean autoConnect,
                                     BluetoothGattCallback callback) {
        return (connectGatt(context, autoConnect,callback, TRANSPORT_AUTO));
    }

所以实现bluetoothGattCallback:

    BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
        /**
         * 返回链接状态
         * @param gatt
         * @param status 链接或者断开连接是否成功 {@link BluetoothGatt#GATT_SUCCESS}
         * @param newState 返回一个新的状态{@link BluetoothProfile#STATE_DISCONNECTED} or
         *                                  {@link BluetoothProfile#STATE_CONNECTED}
         */
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
        }

        /**
         *  获取到链接设备的GATT服务时的回调
         * @param gatt
         * @param status 成功返回{@link BluetoothGatt#GATT_SUCCESS}
         */
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);

        }

        /**
         * 读特征的时候的回调
         * @param gatt
         * @param characteristic 从相关设备上面读取到的特征值
         * @param status
         */
        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicRead(gatt, characteristic, status);
        }

        /**
         * 指定特征写入操作的回调结果
         * @param gatt
         * @param characteristic
         * @param status
         */
        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);
        }

        /**
         * 设备发出通知时会调用到该接口
         * @param gatt
         * @param characteristic
         */
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
        }

        /**
         * 指定描述符的读操作的回调
         * @param gatt
         * @param descriptor
         * @param status
         */
        @Override
        public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorRead(gatt, descriptor, status);
        }

        /**
         * 指定描述符的写操作
         * @param gatt
         * @param descriptor
         * @param status
         */
        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorWrite(gatt, descriptor, status);
        }

        /**
         * 当一个写入事物完成时的回调
         * @param gatt
         * @param 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);
        }

        @Override
        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
            super.onMtuChanged(gatt, mtu, status);
        }
    };

可以看到BluetoothGattCallback有很多方法,根据名字不难明白其意思,接着根据蓝牙的使用过程来一个一个的分析就能清晰明了其中功能。

因为我们前面进行了链接,所以首先触发的回调是onConnectionStateChange()方法:


         /**
         * 返回链接状态
         * @param gatt
         * @param status 链接或者断开连接是否成功 {@link BluetoothGatt#GATT_SUCCESS}即表示操作是否成功
         * @param newState 返回一个新的状态{@link BluetoothProfile#STATE_DISCONNECTED} or  即表示当前的状态
         *                                  {@link BluetoothProfile#STATE_CONNECTED}

         因此使用newState参数来做判断
         */
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                //连接成功  因为是异步调用的 所以刷新UI的操作要放在主线程中,当然也可以使用hanlder  Eventbus等 随便
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        connectTv.setText("连接成功");
                    }
                });
                //链接成功
                gatt.discoverServices();
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                //断开连接
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        connectTv.setText("连接断开");
                    }
                });
            }
        }

在上面的代码中发现当连接成功后调用了gatt.discoverServices();方法,这个方法是用来发现远程设备提供的服务,以及它们包含的特征特性和描述符等等。

因为根据前面的介绍可以知道,要想app和蓝牙模块进行通讯,需要通过这些服务和特征等。这个方法会触发回调onServicesDiscovered()

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
            if (BluetoothGatt.GATT_SUCCESS == status) {
                gatt.getServices();

                /*
一个GATT服务表现为一个 BluetoothGattService 对象,我们需要通过适当的UUID从 BluetoothGatt 实例中获得;
一个GATT特征表示为一个 BluetoothGattCharacteristic  对象,我们可以通过适当的UUID从 BluetoothGattService 中得到;
相当于一个数据类型,它包括一个value和0~n个value的描述(BluetoothGattDescriptor)
一个GATT描述符表现为一个 BluetoothGattDescriptor 对象,我们可以通过适当的UUID从BluetoothGattCharacteristic  对象中获得:
描述符,对Characteristic的描述,包括范围、计量单位等
                 */

                gattCharacteristicList.clear();
                String uuid = null;
                ArrayList<HashMap<String, String>> gattServiceData = new ArrayList<HashMap<String, String>>();
                ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData = new ArrayList<ArrayList<HashMap<String, String>>>();

                //获取服务
                for (BluetoothGattService gattService : gatt.getServices()) {
                    HashMap<String, String> currentServiceData = new HashMap<String, String>();
                    uuid = gattService.getUuid().toString();
                    currentServiceData.put("name",
                            SampleGattAttributes.lookup(uuid, "未知服务"));
                    currentServiceData.put("uuid", uuid);
                    gattServiceData.add(currentServiceData);

                    ArrayList<HashMap<String, String>> gattCharacteristicGroupData = new ArrayList<HashMap<String, String>>();
                    List<BluetoothGattCharacteristic> gattCharacteristics = gattService
                            .getCharacteristics();
                    ArrayList<BluetoothGattCharacteristic> charas = new ArrayList<BluetoothGattCharacteristic>();

                    // 获取每个服务中包含的特征
                    for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
                        charas.add(gattCharacteristic);
                        HashMap<String, String> currentCharaData = new HashMap<String, String>();
                        uuid = gattCharacteristic.getUuid().toString();
                        currentCharaData.put("name",
                                SampleGattAttributes.lookup(uuid, "未知特征"));
                        currentCharaData.put("uuid", uuid);

                        //当某个特征的UUID为0000ff02-0000-1000-8000-00805f9b34fb时 使能BLE透传模块通知功能
                        if (uuid.equals("0000ff02-0000-1000-8000-00805f9b34fb")) {
                            setCharacteristicNotification(gattCharacteristic, true);
                        }
                        gattCharacteristicGroupData.add(currentCharaData);
                    }   
                    gattCharacteristicList.add(charas);
                    gattCharacteristicData.add(gattCharacteristicGroupData);
                }

                // 用一个可折叠的列表来展示 这些服务和 特征 
                final SimpleExpandableListAdapter gattServiceAdapter = new SimpleExpandableListAdapter(
                        BLEDeviceTestActivity.this, gattServiceData,
                        android.R.layout.simple_expandable_list_item_2, new String[]{
                        "name", "uuid"}, new int[]{android.R.id.text1,
                        android.R.id.text2}, gattCharacteristicData,
                        android.R.layout.simple_expandable_list_item_2, new String[]{
                        "name", "uuid"}, new int[]{android.R.id.text1,
                        android.R.id.text2});
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        listView.setAdapter(gattServiceAdapter);
                    }
                });
            }
        }

上面一大堆代码,就是把获取到的服务和特征用一个列表展示出来并不是关键,因为根据前面的characteristic说明可以看到0xff02开头的特征值是使能通知,所以有下面代码:

 if (uuid.equals("0000ff02-0000-1000-8000-00805f9b34fb")) {
                            setCharacteristicNotification(gattCharacteristic, true);
                        }

setCharacteristicNotification()方法如下是启用一个指定特征的通知权限。这里被小小的坑了一下,本来以为bluetoothGatt.setCharacteristicNotification(characteristic, enabled);

设置true就够了的,但是调试的时候发现不能够接收到通知,查阅了资料才知道还需要给描述符设置通知权限启用才行,但是具体哪个描述符就不知道了,最后还是根据蓝牙模块厂家发来的demo反编译,查看了其源码才知道相应的UUID。而且对比发现此描述符的UUID就是以0x2902开头的,由此猜测这个此描述符应该是GATT协议中已经写死的一个描述符。


    public static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";   

    public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) {
        //仅仅有这一句是不够的
        bluetoothGatt.setCharacteristicNotification(characteristic, enabled);
        //需要为指定特征的特定的描述符设置启用才行
        BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID
                .fromString(CLIENT_CHARACTERISTIC_CONFIG));
        if (descriptor != null) {
            System.out.println("write descriptor");
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            bluetoothGatt.writeDescriptor(descriptor);
        }
    }

当设置了通知以后,如果BLE设备通过通知的方式发送数据的话,app端接到通知会触发onCharacteristicChanged()方法,此方法就可以用来接收BLE模块通过广播形式给返回的:

        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(BLEDeviceTestActivity.this, "接收到数据", Toast.LENGTH_SHORT).show();
                }
            });
            //以字节码数组的形式接收到数据
            final byte[] data = characteristic.getValue();
            if (data != null && data.length > 0) {
                final StringBuilder stringBuilder = new StringBuilder(
                        data.length);
                StringBuffer test = new StringBuffer();
                for (byte byteChar : data) {
                    test.append(byteChar);
                    stringBuilder.append(String.format("%02X ", byteChar));//以两位16进制输出 不足的补0
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        //数据展示
                        dataTv.setText(new String(data) + "\n"
                                + stringBuilder.toString());
                    }
                });
            }
        }

根据前面的介绍知道蓝牙的透传是通过characteristics进行的,所以当然还有读和写的操作,在进行操作之前需要先对服务的UUID进行判断

可以根据characteristic.getUuid()来得到UUID,或者在知道UUID的情况下,主动获取对应的特征进行操作。

进行读操作如下,触发回调函数onCharacteristicRead():

        //对相应的特征进行读操作
        bluetoothGatt.readCharacteristic(characteristic);

        //读取成功触发回调函数 
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicRead(gatt, characteristic, status);

            //读取到的数据存在characteristic当中,可以通过characteristic.getValue();函数取出。然后再进行解析操作。
            if (BluetoothGatt.GATT_SUCCESS == status) {
                final byte[] data = characteristic.getValue();
                ...
                //和通知一样也是通过字节码的形式传递数据 这里省略不写
            }

        }

同样既然有了读,也可以进行写的操作,写操作如下,会触发回调函数onCharacteristicWrite():

        //这里写入需要是字节码数组的形式来进行
        characteristic.setValue(byte[] value);
        bluetoothGatt.writeCharacteristic(characteristic);

        //这里没什么好说的,就是判断写入是否成功
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);
            if (BluetoothGatt.GATT_SUCCESS == status) {
                Log.d("BLEDeviceTestActivity", "写入成功");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(BLEDeviceTestActivity.this, "写入成功", Toast.LENGTH_SHORT).show();
                    }
                });
            }

如果写入成功,并且透传模块有应答,一般的应答方式是通过通知的方式进行的。所以一般在项目中在链接成功以后就要开启通知的权限,省的后面写入的时候忘记。至此,手机和BLE模块之间的通讯基本完成,其他的写入描述符这些回调过程大同小异。

当然,这只是简单的完成了通讯,真正使用的时候由于数据包大小的限制还需要分包传输,接受等等等等很多需要完善的地方。

猜你喜欢

转载自blog.csdn.net/u011494285/article/details/52414583