Android BLE4.0(蓝牙通信)

前言:

本文参考文献:
1、https://learn.adafruit.com/introduction-to-bluetooth-low-energy?view=all
中文翻译:
http://www.race604.com/gatt-profile-intro/
2、http://blog.csdn.net/jimoduwu/article/details/21604215

一、工作原理

在说蓝牙通信之前,我们先了解一下蓝牙的工作原理。

蓝牙规定每一对设备进行通讯时,必须一个为主角色,另一为从角色,必须由主端进行查找,发起配对。连接成功,双方才可收发数据。

一个具备蓝牙功能的设备,可以在两个角色间切换,平时处于从模式,等待其它主设备来连接,需要时,转换为主模式,向其它设备发起呼叫。一个蓝牙设备以主模式发起连接时,需要知道对方的蓝牙地址,配对密码等信息。

二、相关名词和概念

在介绍名词概念之前,我们应该知道,现在的BLE连接都是建立在 GATT (Generic Attribute Profile) 协议之上。GATT 是一个在蓝牙连接之后,发送和接收很短的数据段的通用规范,这些很短的数据段被称为属性(Attribute)。

GAP

介绍 GATT 之前,需要了解 GAP (Generic Access Profile),它用来控制设备连接和广播。GAP 使一个设备可被其他设备发现,并决定了该设备是否可以或者怎样与其他设备进行交互。

GAP 给设备定义了若干角色,其中主要的两个是:

  • 外围设备(Peripheral):一般是非常小或者简单的低功耗设备,用来提供数据,并能够连接到一个更加相对强大的中心设备。如蓝牙手环。
  • 中心设备(Central):相对比较强大,用来连接其他外围设备。如手机。

GATT

GATT 的全名是 Generic Attribute Profile(普通属性协议),它定义两个 BLE 设备通过叫做 Service 和 Characteristic 的东西进行通信。

中心设备和外设需要双向通信的话,唯一的方式就是建立 GATT 连接。

GATT 结构

GATT 事务是建立在嵌套的Profiles、Services 和 Characteristics之上的,如图:

Paste_Image.png
  • Profile: Profile 并不是实际存在于 BLE 外设上的,它只是一个被 Bluetooth SIG 或者外设设计者预先定义的 Service 的集合。

  • Service: Service 是把数据分成一个个独立逻辑项,它包含一个或者多个 Characteristic。每个 Service 有一个 UUID 唯一标识。

  • Characteristic: Characteristic在 GATT 事务中是最低级别的,是最小的逻辑数据单元,与 Service 类似,每个 Characteristic 也有一个UUID 唯一标识。

三、蓝牙通信

1、对应Android的API

在Android BLE的API中,对应于GATT结构的有3个文件:

  • ** BluetoothGatt**
    作为中央来使用和处理数据,通过device.connectGatt(this, false, mGattCallback) 得到。

  • ** BluetoothGattServer**
    作为周边来提供数据,通过BluetoothGatt.getService(uuid)获取指定的BluetoothGattServer。

  • ** BluetoothGattCharacteristic**
    周边服务的一些特性,分为Read,Write,notification。

2、获取蓝牙连接状态

2.1 BluetoothGattCallback

在上一篇中介绍了如何连接一个设备:

device.connectGatt(this, false, mGattCallback) 

其中有一个BluetoothGattCallback 的一个实例:mGattCallback。

BluetoothGattCallback是返回中央的状态和周边提供的数据的一个重要的抽象类。看一下源码:


public abstract class BluetoothGattCallback {

