Android蓝牙BLE开发全面总结

一、蓝牙BLE产生背景——蓝牙的发展历程

要说蓝牙BLE的产生背景,首先要放到蓝牙的发展历程里面去看。说起蓝牙,大家一定听过蓝牙1.0、蓝牙2.0、蓝牙3.0、蓝牙4.0,不过现在大部分已经不再用版本号区分蓝牙了,蓝牙1.0~3.0都是经典蓝牙,在塞班系统就已经开始使用了。什么是经典蓝牙?它和蓝牙BLE有什么区别?——这就要从头说起:

蓝牙诞生之初,使用的是BR(Basic Rate)技术,此时蓝牙的理论传输速率,只能达到721.2Kbps。在那个年代,56Kbps的Modem就是高大上了,这个速度可以说是惊为天人了啊!但是科技变化太快了,BR技术转眼就过时了。那怎么办呢?缝缝补补一下,增强速度呗,EDR(Enhanced Data Rate)就出现了。使用EDR技术的蓝牙,理论速率可以达到2.1Mbps。这一次的升级换代,还算优雅,因为没有任何的硬件架构、软件架构和使用方式上的改变。也许你也猜到了,很快EDR又落伍了,看看人家WIFI(WLAN),几十Mbps,上百Mbps,咱们才2.1Mbps,也太寒酸了吧!那怎么办呢?蓝牙组织想了个坏主意:哎,WIFI!把你的PHY层和MAC(Media Access Control)层借我用用呗!这就·是AMP(Alternate MAC and PHY layer extension)。艾玛,终于松口气了,我们可以达到54Mbps了。不过呢,由于蓝牙自身的物理层和AMP技术差异太明显了,这次扩展只能是交替使用(Alternate)的,也就是说,有我(BR/EDR)没你(AMP)。

蓝牙配置

这里特别强调了optional和alternate这两个字眼,这是蓝牙Spec的原话。它意味着,BR和EDR是可以同时存在的,但BR/EDR和AMP只能二选一。总的来说,BR是正宗的蓝牙技术,可以包括可选(optional)的EDR技术,以及AMP(交替使用(Alternate)的MAC层和PHY层扩展)

上面所讲的蓝牙技术的进化路线,就是传输速率的加快、加快、再加快。但能量是守恒的,你想传的更快,代价就是消耗更多的能量。而有很多的应用场景,并不关心传输速率,反而非常关心功耗。这就是BLE(低功耗蓝牙)产生的背景。

有些人一直认为蓝牙4.0就是蓝牙BLE,其实并不正确。准确来说4.0是双模的,既包括经典蓝牙又包括低功耗蓝牙。经典蓝牙和蓝牙BLE虽然都是蓝牙,但其实还是存在很大区别的。蓝牙技术联盟在2010年6月30号公布了蓝牙4.0标准,4.0标准在蓝牙3.0+HS标准的基础上增加了对低功耗蓝牙(Bluetooth Low Energy, BLE)的支持。蓝牙核心规范4.0的模块增加了以下几个BLE组件:GATTATTSMP(这几个概念后面会解释)。

这里插一句题外话:

SIG(蓝牙特别利益小组即蓝牙官方组织)早已发布4.1以下旧版本的废弃时间确定在2019年1月28开始,目前官方建议使用的版本号为蓝牙5.0,蓝牙6.0,蓝牙7.0。

Withdrawal of the following on January 28, 2019:

  • Bluetooth Specification Version 2.0 + EDR

Deprecation of the following on January 28, 2019 and withdrawal on July 1, 2020:

  • Bluetooth Specification Version 2.1 + EDR
  • Bluetooth Specification Version 3.0 + HS
  • Bluetooth Specification Version 4.0
  • Bluetooth Specification Version 4.1

言归正传,经典蓝牙和蓝牙BLE都包括搜索(discovery)管理、连接(connection)管理等机制,相互独立。但是相比传统蓝牙,BLE最大的特点就是低功耗,低延时,快速的搜索和连接速度,但数据传输速度相比传统蓝牙低,传输的数据量也很小,每次只有20个字节(理论上可以通过一些方法去突破限制,参见蓝牙BLE MTU规则与约定)。BLE技术相比BR技术,差异非常大,或者说就是两种不同的技术,凑巧都加一个“蓝牙”的前缀而已。蓝牙BLE因为其低能耗的优点,在智能穿戴设备和车载系统上的应用越来越广泛。


二、蓝牙BLE的基本概念

