前言:如果你是刚开始接触android关于低功耗(ble)蓝牙的开发,还是应该花点是时间了解一下BLE协议,因为哪怕你把蓝牙ble协议梳理个一知半解,那么开发就只剩下调用API了...
为了快速编辑,本文多处直接拷贝了 https://www.cnblogs.com/iini/p/12334646.html https://www.cnblogs.com/iini/p/8969828.html 两篇博文,想更多了解蓝牙ble原理的可以去看原文,如果你是新手,
首先我们需要了解基于ble协议的蓝牙设备之间交互的模式是啥?BLE采用了client/server (C/S) 架构来进行数据交互,C/S架构是一种非常常见的架构,蓝牙设备间总是一个充当客户端,一个充当服务端。比如蓝牙体温计,它可以提供 “体温” 数据服务,因此是一个server,而手机则可以请求“体温”数据以显示在手机上,因此手机是一个client。
我们把上面的例子抽象一下,是不是可以理解成,蓝牙设备的服务端其实就是一个“数据库”,这个数据库保存体温数据。手机通过蓝牙(ble)协议去和这个“数据库交互”,获取自己想获取的数据。其实没错了,每个蓝牙设备就是用来提供服务的,而服务就是众多数据的合集,这个合集可以称为数据库,数据库里面每个条目都是一个attribute,或者说每条数据就是一个attribute。所以在这里我把attribute翻译成数据条目。大家可以把一个蓝牙设备想象成一个表格,表格里面每一行就是一个attribute。
1.attribute
attribute可以用下图来表示:
1.Attribute handle,Attribute句柄,16-bit长度。Client要访问Server的Attribute,都是通过这个句柄来访问的,也就是说ATT PDU一般都包含handle的值。用户在软件代码添加characteristic的时候,系统会自动按顺序地为相关attribute生成句柄。
2.Attribute type,Attribute类型,2字节或者16字节长。在BLE中我们使用UUID来定义数据的类型,UUID是128 bit的,所以我们有足够的UUID来表达万事万物。其中有一个UUID非常特殊,它被蓝牙联盟采用为官方UUID,
由于这个UUID众所周知,蓝牙联盟将自己定义的attribute或者数据只用16bit UUID来表示,比如0x1234,其实它也是128bit,完整表示为:
这个Attribute type 非常重要,我们在应用开发的时候正是通过这个UUID来指定我们需要获取的数值,像数据库的id一样,这个后面再说。
3.Attribute value,就是数据真正的值,0到512字节长。
4.Attribute permissions,Attribute的权限属性,权限属性不会直接在空口包中体现,而是隐含在ATT命令的操作结果中。假设一个attribute read属性设为open(即读操作不需要任何权限),那么client去读这个attribute时server将直接返回attribute的值;如果这个attribute read属性设为authentication(即需要配对才能访问),如果client没有与server配对而直接去访问这个attribute,那么server会返回一个错误码:告诉client你的权限不够,此时client会对server发起配对请求,以满足这个attribute的读属性要求,从而在第二次读操作时server将把相应的数据返回给client。目前主要有如下四种权限属性:
一个应用所有的attribute组成一个database,也称为attribute table,一个attribute table示例如下所示:
好了,现在数据库中的数据有了,那现在是不是该轮到“SQL语句”上场了?当然!我们可以通过一套指令去访问蓝牙,然后获取需要的数据,就像“select”一样,我们查询某条数据肯定需要一个“where”判断的值,在这里,“where”判断的值就是上面所提到的 Attribute value UUID。指令就是下面要说的ATT 命令。
2.ATT
ATT,全称attribute protocol(数据交互协议),ATT是由一群ATT命令组成,通过这一群命令就可以操作蓝牙attribute数据表,ATT也是蓝牙空口包中的最上层,也就是说,ATT就是大家对蓝牙数据包进行分析的最多的地方。
ATT命令,正式称谓ATT PDU(Protocol Data Unit,协议数据交互单元)包括4类:读,写,notify(通知)和indicate(指示)。这些命令又可以分成两种:如果它需要response,那么会在相应命令后面加上request;相反,如果它只需要ACK而不需要response,那么它的后面就不会带request。这里要特别强调一点,ATT所有命令都是“必达”的,也就是说每个命令发出去之后,会立马等ACK信息,如果收到了ACK包,发送方认为命令完成;否则发送方会一直重传该命令直到超时导致BLE连接断开。换句话说,只要你的BLE连接没有断开,那么你之前发送的数据包,不管它是用什么ATT PDU来发送的,它肯定被对方收到了。我估计很多人对此会产生疑问,因为他们经常碰到丢包的情况,其实大家经常碰到的“丢包”,不是空中把包丢了或者包在空中被干扰了,而是大家发送的代码写得有问题,导致你要发送的包没有被安全送达到协议栈射频FIFO中,从而出现所谓的“丢包”。以后大家碰到丢包情况,请先检查你的代码,保证你的数据包正确完整安全地送达到协议栈射频FIFO中,只要数据包放到了协议栈射频FIFO中,蓝牙协议栈就能保证该数据包“必达”对方。既然每个ATT命令都必达对方,那么还需要request类型的命令做什么?如果一个命令带有request后缀,那么发起方就可以收到命令的response包,这个response包在应用层是有回调事件的,而前述的ACK包在应用层是没有回调事件的。换句话说,不带request的命令,虽然协议栈底层确保了该命令必达对方,但应用层其实并不知道(私有实现方法除外),当你需要实现一个通信序列的时候,这种命令就显得不足了。而采用request/response方式的命令对,request命令发出去之后,必须等到相应的response命令回复才能进行下一步操作,比如发送下一个request命令,这样应用层可以严格按照规定逻辑执行一系列的操作,这个在很多应用场合是非常有用的。Request/response命令对还有一个副作用:大大降低通信的有效速率(吞吐率),因为request/response命令必须在不同的连接间隔中出现,也就是说,你在间隔1中发送了一个request命令,那么response包必须在间隔2或者稍后间隔中回复,而不能在间隔1中回复,这就导一个数据包的发送需要跨两个连接间隔甚至更多。而不带request后缀的ATT命令就没有这个限制,ACK可以在同一个连接间隔中回复,这样一个连接间隔中可以同时发出多个数据包,这样将大大提高通信速率。大家可以参考下图来理解request和非request命令的区别:
注:第1个连接间隔中的蓝色包为request命令,旁边的灰色包是该request的ACK;第2个连接间隔的绿色包是response包,而它的ACK是第3个连接间隔中的蓝色包
注:图中的绿色包就是非request命令,而紧随其后的灰色包就是它的ACK
不带request的命令只有2个:write command和notification,其余的命令都是带request:所有 read命令,所有write 命令,find命令以及indicate命令,完整的ATT命令(ATT PDU)列表如下所示:
到此,蓝牙的数据有Attribute,对数据的操作有ATT数据交互协议,有数据有协议好像蓝牙(ble)协议的介绍应该到此完结撒花了,其实不然,我们在应该层面的开发其实很少直接接触ATT,前文介绍这么多,其实是想让大家很好的理解蓝牙的工作原理,试想如果我们在应该层面的直接对每条“冰冷的”attribute进行操作,那么开发将会繁琐、枯燥、易错、难兼容、效率低...接下来应用层开发的重头戏GATT即将登场。
3.GATT
如果说ATT层定义了一个通信的基本框架,数据的基本结构,以及通信的指令,而GATT层用来赋予每个数据一个具体的内涵,让数据变得有结构和意义。换句话说,没有GATT层,低功耗蓝牙也可以通信起来,但会产生兼容性问题以及通信的低效率。
其实GATT就是一个应用层开发的规范(profile),使遵守这个规范的任意两个蓝牙设备交互的过程中,相互“理解”。举个例子: 前面说过,蓝牙设备就是一个数据表,表中attribute是一条条数据,假如有一条数据为 “37” ,有可能是说体温“37度”,也有可能是说心率“37次”或者湿度“37%”,这可咋搞?当然我们可以提前约定某个数据的UUID表示温度,某个数据UUID表示体温...但是这给蓝牙应用的开发带来了很多繁琐且不稳定的东西。 因此必须对数据进行分类和定义,这就是GATT的存在价值。
蓝牙开发联盟在GATT层根据蓝牙数据的具体含义把一条条attribute 归纳归类规范化,定义成service和characteristic的形式:
在GATT规范中,每一个具体的蓝牙应用是由多个service组成的,而每一个service又是由多个characteristic组成的。service/characteristic是attribute的逻辑表现形式,而attribute是service/characteristic具体实现方式。尤其要注意的是,一条characteristic不是对应一条attribute,而是由多条attribute组成。虽然一个数据最有价值的部分是它的值(value)。
一个characteristic包含三种类型的数据条目(attribute):characteristic声明条目(declaration attribute),characteristic值条目(value attribute)以及characteristic描述符条目(descriptor attribute)(一个characteristic可以有多个描述符条目),如下所示:
characteristic declaration就是每个characteristic的分界符,解析时一旦遇到characteristic declaration,就可以认为接下来又是一个新的characteristic了,同时characteristic declaration还将包含value attribute的读写属性等。Characteristic value就是数据的值了,它也是一个单独的attribute,这个比较好理解就不再说了。Characteristic descriptor就是数据的额外信息,比如温度的单位是什么,数据是用小数表示还是百分比表示等之类的数据描述信息。Descriptor属于可选条目,也就是说,一个characteristic可以不包含任何一条descriptor。这里着重提一种特殊的descriptor:CCCD。一般而言,都是client来访问server的characteristic,即通过ATT读或者写PDU访问相关数据。如果server想直接把自己的characteristic的值告诉client,就需要通过notify或者indicate PDU,跟其他PDU相比,这2个PDU是由server自己决定什么时候开始传送,而不是被动接受client的命令请求。但client毕竟是客户啊,它得有自主权,所以引入了一个CCCD来帮助client控制server的行为。client可以通过禁止CCCD以不接收 notify或者indicate命令,client也可以通过使能CCCD以允许notify或者indicate命令。重新总结一下,当CCCD使能的情况下,server可以随时notify或者indicate数据给client;当CCCD禁止的时候,哪怕server有数据,它也不能notify或者indicate给client。这里还是比较重要的,在应用层开发需要设置这个属性,后面再说。这里强调一下,当characteristic具有notify或者indicate操作功能时,蓝牙规范要求必须为其添加CCCD attribute。
我们就可以把上面的attribute table进行GATT化,得到下面有内涵,有层次,有定义的数据表格:
到此GATT 基本介绍完了,一句话总结,GATT约束了蓝牙数据结构规范,大家都在一个规范下,那么一切的交互就会简单、高效、不易错。比如说心率计profile,就是一个蓝牙联盟定义的蓝牙设备,蓝牙联盟有一份专门的spec来定义心率计profile,在这份spec中规定了心率计profile除了包含心率service,还包含电池service,设备信息service等。下面开始说说蓝牙(BLE)协议栈。
什么是协议栈?一般而言,我们把某个协议的实现代码称为协议栈(protocol stack),BLE协议栈就是实现低功耗蓝牙协议的代码,比如android SDK已经帮助开发者实现了BLE协议的协议栈,开发者只需要调用相关的API就可以很容易的实现BLE协议,这里文章后面再细说。理解和掌握BLE协议是实现BLE协议栈的前提。BLE协议栈整体架构;
1.PHY层(Physical layer物理层)。PHY层用来指定BLE所用的无线频段,调制解调方式和方法等。PHY层做得好不好,直接决定整个BLE芯片的功耗,灵敏度以及selectivity等射频指标。
2.LL层(Link Layer链路层)。LL层是整个BLE协议栈的核心,也是BLE协议栈的难点和重点。像Nordic的BLE协议栈能同时支持20个link(连接),就是LL层的功劳。LL层要做的事情非常多,比如具体选择哪个射频通道进行通信,怎么识别空中数据包,具体在哪个时间点把数据包发送出去,怎么保证数据的完整性,ACK如何接收,如何进行重传,以及如何对链路进行管理和控制等等。LL层只负责把数据发出去或者收回来,对数据进行怎样的解析则交给上面的GAP或者GATT。
3.HCI(Host controller interface)。HCI是可选的(具体请参考文章: 三种蓝牙架构实现方案(蓝牙协议栈方案)),HCI主要用于2颗芯片实现BLE协议栈的场合,用来规范两者之间的通信协议和通信命令等。
4.GAP层(Generic access profile)。GAP是对LL层payload(有效数据包)如何进行解析的两种方式中的一种,而且是最简单的那一种。GAP简单的对LL payload进行一些规范和定义,因此GAP能实现的功能极其有限。GAP目前主要用来进行广播,扫描和发起连接等。
5.L2CAP层(Logic link control and adaptation protocol)。L2CAP对LL进行了一次简单封装,LL只关心传输的数据本身,L2CAP就要区分是加密通道还是普通通道,同时还要对连接间隔进行管理。
6.SMP(Secure manager protocol)。SMP用来管理BLE连接的加密和安全的,如何保证连接的安全性,同时不影响用户的体验,这些都是SMP要考虑的工作。
7.ATT(Attribute protocol)。简单来说,ATT层用来定义用户命令及命令操作的数据,比如读取某个数据或者写某个数据。BLE协议栈中,开发者接触最多的就是ATT。BLE引入了attribute概念,用来描述一条一条的数据。Attribute除了定义数据,同时定义该数据可以使用的ATT命令,因此这一层被称为ATT层。
8.GATT(Generic attribute profile )。GATT用来规范attribute中的数据内容,并运用group(分组)的概念对attribute进行分类管理。没有GATT,BLE协议栈也能跑,但互联互通就会出问题,也正是因为有了GATT和各种各样的应用profile,BLE摆脱了ZigBee等无线协议的兼容性困境,成了出货量最大的2.4G无线通信产品。
蓝牙ble协议栈就介绍到这里,其实作为一个应用层的开发者,并不需要过多的去关注协议栈,因为并不需要我们去实现协议栈,作为Android开发我们只需要调用api即可。下面我们就来看一下android如何进行蓝牙ble开发
在android官方网站已经有比较完整的开发开发流程蓝牙低功耗概览,相信看到这里,是能够比较轻松的消化官方教程的,整个流程可以概括为以下几步:
第一步:初始化,判断当前设备是否支持蓝牙ble,判断用户是否授权蓝牙权限,主要用到了蓝牙适配器BluetoothAdapter,BluetoothAdapter是整个蓝牙开发的基础。
第二步:扫描蓝牙设备, startLeScan()
方法。此方法将 BluetoothAdapter.LeScanCallback
作为参数,这个参数就是一个回调函数,我们可以利用这个回调函数,在蓝牙列表上实时更新扫描到的蓝牙设备。
第三步:连接蓝牙,连接到 GATT 服务器,主要用到了一个回调函数 BluetoothGattCallback
,蓝牙的所有操作都是基于这个回调函数,比如特征值的改变,服务的发现、蓝牙的断开等..
第四步:设置特征值,就像订阅一样,我们需要设置我们关心的特征值。这一切做完,我们就可以进行蓝牙设备间的数据交互了,其实就是对特征值的读和写。
具体代码细节,再次就不赘述了,请看demo。
我开发过程中遇到了两个天坑可能你也会遇到!!!!!!!
1.每次只能读取20个字节
每次发送的数据长度最大值为 MTU-3,早期低功耗蓝牙协议 MTU 固 定为 23(安卓 5.2 以前),所以每个数据包最多 20 个字节。之后的协议 MTU 可以修改,模块支持的 MTU 最大为 247,所以每个包最大的长度为 244 字节, 无论安卓手机还是苹果手机,都可以实现每个包传输 244 字节。MTU 值模块 只能读取,不能设置,蓝牙未连接时 MTU 固定为 23,这个值没有任何意义, 蓝牙连接后 MTU 值为模块和手机协商之后的值,通常是 247,可以用手机修 改 MTU 值。如果发现最长只能传输 20 个字节,说明手机端没有调用 setMTU 来设置 MTU 值。设置MTU的方法就是在回调函数中添加gatt.requestMtu(500),500的意思就是设置每次可读取500个字节。注意!!!!!!!!我们还需要在回调BluetoothGattCallback中添加回调函数onMtuChanged(),程序调用过gatt.requestMtu(500)之后就会进入onMtuChanged()回调函数。
@Override//重写onServicesDiscovered,发现蓝牙服务
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS)//发现到服务
{
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
Log.i(TAG, "--onServicesDiscovered called--");
gatt.requestMtu(500);
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
System.out.println("onServicesDiscovered received: " + status);
}
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
if (BluetoothGatt.GATT_SUCCESS == status) {
Log.e("BLEService", "onMtuChanged success MTU = " + mtu);
displayGattServices(mBluetoothGatt.getServices());
} else {
Log.e("BLEService", "onMtuChanged fail ");
}
}
再注意!!!!!!当调用gatt.requestMtu(500)之后,我们设置的特征值就会失效,我们一定要在回调函数onMtuChanged()中重新设置特征值,否则你会发现你啥也收不到也发不出去。我的代码
displayGattServices(mBluetoothGatt.getServices());
就是设置特征值的。
2.为什么搜索不到设备?
需要使用 App 来搜索而不是手机系统的蓝牙搜索界面,因为系统的 蓝牙连接都是标准的 UUID,是由 SIG 定义好的标准蓝牙服务,例如蓝牙耳机、 蓝牙音箱、蓝牙键盘鼠标等,BLE 透传模块通常使用的是自定义的非标的 UUID ,所以使用系统蓝牙搜索界面是搜索不到的,只能使用 App或小程来搜索。
完结!