    /**
     * 当GATT客户端已连接到GATT服务器或者从GATT服务器断开连接
     * 时回调。
     *
     * @param gatt 
     * @param status 连接或断开操作的状态。BluetoothGatt.GATT_SUCCESS表示操作成功
     *  
     * @param newState 返回新的连接状态。 如 BluetoothProfile.STATE_DISCONNECTED或
      * BluetoothProfile.STATE_CONNECTED
     */
    public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                        int newState) {
    }

    /**
     * 当远程设备的远程服务列表,特征和描述符已被更新,即已发现新服务时,调用回调。
     *
     * @param gatt 
     * @param status BluetoothGatt.GATT_SUCCESS 远程设备的远程服务列表可被发现
     */
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    }

    /**
     * Read特性的操作回调
     *
     * @param gatt 
     * @param characteristic 从相关的远程设备读取的特性。
     * @param status BluetoothGatt.GATT_SUCCESS 操作成功
     */
    public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
                                     int status) {
    }

    /**
     * write 特性的操作回调
     *
     * 如果在可靠的写事务正在进行时调用此回调,则characteristic的值表示远程设备报告的值。
     * 应用程序应该将此值与要写入的所需值进行比较。
     * 如果值不匹配,应用程序必须中止可靠的写事务。
     *
     * @param gatt 
     * @param characteristic 
     * @param status write 操作的结果
     *               {BluetoothGatt.GATT_SUCCESS}
     */
    public void onCharacteristicWrite(BluetoothGatt gatt,
                                      BluetoothGattCharacteristic characteristic, int status) {
    }

    /**
     * notification 特性的结果
     *
     * @param gatt 
     * @param characteristic 由于远程通知事件而更新的特性。
     */
    public void onCharacteristicChanged(BluetoothGatt gatt,
                                        BluetoothGattCharacteristic characteristic) {
    }

    /**
     * 报告描述符读操作的结果的回调.
     *
     * @param gatt 
     * @param descriptor 从关联的远程设备读取的描述符。
     * @param status 如果读操作已成功完成 {BluetoothGatt.GATT_SUCCESS} 
     */
    public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
                                 int status) {
    }

    /**
     * 指示描述符写操作的结果。
     *
     * @param gatt 
     * @param descriptor 写入相关远程设备的描述符
     * @param status 如果操作成功,写入操作的结果为{BluetoothGatt#GATT_SUCCESS}。
     */
    public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
                                  int status) {
    }

    /**
     * 当可靠的写事务已完成时调用回调。
     *
     * @param gatt 
     * @param status 如果可靠的写事务已成功执行,则{BluetoothGatt#GATT_SUCCESS}
     */
    public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
    }

    /**
     * 报告远程设备连接的RSSI。
     *
     * 此回调是响应{BluetoothGatt#readRemoteRssi}函数而触发的。
     *
     * @param gatt 
     * @param rssi 远程设备的RSSI值
     * @param status 如果RSSI已成功读取 {BluetoothGatt#GATT_SUCCESS}
     */
    public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
    }

    /**
     * 指示给定设备连接的MTU已更改。
     *
     * 此回调是响应{BluetoothGatt#requestMtu}函数或响应连接事件而触发的。
     *
     * @param gatt 
     * @param mtu 新的MTU大小
     * @param status 如果MTU已成功更改{ BluetoothGatt#GATT_SUCCESS}
     */
    public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
    }
}

从上面的代码可以看出,当我们与一个外设建立连接时,连接的状态以及连接成功之后与设备通信都会在BluetoothGattCallback中回调过来。

3、与设备的通信

3.1、BluetoothGatt.discoverServices()

此方法在成功连接到远程设备时调用,不调用此方法,无法与远程设备进行后续的通信。

@Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            Log.i(TAG, "Connected to GATT server.");
            mBluetoothGatt.discoverServices();
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            Log.i(TAG, "Disconnected from GATT server.");
        }
    }

但是这个方法是异步操作,在回调函数onServicesDiscovered中得到status,通过判断status是否等于BluetoothGatt.GATT_SUCCESS来判断查找Service是否成功

@Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            //服务发现成功
        } else {
            //服务发现失败
        }
    }

只有当Service可被发现是即status为BluetoothGatt.GATT_SUCCESS时,我们才能继续后续的操作。

3.2、启用notification

如果设备主动发信息,可以通过notification的方式,这种方式不用去轮询地读设备上的数据。

        BluetoothGattService service = bluetoothGatt.getService(serviceUuid);
        if (service == null) {
            return;
        }

        BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicUuid);
        if (characteristic == null) {
            return;
        }

        bluetoothGatt.setCharacteristicNotification(characteristic, true);//激活通知

        BluetoothGattDescriptor descriptor = characteristic.getDescriptor(descriptorUuid);
        descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        bluetoothGatt.writeDescriptor(descriptor);

如果notificaiton方式对于某个Characteristic是enable的,那么当设备上的这个Characteristic改变时,手机上的onCharacteristicChanged()回调就会被促发。


        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);

            byte[] notice = characteristic.getValue();

            onNotify(notice);
        }

3.3、发送数据

BluetoothGattService service = bluetoothGatt.getService(serviceUuid);
    if (service == null) {
        return;
    }
    BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicUuid);
    if (characteristic == null) {
        return;
    }
    characteristic.setValue(data);
    bluetoothGatt.writeCharacteristic(characteristic);
}

3.4、读取数据


    public void readData(UUID serviceUuid, UUID characteristicUuid) {
        BluetoothGattService service = bluetoothGatt.getService(serviceUuid);
        if (service == null) {
            return;
        }
        BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicUuid);
        if (characteristic == null) {
            return;
        }
        bluetoothGatt.readCharacteristic(characteristic);
    }

官方建议在进行read是如果有通知服务进行中,先关闭

bluetoothGatt.setCharacteristicNotification(characteristic, false);

读取的数据会在onCharacteristicRead()回调中返回

@Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         int status) {
            //读取到值,在这里读数据
            if (status == BluetoothGatt.GATT_SUCCESS) {
            }
        }

4、断开连接

public void disConnection() {
    if (bluetoothGatt == null) {
        return;
    }
    bluetoothGatt.disconnect();
}

5、关闭Gatt

使用给定的BLE设备后,应用程序必须调用此方法以确保资源正确释放。

    /**
     *
     */
    public void close() {
        if (mBluetoothGatt == null) {
            return;
        }
        mBluetoothGatt.close();
        mBluetoothGatt = null;
    }

总结:

关于蓝牙BLE的开发流程,到这里就结束了,从扫描、连接到与设备的收发数据,基本上也就这些东西。

最后加上GitHub上面的仓库:https://github.com/eson-yunfei/AndroidBle ;项目刚创建不久,还在完善,欢迎大家Fork。

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

推荐阅读更多精彩内容