原文
执行常规的中心角色(Central Role)任务
在低功耗蓝牙(以下简称“蓝牙”)通信中,充当中心角色的设备执行着几个常规任务——例如,发现和链接有效的外部设备(以下简称“外设”),探测和交互外设提供的数据。充当外设角色的设备也执行着常规但是不同的任务——例如,发布和广告服务,响应来自连接中心的读,写和订阅请求。
在这一章中,你将学会如何使用Core Bluetooth框架去执行蓝牙任务的中心部分的大多数常规任务。伴随基于代码的例子将帮你开发应用,以实现在你的本地设备上实现中心角色。主要的,你会学到:
- 开启一个中心管理器对象
- 发现和链接正在广告的外设
- 连接上外设之后,探测它上面的数据
- 向外设服务中特性的值发送读写请求
- 订阅特性的值,以察觉它的更新
下一章,你学会如何在你的本地设备上开发实现了外设角色的应用。
你在本章中找到的代码例子是简化和抽象的;你也许需要适当修改才能将它包含到你现实世界的应用中。更多相于实现中心角色高级主题——包括建议,技巧和最佳实践——包含在以后的章节中,Background Processing for iOS Apps 和 Best Practices for Interacting with a Remote Peripheral Device。
开启中心管理器(Central Manager)
由于CBCentralManager对象是本地中心设备的Core Bluetooth面向对象的代表,在你开始执行任何蓝牙事务之前你要开辟和初始化一个中心管理器实例。通过调用中心管理器的initWithDelegate:queue:options:方法初始化它:
myCentralManager =
[[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];
在这个例子中, self
被设为代理,以接受各种中心角色的事件。通过指定派发队列为nil
,中心管理器使用主队列派发中心角色事件。
当你创建了一个中心管理器,它会调用其代理对象的方法centralManagerDidUpdateState:。你必须实现这个代理方法以确保中心设备蓝牙的有效性。更多关于何实现这个代理方法的信息,请见CBCentralManagerDelegate Protocol Reference。
发现正在广告的外设
一旦完成初始化,中心管理器的第一个任务是发现外设。正如在Centrals Discover and Connect to Peripherals That Are Advertising中提到的,外设以广告的方式来被察觉。你的应用发现附近正在广告的外设的方式是,调用中心管理器的scanForPeripheralsWithServices:options:方法:
[myCentralManager scanForPeripheralsWithServices:nil options:nil];
注意:如果你指定
nil
为第一个参数,那么中心管理器返回所有发现的外设,忽视它们所支持的服务。在真实的应用中,你通常指定的是一个包含CBUUID对象的数组,其中每一个代表外设正在广播的服务的通用唯一识别码(UUID)。当你指定了服务UUID的数组,中心管理器只返回广播了那些服务的外设,让你只扫描到你可能感兴趣的设备。
UUID,以及它们的代表CBUUID,将会在Services and Characteristics Are Identified by UUIDs详细讨论。
每当中心管理器发现一个外设,它会调用其代理的方法centralManager:didDiscoverPeripheral:advertisementData:RSSI:。刚被发现的外设以一个CBPeripheral对象返回。如果你打算连接它,持有一个对它的强引用,遮掩系统就不会释放它。以下例子的场景是,用一个类的属性维持对一个已发现的外设的引用:
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData
RSSI:(NSNumber *)RSSI {
NSLog(@"Discovered %@", peripheral.name);
self.discoveredPeripheral = peripheral;
...
如果你想连接多个外设,你可以相应地持有一个外设的数组。任何情况下,一旦你已经发现了所有你想连接的外设,停止对其他设备的扫描以省电:
[myCentralManager stopScan];
连接你发现的外设
发现你想要的外设之后,调用中心管理器的connectPeripheral:options:方法以请求连接到它,传入你想连接的外设:
[myCentralManager connectPeripheral:peripheral options:nil];
如果连接成功,中心管理器会调用其代理的centralManager:didConnectPeripheral:方法。在与外设交互之前,你要设置它的代理以确保代理能收到准确的回调:
- (void)centralManager:(CBCentralManager *)central
didConnectPeripheral:(CBPeripheral *)peripheral {
NSLog(@"Peripheral connected");
peripheral.delegate = self;
...
发现你连接的设备中的服务
同一个外设建立连接之后,你可以探测它的数据。探测数据的第一步是发现它有效的服务。因为一个外设可以广告的数据大小是有限的,你可能会发现一个外设拥有的服务总数超过它所广告的(在它的广告包中)。你可以调用方法discoverServices:来发现外设提供的所有服务,像这样:
[peripheral discoverServices:nil];
注意:在现实的应用中,你通常不会传
nil
作为参数,因为这样做会返回外设上所有的服务。因为一个外设包含的服务可能比你想要的多,全部发现它们会浪费电池寿命和时间。相反的,你通常会指定你感兴趣的服务的UUID,像Explore a Peripheral’s Data Wisely中所展示的一样。
当指定的设备被发现了,外设(我们连接的CBPeripheral对象)就回调用其代理的peripheral:didDiscoverServices:方法。Core Bluetooth创建了一个CBService数组——包含着外设上发现的服务。如下所示,你可以实现这个代理方法来以获取已发现服务的数组。
- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverServices:(NSError *)error {
for (CBService *service in peripheral.services) {
NSLog(@"Discovered service %@", service);
...
}
...
发现服务中的特性
当你发现了你感兴趣的服务,探测外设的下一步就是要发现服务的所有特性。发现所有的特性只需调用外设的discoverCharacteristics:forService:方法,同时传入合适的服务,像这样:
NSLog(@"Discovering characteristics for service %@", interestingService);
[peripheral discoverCharacteristics:nil forService:interestingService];
注意:在现实的应用中,你通常不会传
nil
作为第一个参数,因为这样做会返回外设的服务中所有的特性。因为一个外设的服务所包含的特性可能比你想要的多,全部发现它们会浪费电池寿命和时间。相反的,你通常会指定你感兴趣的特性的UUID。
目标服务的特性被发现的时候,外设就会调用其代理的peripheral:didDiscoverCharacteristicsForService:error: 方法。Core Bluetooth创建了一个CBCharacteristic数组——包含了被发现的特性。以下的例子展示了你如何可以实现这个代理方法来打印被发现的特性:
- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverCharacteristicsForService:(CBService *)service
error:(NSError *)error {
for (CBCharacteristic *characteristic in service.characteristics) {
NSLog(@"Discovered characteristic %@", characteristic);
...
}
...
获取特性的值
每个特性包含了一个单一的值,描述一个外设的服务的信息。例如,一个体温计服务的温度测量值特性可以有一个以摄氏度为单位的温度值。获取特性的值可以通过直接读取或订阅它。
读取特性的值
当你已经找到你感兴趣的特性,要从中读值可以通过调用外设的readValueForCharacteristic:方法,同时传入适当的特性,像这样:
NSLog(@"Reading value for characteristic %@", interestingCharacteristic);
[peripheral readValueForCharacteristic:interestingCharacteristic];
当你试图读取特性的值,外设会调用其代理的peripheral:didUpdateValueForCharacteristic:error:方法来获取。如果成功获取,你可以通过特性的value属性来存取它,像这样:
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {
NSData *data = characteristic.value;
// parse the data as needed
...
注意:并非所有的特性都是可读的。通过检查特性的properties属性是否含有常量CBCharacteristicPropertyRead,你可以知道它是否可读。如果你试图读取一个不可读的特性的值,代理方法peripheral:didUpdateValueForCharacteristic:error:将返回一个对应的错误。
订阅特性的值
虽然使用readValueForCharacteristic:方法可以有效的读取特性的静态值,但这并不是读取动态值最有效的方式。通过订阅的方式获取随时变化的特性值——例如,你的心率。当你订阅了特性的值,每当它发生变化,你就会收到通知。
订阅你感兴趣的特性的方法是调用外设的setNotifyValue:forCharacteristic:方法,第一个参数指定为YES,像这样像这样:
[peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];
当你订阅(或者取消订阅)一个特性值,外设会调用其代理的setNotifyValue:forCharacteristic:,像这样:
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {
if (error) {
NSLog(@"Error changing notification state: %@",
[error localizedDescription]);
}
...
注意:并非所有特性提供订阅。通过检查特性的properties属性是否含有常量CBCharacteristicPropertyNotify或CBCharacteristicPropertyIndicate可以知道它是否提供订阅。
完成对特性的值的订阅之后,外设会通知你的app特性的值的改变。每当只发生变化,外设会调用其代理的peripheral:didUpdateValueForCharacteristic:error:方法。为了获取更新后的值,你可以按上述Reading the Value of a Characteristic的方法实现。
书写特性的值
偶尔需要书写特性值。例如,你的应用和一个蓝牙数字恒温器交互,你可能需要提供恒温器一个房间的温度。如果特性的值是可书写的,你可以使用data(NSData对象)书写它的值,通过调用外设的writeValue:forCharacteristic:type:方法,像这样:
NSLog(@"Writing value for characteristic %@", interestingCharacteristic);
[peripheral writeValue:dataToWrite forCharacteristic:interestingCharacteristic
type:CBCharacteristicWriteWithResponse];
书写特性的值,你要指定你执行书写的类型。上面的例子中,书写的类型是CBCharacteristicWriteWithResponse,它让外设通知你的应用书写是否成功,通过调用其代理的peripheral:didWriteValueForCharacteristic:error:方法。你要实现这个代理方法来处理错误的情况,如下所示:
- (void)peripheral:(CBPeripheral *)peripheral
didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {
if (error) {
NSLog(@"Error writing characteristic value: %@",
[error localizedDescription]);
}
...
如果相反你指定的书写类型是CBCharacteristicWriteWithoutResponse,那么书写操作以尽力而为的方式执行,而且交付过程既没有保证也没有报告。外设不会回调代理方法。更多关于Core Bluetooth框架支持的书写类型的信息,参考CBPeripheral Class Reference中的枚举BCharacteristicWriteType。
注意:一些特性可能只支持某几种书写类型,甚至完全不支持。查看特性的书写类型,要通过检查它的properties属性是否具有CBCharacteristicPropertyWriteWithoutResponse或者CBCharacteristicPropertyWrite常量。