执行常见的中央角色任务

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

在本章中, 我们将学习如何使用核心蓝牙框架从中央侧执行最常见的蓝牙低功耗任务类型.以下基于代码的示例将帮助你开发应用,以在本地设备上实现中央角色。具体来说,我们将学习如何:

启动中央管理器对象

启动中央管理器

由于 CBCentralManager 对象是本地中央设备面向核心蓝牙对象的表示形式,因此我们可以先分配和初始化中央管理器实例,然后才能执行任何低功耗蓝牙事务。我们可以通过调用其 initWithDelegate:queue:options: 方法来初始化中央管理器:

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

在此示例中,self 设置为委托以接收任何中央角色事件。通过将调度队列指定为 nil,中央管理器使用主队列调度中央角色事件。

创建中央管理器时,中央管理器调用其委托对象的 centralManagerDidUpdateState: 方法。必须实现此委托方法,以确保支持低功耗蓝牙并可在中央设备上使用。

发现正在投放广播的外围设备

初始化后,中央管理器的第一个任务是外围设备发现。如中央发现并连接到正在广告的外围设备中所述,外围设备通过广告来宣传其存在。你的应用通过调用中央管理器的 scanForPeripheralsWithServices:options: 方法来发现附近正在播发的外围设备:

    [myCentralManager scanForPeripheralsWithServices:nil options:nil];

注: 如果为第一个参数指定 nil,中央管理器将返回所有发现的外围设备,而不考虑其支持的服务。在实际应用中,通常指定一个 CBUUID 对象数组,每个对象都表示外围设备播发的服务的通用唯一标识符 (UUID)。指定服务 UUID 数组时,中央管理器仅返回通告这些服务的外围设备,从而允许我们仅扫描我们可能感兴趣的设备。

UUID 以及表示它们的 CBUUID 对象在 UUID 标识的服务和特征中进行了更详细的讨论。

每次中央管理器发现外围设备时,它都会调用 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;
    ...

如果我们希望连接到多个设备,则可以保留已发现外围设备的 NSArray。无论如何,一旦找到我们有兴趣连接的所有外围设备,请停止扫描其他设备以节省电量:

[myCentralManager stopScan];

发现外围设备后连接到外围设备

发现我们感兴趣的外围设备广告服务后,通过调用中央管理器的连接外围设备:选项:方法,命名要连接到的已发现外围设备,请求与外围设备的连接:

[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,如明智地探索外围设备的数据中所示。

发现指定的服务时,外围设备(连接到的 CBPeripheral 对象)调用其委托对象的外围设备:didDiscoverServices: 方法。核心蓝牙创建 CBService 对象数组 — 外围设备上发现的每个服务对应一个对象。如下所示,可以实现此委托方法来访问已发现服务的数组:

- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverServices:(NSError *)error {
 
    for (CBService *service in peripheral.services) {
        NSLog(@"Discovered service %@", service);
        ...
    }
    ...

发现服务的特征

当我们找到感兴趣的服务时,探索外围设备必须提供的功能的下一步是发现服务的所有特征。发现服务的所有特征就像调用外设的 discoverFeatures:forService: 方法一样简单,指定适当的服务,如下所示:

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

注意:在实际应用中,通常不会传入 nil 作为第一个参数,因为这样做会返回外围设备服务的所有特征。由于外围设备的服务可能包含比我们感兴趣的更多的特征,因此发现所有这些特征可能会浪费电池寿命并且是不必要的时间使用。相反,我们通常会指定我们已经知道有兴趣发现的特征的 UUID。

外设调用外围设备:didDiscoverFeaturesForService:error:当发现指定服务的特征时,其委托对象的方法。核心蓝牙创建一组 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];

当我们尝试读取特征的值时,外围设备调用其委托对象的外围设备:didUpdateValueForCharacteristic:error:方法来检索该值。如果成功检索了该值,则可以通过特征的 value 属性访问它,如下所示:

- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
             error:(NSError *)error {
 
    NSData *data = characteristic.value;
    // parse the data as needed
    ...

注: 并非所有特征都是可读的。我们可以通过检查特征的属性属性是否包含 CBCharacteristicPropertyRead 常量来确定特征是否可读。如果尝试读取不可读的特征的值,外围设备:didUpdateValueForCharacteristic:error: delegate 方法将返回合适的错误。

订阅特征的值

尽管使用 readValueForCharacteristic: 方法读取特征的值对于静态值可能有效,但它不是检索动态值的最有效方法。通过订阅来检索随时间变化的特征值(例如,心率)。当我们订阅特征的值时,当值更改时,我们会收到来自外设的通知。

通过调用外围设备的 setNotifyValue:forCharacteristic: 方法,将第一个参数指定为 YES,我们可以订阅我们感兴趣的特征的值,如下所示:

 [peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];

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

- (void)peripheral:(CBPeripheral *)peripheral
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
             error:(NSError *)error {
 
    if (error) {
        NSLog(@"Error changing notification state: %@",
           [error localizedDescription]);
    }
    ...

注意:并非所有特征都提供订阅服务。我们可以通过检查特征的属性属性是否包含 CBCharacteristicPropertyNotify 或 CBCharacteristicPropertyIndicate 常量来确定特征是否提供订阅。

成功订阅特征的值后,外围设备会在值更改时通知应用。每次值更改时,外设都会调用其委托对象的外围设备:didUpdateValueForCharacteristic:error: 方法。若要检索更新的值,可以按照上述读取特征的值中所述的相同方式实现此方法。

写入特征的值

有时写一个特征的值是有意义的。例如,如果你的应用与蓝牙低功耗数字恒温器交互,你可能希望为恒温器提供一个值来设置房间的温度。如果特征的值是可写的,则可以通过调用外设的 writeValue:forCharacteristic:type: 方法,将其值写入数据(NSData 的实例),如下所示:

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

写入特征的值时,可以指定要执行的写入类型。在上面的示例中,写入类型为 CBCharacteristicWriteWithResponse,它指示外围设备通过调用其委托对象的外围设备:didWriteValueForCharacteristic:error: 方法来让应用知道写入是否成功。实现此委托方法来处理错误条件,如以下示例所示:

- (void)peripheral:(CBPeripheral *)peripheral
didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
             error:(NSError *)error {
 
    if (error) {
        NSLog(@"Error writing characteristic value: %@",
            [error localizedDescription]);
    }
    ...

相反,如果将写入类型指定为 CBCharacteristicWriteWithoutResponse,则写入操作将尽力而为地执行,并且既不保证也不报告传递。外围设备不调用任何委托方法。有关核心蓝牙框架中支持的写入类型的详细信息,请参阅 CBPeripheral 类参考中的 CBCharacteristicWriteType 枚举。

注意:特征可能仅支持某些类型的写入,或者根本不支持。通过检查 CBCharacteristicPropertyWriteWithoutResponse 或 CBCharacteristicPropertyWrite 常量之一的属性属性,可以确定特征支持的写入类型(如果有)。