在前一章節中你掌握了執行常見的藍牙低功耗核心端的任務.在本章中,你要學習執行常見的藍牙低功耗外設端的任務.基於代碼的例子會幫助你在本地設備中扮演外設端.特別的,你將掌握如何:
- 啓動一個peripheral manager對象
- 在本地外設中建立services 和characteristics
- 將services和characteristics發送到設備的本地數據庫
- 廣播services
- 響應與自身建立連接的central(核心端)的讀寫請求
- 向訂閱自己的central(核心端)更新特徵值
本章中的樣例簡單並抽象,你可能需要在實際開發中做出相應更改.更多外設端的進階主題-包括小貼士/技巧/最佳實踐在隨後的章節:Core Bluetooth Background Processing for iOS Apps
和 Best Practices for Setting Up Your Local Device as a Peripheral
.
啓動一個Peripheral Manager
第一步就是分配內存並初始化一個peripheral manager實例(CBPeripheralManager對象)調用CBPeripheralManager類的 initWithDelegate:queue:options
方法:
myPeripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
在本例中,self設置成代理對象來接收外設端的事件.當你指定隊列爲nil時,peripheral manager使用主隊列來分發事件.
當創建一個peripheral manager, peripheral 調用peripheralManagerDidUpdateState
代理方法.爲了確保本地外設支持藍牙低功耗並可用,你必須實現該代理.關於實現該代理方法的更多信息,請點擊CBPeripheralManagerDelegate Protocol Reference
建立Services和Characteristics
如上圖所示,一個本地外設的數據庫(存儲services和characteristics)是一個樹狀結構.在本地的外設上建立services和characteristics時必須將他們構造成這個樹狀結構.第一步就是理解services和characteristics是如何標識的.
Services和Characteristics通過UUIDs區分
外設的services和characteristics通過128位特定的藍牙UUID區分,在Core BlueTooth框架中用作CBUUID對象.儘管SIG並未預定義用來區分service和characteristic的UUID,SIG定義併發布了一些常見的便於使用的16位的UUID.舉個栗子,SIG將心率設備的標識符定義爲16位的180D
.這個UUID是由和它等價的128位UUID縮短而成,0000180D-0000-1000-8000-00805F9B34FB
,基於基本的UUID定義爲藍牙4.0規範,頻道3,part F, Section3.2.1
CBUUID類提供了可讓你在開發中快速處理長UUID的工廠方法.例如,你可以簡單的使用UUIDWithString
來從系統預定義的16位UUID創建CBUUID對象,從而取代128位心率service的UUID:
CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString: @"180D"];
爲自定義的Services和Characteristics創建自己的UUIDs
你可能會有一些沒有預定義UUID的services和characteristic.如果這麼做,你需要爲它們生成自己的128位UUID.
使用命令行工具uuidgen
可以快速的生成128位UUID.首先,開啓一個終端窗口,下一步爲每一個需要標識的service和characteristic生成爲一個128位使用-連接的ASCII值:
$ uuidgen
71DA3FD1-7E10-41C1-B16F-4430B506CDE7
然後你可以使用這個UUID通過UUIDWithString
創建一個CBUUID對象:
CBUUID *myCustomServiceUUID = [CBUUID UUIDWithString:@"71DA3FD1-7E10-41C1-B16F-4430B506CDE7"];
創建Services和characteristics的樹形結構
在你擁有了services和characteristic的UUIDs之後,你可以創建可變的service和characteristics並且將它們構成樹形.舉例,如果你有一個characteristic的UUID,你可以通過initWithType:properties:value:permissions
創建可變的characteristic:
myCharacteristic =
[[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID
properties:CBCharacteristicPropertyRead
value:myValue permissions:CBAttributePermissionsReadable];
當創建了一個可變的characteristic後設置他的屬性
,值
和授權
.除了其他事項外,屬性
和授權
決定了characteristic value(特徵值)是可讀的還是可寫的,以及與本設備建立連接的central(核心)設備是否可以訂閱該characteristic.在本例中,特徵值被設置成可以被連接的核心設備讀取.更多關於可變characteristic相關的可用屬性和授權請戳CNMutableCharacteristic Class Reference
Note:如果你爲characteristic指定了一個值,這個值被緩存下來並且它的屬性和授權被設置爲了可讀.如果你需要將characteristic設置爲可寫的,或者在發佈service的生命週期中改變characteristic的值,你必須指定值爲nil.如此確保在外設收到核心設備的讀寫請求時,值是動態變化的
現在你創建了一個可變的characteristic,你可以創建可變的service來聯結characteristic,調用CBMutableService
類的 initWithType:primary:
myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];
在本例中,第二個參數設置成了Yes,意味着service是主要的.一個主service描述一個設備的主要功能並可以被其他service引用.次service描述了一個只在引用它的其他service的上下文中相關(翻譯的好像不太對.A secondary service describes a service that is relevant only in the context of another service that has referenced it).舉個栗子,心率檢測器的主service可能從心率傳感器的傳輸心率數據,而次service可能傳輸傳感器的電量數據.
在創建了一個service後,你可以設置service的characteristic數組來聯結它們:
myService.characteristics = @[myCharacteristic];
發佈你的Service和Characteristic
在你創建好樹形的services和characteristics後,下一步是將他們發佈到本地設備的services和characteristics數據庫中.使用CB可以簡單地實現,你可以調用CBPeripheralManager
類的addService:
[myPeripheralManager addService:myService];
當你調用這個方法發佈services時,peripheral manager調用peripheralManager:didAddService:error:
代理函數.如果錯誤發生了並且services不能被髮出,實現該代理來捕獲錯誤信息:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didAddService:(CBService *)service
error:(NSError *)error {
if (error) {
NSLog(@"Error publishing service: %@", [error localizedDescription]);
}
...
Note:在你向數據庫發佈一個service以及相關的characteristic後,這個service被緩存下來並且你不能再改變它
廣播Service
當你將services和characteristics發佈到設備的service和characteristic數據庫中後,你已經準備將他們廣播給一些正在監聽的central設備.如下例所示,你可以調用CBPeripheralManager
類中的 startAdvertising:
,傳入一個NSDictionary類型的廣播數據.
[myPeripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey : @[myFirstService.UUID, mySecondService.UUID] }];
在本例中,NSDictionary的唯一鍵CBAdvertisementDataServiceUUIDsKey
,對應需要一個存放CBUUID對象的數組(代表着你想廣播的service的UUIDs).其他你可能用到的NSDictionary中的鍵在這裏.也就是說,只有兩個鍵是支持peripheral manager對象的:CBAdvertisementDataLocalNameKey
和 CBAdvertisementDataServiceUUIDsKey.
當你開始在本地外設中廣播數據時,peripheral manager調用peripheralManagerDidStartAdvertising:error:
代理方法.如果發生錯誤可以在代理中捕獲錯誤信息:
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error {
if (error) {
NSLog(@"Error advertising: %@", [error localizedDescription]);
}
...
Note:數據廣播是”努力”的,因爲容量有限並且同時會有多個app廣播,更多相關信息請查看startAdvertiseing:
,廣播的行爲也會因程序在後臺受到影響,在下一章中將討論該問題,Core Bluetooth Background Processing for iOS Apps
一旦你開始廣播數據,遠程的核心設備可以發現並且與你建立連接.
響應核心設備發來的讀寫請求
在你連接了一個或多個遠程核心設備後,你可能開始接收他們的讀寫請求.當你這麼做時,確保用適當的方式來響應這些請求.下例描述瞭如何處理這些請求.
當一個連接的核心設備請求讀取一個你的characteristic, Peripheral manager調用peripheralManager:didReceiveReadRequest:
代理方法.這個代理方法會以CBATTRequest對象向你傳遞核心端傳來的請求(擁有許多你可以用來實現請求的屬性)
舉例,當你接收到一個簡單的要讀取characteristic的請求,從代理中獲取的CBATTRequest
對象可以用來確定核心端指定要讀的characteristic和你本地設備數據庫中的characteristic相匹配.你可以實現這個代理:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveReadRequest:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
...
如果characteristic的UUID匹配成功,下一步要確保這個讀取請求要讀取的index位置不在你的characteristic值的外界.如下例所示,你可一個使用CBATTRequest
的offset
屬性來保證讀取請求不是在讀取屬性外界的數據.
if (request.offset > myCharacteristic.value.length) {
[myPeripheralManager respondToRequest:request
withResult:CBATTErrorInvalidOffset];
return;
}
假如請求的偏移量成功驗證,用本地外設端創建的characteristic值覆蓋請求中的characteristic屬性值,並且要顧及到讀取請求的偏移量:
request.value = [myCharacteristic.value
subdataWithRange:NSMakeRange(request.offset,
myCharacteristic.value.length - request.offset)];
在設置完值之後,需要向遠程核心設備聲明請求已成功處理.需調用CBPeripheralManager
的 respondToRequest:withResult:
方法,將你更新的request和請求處理的結果一併返回:
[myPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
在每一次peripheralManager:didReceiveReadRequest:
被調用後你都需要調用一次respondToRequest:withResult:
Note:如果characteristic的UUID不匹配,或因其他原因不能完成讀取請求,你無須嘗試完成請求.相反的你應該立刻調用respondToRequest:withResult:
並且提供一個表明失敗原因的result.對於你可能需要指定的結果,查看CBATTError Constants
處理核心設備發送的寫入請求也很直接.當連接的核心設備向你發送一個或多個characteristic讀寫請求時,peripheral manager調用peripheralManager:didReceiveWriteRequests:
代理.此時代理方法會以一個包含多個CBRequest
對象的數組向你傳輸請求,其中每一個代表一個寫入請求.在你確認完成了一個寫入請求後,你可以寫characteristic的值:
myCharacteristic.value = request.value;
儘管上述例子沒有演示,你還要確保在寫入characteristic時考慮請求的偏移量屬性.
正如迴應讀取請求一樣,在每一次peripheralManager:didReceiveWriteRequest:
被調用後你都需要調用一次respondToRequest:withResult:
,也就是說第一個參數需要一個CBATTRequest
對象,即使你從peripheralManager:didReceiveWriteRequests:
收到了多於一個Request的數組.你應該傳入數組的第一個請求:
[myPeripheralManager respondToRequest:[requests objectAtIndex:0]
withResult:CBATTErrorSuccess];
將多個請求作爲單個請求來處理,如果任一請求不能完成,你不應該完成其中的任何一個.相反,立刻調用respondToRequest:withResult:
報錯
向訂閱的核心設備發送更新的characteristic
通常情況下,連接的核心設備會訂閱你的一個或多個characteristic值,如上一節的訂閱characteristic值
.當訂閱characteristic值發生改變,你有責任發出通知.下例將指導你如何去做.
當一個連接的核心設備訂閱了你的若干characteristic.peripheral manager 調用peripheralManager:central:didSubscribeToCharacteristic:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
central:(CBCentral *)central
didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
NSLog(@"Central subscribed to characteristic %@", characteristic);
...
使用上面的代理方法來向核心設備發送更新值.
下一步,獲得更新的characteristic值並且通過CBPeripheralManager
的updateValue:forCharacteristic:onSubscribedCentrals:
方法發送.
NSData *updatedValue = // fetch the characteristic's new value
BOOL didSendValue = [myPeripheralManager updateValue:updatedValue
forCharacteristic:characteristic onSubscribedCentrals:nil];
當你調用該方法時,可以通過最後一個參數來指定你想要更新的訂閱characteristic值核心設備.上例中,如果你指定爲nil,所有訂閱的連接核心設備都將收到更新.
updateValue:forCharacteristic:onSubscribedCentrals:
返回一個布爾值(意味向訂閱的核心設備發送更新成功).如果用來傳輸更新數據的底層隊列滿了,返回NO
.Perpheral manager 會在傳輸隊列有空間時調用peripheralManagerIsReadyToUpdateSubscribers:
,可以聲明本方法來重發值,依舊使用updateValue:forCharacteristic:onSubscribedCentrals:
Note:使用通知來向訂閱的核心設備發送的一個數據包,也就是說,當你更新一個訂閱的核心設備,你應該在一個通知中發送全部更新數據,通過調用一次updateValue:forCharacteristic:onSubscribedCentrals:
.
不是所有的數據都可以通過通知(notification)傳輸,這取決於characteristic值的大小.如果數據較大,應該在覈心端調用CBPeripheral
的readValueForCharacteristic:
方法接收全部數據