BLE in iOS
最近在研究和编写 BLE 相关的代码,花了一个星期的时间,想彻底搞清楚 BLE 的工作过程和用法需要查阅大量的资料(资料列在了最后面)和不断的编写 demo 来验证。本文从开发者的角度,从几个方面,总结了下目前对 BLE 的理解,随着后面研究的深入,内容将会持续补充。
BLE 101
在 BLE 之前是蓝牙 3.0,蓝牙 3.0 也叫 high speed bluetooth 主要是为快速传输而设计。蓝牙 4.0 也叫 Bluetooth Low Energy(BLE),起源于 Nokia 在 2001 年的一项研究,后来这个研究被 IEEE 采纳,被标准化为 ZigBee,Nokia 在这基础上又进一步开发了 Wibree 标准,在 2007 年 Bluetooth SIG 开始基于这个标准构建 BLE,在 2010 年 6 月发布了蓝牙 4.0 标准。和传统蓝牙 3.0 相比,BLE 的功耗较低,它设计的初衷是将蓝牙用到纽扣电池的设备上,让这种设备工作的时间更长,传输的距离更远。低功耗的原因主要是下面几点:
- 它的协议栈(GATT)重新设计了,packet 长度变少了。
- 它采用的是 adaptive frequency hopping,这种自适应跳频模式和传统蓝牙相比更省电。
- 只在工作的时候被唤醒。根据 WWDC2012 中给出的数据来看,BLE 会比传统蓝牙节省约 8 倍的电量,但是它同时也牺牲了一定的性能,传输速率和高速蓝牙相比大大降低(右图),这意味着我们用 BLE 传大量的数据:
Session703 还列出了 BLE 的几种使用场景:医疗,运动,安全,自动化家居,娱乐,玩具,支付,多人通信。这几种场景在未来都有足够的想象力,目前来看市场上针对这几种场景都有对应的产品,我研究 BLE 的初衷是玩具,我的目的是通过 iPhone 控制玩具。这也是 BLE 的另一个好处,它让 iOS 设备摆脱了 MFi 的限制,同时又能和 Android 设备互联,这点我认为意义更大。在 2013 年的 WWDC 上,Apple 展示了一种看似很神奇的 iBeacon 技术,它本质上就是一个特定工作模式的 BLE 设备,后面我们会来详细讨论 iBeacon。
接下来我们来介绍一些 BLE 的术语:
- Central/Master:可以理解为 client,也可以扫描周围的 BLE 设备同时可以连接这些设备。
- Peripheral/Slave:可以理解为 server,一般是不断的广播自己,可以被 Central 连接,通过 UUID 来标识自己。
- Service:Peripheral 设备提供的服务,一个 Peripheral 可以提供多种自定义服务和标准化的服务:比如设备信息(UUID:0x180A),电池信息(UUID:0X180F),时间信息(UUID:0X1805 等)等。service 通过 UUID 来标识自己。
- Characteristic:service 中支持的数据传输方式,比如,你是想读 service 中的内容,或者向 service 中写数据,还是希望 service 向你 push 数据等。一个 service 可以支持多种数据传输方式(characteristic),通过 UUID 来区分,其中一些 UUID 是不能被占用的。在数据传输方式上,BLE 规定了几种标准的数据传输模式,常用的有 4 种:
- Read/write:central 和 peripheral 之间的数据交换。
- Notify/indicate:由 central 开启,但是由 peripheral 发起数据传输,notify 允许 peripheral 向 client push 数据,indicate 是双向的。
Packet Format
在讨论 BLE 工作方式之前,有必要先了解它的包格式。根据参考文献 3,包格式为:
- 包含 8bit 前缀
- 32bit 的跳频信道地址,对于广播包,这个值永远是 0x8E89BED
- 然后是 2-39 字节的 PDU
- 最后是 24bit 的循环校验码。
这中间我们关心的是 PDU,因为里面包含了我们要传输的数据。一个 PDU 首先是 2 字节的头标识 payload 的大小和类型(Event_Type,对应第四节的四种广播包类型),然后是 6 个字节的 MAC 地址,最后是 31 字节可用的 payload。
iBeacon
根据上面我们了解的术语,我们先来定义一下 iBeacon。iBeacon 是一个不断广播,不提供 service,不能被 connection 的 peripheral 设备。它发送的 packet 的 payload 如下
02 01 06 1A FF 4C 00 02 15 B9 40 7F 30 F5 F8 46 6E AF F9 25 55 6B 57 FE 6D 00 49 00 0A C5
这一共有 30 字节,也就是 payload 中的内容,没有 PDU 的头和 MAC 地址。这实际上就是 iBeacon 的包格式
02 01 06 1A FF 4C 00 02 15: iBeacon prefix (fixed)
B9 40 7F 30 F5 F8 46 6E AF F9 25 55 6B 57 FE 6D: proximity UUID (here: Estimote’s fixed UUID)
00 49: major
00 0A: minor
C5: 2’s complement of measured TX power
也就是说对于 31 字节的 payload,从第 6 字节开始后的四字节是:4C 00 02 15
的 advertising packet 都属于 Apple 的 iBeacon 设备。并且 iBeacon 不提供 service 也就不支持 peripheral 的 connection,下面是 iBeacon 的包格式:
由于 iBeacon 设计的场景就是室内购物,因此它规定了一些字段来应对这种场景:
- UUID:用来区分不同的 iBeacon 设备,比如有一个连锁店,所有分店内的 iBeacon 都应该具有相同的 UUID。
- major:2 字节,用来归纳一组 iBeacon 设备,比如同一家店,部署了多个 iBeacon,那么他们的 major 值应该相同。
- minor:2 字节,用来区分每一个 iBeacon,同一店中的每个 iBeacon 的 minor 值应该不同。
- TX:这个值直接对应 BLE 的 RSSI,换算方法为 RSSI = - (256 - TX)db。以上面的 payload 为例:RSSI = -(256-197) = -59db。如果是学通信的对这个单位应该并不陌生,蓝牙信号本质上也是电磁波,是电磁波就会有功率,db 就是描述功率的幅值,也可以理解为能量的强弱。我们可以通过这个值来计算 iBeacon 和 client 的距离,但是这种计算并不精确,因为蓝牙这种电磁波本身就很弱,很难穿墙,对于室内复杂的环境,这个值并不准确。
后面会对 iBeacon 做代码上的分析,如果你已经了解了基本的 BLE 工作方式,也可以略过下一节,直接阅读 More on iBeacon 一节。
BLE 工作原理
BLE 工作在 2.4GHZ-2.4835GHZ 频段范围内,一共有 40 信道用来跳频通信,信道间隔为 2MHZ(2.4GHZ + 40*2MHZ ~= 2.48GHZ)。在 40 个信道中,3 个信道:No.37,No.38,No.39 用来广播数据,剩下 37 个用来传输数据。
Advertising
首先广播数据,BLE 设备(peripheral)会在 37,38,39 信道上间歇性的广播数据,选择这三个信道是为了避开和 wifi AP 的干扰。同时 iphone(central)在 37,38,39 信道上间歇性的扫描数据:
在广播阶段,BLE 设备可以广播四种格式的 packet:
- Non-connection Advertising : BLE 设备会一直处于 Advertising 状态,设备不可链接,一般 Beacon 设备都是广播这种 packet。
- Discoverable Advertising : 和第一种类似,但是 client 可以在不和 BLE 设备建立链接的情况下发送一个 request 来获得更多的信息。这种通常不常用。
- General Advertising:这种就是通用的模式,client 可以和 BLE 设备建立链接并传输数据。
- Directed Advertising:这种包用来让 client 快速重连 BLE 设备,这种模式目前 iOS7 还不支持。
Connection
一旦 iPhone 找到了要连接的 BLE 设备,它会向 BLE 设备发送 connection 的 request,如果 BLE 设备支持链接(iBeacon 就不支持),则会通知 iPhone 链接成功:
值得注意的是,这种链接不需要配对的过程,这可能会很奇怪,如果 A 先链接了 BLE 设备,那么 B 则无法链接,这无疑存在着很大的安全隐患,我们后面再来探讨这个问题。
Transfer Data
链接建立起来后,就可以传数据,值得注意的是,由于前面已经降到 BLE 是 low energy 的,它在传数据的时候(比如 peripheral 向 central 发送一副很大图片)带宽其实很小的,一次最多发 30 个 byte,而且还不是一次性传完,传输的过程也是有时间间隔的,这意味着传输的时间也会变长。
Topology
拓扑图,什么意思呢?很简单:
- 对于可连接的 BLE 设备,它自己可以是 central 也可以是 peripheral:一个 central 可以链接多个 peripheral,但是一个 peripheral 只能被一个 central 连接。
- 对于不可连接的 BLE 设备,如 iBeacon,它只能是 peripheral,只能广播数据。
Test and Debug
文字理解毕竟不够直观,理解 BLE 工作方式最好的办法就是亲自 Debug。
目前有三种 Debug 的方式是比较有效的,这三种方式我认为也是理解 BLE 的最佳学习曲线:
-
使用客户端工具摆弄 BLE 设备,做感性认识,找到规律,形成假设。这个工具推荐使用 iOS 的一个 App:LightBlue。
-
抓包分析,验证你的假设从而得出结论或者提出新的假设。抓包工具在 windows 下可以使用 TI 的一个很好用的Sniffer,在 Mac 下,比较麻烦,我尝试了使用 CSR4.0 USB dongle,失败了,最终的办法是将系统升级到 10.9,使用 XCode 新的蓝牙 Hardware Tool:
- 编写代码,进一步印证你的结论。
这一小节,主要讨论第两点,关于 LightBlue 的使用简易明了,关于代码,我们放到后面做详细讨论。
由前一节的讨论我们知道,建立通信的第一步是 Advertising,我们抓取一个 Advertising 的 packet:
EventType 标识了这个 Advertising packet 是前一节提到的第三种,可支持连接的设备。关于 EventType 的值是不会定义到 payload 中的(见第二节,关于 EventType 更多内容,可以参考文献 11)。Peer_Address 是这个设备的 MAC 地址,通常这个值能被 sniffer 抓到,但是在程序中是无法获取的。16bit 的 UUID 是目前来看是 service 的 UUID;Data 是 packet 的 payload,第一个字节 0x02:后面有两个字节内容,然后是 0x03 标识后面有三个字节内容。
当 BLE 谁被被 scan 后,会发一个 response 的 packet 给 client,里面包含了一些信息: LocalName 标识了这个设备的 name 存到了 payload 中,Data 中的字段解析后就是这个 localName 的名称
在测试中有两点让我很费解的地方:
- 在 lightBlue 中看到的 device 的 UUID:34EE-….是哪里来的?
- 在 lightBlue 中看到的 device 的 name:ZBModule01 是哪里来的?
关于这两点的讨论,分别对应参考文献的 8 和 9,从目前搜集到的资料来看,没有答案,又找不到官方的解释,只能猜测,我谈谈自己的看法: 第一点是 device UUID 这个值,应该是 iOS 自己根据某种规则生成的 Unique 值,然后 cache 到了系统中的,原因是你是没办法指定设备的 UUID 的(至少我没找到方法)。而且在不同的 iOS 设备上看到同一个 BLE 设备的 UUID 值是不同的。第二点是 ZBModule 这个 name 是怎么读到的呢?Advertising packet 的 local name 和这个值不同,关于这点我还没有线索。
CoreBluetooth on iOS device
接下来我们开始讨论一些代码。
Apple 在 iPhone4s 之后的终端设备开始支持 BLE,并提供了一套 API 叫做 CoreBluetooth:
CoreBluetooth 完整的实现了 GATT,但是我个人的观点是 CoreBluetooth 这个 framework 设计的不是一般的差:
- API 断档很严重,从 5.0 到 7.0
- 回调基于 delegate pattern,代码组织很散乱
- 使用 CentralManager 完成和 peripheral 的数据传输,CentralManger 的代码中居然要耦合 peripheral 的代码(WWDC2012 session705 的 demo),再加上上面第二点,20 多个回调的方法,轻松完爆你!
- API 设计太麻烦,比如很简单的一个需求:central 向一个 peripheral 写数据,居然要写 100 多行。
上面列出的第一个问题,我认为是所有问题的根源,设计的时候显然是没有想好,准备先开放一些 API 给大家玩,收集大家的反馈,如果用的人多,我们再开放一些 API。这种想法没错,但是最初的 API 和 framework 的设计扩展性太差,使后面版本扩展起来很麻烦,又不能重构,只能这么将就下去了。因此,我极其不建议通过 Apple 的 demo 来学习 CoreBluetooth,如果没人跟你解释,你根本学不明白。在 Github 上有一些开源的项目,比如YmsCoreBluetooth实现了扫描 BLE 设备的代码,这个开源项目功能很简单,但是作为学习 CoreBluetooth 足够了。
2011 年 iOS 5.0 率先引入 CoreBluetooth,只提供 Central 端的 API,这意味着你只能作为 client 链接别人,接受广播。但是,如果你想作为 Peripheral 和 client 交换数据,就要用到 iOS 6.0 的 API 了:
iOS 6.0 之后 CoreBluetooth 的 API 框架就基本定型了,到了 iOS 7.0,Apple 对它又进行了一些优化,包括 API 的重新设计,一些方便的状态判断等。 在性能上,iOS 6.0 先引入了 caching 机制,这个缓存机制只做到了 Central 端,也就是说系统会自动 cache 你扫描到的 BLE 设备,并且记住它提供的 service 和 characteristic。iOS 7.0 在这个基础上增加了更多 cache 的内容,但是 iOS7 对 BLE 最大的优化在传数据上,它提高了 20%的传输带宽,此外在支持后台运行上,iOS7 做了较大的改动:
比如我们的程序在和 BLE 设备进行数据传输或者其它操作,它是一个 long-term 的 task,我们把它退到了后台,结果怎样呢?
根据 WWDC2013 session703 的内容,我们的程序因为系统内存不足要被 kill 掉,这时候,系统会接管我们的 BLE 任务,然后让它持续运行。这一点我还没来的及验证,如果真的可行,那确实很 cool。想要实现它,首先要修改 info.plist:
然后在构造 CentralManager 时,要对指定 option:
_cbManager = [[CBCentralManager alloc]initWithDelegate:self queue:_queue options:@{CBCentralManagerOptionRestoreIdentifierKey:kCentralRestoreIdentifier}];</pre>
然后当 app 在后台被唤起到前台时,CBCentralManager 会首先回调这个方法:
- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary *)dict
{
//do something to restore previous state..
}
最后,如果程序中有多个 CBCentralManager,在被唤醒时,需要判断一下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSArray* centralManagerIds = launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
for (NSString* identifier in centralManagerIds) {
if ([identifier isEqualToString:@"abc"]) {
//get your manager
}
}
// Override point for customization after application launch.
return YES;
}
More on iBeacon
有了上面的基础,我们可以自己定义一个 iBeacon 设备,他要满足如下条件:
- 在 packet 的 Header 中,要指定 Event_Type 为:
- 它 packet 的 payload 中,要符合第三节中提到的规范。
满足这两点,我们可不可以通过 CoreBluetooth 的 API 来创建一个 CBPeripheralManger 来表征 iBeacon 呢?答案是 NO! 原因是 CoreBluetooth 创建的 CBPeripheralManager 都是可链接的设备:“kCBAdvDataServiceUUIDs”。
在 WWDC2013 中,Apple 提供了 AirLocate 的 demo,提供了创建 iBeacon 的 API:
NSDictionary *peripheralData = nil;
_region = [[CLBeaconRegion alloc] initWithProximityUUID:self.uuid major:[self.major shortValue] minor:[self.minor shortValue] identifier:BeaconIdentifier];
peripheralData = [_region peripheralDataWithMeasuredPower:_power];
if(peripheralData)
{
[self.peripheralManager startAdvertising:peripheralData];
}
这种方式创建出的 CBPerpheralManager 的类型是:” kCBAdvDataAppleBeaconKey”。
关于 iBeacon,我认为有下面几点需要考虑:
- 部署
由于 BLE 工作在 2.4GHz,会和 wifi 的 AP 产生一定的干扰,因此 Beacon 在部署上要避开 wifi 设备。同样,也不能部署在金属架子上,这会影响新号的衰减,影响定位等,部署是个大问题。
- 更新
由于目前 iBeacon 的数据都是烧写死的,不支持在线更新。那么如果大规模部署后,更新数据将是一场灾难。这种情况同样适用于其它的自定义 Beacon,一种解决方案是将 Beacon 和 wifi 集成,也许 Arduino 是个不错的选择。
- 用作支付
在 iBeacon 发布之后,Paypal 发布他们的 Paypal beacon,允许用户通过蓝牙进行支付,PayPal Beacon 的系统要比 iBeacon 复杂的多,PayPal beacon 是可连接的 beacon,并且上面集成了 wifi 模块。它的工作模式猜测是下面几步:
- PayPal Beacon 广播,客户端发现后与其建立加密的链接。
- PayPal Beacon 收到客户端传来的认证信息后通过 wifi 到 server 上进行认证,认证成功后客户端会收到消息。
- server 端认证成功后,会将信息发送给店里的 POS 机
- 当用户付钱时,需要做进一步的认证,比如收到短信验证码之类的。
Resources
- WWDC2012 Session 703,705
- WWDC2013 Session 703
- Bluetooth Low Energy Version 4.0</a>
- How do iBeacons work?
- BLE master/slave, GATT client/server, and data RX/TX basics
- A simple way to simulate Apple iOS7 iBeacon feature with two iOS6 devices
- stackoverflow:What is the iBeacon Bluetooth Profile
- ,Peripheral UUID?
- reading the Bluetooth peripheral device name
- BLUETOOTH LOW ENERGY, BEACONS AND RETAIL
- BLE 包结构及传输速率