iOS - Bluetooth 藍牙


 

iOS - Bluetooth 藍牙

 

1、藍牙介紹

2、iBeacon

  • 具體講解見 Beacon

  • iBeacon 是蘋果公司 2013 年 9 月發布的移動設備用 OS(iOS7)上配備的新功能。其工作方式是,配備有低功耗藍牙(BLE)通信功能的設備使用 BLE 技術向周圍發送自己特有的 ID,接收到該 ID 的應用軟件會根據該 ID 采取一些行動。比如,在店鋪里設置 iBeacon 通信模塊的話,便可讓 iPhone 和 iPad 上運行一資訊告知服務器,或者由服務器向顧客發送折扣券及進店積分。此外,還可以在家電發生故障或停止工作時使用 iBeacon 向應用軟件發送資訊。

  • 蘋果 WWDC 14 之后,對 iBeacon 加大了技術支持和對其用於室內地圖的應用有個更明確的規划。蘋果公司公布了 iBeacon for Developers 和 Maps for Developers 等專題頁面。

3、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 之間的關系一樣。

      Bluetooth14

  • 那怎么發現 peripheral 呢

    • 在 BLE 中,最常見的就是廣播。實際上,peripheral 在不停的發送廣播,希望被 central 找到。廣播的信息中包含它的名字等信息。如果是一個溫度調節器,那么廣播的信息應該還會包含當前溫度什么的。那么 central 的作用則是去 scan,找到需要連接的 peripheral,連接后便可進行通信了。

    • 當 central 成功連上 peripheral 后,它便可以獲取 peripheral 提供的所有 service 和 characteristic。通過對 characteristic 的數據進行讀寫,便可以實現 central 和 peripheral 的通信。

  • CoreBluetooth 框架的核心其實是兩個東西,central 和 peripheral, 對應他們分別有一組相關的 API 和類。

    • 這兩組 API 分別對應不同的業務場景,如下圖,左側叫做中心模式,就是以你的手機(App)作為中心,連接其他的外設的場景。而右側稱為外設模式,使用手機作為外設連接其他中心設備操作的場景。

      Bluetooth13

  • iOS 設備(App)作為 central 時:

    • 當 central 和 peripheral 通信時,絕大部分操作都在 central 這邊。此時,central 被描述為 CBCentralManager,這個類提供了掃描、尋找、連接 peripheral(被描述為 CBPeripheral)的方法。

    • 下圖標示了 central 和 peripheral 在 Core Bluetooth 中的表示方式:

      Bluetooth16

    • 當你操作 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)流程:

    • 1、建立中心角色
    • 2、掃描外設(discover)
    • 3、連接外設(connect)
    • 4、掃描外設中的服務和特征(discover)
      • 4.1 獲取外設的 services
      • 4.2 獲取外設的 Characteristics,獲取 Characteristics 的值,獲取 Characteristics 的 Descriptor 和 Descriptor 的值
    • 5、與外設做數據交互(explore and interact)
    • 6、訂閱 Characteristic 的通知
    • 7、斷開連接(disconnect)
  • 外設模式(CBPeripheralManager)流程:

    • 1、啟動一個 Peripheral 管理對象
    • 2、設置本地 Peripheral 服務、特性、描述、權限等等
    • 3、設置 Peripheral 發送廣播
    • 4、設置處理訂閱、取消訂閱、讀 characteristic、寫 characteristic 的委托方法

3.3 服務、特征和特征的屬性

  • 一個 peripheral 包含一個或多個 service,或提供關於信號強度的信息。service 是數據和相關行為的集合。例如,一個心率監測儀的數據就可能是心率數據。

  • service 本身又是由 characteristic 或者其他 service 組成的。characteristic 又提供了更為詳細的 service 信息。還是以心率監測儀為例,service 可能會包含兩個 characteristic,一個描述當前心率帶的位置,一個描述當前心率的數據。

    Bluetooth15

  • 每個 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 };
  • 外設、服務、特征間的關系

    Bluetooth1

    • 一個 CBPeripheral(藍牙設備) 有一個或者多個 CBService(服務),而每一個 CBService 有一個或者多個 CBCharacteristic(特征),通過可寫的 CBCharacteristic 發送數據,而每一個 CBCharacteristic 有一個或者多個 Description 用於描述 characteristic 的信息或屬性。

