android蓝牙BLE(二) —— 通信

一、蓝牙基础协议

想了解蓝牙通信之前,需要先了解蓝牙两个最基本的协议:GAP 和 GATT

GAP(Generic Access Profile)简介

​ GAP是通用访问配置文件的首字母缩写,主要控制蓝牙连接和广播。GAP使蓝牙设备对外界可见,并决定设备是否可以或者怎样与其他设备进行交互。

GAP定义了多种角色,但主要的两个是:中心设备 和 外围设备。

  • 中心设备:可以扫描并连接多个外围设备,从外设中获取信息。

  • 外围设备:小型,低功耗,资源有限的设备。可以连接到功能更强大的中心设备,并为其提供数据。

GAP广播数据

GAP 中外围设备通过两种方式向外广播数据:广播数据 和 扫描回复( 每种数据最长可以包含 31 byte。)。

​ 广播数据是必需的,因为外设必需不停的向外广播,让中心设备知道它的存在。而扫描回复是可选的,中心设备可以向外设请求扫描回复,这里包含一些设备额外的信息。

image

外围设备会设定一个广播间隔。每个广播间隔中,它会重新发送自己的广播数据。广播间隔越长,越省电,同时也不太容易扫描到。

广播的网络拓扑结构

​ 外设通过广播自己让中心设备发现自己,并建立 GATT 连接,从而进行更多的数据交换。但有些情况是不需要连接的,只要外设广播自己的数据即可。目的是让外围设备,把自己的信息发送给多个中心设备。因为基于 GATT 连接的方式的,只能是一个外设连接一个中心设备。


image

GATT(Generic Attribute Profile)简介

​ GATT配置文件是一个通用规范,用于在BLE链路上发送和接收被称为“属性”的数据块。目前所有的BLE应用都基于GATT。

BLE设备通过叫做 ServiceCharacteristic 的东西进行通信

​ GATT使用了 ATT(Attribute Protocol)协议,ATT 协议把 Service, Characteristic对应的数据保存在一个查询表中,次查找表使用 16 bit ID 作为每一项的索引。

​ GATT 连接是独占的。也就是一个 BLE 外设同时只能被一个中心设备连接。一旦外设被连接,它就会马上停止广播,这样它就对其他设备不可见了。当外设与中心设备断开,外设又开始广播,让其他中心设备感知该外设的存在。而中心设备可同时与多个外设进行连接。

image

GATT 通信

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

​ GATT 通信的双方是 C/S 关系。外设作为 GATT 服务端(Server),它维持了 ATT 的查找表以及 service 和 characteristic 的定义。中心设备是 GATT 客户端(Client),它向 外设(Server) 发起请求来获取数据。

GATT 结构

image
  • Profile:并不是实际存在于 BLE 外设上的,它只是一个被 Bluetooth SIG 或者外设设计者预先定义的 Service 的集合。例如心率ProfileHeart Rate Profile)就是结合了 Heart Rate ServiceDevice Information Service

  • Service:包含一个或者多个 Characteristic。每个 Service 有一个 UUID 唯一标识。

  • Characteristic: 是最小的逻辑数据单元。一个Characteristic包括一个单一value变量和0-n个用来描述characteristic变量的Descriptor。与 Service 类似,每个 Characteristic 用 16 bit 或者 128 bit 的 UUID 唯一标识。

实际开发中,和 BLE 外设打交道,主要是通过 Characteristic。可以从 Characteristic 读取数据,也可以往 Characteristic 写数据,从而实现双向的通信。

    UUID 有 16 bit 、32bit 和 128 bit 的。16 bit 的 UUID 是官方通过认证的,需要花钱购买。

Bluetooth_Base_UUID定义为 00000000-0000-1000-8000-00805F9B34FB

  • 若16 bit UUID为xxxx,转换为128 bit UUID为0000xxxx-0000-1000-8000-00805F9B34FB
  • 若32 bit UUID为xxxxxxxx,转换为128 bit UUID为xxxxxxxx-0000-1000-8000-00805F9B34FB

二、中心设备与外设通讯

简单介绍BLE开发当中各种主要类和其作用:

  • BluetoothDeivce:蓝牙设备,代表一个具体的蓝牙外设。
  • BluetoothGatt:通用属性协议,定义了BLE通讯的基本规则和操作
  • BluetoothGattCallback:GATT通信回调类,用于回调的各种状态和结果。
  • BluetoothGattService:服务,由零或多个特征组构成。
  • BluetoothGattCharacteristic:特征,里面包含了一组或多组数据,是GATT通信中的最小数据单元。
    BluetoothGattDescriptor:特征描述符,对特征的额外描述,包括但不仅限于特征的单位,属性等。

获取蓝牙设备对象