上面我们知道了蓝牙4.0版本中诞生了蓝牙BLE,而Android当时4.2版本已经发布了,所以真正引入蓝牙BLE是在Android4.3系统,但是仅作为中央设备,直到5.0以后才可以既作为中央设备又可以作为周边设备。通俗的说,也就是5.0系统以后,可以手机控制手机了,不过绝大多数的场景手机还是作为中央设备去控制其他的周边设备。蓝牙BLE主要用于手机与周边设备进行通信,当然也可以用于所有BLE设备之间的通信。使用BLE可以实现Android与iOS之间的蓝牙通信,而普通蓝牙却不可以。

蓝牙BLE是基于GATT进行通信的,GATT(Generic Attribute Profile)是一种属性传输协议,简单的讲可以认为是一种属性传输的应用层协议。GATT是蓝牙4.0特有的Profile通用规范,BLE应用的Profile均基于GATT。GATT定义了一个服务框架规范,该框架包括对服务(Service)和服务特性(Characteristic)的定义和规范,和其中读、写、通知的特性等。可以将GATT理解成BLE框架,我们在GATT上面实现BLE功能。

GATT连接是独占的。也就是一个BLE外设同时只能被一个中心设备连接。一旦外设被连接,它就会马上停止广播,这样它就对其他设备不可见了。当设备断开,它又开始广播。

GATT已经成为BLE通信的规定,每一个设备中存在很多的Service,Service中还包含有多个Characteristic。在蓝牙实际数据交换中,就是通过读写这些“Characteristic”来实现的。
下图是GATT中的Service,Characteristic, Descriptor三者之间的关系图,在Android的BLE源码中这三类变量也经常出现。

GATT结构图

结构的组成:

  • 每个BLE设备由多个Profile(GATT)组成
  • 每个Profile由多个的Service服务组成
  • 每个Service由多个Characteristic特征组成
  • 每个Characteristic由一个Value值和多个Descriptor描述组成

结构的用途:

  • Service: 是完成一个特定功能的数据和行为集合。在Gatt中,一个Service可能包含Service引用以及强制或者可选的Characteristic。
  • Characteristic: 一个Characteristic的定义包含了Characteristic本身,数值以及描述(Descriptor)的声明。Characteristic是完成BLE具体功能的基本单位。
  • Value: 是Characteristic的属性值。
  • Descriptor: 是对Value不同角度的描述和说明,所以有多个Descriptor

图中画的比较少,实际上一个蓝牙协议里面包含的Service、Characteristic和Descriptor是比较多的 ,这时候你可能会问,这么多的同名属性用什么来区分呢?答案就是——UUID。UUID既有16位的也 有128位的。16位的UUID是经过蓝牙组织认证的,是需要购买的,而128位的UUID则可以自定义,当然也有许多通用的UUID。每个Service、Characteristic或者Descriptor都有一个 128 bit 的UUID来标识。但那些被蓝牙技术联盟的标准中定义的UUID是以16 bit 来表示的。实际上,16 bit 的UUID,是有附加 Bluetooth Base UUID,即变成0000****-0000-1000-8000-00805f9b34fb(16位UUID被输入在****的位置)。

Service可以理解为一个功能集合,而Characteristic比较重要,蓝牙设备正是通过Characteristic来进行设备间的交互的(如读、写、通知等操作)。可以这样来理解这两个概念:service即面向对象中的“类”的概念,characteristic即面向对象中“属性”的概念。

总结一下就是,蓝牙BLE基于GATT协议传输数据,提供了Serivice和Characteristic进行设备之间的通讯。这就是蓝牙BLE的基本概念。


三、蓝牙BLE的架构介绍

1. 蓝牙BLE架构概览

一般而言,我们把某个协议的实现代码称为协议栈(protocol stack),BLE协议栈就是实现低功耗蓝牙协议的代码,理解和掌握BLE协议是实现BLE协议栈的前提。在深入BLE协议栈各个组成部分之前,我们先看一下BLE协议栈整体架构。

BLE整体架构