3.4 設備狀態

  • 藍牙設備狀態:

    • 1、待機狀態(standby):設備沒有傳輸和發送數據,並且沒有連接到任何設備。
    • 2、廣播狀態(Advertiser):周期性廣播狀態。
    • 3、掃描狀態(Scanner):主動尋找正在廣播的設備。
    • 4、發起鏈接狀態(Initiator):主動向掃描設備發起連接。
    • 5、主設備(Master):作為主設備連接到其他設備。
    • 6、從設備(Slave):作為從設備連接到其他設備。
  • 五種工作狀態:

    • 准備(standby)
    • 廣播(advertising)
    • 監聽掃描(Scanning)
    • 發起連接(Initiating)
    • 已連接(Connected)

3.5 藍牙和版本的使用限制

  • 藍牙 2.0:越獄設備

  • 藍牙 4.0:iOS 6 以上

  • MFi 認證設備:無限制

3.6 設置系統使用藍牙權限

  • 設置系統使用藍牙權限

    Bluetooth2

4、中心模式的使用

  • 中心模式的應用場景:主設備(手機去掃描連接外設,發現外設服務和屬性,操作服務和屬性的應用。一般來說,外設(藍牙設備,比如智能手環之類的東西)會由硬件工程師開發好,並定義好設備提供的服務,每個服務對於的特征,每個特征的屬性(只讀,只寫,通知等等)。

  • 藍牙程序需要使用真機調試。

4.1 App 連接外設的實現

  • 1、建立中心角色

    • 導入 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); }
  • 2、掃描外設(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}]; }
  • 3、連接外設(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]; } }
  • 4、掃描外設中的服務和特征(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(@"該字段不可寫!"); } }
  • 6、訂閱 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]; }
  • 7、斷開連接(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 要取決於你的需求,下圖展示了當你嘗試重連時可以選擇的流程:

    Bluetooth18

  • 三列代表着三種重連的方式。當然這也是你可以選擇進行實現的,這三種方式也並不是都需要去實現,依然取決於你的需求。

  • 1、嘗試連接已知的 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: 方法來進行重新搜索。
    • 更多關於隨機地址的資料可以看 《蘋果產品的藍牙附件設計指南》

  • 2、連接系統已經連接過的 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,那在每次發送成功的回調中,再進行發送就好,直到發送完成。

  • 下面來討論下是怎么做的吧。

  • 1、區別普通寫入與文件寫入

    • 分割數據並發送,每次都要記錄上一次已經寫入長度(偏移量 self.otaSubDataOffset),然后截取 20 個長度。需要注意的是最后一次的長度,注意不要越界了。

    • 數據的發送和普通寫入沒什么區別。

  • 2、當前已發送長度與發送結束的回調

    • 因為 OTA 的寫入可能需要做進度條之類的,所以最好和普通的寫入回調區分開。

    • 在每次寫入成功中,判斷是否已經發送完成(已發送的長度和總長度相比)。如果還未發送完成,則返回已發送的長度給控制器(可以通過代理實現)。如果已發送完成,則返回發送完成(可以通過代理實現)。

5、外設模式的使用

5.1 App 作為外設被連接的實現

  • 1、啟動一個 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; } }
  • 2、配置本地 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); }
  • 3、開啟廣播 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(@"已經開始發送廣播"); }
  • 4、設置處理訂閱、取消訂閱、讀 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 數據庫是一個樹形結構。

    Bluetooth24

  • 所以在創建 peripheral 的時候,也要像這種樹形結構一樣,將 service 和 characteristic 裝進去。在此之前,我們需要做的是學會如何標識 service 和 characteristic。

  • 1、使用 UUID 來標識 service 和 characteristic

    • service 和 characteristic 都通過 128 位的 UUID 來進行標識,Core Bluetooth 將 UUID 封裝為了 CBUUID 。關於詳細 UUID 的介紹,請參考上面的 4.3.1 CBUUID 講解。
  • 2、為自定義的 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"];
  • 3、構建 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 有可能會收到讀寫請求。此時,你應該根據請求作出相應的響應,接下來便會提到這方面的處理。

  • 1、讀取請求

    • 當收到讀請求時,會回調 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: 方法,並返回失敗原因。官方提供了一個失敗原因枚舉,可能有你需要的。

  • 2、寫入請求

    • 寫入請求和讀取請求一樣簡單。當 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 的一個重要操作,接下來會講到廣播的正確姿勢。

  • 1、注意廣播對數據大小的限制

    • 正如前文提到過的那樣,廣播是通過調用 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,請正確構建廣播的數據。

  • 2、只廣播必要的數據

    • 當 peripheral 想要被發現時,它會向外界發送廣播,此時會用到設備的無線電(當然還有電池)。一旦連接成功,central 便能直接從 peripheral 中讀取數據了,那么此時廣播的數據將不再有用。所以,為了減少無線電的使用、提高手機性能、保護設備電池,應該在被連接后,及時關閉廣播。停止廣播調用 CBPeripheralManager 的 stopAdvertising 方法即可。
        [myPeripheralManager stopAdvertising];
  • 3、手動開啟廣播

    • 其實什么時候應該廣播,多數情況下,用戶比我們更清楚。比如,他們知道周圍沒有開着的 BLE 設備,那他就不會把 peripheral 的廣播打開。所以提供給用戶一個手動開啟廣播的 UI 更為合適。

