常用 peripheral 角色的任务
Performing Common Peripheral Role Tasks
在上一章中,你学习了如何在 central 端执行大多数常用 BLE 任务。本章,你将学习如何使用 CoreBluetooth framework 在 peripheral 端 执行大多数常用 BLE 任务。下面一系列基于代码的例子将帮助你开发自己的应用,在本地设备上实现 peripheral 角色。具体说,你将学到如何:
- 启动一个 Peripheral Manager
- 配置你的 Services 和 Characteristics
- 发布你的 Services 和 Characteristics
- 广播你的 Services
- 响应 Central 端发来的读/写请求
- 发送更新的 Characteristic 值给订阅了的 Centrals
本章中你看到的例子都非常简略和抽象,你或许要做一些恰当的改变才能将其应用到你的实际app中。更多高级主题关于如何在本地设备上实现 central 角色----包括小贴士、小窍门和最佳实践----将在后面的章节覆盖到,即第 4 章:Core Bluetooth Background Processing for iOS Apps 和第 6 章:Best Practices for Setting Up Your Local Device as a Peripheral。
启动一个 Peripheral Manager
Starting Up a Peripheral Manager
要在本地设备上实现 peripheral 角色,第一步是分配(allocate)以及初始化(initialize)一个 peripheral manager 实例(表现为一个 CBPeripheralManager 对象)。可以通过调用 CBPeripheralManager 类的 initWithDelegate:queue:options: 方法启动你的 peripheral manager,如下:
myPeripheralManager =
[[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
在这个例子中,self 被设置为 delegate,以收取任何 peripheral 角色的事件。若你指定分发队列为 nil,peripheral manager 将使用主线程分发 peripheral 角色事件。
当你创建了一个 peripheral manager,它会调用它的 delegate 对象的 peripheralManagerDidUpdateState: 方法。你必须实现这个 delegate 方法,以确保在你本地 peripheral 设备上 BLE 支持已经开启并且处于可用状态。
关于如何实现这个 delegate 方法的更多信息,请看 CBPeripheralManagerDelegate Protocol Reference 。
配置你的 Services 和 Characteristics
Setting Up Your Services and Characteristics
如图1-7所示,本地 peripheral 上的 services 和 characteristics 数据库被组织为树状结构。要在本地 peripheral 上配置你的 services 和 characteristics,你必须也将他们组织成树状结构。要完成这一步,首先要理解 services 和 characteristics 是如何标识的。
Services 和 Characteristics 是通过 UUID 标识的
Services and Characteristics Are Identified by UUIDs
peripheral 上的 services 和 characteristics 通过128位的蓝牙特定 UUID 来标识,在 Core Bluetooth framework 中表示为 CBUUID 对象。虽然不是所有标识一个 service 或 characteristic 的 UUID 都被 Bluetooth Special Interest Group (SIG) 预定义过,Bluetooth SIG 确实已经定义且发布了许多常用 UUID,为了便于使用这些 UUID 已被缩短为16位。例如,Bluetooth SIG 已将用于标识心率 service 的16位 UUID 预定义为 180D。这个 UUID 是从对应的128位 UUID, 0000180D-0000-1000-8000-00805F9B34FB,缩短而来。这种简化基于 Bluetooth 基础 UUID,定义于 Bluetooth 4.0 specification, Volume 3, Part F, Section 3.2.1。
CBUUID 类提供了一组工厂方法,使得处理长 UUID 更加容易。例如,你可以简单的使用 UUIDWithString 方法,依据 service 预定义的16位 UUID,创建一个 CBUUID 对象,而不需要在代码中各种传递字符串形式的128位心率 service UUID,如下:
CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString: @"180D"];
当你从预定义的16位 UUID 创建一个 CBUUID 对象。Core Bluetooth 会依据 Bluetooth 基础 UUID 填满余下的128位 UUID。
为自定义 Services 和 Characteristics 创建你自己的 UUID
Create Your Own UUIDs for Custom Services and Characteristics
你也许有一些不能被预定义 Bluetooth UUIDs 标识的 services 和 characteristics。如果是,你需要生成你自己的128位 UUID 来标识他们。
使用命令行工具 uuidgen 可以简单的生成128位 UUID。首先,打开一个 Terminal 窗口。然后在命令行中,为每一个你需要用 UUID 标识的 service 和 characteristic 输入一遍 uuidgen,你会得到一个唯一的128位由连字符“-”分隔的 ASCII 字符串,如下例所示:
$ uuidgen
71DA3FD1-7E10-41C1-B16F-4430B506CDE7
然后你就可以用这个 UUID 创建 CBUUID 对象,通过 UUIDWithString 方法,如下:
CBUUID *myCustomServiceUUID =
[CBUUID UUIDWithString:@"71DA3FD1-7E10-41C1-B16F-4430B506CDE7"];
构建你的 Services 和 Characteristics 树状结构
Build Your Tree of Services and Characteristics
在你有了 services 和 characteristics 的 UUID (表示为 CBUUID 对象),你就可以创建可变的 services 和 characteristics 并将它们组织成之前所说的树状结构。例如,如果你有一个 characteristic 的 UUID,你就可以创建一个可变的 characteristic,通过调用 CBMutableCharacteristic 类的 initWithType:properties:value:permissions: 方法,如下:
myCharacteristic =
[[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID
properties:CBCharacteristicPropertyRead
value:myValue permissions:CBAttributePermissionsReadable];
当你创建一个可变的 characteristic,你要设置它的属性(properties)、值(value)和权限许可(permissions)。你所设置的属性和权限将决定,这个 characteristic 值是可读或是可写,以及连接着的 central 是否可订阅这个值。在这个例子中,characteristic 值被设置为可被连接的 central 读取。关于可变 characteristic 所支持的属性和权限设置范围,请看 CBMutableCharacteristic Class Reference。
注意:若你为 characteristic 指定了一个值,这个值会被缓存且它的属性和权限会被设置为可读。因此,若你需要这个值是可写的,或若你期望这个值在该 characteristic 所属的已发布的 service 的生命周期中会改变,你必须指定值为 nil。遵循这一条可以保证这个值会被动态处理,会被 peripheral manager 请求,每当它收到从已连接 central 发来的读或写请求。
现在你已经创建了一个可变的 characteristic,你可以创建可变的 service 与这个 characteristic 关联。要这样做,调用 CBMutableService 类的 initWithType:primary: 方法,如下所示:
myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];
在这个例子中,第二个参数被设置为 YES,代表了这个 service 是主要(primary),而非次要(secondary)。一个主要 service 描述了设备的主要功能且能够被另一个 service 包含(引用)。一个次要 service 描述了一个 service 仅仅在引用它的另一个 service 的上下文中的有意义。例如,一个心率监测仪的主要 service 可能是从心率传感器中暴露出心率数据,然而一个次要 service 可能是暴露传感器的电池数据。
在你创建 service 之后,你可以将其与 characteristic 关联,通过设置 service 的 characteristics 数组,如下:
myService.characteristics = @[myCharacteristic];
发布你的 Services 和 Characteristics
Publishing Your Services and Characteristics
在你完成构建 services 和 characteristics 树之后,下一步是将它们发布到设备的 services 和 characteristics 数据库中。这个步骤用 Core Bluetooth framework 很容易完成。你可以调用 CBPeripheralManager 类的 addService:
方法,如下:
[myPeripheralManager addService:myService];
当你调用这个方法发布 service 时,peripheral manager 会调用其 delegate 对象上的 peripheralManager:didAddService:error: 方法。如果发生错误你的 service 无法发布,实现这个 delegate 方法以取得错误原因,如下例所示:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didAddService:(CBService *)service
error:(NSError *)error {
if (error) {
NSLog(@"Error publishing service: %@", [error localizedDescription]);
}
...
注意:当你发布完一个 service 及其关联 characteristics 到 peripheral 数据库中之后,这个 service 将被缓存且你将不能再对其做修改。
广播你的 Services
Advertising Your Services
当你完成发布你的 services 和 characteristics 到你设备的 services 和 characteristics 数据库,你就已经准备好广播其中的几个,给任何可能正在监听的 central。如下例所示,你可以调用 CBPeripheralManager 类的 startAdvertising: 方法来广播你的 services 中的某几个,传入一个广播数据的 dictionary(一个 NSDictionary 实例)。
[myPeripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey :
@[myFirstService.UUID, mySecondService.UUID] }];
在这个例子中,dictionary 中唯一的 key,CBAdvertisementDataServiceUUIDsKey,期待了对应的 value 是一个由 CBUUID 对象组成的数组(一个 NSArray 实例),代表了你想要广播的 services 的 UUID。倒装句 在这个 dictionary 中可以指定的可能的 key 值详细定义在 CBCentralManagerDelegate Protocol Reference 协议的 Advertisement Data Retrieval Keys 常量中。然而其实只有两种 key 值是被 peripheral manager 对象支持的:CBAdvertisementDataLocalNameKey
和 CBAdvertisementDataServiceUUIDsKey。
当你在本地 peripheral 上开始广播一些数据时,该 peripheral manager 调用其 delegate 对象上的 peripheralManagerDidStartAdvertising:error: 方法。如果发生错误你的 services 无法发布,可以实现这个 delegate 方法获取错误原因,如下:
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
error:(NSError *)error {
if (error) {
NSLog(@"Error advertising: %@", [error localizedDescription]);
}
...
注意:数据广播基于“最大努力原则”,因为空间有限且另外可能有多个应用在持续广播。更多信息,请看 CBPeripheralManager Class Reference 中 startAdvertising: 方法的讨论。
当你的应用在后台时,广播行为也会受到影响。这个主题在下一章中讨论,Core Bluetooth Background Processing for iOS Apps。
一旦你开始广播数据,远程 centrals 即可以发现并初始化一个和你的连接。
响应 Central 端发来的读/写请求
Responding to Read and Write Requests from a Central
当你连接到一个或多个远程 centrals 之后,你可以开始从它们那里接收读或写的请求。当你这样做,请确保用恰当的方式回应这些请求。下面的例子描述了如何处理这些请求。
当一个已连接的 central 请求读取你某一个 characteristic 的值,peripheral manager 会调用它的 delegate 对象上的 peripheralManager:didReceiveReadRequest: 方法。这个 delegate 方法将此请求以 CBATTRequest 对象的方式传递给你,这个对象中有一系列属性供你使用以完成这个请求。
例如,当你收到一个简单的请求要读取一个 characteristic 的值,可以使用从 delegate 对象那里收到的 CBATTRequest 对象来确保你设备的数据库中的 characteristic 和远程 central 在原始读取请求中指定的是一致的。你可以开始实现这个 delegate 方法,如下:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveReadRequest:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
...
若 characteristic 的 UUID 匹配,下一步是确保这个读取请求的读取的地址范围没有超出 characteristic 值的边界。如下例所示,你可以使用 CBATTRequest 对象的 offset 属性来保证读取请求没有尝试读到边界之外。
if (request.offset > myCharacteristic.value.length) {
[myPeripheralManager respondToRequest:request
withResult:CBATTErrorInvalidOffset];
return;
}
假设请求的偏移值验证通过,现在你就可以将请求的 characteristic 属性值(默认值为 nil )设置为你在本地 peripheral 上创建的 characteristic 值,当然了要考虑请求的偏移值:
request.value = [myCharacteristic.value
subdataWithRange:NSMakeRange(request.offset,
myCharacteristic.value.length - request.offset)];
当你设置完值之后,就可以回应远程 central 表明请求已经成功完成。通过调用 CBPeripheralManager 类的 respondToRequest:withResult: 方法来做这件事,回传请求对象(你已经更新了它的值)以及请求的结果,如下:
[myPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
...
请勿必保证在每次 peripheralManager:didReceiveReadRequest: delegate 方法被调用时,只调用一次 respondToRequest:withResult: 方法。
注意:如果 characteristic 的 UUID 不匹配,或者读取因为其它任何原因而无法完成,你不应该尝试完成这个请求。相反,你应该立即调用 respondToRequest:withResult: 方法,返回一个表示失败原因的结果。关于你可能指定的结果的列表,请看 Core Bluetooth Constants Reference 的 CBATTError Constants 枚举。
处理远程 central 的写入请求同样非常直接。当一个已连接的 central 发送一个请求要写入一个或多个你的 characteristic 值,peripheral manager 调用 delegate 对象上的 peripheralManager:didReceiveWriteRequests: 方法。不过这一次,delegate 对象传递给你请求对象时是以数组的形式,包含一个或多个 CBATTRequest 对象,每项代表一个写入请求。当你确认写入请求能被完成之时,你可以写入这个 characteristic 值,如下:
myCharacteristic.value = request.value;
虽然上例没有展示,你写入 characteristic 值的时候还是要考虑进去请求的偏移属性。
就像你回应读取请求一样,每次 peripheralManager:didReceiveWriteRequests: delegate 方法调用时请务必只调用一次 respondToRequest:withResult: 方法。不过,respondToRequest:withResult: 方法的第一个参数期望的是一个单独的 CBATTRequest 对象,即使你在 peripheralManager:didReceiveWriteRequests: delegate 方法中可能收到的数组包含不止一个请求。你应该传入数组的第一个请求对象,如下:
[myPeripheralManager respondToRequest:[requests objectAtIndex:0
withResult:CBATTErrorSuccess];
注意:对待多个请求就像你对待一个请求那样,如果任何一个请求无法完成,你就不应该去完成其它任何一个。而是,立即调用 respondToRequest:withResult: 方法,提供指示了错误原因的返回结果。
发送更新的 Characteristic 值给订阅了的 Centrals
Sending Updated Characteristic Values to Subscribed Centrals
通常,已连接的 central 订阅一个或多个你的 characteristic 值,如同第4章中 Subscribing to a Characteristic’s Value 所描述。当它们这样做,你就要负责当它们订阅的 characteristic 值变化时通知它们。下面的例子描述了如何做。
当一个已连接的 central 订阅了你的 characteristic 值之一时,peripheral manager 会调用它的 delegate 对象上的 peripheralManager:central:didSubscribeToCharacteristic: 方法。
- (void)peripheralManager:(CBPeripheralManager *)peripheral
central:(CBCentral *)central
didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
NSLog(@"Central subscribed to characteristic %@", characteristic);
...
使用上面的 delegate 方法做为一个入口,开始给 central 发送更新的值。
然后,取得更新后的 characteristic 值并调用 CBPeripheralManager 类的 updateValue:forCharacteristic:onSubscribedCentrals: 方法将其发送给 central 。
NSData *updatedValue = // fetch the characteristic's new value
BOOL didSendValue = [myPeripheralManager updateValue:updatedValue
forCharacteristic:characteristic onSubscribedCentrals:nil];
当你调用这个方法发送更新后的值给订阅的 central ,你可以在最后一个参数指定你想要更新哪个 central 。若像上面的例子,你指定了 nil ,所有已连接的且已订阅的 central 都将被更新。(当然了已连接但未订阅的 central 会被忽略。)
updateValue:forCharacteristic:onSubscribedCentrals: 方法返回一个 Boolean 值,表示更新是否被成功发送到了订阅的 central 。如果传输更新值所用的下层队列已满,这个方法返回 NO。peripheral manager 于是会等到传输队列有可用空间时,调用它的 delegate 对象的 peripheralManagerIsReadyToUpdateSubscribers: 方法。于是你可以实现这个 delegate 方法重发这个值,还是使用 updateValue:forCharacteristic:onSubscribedCentrals: 方法。
注意:使用通知来给订阅的 central 发送单独的数据包。也就是说,当你更新一个已订阅的 central 时,你应该在一个单独的通知中发送整个更新值,仅仅调用 updateValue:forCharacteristic:onSubscribedCentrals: 方法一次。
取决于你的 characteristic 值的长度,可能通知无法传递所有的数据。如果是这样,这个情况应该在 central 端处理,通过调用 CBPeripheral 类的 readValueForCharacteristic: 方法,这个方法可以取得整个值。
网友评论