iOS - Bluetooth 蓝牙
一、蓝牙介绍
二、iBeacon
-
具体讲解见 Beaconios
-
iBeacon 是苹果公司 2013 年 9 月发布的移动设备用 OS(iOS7)上配备的新功能。其工做方式是,配备有低功耗蓝牙(BLE)通讯功能的设备使用 BLE 技术向周围发送本身特有的 ID,接收到该 ID 的应用软件会根据该 ID 采起一些行动。好比,在店铺里设置 iBeacon 通讯模块的话,即可让 iPhone 和 iPad 上运行一资讯告知服务器,或者由服务器向顾客发送折扣券及进店积分。此外,还能够在家电发生故障或中止工做时使用 iBeacon 向应用软件发送资讯。git
-
苹果 WWDC 14 以后,对 iBeacon 加大了技术支持和对其用于室内地图的应用有个更明确的规划。苹果公司公布了 iBeacon for Developers 和 Maps for Developers 等专题页面。github
三、iOS 蓝牙
3.1 常见简称
-
MFi:make for ipad ,iphone, itouch 专们为苹果设备制做的设备,开发使用 ExternalAccessory 框架。认证流程挺复杂的,并且对公司的资质要求较高,详见 iOS - MFi 认证。数据库
-
BLE:buletouch low energy,蓝牙 4.0 设备由于低耗电,因此也叫作 BLE,开发使用 CoreBluetooth 框架。数组
- GATT Profile(Generic Attribute Profile):GATT 配置文件是一个通用规范,用于在 BLE 链路上发送和接收被称为 “属性”(Attribute)的数据块。目前全部的 BLE 应用都基于 GATT。
- 1) 定义两个 BLE 设备经过叫作 Service 和 Characteristic 的东西进行通讯。中心设备和外设须要双向通讯的话,惟一的方式就是创建 GATT 链接。
- 2) GATT 链接是独占的。基于 GATT 链接的方式的,只能是一个外设链接一个中心设备。
- 3) 配置文件是设备如何在特定的应用程序中工做的规格说明,一个设备能够实现多个配置文件。
- GAP(Generic Access Profile):用来控制设备链接和广播,GAP 使你的设备被其余设备可见,并决定了你的设备是否能够或者怎样与合同设备进行交互。
- 1) GATT 链接,必需先通过 GAP 协议。
- 2) GAP 给设备定义了若干角色,主要两个:外围设备(Peripheral)和中心设备(Central)。
- 3) 在 GAP 中外围设备经过两种方式向外广播数据:Advertising Data Payload(广播数据)和 Scan Response Data Payload(扫描回复)。
-
Profile:并非实际存在于 BLE 外设上的,它只是一个被 Bluetooth SIG(一个以制定蓝牙规范,以推进蓝牙技术为宗旨的跨国组织)或者外设设计者预先定义的 Service 的集合。缓存
-
Service:服务,是把数据分红一个个的独立逻辑项,它包含一个或者多个 Characteristic。每一个 Service 有一个 UUID 惟一标识。UUID 有 16 bit 的,或者 128 bit 的。16 bit 的 UUID 是官方经过认证的,须要花钱购买,128 bit 是自定义的,能够本身设置。每一个外设会有不少服务,每一个服务中包含不少字段,这些字段的权限通常分为读 read,写 write,通知 notiy 几种,就是咱们链接设备后具体须要操做的内容。安全
-
Characteristic:特征,GATT 事务中的最低界别,Characteristic 是最小的逻辑数据单元,固然它可能包含一个组关联的数据,例如加速度计的 X/Y/Z 三轴值。与 Service 相似,每一个 Characteristic 用 16 bit 或者 128 bit 的 UUID 惟一标识。每一个设备会提供服务和特征,相似于服务端的 API,可是机构不一样。
-
Description:每一个 Characteristic 能够对应一个或多个 Description 用户描述 Characteristic 的信息或属性。
-
Peripheral、Central:外设和中心,发起链接的是 Central,被链接的设备为 Peripheral。
3.2 工做模式
-
蓝牙通讯中,首先须要提到的就是 central 和 peripheral 两个概念。这是设备在通讯过程当中扮演的两种角色。直译过来就是 [中心] 和 [周边(能够理解为外设)]。iOS 设备既能够做为 central,也能够做为 peripheral,这主要取决于通讯需求。
-
例如在和心率监测仪通讯的过程当中,监测仪做为 peripheral,iOS 设备做为 central。区分的方式便是这两个角色的重要特色:提供数据的是谁,谁就是 peripheral;须要数据的是谁,谁就是 central。就像是 client 和 server 之间的关系同样。
-
-
那怎么发现 peripheral 呢
-
在 BLE 中,最多见的就是广播。实际上,peripheral 在不停的发送广播,但愿被 central 找到。广播的信息中包含它的名字等信息。若是是一个温度调节器,那么广播的信息应该还会包含当前温度什么的。那么 central 的做用则是去 scan,找到须要链接的 peripheral,链接后即可进行通讯了。
-
当 central 成功连上 peripheral 后,它即可以获取 peripheral 提供的全部 service 和 characteristic。经过对 characteristic 的数据进行读写,即可以实现 central 和 peripheral 的通讯。
-
-
CoreBluetooth 框架的核心实际上是两个东西,central 和 peripheral, 对应他们分别有一组相关的 API 和类。
-
这两组 API 分别对应不一样的业务场景,以下图,左侧叫作中心模式,就是以你的手机(App)做为中心,链接其余的外设的场景。而右侧称为外设模式,使用手机做为外设链接其余中心设备操做的场景。
-
-
iOS 设备(App)做为 central 时:
-
当 central 和 peripheral 通讯时,绝大部分操做都在 central 这边。此时,central 被描述为 CBCentralManager,这个类提供了扫描、寻找、链接 peripheral(被描述为 CBPeripheral)的方法。
-
下图标示了 central 和 peripheral 在 Core Bluetooth 中的表示方式:
-
当你操做 peripheral 的时候,其实是在和它的 service 和 characteristic 打交道,这两个分别由 CBService 和 CBCharacteristic 表示。
-
-
iOS 设备(App)做为 Peripheral 时:
-
在 OS X 10.9 和 iOS 6 之后,设备除了能做为 central 外,还能够做为 peripheral。也就是说,能够发起数据,而不像之前只能管理数据了。
-
那么在此时,它被描述为 CBPeripheralManager,既然是做为 peripheral,那么这个类提供的主要方法则是对 service 的管理,同时还兼备着向 central 广播数据的功能。peripheral 一样会对 central 的读写要求作出相应。
-
下图则是设备做为 central 和 Peripheral 的示意图:
-
在充当 peripheral 时,CBPeripheralManager 处理的是可变的 service 和 characteristic,分别由 CBMutableService 和 CBMutableCharacteristic 表示。
-
-
中心模式(CBCentralManager)流程:
- 一、创建中心角色
- 二、扫描外设(discover)
- 三、链接外设(connect)
- 四、扫描外设中的服务和特征(discover)
- 4.1 获取外设的 services
- 4.2 获取外设的 Characteristics,获取 Characteristics 的值,获取 Characteristics 的 Descriptor 和 Descriptor 的值
- 五、与外设作数据交互(explore and interact)
- 六、订阅 Characteristic 的通知
- 七、断开链接(disconnect)
-
外设模式(CBPeripheralManager)流程:
- 一、启动一个 Peripheral 管理对象
- 二、设置本地 Peripheral 服务、特性、描述、权限等等
- 三、设置 Peripheral 发送广播
- 四、设置处理订阅、取消订阅、读 characteristic、写 characteristic 的委托方法
3.3 服务、特征和特征的属性
-
一个 peripheral 包含一个或多个 service,或提供关于信号强度的信息。service 是数据和相关行为的集合。例如,一个心率监测仪的数据就多是心率数据。
-
service 自己又是由 characteristic 或者其余 service 组成的。characteristic 又提供了更为详细的 service 信息。仍是以心率监测仪为例,service 可能会包含两个 characteristic,一个描述当前心率带的位置,一个描述当前心率的数据。
-
每一个 characteristic 属性分为这么几种:读,写,通知这么几种方式。
// 特征的定义枚举 typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) { CBCharacteristicPropertyBroadcast = 0x01, // 广播 CBCharacteristicPropertyRead = 0x02, // 读 CBCharacteristicPropertyWriteWithoutResponse = 0x04, // 写 CBCharacteristicPropertyWrite = 0x08, CBCharacteristicPropertyNotify = 0x10, // 通知 CBCharacteristicPropertyIndicate = 0x20, CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40, CBCharacteristicPropertyExtendedProperties = 0x80, CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x100, CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x200 };
-
外设、服务、特征间的关系
- 一个 CBPeripheral(蓝牙设备) 有一个或者多个 CBService(服务),而每个 CBService 有一个或者多个 CBCharacteristic(特征),经过可写的 CBCharacteristic 发送数据,而每个 CBCharacteristic 有一个或者多个 Description 用于描述 characteristic 的信息或属性。
3.4 设备状态
-
蓝牙设备状态:
- 一、待机状态(standby):设备没有传输和发送数据,而且没有链接到任何设备。
- 二、广播状态(Advertiser):周期性广播状态。
- 三、扫描状态(Scanner):主动寻找正在广播的设备。
- 四、发起连接状态(Initiator):主动向扫描设备发起链接。
- 五、主设备(Master):做为主设备链接到其余设备。
- 六、从设备(Slave):做为从设备链接到其余设备。
-
五种工做状态:
- 准备(standby)
- 广播(advertising)
- 监听扫描(Scanning)
- 发起链接(Initiating)
- 已链接(Connected)
3.5 蓝牙和版本的使用限制
-
蓝牙 2.0:越狱设备
-
蓝牙 4.0:iOS 6 以上
-
MFi 认证设备:无限制
3.6 设置系统使用蓝牙权限
-
设置系统使用蓝牙权限
四、中心模式的使用
-
中心模式的应用场景:主设备(手机去扫描链接外设,发现外设服务和属性,操做服务和属性的应用。通常来讲,外设(蓝牙设备,好比智能手环之类的东西)会由硬件工程师开发好,并定义好设备提供的服务,每一个服务对于的特征,每一个特征的属性(只读,只写,通知等等)。
-
蓝牙程序须要使用真机调试。
4.1 App 链接外设的实现
-
一、创建中心角色
-
导入 CoreBluetooth 头文件,创建中心设备管理类,设置主设备委托。
// 包含头文件 #import <CoreBluetooth/CoreBluetooth.h> // 遵照协议 @interface ViewController () <CBCentralManagerDelegate> // 中心设备管理器 @property (nonatomic, strong) CBCentralManager *centralManager; - (IBAction)start:(UIButton *)sender { // 初始化 centralManager,nil 默认为主线程 self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]; } #pragma mark - CBCentralManagerDelegate // 检查 App 设备蓝牙是否可用,协议方法 - (void)centralManagerDidUpdateState:(CBCentralManager *)central { // 在初始化 CBCentralManager 的时候会打开设备,只有当设备正确打开后才能使用 switch (central.state){ case CBManagerStatePoweredOn: // 蓝牙已打开,开始扫描外设 NSLog(@"蓝牙已打开,开始扫描外设"); // 开始扫描周围的设备,自定义方法 [self sacnNearPerpherals]; break; case CBManagerStateUnsupported: NSLog(@"您的设备不支持蓝牙或蓝牙 4.0"); break; case CBManagerStateUnauthorized: NSLog(@"未受权打开蓝牙"); break; case CBManagerStatePoweredOff: // 蓝牙未打开,系统会自动提示打开,因此不用自行提示 default: break; } } // 发现外围设备,协议方法 - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI { /* * central 中心设备 * peripheral 外围设备 * advertisementData 特征数据 * RSSI 信号强度 */ NSMutableString *string = [NSMutableString stringWithString:@"\n\n"]; [string appendFormat:@"NAME: %@\n" , peripheral.name]; [string appendFormat:@"UUID(identifier): %@\n", peripheral.identifier]; [string appendFormat:@"RSSI: %@\n" , RSSI]; [string appendFormat:@"adverisement:%@\n" , advertisementData]; NSLog(@"发现外设 Peripheral Info:\n %@", string); // 链接指定的设备,自定义方法 [self connectPeripheral:peripheral]; } // 链接外设成功,协议方法 - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { NSLog(@"%@ 链接成功", peripheral.name); // 中止扫描 [central stopScan]; // 扫描外设中的服务和特征,自定义方法 [self discoverPeripheralServices:peripheral]; } // 链接外设失败,协议方法 - (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { NSLog(@"%@ 链接失败", peripheral.name); } // 链接外设断开,协议方法 - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { NSLog(@"%@ 链接已断开", peripheral.name); }
-
-
二、扫描外设(discover)
-
扫描外设的方法须要放在 centralManager 成功打开的代理方法
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
中,由于只有设备成功打开,才能开始扫描,不然会报错。 -
扫描到外设后会进入代理方法
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI;
中。// 开始扫描周围的设备,自定义方法 - (void)sacnNearPerpherals { NSLog(@"开始扫描周围的设备"); /* * 第一个参数为 Services 的 UUID(外设端的 UUID),nil 为扫描周围全部的外设。 * 第二参数的 CBCentralManagerScanOptionAllowDuplicatesKey 为已发现的设备是否重复扫描,YES 同一设备会屡次回调。nil 时默认为 NO。 */ [self.centralManager scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@NO}]; }
-
-
三、链接外设(connect)
-
对要链接的设备须要进行强引用,不然会报错。
-
一个主设备最多能连 7 个外设,每一个外设最多只能给一个主设备链接,链接成功,失败,断开会进入各自的代理方法中。
// 设备 @property (nonatomic, strong) CBPeripheral *peripheral; // 链接指定的设备,自定义方法 - (void)connectPeripheral:(CBPeripheral *)peripheral { NSLog(@"链接指定的设备"); // 设置链接规则,这里设置的是 以 J 开头的设备 if ([peripheral.name hasPrefix:@"J"]) { // 对要链接的设备进行强引用,不然会报错 self.peripheral = peripheral; // 链接设备 [self.centralManager connectPeripheral:peripheral options:nil]; } }
-
-
四、扫描外设中的服务和特征(discover)
-
设备链接成功后,就能够扫描设备的服务了,一样是经过委托形式,扫描到结果后会进入委托方法。可是这个委托已经再也不是主设备的委托(CBCentralManagerDelegate),而是外设的委托(CBPeripheralDelegate),这个委托包含了主设备与外设交互的许多回调方法,包括获取 services,获取 characteristics,获取 characteristics 的值,获取 characteristics 的 Descriptor,和 Descriptor的值,写数据,读 RSSI,用通知的方式订阅数据等等。
// 遵照协议 @interface ViewController () <CBPeripheralDelegate> // 扫描外设中的服务和特征,自定义方法 - (void)discoverPeripheralServices:(CBPeripheral *)peripheral { // 设置外设代理 self.peripheral.delegate = self; // 开始扫描外设 [self.peripheral discoverServices:nil]; } #pragma mark - CBPeripheralDelegate // 扫描到外设服务,协议方法 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error { if (error) { NSLog(@"Discovered services for %@ with error: %@", peripheral.name, error.localizedDescription); return; } for (CBService *service in peripheral.services) { NSLog(@"扫描到外设服务:%@", service); // 扫描服务的特征 [peripheral discoverCharacteristics:nil forService:service]; } } // 扫描到服务的特征,协议方法 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error { if (error) { NSLog(@"error Discovered characteristics for %@ with error: %@", service.UUID, error.localizedDescription); return; } for (CBCharacteristic *characteristic in service.characteristics) { NSLog(@"扫描到服务:%@ 的特征:%@", service.UUID, characteristic.UUID); // 获取特征的值 [peripheral readValueForCharacteristic:characteristic]; // 搜索特征的 Descriptors [peripheral discoverDescriptorsForCharacteristic:characteristic]; // // 链接成功,开始配对,发送第一次校验的数据,自定义方法 // [self writeCharacteristic:peripheral characteristic:characteristic value:self.pairAuthDatas[0]]; } } // 获取到特征的值,协议方法 - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { // value 的类型是 NSData,具体开发时,会根据外设协议制定的方式去解析数据 NSLog(@"获取到特征:%@ 的值:%@", characteristic.UUID, [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding]); // if (...) { // 第一次配对成功 // // [self writeCharacteristic:peripheral characteristic:characteristic value:self.pairAuthDatas[1]]; // } // // if (...) { // 第二次配对成功 // // NSLog(@"正式创建的链接 -----------"); // } } // 搜索到特征的 Descriptors,协议方法 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { for (CBDescriptor *descriptor in characteristic.descriptors) { NSLog(@"搜索到特征:%@ 的 Descriptors:%@", characteristic.UUID, descriptor.UUID); // 获取到 Descriptors 的值 [peripheral readValueForDescriptor:descriptor]; } } // 获取到 Descriptors 的值,协议方法 - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForDescriptor:(CBDescriptor *)descriptor error:(NSError *)error{ // 这个 descriptor 都是对于特征的描述,通常都是字符串 NSLog(@"获取到 Descriptors:%@ 的值:%@", descriptor.UUID, descriptor.value); } // 写数据到特征中完成,协议方法 - (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { NSLog(@"写数据完成到特征:%@ 中完成:%@", characteristic.UUID, characteristic.value); }
-
-
5 把数据写到 Characteristic 中
// 配对信息 @property (nonatomic, strong) NSArray<NSData *> *pairAuthDatas; // 加载配对信息 - (NSArray<NSData *> *)pairAuthDatas { if (_pairAuthDatas) { // 具体开发时,根据配对协议加载配对须要的数据 // _pairAuthDatas = ... } return _pairAuthDatas; } // 把数据写到 Characteristic 中,自定义方法 - (void)writeCharacteristic:(CBPeripheral *)peripheral characteristic:(CBCharacteristic *)characteristic value:(NSData *)value { NSLog(@"%lu", (unsigned long)characteristic.properties); // 只有 characteristic.properties 有 write 的权限才能够写 if (characteristic.properties & CBCharacteristicPropertyWrite || characteristic.properties & CBCharacteristicPropertyWriteWithoutResponse) { // 写入数据 [peripheral writeValue:value forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse]; } else { NSLog(@"该字段不可写!"); } }
-
六、订阅 Characteristic 的通知
// 设置通知,自定义方法 - (void)notifyCharacteristic:(CBPeripheral *)peripheral characteristic:(CBCharacteristic *)characteristic{ // 设置通知,数据通知会进入:didUpdateValueForCharacteristic 方法 [peripheral setNotifyValue:YES forCharacteristic:characteristic]; } // 取消通知,自定义方法 - (void)cancelNotifyCharacteristic:(CBPeripheral *)peripheral characteristic:(CBCharacteristic *)characteristic{ [peripheral setNotifyValue:NO forCharacteristic:characteristic]; }
-
七、断开链接(disconnect)
// 中止扫描并断开链接,自定义方法 - (void)disconnectPeripheral:(CBCentralManager *)centralManager peripheral:(CBPeripheral *)peripheral{ // 中止扫描 [centralManager stopScan]; // 断开链接 [centralManager cancelPeripheralConnection:peripheral]; }
-
运行效果
02:38:33.336775 BluetoothDemo[776:263266] 蓝牙已打开,开始扫描外设 02:38:33.337034 BluetoothDemo[776:263266] 开始扫描周围的设备 02:38:33.361782 BluetoothDemo[776:263266] 发现外设 Peripheral Info: NAME: JHQ0228-MacBookAir UUID(identifier): 41E85E3E-0AF2-9992-B399-21730E2B342F RSSI: -54 adverisement:{ kCBAdvDataIsConnectable = 1; } 02:38:33.362378 BluetoothDemo[776:263266] 链接指定的设备 02:38:33.795614 BluetoothDemo[776:263266] JHQ0228-MacBookAir 链接成功 02:38:33.951722 BluetoothDemo[776:263266] 扫描到外设服务:<CBService: 0x17406e9c0, isPrimary = YES, UUID = Device Information> 02:38:33.952587 BluetoothDemo[776:263266] 扫描到外设服务:<CBService: 0x170078940, isPrimary = YES, UUID = Continuity> 02:38:33.953509 BluetoothDemo[776:263266] 扫描到外设服务:<CBService: 0x170078900, isPrimary = YES, UUID = 9FA480E0-4967-4542-9390-D343DC5D04AE> 02:38:33.956941 BluetoothDemo[776:263266] 扫描到服务:Device Information 的特征:Manufacturer Name String 02:38:33.958529 BluetoothDemo[776:263266] 扫描到服务:Device Information 的特征:Model Number String 02:38:33.959987 BluetoothDemo[776:263266] 扫描到服务:Continuity 的特征:Continuity 02:38:33.961416 BluetoothDemo[776:263266] 扫描到服务:9FA480E0-4967-4542-9390-D343DC5D04AE 的特征:AF0BADB1-5B99-43CD-917A-A77BC549E3CC 02:38:34.010710 BluetoothDemo[776:263266] 获取到特征:Manufacturer Name String 的值:Apple Inc 02:38:34.070137 BluetoothDemo[776:263266] 获取到特征:Model Number String 的值:MacBookAir7,2 02:38:34.130098 BluetoothDemo[776:263266] 获取到特征:Continuity 的值:(null) 02:38:34.131258 BluetoothDemo[776:263266] 搜索到特征:Continuity 的 Descriptors:Client Characteristic Configuration 02:38:34.190588 BluetoothDemo[776:263266] 获取到特征:AF0BADB1-5B99-43CD-917A-A77BC549E3CC 的值: 02:38:34.191409 BluetoothDemo[776:263266] 搜索到特征:AF0BADB1-5B99-43CD-917A-A77BC549E3CC 的 Descriptors:Client Characteristic Configuration 02:38:34.245280 BluetoothDemo[776:263266] 获取到 Descriptors:Client Characteristic Configuration 的值:1 02:38:34.275359 BluetoothDemo[776:263266] 获取到 Descriptors:Client Characteristic Configuration 的值:0
4.2 做为 Central 时的数据读写
4.2.1 初始化 CBCentralManager
-
第一步先进行初始化,可使用
initWithDelegate:queue:options:
方法:myCentralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];
-
上面的代码中,将 self 设置为代理,用于接收各类 central 事件。将 queue 设置为 nil,则表示直接在主线程中运行。
-
初始化 central manager 以后,设置的代理会调用
centralManagerDidUpdateState:
方法,因此须要去遵循<CBCentralManagerDelegate>
协议。这个 did update state 的方法,能得到当前设备是否能做为 central。关于这个协议的实现和其余方法,接下来会讲到,也能够先看看官方 API。
4.2.2 搜索当前可用的 peripheral
-
可使用 CBCentralManager 的
scanForPeripheralsWithServices:options:
方法来扫描周围正在发出广播的 Peripheral 设备。[myCentralManager scanForPeripheralsWithServices:nil options:nil];
-
第一个参数为 nil,表示全部周围所有可用的设备。在实际应用中,你能够传入一个 CBUUID 的数组(注意,这个 UUID 是 service 的 UUID 数组),表示只搜索当前数组包含的设备(每一个 peripheral 的 service 都有惟一标识 UUID)。因此,若是你传入了这样一个数组,那么 central manager 则只会去搜素包含这些 service UUID 的 Peripheral。
-
CBUUID 是和 peripheral 相关的,和 central 自己关系不大,若是你是作的硬件对接,那么能够向硬件同事询问。
-
在调用
scanForPeripheralsWithServices:options:
方法以后,找到可用设备,系统会回调(每找到一个都会回调)centralManager:didDiscoverPeripheral:advertisementData:RSSI:
。该方法会返回找到的 peripheral,因此你可使用数组将找到的 peripheral 存起来。- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { NSLog(@"Discovered %@", peripheral.name); }
-
当你找到你须要的那个 peripheral 时,能够调用 stop 方法来中止搜索。
[myCentralManager stopScan]; NSLog(@"Scanning stopped");
4.2.3 链接 peripheral
-
找到你须要的 peripheral 以后,下一步就是调用
connectPeripheral:options:
方法来链接。[myCentralManager connectPeripheral:peripheral options:nil];
-
当链接成功后,会回调方法
centralManager:didConnectPeripheral:
。在这个方法中,你能够去记录当前的链接状态等数据。- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { NSLog(@"Peripheral connected"); }
-
不过在进行其余操做以前,你应该给已链接的这个 peripheral 设置代理(须要去遵循
<CBPeripheralDelegate>
协议),这样才能收到 peripheral 的回调(能够就写在上面这个方法中)。peripheral.delegate = self;
-
注意:在链接设备以前须要对要链接的设备进行强引用,不然会报错
[CoreBluetooth] API MISUSE: Cancelling connection for unused peripheral <CBPeripheral: 0x1702e6680, identifier = 41E85E3E-0AF2-9992-B399-21730E2B342F, name = MacBookAir, state = connecting>, Did you forget to keep a reference to it?`
@property (nonatomic, strong) CBPeripheral *peripheral; // 对要链接的设备进行强引用 self.peripheral = peripheral;
4.2.4搜索 peripheral 的 service
-
当与 peripheral 成功创建链接之后,就能够通讯了。第一步是先找到当前 peripheral 提供的 service,由于 service 广播的数据有大小限制(貌似是 31 bytes),因此你实际找到的 service 的数量可能要比它广播时候说的数量要多。调用 CBPeripheral 的
discoverServices:
方法能够找到当前 peripheral 的全部 service。[peripheral discoverServices:nil];
-
在实际项目中,这个参数应该不是 nil 的,由于 nil 表示查找全部可用的 Service,但实际上,你可能只须要其中的某几个。搜索所有的操做既耗时又耗电,因此应该提供一个要搜索的 service 的 UUID 数组。
-
当找到特定的 Service 之后,会回调
<CBPeripheralDelegate>
的peripheral:didDiscoverServices:
方法。Core Bluetooth 提供了 CBService 类来表示 service,找到之后,它们以数组的形式存入了当前 peripheral 的 services 属性中,你能够在当前回调中遍历这个属性。- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error { for (CBService *service in peripheral.services) { NSLog(@"Discovered service %@", service); } }
-
若是是搜索的所有 service 的话,你能够选择在遍历的过程当中,去对比 UUID 是否是你要找的那个。
4.2.5 搜索 service 的 characteristic
-
找到须要的 service 以后,下一步是找它所提供的 characteristic。若是搜索所有 characteristic,那调用 CBPeripheral 的
discoverCharacteristics:forService:
方法便可。若是是搜索当前 service 的 characteristic,那还应该传入相应的 CBService 对象。NSLog(@"Discovering characteristics for service %@", interestingService); [peripheral discoverCharacteristics:nil forService:interestingService];
-
一样是出于节能的考虑,第一个参数在实际项目中应该是 characteristic 的 UUID 数组。也一样能在最佳实践中介绍。
-
找到全部 characteristic 以后,回调
peripheral:didDiscoverCharacteristicsForService:error:
方法,此时 Core Bluetooth 提供了 CBCharacteristic 类来表示 characteristic。能够经过如下代码来遍历找到的 characteristic。- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error { for (CBCharacteristic *characteristic in service.characteristics) { NSLog(@"Discovered characteristic %@", characteristic); } }
-
一样也能够经过添加 UUID 的判断来找到须要的 characteristic。
4.2.6 读取 characteristic 数据
-
characteristic 包含了 service 要传输的数据。例如温度设备中表达温度的 characteristic,就可能包含着当前温度值。这时咱们就能够经过读取 characteristic,来获得里面的数据。
-
当找到 characteristic 以后,能够经过调用 CBPeripheral 的
readValueForCharacteristic:
方法来进行读取。NSLog(@"Reading value for characteristic %@", interestingCharacteristic); [peripheral readValueForCharacteristic:interestingCharacteristic];
-
当你调用上面这方法后,会回调
peripheral:didUpdateValueForCharacteristic:error:
方法,其中包含了要读取的数据。若是读取正确,能够用如下方式来得到值:- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { NSData *data = characteristic.value; // parse the data as needed }
-
注意,不是全部 characteristic 的值都是可读的,你能够经过 CBCharacteristicPropertyRead options 来进行判断。若是你尝试读取不可读的数据,那上面的代理方法会返回相应的 error。
4.2.7 订阅 Characteristic 数据
-
其实使用
readValueForCharacteristic:
方法并非实时的。考虑到不少实时的数据,好比心率这种,那就须要订阅 characteristic 了。 -
能够经过调用 CBPeripheral 的
setNotifyValue:forCharacteristic:
方法来实现订阅,注意第一个参数是 YES。[peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];
-
若是是订阅,成功与否的回调是
peripheral:didUpdateNotificationStateForCharacteristic:error:
,读取中的错误会以 error 形式传回。- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { if (error) { NSLog(@"Error changing notification state: %@", [error localizedDescription]); } }
-
固然也不是全部 characteristic 都容许订阅,依然能够经过 CBCharacteristicPropertyNoify options 来进行判断。
-
当订阅成功之后,那数据便会实时的传回了,数据的回调依然和以前读取 characteristic 的回调相同(注意,不是订阅的那个回调)
peripheral:didUpdateValueForCharacteristic:error:
。
4.2.8 向 characteristic 写数据
-
写数据实际上是一个很常见的需求,若是 characteristic 可写,你能够经过 CBPeripheral 类的
writeValue:forCharacteristic:type:
方法来向设备写入 NSData 数据。NSLog(@"Writing value for characteristic %@", interestingCharacteristic); [peripheral writeValue:dataToWrite forCharacteristic:interestingCharacteristic type:CBCharacteristicWriteWithResponse];
-
关于写入数据的 type,如上面这行代码,type 就是 CBCharacteristicWriteWithResponse,表示当写入成功时,要进行回调。更多的类型能够参考 CBCharacteristicWriteType 枚举。
-
若是写入成功后要回调,那么回调方法是
peripheral:didWriteValueForCharacteristic:error:
。若是写入失败,那么会包含到 error 参数返回。- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { if (error) { NSLog(@"Error writing characteristic value: %@", [error localizedDescription]); } }
-
注意:characteristic 也可能并不支持写操做,能够经过 CBCharacteristic 的 properties 属性来判断。
if (characteristic.properties & CBCharacteristicPropertyWrite || characteristic.properties & CBCharacteristicPropertyWriteWithoutResponse) { // 写数据 [peripheral writeValue:dataToWrite forCharacteristic:interestingCharacteristic type:CBCharacteristicWriteWithResponse]; } else { NSLog(@"该字段不可写!"); }
4.3 数据读写 - 知识补充
4.3.1 CBUUID
-
CBUUID 对象是用于 BLE 通讯中 128 位的惟一标示符。peripheral 的 service,characteristic,characteristic descriptor 都包含这个属性。这个类包含了一系列生成 UUID 的方法。
-
UUID 有 16 位的,也有 128 位的。其中 SIG 组织提供了一部分 16 位的 UUID,这部分 UUID 主要用于公共设备,例若有个用蓝牙链接的心率监测仪,若是是用的公共的 UUID,那么不管谁作一个 app,均可以进行链接,由于它的 UUID 是 SIG 官方提供的,是公开的。若是公司是要作一个只能本身的 app 才能链接的设备,那么就须要硬件方面自定义 UUID。(关于这方面,包括通讯的 GATT 协议、广播流程等详细介绍,能够看 iOS - GATT Profile 简介 这篇文章。讲得比较详细,能在很大程度上帮助咱们理解 BLE 通讯)。
-
CBUUID 类提供了能够将 16 位 UUID 转为 128 位 UUID 的方法。下面的代码是 SIG 提供的 16 位的心率 service UUID 转为 128 位 UUID 的方法:
CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString:@"180D"];
-
若是须要获取 NSString 形式的 UUID,能够访问 CBUUID 的 UUIDString 只读属性。
NSString *uuidString = [CBUUID UUIDWithString:ServiceUUIDString1].UUIDString;
4.3.2 设备惟一标识符
-
在有些时候,须要获取 peripheral 的惟一标示符(好比要作自动链接或绑定用户等操做),可是在搜索到 peripheral 以后,只能拿到 identifier,并且这个 identifier 根据链接的 central 不一样而不一样。也就是说,不一样的手机连上以后,identifier 是不一样的。虽然比较坑爹,可是这并不影响你作蓝牙自动链接。
CB_EXTERN_CLASS @interface CBPeripheral : CBPeer // 蓝牙设备的名称 @property(retain, readonly, nullable) NSString *name; // 蓝牙设备的信号强度 @property(retain, readonly, nullable) NSNumber *RSSI NS_DEPRECATED(NA, NA, 5_0, 8_0); // 蓝牙设备的链接状态,枚举值 @property(readonly) CBPeripheralState state; // 蓝牙设备包含的服务 @property(retain, readonly, nullable) NSArray<CBService *> *services; CB_EXTERN_CLASS @interface CBPeer : NSObject <NSCopying> // 蓝牙设备的 UUID 标识符 @property(readonly, nonatomic) NSUUID *identifier NS_AVAILABLE(NA, 7_0);
-
惟一标示符(而且不会变的)是设备的 MAC 地址,对于 Android 来讲,轻轻松松就能拿到,但对于 iOS,目前这一属性仍是私有的。
-
若是必定有这样的需求(即必定要使用 MAC 地址),能够和硬件工程师沟通,使用下面的某一种方式解决:
- 将 MAC 地址写在某一个蓝牙特征中,当咱们链接蓝牙设备以后,经过某一个特征获取 MAC 地址。
- 将 MAC 地址放在蓝牙设备的广播数据当中,而后在广播的时候,将 MAC 地址以广播的形式发出来,在不创建链接的状况下,就能拿到 MAC 地址。
- 咱们能够经过蓝牙设备的出厂设备或者后期手动修改蓝牙设备的 name,做为惟一标识。
4.3.3 检查设备是否能做为 central
-
初始化 CBCentralManager 的时候,传入的 self 代理会触发回调
centralManagerDidUpdateState:
。在该方法中可经过central.state
来得到当前设备是否能做为 central。state 为 CBManagerState 枚举类型,具体定义以下: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"); typedef NS_ENUM(NSInteger, CBManagerState) { CBManagerStateUnknown = 0, CBManagerStateResetting, CBManagerStateUnsupported, CBManagerStateUnauthorized, CBManagerStatePoweredOff, CBManagerStatePoweredOn, } NS_ENUM_AVAILABLE(NA, 10_0);
-
只有当
state == CBManagerStatePoweredOn
时,才表明正常。
4.3.4 检查 characteristic 访问权限
-
若是不检查也没事,由于无权访问会在回调中返回 error,但这毕竟是马后炮。若是有须要在读写以前检测,能够经过 characteristic 的 properties 属性来判断。该属性为 CBCharacteristicProperties 的 NS_OPIONS。
typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) { CBCharacteristicPropertyBroadcast = 0x01, CBCharacteristicPropertyRead = 0x02, CBCharacteristicPropertyWriteWithoutResponse = 0x04, CBCharacteristicPropertyWrite = 0x08, CBCharacteristicPropertyNotify = 0x10, CBCharacteristicPropertyIndicate = 0x20, CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40, CBCharacteristicPropertyExtendedProperties = 0x80, CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x100, CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x200 };
-
多个权限能够经过
|
和&
来判断是否支持,好比判断是否支持读或写。if (characteristic.properties & (CBCharacteristicPropertyRead | CBCharacteristicPropertyWrite)) { }
4.3.5 写入后是否回调
-
在写入 characteristic 时,能够选择是否在写入后进行回调。调用方法和枚举常量以下。
[self.connectedPeripheral writeValue:data forCharacteristic:connectedCharacteristic type:CBCharacteristicWriteWithResponse]; typedef NS_ENUM(NSInteger, CBCharacteristicWriteType) { CBCharacteristicWriteWithResponse = 0, CBCharacteristicWriteWithoutResponse, };
-
回调方法为
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error;
-
因此即便没有判断写入权限,也能够经过回调的 error 来判断,但这样比起写入前判断更耗资源。
4.4 数据读写 - 最佳实践
- 在设备上通常都有不少地方要用到无线电通讯,Wi-Fi、传统的蓝牙、以及使用 BLE 通讯的 app 等等。这些服务都是很耗资源的,尤为是在 iOS 设备上。因此这里会讲解到如何正确的使用 BLE 以达到节能的效果。
4.4.1 只扫描你须要的 peripheral
-
在调用 CBCentralManager 的
scanForPeripheralsWithServices:options:
方法时,central 会打开无线电去监听正在广播的 peripheral,而且这一过程不会自动超时。因此须要咱们手动设置 timer 去停掉。 -
若是只须要链接一个 peripheral,那应该在
centralManager:didConnectPeripheral:
的回调中,用 stopScan 方法中止搜索。- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { // 中止搜索 [central stopScan]; ...... }
4.4.2 只在必要的时候设置 CBCentralManagerScanOptionAllowDuplicatesKey
-
peripheral 每秒都在发送大量的数据包,
scanForPeripheralsWithServices:options:
方法会将同一 peripheral 发出的多个数据包合并为一个事件,而后每找到一个 peripheral 都会调用centralManager:didDiscoverPeripheral:advertisementData:RSSI:
方法。另外,当已发现的 peripheral 发送的数据包有变化时,这个代理方法一样会调用。 -
以上合并事件的操做是
scanForPeripheralsWithServices:options:
的默认行为,即未设置 option 参数。若是不想要默认行为,可将 option 设置为 CBCentralManagerScanOptionAllowDuplicatesKey。设置之后,每收到广播,就会调用上面的回调(不管广播数据是否同样)。[peripheral scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@YES}];
-
关闭默认行为通常用于如下场景:根据 peripheral 的距离来初始化链接(根据可用信号强度 RSSI 来判断)。设置这个 option 会对电池寿命和 app 的性能产生不利影响,因此必定要在必要的时候,再对其进行设置。
4.4.3 正确的搜索 service 与 characteristic
-
在搜索过程当中,并非全部的 service 和 characteristic 都是咱们须要的,若是所有搜索,依然会形成没必要要的资源浪费。假设你只须要用到 peripheral 提供的众多 service 中的两个,那么在搜索 service 的时候能够设置要搜索的 service 的 UUID。
[peripheral discoverServices:@[firstServiceUUID, secondServiceUUID]];
-
用这种方式搜索到 service 之后,也能够用相似的办法来限制 characteristic 的搜索范围(
discoverCharacteristics:forService:
)。
4.4.4 接收 characteristic 数据
-
接收 characteristic 数据的方式有两种:
- 在须要接收数据的时候,调用
readValueForCharacteristic:
,这种是须要主动去接收的。 - 用
setNotifyValue:forCharacteristic:
方法订阅,当有数据发送时,能够直接在回调中接收。
- 在须要接收数据的时候,调用
-
若是 characteristic 的数据常常变化,那么采用订阅的方式更好。
4.4.5 适时断开链接
-
在不用和 peripheral 通讯的时候,应当将链接断开,这也对节能有好处。
- 在如下两种状况下,链接应该被断开:
- 当 characteristic 再也不发送数据时。(能够经过 isNotifying 属性来判断)
- 你已经接收到了你所须要的全部数据时。
-
以上两种状况,都须要先结束订阅,而后断开链接。
// 结束订阅 [peripheral setNotifyValue:NO forCharacteristic:characteristic]; // 断开链接 [myCentralManager cancelPeripheralConnection:peripheral];
-
注意:
cancelPeripheralConnection:
是非阻塞性的,若是在 peripheral 挂起的状态去尝试断开链接,那么这个断开操做可能执行,也可能不会。由于可能还有其余的 central 连着它,因此取消链接并不表明底层链接也断开。从 app 的层面来说,在决定断开 peripheral 的时候,会调用 CBCentralManagerDelegate 的centralManager:didDisconnectPeripheral:error:
方法。
4.4.6 再次链接 peripheral
-
CoreBluetooth 提供了三种再次链接 peripheral 的方式:
- 调用
retrievePeripheralsWithIdentifiers:
方法,重连已知的 peripheral 列表中的 peripheral(之前发现的,或者之前链接过的)。 - 调用
retrieveConnectedPeripheralsWithServices:
方法,从新链接当前【系统】已经链接的 peripheral。 - 调用
scanForPeripheralsWithServices:options:
方法,链接搜索到的 peripheral。
- 调用
-
是否须要从新链接之前链接过的 peripheral 要取决于你的需求,下图展现了当你尝试重连时能够选择的流程:
-
三列表明着三种重连的方式。固然这也是你能够选择进行实现的,这三种方式也并非都须要去实现,依然取决于你的需求。
-
一、尝试链接已知的 peripheral
-
在第一次成功连上 peripheral 以后,iOS 设备会自动给 peripheral 生成一个 identifier(NSUUID 类型),这个标识符可经过 peripheral.identifier 来访问。这个属性由 CBPeriperal 的父类 CBPeer 提供,API 注释写着: The unique, persistent identifier associated with the peer.
-
由于 iOS 拿不到 peripheral 的 MAC 地址,因此没法惟一标识每一个硬件设备,根据这个注释来看,应该 Apple 更但愿你使用这个 identifer 而不是 MAC 地址。值得注意的是,不一样的 iOS 链接同一个 peripheral 得到的 identifier 是不同的。因此若是必定要得到惟一的 MAC 地址,能够和硬件工程师协商,让 peripheral 返给你。
-
当第一次链接上 peripheral 而且系统自动生成 identifier 以后,咱们须要将它存下来(可使用 NSUserDefaults)。在再次链接的时候,使用
retrievePeripheralsWithIdentifiers:
方法将以前记录的 peripheral 读取出来,而后咱们去调用connectPeripheral:options:
方法来进行从新链接。knownPeripherals = [myCentralManager retrievePeripheralsWithIdentifiers:savedIdentifiers]; [myCentralManager connectPeripheral:knownPeripherals options:nil];
-
调用这个方法以后,会返回一个 CBPeripheral 的数组,包含了之前连过的 peripheral。若是这个数组为空,则说明没找到,那么你须要去尝试另外两种重连方式。若是这个数组有多个值,那么你应该提供一个界面让用户去选择。
-
若是用户选择了一个,那么能够调用
connectPeripheral:options:
方法来进行链接,链接成功以后依然会走centralManager:didConnectPeripheral:
回调。 -
注意,链接失败一般有一下几个缘由:
- peripheral 与 central 的距离超出了链接范围。
- 有一些 BLE 设备的地址是周期性变化的。因此,即便 peripheral 就在旁边,若是它的地址已经变化,而你记录的地址已经变化了,那么也是链接不上的。若是是由于这种缘由链接不上,那你须要调用
scanForPeripheralsWithServices:options:
方法来进行从新搜索。
-
更多关于随机地址的资料能够看 《苹果产品的蓝牙附件设计指南》。
-
-
二、链接系统已经链接过的 peripheral
-
另一种重连的方式是经过检测当前系统是否已经连上了须要的 peripheral(可能被其余 app 链接了)。调用
retrieveConnectedPeripheralsWithServices:
会返回一个 CBPeripheral 的数组。 -
由于当前可能不止一个 peripheral 连上的,因此你能够经过传入一个 service 的 CBUUID 的数组来过滤掉一些不须要的 peripheral。一样,这个数组有可能为空,也有可能不为空,处理方式和上一节的方式相同。找到要链接的 peripheral 以后,处理方式也和上一节相同。
-
4.4.7 自动链接
-
能够在程序启动或者须要使用蓝牙的时候,判断是否须要自动链接。若是须要,则能够尝试链接已知的 peripheral。这个重连上一个小节恰好提到过:在上一次链接成功后,记录 peripheral 的 identifier,而后重连的时候,读取便可。
-
在自动链接这一块,还有一个小坑。在使用
retrievePeripheralsWithIdentifiers:
方法将以前记录的 peripheral 读取出来,而后咱们去调用connectPeripheral:options:
方法来进行从新链接。我以前怎么试都有问题,最后在 CBCentralManager 的文档上找到了这样一句话:Pending connection attempts are also canceled automatically when peripheral is deallocated.这句话的意思是说,在 peripheral 的引用释放以后,链接会自动取消。由于我在读取出来以后,接收的 CBPeripheral 是临时变量,没有强引用,因此出了做用域就自动释放了,从而链接也自动释放了。因此在自动链接的时候,读取出来别忘了去保存引用。
4.4.8 链接超时
- 由于 CoreBluetooth 并未帮咱们处理链接超时相关的操做,因此超时的判断还须要本身维护一个 timer。能够在 start scan 的时候启动(注意若是是自动链接,那么重连的时候也须要启动),而后在搜索到之后 stop timer。固然,若是超时,则看你具体的处理方式了,能够选择 stop scan,而后让用户手动刷新。
4.4.9 蓝牙名称更新
-
在 peripheral 修更名字事后,iOS 存在搜索到蓝牙名字还未更新的问题。先来讲一下出现这个问题的缘由,如下是摘自 Apple Developer Forums 上的回答:
- There are 2 names to consider. The advertising name and the GAP (Generic Access Profile) name.
- For a peripheral which iOS has never connected before, the ‘name’ property reported is the advertising name. Once it is connected, the GAP name is cached, and is reported as the peripheral’s name. GAP name is considered a “better” name due to the size restrictions on the advertising name.
- There is no rule that says both names must match. That depends on your use case and implementation. Some people will consider the GAP name as the fixed name, but the advertising name more of an “alias”, as it can easily be changed.
- If you want both names in sync, you should change the GAP name as well along with the advertised name. Implemented properly, your CB manager delegate will receive a call to – peripheralDidUpdateName:
-
If you want to manually clear the cache, you need to reset the iOS device.
-
大体意思是:peripheral 其实存在两个名字,一个 advertising name,一个 GAP name。在没有链接过期,收到的 CBPeripheral 的 name 属性是 advertising name(暂且把这个名字称为正确的名字,由于在升级或换名字以后,这个名字才是最新的)。一旦 iOS 设备和 peripheral 链接过,GAP name 就会被缓存,与此同时,CBPeripheral 的 name 属性变成 GAP name,因此在搜索到设备时,打印 CBPeripheral 的 name,怎么都没有变。上文给出的解释是,由于数据大小限制,GAP name 更优于 advertising name。这两个名字不要求要相同,而且,若是要清除 GAP name 的缓存,那么须要重置 iOS 设备。
-
下面来讲一下解决方案,主要分为两种,一种是更新 GAP name,一种是直接拿 advertising name。
-
更新 GAP name 的方式我目前没找到方法,有些人说是 Apple 的 bug,这个还不清楚,但愿有解决方案的朋友联系我。
-
那就来讲下怎么拿到 advertising name 吧。
centralManager:didDiscoverPeripheral:advertisementData:RSSI:
方法中能够经过 advertisementData 来拿到 advertising name,以下:NSLog(@"%@", advertisementData[CBAdvertisementDataLocalNameKey]);
-
而后能够选择把这个 name 返回外部容器来进行显示,用户也能够经过这个来进行选择。
-
关于这个部分查找的资料有:
4.5 数据读写 - OTA 固件升级与文件传输
-
OTA(Over-the-Air):空中传输,通常用于固件升级,网上的资料大可能是怎么给手机系统升级,少部分资料是 peripheral 怎么接收并进行升级,惟独没有 central 端怎么传输的。其实文件传输很简单,只是蓝牙传输的数据大小使得这一步骤稍显复杂。
-
首先,文件传输,其实也是传输的数据,即 NSData,和普通的 peripheral 写入没什么区别。固件升级的文件通常是
.bin
文件,也有.zip
的。不过这些文件,都是数据,因此首先将文件转为 NSData。 -
可是 data 通常很长,毕竟是文件。直接经过
writeValue:forCharacteristic:type:
写入的话,不会有任何回调。哪怕是错误的回调,都没有。这是由于蓝牙单次传输的数据大小是有限制的。具体的大小我不太明确,看到 StackOverflow 上有人给出的 20 bytes,我就直接用了,并无去具体查证(不过试了试 30 bytes,回调数据长度错误)。既然长度是 20,那在每次发送成功的回调中,再进行发送就好,直到发送完成。 -
下面来讨论下是怎么作的吧。
-
一、区别普通写入与文件写入
-
分割数据并发送,每次都要记录上一次已经写入长度(偏移量 self.otaSubDataOffset),而后截取 20 个长度。须要注意的是最后一次的长度,注意不要越界了。
-
数据的发送和普通写入没什么区别。
-
-
二、当前已发送长度与发送结束的回调
-
由于 OTA 的写入可能须要作进度条之类的,因此最好和普通的写入回调区分开。
-
在每次写入成功中,判断是否已经发送完成(已发送的长度和总长度相比)。若是还未发送完成,则返回已发送的长度给控制器(能够经过代理实现)。若是已发送完成,则返回发送完成(能够经过代理实现)。
-
五、外设模式的使用
5.1 App 做为外设被链接的实现
-
一、启动一个 Peripheral 管理对象
-
打开 peripheralManager,设置 peripheralManager 的委托。
// 包含头文件 #import <CoreBluetooth/CoreBluetooth.h> // 遵照协议 @interface ViewController () <CBPeripheralManagerDelegate> // 外设管理器 @property (nonatomic, strong) CBPeripheralManager *peripheralManager; - (IBAction)start:(UIButton *)sender { // 初始化 centralManager,nil 默认为主线程 self.peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil]; } #pragma mark - CBPeripheralManagerDelegate // 检查 App 设备蓝牙是否可用,协议方法 - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { // 在初始化 CBPeripheralManager 的时候会打开设备,只有当设备正确打开后才能使用 switch (peripheral.state){ case CBManagerStatePoweredOn: // 蓝牙已打开 NSLog(@"蓝牙已打开"); // 添加服务 [self addServiceToPeripheralManager]; break; case CBManagerStateUnsupported: NSLog(@"您的设备不支持蓝牙或蓝牙 4.0"); break; case CBManagerStateUnauthorized: NSLog(@"未受权打开蓝牙"); break; case CBManagerStatePoweredOff: // 蓝牙未打开,系统会自动提示打开,因此不用自行提示 default: break; } }
-
-
二、配置本地 Peripheral,设置服务、特性、描述、权限等等
-
建立 characteristics,characteristics 的 description,建立 service,把 characteristics 添加到 service 中,再把 service 添加到 peripheralManager 中。
-
当 peripheral 成功打开后,才能够配置 service 和 characteristics。这里建立的 service 和 characteristics 对象是 CBMutableCharacteristic 和 CBMutableService。他们的区别就像 NSArray 和 NSMutableArray 区别相似。咱们先建立 characteristics 和 description,description 是 characteristics 的描述,描述分不少种,经常使用的就是 CBUUIDCharacteristicUserDescriptionString。
// 定义设备服务和特性的 UUIDString static NSString * const ServiceUUIDString1 = @"A77B"; static NSString * const ServiceUUIDString2 = @"D44BC439-ABFD-45A2-B575-A77BC549E3CC"; static NSString * const CharacteristicNotiyUUIDString = @"D44BC439-ABFD-45A2-B575-A77BC549E301"; static NSString * const CharacteristicReadWriteUUIDString = @"D44BC439-ABFD-45A2-B575-A77BC549E302"; static NSString * const CharacteristicReadUUIDString = @"D44BC439-ABFD-45A2-B575-A77BC549E303"; // 配置本地 Peripheral - (void)addServiceToPeripheralManager { // 设置能够通知的 Characteristic /* properties :CBCharacteristicPropertyNotify permissions:CBAttributePermissionsReadable */ CBMutableCharacteristic *notiyCharacteristic = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:CharacteristicNotiyUUIDString] properties:CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable]; // 设置可读写的 characteristics /* properties :CBCharacteristicPropertyWrite | CBCharacteristicPropertyRead permissions:CBAttributePermissionsReadable | CBAttributePermissionsWriteable */ CBMutableCharacteristic *readwriteCharacteristic = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:CharacteristicReadWriteUUIDString] properties:CBCharacteristicPropertyWrite | CBCharacteristicPropertyRead value:nil permissions:CBAttributePermissionsReadable | CBAttributePermissionsWriteable]; // 设置 characteristics 的 description CBUUID *CBUUIDCharacteristicUserDescriptionStringUUID = [CBUUID UUIDWithString:CBUUIDCharacteristicUserDescriptionString]; CBMutableDescriptor *readwriteCharacteristicDescription1 = [[CBMutableDescriptor alloc] initWithType:CBUUIDCharacteristicUserDescriptionStringUUID value:@"name"]; readwriteCharacteristic.descriptors = @[readwriteCharacteristicDescription1]; // 只读的 Characteristic /* properties :CBCharacteristicPropertyRead permissions:CBAttributePermissionsReadable */ CBMutableCharacteristic *readCharacteristic = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:CharacteristicReadUUIDString] properties:CBCharacteristicPropertyRead value:nil permissions:CBAttributePermissionsReadable]; // service1 初始化并加入两个 characteristics CBMutableService *service1 = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:ServiceUUIDString1] primary:YES]; service1.characteristics = @[notiyCharacteristic, readwriteCharacteristic]; // service2 初始化并加入一个 characteristics CBMutableService *service2 = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:ServiceUUIDString2] primary:YES]; service2.characteristics = @[readCharacteristic]; // 添加服务,添加后就会调用代理的 peripheralManager:didAddService:error: 方法 [self.peripheralManager addService:service1]; [self.peripheralManager addService:service2]; } // 已经添加服务,协议方法 - (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(nullable NSError *)error { NSLog(@"已经添加服务 %@", service); }
-
-
三、开启广播 advertising
-
添加发送广播后悔调用代理的 peripheralManagerDidStartAdvertising:error: 方法。
// 已经添加服务,协议方法 - (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(nullable NSError *)error { NSLog(@"已经添加服务 %@", service); static int serviceNum = 0; if (error == nil) { serviceNum++; } // 由于咱们添加了 2 个服务,因此 2 次都添加完成后才去发送广播 if (serviceNum == 2) { // 添加服务后能够在此向外界发出广播 /* @"LocalNameKey" 为在其余设备上搜索到的蓝牙设备名称 */ [peripheral startAdvertising:@{CBAdvertisementDataServiceUUIDsKey:@[[CBUUID UUIDWithString:ServiceUUIDString1], [CBUUID UUIDWithString:ServiceUUIDString2]], CBAdvertisementDataLocalNameKey:@"LocalNameKey"}]; } } // 已经开始发送广播,协议方法 - (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(nullable NSError *)error { NSLog(@"已经开始发送广播"); }
-
-
四、设置处理订阅、取消订阅、读 characteristic、写 characteristic 的委托方法
```objc // 订阅 characteristics,协议方法 - (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic { NSLog(@"订阅了 %@ 的数据", characteristic.UUID); // 每秒执行一次给主设备发送一个当前时间的秒数 [self sendData:@"hello" oCharacteristic:characteristic]; } // 取消订阅 characteristics,协议方法 - (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic { NSLog(@"取消订阅 %@ 的数据",characteristic.UUID); } // 准备好发送订阅 - (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral { } // 收到读 characteristics 请求,协议方法 - (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request { NSLog(@"收到读 characteristics 请求"); // 判断是否有读数据的权限 if (request.characteristic.properties & CBCharacteristicPropertyRead) { NSData *data = request.characteristic.value; [request setValue:data]; // 对请求做出成功响应 [peripheral respondToRequest:request withResult:CBATTErrorSuccess]; } else { [peripheral respondToRequest:request withResult:CBATTErrorWriteNotPermitted]; } } // 收到写 characteristics 请求,协议方法 - (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray<CBATTRequest *> *)requests { NSLog(@"收到写 characteristics 请求"); CBATTRequest *request = requests[0]; // 判断是否有写数据的权限 if (request.characteristic.properties & CBCharacteristicPropertyWrite) { // 须要转换成 CBMutableCharacteristic 对象才能进行写值 CBMutableCharacteristic *c =(CBMutableCharacteristic *)request.characteristic; c.value = request.value; [peripheral respondToRequest:request withResult:CBATTErrorSuccess]; } else { [peripheral respondToRequest:request withResult:CBATTErrorWriteNotPermitted]; } } // 发送数据,自定义方法 - (void)sendData:(NSString *)string oCharacteristic:(CBCharacteristic *)characteristic { NSData *sendData = [string dataUsingEncoding:NSUTF8StringEncoding]; // 发送 [self.peripheralManager updateValue:sendData forCharacteristic:(CBMutableCharacteristic *)characteristic onSubscribedCentrals:nil]; } ```
5.2 做为 Peripheral 时的请求响应
5.2.1 初始化 CBPeripheralManager
-
将设备做为 peripheral,第一步就是初始化 CBPeripheralManager 对象。能够经过调用 CBPeripheralManager 的
initWithDelegate:queue:options:
方法来进行初始化:myPeripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
-
上面的几个参数中,将 self 设为代理来接收相关回调,queue 为 nil 表示在主线程。
-
当你调用上面这方法后,便会回调
peripheralManagerDidUpdateState:
。因此在此以前,你须要先遵循CBPeripheralManagerDelegate
。这个代理方法能获取当前 iOS 设备可否做为 peripheral。
5.2.2 配置 service 和 characteristic
-
就像以前讲到的同样,peripheral 数据库是一个树形结构。
-
因此在建立 peripheral 的时候,也要像这种树形结构同样,将 service 和 characteristic 装进去。在此以前,咱们须要作的是学会如何标识 service 和 characteristic。
-
一、使用 UUID 来标识 service 和 characteristic
- service 和 characteristic 都经过 128 位的 UUID 来进行标识,Core Bluetooth 将 UUID 封装为了 CBUUID 。关于详细 UUID 的介绍,请参考上面的 4.3.1 CBUUID 讲解。
-
二、为自定义的 service 和 characteristic 建立 UUID
-
你的 service 或者 characteristic 的 UUID 并无公共的 UUID,这时你须要建立本身的 UUID。
-
使用命令行的
uuidgen
能很容易的生成 UUID。首先打开终端,为你的每个 service 和 characteristic 建立 UUID。在终端输入uuidgen
而后回车,具体以下:$ uuidgen 71DA3FD1-7E10-41C1-B16F-4430B506CDE7
-
能够经过
UUIDWithString:
方法,将 UUID 生成 CBUUID 对象。CBUUID *myCustomServiceUUID = [CBUUID UUIDWithString:@"71DA3FD1-7E10-41C1-B16F-4430B506CDE7"];
-
-
三、构建 service 和 characteristic 树形结构
-
在将 UUID 打包为 CBUUID 以后,就能够建立 CBMutableService 和 CBMutableCharacteristic 并把他们组成一个树形结构了。建立 CBMutableCharacteristic 对象能够经过该类的
initWithType:properties:value:permissions:
方法:myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead value:myValue permissions:CBAttributePermissionsReadable];
-
建立 characteristic 的时候,就为他设置了 properties 和 permissions。这两个属性分别定义了 characteristic 的可读写状态和 central 链接后是否能订阅。上面这种初始化方式,表明着 characteristic 可读。更多的选项,能够去看看 CBMutableCharacteristic Class Reference。
-
若是给 characteristic 设置了 value 参数,那么这个 value 会被缓存,而且 properties 和 permissions 会自动设置为可读。若是想要 characteristic 可写,或者在其生命周期会改变它的值,那须要将 value 设置为 nil。这样的话,就会动态的来处理 value 。
-
如今已经成功的建立了 characteristic,下一步就是建立一个 service,并将它们构成树形结构。调用 CBMutableService 的
initWithType:primary:
方法来初始化 service:myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];
-
第二个参数 primary 设置为 YES 表示该 service 为 primary service(主服务),与 secondary service(次服务)相对。primary service 描述了设备的主要功能,而且能包含其余 service。secondary service 描述的是引用它的那个 service 的相关信息。好比,一个心率监测器,primary service 描述的是当前心率数据,secondary service 描述描述的是当前电量。
-
建立了 service 以后,就能够包含 characteristic 了:
myService.characteristics = @[myCharacteristic];
-
5.2.3 发布 service 和 characteristic
-
构建好树形结构以后,接下来便须要将这结构加入设备的数据库。这一操做 Core Bluetooth 已经封装好了,调用 CBPeripheralManager 的
addService:
方法便可:```objc [myPeripheralManager addService:myService]; ```
-
当调用以上方法时,便会回调 CBPeripheralDelegate 的
peripheralManager:didAddService:error:
回调。当有错误,或者当前 service 不能发布的时候,能够在这个代理中来进行检测:- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error { if (error) { NSLog(@"Error publishing service: %@", [error localizedDescription]); } }
-
当你发布 service 以后,service 就会缓存下来,而且没法再修改。
5.2.4 广播 service
-
搞定发布 service 和 characteristic 以后,就能够开始给正在监听的 central 发广播了。能够经过调用 CBPeripheralManager 的
startAdvertising:
方法并传入字典做为参数来进行广播:[myPeripheralManager startAdvertising:@{CBAdvertisementDataServiceUUIDsKey:@[myFirstService.UUID, mySecondService.UUID]}];
-
上面的代码中,key 只用到了 CBAdvertisementDataServiceUUIDsKey,对应的 value 是包含须要广播的 service 的 CBUUID 类型数组。除此以外,还有如下 key:
NSString *const CBAdvertisementDataLocalNameKey; // 在其余设备上搜索到的蓝牙设备名称 NSString *const CBAdvertisementDataManufacturerDataKey; NSString *const CBAdvertisementDataServiceDataKey; NSString *const CBAdvertisementDataServiceUUIDsKey; // 添加的蓝牙服务的 UUID NSString *const CBAdvertisementDataOverflowServiceUUIDsKey; NSString *const CBAdvertisementDataTxPowerLevelKey; NSString *const CBAdvertisementDataIsConnectable; NSString *const CBAdvertisementDataSolicitedServiceUUIDsKey;
-
可是只有 CBAdvertisementDataLocalNameKey 和 CBAdvertisementDataServiceUUIDsKey 才是 peripheral Manager 支持的。
-
当开始广播时,peripheral Manager 会回调
peripheralManagerDidStartAdvertising:error:
方法。若是有错或者 service 没法进行广播,则能够在该该方法中检测:- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error { if (error) { NSLog(@"Error advertising: %@", [error localizedDescription]); } }
-
由于空间的限制,而且还可能有多个 app 在同时发起广播,因此数据广播基于 best effort(即在接口发生拥塞时,当即丢包,直到业务量减少)。
-
广播服务在程序挂起时依然可用。
5.2.5 响应 central 的读写操做
-
在链接到一个或多个 central 以后,peripheral 有可能会收到读写请求。此时,你应该根据请求做出相应的响应,接下来便会提到这方面的处理。
-
一、读取请求
-
当收到读请求时,会回调
peripheralManager:didReceiveReadRequest:
方法。该回调将请求封装为了 CBATTRequest 对象,在该对象中,包含不少可用的属性。 -
其中一种用法是在收到读请求时,能够经过 CBATTRequest 的 characteristic 属性来判断当前被读的 characteristic 是哪个 characteristic:
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request { if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) { } }
-
匹配上 UUID 以后,接下来须要确保读取数据的 offset(偏移量)不会超过 characteristic 数据的总长度:
if (request.offset > myCharacteristic.value.length) { [myPeripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset]; return; }
-
假设偏移量验证经过,下面须要截取 characteristic 中的数据,并赋值给
request.value
。注意,offset 也要参与计算:request.value = [myCharacteristic.value subdataWithRange:NSMakeRange(request.offset, myCharacteristic.value.length - request.offset)];
-
读取完成后,记着调用 CBPeripheralManager 的
respondToRequest:withResult:
方法,告诉 central 已经读取成功了:[myPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
-
若是 UUID 匹配不上,或者是由于其余缘由致使读取失败,那么也应该调用
respondToRequest:withResult:
方法,并返回失败缘由。官方提供了一个失败缘由枚举,可能有你须要的。
-
-
二、写入请求
-
写入请求和读取请求同样简单。当 central 想要写入一个或多个 characteristic 时,CBPeripheralManager 回调
peripheralManager:didReceiveWriteRequests:
。该方法会得到一个 CBATTRequest 数组,包含全部写入请求。当确保一切验证没问题后(与读取操做验证相似:UUID 与 offset),即可以进行写入:myCharacteristic.value = requests[0].value;
-
成功后,一样去调用
respondToRequest:withResult:
。可是和读取操做不一样的是,读取只有一个 CBATTRequest,可是写入是一个 CBATTRequest 数组,因此这里直接传入第一个 request 就行:[myPeripheralManager respondToRequest:[requests objectAtIndex:0] withResult:CBATTErrorSuccess];
-
由于收到的是一个请求数组,因此,当他们其中有任何一个不知足条件,那就没必要再处理下去了,直接调用
respondToRequest:withResult:
方法返回相应的错误。
-
5.2.6 发送更新数据给订阅了的 central
-
central 可能会订阅了一个或多个 characteristic,当数据更新时,须要给他们发送通知。下面就来详细介绍下。
-
当 central 订阅 characteristic 的时候,会回调 CBPeripheralManager 的
peripheralManager:central:didSubscribeToCharacteristic:
方法:- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic { NSLog(@"Central subscribed to characteristic %@", characteristic); }
-
经过上面这个代理,能够用个数组来保存被订阅的 characteristic,并在它们的数据更新时,调用 CBPeripheralManager 的
updateValue:forCharacteristic:onSubscribedCentrals:
方法来告诉 central 有新的数据:NSData *updatedValue = // fetch the characteristic's new value BOOL didSendValue = [myPeripheralManager updateValue:updatedValue forCharacteristic:characteristic onSubscribedCentrals:nil];
-
这个方法的最后一个参数能指定要通知的 central。若是参数为 nil,则表示想全部订阅了的 central 发送通知。
-
同时
updateValue:forCharacteristic:onSubscribedCentrals:
方法会返回一个 BOOL 标识是否发送成功。若是发送队列任务是满的,则会返回 NO。当有可用的空间时,会回调peripheralManagerIsReadyToUpdateSubscribers:
方法。因此你能够在这个回调用调用updateValue:forCharacteristic:onSubscribedCentrals:
从新发送数据。 -
发送数据使用到的是通知,当你更新订阅的 central 时,应该调用一次
updateValue:forCharacteristic:onSubscribedCentrals:
。 -
由于 characteristic 数据大小的关系,不是全部的更新都能发送成功,这种问题应该由 central 端来处理。调用 CBPeripheral 的
readValueForCharacteristic:
方法,来主动获取数据。
5.3 请求响应 - 最佳实践
5.3.1 关于广播的思考
-
广播是 peripheral 的一个重要操做,接下来会讲到广播的正确姿式。
-
一、注意广播对数据大小的限制
-
正如前文提到过的那样,广播是经过调用 CBPeripheralManager 的
startAdvertising:
方法发起的。当你将要发送的数据打包成字典后,千万要记住数据大小是有限制的。 -
即便广播能够包含 peripheral 的不少信息,可是其实只须要广播 peripheral 的名称和 service 的 UUID 就足够了。也就是构建字典时,填写 CBAdvertisementDataLocalNameKey 和 CBAdvertisementDataServiceUUIDsKey 对应的 value 便可,若是使用其余 key,将会致使错误。
-
当 app 运行在前台时,有 28 bytes 的空间可用于广播。若是这 28 bytes 用完了,则会在扫描响应时额外分配 10 bytes 的空间,但这空间只能用于被 CBAdvertisementDataLocalNameKey 修饰的 local name(即在
startAdvertising:
时传入的数据)。以上提到的空间,均不包含 2 bytes 的报文头。被 CBAdvertisementDataServiceUUIDsKey 修饰的 service 的 UUID 数组数据,均不会添加到特殊的 overflow 区域。而且这些 service 只能被 iOS 设备发现。当程序挂起后,local name 和 UUID 都会被加入到 overflow 区。 -
为了保证在有限的空间中,正确的标识设备和 service UUID,请正确构建广播的数据。
-
-
二、只广播必要的数据
- 当 peripheral 想要被发现时,它会向外界发送广播,此时会用到设备的无线电(固然还有电池)。一旦链接成功,central 便能直接从 peripheral 中读取数据了,那么此时广播的数据将再也不有用。因此,为了减小无线电的使用、提升手机性能、保护设备电池,应该在被链接后,及时关闭广播。中止广播调用 CBPeripheralManager 的
stopAdvertising
方法便可。
[myPeripheralManager stopAdvertising];
- 当 peripheral 想要被发现时,它会向外界发送广播,此时会用到设备的无线电(固然还有电池)。一旦链接成功,central 便能直接从 peripheral 中读取数据了,那么此时广播的数据将再也不有用。因此,为了减小无线电的使用、提升手机性能、保护设备电池,应该在被链接后,及时关闭广播。中止广播调用 CBPeripheralManager 的
-
三、手动开启广播
- 其实何时应该广播,多数状况下,用户比咱们更清楚。好比,他们知道周围没有开着的 BLE 设备,那他就不会把 peripheral 的广播打开。因此提供给用户一个手动开启广播的 UI 更为合适。
5.3.2 配置 characteristic
-
在建立 characteristic 的时候,就为它设定了相应的 properties、value 和 promissions。这些属性决定了 central 如何和 characteristic 通讯。properties 和 promissions 可能须要根据 app 的需求来设置,下来就来谈谈如何配置 characteristic:
-
一、让 characteristic 支持通知
-
以前在 central 的时候提到过,若是要读取常常变化的 characteristic 的数据,更推荐使用订阅。因此,若是能够,最好 characteristic 容许订阅。
-
若是像下面这样初始化 characteristic 就是容许读和订阅:
myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable];
-
-
二、限制只能配对的 central 才能访问敏感信息
-
有些时候,可能有这样的需求:须要 service 的一个或多个 characteristic 的数据安全性。假若有一个社交媒体的 service,那么它的 characteristic 可能包含了用户的姓名、邮箱等私人信息,因此只让信任的 central 才能访问这些数据是颇有必要的。
-
这能够经过设置相应的 properties 和 promissions 来达到效果:
emailCharacteristic = [[CBMutableCharacteristic alloc] initWithType:emailCharacteristicUUID properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotifyEncryptionRequired value:nil permissions:CBAttributePermissionsReadEncryptionRequired];
-
像上面这样设置,便能只让配对的 central 才能进行订阅。而且在链接过程当中,Core Bluetooth 还会自动创建安全链接。
-
在尝试配对时,两端都会弹出警告框,central 端会提供 code,peripheral 端必需要输入该 code 才能配对成功。成功以后,peripheral 才会信任该 central,并容许读写数据。
-
六、后台运行蓝牙服务
-
对于 iOS app 来讲,知道如今是运行在前台和后台是相当重要的。由于当程序挂起后,对资源的使用是至关有限的。关于多任务的介绍,能够看 app 开发手册。
-
默认状况下,Core Bluetooth 是不会在后台运行的(不管是 central 仍是 peripheral)。但你也能够配置在 app 收到事件后,从挂起状态唤醒。即便程序不是彻底的支持后台模式,也能够要求在有重要事件时接收系统通知。
-
即便在以上两种状况下(彻底容许后台和部分容许后台),程序也有可能不会永远挂起。在前台程序须要更多内存时,被挂起的程序颇有可能会被强制退出,那样会断开全部的链接。从 iOS 7 开始,可以先保存状态(不管是 central 仍是 peripheral),并在从新打开 app 时还原这些状态。经过这一特性,就能够作长时间操做了。
6.1 运行在前台的 app(Foreground-Only)
-
除非去申请后台权限,不然 app 都是只在前台运行的,程序在进入后台不久便会切换到挂起状态。挂起后,程序将没法再接收任何蓝牙事件。
-
对于 central 来讲,挂起将没法再进行扫描和搜索 peripheral。对于 peripheral 来讲,将没法再发起广播,central 也没法再访问动态变化的 characteristic 数据,访问将返回 error。
-
根据不一样状况,这种机制会影响程序在如下几个方面的运用。你正在读取 peripheral 的数据,结果程序被挂起了(多是用户切换到了另一个 app),此时链接会被断开,可是要直到程序从新唤醒时,你才知道被断开了。
-
一、利用链接 Peripheral 时的选项
-
Foreground-Only app 在挂起的时候,便会加入到系统的一个队列中,当程序从新唤醒时,系统便会通知程序。Core Bluetooth 会在程序中包含 central 时,给用户以提示。用户可根据提示来判断是否要唤醒该 app。
-
能够利用 central 在链接 peripheral 时的方法
connectPeripheral:options:
中的 options 来触发提示:CBConnectPeripheralOptionNotifyOnConnectionKey :在链接成功后,程序被挂起,给出系统提示。 CBConnectPeripheralOptionNotifyOnDisconnectionKey :在程序挂起后,蓝牙链接断开时,给出系统提示。 CBConnectPeripheralOptionNotifyOnNotificationKey :在程序挂起后,收到 peripheral 数据时,给出系统提示。
-
6.2 Core Bluetooth 后台模式
-
若是你想让你的 app 能在后台运行蓝牙,那么必须在 info.plist 中打开蓝牙的后台运行模式。当配置以后,收到相关事件便会从后台唤醒。这一机制对按期接收数据的 app 颇有用,好比心率监测器。
-
下面会介绍两种后台模式,一种是做为 central 的,一种是做为 peripheral 的,若是 app 两种角色都有,那则须要开启两种模式。配置便是在 info.plist 中添加
UIBackgroundModes key
,类型为数组,value 则根据你当前角色来选择:bluetooth-central
:即 Central。bluetooth-peripheral
:即 Peripheral。
-
这个配置在 Xcode 中,也能够在 Capabilities 中进行配置,而不用直接面对 key-value。若是要看到 key-value,能够在 info.plist 中打开查看。
-
一、做为 Central 的后台模式
-
若是在 info.plist 中配置了 UIBackgroundModes – bluetooth-central,那么系统则容许程序在后台处理蓝牙相关事件。在程序进入后台后,依然能扫描、搜索 peripheral,而且还能进行数据交互。当 CBCentralManagerDelegate 和 CBPeripheralDelegate 的代理方法被调用时,系统将会唤醒程序。此时容许你去处理重要的事件,好比:链接的创建或断开,peripheral 发送了数据,central manager 的状态改变。
-
虽然此时程序能在后台运行,可是对 peripheral 的扫描和在前台时是不同的。实际状况是这样的:
- 设置的 CBCentralManagerScanOptionAllowDuplicatesKey 将失效,并将发现的多个 peripheral 广播的事件合并为一个。
-
若是所有的 app 都在后台搜索 peripheral,那么每次搜索的时间间隔会更大。这会致使搜索到 peripheral 的时间变长。
-
这些相应的调整会减小无线电使用,并提高续航能力。
-
-
二、做为 peripheral 的后台模式
-
做为 peripheral 时,若是须要支持后台模式,则在 info.plist 中配置 UIBackgroundModes – bluetooth-peripheral。配置后,系统会在有读写请求和订阅事件时,唤醒程序。
-
在后台,除了容许处理读写请求和订阅事件外,Core Bluetooth 框架还容许 peripheral 发出广播。一样,广播事件也有先后台区别。在后台发起时是这样的:
- CBAdvertisementDataLocalNameKey 将失效,在广播时,广播数据将再也不包含 peripheral 的名字。
- 被 CBAdvertisementDataServiceUUIDsKey 修饰的 UUID 数组将会被放到 overflow 区域中,意味着只能被明确标识了搜索 service UUID 的 iOS 设备找到。
- 若是全部 app 都在后台发起广播,那么发起频率会下降。
-
6.3 巧妙的使用后台模式
-
虽然程序支持一个或多个 Core Bluetooth 服务在后台运行,但也不要滥用。由于蓝牙服务会占用 iOS 设备的无线电资源,这也会间接影响到续航能力,因此尽量少的去使用后台模式。app 会唤醒程序并处理相关事务,完成后又会快速回到挂起状态。
-
不管是 central 仍是 peripheral,要支持后台模式都应该遵循如下几点:
- 程序应该提供 UI,让用户决定是否要在后台运行。
- 一旦程序在后台被唤醒,程序只有 10s 的时间来处理相关事务。因此应该在程序再次挂起前处理完事件。后台运行的太耗时的程序会被系统强制关闭进程。
- 处理无关的事件不该该唤醒程序。
-
和后台运行的更多介绍,能够查看 App Programming Guide for iOS。
6.4 处理常驻后台任务
-
某些 app 可能须要 Core Bluetooth 常驻后台,好比,一款用 BLE 技术和门锁通讯的 app。当用户离开时,自动上锁,回来时,自动开锁(即便程序运行在后台)。当用户离开时,可能已超出蓝牙链接范围,因此没办法给锁通讯。此时能够调用 CBCentralManager 的
connectPeripheral:options:
方法,由于该方法没有超时设置,因此,在用户返回时,能够从新链接到锁。 -
可是还有这样的情形:用户可能离开家好几天,而且在这期间,程序已经被彻底退出了。那么用户再次回家时,就不能自动开锁。对于这类 app 来讲,常驻后台操做就显得尤其重要。
-
一、状态保存与恢复
-
由于状态的保存和恢复 Core Bluetooth 都为咱们封装好了,因此咱们只须要选择是否须要这个特性便可。系统会保存当前 central manager 或 peripheral manager,而且继续执行蓝牙相关事件(即便程序已经再也不运行)。一旦事件执行完毕,系统会在后台重启 app,这时你有机会去存储当前状态,而且处理一些事物。在以前提到的 “门锁” 的例子中,系统会监视链接请求,并在
centralManager:didConnectPeripheral:
回调时,重启 app,在用户回家后,链接操做结束。 -
Core Bluetooth 的状态保存与恢复在设备做为 central、peripheral 或者这两种角色时,均可用。在设备做为 central 并添加了状态保存与恢复支持后,若是 app 被强行关闭进程,系统会自动保存 central manager 的状态(若是 app 有多个 central manager,你能够选择哪个须要系统保存)。
-
对于 CBCentralManager,系统会保存如下信息:
- central 准备链接或已经链接的 peripheral
- central 须要扫描的 service(包括扫描时,配置的 options)
- central 订阅的 characteristic
-
对于 peripheral 来讲,状况也差很少。系统对 CBPeripheralManager 的处理方式以下:
- peripheral 在广播的数据
- peripheral 存入的 service 和 characteristic 的树形结构
- 已经被 central 订阅了的 characteristic 的值
-
当系统在后台从新加载程序后(多是由于找到了要找的 peripheral),你能够从新实例化 central manager 或 peripheral 并恢复他们的状态。
-
-
二、添加状态存储和恢复支持
-
状态的存储和恢复功能在 Core Bluetooth 中是可选的,添加支持能够经过如下几个步骤:
- (必须)在初始化 central manager 或 peripheral manager 时,要选择是否须要支持。会在文后的【三、选择支持存储和恢复】中介绍。
- (必须)在系统从后台从新加载程序时,从新初始化 central manager 或 peripheral manager。会在文后的【四、从新初始化 central manager 和 peripheral manager】中介绍。
- (必须)实现恢复状态相关的代理方法。会在文后的【五、实现恢复状态的代理方法】中介绍。
- (可选)更新 central manager 或 peripheral manager 的初始化过程。会在文后的【六、更新 manager 初始化过程】中介绍。
-
-
三、选择支持存储和恢复
-
若是要支持存储和恢复,则须要在初始化 manager 的时候给一个 restoration identifier。restoration identifier 是 string 类型,并标识了 app 中的 central manager 或 peripheral manager。这个 string 很重要,它将会告诉 Core Bluetooth 须要存储状态,毕竟 Core Bluetooth 恢复有 identifier 的对象。
-
例如,在 central 端,要想支持该特性,能够在调用 CBCentralManager 的初始化方法时,配置 CBCentralManagerOptionRestoreIdentifierKey:
myCentralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey:@"myCentralManagerIdentifier"}];
-
虽然以上代码没有展现出来,其实在 peripheral manager 中要设置 identifier 也是这样的。只是在初始化时,将 key 改为了 CBPeripheralManagerOptionRestoreIdentifierKey。
-
由于程序能够有多个 CBCentralManager 和 CBPeripheralManager,因此要确保每一个 identifier 都是惟一的。
-
-
四、从新初始化 central manager 和 peripheral manager
-
当系统从新在后台加载程序时,首先须要作的即根据存储的 identifier,从新初始化 central manager 或 peripheral manager。若是你只有一个 manager,而且 manager 存在于 app 生命周期中,那这个步骤就不须要作什么了。
-
若是 app 中包含多个 manager,或者 manager 不是在整个 app 生命周期中都存在的,那 app 就必需要区分你要从新初始化哪一个 manager 了。你能够经过从 app delegate 中的
application:didFinishLaunchingWithOptions:
中取出 key(UIApplicationLaunchOptionsBluetoothCentralsKey 或 UIApplicationLaunchOptionsBluetoothPeripheralsKey)中的 value(数组类型)来获得程序退出以前存储的 manager identifier 列表:- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSArray *centralManagerIdentifiers = launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey]; return YES; }
-
拿到这个列表后,就能够经过循环来从新初始化全部的 manager 了。
centralManagerIdentifiers[0] = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey:@"myCentralManagerIdentifier"}];
-
-
五、实现恢复状态的代理方法
-
在从新初始化 manager 以后,接下来须要同步 Core Bluetooth 存储的他们的状态。要想弄清楚在程序被退出时都在作些什么,就须要正确的实现代理方法。对于 central manager 来讲,须要实现
centralManager:willRestoreState:
;对于 peripheral manager 来讲,须要实现peripheralManager:willRestoreState:
。 -
注意:若是选择存储和恢复状态,当系统在后台从新加载程序时,首先调用的方法是
centralManager:willRestoreState:
或peripheralManager:willRestoreState:
。若是没有选择存储的恢复状态(或者唤醒时没有什么内容须要恢复),那么首先调用的方法是centralManagerDidUpdateState:
或peripheralManagerDidUpdateState:
。 -
不管是以上哪一种代理方法,最后一个参数都是一个包含程序退出前状态的字典。字典中,可用的 key ,central 端有:
NSString *const CBCentralManagerRestoredStatePeripheralsKey; NSString *const CBCentralManagerRestoredStateScanServicesKey; NSString *const CBCentralManagerRestoredStateScanOptionsKey;
-
peripheral 端有:
NSString *const CBPeripheralManagerRestoredStateServicesKey; NSString *const CBPeripheralManagerRestoredStateAdvertisementDataKey;
-
要恢复 central manager 的状态,能够用
centralManager:willRestoreState:
返回字典中的 key 来获得。假如说 central manager 有想要或者已经链接的 peripheral,那么能够经过 CBCentralManagerRestoredStatePeripheralsKey 对应获得的 peripheral(CBPeripheral 对象)数组来获得。- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary *)state { NSArray *peripherals = state[CBCentralManagerRestoredStatePeripheralsKey]; }
-
具体要对拿到的 peripheral 数组作什么就要根据需求来了。若是这是个 central manager 搜索到的 peripheral 数组,那就能够存储这个数组的引用,而且开始创建链接了(注意给这些 peripheral 设置代理,不然链接后不会走 peripheral 的代理方法)。
-
恢复 peripheral manager 的状态和 central manager 的方式相似,就只是把代理方法换成了
peripheralManager:willRestoreState:
,而且使用对应的 key 便可。
-
-
六、更新 manager 初始化过程
-
在实现了所有的必须步骤后,你可能想要更新 manager 的初始化过程。虽然这是个可选的操做,可是它对确保各类操做能正常进行尤其重要。假如,你的应用在 central 和 peripheral 作数据交互时,被强制退出了。即便 app 最后恢复状态时,找到了这个 peripheral,那你也不知道 central 和这个 peripheral 当时的具体状态。但其实咱们在恢复时,是想恢复到程序被强制退出前的那一步。
-
这个需求,能够在代理方法
centralManagerDidUpdateState:
中,经过发现恢复的 peripheral 是否以前已经成功链接来实现:NSUInteger serviceUUIDIndex = [peripheral.services indexOfObjectPassingTest:^BOOL(CBService *obj, NSUInteger index, BOOL *stop) { return [obj.UUID isEqual:myServiceUUIDString]; }]; if (serviceUUIDIndex == NSNotFound) { [peripheral discoverServices:@[myServiceUUIDString]]; }
-
上面的代码描述了,当系统在完成搜索 service 以后才退出的程序,能够经过调用
discoverServices:
方法来恢复 peripheral 的数据。若是 app 成功搜索到 service,你能够是否能搜索到须要的 characteristic(或者已经订阅过)。经过更新初始化过程,能够确保在正确的时间点,调用正确的方法。
-
七、第三方框架
-
iOS 蓝牙开发中经常使用的第三方框架
若是您以为阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写做动力!欢迎各位转载,可是未经做者本人赞成,转载文章以后必须在文章页面明显位置给出做者和原文链接,不然保留追究法律责任的权利。