如上图所述,要实现一个BLE应用,首先需要一个支持BLE射频的芯片,然后还需要提供一个与此芯片配套的BLE协议栈,最后在协议栈上开发自己的应用。可以看出BLE协议栈是连接芯片和应用的桥梁,是实现整个BLE应用的关键。那BLE协议栈具体包含哪些功能呢?简单来说——BLE协议栈主要用来对你的应用数据进行层层封包,以生成一个满足BLE协议的空中数据包,换句话说,就是把应用数据包裹在一系列的帧头(header)和帧尾(tail)中。具体来说,BLE协议栈主要由如下几部分组成:

  • PHY层(Physical layer物理层)。PHY层用来指定BLE所用的无线频段,调制解调方式和方法等。PHY层做得好不好,直接决定整个BLE芯片的功耗,灵敏度以及selectivity等射频指标。

  • LL层(Link Layer链路层)。LL层是整个BLE协议栈的核心,也是BLE协议栈的难点和重点。像Nordic的BLE协议栈能同时支持20个link(连接),就是LL层的功劳。LL层要做的事情非常多,比如具体选择哪个射频通道进行通信,怎么识别空中数据包,具体在哪个时间点把数据包发送出去,怎么保证数据的完整性,ACK如何接收,如何进行重传,以及如何对链路进行管理和控制等等。LL层只负责把数据发出去或者收回来,对数据进行怎样的解析则交给上面的GAP或者GATT。

  • HCI(Host controller interface)。HCI是可选的(具体请参考文章: 三种蓝牙架构实现方案(蓝牙协议栈方案)),HCI主要用于2颗芯片实现BLE协议栈的场合,用来规范两者之间的通信协议和通信命令等。

  • GAP层(Generic access profile)。GAP是对LL层payload(有效数据包)如何进行解析的两种方式中的一种,而且是最简单的那一种。GAP简单的对LL payload进行一些规范和定义,因此GAP能实现的功能极其有限。GAP目前主要用来进行广播,扫描和发起连接等。

  • L2CAP层(Logic link control and adaptation protocol)。L2CAP对LL进行了一次简单封装,LL只关心传输的数据本身,L2CAP就要区分是加密通道还是普通通道,同时还要对连接间隔进行管理。

  • SMP(Secure manager protocol)。SMP用来管理BLE连接的加密和安全的,如何保证连接的安全性,同时不影响用户的体验,这些都是SMP要考虑的工作。

  • ATT(Attribute protocol)。简单来说,ATT层用来定义用户命令及命令操作的数据,比如读取某个数据或者写某个数据。BLE协议栈中,开发者接触最多的就是ATT。BLE引入了attribute概念,用来描述一条一条的数据。Attribute除了定义数据,同时定义该数据可以使用的ATT命令,因此这一层被称为ATT层。

  • GATT(Generic attribute profile )。GATT用来规范attribute中的数据内容,并运用group(分组)的概念对attribute进行分类管理。没有GATT,BLE协议栈也能跑,但互联互通就会出问题,也正是因为有了GATT和各种各样的应用profile,BLE摆脱了ZigBee等无线协议的兼容性困境,成了出货量最大的2.4G无线通信产品。

我相信很多人看了上面的介绍,还是不懂BLE协议栈的工作原理,以及每一层具体干什么的,为什么要这么分层。下面我以如何发送一个数据包为例来讲解BLE协议栈各层是如何紧密配合,以完成发送任务的。

2. 简述BLE如何发送数据包

假设有设备A和设备B,设备A要把自己目前的电量状态83%(十六进制表示为0x53)发给设备B,该怎么做呢?作为一个开发者,他希望越简单越好,对他而言,他希望调用一个简单的API就能完成这件事,比如send(0x53),实际上我们的BLE协议栈就是这样设计的,开发者只需调用send(0x53)就可以把数据发送出去了,其余的事情BLE协议栈帮你搞定。很多人会想,BLE协议栈是不是直接在物理层就把0x53发出去,就如下图所示:

这种方式初看起来挺美的,但由于很多细节没有考虑到,实际是不可行的。首先,它没有考虑用哪一个射频信道来进行传输,在不更改API的情况下,我们只能对协议栈进行分层,为此引入LL层,开发者还是调用send(0x53),send(0x53)再调用send_LL(0x53,2402M)(注:2402M为信道频率)。这里还有一个问题,设备B怎么知道这个数据包是发给自己的还是其他人的,为此BLE引入access address概念,用来指明接收者身份,其中,0x8E89BED6这个access address比较特殊,它表示要发给周边所有设备,即广播。如果你要一对一的进行通信(BLE协议将其称为连接),即设备A的数据包只能设备B接收,同样设备B的数据包只能设备A接收,那么就必须生成一个独特的随机access address以标识设备A和设备B两者之间的连接。

2.1 广播方式

我们先来看一下简单的广播情况,这种情况下,我们把设备A叫advertiser(广播者),设备B叫scanner或者observer(扫描者)。广播状态下设备A的LL层API将变成send_LL(0x53,2402M, 0x8E89BED6)。由于设备B可以同时接收到很多设备的广播,因此数据包还必须包含设备A的device address(0xE1022AAB753B)以确认该广播包来自设备A,为此send_LL参数需要变成(0x53,2402M, 0x8E89BED6, 0xE1022AAB753B)。LL层还要检查数据的完整性,即数据在传输过程中有没有发生窜改,为此引入CRC24对数据包进行检验 (假设为0xB2C78E) 。同时为了调制解调电路工作更高效,每一个数据包的最前面会加上1个字节的preamble(前导帧),preamble一般为0x55或者0xAA。这样,整个空中包就变成(注:空中包用小端模式表示!):

上面这个数据包还有如下问题:

  1. 没有对数据包进行分类组织,设备B无法找到自己想要的数据0x53。为此我们需要在access address之后加入两个字段:LL header和长度字节。LL header用来表示数据包的LL类型,长度字节用来指明payload的长度
  2. 设备B什么时候开启射频窗口以接收空中数据包?如上图case1所示,当设备A的数据包在空中传输的时候,设备B把接收窗口关闭,此时通信将失败;同样对case2来说,当设备A没有在空中发送数据包时,设备B把接收窗口打开,此时通信也将失败。只有case3的情况,通信才能成功,即设备A的数据包在空中传输时,设备B正好打开射频接收窗口,此时通信才能成功,换句话说,LL层还必须定义通信时序
  3. 当设备B拿到数据0x53后,该如何解析这个数据呢?它到底表示湿度还是电量,还是别的意思?这个就是GAP层要做的工作,GAP层引入了LTV(Length-Type-Value)结构来定义数据,比如020105,02-长度,01-类型(强制字段,表示广播flag,广播包必须包含该字段),05-值。由于广播包最大只能为31个字节,它能定义的数据类型极其有限,像这里说的电量,GAP就没有定义,因此要通过广播方式把电量数据发出去,只能使用供应商自定义数据类型0xFF,即04FF590053,其中04表示长度,FF表示数据类型(自定义数据),0x0059是供应商ID(自定义数据中的强制字段),0x53就是我们的数据(设备双方约定0x53就是表示电量,而不是其他意思)。

最终空中传输的数据包将变成:

  • AAD6BE898E600E3B75AB2A02E102010504FF5900538EC7B2
    • AA – 前导帧(preamble)
    • D6BE898E – 访问地址(access address)
    • 60 – LL帧头字段(LL header)
    • 0E – 有效数据包长度(payload length)
    • 3B75AB2A02E1 – 广播者设备地址(advertiser address)
    • 02010504FF590053广播数据
    • 8EC7B2 – CRC24值

有了PHY,LL和GAP,就可以发送广播包了,但广播包携带的信息极其有限,而且还有如下几大限制:

  1. 无法进行一对一双向通信 (广播是一对多通信,而且是单方向的通信)
  2. 由于不支持组包和拆包,因此无法传输大数据
  3. 通信不可靠及效率低下。广播信道不能太多,否则将导致扫描端效率低下。为此,BLE只使用37(2402MHz) /38(2426MHz) /39(2480MHz)三个信道进行广播和扫描,因此广播不支持跳频。由于广播是一对多的,所以广播也无法支持ACK。这些都使广播通信变得不可靠。
  4. 扫描端功耗高。由于扫描端不知道设备端何时广播,也不知道设备端选用哪个频道进行广播,扫描端只能拉长扫描窗口时间,并同时对37/38/39三个通道进行扫描,这样功耗就会比较高。

而连接则可以很好解决上述问题,下面我们就来看看连接是如何将0x53发送出去的。

2.2 连接方式

到底什么叫连接(connection)?像有线UART,很容易理解,就是用线(Rx和Tx等)把设备A和设备B相连,即为连接。用“线”把两个设备相连,实际是让2个设备有共同的通信媒介,并让两者时钟同步起来。蓝牙连接有何尝不是这个道理,所谓设备A和设备B建立蓝牙连接,就是指设备A和设备B两者一对一“同步”成功,其具体包含以下几方面:

  • 设备A和设备B对接下来要使用的物理信道达成一致
  • 设备A和设备B双方建立一个共同的时间锚点,也就是说,把双方的时间原点变成同一个点
  • 设备A和设备B两者时钟同步成功,即双方都知道对方什么时候发送数据包什么时候接收数据包
  • 连接成功后,设备A和设备B通信流程如下所示:

如上图所示,一旦设备A和设备B连接成功(此种情况下,我们把设备A称为Master或者Central,把设备B称为Slave或者Peripheral),设备A将周期性以CI(connection interval)为间隔向设备B发送数据包,而设备B也周期性地以CI为间隔打开射频接收窗口以接收设备A的数据包。同时按照蓝牙spec要求,设备B收到设备A数据包150us后,设备B切换到发送状态,把自己的数据发给设备A;设备A则切换到接收状态,接收设备B发过来的数据。由此可见,连接状态下,设备A和设备B的射频发送和接收窗口都是周期性地有计划地开和关,而且开的时间非常短,从而大大降低系统功耗并大大提高系统效率。

现在我们看看连接状态下是如何把数据0x53发送出去的,从中大家可以体会到蓝牙协议栈分层的妙处。

  • 对上层开发者来说,很简单,他只需要调用send(0x53)
  • GATT层定义数据的类型和分组,方便起见,我们用0x0013表示电量这种数据类型,这样GATT层把数据打包成130053(小端模式!)
  • ATT层用来选择具体的通信命令,比如读/写/notify/indicate等,这里选择notify命令0x1B,这样数据包变成了:1B130053
  • L2CAP用来指定connection interval(连接间隔),比如每10ms同步一次(CI不体现在数据包中),同时指定逻辑通道编号0004(表示ATT命令),最后把ATT数据长度0x0004加在包头,这样数据就变为:040004001B130053
  • LL层要做的工作很多,首先LL层需要指定用哪个物理信道进行传输(物理信道不体现在数据包中),然后再给此连接分配一个Access address(0x50655DAB)以标识此连接只为设备A和设备B直连服务,然后加上LL header和payload length字段,LL header标识此packet为数据packet,而不是control packet等,payload length为整个L2CAP字段的长度,最后加上CRC24字段,以保证整个packet的数据完整性,所以数据包最后变成:
    • AAAB5D65501E08040004001B130053D550F6
      • AA – 前导帧(preamble)
      • 0x50655DAB – 访问地址(access address)
      • 1E – LL帧头字段(LL header)
      • 08 – 有效数据包长度(payload length)
      • 04000400 – ATT数据长度,以及L2CAP通道编号
      • 1B – notify command
      • 0x0013 – 电量数据handle
      • 0x53 – 真正要发送的电量数据
      • 0xF650D5 – CRC24值
      • 虽然上层开发者只调用了 send(0x53),但由于蓝牙BLE协议栈层层打包,最后空中实际传输的数据将变成下图所示的模样,这就既满足了低功耗蓝牙通信的需求,又让用户API变得简单,可谓一箭双雕!

四、开发一个BLE应用

前面我们讲了蓝牙BLE的架构和内部处理逻辑,但是很多上层开发人员其实并不关心这些,只想知道如何去开发一个BLE应用。接下来我用一个实例去讲一下。

前一段时间有一个项目需求,是做一款智能车钥匙APP。主要为了在APP上面,通过蓝牙BLE消息的发送与接收,与汽车上装置的蓝牙盒子(下面简写成车顶盒)进行无线通信,车顶盒有线接入车机网络,以实现控制汽车打开关闭车门、打开关闭后备箱打开关闭发动机等一系列操作。界面很简单,只有几个按键,仿照的车钥匙的外观,保密原则,就不放上来了。

下面大概说一下基本实现思路:

  • APP开放了一个临时的入口,用于输入车顶盒的MAC地址,用于自动连接。
  • 系统启动时,APP中的服务会在接收到开机广播后,主动开启。服务启动是会开启一个线程,在线程中会判断当前APP与车顶盒未连接时,每隔一段时间使用之前保存的车顶盒MAC地址去进行连接操作,知道连接成功为止。
  • 建立BLE连接,点击主界面按钮,发送BLE消息给车顶盒,进行对应操作。

接下来从蓝牙BLE代码实现的角度描述一下如何实现,先简单看一下大概流程:

先说一下关键的角色:
BluetoothAdapter
BluetoothAdapter 拥有基本的蓝牙操作,例如开启蓝牙扫描,使用已知的 MAC 地址 (BluetoothAdapter#getRemoteDevice)实例化一个 BluetoothDevice 用于连接蓝牙设备的操作等等。

BluetoothDevice
代表一个远程蓝牙设备。这个类可以让你连接所代表的蓝牙设备或者获取一些有关它的信息,例如它的名字,地址和绑定状态等等。

BluetoothGatt
这个类提供了 Bluetooth GATT 的基本功能。例如重新连接蓝牙设备,发现蓝牙设备的 Service 等等。

BluetoothGattService
这个类通过 BluetoothGatt.getService 获得,如果当前服务不可见那么将返回一个 null。我们可以通过这个类的 getCharacteristic(UUID uuid) 进一步获取 Characteristic 实现 蓝牙数据的双向传输。

BluetoothGattCharacteristic
通过这个类定义需要往外围设备写入的数据和读取外围设备发送过来的数据。

4.1 准备

从硬件工程师手上拿到需要的UUID和MAC地址:

    public static final String MAC = "54:6C:0E:A0:47:5B"; //车顶盒MAC地址
    public static final UUID UUID_SERVICE = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb");  //主Service的UUID
    public static final String UUID_CHARA = "0000fff6-0000-1000-8000-00805f9b34fb"; //Characteristic的UUID

4.2 在清单文件中配置权限

    <!-- 蓝牙必须的权限-->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

    <!-- Android6.0及以上必须获取位置权限,否则无法扫描到周边的蓝牙设备  --> 
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

    <!-- 如果required=true,则应用只能在支持BLE的Android设备上安装运行,不支持BLE的设备将finish  --> 
    <uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="true" />

4.3 检查设备

首先要检查定位权限以及GPS是否开启

    public static boolean checkGPSPermissions(Activity activity) {
        String[] permissions = {Manifest.permission.ACCESS_FINE_LOCATION};
        List<String> permissionDeniedList = new ArrayList<>();
        for (String permission : permissions) {
            int permissionCheck = ContextCompat.checkSelfPermission(activity, permission);
            if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
                onPermissionGranted(activity, permission);
            } else {
                permissionDeniedList.add(permission);
            }
        }
        if (!permissionDeniedList.isEmpty()) {
            String[] deniedPermissions = permissionDeniedList.toArray(new String[permissionDeniedList.size()]);
            ActivityCompat.requestPermissions(activity, deniedPermissions, REQUEST_CODE_PERMISSION_LOCATION);
        }
        return true;
    }
    public static boolean checkGPSIsOpen(Activity activity) {
        LogUtil.d("检查GPS是否打开");
        LocationManager locationManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE);
        if (locationManager == null)
            return false;
        return locationManager.isProviderEnabled(android.location.LocationManager.GPS_PROVIDER);
    }

然后检查是否支持蓝牙BLE,并开启蓝牙

    public Boolean ensureBLEExists() {
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            return false;
        }
        //获取BluetoothAdapter
        BluetoothManager bm = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        if (bm!=null) mBluetoothAdapter = bm.getAdapter();
        // 开启蓝牙
        if (mBluetoothAdapter!=null){
            if (!mBluetoothAdapter.isEnabled()) { //蓝牙未开启,通过隐式意图请求开启蓝牙
                Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                startActivityForResult(enableBtIntent, 0);
            }
        }
        return true;
    }

4.4 通过UUID扫描指定的设备

   public BluetoothAdapter.LeScanCallback mScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
        //device是设备对象,rssi是信号强度,scanRecord是扫描记录
            if (device != null) {
              //接口回调扫描到的设备
                synchronized (mCallBacks){
                    for (BleAdapterCallBack callBack : mCallBacks) {
                        callBack.onDeviceFound(device, rssi);
                    }
                }
        }
    };

    private void startScan(){
        UUID[] uuid = {UUID_SERVICE };
        if(mIsScanning){ //如果当前正在扫描则先停止扫描
            mBluetoothAdapter.stopLeScan(mScanCallback);
        }
        //mBluetoothAdapter.startLeScan(mScanCallback);//不进行特定设备过滤,扫描所有设备
        //进行特定uuid过滤,只扫描具有指定Service UUID的设备
        mBluetoothAdapter.startLeScan(uuid, mScanCallback);
        // 10秒后停止扫描
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                //结束扫描
                mBluetoothAdapter.stopLeScan(mScanCallback);
            }
        },10000);
    }

在 LeScanCallback 回调的方法中,第一个参数是代表蓝牙设备的类,可以通过这个类建立蓝牙连接获取关于这一个设备的一系列详细的参数,例如名字,MAC 地址等等;第二个参数是蓝牙的信号强弱指标,通过蓝牙的信号指标,我们可以大概计算出蓝牙设备离手机的距离。计算公式为:d = 10^((abs(RSSI) - A) / (10 * n))(A:发射端和接收端相隔1米时的信号强度, n: 环境衰减因子,A和n的值,需要根据实际环境进行检测得出);第三个参数是蓝牙广播出来的广告数据,包含 广播数据 和 扫描响应数据 (如果有的话),所以长度一般就是 62 字节,BLE4.0规定,如果广播包和扫描应答包不足字节,则以0补齐。

另外,我们可以调用mBluetoothAdapter.startLeScan(uuid, mScanCallback),扫描具有指定Service UUID的设备,也可以调用mBluetoothAdapter.startLeScan(scanCallback),扫描所有的蓝牙设备,可以根据不同的方法自行选择。

蓝牙扫描是比较耗费资源的,如果扫描频率比较高或者时间比较长,在性能差一点手机上会出现电量消耗比较大和发热比较严重的情况,所以除非有特别的需求,要设置适当的扫描时间。

4.5 连接设备

连接蓝牙设备可以通过 BluetoothDevice#ConnectGatt 方法连接,也可以通过 BluetoothGatt#connect 方法进行重新连接。以下分别是两个方法的官方说明:

BluetoothDevice.connectGatt

BluetoothGatt   connect(Context context, boolean autoConnect, BluetoothGattCallback callback)

第二个参数表示是否需要自动连接。如果设置为 true, 表示如果设备断开了,会不断的尝试自动连接。设置为 false 表示只进行一次连接尝试。第三个参数是连接后进行的一系列操作的回调,例如连接和断开连接的回调,发现服务的回调,成功写入数据,成功读取数据的回调等等。

BluetoothGatt.connect

boolean connect()

调用这一个方法相当与调用 BluetoothDevice.connectGatt 且第二个参数 autoConnect 设置为 true。

当调用蓝牙的连接方法之后,蓝牙会异步执行蓝牙连接的操作,如果连接成功会回调 BluetoothGattCalback.onConnectionStateChange 方法。这个方法运行的线程是一个 Binder 线程,所以不建议直接在这个线程处理耗时的任务,因为这可能导致蓝牙相关的线程被阻塞。

    //连接状态变化的回调
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        Log.i(TAG, "连接状态:status:" + status + ",newState:" + newState)
         if (status == BluetoothGatt.GATT_SUCCESS) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                //连接成功,调用发现服务的方法
                gatt.discoverServices();
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.i(TAG, "断开连接");
                gatt.close();
            }
        } else {
            Log.i(TAG, "连接失败:" + status);
            gatt.close();
        }
    }

这一个方法有三个参数,第一个就蓝牙设备的 Gatt 服务连接类。第二个参数代表是否成功执行了连接操作,如果为 BluetoothGatt.GATT_SUCCESS 表示成功执行连接操作,第三个参数才有效,否则说明这次连接尝试不成功。有时候,我们会遇到 status == 133 的情况,根据网上大部分人的说法,这是因为 Android 最多支持连接 6 到 7 个左右的蓝牙设备,如果超出了这个数量就无法再连接了。所以当我们断开蓝牙设备的连接时,还必须调用 BluetoothGatt.close 方法释放连接资源。否则,在多次尝试连接蓝牙设备之后很快就会超出这一个限制,导致出现这一个错误再也无法连接蓝牙设备。第三个参数代表当前设备的连接状态,如果 newState == BluetoothProfile.STATE_CONNECTED 说明设备已经连接,可以进行下一步的操作了(发现蓝牙服务,也就是 Service)。当蓝牙设备断开连接时,这一个方法也会被回调其中的 newState == BluetoothProfile.STATE_DISCONNECTED。

4.6 获取GATT服务,进行读写通知操作

在成功连接到蓝牙设备之后才能进行这一个步骤,也就是说在 BluetoothGattCallback.onConnectionStateChange 方法被成功回调且表示成功连接之后调用 BluetoothGatt.discoverService 这一个方法。当这一个方法被调用之后,系统会异步执行发现服务的过程,直到 BluetoothGattCallback.onServicesDiscovered 被系统回调之后,手机设备和蓝牙设备才算是真正建立了可通信的连接。

    //发现Service的回调
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            //if(D) Log.i(TAG, "onServicesDiscovered success.");
            mBluetoothGatt = gatt;
            BluetoothGattService service = gatt.getService(UUID_SERVICE);// 获取服务对象
            if (service == null) {
                close();
                return;
            }
                // 获取BluetoothGattCharactristic
                BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(UUID_CHARA );
        }
    }

当我们发现服务之后就可以通过 BluetoothGatt.getService 获取 BluetoothGattService,接着通过 BluetoothGattService.getCharactristic 获取 BluetoothGattCharactristic。

到这一步,我们已经成功和蓝牙设备建立了可通信的连接,接下来就可以执行相应的蓝牙通信操作了,例如写入数据,读取蓝牙设备的数据等等。

4.6.1 读取数据

通过 BluetoothGattCharactristic.readCharacteristic 方法可以通知系统去读取特定的数据。如果系统读取到了蓝牙设备发送过来的数据就会调用 BluetoothGattCallback.onCharacteristicRead 方法。通过 BluetoothGattCharacteristic.getValue 可以读取到蓝牙设备的数据。以下是代码示例:

// 读取数据
gatt.readCharacteristic();

// 读取数据回调
@Override
public void onCharacteristicRead(final BluetoothGatt gatt,
                                    final BluetoothGattCharacteristic characteristic,final int status) {
 
    Log.d(TAG, "callback characteristic read status " + status
            + " in thread " + Thread.currentThread());
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d(TAG, "read value: " + characteristic.getValue());
    }
}
4.6.2 写入数据

和读取数据一样,在执行写入数据前需要获取到 BluetoothGattCharactristic。接着执行一下步骤:

  • 调用 BluetoothGattCharactristic.setValue 传入需要写入的数据(蓝牙最多单次1支持 20 个字节数据的传输,如果需要传输的数据大于这一个字节则需要分包传输)。
  • 调用 BluetoothGattCharactristic.writeCharacteristic 方法通知系统异步往设备写入数据。
  • 系统回调 BluetoothGattCallback.onCharacteristicWrite 方法通知数据已经完成写入。此时,我们需要执行 BluetoothGattCharactristic.getValue 方法检查一下写入的数据是否我们需要发送的数据,如果不是按照项目的需要判断是否需要重发。
    以下是示例代码:
// 写入数据
characteristic.setValue(sendValue);
gatt.writeCharacteristic(characteristic);

// 写入数据回调
@Override
public void onCharacteristicWrite(final BluetoothGatt gatt,
                                    final BluetoothGattCharacteristic characteristic,
                                    final int status) {
    Log.d(TAG, "callback characteristic write in thread " + Thread.currentThread());
    if(!characteristic.getValue().equal(sendValue)) {
        // 执行重发策略
        gatt.writeCharacteristic(characteristic);
    }
}
4.6.3 数据通知

BLE app通常需要获取设备中characteristic 变化的通知。下面的代码演示了怎么为一个Characteristic 设置一个监听:

// 注册数据通知监听
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
 
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);

// 数据通知回调
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    super.onCharacteristicChanged(gatt, characteristic);
        byte[] data = characteristic.getValue();    //取出接收到的数据
}

值得注意的是,除了通过 BluetoothGatt.setCharacteristicNotification 开启接收通知的开关,还需要往 Characteristic 的 Descriptor 属性写入开启通知的数据开关使得当硬件的数据改变时,主动往手机发送数据。

4.7 断开连接

当我们连接蓝牙设备完成一系列的蓝牙操作之后就可以断开蓝牙设备的连接了。通过 BluetoothGatt.disconnect 可以断开正在连接的蓝牙设备。当这一个方法被调用之后,跟connect一样系统也会异步回调 BluetoothGattCallback.onConnectionStateChange 方法。通过这个方法的 newState 参数可以判断是连接成功还是断开成功的回调。

由于 Android 蓝牙连接设备的资源有限,当我们执行断开蓝牙操作之后必须执行 BluetoothGatt.close 方法释放资源。需要注意的是通过 BluetoothGatt.close 方法也可以执行断开蓝牙的操作,不过 BluetoothGattCallback.onConnectionStateChange 将不会收到任何回调。此时如果执行 BluetoothGatt.connect 方法会得到一个蓝牙 API 的空指针异常。所以,我们推荐的写法是当蓝牙成功连接之后,通过 BluetoothGatt.disconnect 断开蓝牙的连接,紧接着在 BluetoothGattCallback.onConnectionStateChange 执行 BluetoothGatt.close 方法释放资源。(代码见4.5 连接设备

以上,就是这样一个需求的简单介绍,通过这个案例,应该可以对一个BLE项目有一个大概的了解。至于架构和代码都是比较简略甚至很多不合理的细节,大家不必细究。也可以自己动手写写,相信会比我写的完美。

五、结语

上面就是关于蓝牙BLE的全面总结。当然说是全面,有些夸张,BLE还有很多指的摸索的细节。蓝牙BLE虽然很轻量,但是却渗透在我们生活的方方面面,随着技术的日新月异,蓝牙BLE一定会得到更广泛的应用,希望看完这篇总结,大家能够有所收获。

之所以说是总结,因为引用的内容占了一半。有些来自其他优秀的文章,有的来自官方文档,由于实在查阅了很多的资料,写到最后已经找不到了来源,因此就省略了参考出处,希望不要介意,目的只是总结和分享,文章对你有些帮助和启发,就点个赞让我看到吧。

多谢查看,这里也祝大家元旦快乐!

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

推荐阅读更多精彩内容