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 藍牙開發中經常使用的第三方框架
若是您以爲閱讀本文對您有幫助,請點一下「推薦」按鈕,您的「推薦」將是我最大的寫做動力!歡迎各位轉載,可是未經做者本人贊成,轉載文章以後必須在文章頁面明顯位置給出做者和原文鏈接,不然保留追究法律責任的權利。