在上一章中,我们学习了如何从中央侧执行最常见的低功耗蓝牙任务。在本章中,我们将学习如何使用酷睿蓝牙框架从外设端执行最常见的低功耗蓝牙任务。以下基于代码的示例将帮助你开发应用,以在本地设备上实现外围角色。具体来说,我们将学习如何:
在本章中找到的代码示例简单而抽象;我们可能需要进行适当的更改才能将它们合并到实际应用中。与在本地设备上实现外围角色相关的更多高级主题(包括提示、技巧和最佳实践)将在后面的章节中介绍:iOS 应用的核心蓝牙后台处理和将本地设备设置为外围设备的最佳做法。
在本地设备上实现外设角色的第一步是分配和初始化外设管理器实例(由 CBPeripheralManager 对象表示)。通过调用 CBPeripheralManager 类的 initWithDelegate:queue:options: 方法来启动外设管理器,如下所示:
myPeripheralManager =
[[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
在此示例中,self 设置为委托以接收任何外围角色事件。将调度队列指定为 nil 时,外围管理器将使用主队列调度外围角色事件。
创建外围管理器时,外围管理器调用其委托对象的外围管理器DidUpdateState: 方法。必须实现此委托方法,以确保支持低功耗蓝牙并可在本地外围设备上使用。有关如何实现此委托方法的详细信息,请参阅 CBPeripheralManagerDelegate 协议参考。
如图 1-7 所示,本地外围设备的服务和特征数据库以树状方式组织。我们必须以这种树状方式组织它们,以在本地外围设备上设置服务和特征。执行这些任务的第一步是了解如何识别服务和特征。
外设的服务和特征由特定于 128 位蓝牙的 UUID 标识,这些 UUID 在核心蓝牙框架中由 CBUUID 对象表示。虽然并非所有标识服务或特征的 UUID 都是由蓝牙特别兴趣组 (SIG) 预定义的,但蓝牙 SIG 已定义并发布了许多常用的 UUID,为方便起见,这些 UUID 已缩短为 16 位。例如,蓝牙 SIG 预定义了将心率服务标识为 180D 的 16 位 UUID。此 UUID 是从其等效的 128 位 UUID 0000180D-0000-1000-8000-00805F9B34FB 缩短而来的,后者基于蓝牙 4.0 规范第 3 卷 F 部分第 3.2.1 节中定义的蓝牙基础 UUID。
CBUUID 类提供了工厂方法,使得在开发应用时处理长 UUID 变得更加容易。例如,无需在代码中传递心率服务的 128 位 UUID 的字符串表示形式,只需使用 UUIDWithString 方法从服务的预定义 16 位 UUID 创建 CBUUID 对象,如下所示:
CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString: @"180D"];
从预定义的 16 位 UUID 创建 CBUUID 对象时,核心蓝牙会使用蓝牙基本 UUID 预填充 128 位 UUID 的其余部分。
我们可能具有预定义的蓝牙 UUID 无法识别的服务和特征。如果这样做,则需要生成自己的 128 位 UUID 来识别它们。
使用命令行实用程序 uuidgen 轻松生成 128 位 UUID。要开始使用,请在终端中打开一个窗口。接下来,对于需要使用 UUID 标识的每个服务和特征,在命令行上键入 uuidgen,以接收由连字符标点的 ASCII 字符串形式的唯一 128 位值,如以下示例所示:
$ uuidgen
71DA3FD1-7E10-41C1-B16F-4430B506CDE7
然后,我们可以使用此 UUID 通过 UUIDWithString 方法创建 CBUUID 对象,如下所示:
CBUUID *myCustomServiceUUID =
[CBUUID UUIDWithString:@“71DA3FD1-7E10-41C1-B16F-4430B506CDE7”];
获得服务和特征的 UUID 后(由 CBUUID 对象表示),我们可以创建可变服务和特征,并以上述树状方式组织它们。例如,如果你有一个特征的 UUID,你可以通过调用 CBMutableCharacteristic 类的 initWithType:properties:value:permissions: 方法来创建一个可变特征,如下所示:
myCharacteristic =
[[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID
properties:CBCharacteristicPropertyRead
value:myValue permissions:CBAttributePermissionsReadable];
创建可变特征时,可以设置其属性、值和权限。我们设置的属性和权限确定特征的值是可读还是可写,以及连接的中央是否可以订阅特征的值。在此示例中,特征的值设置为可由连接的中央读取。有关可变特征的支持属性和权限范围的详细信息,请参阅 CBMutableCharacter 类参考。
注: 如果为特征指定值,则会缓存该值,并将其属性和权限设置为可读。因此,如果需要特征的值可写,或者希望该值在该特征所属的已发布服务的生存期内发生变化,则必须将值指定为 nil。遵循此方法可确保在外围管理器收到来自连接的中央中央的读取或写入请求时,外围管理器动态处理和请求该值。
创建可变特征后,可以创建与该特征关联的可变服务。为此,请调用 CBMutableService 类的 initWithType:primary: 方法,如下所示:
myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];
在此示例中,第二个参数设置为 YES,表示服务是主服务而不是辅助服务。主服务描述设备的主要功能,可由其他服务包含(引用)。辅助服务描述仅在引用它的另一个服务的上下文中相关的服务。例如,心率监测器的主要服务可能是公开来自监测器的心率传感器的心率数据,而辅助服务可能是公开传感器的电池数据。
创建服务后,可以通过设置服务的特征数组将特征与其关联,如下所示:
myService.characteristics = @[myCharacteristic];
构建服务和特征树后,在本地设备上实现外围角色的下一步是将它们发布到设备的服务和特征数据库。使用核心蓝牙框架可以轻松执行此任务。你调用 CBPeripheralManager 类的 addService: 方法,如下所示:
[myperiipheralManager addService:myService];
当我们调用此方法来发布服务时,外围管理器将调用其委托对象的外围管理器:didAddService:error:方法。如果发生错误并且无法发布服务,请实现此委托方法来访问错误的原因,如以下示例所示:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didAddService:(CBService *)service
error:(NSError *)error {
if (error) {
NSLog(@"Error publishing service: %@", [error localizedDescription]);
}
...
注: 将服务及其任何关联特征发布到外围设备的数据库后,将缓存该服务,我们无法再对其进行更改。
将服务和特征发布到设备的服务和特征数据库后,我们就可以开始向可能正在侦听的任何中央播发其中一些服务和特征。如以下示例所示,我们可以通过调用 CBPeripheralManager 类的 startAdvertising: 方法来播发某些服务,该方法传入播发数据的字典(NSDictionary 的实例):
[myPeripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey :
@[myFirstService.UUID, mySecondService.UUID] }];
在此示例中,字典中唯一的键 CBAdvertisementDataServiceUUIDsKey 期望 CBUUID 对象的数组(NSArray 的实例)作为值,这些对象表示要通告的服务的 UUID。可以在播发数据字典中指定的可能键在 CBCentralManagerDelegate 协议参考中的通告数据检索键中所述的常量中详细介绍。也就是说,外围管理器对象仅支持两个键:CBAdvertisementDataLocalNameKey 和 CBAdvertisementDataServiceUUIDsKey。
当我们开始在本地外围设备上播发某些数据时,外围管理器将调用外围设备管理器DidStartAdvertising:error:其委托对象的方法。如果发生错误并且无法播发服务,请实现此委托方法来访问错误的原因,如下所示:
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
error:(NSError *)error {
if (error) {
NSLog(@"Error advertising: %@", [error localizedDescription]);
}
...
注意:数据广播是在“尽力而为”的基础上进行的,因为空间有限,并且可能会有多个应用同时投放广播。有关详细信息,请参阅 CBPeripheralManager 类参考中对 startAdvertising: 方法的讨论。
当我们的应用在后台运行时,广播行为也会受到影响。本主题将在下一章 iOS 应用的核心蓝牙后台处理中讨论。
一旦我们开始广播数据,远程中央就可以发现并启动与我们的连接。
连接到一个或多个远程中央后,我们可以开始接收来自它们的读取或写入请求。执行此操作时,请务必以适当的方式响应这些请求。以下示例介绍如何处理此类请求。
当连接的中央请求读取我们的特征之一的值时,外围管理器调用外围设备管理器:didReceiveReadRequest:其委托对象的方法。委托方法以 CBATTRequest 对象的形式将请求传递给我们,该对象具有许多可用于完成请求的属性。
例如,当我们收到读取特征值的简单请求时,可以使用从委托方法接收的 CBATTRequest 对象的属性来确保设备数据库中的特征与远程中央在原始读取请求中指定的特征匹配。我们可以开始实现此委托方法,如下所示:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveReadRequest:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
...
如果特征的 UUID 匹配,下一步是确保读取请求不会要求从超出特征值边界的索引位置读取。如以下示例所示,我们可以使用 CBATTRequest 对象的 offset 属性来确保读取请求不会尝试读取超出正确的边界:
if (request.offset > myCharacteristic.value.length) {
[myPeripheralManager respondToRequest:request
withResult:CBATTErrorInvalidOffset];
return;
}
假设请求的偏移量已验证,现在将请求的特征属性的值(其值默认为 nil)设置为在本地外围设备上创建的特征的值,同时考虑读取请求的偏移量:
request.value = [myCharacteristic.value
subdataWithRange:NSMakeRange(request.offset,
myCharacteristic.value.length - request.offset)];
设置值后,响应远程中央以指示请求已成功完成。为此,调用 CBPeripheralManager 类的 respondToRequest:withResult: 方法,传回请求(我们更新了其值)和请求的结果,如下所示:
[myPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
...
每次调用外围设备管理器:didReceiveReadRequest: 委托方法时,只调用一次 respondToRequest:withResult: 方法。
注意:如果特征的 UUID 不匹配,或者由于任何其他原因无法完成读取,则不会尝试完成请求。相反,我们将立即调用 respondToRequest:withResult: 方法,并提供指示失败原因的结果。有关可能指定的结果的列表,请参阅核心蓝牙常量参考中的 CBATTError 常量枚举。
处理来自连接的中央发出的写入请求也很简单。当连接的中央发送请求以写入一个或多个特征的值时,外围管理器调用其委托对象的外围管理器:didReceiveWriteRequests:方法。这一次,委托方法以包含一个或多个 CBATTRequest 对象的数组的形式将请求传递给我们,每个对象表示一个写入请求。确保可以满足写入请求后,可以写入特征的值,如下所示:
myCharacteristic.value = request.value;
尽管上面的示例没有说明这一点,但在写入特征的值时,请务必考虑请求的 offset 属性。
就像响应读取请求一样,每次调用外围设备管理器:didReceiveWriteRequests:delegate方法时,只需调用一次 respondToRequest:withResult: 方法。也就是说,respondToRequest:withResult: 方法的第一个参数需要单个 CBATTRequest 对象,即使我们可能已从外围管理器:didReceiveWriteRequests: delegate 方法收到包含多个 CBATTRequest 对象的数组。我们应该传入数组的第一个请求,如下所示:
[myPeripheralManager respondToRequest:[requests objectAtIndex:0]
withResult:CBATTErrorSuccess];
注意:像对待单个请求一样对待多个请求 - 如果无法满足任何单个请求,则不应满足其中任何一个请求。相反,请立即调用 respondToRequest:withResult: 方法,并提供指示失败原因的结果。
通常,连接的中央将订阅一个或多个特征值,如订阅特征值中所述。当他们这样做时,我们有责任在他们订阅的特征值发生变化时向他们发送通知。以下示例介绍了如何操作。
当连接的中央订阅我们的特征之一的值时,外围管理器调用外围设备管理器:中央:didSubscribeToCharacteristic:其委托对象的方法:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
central:(CBCentral *)central
didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
NSLog(@"Central subscribed to characteristic %@", characteristic);
...
使用上述委托方法作为提示开始发送中央更新的值。
接下来,获取特征的更新值,并通过调用 CBPeripheralManager 类的 updateValue:forCharacteristic:onSubscribedCentrals: 方法将其发送到中央。
NSData *updatedValue = // fetch the characteristic's new value
BOOL didSendValue = [myPeripheralManager updateValue:updatedValue
forCharacteristic:characteristic onSubscribedCentrals:nil];
调用此方法将更新的特征值发送到订阅的中央时,可以在最后一个参数中指定要更新的中央。如上例所示,如果指定 nil,则会更新所有已连接和已订阅的中央(并忽略尚未订阅的任何已连接中央)。
updateValue:forCharacteristic:onSubscribedCentrals: 方法返回一个布尔值,该值指示更新是否已成功发送到订阅的中央。如果用于传输更新值的基础队列已满,则该方法返回 NO。然后,当传输队列中的更多空间可用时,外围管理器调用其委托对象的外围管理器是准备更新订阅者:方法。然后,我们可以实现此委托方法来重新发送值,再次使用 updateValue:forCharacteristic:onSubscribedCentrals: 方法。
注意:使用通知将单个数据包发送到订阅的中央。也就是说,当我们更新订阅的中央时,我们应该在单个通知中发送整个更新的值,方法是仅调用一次 updateValue:forCharacteristic:onSubscribedCentrals: 方法。
根据特征值的大小,并非所有数据都可以通过通知传输。如果发生这种情况,则应通过调用 CBPeripheral 类的 readValueForCharacteristic: 方法在中央端处理这种情况,该方法可以检索整个值。