iOS

蓝牙技术

2019-01-08  本文已影响5人  努力奔跑的小男孩
关键词.PNG

其他文章
蓝牙一 有关代理协议详解 和 info.plist设置


第一部分:蓝牙概述
第二部分:central角色的实现
第三部分:peripheral角色的实现
第四部分:iOS蓝牙应用的后台处理
第五部分:与peripheral通信的最佳实践
第六部分:作为Peripheral端的最佳方式


一、蓝牙概述

1.1 centralperipheral在蓝牙通讯中的关系

centralperipheral设备及他们在蓝牙通讯中的角色。
蓝牙通讯中的两个重要的角色,centralperipheral。相对于传统的client-server架构,peripheral代表着拥有数据的一方并且外界是需要这数据的。central代表着使用数据方,从peripheral那里获得数据并完成特定的事情。
centrals发现并连接到正在广播的peripherals
peripheral在广播包中广播他们的数据。广播包中应该包含一些有用的信息,比如peripheral的名称和主要功能等。
central可以扫描和监听任何他感兴趣的peripheral设备。

Central扫描和监听感兴趣的Peripheral.PNG
1.2peripheral如何组织数据

peripheral可以包含一个或多个services或提供它们连接信号强度的信息。service是数据和辅助行为的集合。
service本身由characteristics和引用的services组成。characteristic提供服务的详细的信息。

Service的组成.PNG
1.3 central检索并与peripheral交互数据

建立连接之后,就可以发现所有的servicescharacteristics(广播中可能只包含部分services

central可以通过servicescharacteristic来读或写数据。

1.4 centralsperipheralsperipheral的数据如何表示

角色和数据在蓝牙框架中以一种简单直接的方式表示。

1.4.1 central端的对象

当你使用central来与peripheral通讯,你执行的是central端的操作。
central端,设备用CBCentralManager对象代表。这个对象用来管理被发现的已连接的peripherals

蓝牙框架中如何表示central和peripheral.PNG

peripheral的数据用CBServiceCBCharacteristic来表示。
当你与peripheral交互时,你需要使用他的sevicescharacteristics
在蓝牙sdkserviceCBService对象表示,characteristicCBCharacteristic对象表示。

CBPeripheral的结构.PNG
1.4.2 peripheral 端的对象

如果是安装OS X v10.9iOS6,那你的macios设备就可以作为peripheral端设备,为其他设别提供数据。让你的设备实现peripheral角色,执行peripheral端的功能。

本地peripherals和远端centrals
peripheral端,peripheral设备用CBPeripheralManager对象表示。这个对象用来管理发布servicescharacteristics,并发出广播。同时peripheral manager可用来响应读和写请求。

CBCentral 和 CBPeripheralManager 的关系.PNG

本地peripheral的数据用CBMutableServiceCBMutableCharacteristic对象表示

当你创建并想设置peripheral的数据。你需要处理peripheral manager 对象的services (CBMutableService实例),同样,characteristic的实例也是CBMutableCharacteristic对象。

CBMutableService 和 CBMutableCharacteristic 的关系.PNG

二、central角色的实现

2.1 central角色的实现

central角色需要完成的几件事情,如 :搜索,连接,与peripheral交互数据。
peripheral角色同样需要完成几件事情,如:发布和广播服务,响应读,写,订阅请求

接下来,你将学习如何完成central端的功能。

2.1.1 创建Central manager对象

引入头文件#import <CoreBluetooth/CoreBluetooth.h>
遵守协议方法<CBCentralManagerDelegate,CBPeripheralDelegate>

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

方法说明:
在这里self设置成central角色的代理。dispath queue设置为nil,意味着central事件将交由main queue处理。

创建central manager时,会触发centralManagerDidUpdateState:代理方法。你必须实现这个代理。



// 中心设备的蓝牙状态发生变化之后会调用此方法 [必须实现的方法]
 - (void)centralManagerDidUpdateState:(CBCentralManager *)central;
 // 中心设备状态枚举 
typedef NS_ENUM(NSInteger, CBCentralManagerState) { 
CBCentralManagerStateUnknown = CBManagerStateUnknown,// 蓝牙状态未知
CBCentralManagerStateResetting = CBManagerStateResetting, 
CBCentralManagerStateUnsupported = CBManagerStateUnsupported, // 不支持蓝牙 
CBCentralManagerStateUnauthorized = CBManagerStateUnauthorized, // 蓝牙未授权 CBCentralManagerStatePoweredOff = CBManagerStatePoweredOff, // 蓝牙关闭状态 
CBCentralManagerStatePoweredOn = CBManagerStatePoweredOn, // 蓝牙开启状态
 } NS_DEPRECATED(NA, NA, 5_0, 10_0, "Use CBManagerState instead");
2.1.2 搜索正在发送广播的peripheral
[myCentralManager scanForPeripheralsWithServices:nil options:nil];

方法说明:
注意:如果第一个参数设置成nil,那么centralmanager会返回所有被发现的peripherals,在实际应用中,你应该给他赋值 CBUUID 对象数组。这样只有有广播这些uuid服务的peripheral才会被返回,一旦发现peripheral,将触发centralManager:didDiscoverPeripheral:advertisementData:RSSI:代理方法,如果你想连接这个peripheral,请使用强引用变量引用它,这样系统就不会释放掉它。

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral 
advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
    NSLog(@"Discovered %@", peripheral.name); 
    self.discoveredPeripheral = peripheral; 
    ... 
}

如果你想连接多个设备,你可以使用NSArray来管理他们。不管什么情况,一旦你找到所有你想要的设备,你应该停止扫描以便节省电量。
[myCentralManager stopScan];

2.1.3 连接peripheral

[myCentralManager connectPeripheral:peripheral options:nil];
建议使用下面这种连接外设方式

 [self.manager connectPeripheral:peripheral
                                options:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:CBConnectPeripheralOptionNotifyOnDisconnectionKey]];
}

options:参数说明

CBConnectPeripheralOptionNotifyOnConnectionKey: 在应用挂起后,与指定的peripheral成功建立连接,则发出通知(在连接成功后,程序被挂起,给出系统提示。 
)
CBConnectPeripheralOptionNotifyOnDisconnectionKey: 在应用挂起后,如果与指定的peripheral断开连接,则发出通知(在程序挂起,蓝牙连接断开时,给出系统提示。 
)
CBConnectPeripheralOptionNotifyOnNotificationKey: 在应用挂起后,指定的peripheral有任何通知都进行提示,建议使用这个( 在程序挂起后,收到 peripheral 数据时,给出系统提示。 Core Bluetooth 后台模式 
)

如果连接成功,则会触发centralManager:didConnectPeripheral:代理方法,与之通讯之前,你需要为它设置代理

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
    NSLog(@"Peripheral connected");
    peripheral.delegate = self; // 设置代理
    ...
}
2.1.4 查询peripheral的服务

建立连接之后,就可以查询数据,第一步就是查询有哪些services。因为广播包有大小限制,在广播中可能没有显示全部的服务信息,这里你可以使用discoverServices:来查询所有的services。
[peripheral discoverServices:nil];
注意:实际开发中,你不应该传值nil,因为这样做会返回所有的services,包括那些你不需要的services,这样做会浪费时间和电量。所以你应该传递你需要的uuids

查找到所有的服务会触发peripheral:didDiscoverServices:代理方法

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

发现service之后,下一步就是查找characteristics

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

注意:实际开发中,不要传nil,这样做会返回所有的characteristics,包括那些你不感兴趣的characteristics。这样做即浪费时间又损耗电量。所以你应该传你需要的uuids的值

查找到characteristics之后,会触发peripheral:didDiscoverCharacteristicsForService:error:代理方法

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
    for (CBCharacteristic *characteristic in service.characteristics) {
    NSLog(@"Discovered characteristic %@", characteristic);
    ...
}
2.1.6 获取数据

获取到数据后peripheral调用peripheral:didUpdateValueForCharacteristic:error:代理方法

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

一个characteristic只包含一种信息数据。比如恒温service下的温度characteristic只包含当前温度值,你可以通过读取或订阅来获得这个值。

####### 2.1.6.1 读取characteristic的数据

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

注意:并不是所有的characteristic都是可读的,你可以通过 characteristicproterties属性是否包含CBCharacteristicPropertyRead常量来判断是否可读。在读一个不可读的characteristic的数据值,会在代理方法的error参数中体现异常信息

####### 2.1.6.2订阅characteristic的值
虽然通过readValueForCharacteristic: 可以有效获取静态值,但如果值是动态改变的,则最好使用订阅的方法。当值变化时,自动获得通知。
设置订阅

[peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];

订阅的时候peripheral会调用peripheral:didUpdateNotificationStateForCharacteristic:error:代理方法。如果订阅失败了,也可以通过这个方法查询失败的原因。

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

注意:并不是所有的characteristics都可以订阅,可以通过检查characteristicproperties属性是否包含CBCharacteristicPropertyNotifyCBCharacteristicPropertyIndicate常量来判断是否可订阅。

2.1.7 写入数据

有时也是需要写数据的,比如恒温器,你需要设置目标温度。如果characteristic是可写的,那么你就可以调用writeValue:forCharacteristic:type:方法来写入数据。如下:

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

当你写数据时,你可以标明写类型。上例中,写类型是CBCharacteristicWriteWithResponse, 这种情况下,不管有没有写成功,peripheral都会通过代理通知你。你需要实现这个方法以便处理异常情况。

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

如果你设置的写类型是CBCharacteristicWriteWithoutResponse, 那么写操作会以更有效的方式执行,但不保证写成功,并且不会有报告。peripheral不会通知任何回调。

注意:characteristic可能只支持特定的写类型,或不支持写操作。你可以通过检查properties属性是否包含CBCharacteristicPropertyWriteWithoutResponseCBCharacteristicPropertyWrite 来判断。

三、peripheral角色的实现
3.1 peripheral角色的实现

接下来,你将学习如何使用peripheral

3.1.1 创建外设管理器
myPeripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];

方法说明:创建的时候,peripheral manager将调用代理对象的peripheralManagerDidUpdateState:方法。

3.1.2 构建服务和特征值

servicescharacteristic是树形结构组织的,服务和特征值使用uuid标识。

CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString: @"180D"];
CBUUID *myCustomServiceUUID = [CBUUID UUIDWithString:@"71DA3FD1-7E10-41C1-B16F-4430B506CDE7"];
myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead value:myValue permissions:CBAttributePermissionsReadable];

propetiespermissions的设置决定了这个characteristic是否可读,是否可写,是否可订阅。上例中我们把它设置成可读。

注意:如果你设置value的值,这个值将被缓存,并且propertiespermissions将是只读的。因此,如果你希望value是可写的,或value值根据具体情况呈不同的值时,你必须把它设置成nil。这样才能使它被动态赋值。才能响应peripheral manager的请求。
补充:

CBCharacteristicPropertyBroadcast: 允许一个广播特性值,用于描述特性配置,不允许本地特性
CBCharacteristicPropertyRead: 允许读一个特性值
CBCharacteristicPropertyWriteWithoutResponse: 允许写一个特性值,没有反馈
CBCharacteristicPropertyWrite: 允许写一个特性值
CBCharacteristicPropertyNotify: 允许通知一个特性值,没有反馈
CBCharacteristicPropertyIndicate: 允许标识一个特性值
CBCharacteristicPropertyAuthenticatedSignedWrites: 允许签名一个可写的特性值
CBCharacteristicPropertyExtendedProperties: 如果设置后,附加特性属性为一个扩展的属性说明,不允许本地特性
CBCharacteristicPropertyNotifyEncryptionRequired: 如果设置后,仅允许信任的设备可以打开通知特性值
CBCharacteristicPropertyIndicateEncryptionRequired: 如果设置后,仅允许信任的设备可以打开标识特性值

CBAttributePermissionsReadable // 读
CBAttributePermissionsWriteable // 写
CBAttributePermissionsReadEncryptionRequired //需要连接成功后,允许读(建议:安全数据这么做)
CBAttributePermissionsWriteEncryptionRequired //需要连接成功后,允许写(建议:安全数据这么做)

myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];

说明:这里的第二个参数设置为YES,标明这个服务值主要的。主要服务体现了主要的功能,并能够被其它服务引用。次要服务只用来描述其引用的服务的相关的信息。比如,心率监控的主要服务用来显示心率数据,这时次要服务可能就用来显示电池数据。

myService.characteristics = @[myCharacteristic];
[myPeripheralManager addService:myService];

这里会触发代理消息

- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error{
    if (error){
        NSLog(@"Error publishing service: %@", [error localizedDescription]);
    }
}

如果有异常,请通过error查询。
注意:服务和特征值一旦发布,不能更改。

[myPeripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey :@[myFirstService.UUID, mySecondService.UUID] }];

例子中参数是dictionary,不是array。并且只有一个key。在这里只支持两个key,CBAdvertisementDataLocalNameKeyCBAdvertisementDataServiceUUIDsKey。详情参见Advertisement Data Retrieval Keys in CBCentralManagerDelegate Protocol Reference。
当你开始广播你的服务,peripheral manager就会通知代理peripheralManagerDidStartAdvertising:error:
。如果有异常将不会发出广播,并在代理中可查到异常的原因:

- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error {
if (error) {
NSLog(@"Error advertising: %@", [error localizedDescription]);
  }
}

当你发出广播后,远程的centrals将可发现并初始化来取得连接。

- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveReadRequest:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
 ...
}

如果特征值的UUID匹配,下一步就是确保请求所读的数据不越界。

if (request.offset > myCharacteristic.value.length) {
    [myPeripheralManager respondToRequest:request
    withResult:CBATTErrorInvalidOffset];
    return;
}

如果请求的偏移量没有越界,那么设置请求的值。

request.value = [myCharacteristic.value subdataWithRange:NSMakeRange(request.offset,myCharacteristic.value.length - request.offset)];

设置好返回值(request.value)之后,使用respondToRequest:withResult:方法来返回数据,类似如下:

[myPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];

每次收到的请求peripheralManager:didReceiveReadRequest: 都应该有相应的返回respondToRequest:withResult:方法。
注意:假如特征值的UUID不匹配,或是读请求由于某些原因无法完成。你也应该调用respondToRequest:withResult:方法来返回失败结果。失败结果列表参见CBATTError Constants enumeration in Core Bluetooth Constants Reference。

- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveWriteRequests:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
 ...
}

如果特征值的UUID匹配,下一步就是确保请求所读的数据不越界。

if (request.offset > myCharacteristic.value.length) {
    [myPeripheralManager respondToRequest:request
    withResult:CBATTErrorInvalidOffset];
    return;
}

如果请求的偏移量没有越界,那么设置请求的值。

myCharacteristic.value = request.value;

这里同样需要注意请求的偏移量问题。
跟读请求相似,每次都要调用respondToRequest:withResult:方法来回应消息。也就是说,虽然代理中入参是array,可能包含多个CBATTRequest,但是回应时是单个的CBATTRequest对象。你必须传入array中的第一个对象。如下:

[myPeripheralManager respondToRequest:[requests objectAtIndex:0] withResult:CBATTErrorSuccess];

注意:把多个请求看成一个对待,如果其中有一个请求无法实现,那么所有的请求就将无法实现。同时,调用respondToRequest:withResult:方法回应,并提供失败的原因。

- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
    NSLog(@"Central subscribed to characteristic %@", characteristic);
}
NSData *updatedValue = // fetch the characteristic's new value
BOOL didSendValue = [myPeripheralManager updateValue:updatedValue forCharacteristic:characteristic onSubscribedCentrals:nil];

在你调用这个方法来发送数据给订阅者时,你可以在最后一个参数标明哪个central,如上。如果你写nil,所有连接着的有订阅的centrals都将收到信息,当然有连接但没订阅的会被忽略。

updateValue:forCharacteristic:onSubscribedCentrals: 这个方法返回Boolean值,指明数据是否成功发送。如果底层的队列正在传输数据,这个方法就会返回NO。当传输队列重新变为空闲时,则会调用peripheral manager的代理方法peripheralManagerIsReadyToUpdateSubscribers:,这时你就可以利用这个代理重新发送数据,而不需要重新调用updateValue:forCharacteristic:onSubscribedCentrals:方法。

注意:使用通知来发送一个数据包给central订阅者。也就是说,当你需要给订阅者更新数据时,你应该在通知中发送整个数据,通过一次调用updateValue:forCharacteristic:onSubscribedCentrals:方法来实现。局限于特征值数据大小的限制,并不是所有数据都能用通知来传递。这种情况下,应该由central端通过调用CBPeripheralreadValueForCharacteristic: 方法来获取整个数据。

四、iOS蓝牙应用的后台处理
4.1 iOS蓝牙应用的后台处理

对于iOS应用,你必须要清楚它是在前台运行,还是在后台运行。因为资源有限,你要对这两种模式区别处理。
默认情况下,当应用进入后台或挂起时,蓝牙任务是不执行的。但是,你可以把应用声明为支持蓝牙后台执行模式,这样当有蓝牙相关事件发生时,你的应用就可以被唤醒来处理任务。即使你的应用不要求后台处理支持,当有重要的事件发生时,系统仍然可能跳出警告,要求处理。

即使你的应用支持一种或两种都支持后台执行模式,也不是就一定能永远执行。在某些情况下,系统可能终止你的应用以便为前台应用让出内存,这将导致当前活动和连接等信息丢失。自iOS7之后,蓝牙库支持保存状态信息,并可在下次启动app时还原状态信息。你可以通过这个特性来实现长连接。

4.1.1 只支持前台运行的应用

大部分的apps,除非你要求后台运行,在进入后台后,应用会很快被挂起。在挂起状态下,应用无法处理蓝牙相关任务,无法接收蓝牙事件。直到重新回到前台。

central端,只支持前台运行的应用,在进入后台或被挂起时就无法扫描和发现peripheral的广播包。如果是在peripheral端,广播将停止,任何central想访问characteristic的值都将收到异常信息。

不同情况下,默认的行为可能会影响你的程序。比如,在你与peripheral交互数据时,应用挂起(比如用户切到另一个应用)。这时连接可能会断开,你并不会收到通知,直到应用重新激活。

####### 4.1.1.1 利用peripheral连接选项
只支持前台的蓝牙应用在挂起后发生的蓝牙事件会被系统排队,并在应用进入前台时把事件发给应用。当特定的central事件发生时,蓝牙库可以提供一种方式来提示用户。用户可以根据这些提示来决定是否激活应用。

若想利用使用这些提示,你需要在调用connectPeripheral:options: 方法时传入如下参数。
CBConnectPeripheralOptionNotifyOnConnectionKey: 在应用挂起后,与指定的peripheral成功建立连接,则发出通知
CBConnectPeripheralOptionNotifyOnDisconnectionKey: 在应用挂起后,如果与指定的peripheral断开连接,则发出通知
CBConnectPeripheralOptionNotifyOnNotificationKey: 在应用挂起后,指定的peripheral有任何通知都进行提示

####### 4.1.1.2 后台执行模式

如果你的应用在后台时也需要处理蓝牙事件,就必须在Info.plist中声明应用要支持蓝牙后台模式,这样,当有蓝牙事件发生时,系统会唤醒应用来处理。

有两种蓝牙后台模式,一种为central角色,另一种为peripheral角色。如果应用需要两种角色,则可以声明支持两种模式。声明方式:增加UIBackgroundModes 键,并增加包含下列字符串的array值。

• bluetooth-central —The app communicates with Bluetooth low energy peripherals using the Core Bluetooth framework. 
• bluetooth-peripheral —The app shares data using the Core Bluetooth framework

注意:Info.plist中会显示为更加人性化的文本,不是直接显示实际的键值对。如要显示实际值,可右键,或control点击,在弹出菜单中选择Open As -> Source Code

 <key>UIBackgroundModes</key>
 <array>
 <string>bluetooth-central</string>
 <string>bluetooth-peripheral</string>
 </array>

或者(推荐使用这个操作)


设置后台模式.png

对后台模式的说明:

  • 1, CBCentralManagerScanOptionAllowDuplicatesKey这个键会被忽略,多次发现同一peripheral会被合并成一个发现事件。
  • 2,如果所有扫描中的应用都在后台,那么你应用的扫描间隙会延长。结果是,扫描到peripheral的时间可能会延长。这样做是为了减少辐射节省电量。
  • 1,CBAdvertisementDataLocalNameKey 这个键会被忽略,并且peripherallocal name不会被广播
  • 2,CBAdvertisementDataServiceUUIDsKey 的值中包含的所有service uuids都会被放到“overflow”区域;只有iOS设备显示指明在搜索它时才会搜索到这些值。
  • 3,如果所有的处于广播状态的应用都在后台,广播频率将降低。

####### 4.1.1.3 明智使用后台运行模式
虽然为了完成某些事情,有必要把你的应用声明成支持后台运行模式,你也应该要能有效处理后台任务。因为执行蓝牙任务会使用无线电,从而耗费电池电量,所以尽量最小化后台任务。应用被蓝牙事件唤醒后应能尽快处理好任务,以便被重新挂起。
支持后台运行的任务要遵循几个原则

  • 1,应用应该是基于会话的,并提供接口让用户决定何时开始或停止蓝牙事件。
  • 2,应用被唤醒后,大约有10秒钟的时间来完成任务,所以应该尽快完成任务并重新挂起。若在后台花费太多时间,则将受到系统的遏制甚至被扼杀。
  • 3,应用不应该使用这种被唤醒的机会来执行与之无关的事情。

####### 4.1.1.4 后台长时间执行
一些应用需要长时间后台运行。举个例子,你可发一款家庭安全应用,iOS设备与蓝牙门锁通讯。当用户离开家时自动锁门,当用户回到家时门自动打开,整个过程应用都是后台运行。当用户离开家时,iOS设备与门锁断开连接。这是应用只简单调用connectPeripheral:options:,因为连接没有超时,iOS设备将在用户回到家时重新连接上。
假设用户离开家好几天,并假设app被系统终止,应用将无法在用户回到家时重连门锁,这时用户将无法开门。对于这类应用,很重要的一点要能够继续使用蓝牙执行长时事件,如管理活动和悬停连接。

蓝牙库支持状态保存和还原,支持central角色,peripheral角色。
当应用实现central角色并增加支持状态保存和还原,系统就会在终止应用释放内存前保存central manager对象的状态,如果应用有多个central managers,你可选择哪些对象你希望系统为你维护。对于CBcentralManger对象,系统如此维护:

  • 1,central manager扫描的services和对应的options
  • 2,已连接的和未连接上的peripherals
  • 3,订阅的characteristics

当应用实现peirpheral角色的应用类似处理。对于CBPeripheralManager对象,系统这样维护:

  • 1,广播的数据
  • 2,peripheral manager发布到设备数据库的servicescharacteristic
  • 3,那些订阅了你characteristics的值得centrals

当应用被系统重新激活到后台,假如应用之前有发现peripheral,你可以重新创建应用的centralperipheral manager,并还原他们的状态。后面将继续说明如何利用状态保存与还原。

  • 1,(必须)在创建和初始化时选择支持状态保存和还原。Opt In to State Preservation and Restoration 这一节将更详细描述
  • 2,(必须)在应用被系统唤醒时复原centralperipheral manager对象。Reinstantiate Your Central and Peripheral Managers 这里将继续描述
  • 3,(必须)实现还原代理方法。Implement the Appropriate Restoration Delegate Method。 这里将继续说明
  • 4,(可选)更新centralperipheral managers的初始化过程。Update Your Initialization Process。这里将继续说明

假如,选择支持状态保存和还原的应用只有一个CBCentralMnager对象实例实现了central角色,那么在初始化时初始化options中增加CBCentralManagerOptionRestoreIdentifierKey 键,并赋值还原id

myCentralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{ CBCentralManagerOptionRestoreIdentifierKey: @"myCentralManagerIdentifier" }];

peripheral manager的处理也是类似的,keyCBPeripheralManagerOptionRestoreIdentifierKey

注意:因为应用可以有多个CBCentralManagerCBPeripheralManger实例。注意每个还原id都是唯一的,这样系统才能区分开来。

当应用被系统唤醒,你需要做的第一件事是使用还原id复原central and peripheral manager。如果应用中只有一个central or peripheral manager,并且在应用的整个生命周期中存在,那么就简单了。

如果应用使用多个central or peripheral manager或如果应用使用的manager不是在app的整个生命周期中存在,那么应用需要知道哪些managers需要复原。在实现application:didFinishLaunchingWithOptions: 这个代理方法时,通过使用参数launchoptions中的键(UIApplicationLaunchOptionsBluetoothCentralsKey or UIApplicationLaunchOptionsBluetoothPeripheralsKey)可以获得应用在终止时为我们保存的还原id列表。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 
 NSArray *centralManagerIdentifiers = launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
 ...
}

有了还原id列表后,就可以复原出central manager对象了。
注意:当应用被激活时,系统只提供那些应用终止时有蓝牙任务的central and peripheral managers的还原ids

重要:对于使用状态保存和还原特性的应用,应用被激活到后台的第一个代理调用是centralManager:willRestoreState: and peripheralManager:willRestoreState:。对于未使用这一特性的应用,第一个代理调用是centralManagerDidUpdateState: and peripheralManagerDidUpdateState:

// 应用从后台恢复到前台的时候,会和系统蓝牙进行同步,调用此方法
 - (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary<NSString *, id> *)dict{
    NSArray *peripherals = state[CBCentralManagerRestoredStatePeripheralsKey];
// 如果数组不为空,这里直接设置代理,发现服务
if(peripherals.count != 0){
    CPeripheral *peripheral = [peripherals firstObject];
    Self.peripheral = peripheral; //强引用一下
    Peripheral.delegate = self; // 设置代理
    [peripheral discoverServices:@[]];// 服务数组
  }
}

在这些代理中,最后一个参数是dictionary,包含了应用被终止时managers的信息。可用键值参考 Central Manager State Restoration Options constants in CBCentralManagerDelegate Protocol Reference and the Peripheral_Manager_State_Restoration_Options constants in CBPeripheralManagerDelegate Protocol Reference

CBCentralManagerRestoredStatePeripheralsKey // 返回一个中心设备正在连接的所有外设数组 
CBCentralManagerRestoredStateScanServicesKey // 返回一个中心设备正在扫描的所有服务UUID的数组 
CBCentralManagerRestoredStateScanOptionsKey // 返回一个字典包含正在被使用的外设的扫描选项

要还原CBCentralMnager 对象的状态,要使用centralManager:willRestoreState: 方法中dictionary的键值对。举个例子,假如centralmanger对象在app被终止时有acitvepending连接,系统会继续管理他们。就像下面代码所示,可以使用CBCentralManagerRestoredStatePeripheralsKey 键从dictionary中获取所有设备的列表,这些设备就是central manger已连接或正在连接的设备。

如何使用这个列表要看具体情况。比如,如果应用要维护central manger已发现peripherals的列表,你可能就需要利用到它。参见Connecting to a Peripheral Device After You’ve Discovered It, 请注意在需要给peripheral设置相应的代理。

对于CBPeripheralManager对象,也需要类似的处理,相应的代理方法是peripheralManager:willRestoreState:

在前面的三个步骤之后,你可能想知道central and peripheral manager的初始化进程。虽然这是一个可选步骤,但如果想让你的应用跑起来更流畅,这可是很重要的。假设应用在检索peripheral的服务时被终止。当应用还原后,它不知道这个过程到底进行到哪一步了。你也想知道从哪一步继续。
举例,当在centralManagerDidUpdateState:方法中初始化你的应用时,你可以查到在应用被终止时你是否成功发现被还原peripheral的某个service,如下:

NSUInteger serviceUUIDIndex = [peripheral.services indexOfObjectPassingTest:^BOOL(CBService *obj,NSUInteger index, BOOL *stop) {
    return [obj.UUID isEqual:myService];
}];

if (serviceUUIDIndex == NSNotFound) {
    [peripheral discoverServices:@[myService]];
    ...
}else { // 应用在被终止前已搜索到`service`,那么你需要检查时候搜索到你要的`characteristics`
}

如上,如果系统在应用发现service之前终止它,那么开始搜索peripheral的数据,使用discoverServices:搜索。如果应用在被终止前已搜索到service,那么你需要检查时候搜索到你要的characteristics,(如果有订阅,也检查是否已订阅)。通过检查初始化过程,可以确保在这时调用到最合适的方法。

五、与peripheral通信的最佳实践
5.1 与peripheral通信的最佳实践

蓝牙库使得central端的一些事情的处理变得透明。也就是你的应用可以实现central端的大部分事情,如搜索和连接设备,检索和交互peripheral的数据。这一章将提供开发指引及如何最佳实现。

5.2 永远记住需要使用无线电并且会消耗电量

当开发应用需要用到蓝牙低功耗设备,请记住蓝牙低功耗通讯需要使用你设备的无线电来传输信号。同时其他形式的无线通讯也可能使用设备的无线电—比如,wi-fi,经典蓝牙,甚至其他使用蓝牙低功耗的应用—所以,尽量让你的应用尽少使用无线电。

在开发iOS应用中,尽少使用无线电是非常重要的,因为无线电的使用会减少电池的寿命。下面的指引将帮助你如何以更好的方式使用无线电,从而使你的应用表现更佳,更省电。
####### 5.2.1 只有在需要的时候才搜索设备
当你调用CBCentralManagerscanForPeripheralsWithServices:options: 方法来搜索(发现)当前正在发送广播的peripheral,这时你正在使用你设备的无线电来监听广播设备,直到你显示的停止它。
除非你需要搜索更多的设备,否则在你搜索到所需要的设备后,及时的停止搜索。使用CBCentralManagerstopScan方法停止搜索设备,参见Connecting to a Peripheral Device After You’ve Discovered It.

####### 5.2.2 只有在需要的时候才添加CBCentralManagerScanOptionAllowDuplicatesKey搜索参数

peripheral设备一秒钟可能发送多个广播包。在搜索peripheral时,默认情况下,多次搜到的广播包只会触发一次事件代理。结果就是不管收到多少个广播包,central manager对一个peripheral只会触发centralManager:didDiscoverPeripheral:advertisementData:RSSI: 方法一次。但是如果peripheral的广播数据变了,也会重新触发代理事件。

如果你不喜欢这种默认的方式,你可以在搜索时给搜索方法scanForPeripheralsWithServices:options:添加CBCentralManagerScanOptionAllowDuplicatesKey参数。 这样的话,就可以每次收到广播包信息都触发消息。在某些情况下,我们需要这么做。比如说需要检查设备是在靠近还是在远离(通过RSSI值判断)。所以,请记住添加这个参数会消耗电量,并影响性能,只有在确定需要的时候才使用这个参数。

####### 5.2.3 明智的检索peripheral的数据
peripheral设备可能会有多个servicescharacteristics。如果搜索全部的servicescharacteristics会浪费电量,对应用性能也不好。所以我们应该只搜索那些我们需要的servicescharacteristics

[peripheral discoverServices:@[firstServiceUUID, secondServiceUUID]];

在搜索到需要的services之后,接着就搜索需要的characteristics。同样的,使用CBUUID封装好需要的characteristics,使用CBPeripheraldiscoverCharacteristics:forService:方法开始搜索。

####### 5.2.4 订阅经常改变的characteristic
Retrieving the Value of a Characteristic谈过,有两种方式可以获得characteristic的值。

  • 1,在需要的时候使用readValueForCharacteristic: 轮询characteristic的值
  • 2,使用setNotifyValue:forCharacteristic: 订阅这个characteristic。

可能的话,最好采用订阅的方式,特别是在characteristic的值经常变的情况下。至于如何订阅参见Subscribing to a Characteristic’s Value.

####### 5.2.5 当获得全部所需数据后断开设备连接
当连接已不再需要的时候,断开连接,这样有助于降低无线电的使用。下面两种情况,你应该断开与peripheral的连接。

  • 1,你所订阅的特征值已不再发通知。(可以通过characteristic的isNotifying属性得知
  • 2,你已获得了全部所需要的数据。

这两种情况下,取消订阅并断开连接。通过setNotifyValue: forCharacteristic:设置第一个参数 为NO来取消订阅,通过CBCentralManager 的cancelPeripheralConnection:方法取消连接。如下:
取消订阅

 [self.peripheral setNotifyValue:YES forCharacteristic:characteristic];  // NO 取消订阅

断开连接

[myCentralManager cancelPeripheralConnection:peripheral];

注意cancelPeripheralConnection: 这个方法是非阻塞的,所以其他正在进行的通讯可能完成也可能因此没完成。因为其他应用可能仍保持跟这个设备的连接,所以当前的取消连接并不能保证底层就立即断开物理上的连接。但从应用本身看来,这个设备已经被认为是断开连接了,central manager对象也对触发代理的centralManager:didDisconnectPeripheral:error: 方法。

5.3 重新连接peripherals

有三种方式可以重连

  • 1,重新获取已发现的设备列表(搜索到的或是连接过的设备),使用 retrievePeripheralsWithIdentifiers: 。如果列表中有想要寻找的设备,那么发起连接。参见Retrieving a List of Known Peripherals
  • 2,重新获取当前连接着的设备列表,使用retrieveConnectedPeripheralsWithServices:。 如果列表中有想要寻找的设备,发起本地连接,使得应用与之连接上。参见Retrieving a List of Connected Peripherals
  • 3,使用scanForPeripheralsWithServices:options:重新搜索设备,如果找到了就去连接。参见Discovering Peripheral Devices That Are Advertising and Connecting to a Peripheral Device After You’ve Discovered It.

实际情况下,你可能不想每次连接都先去搜索设备,你可能更希望使用另外两种方式。如图所示,一种先尝试使用另两种方式连接的方法

一种先尝试使用另两种方式连接的方法 .png

注意:采用什么方式,什么顺序依情况而定,比如你可以不采用第一种方式,也可以同时使用前两种并行的方式。

####### 5.3.1 Retrieving a List of Known Peripherals(获取已知设备列表)
当你第一次发现某个peripheral,系统会为他生成一个identifier(一个NSUUID对象),你可以保存这个identifier(比如用NSUserDefaults保存),然后在后面需要用的时候使用retrievePeripheralsWithIdentifiers:重新连接。

knownPeripherals = [myCentralManager retrievePeripheralsWithIdentifiers:savedIdentifiers];

central manager尝试匹配你传入的identifiers,并返回CBPeripheral对象。如果没有匹配的设备,array将为空,这时你需要使用另外两种方式。如果不为空,那么让用户选择要连接到哪个设备。

当用户选择了要连接的设备,则调用connectPeripheral:options:方法来尝试连接。如果设备被连接上,则会触发代理消息centralManager:didConnectPeripheral:

注意:可能有多种原因导致设备不能被连接上。比如,设备不在附近。还有一种可能,一些低功耗蓝牙使用随机设备地址,在重新连接时,它的地址可能已经变了。因此,即使设备就在附近,设备的地址也已经变了,这种情况下,你想要连接的设备与实际设备已经不匹配了。这种情况,你只有重新搜索了。更多详情请参考Bluetooth 4.0 specification, Volume 3, Part C, Section 10.8 and Bluetooth Accessory Design Guidelines for Apple Products.

####### 5.3.2 Retrieving a List of Connected Peripherals(获取已连接蓝牙)

另一种重新连接的方法是检查你想要连接的设备是否已经连接到系统了(可能其他应用正连着呢)。你可以使用retrieveConnectedPeripheralsWithServices:方法获取CBPeripheral对象Array

因为当前可能有多个peripheral连接着系统,你可以传递CBUUID对象(注意是serviceUUID)的Array。这样他将只返回当前连接着的,并且包含array中所有serivesperipheral。如果没有符合条件的,则返回的array为空,这时你需要使用别的方法。如果array不为空,那么让用户来选择。

如果用户找到并选择了所要的peripheral,使用connectPeripheral:options:把它连接到你的应用。当连接建立,会触发代理centralManager:didConnectPeripheral:这时说明连接成功了。

六、作为Peripheral端的最佳方式

作为Peripheral端的最佳方式
central端类似,蓝牙库让你能够实现peripheral角色的多方面的控制。本章节提供指引,并讲述如何使用。

6.1 广播是实现peripheral建立连接的一个重要部分。

####### 6.1.1 注意广播数据的限制

CBPeripheralManagerstartAdvertising:方法中,通过dictionary参数传递peripheral的广播数据。创建广播字典时,时刻记住有哪些限制。

  • 1.虽然广播包通常情况下可以有多种的信息,但你只能广播设备名称和servicesuuid。也就是说,在你创建广播字典时,你只能设置这两个key: CBAdvertisementDataLocalNameKey and CBAdvertisementDataServiceUUIDsKey 。设置其他key将导致错误。
  • 2.广播数据的空间也有限制。当应用在前台时,这两个key的值最多有28 字节可用。搜索时,如果这个空间用完,另外还有10个字节的响应数据可以用来标识设备名称。超出规定空间的service uuids将会被放到“Overflow”区域,在iOS设备显示搜索他们时会被搜索到。当设备在后台时,local name不会被广播,同时所有的services uuids都放入 overflow 区域。

注意:这个限制的数值不包括2字节的头部信息。详细格式信息参见Bluetooth 4.0 specification, Volume 3, Part C, Section 11.

为了使你的数据符合空间要求,请使用主要的services的uuid

####### 6.1.2 只在需要的时候广播数据
广播数据会使用设备的无线电(当然了还有电池),所以只有在你希望被连的时候才广播数据。一旦连接上,这个设备就可以检索和交互数据了,而不需要你在广播数据了。因此,为了少使用无线电,增加应用响应性能,保护电池,请停止发送广播。使用stopAdvertising方法停止,如下

[myPeripheralManager stopAdvertising];

####### 6.1.3 让用户决定什么时候广播
往往只有用户才最知道什么时候需要发送广播。如果附近没有蓝牙设备,你发送广播也是没有意义的。既然应用本身不知道何时需要发送广播,那就提供接口让用户来决定。

6.2 配置characteristic

创建可变characteristic之后,我们修改其属性,值,和访问权限。这些设置决定连接的central如何访问和与之交互数据。虽然不同的属性和访问权限由各自app决定,但当你要完成下面两项任务时,这里提供相关指引:

  • 1,允许central订阅你的characteristics
  • 2,如果未配对,请保护敏感数据
6.3 设置characteristic,使之支持通知

Subscribe to Characteristic Values That Change Often中有描述,推荐在characteristic的值经常变化时,使用订阅的方式。如果可能的话,尽量让central端能够采用订阅的方式。

创建的时候,通过CBCharacteristicPropertyNotify设置characteristic的属性,使之支持订阅。

myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable];

这样,characteristic会是可读,可被订阅的。

6.4 要求配对连接才能访问敏感数据

实际情况下,你可能需要发送敏感数据。比如数据中有姓名,email地址等,你希望只有可信任的设备才能访问这些数据。

要保证只有可信任的设备才能访问敏感数据,可以通过设置合适的characteristic属性和访问权限。比如上面创建的例子,可做如下修改:

emailCharacteristic = [[CBMutableCharacteristic alloc] initWithType:emailCharacteristicUUID properties:CBCharacteristicPropertyRead| CBCharacteristicPropertyNotifyEncryptionRequired value:nil permissions:CBAttributePermissionsReadEncryptionRequired];

在这里characteristic被配置成只允许可信任设备访问和订阅他的值。当central想要连接和订阅这个characteristic的值时,蓝牙库就会尝试配对来建立安全连接。

配对过程完成后,peripheral就会认为这个central是一个可信任设备,并允许central访问其加密过的值。

参考文献
参考文献

上一篇下一篇

猜你喜欢

热点阅读