BLE,即 Bluetooth Low Energy 低功耗蓝牙。可穿戴产品、无线耳机、无线鼠标等低功耗外设都使用BLE技术。轩霆科技所使用的手环和手表产品就是通过BLE连接与手机同步数据。

本文以官方文档作为主要教材进行学习,所有插图均来源于苹果官方文档,文末提供文档链接。

认识 CoreBluetooth 框架

CoreBluetooth框架的核心是 Central 和 Peripheral,即中心管理与外设。Central 和 Peripheral 的关系 Client 和 Server 的关系一样。



中心发现并连接到正在广播的外围设备

外围设备以广播包的形式广播他们拥有的一些数据。广播数据包是一个相对较小的数据包,可能包含有关外设必须提供的有用信息,例如外设的名称和主要功能。例如,数字恒温器可以广播它提供房间的当前温度。在低功耗蓝牙中,广播是外围设备了解其存在的主要方式。

另一方面,中央可以扫描和侦听任何正在广播其感兴趣的信息的外围设备,如图下图所示。中央可以要求连接到它发现的任何正在广播的外围设备。



外围设备的数据构成

外围设备可能包含一项或多项服务,或提供有关其连接信号强度的有用信息。服务是用于完成设备(或该设备的一部分)的功能或特征的数据和相关行为的集合。例如,心率监测器的一项服务可以是从监测器的心率传感器暴露心率数据。

服务本身由特征或包含的服务(即对其他服务的引用)组成。特性提供了有关外围设备服务的更多详细信息。例如,刚刚描述的心率服务可以包含描述设备的心率传感器的预期身体位置的一个特征和发送心率测量数据的另一个特征。下图说明了心率监测器服务和特征的一种可能结构。



中心与外围设备的数据交互

在中心成功建立与外围设备的连接之后,它可以发现外围设备必须提供的全部服务和特性(广告数据可能只包含一小部分可用服务)。

中央还可以通过读取或写入该服务特征的值来与外围设备的服务进行交互。例如,应用可以从数字恒温器请求当前室温,或者它可以为恒温器提供设置房间温度的值。

设备作为 Central 时

当 Central 和作为外设的 Peripheral 通信时,绝大部分操作都在 Central 这边。此时,Central 被描述为CBCentralManager,这个类提供了扫描、寻找、连接 Peripheral (被描述为CBPeripheral)的方法。

下图标示了 Central 和 Peripheral 在 Core Bluetooth 中的表示方式:



当你操作 Peripheral 的时候,实际上是在和它的 Service 和 Characteristic 打交道,这两个分别由CBService和CBCharacteristic表示。

一个 Peripheral 包含多个 Service,而一个 Service 又可以包含多个 Characteristic,所以他们的关系大致可以表示为:



设备作为 Peripheral 时

在 OS X 10.9 和 iOS 6 以后,设备除了能作为 Central 外,还可以作为 Peripheral。也就是说,可以发起数据,而不像以前只能管理数据了。

那么在此时,它被描述为CBPeripheralManager,既然是作为 Peripheral,那么这个类提供的主要方法则是对 Service 的管理,同时还兼备着向 Central 广播数据的功能。Peripheral 同样会对 Central 的读写要求做出相应。

下图则是设备作为 Central 和 Peripheral 的示意图:



在充当 Peripheral 时,CBPeripheralManager处理的是可变的 Service 和 Characteristic,分别由CBMutableService和CBMutableCharacteristic表示。

下图则是在设备 Peripheral 时,相关类的关系:



作为 Central 时的数据读写

在蓝牙低功耗通信中实现核心作用的设备执行许多常见任务 - 例如,发现和连接到可用外围设备,以及探索外围设备必须提供的数据并与之交互。实现外围设备角色的设备还执行许多常见但不同的任务 - 例如,发布和广告服务,以及响应来自连接中心的读取,写入和订阅请求。

  • 使用CBCentralManager
  • 搜索并连接可用的 Peripheral
  • 连接时候,进行数据接收
  • 对 Characteristic 进行数据读写
  • 当 Characteristic 状态更新时,进行回调

初始化 CBCentralManager

1
myCentralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];

将self设置为代理,用于接收各种 central 事件。将queue设置为nil,则表示直接在主线程中执行。

发现 Peripheral

1
[myCentralManager scanForPeripheralsWithServices:nil options:nil];

第一个参数为nil,表示所有周围全部可用的设备。在实际应用中,你可以传入一个CBUUID的数组(注意,这个 UUID 是 Service 的 UUID 数组,如有不明白可以参考 最佳实践),表示只搜索当前数组包含的设备(每个 Peripheral 的 Service 都有唯一标识——UUID)。所以,如果你传入了这样一个数组,那么 Central Manager 则只会去包含这些 Service UUID 的 Peripheral。

在调用scanForPeripheralsWithServices:options:方法之后,找到可用设备,系统会回调(每找到一个都会回调)centralManager:didDiscoverPeripheral:advertisementData:RSSI:。该方法会以 CBPeripheral 对象返回找到的 Peripheral,所以你可以使用数组将找到的 Peripheral 存起来。