对扫描到的蓝牙可以用集合形式进行缓存,也可只保存其mac地址,存储到字符集合中,用于后续的连接。

根据mac地址获取到BluetoothDeivce用于连接

BluetoothManager bluetoothmanager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
 mBluetoothAdapter = bluetoothmanager.getAdapter();
 //获取蓝牙设备对象进行连接
mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(macAddressStr)

蓝牙gatt回调

实现BluetoothGattCallBack类,监听蓝牙连接过程中各种回调的监听。蓝牙Gatt回调方法中都不应该进行耗时操作,需要将其方法内进行的操作丢进另一个线程,尽快返回。

//定义子线程handle,用于在BluetoothGattCallback中回调方法中的操作抛到该线程工作。
private Handler mHandler;
//定义handler工作的子线程
private HandlerThread mHandlerThread;

初始化handler
mHandlerThread = new HandlerThread("daqi");
mHandlerThread.start();
//将handler绑定到子线程中
mHandler = new Handler(mHandlerThread.getLooper());


//定义蓝牙Gatt回调类
public class daqiBluetoothGattCallback extends BluetoothGattCallback{
        //连接状态回调
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            // status 用于返回操作是否成功,会返回异常码。
            // newState 返回连接状态,如BluetoothProfile#STATE_DISCONNECTED、BluetoothProfile#STATE_CONNECTED
            
            //操作成功的情况下
            if (status == BluetoothGatt.GATT_SUCCESS){
                //判断是否连接码
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                
                }else if(newState == BluetoothProfile.STATE_DISCONNECTED){
                    //判断是否断开连接码
                    
                }
            }else{
                //异常码
                
            }
        }
        
        //服务发现回调
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, 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 onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorWrite(gatt, descriptor, status);
        }
    }

连接设备

​ 调用BluetoothDevice#connectGatt()进行ble连接,第二个参数默认选择false,不自动连接。并定义BluetoothGatt变量,存储BluetoothDevice#connectGatt()返回的对象。

image

//定义Gatt实现类
private BluetoothGatt mBluetoothGatt; 

//创建Gatt回调
private BluetoothGattCallback mGattCallback = new daqiBluetoothGattCallback();
//连接设备
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    mBluetoothGatt = mBluetoothDevice.connectGatt(mContext,
            false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
} else {
    mBluetoothGatt = mBluetoothDevice.connectGatt(mContext, false, mGattCallback);
}
连接异常处理

​ 蓝牙连接时,不一定百分百连接成功。连接出错时,会返回异常码进行错误描述。对于大多数异常码,可以通过重连来达到连接成功的目的。

错误代码:

  • 133 :连接超时或未找到设备。
  • 8 : 设备超出范围
  • 22 :表示本地设备终止了连接
//定义重连次数
private int reConnectionNum = 0;
//最多重连次数
private int maxConnectionNum = 3;

public class daqiBluetoothGattCallback extends BluetoothGattCallback{

    //连接状态回调
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        // status 用于返回操作是否成功,会返回异常码。
        //操作成功的情况下
        if (status == BluetoothGatt.GATT_SUCCESS){
            
        }else{
            //重连次数不大于最大重连次数
            if(reConnectionNum < maxConnectionNum){
                //重连次数自增
                reConnectionNum++
                //连接设备
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    mBluetoothGatt = mBluetoothDevice.connectGatt(mContext,
                            false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
                } else {
                    mBluetoothGatt = mBluetoothDevice.connectGatt(mContext, false, mGattCallback);
                }
            }else{
                //断开连接,返回连接失败回调
                
            }
        }
    }
    
    //其他回调方法
}

发现服务

连接成功后,触发BluetoothGattCallback#onConnectionStateChange()方法。

public class daqiBluetoothGattCallback extends BluetoothGattCallback{

    //连接状态回调
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        // status 用于返回操作是否成功,会返回异常码。
        //操作成功的情况下
        if (status == BluetoothGatt.GATT_SUCCESS){
            //判断是否连接码
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    //可延迟发现服务,也可不延迟
                    mHandler.post(() ->
                        //发现服务
                        mBluetoothGatt.discoverServices();
                    );
                }else if(newState == BluetoothProfile.STATE_DISCONNECTED){
                    //判断是否断开连接码
                    
                }
        }
    }
    
    //其他回调方法
}

当发现服务成功后,会触发BluetoothGattCallback#onServicesDiscovered()回调:

//定义需要进行通信的ServiceUUID
private UUID mServiceUUID = UUID.fromString("0000xxxx-0000-1000-8000-00805f9b34fb");

//定义蓝牙Gatt回调类
public class daqiBluetoothGattCallback extends BluetoothGattCallback{

    //服务发现回调
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            mHandler.post(() ->
                //获取指定uuid的service
                BluetoothGattService gattService = mBluetoothGatt.getService(mServiceUUID);
                //获取到特定的服务不为空
                if(gattService != null){
                    
                }else{
                    //获取特定服务失败
                    
                }
            );
        }
    }
}
发现服务失败

​ 发现服务时,会存在发现不了特定服务的情况。或者说,整个BluetoothGatt对象中的服务列表为空。
BluetoothGatt类中存在一个隐藏的方法refresh(),用于刷新Gatt的服务列表。当发现不了服务时,可以通过反射去调用该方法,再发现一遍服务。

读取和修改特征值

//定义需要进行通信的ServiceUUID
private UUID mServiceUUID = UUID.fromString("0000xxxx-0000-1000-8000-00805f9b34fb");
//定义需要进行通信的CharacteristicUUID
private UUID mCharacteristicUUID = UUID.fromString("0000yyyy-0000-1000-8000-00805f9b34fb");


//定义蓝牙Gatt回调类
public class daqiBluetoothGattCallback extends BluetoothGattCallback{

    //服务发现回调
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            mHandler.post(() ->
                //获取指定uuid的service
                BluetoothGattService gattService = mBluetoothGatt.getService(mServiceUUID);
                //获取到特定的服务不为空
                if(gattService != null){
                    //获取指定uuid的Characteristic
                    BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(mCharacteristicUUID);
                    //获取特定特征成功
                    if(gattCharacteristic != null){
                        //写入你需要传递给外设的特征值(即传递给外设的信息)
                        gattCharacteristic.setValue(bytes);
                        //通过GATt实体类将,特征值写入到外设中。
                        mBluetoothGatt.writeCharacteristic(gattCharacteristic);
                        
                        //如果只是需要读取外设的特征值:
                        //通过Gatt对象读取特定特征(Characteristic)的特征值
                        mBluetoothGatt.readCharacteristic(gattCharacteristic);
                    }
                }else{
                    //获取特定服务失败
                    
                }
            );
        }
    }
}

当成功读取特征值时,会触发BluetoothGattCallback#onCharacteristicRead()回调。

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    super.onCharacteristicRead(gatt, characteristic, status);
    if (status == BluetoothGatt.GATT_SUCCESS) {
        //获取读取到的特征值
        characteristic.getValue()
    }
}

当成功写入特征值到外设时,会触发BluetoothGattCallback#onCharacteristicWrite()回调。

@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    super.onCharacteristicWrite(gatt, characteristic, status);
    if (status == BluetoothGatt.GATT_SUCCESS) {
        //获取写入到外设的特征值
        characteristic.getValue()
    }
}

监听外设特征值改变

​ 无论是对外设写入新值,还是读取外设特定Characteristic的值,其实都只是单方通信。如果需要双向通信,可以在BluetoothGattCallback#onServicesDiscovered中对某个特征值设置监听(前提是该Characteristic具有NOTIFY属性):

//设置订阅notificationGattCharacteristic值改变的通知
mBluetoothGatt.setCharacteristicNotification(notificationGattCharacteristic, true);
//获取其对应的通知Descriptor
BluetoothGattDescriptor descriptor = notificationGattCharacteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (descriptor != null){ 
    //设置通知值
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
    boolean descriptorResult = mBluetoothGatt.writeDescriptor(descriptor);
}

​ 当写入完特征值后,外设修改自己的特征值进行回复时,手机端会触发BluetoothGattCallback#onCharacteristicChanged()方法,获取到外设回复的值,从而实现双向通信。

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
     if (status == BluetoothGatt.GATT_SUCCESS) {
        //获取外设修改的特征值
        String value = characteristic.getValue()
        //对特征值进行解析
        
    }
}

断开连接

断开连接的操作分为两步:

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

​ 调用disconnect()后,会触发手机会触发BluetoothGattCallback#onConnectionStateChange()的回调,回调断开连接信息,newState = BluetoothProfile.STATE_DISCONNECTED。但调用完disconnect()紧接着马上调用close(),会终止BluetoothGattCallback#onConnectionStateChange()的回调。可以看情况将两个进行拆分调用,来实现断开连接,但必须两个方法都调用。

例如:
需要在外设修改特征值触发BluetoothGattCallback#onCharacteristicChanged()时,断开连接。可以先在BluetoothGattCallback#onCharacteristicChanged()中调用disconnect(),并等调用BluetoothGattCallback#onConnectionStateChange()回调,返回断开连接信息后,再调用close()对Gatt资源进行关闭。

当和外设进行ble通信时,如出现任何意外情况,马上调用断开连接操作。

android BLE系列:

android蓝牙BLE(一) —— 扫描

android蓝牙BLE(二) —— 通信

android蓝牙BLE(三) —— 广播

android蓝牙BLE(四) —— 实战

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