Bluetooth 笔记(摘抄)
BLE开发的各种坑 (https://www.race604.com/android-ble-tips/)
12 MARCH 2015 on Android, Bluetooth
这段时间在做低功耗蓝牙(BLE)应用的开发(并不涉及蓝牙协议栈)。总体感觉 Android BLE 还是不太稳定,开发起来也是各种痛苦。这里记录一些杂项和开发中遇到的问题及其解决方法,避免大家踩坑。本文说的问题有些没有得到官方文档的验证,不过也有一些论坛帖子的支持,也可以算是有一定根据。
-
Android 从 4.3(API Level 18) 开始支持低功耗蓝牙,但是只支持作为中心设备(Central)模式,这就意味着 Android 设备只能主动扫描和链接其他外围设备(Peripheral)。从 Android 5.0(API Level 21) 开始两种模式都支持。BLE 官方文档在 这里。
-
在
BluetoothAdapter.startLeScan()
的时候,在BluetoothAdapter.LeScanCallback.onLeScan()
中不能做太多事情,特别是周围的BLE设备多的时候,非常容易导致出现如下错误:E/GKILINUX(17741): ##### ERROR : GKIexception: GKIexception(): Task State Table E/GKILINUX(17741): #####
E/GKILINUX(17741): ##### ERROR : GKIexception: TASK ID [0] task name [BTU] state [1]
E/GKILINUX(17741): #####
E/GKILINUX(17741): ##### ERROR : GKIexception: TASK ID [1] task name [BTIF] state [1]
E/GKILINUX(17741): #####
E/GKILINUX(17741): ##### ERROR : GKIexception: TASK ID [2] task name [A2DP-MEDIA] state [1]
E/GKILINUX(17741): #####
E/GKILINUX(17741): ##### ERROR : GKIexception: GKIexception 65524 getbuf: out of buffers#####
E/GKILINUX(17741): ##### ERROR : GKIexception:
E/GKI_LINUX(17741): **********************开发建议:在
onLeScan()
回调中只做尽量少的工作,可以把扫描到的设备,扔到另外一个线程中去处理,让onLeScan()
尽快返回。 [参考帖子] -
在使用
BluetoothDevice.connectGatt()
或者BluetoothGatt.connect()
等建立BluetoothGatt
连接的时候,在任何时刻都只能最多一个设备在尝试建立连接。如果同时对多个蓝牙设备发起建立 Gatt 连接请求。如果前面的设备连接失败了,后面的设备请求会被永远阻塞住,不会有任何连接回调。开发建议:如果要对多个设备发起连接请求,最好是有一个同一个的设备连接管理,把发起连接请求序列化起来。前一个设备请求建立连接,后面请求在队列中等待。如果连接成功了,就处理下一个连接请求。如果连接失败了(例如出错,或者连接超时失败),就马上调用
BluetoothGatt.disconnect()
来释放建立连接请求,然后处理下一个设备连接请求。 [参考帖子] -
对 BluetoothGatt 操作
(read/write)Characteristic()
,(read/write)Descriptor()
和readRemoteRssi()
都是异步操作。需要特别注意的是,同时只能有一个操作(有些贴这说只能同时有一个writeCharacteristic()
,这个我并没有严格验证),也就是等上一个操作回调(例如onCharacteristicWrite()
)以后,再进行下一个操作。开发建议:把这写操作都封装成同步操作,一个操作回调之前,阻塞主其他调用。[参考帖子]
-
BLE 设备的建立和断开连接的操作,例如
BluetoothDevice.connectGatt()
,BluetoothGatt.connect()
,BluetoothGatt.disconnect()
,BluetoothGatt.discoverServices()
等操作最好都放在主线程中,否则你会遇到很多意想不到的麻烦。开发建议:对
BluetoothGatt
的连接和断开请求,都通过发送消息到 Android 的主线程中,让主线程来执行具体的操作。例如创建一个new Handler(context.getMainLooper());
,把消息发送到这个Handler
中。 [参考帖子] -
如果你在开发 BLE 应用的时候,有时候会发现系统的功耗明显增加了,查看电量使用情况,蓝牙功耗占比非常高,好像低功耗是徒有虚名。使用
adb bugreport
获取的了系统信息,分析发现一个名叫BluetoothRemoteDevices
的WakeLock
锁持有时间非常长,导致系统进入不了休眠。分析源代码发现,在连接 BLE 设备的过程中,系统会持有 (Aquire)这个WakeLock
,直到连接上或者主动断开连接(调用disconnect()
)才会释放。如果BLE设备不在范围内,这个超时时间大约为30s,而这时你可能又要尝试重新连接,这个WakeLock
有被重新持有,这样系统就永远不能休眠了。开发建议:对BLE设备连接,连接过程要尽量短,如果连接不上,不要盲目进行重连,否这你的电池会很快被消耗掉。这个情况,实际上对传统蓝牙设备连接也是一样。 [参考帖子]
-
Android 作为中心设备,最多只能同时连接 6 个 BLE 外围设备(可能不同的设备这个数字不一样),超过 6 个,就会连接不上了。现在 BLE 设备越来越多,其实并不够用,所以在开发的过程中,需要特别的谨慎使用。
开发建议:按照需要连接设备,如果设备使用完了,应该马上释放连接(调用
BluetoothGatt.close()
),腾出系统资源给其他可能的设备连接。 [参考帖子] -
发起蓝牙Gatt连接
BluetoothDevice.connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback)
,这里有一个参数autoConnect,如果为true
的话,系统就会发起一个后台连接,等到系统发现了一个设备,就会自动连上,通常这个过程是非常慢的。为false
的话,就会直接连接,通常会比较快。同样,BluetoothGatt.connect()
只能发起一个后台连接,不是直接连接。所以这个地方需要小心。public boolean connect() { try { mService.clientConnect(mClientIf, mDevice.getAddress(), false, mTransport); // autoConnect is inverse of "isDirect" return true; } catch (RemoteException e) { Log.e(TAG,"",e); return false; } }
开发建议:如果你需要快速连接(通常情况下我们都希望这样),在
connectGatt()
的时候,传入autoConnect=false
的参数。如果需要调用BluetoothGatt.connect()
连接,可一通过反射的方式,强制mService.clientConnect()
发起直接连接,也就是传入参数isDirect=true、
- form https://stackoverflow.com/questions/22214254/android-ble-connect-slowly/23749770#23749770
Android 上的低功耗蓝牙实践
21 NOVEMBER 2016 on Android, Bluetooth
这是我在 Droidcon Beijing 2016 和 GDG Devfest 2016 上做的分享,以下是正文:
Slide 01我今天分享的主题是 Android 上低功耗蓝牙的实践。这个主题比较小众。我在过去的一年多的时间里,主要是在做低功耗蓝牙相关的开发。接触过程中发现,BLE 的开发和通常的 Android APP 的开发有点不一样,这里需要访问硬件资源,而且涉及到一些协议相关的内容,而且这方面的资料也比较少。今天我从 Android 开发者的角度,来分享一下低功耗蓝牙开发实践。
Slide 02今天分享的内容,主要包含如下几个部分:首先对蓝牙和低功耗蓝牙做一个简单的介绍;然后介绍 Android 上对低功耗蓝牙的支持;再介绍一下在 Android 平台上可以开发哪些低功耗蓝牙应用;然后是,开发过程中,可以帮助我们调试的工具;最后,总结一下所谓的 “最佳实践”,低功耗蓝牙开发的一些小经验。
Slide 03在介绍低功耗蓝牙之前,不得不说一下它的超集—蓝牙。蓝牙普及率相当的高,相信每个人都知道它,标识是这样的(图片)。简单来说,蓝牙其实就是一个近距离无线通信技术,最早是由爱立信研发出来。蓝牙 Bluetooth 这个名字看起来是一个合成词,蓝色的牙齿,实际上不是,这个词是一个丹麦的国王的绰号,当时研发它的工程师正在看一个关于这个国王的书,就起了这个名字,从此这个国王图片,就带上了蓝牙耳机。
说到蓝牙,就不得不说蓝牙技术联盟(Bluetooth SIG),它负责蓝牙规范制定和推广的国际组织。做蓝牙相关产品的,少不了和它打交道。蓝牙技术联盟同时也拥有蓝牙商标,如果要使用,需要通过他们的授权。
我们回到蓝牙技术本身。蓝牙它有哪些特点呢?首先,近距离通信,典型距离是 10 米以内,传输速度最高可达 24 Mbps,支持多连接,安全性高,非常适合用智能设备上。
简单介绍一下蓝牙的版本演进。99 年蓝牙 1.0 发布,后来发布的蓝牙 2.1 使用最广的,现在绝大部分蓝牙产品都是这个版本,也是我们所谓的经典蓝牙。蓝牙 3.0,也就是所谓的高速蓝牙,最高传输速度可达 24Mbps。蓝牙 4.0/4.1 在这个版本中引入了低功耗蓝牙。蓝牙 5.0 很快也要发布了,主要是针对物联网方向的改进。
Slide 04下面介绍一下低功耗蓝牙,它的全称是 Bluetooth Low Energy,简称为 BLE。从名字可以看出,它的最大特点就是低功耗,一些 BLE 设备可以用一个纽扣电池使用一两年。还有一些其他的有点,例如成本低呀,连接速度快呀,安全性高呀。另外,低功耗不是没有缺点的,低功耗意味着低传输速率,它被设计就是用来传输少量数据的。总体来说,低功耗蓝牙非常适合用在可穿戴设备或者物联网上。
Slide 05下面我们看一下 BLE 的协议栈,作为 Android 开发者,我们不必理解 BLE 的协议栈每个细节,这里大概介绍一下协议架构。我们都知道,协议一般都是分层设计的。BLE 协议栈也不例外。我们来看一下这个图。整个协议栈大致分为三部分,从下到上分别为,控制器(Controller)→主机(Host)→应用(Applications)。
控制器:它是协议栈的底层的实现,直接与硬件相关,一般直接集成在 SoC 中,由芯片厂商实现,包括物理层和链路层。 主机:这是协议栈的上层实现,是硬件的抽象,与具体的硬件和厂家无关。 应用层:就是使用 Host 层提供的 API,开发的应用。
协议栈里面的模块比较多,我们就简单介绍几个和我们开发相关的几块简单介绍一下。
Slide 06首先是物理层。蓝牙是工作在 2.4GHz 附近,这是工业、科学、医疗 ISM 频段。可以看到它和 WiFi 工作在同一个频段。蓝牙把频段切分为 40 个通道,3 个广播通道,37 个数据通道,按照一定规律跳频通信(高斯频移键控 GFSK)。
在 Host 层和 Controller 之间有一个接口层,简称为 HCI。主机和控制器之间就是通过 HCI 命令和事件交互的。HCI 这一层是协议栈中是可选的,例如在一些简单小型的设备上可能就没有,但是所有的 Android 设备上肯定是有。这是蓝牙上层应用和芯片的交互的必经之路。后面我们会讲到,这一层的 log,能够很好的帮助我们分析和调试问题。
Slide 07在 Host 部分,协议结构要复杂一些,有逻辑链路控制和适配层,安全管理模块等等。我们重点来看属性协议,简称为 ATT,它是 BLE 通信的基础。ATT 把数据封装,向外暴露为“属性”,提供“属性”的为服务端,获取“属性”的为客户端。ATT 是专门为低功耗蓝牙设计的,结构非常简单,数据长度很短。
Slide 08接下来我们看一下 GATT,全称叫做通用属性配置文件,它是建立在前面说的 ATT 的基础上,对 ATT 进行进一步的逻辑封装,定义数据的交互方式和含义。这是我们做 BLE 开发的时候直接接触的概念。GATT 按照层级定义了三个概念:服务(Service)、特征(Characteristic)和描述(Descriptor)。他们的包含关系如右边这个图所表示的:一个 Service 包含若干个 Characteristic,一个 Characteristic 可以包含若干 Descriptor。而 Characteristic 定义了数值和操作。Characteristic 的操作这几种权限:读、写、通知等权限。我们说的 BLE 通信,其实就是对 Characteristic 的读写或者订阅通知。还有最外面一层,Profile配置文件,把若干个相关的 Service 组合在一起,就成为了一个 Profile,Profile 就是定义了一个实际的应用场景。
Slide 09这里还要多说一点,Service、Characteristic 还有 Descriptor 都是使用 UUID 唯一标示的。具体的表现形式,我们在后面会讲到。我们先来说说 UUID 是什么? UUID 是全局唯一标识,它是 128bit 的值,为了便于识别和阅读,一般标示程如下的形式,8-4-4-12 的16进制标示。
关于 UUID 有一些规范,为了避免冲突,一般都不会自己手动去定义。例如 Android 中提供了 UUID.randomUUID() 来生成一个随机的 UUID。我们也看到,UUID 有点太长了,在低功耗蓝牙中这种数据长度非常受限的情况下,使用起来肯定不方便,所以蓝牙又使用了所谓的 16 bit 或者 32 bit 的 UUID。其实本质上并没有什么 16bit 或者 32 bit UUID,蓝牙 SIG 定义了一个基础的UUID(Bluetooth Base UUID),形式如下。除了 XXXX 那几位意外,其他都是固定,所以说,其实 16 bit UUID 是对应了一个 128 bit 的 UUID。这样一来,UUID 就大幅减少了,例如 16 bit uuid 只有有限的 65536 个,所以 16 bit UUID 并不能随便使用。SIG 已经预先定义了一些 UUID,如果你想添加一些自己的 16 bit 的 UUID,可以花钱买。
Slide 10再往上,是通用访问控制配置文件,也就是 GAP,由名字可以看出,它定义了 BLE 整个通信过程中的流程,例如广播、扫描、连接等流程。还定义了参与通信的设备角色,以及他们各自的职能,例如广播数据的 Broadcaster,接收广播的 Observer,还有被连接的“外设” Peripheral 和发起连接的“中心设备” Central。可以看到,参与交互的设备角色都不是对等。详细的交互流程,我们放到后面讲。
最后,就是应用层,就是使用 Host 提供的 API 开发的低功耗蓝牙应用。 到这里,我们就把 BLE 的协议栈过了一下,为我们开发 BLE 有了一些理论基础。
Slide 11接下来,我们来看一下 Android 平台上对低功耗蓝牙的支持。
Slide 12从 Android 4.3 Jelly Bean,也就是 API 18 才开始支持低功耗蓝牙。这时支持 BLE 的 Central 模式,也就是我们在上面 GAP 中说的,Android 设备只能作为中心设备去连接其他设备。从 Android 5.0 开始才支持外设模式。
Android SDK 中 BLE 相关的 API 都在 android.bluetooth.* 下面,同时在 Android 5.0 也引入了一些也需要用到 android.bluetooth.le* 下面的 API。
例如,在前面介绍的 GATT 定义的一些概念,都有对应的类,例如 BluetoothGatt/BluetoothGattService/BluetoothGattCharacteristic 等。有了前面的介绍,我们就能很容易的理解这些类的作用。
另外,要在 APP 中使用蓝牙功能,需要在 Manifest 中申请蓝牙相关的权限。在 Android 6.0 及以上平台中,还需要申请定位权限。为什么会这样?因为 BLE 确实有定位的能力,我们后面会讲到。
Slide 13下面我们来看一下,Android 上蓝牙实现的架构。我们来看右边这个图,这是 Android 上一个非常经典的层级结构。最下面硬件部分,可以使用各厂家的具体实现,通过硬件抽象层(HAL)接口统一连接到 Android AOSP 中,通过 JNI 提供 Java 访问接口。蓝牙服务运行在 com.android.bluetooth 进程中,最后通过 Binder 机制向客户端,也就是 APP 提供相关的 API。
关于蓝牙协议栈,这里多说一点。Android 4.2 以前用的是老牌协议栈实现 BlueZ,4.2 开始换成了由 Google 和 Broadcom(博通)联合开发的 BlueDroid,专门在 Android 平台使用。BlueDroid 作为全新的实现,功能不是完善,我们可以看到在 4.3 以后才支持 BLE,在 5.0 以后才支持外设模式,到目前为止,功能其实还是不是很完善。也有一些 Bug。
Slide 14这里介绍一下 Android 中 BLE 操作的过程,APP 发起一个 BLE 操作,然后理解返回,操作结果通过回调上报。操作被封装为一个消息,然后放到协议栈的消息队列中,有一个独立的线程获取消息进行处理,这里非常类似于我们熟知的 Looper 和 Handler 机制。
因为是使用消息机制,回调的时候必须知道通知哪个客户端?客户端发起请求之前,首先要向协议栈注册客户端,注册成功以后,返回一个 clientIf,这是一个整型,是客户端在协议栈的一个句柄,客户端的后续操作,都只需要带上这个 clientIf 句柄即可。
在操作完成的时候,一般都有一个显式的停止操作,用来释放前面的申请的 clientIf 和资源。如果不能正确的释放,不仅会造成内存泄漏,而且可能会导致后续所有的 BLE 操作都是不能做了。因为这个 clientIf 是有限,在现在蓝牙协议栈中只有 32 个,而且是Android 上所有 APP 共用的。当这些资源用完以后,只有通过杀掉对应的 APP 或者重启蓝牙才能恢复。
Slide 15Android 上 BLE 的实现我们就讲到这里,接下来介绍一下 Android 上能够使用 BLE 做一些哪些事情,开发一些什么应用?
Slide 16BLE 应用可以分为两大类:基于非连接的和连接的。
基于非连接的,这种应用就是依赖 BLE 的广播,也叫作 Beacon。这里有两个角色,发送广播的一方叫做 Broadcaster,监听广播的一方叫 Observer。
基于连接的,就是通过建立 GATT 连接,收发数据。这里也有两个角色,发起连接的一方,叫做中心设备—Central,被连接的设备,叫做外设—Peripheral。
Slide 17我们先来看非连接的,也就是基于广播 Beacon 的应用。它的网络拓扑结构如下。我们知道广播是单向的,Broadcaster 向外广播,监听者接收附近的广播,整体来说形成一个单向的星型。网络中可以有多个外设,也可以有多个监听者(动画)。
完全基于广播的应用,有大名鼎鼎的 iBeacon,这是苹果公司定义的基于 BLE 广播实现的功能,可以实现广告推送和室内定位。这也说明了,APP 使用 BLE,需要定位权限。还有前段时间比较火的,手机 QQ 可以“寻找丢失儿童”的项目,其实也是这个原理,在儿童用品中植入可以发送特定广播的设备,手机扫描到了,根据手机的地理位置,就能大致确定范围。
还有些设备,可以同时实现两个角色,既能发送广播,也可以接收广播。一个设备接收到广播,可以通过处理,然后再转发出去,这样就可以形成一个双向的网格,这就是蓝牙的 Mesh。这样的网络可以不受蓝牙传输距离限制了,只要在空间中布置足够密集的节点,就能把信息从网络一点,传递到任何一点。这个可以应用在物联网和智能家居系统中。
前面在介绍协议栈物理层的时候,我们知道广播只在37、38、39这三个广播频道进行广播,监听者也在这三个频道进行监听。我们前面介绍了,蓝牙通信是跳频的,只有双方设备在某个时刻跳到同一个频到上,才能收到广播,这种传播数据效率比较低,数据量也有限,不适合大规模的数据传输。
Slide 18接下来我们介绍一下广播的数据,也就是广播包。我们所说的广播数据其实包含两部分:Advertising Data(广播数据) 和 Scan Response Data(扫描响应数据)。通常情况下,广播的一方,按照一定的间隔,往空中广播 Advertising Data,当某个监听设备监听到这个广播数据时候,会通过发送 Scan Response Request,请求广播方发送扫描响应数据数据。这两部分数据的长度都是固定的 31 字节。在 Android 中,系统会把这两个数据拼接在一起,返回一个 62 字节的数组。
广播数据包的结构如这个图所示(动画)。广播包中是包含一个一个的小 AD structure,每个 AD structure 是一个完整的数据,它的结构是:第一个字节表示长度 n,后面紧接 n 个字节的数据。数据部分第一个字节表示数据类型,也就是后面的数据含义,后面 n - 1 个字节表示真实数据。例如 0x08 是设备的名字,后面的数据就是设备名字的 UTF-8 编码。
这些广播数据可以自己手动去解析,在 Android 5.0 也提供 ScanRecord 帮你解析,直接可以通过这个类获得有意义的数据。
广播中可以有哪些数据类型呢?设备连接属性,标识设备支持的 BLE 模式,这个是必须的。设备名字,设备包含的关键 GATT service,或者 Service data,厂商自定义数据等等。
这里我们也可以看到,广播数据只有最多62字节,所以广播数据空间每个直接都非常珍贵。一般都把哪些数据放到广播中呢?原则上是广播中要放一些能够表达设备身份的数据,还有一些需要暴露的必要数据。因为空间的限制,所以基于广播做不了太复杂的应用,到了 Bluetooth 5.0,据说要大幅扩展广播数据的容量,扩展到 512 字节,这时想象空间就比较大了。
另外,再顺带提一下,无线通信中基本都有信号强度的概念 — 也就是 RSSI,RSSI 单位是 dB,通过 RSSI 能够大致推测出距离的远近。但是这个在 Android 设备上非常不靠谱,RSSI 的值波动很大,跟环境和手机的角度关系很大。
Slide 19我们来直观看一下扫描到的结果是什么样。例如我们使用一个 APP 扫描,扫到一个设备,这些内容都是从广播数据中解析出来的,例如设备名字,设备类型,GATT 服务数据,产商自定义数据等。原始数据是这样的(动画),这里是一个 62 字节的16进制标示,下面这个表格中,每一行就是一个 AD Structure。
Slide 20我们来看一下 Android 作为接收者怎么接收广播数据,扫描设备。代码其实很简单,首先创建一个 LeScanCallback,用来接收收到广播以后,回调上报数据。然后会用 BluetoothAdapter 的 startLeScan 来开始扫描,需要停止扫描的时候,使用 stopLeScan 来停止。
我么来详细看一下这个回调函数,onLeScan,有 BluetoothDevice 这个参数,代表扫描到的设备,关键是设备的的 MAC 地址信息。然后就是 RSSI,表示扫描到的设备的信号强度,接下来 scanRecord 就是我们前面介绍的广播数据,这个数据的长度是62字节。值得提的一点是,BLE 所有回调函数都不是在主线程中的。
这里有几点需要注意,这里在不需要扫描以后,一定要 stopLeScan,而且 start 和 stop 中传入的 LeScanCallback 一定要是同一个,因为 LeScanCallback 就是我们客户端的标识。否者就会出现我们前面说的 clientIf 不释放的问题。在 Android 开发中,我们经常会使用匿名内部类来做参数,在这里就千万不要这么做。
在 Android 5.0 中,提供了全新的扫描 API — BluetoothLeScanner,它提供了对扫描更加精细的控制。
除了这种方法,还可以使用经典蓝牙扫描的方式,BluetoothAdapter 的 startDiscovery(),然后通过 BroadcastReceiver 来接收收到的广播。如果只是做 BLE 的开发,不建议使用这个方法,这是一个非常重的操作,灵活性非常差。
Slide 21接下来我们来看一下扫描的工作流程。首先 APP 发起扫描请求,通过蓝牙的 Service 发送请求给蓝牙芯片。蓝牙芯片开始扫描,扫描到了设备,就通过回调上报。我们知道,扫描真正执行实在 BT 芯片中,只要 APP 发送了请求下去以后,Android 系统就可以休眠了,也就是我们常说的 AP (Application Processor),等扫描到了设备以后,底层 BP (Baseband Processor)就会唤醒上层 AP,执行回调通知到 APP,(动画)就像我们图中红色框标出的这样。这里有一个问题,随着我们周围的 BLE 设备逐渐增多,频繁扫描到设备,系统就会被频繁的唤醒,甚至睡眠不下去,从而导致耗电严重。
为了避免这种问题,耗电的问题。我们需要尽可能少的使用扫描。即使需要扫描,我们也希望尽可能少的上报扫描到的设备。这里就可以使用 Android 5.0 上提供的新接口,设置 ScanFilter,通过一定的规则过滤,只有扫描到了符合我们的规则的设备才上报,或者通过设置延迟上报,从而减少唤醒系统的次数。
这里总结一下扫描中一些建议。1、首先,尽可能使用新的 API,功能更强大;2、尽可能少地扫描,因为毕竟扫描是一个比较重的操作,耗电,也会减慢 BLE 连接速度;3、扫描的时候,尽量设置 ScanFilter,只扫描那些你感兴趣的设备,而不是全盘扫描;4、正确使用 API,特别是合理停止扫描,防止资源泄漏。
Slide 22下面我们来看一下 Android 作为 Broadcaster 的应用。从 Android 5.0 开始,Android 设备就可以像外设一样发送 BLE 广播了。这时 Android 设备之间就可以通过 BLE 来交互数据,或者发现对方设备了,例如类似 NFC 一样交换简单信息的应用,想象空间还是很大的。
Android 中实现的代码如下,通过前面的介绍,我们知道广播有两种包:Advertising Data 和 Scan Response Data,我们这里设置好这两种包,然后通过 BluetoothLeAdvertier 的 startAdvertising 就可以了。这里需要注意的点和前面一样,Start 了,需要注意 Stop。
Slide 23非连接的应用我们就讲到这里,接下来我们将基于连接的 BLE 应用。它的网络拓扑如图所示。我们看到,参与通信的有两个角色:中心设备(Central)和外设(Peripheral)。一个中心设备可以连接多个外设,一个外设只能被一个中心设备连接。中心设备和外设之间的通信是双向的。可以看到这是一个典型的星型结构。其实一个中心设备能够同时连接的设备数量也是有限,一般最多连接 7 个外设。
Slide 24BLE 连接的建立是通过 GAP 来协商和创建连接。Central 设备发起连接,外设接收连接请求,并协商连接参数。
前面我们介绍了 GATT,GATT 核心内容就是 Service、Characteristic 以及 Descriptor。每个 BLE 外设,根据自己的功能,向外暴露 Service 等。其实最重要的获取 Service 中的 Characteristic,Characteristic 可以被读、写、还有变化的时候有通知,这样就实现了双向的通信。
Slide 25我们通过一个例子来简单看一下 GATT 的结构。在右边这个图中,我们连接了一个小米手环2,这里个列表中每一行就是一个 Service,例如 Generic Access、Device Information 等,还有没有名字的,直接显示了一串 UUID。这是怎么回事呢?前面我们介绍了,蓝牙联盟已经预先定义了一些 Service,都是一些 16bit UUID 的。这些Service有标准的含义和用途。例如 Generic Access 这个 service 就描述了设备连接相关的属性。还有 Immediate Alert 服务,标识设备支持标准的即时提醒功能,例如小米手环作为提醒通知设备,Hear Rate 表示心率服务。而直接显示 UUID 的标示厂商自定义的,有 128 bit 的长的,也有 16 bit 短的,短的标示购买的,可以看到厂商购买了 FEE0 这个。自定义 Service 具体怎么操作,里面属性的含义只有厂商自己知道。
这里也展开了两个 Service,里面列出来的是 Characteristic,有些还有 Descriptor。后面标签标示这个特征有哪些权限,例如 W 标示可写。Character 的 UUID 也是类似,有官方定义的我们直接指导怎么读写,例如我们往 Alert Level 里面写入 1,手环就应该能够发出提醒。直接读取 Hear Rate 就能获得到心率。另外也有厂商自定义的,里面数据的内容,就只有厂商自己知道了。
官方定义了哪些 Service 和 Characteristic,都在蓝牙的官网上有定义,可以通过 KeyNote 中的连接去查看。
Slide 26这是在 Android 中使用 GATT 连接的具体写法。可以看到,Android SDK 相关的类都和 GATT 里面概念一一对应。前面我们也说了,Android 上 BLE 的操作都是异步的,这里也要创建一个 BluetoothGattCallback 用来出来回调,这个类非常重要,所有的 GATT 操作的回调都在这里。
通过 BluetoothDevice 的 connectGatt() 方法得到 BluetoothGatt,表示一个连接,用完以后,记得 close() 来释放资源。
Slide 27我们接下来看一下 Android 中 GATT 操作的流程。右边这个图,APP 是我们的应用,右边蓝牙服务端,从左向右箭头是 APP 发起的请求,从右向左的箭头是回调。我们看到所有的操作都是异步的完成的。连接过程是,首先使用 gattConnect 发起连接,收到 onConnectionStateChange() 通知连接是否成功,若成功,则进行下一步的 discoverService(),这一步就是发现设备所有的 GATT Service,若发现成功,通过 onServiceDiscovered() 回调,这时才算真正的连接成功。然后可以通过 BluetoothGatt 的 getService() 来获得BluetoothGattService,进而获得BluetoothGattCharacteristic 等,然后对 Characteristic 进行读写。
图中这是理想状况。有可能是这样(动画),你在尝试建立一个 GATT 连接,而没有回调返回的时候,同时再发起一个 GATT 连接,这个时候有很大概率连接会失败。所以最好不要同时发起多个连接请求。
另外,在一个 GATT 连接中,你对 Characteristic 发起读写操作的时候,如果上一个请求没有返回之前,不能发起另外一个操作(动画),否者后一个就会失败。所以对于一个连接,请求必须顺序执行,一个请求完成以后,才能进行下一个请求。
我们预期 request 和 response 是成对出现的,但是现实是,有时候会出现发送一个 request,而没有回调的情况。前面我们说了,在前一个回调之前,不能发起新的 request,这样我们是不是就傻等在这里了呢?为了解决这些问题,下面推荐一个解决方案。
Slide 28为了实现只有一个连接请求的需求,我们可以创建连接管理类,在这个类中每次只有一个正在连接的设备,因为连接需要经过多次交互,可以使用状态机是最合适的解决方案。根据回调出发状态转移,同时也设置一个在某个状态停留的时间有个阈值,超过这个时间,即使没有回调也会发生状态转移。把所有的连接请求放到一个队列中,等前面的连接成功或者失败以后,才进行下一个连接请求。
接下来,对于已经建立的 GATT 连接,所有的操作都是异步的,而且需要顺序执行。为了保证这样的逻辑,写起非常麻烦,想象一下你需要连续读写多个 Characteristic 的场景。解决方法是封装异步调用成为同步调用,具体的实现原理是使用 wait/notify 来实现即可。写起来就非常方便,而且不容易出现状态错误。
Slide 29在 Android 5.0 以后,我们还可以把 Android 设备作为外设,被中心设备连接。这个时候开始,Android 设备之间可以真正地使用 BLE 进行双向通信了,应用场景也非常多。具体的实现与 Central 基本的对应,了解了 GATT 原理,这里实现也就非常简单了。这里不详细说了。
Slide 30通过前面的介绍,已经讲解完了 Android 设备作为 BLE 应用的所有四个角色:监听者、广播者、中心设备以及外设。也穿插介绍了一些开发中可能遇到的坑,但是实际开发过程中,我们需要怎么 Debug BLE 相关的问题呢?下面我们介绍一些可以帮助我们的 Debug 工具。
Slide 31首先,我们可以使用一些 BLE 测试 APP 来验证一些基本功能,在应用市场上有很多类似的 APP,这里特别推荐 nRF Connect 这个 App。这个 APP 就像 API demo 一样,实现了所有 Android 提供的 API 能做的事情,APP 质量很高,用起来也非常方便,前面介绍有些截图,就是来自这个 APP。
另外,查看 Log 是最常用的方法。 Logcat 作为 Android 开发者最熟悉不过。Logcat 中可以查看 com.android.bluetooth 这个进程的 log,可以了解 Java 层的一些状态。
更深入一点,我们可以查看协议栈的 log。蓝牙的协议栈 log 默认没有打印,我们可以在开发者选择中打开。(动画)。如图中所示,打开这两个开关:
先说下面这个开关,打开这个开关,在 Logcat 就会中输出 BlueDroid 中的 Log,通过这些 log 可以排查 BlueDroid 里面实现的一些问题。
有时候,有时候知道协议栈的log可能还不够,打开第一个开关,系统就会抓取 HCI 层的 log,log 文件一般在 /sdcard/bt_snoop.log。HCI 层我们之前介绍了,Host 和 Controller 交互的必经之路。这个 log 非常类似我们在网卡上进行的网络抓包。实际上这个 log 文件,也可以通过网络抓包软件 WireShark 打开,如图,这里可以看到所有 HCI 命令和事件,以及对应的数据。通过这个 log 可以排查蓝牙芯片以上的问题。
其实还可以更进一步,可以排查蓝牙芯片是否正常工作,就是通过一些设备直接抓取蓝牙空中包。这个需要购买特定的硬件和软件配合使用。
Slide 32最后我们来总结一下,Android 中 BLE 开发一些所谓的最佳实践。
Slide 33最后我们来总结一下,Android 中 BLE 开发一些所谓的最佳实践。
Slide 34首先,我们要尽量少干一些事情。因为蓝牙的硬件资源有限,对芯片的请求当然是越少越好。另外,BlueDroid 还很年轻,上层对它的压力也是越小越好。我们在保证业务的情况下,可以尽量遵循如下一些原则:尽量少的连接数量,尽量短的连接时间,尽量少的扫描,至于尽量少的 ClientIf,如果在 APP 中多个模块中都要扫描或者连接,应该做尽可能的合并。尽量少的广播数据,尽量少的并行请求,在回调中要做尽量少的工作。
Slide 35另外,我们需要尽量多做一些事情。蓝牙虽然是一个统一的标准,但是不同厂家实现之间还是有一定的差距,例如 MTK 和高通平台是有些不一样的。另外,蓝牙这一块,不同的 ROM 厂家都会有一些修改,需要在不同的 ROM 上都要做一些测试,最后连接的外设,实现也不太一样,也需要做尽量多的测试。
Bluetooth 还在快速的发展,不管是蓝牙规范的制定,还是 Android 平台对蓝牙的支持,给了我们开发者提供了更多的空间和可能性,期待 Android BLE 能够出现更多好用好玩的应用。
Slide 36最后,这次分享没法做到面面俱到,开发过程中还需要更深入的了解,这里有一些参考资料: