低功耗蓝牙ATT/GATT/Profile/Service/Characteristic规格解读
什么是蓝牙service和characteristic?到底怎么理解蓝牙profile?ATT和GATT两者如何区分?什么又是attribute?attribute和characteristic的区别是什么?蓝牙的互联互通为什么能做得这么成功?
本文将对以上问题进行阐述,并重点阐述蓝牙协议栈中的ATT层和GATT层,本文偏重理论,如果你对低功耗蓝牙不是很了解,建议配合如下两篇文章一起阅读:
开发你的第一个BLE应用程序—Blinky
手把手教你开发BLE数据透传应用程序
1. 蓝牙协议栈架构
如文章“深入浅出低功耗蓝牙(BLE)协议栈”所述,低功耗蓝牙协议栈框架结构如下所示:
如图所示,ATT和GATT是蓝牙协议栈重要的2层,也是蓝牙应用开发者打交道最多的两层,用户开发应用程序或者说service/profile的时候,调用的都是GATT API,而GATT又调用了ATT API。在讲解ATT和GATT之前,我们先看一下蓝牙核心规范中一个重要概念:Client/Server(客户端/服务端)架构。
2. BLE client/server(C/S) 架构
BLE采用了client/server (C/S) 架构来进行数据交互,C/S架构是一种非常常见的架构,在我们身边随处可见,比如我们经常用到的浏览器和服务器也是一种C/S架构,这其中浏览器是客户端client,服务器是服务端server,server比如淘宝服务器,提供商品信息,广告,社交等服务,而浏览器就是客户端,比如微软的IE,就可以用来请求这些服务,并使用server提供的服务。BLE与此类似,一般而言设备提供服务,因此设备是server,手机使用设备提供的服务,因此手机是client。比如蓝牙体温计,它可以提供 “体温” 数据服务,因此是一个server,而手机则可以请求“体温”数据以显示在手机上,因此手机是一个client。
服务是以数据为载体的,所以说server提供服务其实就是提供各种有价值的数据。
图1:C/S架构
客户端要访问某一个数据,就发送一个request/请求(其实就是一条命令或者PDU),服务端再把该数据返回给客户端(一条response/响应命令或者PDU),这就是C/S架构。
3. ATT
讲解ATT之前我们先讲解attribute,那么什么是attribute?其实就是一条一条的数据。前面说过,每个蓝牙设备就是用来提供服务的,而服务就是众多数据的合集,这个合集可以称为数据库,数据库里面每个条目都是一个attribute。所以在这里我把attribute翻译成数据条目。大家可以把一个蓝牙设备想象成一个表格,表格里面每一行就是一个attribute。attribute可以用下图来表示:
- Attribute handle,Attribute句柄,16-bit长度。Client要访问Server的Attribute,都是通过这个句柄来访问的,也就是说ATT PDU一般都包含handle的值。用户在软件代码添加characteristic的时候,系统会自动按顺序地为相关attribute生成句柄。
- Attribute type,Attribute类型,2字节或者16字节长。在BLE中我们使用UUID来定义数据的类型,UUID是128 bit的,所以我们有足够的UUID来表达万事万物。其中有一个UUID非常特殊,它被蓝牙联盟采用为官方UUID,这个UUID如下所示:
由于这个UUID众所周知,蓝牙联盟将自己定义的attribute或者数据只用16bit UUID来表示,比如0x1234,其实它也是128bit,完整表示为:
Attribute type一般是由service和characteristic规格来定义,站在蓝牙协议栈角度来看,ATT层定义了一个通信的基本框架,数据的基本结构,以及通信的指令,而GATT层就是定义service和characteristic,GATT层用来赋予每个数据一个具体的内涵,让数据变得有结构和意义。换句话说,没有GATT层,低功耗蓝牙也可以通信起来,但会产生兼容性问题以及通信的低效率。
- Attribute value,就是数据真正的值,0到512字节长。
- Attribute permissions,Attribute的权限属性,权限属性不会直接在空口包中体现,而是隐含在ATT命令的操作结果中。假设一个attribute read属性设为open(即读操作不需要任何权限),那么client去读这个attribute时server将直接返回attribute的值;如果这个attribute read属性设为authentication(即需要配对才能访问),如果client没有与server配对而直接去访问这个attribute,那么server会返回一个错误码:告诉client你的权限不够,此时client会对server发起配对请求,以满足这个attribute的读属性要求,从而在第二次读操作时server将把相应的数据返回给client。目前主要有如下四种权限属性:
- Open,直接可以读或者写
- No Access,禁止读或者写
- Authentication,需要配对才能读或者写,由于配对有多种类型,因此authentication又衍生多种子类型,比如带不带MITM,有没有LESC
- Authorization,跟open一样,不过server返回attribute的值之前需要应用先授权,也就是说应用可以在回调函数里面去修改读或者写的原始值。
- Signed,签名后才能读或者写,这个用得比较少。
一个应用所有的attribute组成一个database,也称为attribute table,一个attribute table示例如下所示:
图2:原始attribute数据库(这个表格不能算是原始attribute,因为它已经把bin数据转成字符了,大家可以把相关字符都看成bin数据,就成了原始attribute表格)
设备支持的服务不同,attribute table就不同。这里说明一下,当你添加,修改或者删除服务时,那么attribute table就会变,attribute table变了,它占用的RAM空间就会变,在Nordic 例程中attribute table占用多少RAM是静态分配的,即通过宏ATTR_TAB_SIZE来控制attribute database大小,因此当你对服务进行了任何修改,记得一定要去修改ATTR_TAB_SIZE的值,否则协议栈初始化会报错:RAM不够或者太多。
ATT,全称attribute protocol(数据交互协议)。说到底,ATT是由一群ATT命令组成,就是上文所述的request(请求)和response(响应)命令,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)列表如下所示:
4. GATT,Service(服务)和Characteristic(特征数据)
讲解GATT之前,我们先看一下什么是profile?profile是一个大家经常见到的英文单词,但是总感觉领会不到这个词的内涵。Profile,英文本义就是脸的侧面轮廓,这里大家一定要注意,脸的轮廓不等于脸本身(脸本身是非常复杂和细致的),但profile本身是对脸的一种抽象,描述和定义,蓝牙核心规范其实也是使用profile这个引申意义而已,换句话说,蓝牙的profile跟英文字典中的profile是同一个意思,意义基本接近。要定义蓝牙,必须要有一个规范,这就是蓝牙核心规范V4.2/V5.0/V5.1……蓝牙规范非常复杂和庞大,大部分蓝牙设备只实现了蓝牙规范中很少一部分,那么没有实现的这些规范对这个蓝牙设备来说能不能称为规范?当然不能!所谓规范或者规格,就是强制的,就必须实现。针对这种情况,profile可以很好地应对。我们把蓝牙某部分规范称为profile,这个profile如果设备要实现它,那么它就是强制的;如果设备不用它,也没关系,这就是profile。基于此,我们可以把profile翻译成子规范或者条件规范或者剖面规范。 “蓝牙规范包含很多子规范”,这句话用中文说问题不是很大,但是你把它翻译成英文,那就难了!这就是英文需要用profile的原因(而不是spec),以及为什么profile在规范中出现的如此频繁。
GATT,全称generic attribute profile,对数据进行一般化/抽象化的子规范,说白了就是对数据进行逻辑化表达的规定。前面说过了,attribute是一条一条的数据,那么这条数据表示什么?如何对其进行分类?这就是GATT要做的事情,GATT将对数据赋予含义,并呈现一定的逻辑结构。
Service和characteristic就是GATT层定义的,前面说过,server端提供服务,服务就是数据,而数据就是一条一条的attribute,而service和characteristic就是数据的逻辑呈现,或者说用户能看到的数据最终都转化为service和characteristic。比如,一个数据 “37” ,有可能是说体温“37度”,也有可能是说心率“37次”或者湿度“37%”,因此必须对数据进行分类和定义。
在蓝牙规格中,每一个具体的蓝牙应用是由多个service组成的,而每一个service又是由多个characteristic组成的,这样我们可以把上面的图1转化成图3。
图3:service和characteristic
那service/characteristic和attribute之间到底是一个怎么样的关系?如前所述,service/characteristic是attribute的逻辑表现形式,而attribute是service/characteristic具体实现方式。尤其要注意的是,一条characteristic不是对应一条attribute,而是由多条attribute组成。虽然一个数据最有价值的部分是它的值(value),但是仅有value是不够的,比如27,到底是表示27°温度还是27%湿度;如果表示的是温度,那么它的单位是摄氏度还是华氏度,同时每个数据还有相应的读写属性以及权限属性,因此一个characteristic包含三种类型的数据条目(attribute):characteristic声明条目(declaration attribute),characteristic值条目(value attribute)以及characteristic描述符条目(descriptor attribute)(一个characteristic可以有多个描述符条目),如下所示:
由于一个service可以包含多个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。
最后再说一下,不管是characteristic declaration,characteristic value还是characteristic descriptor,实现的时候,都是一条数据条目,即attribute。
引入了GATT,我们就可以把图2 的attribute table进行GATT化,得到下面有内涵,有层次,有定义的数据表格:
下面我们再从手机角度来看一下service和characteristic长什么样。大家可以仿照“手把手教你开发BLE数据透传应用程序”,制作一个蓝牙设备,然后通过手机nRF Connect应用程序去连接这个设备,将得到如下界面:
可以看到手机呈现的就是上文讲的service和characteristic,nRF Connect为了让整个界面变得更美观,将访问属性,UUID,handle等跨多列来表示了,并隐藏了很多信息,以致于很多初学者不能把理论和实际二者一一对应起来(nRF connect桌面版显示的信息会更多点,大家也可以用桌面版看看service和characteristic包含哪些信息)。Nordic之前推出过一款Master Control Panel(MCP)PC版应用程序,功能跟nRF connect非常相似,MCP现在已经不推荐使用了,不过MCP有一个好处,它对service和characteristic的组织方式更接近底层实现方式,对大家理解service和characteristic是非常有帮助的。还是这个设备,我用MCP跟它连接并进行服务发现,你会发现它呈现的界面如下所示:
这个图就跟上面讲的理论知识可以一一对应起来了,Nordic UART Service (NUS)服务包含2个characteristic:RX和TX,每一个条目都是一个attribute,NUS服务本身就是一个attribute,而RX characteristic本身又包含2条attribute:一条是declaration attribute,一条是value本身attribute。由于TX支持notify,所以它包含3条attribute,除了declaration和value外,另外一条attribute是CCCD。每个attribute都有handle,UUID和value,handle用来访问该attribute,UUID用来指明该attribute的类型,value就是attribute实际包含的数据本身。
上述蓝牙设备除了包含NUS服务,还包含“GAP”和“GATT”服务,如下所示:
这里的“GAP”,跟我们蓝牙协议栈框架里面的GAP可不是一回事,这里的GAP是一个service,而蓝牙协议栈里面的GAP是一个子协议;GATT也是同样的道理。为了不引起混淆,nRF connect把这里的GAP服务称为“Generic Access”服务,把这里的GATT服务称为“Generic Attribute”服务。“Generic Access” 服务包含Device name,Appearance,PPCP以及CAR等characteristic。 “Generic Attribute”服务包含Service Change等characteristic。
所谓开发蓝牙应用程序,其实就是开发service和characteristic。通过API,添加自己需要的characteristic和service,你自己的蓝牙设备就诞生了。只要characteristic和service是符合GATT规范的,你可以随意添加任何characteristic和service,并将他们组合成一个专门的蓝牙设备。由于这个蓝牙设备是按照规范来定义的,所以它可以与任何其他蓝牙设备,比如手机,互联互通,并完成所要求的的交互动作。这里的蓝牙设备,我们还可以进一步细分为蓝牙profile设备和非profile蓝牙设备。前面也提过,profile就是一个子规范,蓝牙profile设备包含的所有实际service和characteristic都是按照profile规格来添加和定义的,比如说心率计profile,就是一个蓝牙联盟定义的蓝牙设备,蓝牙联盟有一份专门的spec来定义心率计profile,在这份spec中规定了心率计profile除了包含心率service,还包含电池service,设备信息service等。从这可以看出,心率profile和心率service是包含关系,前者包含后者。Nordic开发了很多蓝牙profile应用,如果你的应用就是一个profile,那么可以完全使用这些现成的例子而不做任何修改,这样做还有一个好处:BQB认证流程将大为简化,你只需做一些文档声明工作,不再需要出具测试报告,就可以获得BQB认证证书。这里强调一下,如果你的设备没有完全按照心率profile来,但也包含心率service,这个时候你就不能说你的设备是一个心率profile设备,虽然你的设备不是心率profile设备,但是它提供的功能基本上跟心率profile设备差不多,因为它包含心率service。这种情况下,你的设备还是一个标准的蓝牙设备,还是可以去过BQB认证的,而且过BQB认证的流程也不复杂,就是比profile设备多了一个自测报告而已。这种设备跟手机通信,是没有任何兼容性或者互联互通问题的。最后强调一下,目前大部分人开发的蓝牙应用程序,其实都是非profile蓝牙应用程序,比如手环、共享单车、蓝牙透传等。