1
2
3
4
5
6
7
8
9
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData
RSSI:(NSNumber *)RSSI {

NSLog(@"Discovered %@", peripheral.name);
self.discoveredPeripheral = peripheral;
...
}

当你找到你需要的那个 peripheral 时,可以调用stop方法来停止搜索。

1
[myCentralManager stopScan];

连接 Peripheral

1
[myCentralManager connectPeripheral:peripheral options:nil];

当连接成功后,会回调方法centralManager:didConnectPeripheral:。在这个方法中,你可以去记录当前的连接状态等数据。

1
2
3
4
5
6
7
- (void)centralManager:(CBCentralManager *)central
didConnectPeripheral:(CBPeripheral *)peripheral {

NSLog(@"Peripheral connected");
peripheral.delegate = self;
...
}

发现 Services

当与 peripheral 成功建立连接以后,就可以通信了。第一步是先找到当前 peripheral 提供的 service,因为 service 广播的数据有大小限制(貌似是 31 bytes),所以你实际找到的 service 的数量可能要比它广播时候说的数量要多。调用CBPeripheral的 discoverServices:方法可以找到当前 peripheral 的所有 service。

1
[peripheral discoverServices:nil];

在实际项目中,这个参数应该不是nil的,因为nil表示查找所有可用的Service,但实际上,你可能只需要其中的某几个。搜索全部的操作既耗时又耗电,所以应该提供一个要搜索的 service 的 UUID 数组。更加详细的内容会在最佳实践中讲到。

当找到特定的 Service 以后,会回调的peripheral:didDiscoverServices:方法。Core Bluetooth 提供了CBService类来表示 service,找到以后,它们以数组的形式存入了当前 peripheral 的services属性中,你可以在当前回调中遍历这个属性。

1
2
3
4
5
6
7
8
9
- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverServices:(NSError *)error {

for (CBService *service in peripheral.services) {
NSLog(@"Discovered service %@", service);
...
}
...
}

发现 Characteristics

找到需要的 service 之后,下一步是找它所提供的 characteristic。如果搜索全部 characteristic,那调用CBPeripheral的discoverCharacteristics:forService:方法即可。如果是搜索当前 service 的 characteristic,那还应该传入相应的CBService对象。

1
2
NSLog(@"Discovering characteristics for service %@", interestingService);
[peripheral discoverCharacteristics:nil forService:interestingService];

同样是出于节能的考虑,第一个参数在实际项目中应该是 characteristic 的 UUID 数组。也同样能在最佳实践中介绍。

找到所有 Characteristic 之后,回调peripheral:didDiscoverCharacteristicsForService:error:方法,此时 Core Bluetooth 提供了 CBCharacteristic 类来表示 Characteristic。可以通过以下代码来遍历找到的 Characteristic:

1
2
3
4
5
6
7
8
9
10
- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverCharacteristicsForService:(CBService *)service
error:(NSError *)error {

for (CBCharacteristic *characteristic in service.characteristics) {
NSLog(@"Discovered characteristic %@", characteristic);
...
}
...
}

同样也可以通过添加 UUID 的判断来找到需要的 Characteristic。

检索 Characteristic 的值

特征包含表示外围设备服务信息的单个值。例如,健康温度计服务的温度测量特性可以具有指示摄氏温度的值。通过直接读取或订阅特征来检索特征的值。

读取 Characteristic 的值

1
2
NSLog(@"Reading value for characteristic %@", interestingCharacteristic);
[peripheral readValueForCharacteristic:interestingCharacteristic];

当你调用上面这方法后,会回调peripheral:didUpdateValueForCharacteristic:error:方法,其中包含了要读取的数据。如果读取正确,可以用以下方式来获得值:

1
2
3
4
5
6
7
8
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {

NSData *data = characteristic.value;
// parse the data as needed
...
}

订阅 Characteristic 的值

1
[peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];

当您订阅(或取消订阅)特征的值时,外设调用外设peripheral:didUpdateNotificationStateForCharacteristic:error:其委托对象的方法。如果订阅请求因任何原因失败,您可以实现此委托方法来访问错误原因,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {

if (error) {
NSLog(@"Error changing notification state: %@",
[error localizedDescription]);
}
...
}

写入 Characteristic 的值

1
2
NSLog(@"Writing value for characteristic %@", interestingCharacteristic);
[peripheral writeValue:dataToWrite forCharacteristic:interestingCharacteristic type:CBCharacteristicWriteWithResponse];

关于写入数据的 type,如上面这行代码,type 就是 CBCharacteristicWriteWithResponse,表示当写入成功时,要进行回调。更多的类型可以参考 CBCharacteristicWriteType 枚举。这个枚举会在下一章中介绍到。

如果写入成功后要回调,那么回调方法是peripheral:didWriteValueForCharacteristic:error:。如果写入失败,那么会包含到 error 参数返回。

1
2
3
4
5
6
7
8
9
10
- (void)peripheral:(CBPeripheral *)peripheral
didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {

if (error) {
NSLog(@"Error writing characteristic value: %@",
[error localizedDescription]);
}
...
}

Characteristic 也可能并不支持写操作,可以通过 CBCharacteristic 的 properties 属性来判断。

参考资料


 评论