5.3.2 配置 characteristic

  • 在創建 characteristic 的時候,就為它設定了相應的 properties、value 和 promissions。這些屬性決定了 central 如何和 characteristic 通信。properties 和 promissions 可能需要根據 app 的需求來設置,下來就來談談如何配置 characteristic:

  • 1、讓 characteristic 支持通知

    • 之前在 central 的時候提到過,如果要讀取經常變化的 characteristic 的數據,更推薦使用訂閱。所以,如果可以,最好 characteristic 允許訂閱。

    • 如果像下面這樣初始化 characteristic 就是允許讀和訂閱:

          myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable];
  • 2、限制只能配對的 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,並允許讀寫數據。

6、后台運行藍牙服務

  • 對於 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),此時連接會被斷開,但是要直到程序重新喚醒時,你才知道被斷開了。

  • 1、利用連接 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 中打開查看。

  • 1、作為 Central 的后台模式

    • 如果在 info.plist 中配置了 UIBackgroundModes – bluetooth-central,那么系統則允許程序在后台處理藍牙相關事件。在程序進入后台后,依然能掃描、搜索 peripheral,並且還能進行數據交互。當 CBCentralManagerDelegate 和 CBPeripheralDelegate 的代理方法被調用時,系統將會喚醒程序。此時允許你去處理重要的事件,比如:連接的建立或斷開,peripheral 發送了數據,central manager 的狀態改變。

    • 雖然此時程序能在后台運行,但是對 peripheral 的掃描和在前台時是不一樣的。實際情況是這樣的:

      • 設置的 CBCentralManagerScanOptionAllowDuplicatesKey 將失效,並將發現的多個 peripheral 廣播的事件合並為一個。
      • 如果全部的 app 都在后台搜索 peripheral,那么每次搜索的時間間隔會更大。這會導致搜索到 peripheral 的時間變長。

      • 這些相應的調整會減少無線電使用,並提升續航能力。

  • 2、作為 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 來說,常駐后台操作就顯得尤為重要。

  • 1、狀態保存與恢復

    • 因為狀態的保存和恢復 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 並恢復他們的狀態。

  • 2、添加狀態存儲和恢復支持

    • 狀態的存儲和恢復功能在 Core Bluetooth 中是可選的,添加支持可以通過以下幾個步驟:

      • (必須)在初始化 central manager 或 peripheral manager 時,要選擇是否需要支持。會在文后的【3、選擇支持存儲和恢復】中介紹。
      • (必須)在系統從后台重新加載程序時,重新初始化 central manager 或 peripheral manager。會在文后的【4、重新初始化 central manager 和 peripheral manager】中介紹。
      • (必須)實現恢復狀態相關的代理方法。會在文后的【5、實現恢復狀態的代理方法】中介紹。
      • (可選)更新 central manager 或 peripheral manager 的初始化過程。會在文后的【6、更新 manager 初始化過程】中介紹。
  • 3、選擇支持存儲和恢復

    • 如果要支持存儲和恢復,則需要在初始化 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 都是唯一的。

  • 4、重新初始化 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"}];
  • 5、實現恢復狀態的代理方法

    • 在重新初始化 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 即可。

  • 6、更新 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(或者已經訂閱過)。通過更新初始化過程,可以確保在正確的時間點,調用正確的方法。

7、第三方框架



如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的“推薦”將是我最大的寫作動力!歡迎各位轉載,但是未經作者本人同意,轉載文章之后必須在文章頁面明顯位置給出作者和原文連接,否則保留追究法律責任的權利。
Qian Chia
 
分類:  iOS-Bluetooth, iOS-APP
標簽:  BluetoothiOS 10


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM