iOS BLE 開發小記[3] 如何實現一個 Local Peripheral

muhlenXi發表於2019-04-22

歡迎訪問我的部落格 muhlenXi,該文章出自我的部落格, 歡迎轉載,轉載請註明來源: muhlenxi.com/2017/05/01/…

導語:

在這一節,你將會學到,如何通過 CoreBluetooth 框架來實現 Local Peripheral 方面的功能和代理方法。

在 iOS BLE 開發小記[2]中,你已經學到了如何在 Central 方面去呼叫 BLE 的常用方法。在這一節中,你將學習用 CoreBluetooth 框架來呼叫 Peripheral 方面 BLE 的常用方法。通過本文的示例程式碼,將會引導你開發一個將你的 Local 裝置實現為 Local Peripheral。你將會從中學到:

  • 如何建立一個 Peripheral Manager 物件
  • 如何為你的 Local Peripheral 設定 Services 和 Characteristics
  • 如何釋出你的 Services 和 Characteristics 資料
  • 如何廣播你的裝置
  • 如何對連線的 Central 做讀寫請求響應
  • 如何傳送更新後的值給訂閱的 Central

或許你發現示例程式碼過於簡單和抽象,你需要在你的 App 中做些恰當的練習來掌握這些內容。更高階的技巧和最佳實踐在後續的文章中將會講解。

Peripheral 實現詳情

建立一個 Peripheral Manager 物件

在 Local Device(當前裝置)實現 Peripheral 規範的第一步是分配(allocate)和初始化(initialize)一個周邊管理(Peripheral Manager),(用 CBPeripheralManager 物件表示),通過呼叫 CBPeripheralManager 的 initWithDelegate:queue:options: 方法來建立管理物件,如下所示

myPeripheralManager =
        [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
複製程式碼

在示例程式碼中,設定 Delegate 為 self 是為了接收 Peripheral 的事件響應,將引數 dispatch queue 置為 nil。意味著 Peripheral Manager 將會在主佇列中分發事件。

當你建立一個 Peripheral Manger 物件時,Peripheral Manager 會通過 peripheralManagerDidUpdateState: 方法來代理回撥,你必須實現這個代理方法來確保當前裝置是否支援 BLE 技術,關於代理方法的詳情可以查閱 CBPeripheralManagerDelegate Protocol Reference.

設定你的 Services 和 Characteristics

在第一節中,我們瞭解到,一個 Local Peripheral 採用樹形結構來組織 Services 和 Characteristics 的資料。因此必須採用樹形結構方式來設定 Local Peripheral 的 Services 和 Characteristics。你第一步要做的是搞清和理解 Service 和 Characteristic 是如何標識的。

通過 UUID 標識 Services 和 Characteristics

Peripheral 的 Service 和 Characteristic 是通過 128 位的特定藍芽 UUID(通用唯一識別碼)來標識的,在 CoreBluetooth 中是用 CBUUID 物件來表示的。並不是所有的 UUID 都是通過 Bluetooth Special Interest Group (藍芽特別興趣小組)預定義的。為了方便起見,Bluetooth SIG 定義和釋出了許多通用的 16位 UUID。舉個例子,Bluetooth SIG 事先定義了一個16位的 UUID 用來標識一個心率 Service,該 UUID 是 128位 UUID 0000180D-0000-1000-8000-00805F9B34FB 進行縮減而來的,這是基於藍芽 4.0 規範中,第 3 卷 F 部分第 3.2.1 節定義的藍芽基礎 UUID。

CBUUID 提供了一個處理比較長的 UUID 的工廠方法,舉個例子,生成一個表示心率 Service 的 UUID,可以呼叫 UUIDWithString 方法來通過預定義的 16位 UUID來建立 CBUUID 物件。

CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString: @"180D"];
複製程式碼

當你通過預定義的 16位 UUID 來建立 CBUUID 物件時,CoreBluetooth 會基於128位Bluetooth Base UUID 填充剩下的的 UUID 位。

為你定製的 Services 和 Characteristics 生成 UUID

你的 Service 和 Characteristic 的UUID也許可能沒有被 Bluetooth UUIDs 預定義,如果沒有被預定義,你需要手動生成你自己的 128位 UUID 來表示 Service 和 Characteristic。

通過命令列命令 uuidgen 可以生成 128位的 UUID,開啟你的 Terminal(終端),通過這種方式依次為你的 Services 和 Characteristics 生成一個 UUID (用連字元連線起來的字串)來標識。舉例如下:

$ uuidgen
71DA3FD1-7E10-41C1-B16F-4430B506CDE7
複製程式碼

你可以用上面方法生成的 UUID 呼叫 UUIDWithString 方法來建立一個 CBUUID 物件。

 CBUUID *myCustomServiceUUID =
        [CBUUID UUIDWithString:@"71DA3FD1-7E10-41C1-B16F-4430B506CDE7"];
複製程式碼
構建你的 服務特徵樹

當你為每個 Service 和 Characteristic 建立 CBUUID 物件後,你可以建立 mutable Service(可變服務) 和 mutable Characteristic(可變特徵),然後以樹形的方式組織它們。舉個例子,如果你現在有一個 Characteristic 的 UUID,你可以通過呼叫 CBMutableCharacteristic 類的 initWithType:properties:value:permissions: 方法生成一個 mutable Characteristic。

myCharacteristic =
        [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID
         properties:CBCharacteristicPropertyRead
         value:myValue permissions:CBAttributePermissionsReadable];
複製程式碼

當你建立 mutable Characteristic 的時候,你可以指定它的 properties(屬性)、value(值)和 permissions(許可權許可),你指定的 properties 和 permissions 決定這個 Characteristic 的值是否可以讀或者寫,或者連線的 Central 能否訂閱該 Characteristic 的值。下面的示例中,Characteristic 的值是被指定為可讀的。關於 mutable Characteristic 的 properties 和 permissions 詳情可以查閱 CBMutableCharacteristic Class Reference.

提示:如果你指定了 Characteristic 的值,那麼該值將被快取並且該 Characteristic 的 properties 和 permissions 將被設定為可讀的。因此,如果你需要 Characteristic 的值是可寫的,或者你希望在 Service 釋出後,Characteristic 的值在 lifetime(生命週期)中依然可以更改,你必須將該 Characteristic 的值指定為 nil。通過這種方式可以確保 Characteristic 的值,在 Peripheral Manager 收到來自連線的 Central 的讀或者寫請求的時候,能夠被動態處理。

既然你建立了一個 mutable Characteristic,你也能通過呼叫 CBMutableService 類的 initWithType:primary: 方法建立一個 mutable Service。如下所示:

myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];
複製程式碼

在示例程式碼中,第二個引數被指定為 YES,用來表明該 Service 是 Primary(主要的),而不是 secondary(次要的)。一個 Primary Service 用來描述這個裝置的主要功能,還可以用來引用其他的 Service。一個 Secondary Service 用來描述的是上下文中相關的或者被引用的 Service。舉個例子,從心率感測器中獲取心率的服務是 primary Service,而獲取感測器電量的服務就可以被視為 secondary Service 。

當你建立完 Service 後。你需要設定 Service 的 Characteristic 陣列屬性,如下:

myService.characteristics = @[myCharacteristic];
複製程式碼

傳送你的 Services 和 Characteristics

當你構建好服務特徵樹後,下一步就是按照 BLE 的規範釋出到裝置的服務特徵庫中,用 CoreBluetooth 可以很輕鬆的完成這一步,只需要呼叫 CBPeripheralManager類 的 addService: 方法就可以了。 示例程式碼如下:

[myPeripheralManager addService:myService];
複製程式碼

當你呼叫該方法釋出服務時,Peripheral Manager 會呼叫 peripheralManager:didAddService:error: 方法進行代理回撥,實現這個代理方法可以獲取到產生的錯誤,示例程式碼如下:

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

提示:當你釋出 Service 和相關的 Characteristic 到 Peripheral 的資料庫中後,裝置已經將資料快取,你不能再改變它了。

廣播你的 Service

當你釋出你的 Service 和 Characteristic 到裝置的服務特徵庫時,你可以廣播一些服務給正在監聽的 Central,你可以通過呼叫 CBPeripheralManager 類的 startAdvertising: 方法來開始廣播,傳入的字典是要廣播的資料。

[myPeripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey :
        @[myFirstService.UUID, mySecondService.UUID] }];
複製程式碼

在示例程式碼中,傳入的字典中唯一的 key 是 CBAdvertisementDataServiceUUIDsKey,用一個包含 CBUUID 物件的陣列來表示你想要廣播的服務的 UUID。你在字典中可以指定的其他 key 在 Advertisement Data Retrieval Keys 中有詳細說明。也就是說,僅有 CBAdvertisementDataLocalNameKeyCBAdvertisementDataServiceUUIDsKey 這兩個 key 支援 Peripheral Manager 物件。

當你在本地裝置中廣播一些資料時,Peripheral Manager 會通過 peripheralManagerDidStartAdvertising:error: 方法來代理回撥。如果你的裝置不能廣播而發生錯誤時,實現這個代理方法可以獲取產生的錯誤:

- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
                                       error:(NSError *)error {
 
    if (error) {
        NSLog(@"Error advertising: %@", [error localizedDescription]);
    }
    // ...
}
複製程式碼

提示:廣播資料方法會被盡力執行,因為空間是有限的和多個 APP 可能同時需要廣播資料,更多詳情可以查閱關於 startAdvertising: 方法的討論。

當你的 APP 在後臺執行時也會影響廣播的行為,這一內容將會在下一篇中進行討論。

響應 Central 的讀寫請求

當你連線一個或多個 Central 後,你可能會收到讀或者寫的請求,對這些請求作出響應需要採取恰當的方式,下面的示例程式碼將會描述如何處理這些請求。

當一個連線的 Central 傳送讀取某個 Characteristic 資料的請求時,Peripheral Manager 會呼叫 peripheralManager:didReceiveReadRequest: 方法進行代理回撥。代理方法以 CBATTRequest 物件的方式來傳遞請求,它包含一些請求的屬性。

比如,當你收到一個讀取 Characteristic 值的簡單請求時,可以通過代理方法回撥的 CBATTRequest 對像來判斷 Central 指定要讀取的 Characteristic 是否和裝置服務庫中的 Characteristic 是否相匹配。你可以開始實現這個代理方法,示例程式碼如下:

- (void)peripheralManager:(CBPeripheralManager *)peripheral
    didReceiveReadRequest:(CBATTRequest *)request {
 
    if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
        // ...
    }
}
複製程式碼

如果 Characteristic 的 UUID 能夠匹配,下一步就是確保讀取請求的位置沒有超出 Characteristic 的值的邊界。如下面程式碼所示,你可以通過使用 CBATTRequest 物件的 offset 屬性來確保讀取請求沒有嘗試讀取範圍之外的資料。

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

假如讀取請求的 offset(偏移)已經確認,現在就可以設定請求的 Characteristic 的屬性(預設值為 nil)為你裝置中的 Characteristic 的值了,你應該重視讀取請求的偏移:

request.value = [myCharacteristic.value
        subdataWithRange:NSMakeRange(request.offset,
        myCharacteristic.value.length - request.offset)];
複製程式碼

設定完值後,通過呼叫 respondToRequest:withResult: 方法並傳入 request(更新值後的)和 請求的結果引數來對 Remote Central 的請求作出響應表示請求已經被成功處理。示例程式碼如下:

[myPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
// ...
複製程式碼

只要代理方法 peripheralManager:didReceiveReadRequest: 方法被回撥,就需要準確的呼叫 respondToRequest:withResult: 方法。

提示:如果 Characteristic 的 UUID 不匹配,或者因為某種原因不能完全讀取,不必去填充請求,直接呼叫 respondToRequest:withResult: 方法並提供一個表示失敗的結果即可。你可能指定的結果列表見 CBATTError Constants 常量列舉。

處理連線的 Central 寫入請求也比較易懂。當 Central 傳送一個寫入請求給一個或多個你的 Characteristic 時,Peripheral Manager 會通過 peripheralManager:didReceiveWriteRequests: 方法來代理回撥。這是,代理方法會傳遞一個包含一個或多個 CBATTRequest 物件的陣列給你,陣列中的每個物件都代表一個寫入請求。當你確定寫入請求能夠處理時,你可以設定 Characteristic 的值,示例程式碼如下:

myCharacteristic.value = request.value;
複製程式碼

雖然上述例子沒有證明這一點,但當你給 Characteristic 寫資料的時候,你應該確保請求的 offset 屬性的範圍有效。

就像你響應讀取請求一樣,只要代理方法 peripheralManager:didReceiveWriteRequest: 方法被回撥,就需要準確無誤的呼叫 respondToRequest:withResult: 方法。也就是說,respondToRequest:withResult: 方法期望有一個 CBATTRequest 物件,即使你可能通過 peripheralManager:didReceiveWriteRequests: 代理方法接收到一個包含 CBATTRequest 物件的陣列,你也應該傳入陣列中的第一個物件,示例程式碼如下:

[myPeripheralManager respondToRequest:[requests objectAtIndex:0]
        withResult:CBATTErrorSuccess];
複製程式碼

提示:將多請求視為單一請求來對待,如果個別的請求不能被填充,你就不必填充其餘的請求了,直接呼叫 respondToRequest:withResult: 方法並提供一個表示失敗的結果即可。

傳送更新 Characteristic 的通知給訂閱的 Central

連線的 Central 經常會訂閱一個或多個 Characteristic 的值,當這些值發生變化時,你應該傳送通知給訂閱的 Central 。

當一個連線的 Central 訂閱一個或多個你的 Characteristic 值時,Peripheral Manager 會通過 peripheralManager:central:didSubscribeToCharacteristic: 方法來代理回撥。示例程式碼如下:

- (void)peripheralManager:(CBPeripheralManager *)peripheral
                  central:(CBCentral *)central
didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
 
    NSLog(@"Central subscribed to characteristic %@", characteristic);
    // ...
}

複製程式碼

將上述的代理方法作為一個線索來開始給 Central 傳送更新後的值。

接著,獲取更新後的 Characteristic 的值,通過呼叫 CBPeripheralManager類的 updateValue:forCharacteristic:onSubscribedCentrals: 方法來給 Central 傳送通知。示例程式碼如下:

NSData *updatedValue = // fetch the characteristic's new value
BOOL didSendValue = [myPeripheralManager updateValue:updatedValue
    forCharacteristic:characteristic onSubscribedCentrals:nil];
複製程式碼

當你呼叫這個方法給訂閱的 Central 傳送通知時,你可以通過最後的那個引數來指定要傳送的 Central,示例程式碼中的引數為 nil,表明將會傳送通知給所有連線且訂閱的 Central,沒有訂閱的 Central 則會被忽略。

updateValue:forCharacteristic:onSubscribedCentrals: 方法會返回一個 Boolean 型別的值來表示通知是否成功的傳送給訂閱的 Central 了,如果 base queue (基礎佇列)滿載,該方法會返回 NO,當傳輸佇列存在更多空間時,Peripheral Manager 則會呼叫 peripheralManagerIsReadyToUpdateSubscribers: 代理方法進行回撥。你可以實現這個代理方法,在方法中再次呼叫 updateValue:forCharacteristic:onSubscribedCentrals: 方法傳送通知給訂閱的 Central。

提示:用通知傳送單個資料包給訂閱的 Central,就是說,一旦訂閱的 Central 發行更新時,你就應該呼叫 updateValue:forCharacteristic:onSubscribedCentrals: 方法用單一通知傳送全部的更新值。

並不是所有的資料都是通過通知來傳輸的,這主要取決於你的 Characteristic 的值的大小,只有當 Central 呼叫 CBPeripheral類的 readValueForCharacteristic: 方法時,你可以檢索全部的值。

參考文獻

1、Performing Common Peripheral Role Tasks

結束語

歡迎在本文下面留言一起交流心得...

相關文章