標籤:
原文網址:http://southpeak.github.io/blog/2014/07/29/core-bluetoothkuang-jia-zhi-%5B%3F%5D-:centralyu-peripheral/
iOS和Mac應用使用Core Bluetooth framework來與BLE(低功耗藍芽)裝置通訊。我們的程式可以發現、搜尋並與低功耗外圍(Peripheral)藍牙裝置通訊,如心跳監聽器、數字溫控器、甚至是其它iOS裝置。這個架構抽象了支援藍芽4.0標準低功耗裝置的基本操作,隱藏了4.0標準的底層實現細節,讓我們可以方便的使用BLE裝置。
藍芽通訊中的角色
在BLE通訊中,主要有兩個角色:Central和Peripheral。類似於傳統的用戶端-服務端架構,一個Peripheral端是提供資料的一方(相當於服務端);而Central是使用Peripheral端提供的資料完成特定任務的一方(相當於用戶端)。以心跳監聽器為例展示了這樣一個架構:
Peripheral端以廣告包的形式來廣播一些資料。一個廣告包(advertising packet)是一小束相關資料,可能包含Peripheral提供的有用的資訊,如Peripheral名或主要功能。在BLE下,廣告是Peripheral裝置表現的主要形式。
Central端可以掃描並監聽其感興趣的任何廣播資訊的Peripheral裝置。
資料的廣播及接收需要以一定的資料結構來表示。而服務就是這樣一種資料結構。Peripheral端可能包含一個或多個服務或提供關於串連訊號強度的有用資訊。一個服務是一個裝置的資料的集合及資料相關的操作。
而服務本身又是由特性或所包含的服務組成的。一個特性提供了關於服務的更詳細的資訊。展示了心率監聽器中的各種資料結構
在一個Central端與Peripheral端成功建立串連後,Central可以發現Peripheral端提供的完整的服務及特性的集合。一個Central也可以讀寫Peripheral端的服務特性的值。我們將會在下面詳細介紹。
Central、Peripherals及Peripheral資料的表示
當我們使用本地Central與Peripheral端互動時,我們會在BLE通訊的Central端執行操作。除非我們設定了一個本地Peripheral裝置,否則大部分藍芽互動都是在Central端進行的。(下文也會講Peripheral端的基本操作)
在Central端,本地Central裝置由CBCentralManager對象表示。這個對象用於管理髮現與串連Peripheral裝置(CBPeripheral對象)的操作,包括掃描、尋找和串連。本地Central端與peripheral對象
當與peripheral裝置互動時,我們主要是在處理它的服務及特性。在Core Bluetooth架構中,服務是一個CBService對象,特性是一個CBCharacteristic對象,示範了Central端的服務與特性的基本結構:
蘋果在OS X 10.9和iOS 6版本後,提供了BLE外設(Peripheral)功能,可以將裝置作為Peripheral來處理。在Peripheral端,本地Peripheral裝置表示為一個CBPeripheralManager對象。這些對象用於管理將服務及特性發布到本地Peripheral裝置資料庫,並廣告這些服務給Central裝置。Peripheral管理器也用於響應來自Central端的讀寫請求。如展示了一個Peripheral端角色:
當在本地Peripheral裝置上設定資料時,我們實際上處理的是服務與特性的可變版本。在Core Bluetooth架構中,本地Peripheral服務由CBMutableService對象表示,而特性由CBMutableCharacteristic對象表示,展示了本地Peripheral端服務與特性的基本結構:
Peripheral(Server)端操作
一個Peripheral端操作主要有以下步驟:
- 啟動一個Peripheral管理對象
- 在本地Peripheral中設定服務及特性
- 將服務及特性發布給裝置的本機資料庫
- 廣告我們的服務
- 針對串連的Central端的讀寫請求作出響應
- 發送更新的特性值到訂閱Central端
我們將在下面結合代碼對每一步分別進行講解
啟動一個Peripheral管理器
要在本地裝置上實現一個Peripheral端,我們需要分配並初始化一個Peripheral管理器執行個體,如下代碼所示
// 建立一個Peripheral管理器// 我們將當前類作為peripheralManager,因此必須實現CBPeripheralManagerDelegate// 第二個參數如果指定為nil,則預設使用主隊列peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
建立Peripheral管理器後,Peripheral管理器會調用代理對象的peripheralManagerDidUpdateState:方法。我們需要實現這個方法來確保本地裝置支援BLE。
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral{ NSLog(@"Peripheral Manager Did Update State"); switch (peripheral.state) { case CBPeripheralManagerStatePoweredOn: NSLog(@"CBPeripheralManagerStatePoweredOn"); break; case CBPeripheralManagerStatePoweredOff: NSLog(@"CBPeripheralManagerStatePoweredOff"); break; case CBPeripheralManagerStateUnsupported: NSLog(@"CBPeripheralManagerStateUnsupported"); break; default: break; }}
設定服務及特性
一個本地Peripheral資料庫以類似樹的結構來組織服務及特性。所以,在設定服務及特性時,我們將其組織成樹結構。
一個Peripheral的服務和特性通過128位的藍芽指定的UUID來標識,該標識是一個CBUUID對象。雖然SIG組織沒的預先定義所有的服務與特性的UUID,但是SIG已經定義並發布了一些通過的UUID,這些UUID被簡化成16位以方便使用。例如,SIG定義了一個16位的UUID作為心跳服務的標識(180D)。
CBUUID類提供了方法,以從字串中產生一個CBUUID對象。當字條串使用的是預定義的16位UUID時,Core Bluetooth使用它時會預先自動補全成128位的標識。
CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString:@"180D"];
當然我們也可以自己產生一個128位的UUID來標識我們的服務與特性。在命令列中使用uuidgen命令會產生一個128位的UUID字串,然後我們可以使用它來產生一個CBUUID對象。
產生UUID對象後,我們就可以用這個對象來建立我們的服務及特性,然後再將它們組織成樹狀結構。
建立特性的代碼如下所示
CBUUID *characteristicUUID1 = [CBUUID UUIDWithString:@"C22D1ECA-0F78-463B-8C21-688A517D7D2B"];CBUUID *characteristicUUID2 = [CBUUID UUIDWithString:@"632FB3C9-2078-419B-83AA-DBC64B5B685A"];CBMutableCharacteristic *character1 = [[CBMutableCharacteristic alloc] initWithType:characteristicUUID1 properties:CBCharacteristicPropertyRead value:nil permissions:CBAttributePermissionsReadable];CBMutableCharacteristic *character2 = [[CBMutableCharacteristic alloc] initWithType:characteristicUUID2 properties:CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsWriteable];
我們需要設定特性的屬性、值及許可權。屬性及許可權值確定了屬性值是可讀的還是可寫的,及串連的Central端是否可以訂閱特性的值。另外,如果我們指定了特性的值,則這個值會被緩衝且其屬性及許可權被設定成可讀的。如果我們要讓特性的值是可寫的,或者期望屬性所屬的服務的生命週期裡這個值可以被修改,則必須指定值為nil。
建立的特性之後,我們便可以建立一個與特性相關的服務,然後將特性關聯到服務上,如下代碼所示:
CBUUID *serviceUUID = [CBUUID UUIDWithString:@"3655296F-96CE-44D4-912D-CD83F06E7E7E"];CBMutableService *service = [[CBMutableService alloc] initWithType:serviceUUID primary:YES];service.characteristics = @[character1, character2]; // 組織成樹狀結構
上例中primary參數傳遞的是YES,表示這是一個主服務,即描述了一個裝置的主要功能且能被其它服務引用。與之相對的是次要服務(secondary service),其只在引用它的另一個服務的上下文中描述一個服務。
發布服務及特性
建立服務及特性後交將其組織成樹狀結構後,我們需要將這些服務發布到裝置的本機資料庫上。我們可以使用CBPeripheralManager的addService:方法來完成此工作。如下代碼所示:
[peripheralManager addService:service];
在調用些方法發布服務時,CBPeripheralManager對象會調用它的代理的peripheralManager:didAddService:error:方法。如果發布過程中出現錯誤導致無法以布,則可以實現該代理方法來處理錯誤,如下代碼所示:
- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error{ NSLog(@"Add Service"); if (error) { NSLog(@"Error publishing service: %@", [error localizedDescription]); }}
在將服務與特性發布到裝置資料庫後,服務將會被緩衝,且我們不能再修改這個服務。
廣告服務
處理完以上步驟,我們便可以將這些服務廣告給對服務感興趣的Central端。我們可以通過調用CBPeripheralManager執行個體的startAdvertising:方法來完成這一操作,如下代碼所示:
[peripheralManager startAdvertising:@{CBAdvertisementDataServiceUUIDsKey: @[service.UUID]}];
startAdvertising:的參數是一個字典,Peripheral管理器支援且僅支援兩個key值:CBAdvertisementDataLocalNameKey與CBAdvertisementDataServiceUUIDsKey。這兩個值描述了資料的詳情。key值所對應的value期望是一個表示多個服務的數組。
當廣告服務時,CBPeripheralManager對象會調用代碼對象的peripheralManagerDidStartAdvertising:error:方法,我們可以在此做相應的處理,如下代碼所示:
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error{ NSLog(@"Start Advertising"); if (error) { NSLog(@"Error advertising: %@", [error localizedDescription]); }}
廣告服務之後,Central端便可以發現裝置並初始化一個串連。
對Central端的讀寫請求作出響應
在與Central端進行串連後,可能需要從其接收讀寫請求,我們需要以適當的方式作出響應。
當串連的Central端請求讀取特性的值時,CBPeripheralManager對象會調用代理對象的peripheralManager:didReceiveReadRequest:方法,代理方法提供一個CBATTRequest對象以表示Central端的請求,我們可以使用它的屬性來填充請求。下面代碼簡單展示了這樣一個過程:
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request{ // 查看請求的特性是否是指定的特性 if ([request.characteristic.UUID isEqual:cha1.UUID]) { NSLog(@"Request character 1"); // 確保讀請求所請求的位移量沒有超出我們的特性的值的長度範圍 // offset屬性指定的請求所要讀取值的位移位置 if (request.offset > cha1.value.length) { [peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset]; return; } // 如果讀取位置未越界,則將特性中的值的指定範圍賦給請求的value屬性。 request.value = [cha1.value subdataWithRange:(NSRange){request.offset, cha1.value.length - request.offset}]; // 對請求作出成功響應 [peripheralManager respondToRequest:request withResult:CBATTErrorSuccess]; }}
在每次調用代理對象的peripheralManager:didReceiveReadRequest:時調用respondToRequest:withResult:方法以對請求做出響應。
處理寫請求類似於上述過程,此時會調用代理對象的peripheralManager:didReceiveWriteRequests:方法。不同的是代理方法會給我們一個包含一個或多個CBATTRequest對象的數組,每一個都表示一個寫請求。我們可以使用請求對象的value屬性來給我們的特性屬性賦值,如下代碼所示:
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests{ CBATTRequest *request = requests[0]; cha1.value = request.value; [peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];}
響應處理與請求類似。
發送更新的特性值給訂閱的Central端
如果有一個或多個Central端訂閱了我們的服務的特性時,當特性發生變化時,我們需要通知這些Central端。為此,代理對象需要實現peripheralManager:central:didSubscribeToCharacteristic:方法。如下所示:
- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic{ NSLog(@"Central subscribed to characteristic %@", characteristic); NSData *updatedData = characteristic.value; // 擷取屬性更新的值並調用以下方法將其發送到Central端 // 最後一個參數指定我們想將修改發送給哪個Central端,如果傳nil,則會發送給所有串連的Central // 將方法返回一個BOOL值,表示修改是否被成功發送,如果用於傳送更新值的隊列被填充滿,則方法返回NO BOOL didSendValue = [peripheralManager updateValue:updatedData forCharacteristic:(CBMutableCharacteristic *)characteristic onSubscribedCentrals:nil]; NSLog(@"Send Success ? %@", (didSendValue ? @"YES" : @"NO"));}
在上述代碼中,當傳輸隊列有可用的空間時,CBPeripheralManager對象會調用代碼對象的peripheralManagerIsReadyToUpdateSubscribers:方法。我們可以在這個方法中調用updateValue:forCharacteristic:onSubscribedCentrals:來重新發送值。
我們使用通知來將單個資料包發送給訂閱的Central。當我們更新訂閱的Central時,我們應該通過調用一次updateValue:forCharacteristic:onSubscribedCentrals:方法將整個更新的值放在一個通知中。
由於特性的值大小不一,所以不是所有值都會被通知傳輸。如果發生這種情況,需要在Central端調用CBPeripheral執行個體的readValueForCharacteristic:方法來處理,該方法可以擷取整個值。
Central(Client)端操作
一個Central端主要包含以下操作:
- 啟動一個Central端管理器對象
- 搜尋並串連正在廣告的Peripheral裝置
- 在串連到Peripheral端後查詢資料
- 發送一個對特性值的讀寫請求到Peripheral端
- 當Peripheral端特性值改變時接收通知
我們將在下面結合代碼對每一步分別進行講解
啟動一個Central管理器
CBCentralManager對象在Core Bluetooth中表示一個本地Central裝置,我們在執行任何BLE互動時必須分配並初始化一個Central管理器對象。建立代碼如下所示:
// 指定當前類為代理對象,所以其需要實現CBCentralManagerDelegate協議// 如果queue為nil,則Central管理器使用主隊列來發送事件centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];
建立Central管理器時,管理器對象會調用代理對象的centralManagerDidUpdateState:方法。我們需要實現這個方法來確保本地裝置支援BLE。
- (void)centralManagerDidUpdateState:(CBCentralManager *)central{ NSLog(@"Central Update State"); switch (central.state) { case CBCentralManagerStatePoweredOn: NSLog(@"CBCentralManagerStatePoweredOn"); break; case CBCentralManagerStatePoweredOff: NSLog(@"CBCentralManagerStatePoweredOff"); break; case CBCentralManagerStateUnsupported: NSLog(@"CBCentralManagerStateUnsupported"); break; default: break; }}
發現正在廣告的Peripheral裝置
Central端的首要任務是發現正在廣告的Peripheral裝置,以備後續串連。我們可以調用CBCentralManager執行個體的scanForPeripheralsWithServices:options:方法來發現正在廣告的Peripheral裝置。如下代碼所示:
// 尋找Peripheral裝置// 如果第一個參數傳遞nil,則管理器會返回所有發現的Peripheral裝置。// 通常我們會指定一個UUID對象的數組,來尋找特定的裝置[centralManager scanForPeripheralsWithServices:nil options:nil];
在調用上述方法後,CBCentralManager對象在每次發現裝置時會調用代理對象的centralManager:didDiscoverPeripheral:advertisementData:RSSI:方法。
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{ NSLog(@"Discover name : %@", peripheral.name); // 當我們尋找到Peripheral端時,我們可以停止尋找其它裝置,以節省電量 [centralManager stopScan]; NSLog(@"Scanning stop");}
串連Peripheral裝置
在尋找到Peripheral裝置後,我們可以調用CBCentralManager執行個體的connectPeripheral:options:方法來串連Peripheral裝置。如下代碼所示
[centralManager connectPeripheral:peripheral options:nil];
如果串連成功,則會調用代碼對象的centralManager:didConnectPeripheral:方法,我們可以實現該方法以做相應處理。另外,在開始與Peripheral裝置互動之前,我們需要設定peripheral對象的代理,以確保接收到合適的回調。
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{ NSLog(@"Peripheral Connected"); peripheral.delegate = self;}
尋找所串連Peripheral裝置的服務
建立到Peripheral裝置的串連後,我們就可以開始查詢資料了。首先我們需要尋找Peripheral裝置中可用的服務。由於Peripheral裝置可以廣告的資料有限,所以Peripheral裝置實際的服務可能比它廣告的服務要多。我們可以調用peripheral對象的discoverServices:方法來尋找所有的服務。如下代碼所示:
[peripheral discoverServices:nil];
參數傳遞nil可以尋找所有的服務,但一般情況下我們會指定感興趣的服務。
當調用上述方法時,peripheral會調用代理對象的peripheral:didDiscoverServices:方法。Core Bluetooth建立一個CBService對象的數組,數組中的元素是peripheral中找到的服務。
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{ NSLog(@"Discover Service"); for (CBService *service in peripheral.services) { NSLog(@"Discovered service %@", service); }}
尋找服務中的特性
假設我們已經找到感興趣的服務,接下來就是查詢服務中的特性了。為了尋找服務中的特性,我們只需要調用CBPeripheral類的discoverCharacteristics:forService:方法,如下所示:
NSLog(@"Discovering characteristics for service %@", service);[peripheral discoverCharacteristics:nil forService:service];
當發現特定服務的特性時,peripheral對象會調用代理對象的peripheral:didDiscoverCharacteristicsForService:error:方法。在這個方法中,Core Bluetooth會建立一個CBCharacteristic對象的數組,每個元素表示一個尋找到的特性對象。如下代碼所示:
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{ NSLog(@"Discover Characteristics"); for (CBCharacteristic *characteristic in service.characteristics) { NSLog(@"Discovered characteristic %@", characteristic); }}
擷取特性的值
一個特性包含一個單一的值,這個值包含了Peripheral服務的資訊。在擷取到特性之後,我們就可以從特性中擷取這個值。只需要調用CBPeripheral執行個體的readValueForCharacteristic:方法即可。如下所示:
NSLog(@"Reading value for characteristic %@", characteristic);[peripheral readValueForCharacteristic:characteristic];
當我們讀取特性中的值時,peripheral對象會調用代理對象的peripheral:didUpdateValueForCharacteristic:error:方法來擷取該值。如果擷取成功,我們可以通過特性的value屬性來訪問它,如下所示:
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{ NSData *data = characteristic.value; NSLog(@"Data = %@", data);}
訂閱特性的值
雖然使用readValueForCharacteristic:方法讀取特性值對於一些使用情境非常有效,但對於擷取改變的值不太有效。對於大多數變動的值來講,我們需要通過訂閱來擷取它們。當我們訂閱特性的值時,在值改變時,我們會從peripheral對象收到通知。
我們可以調用CBPeripheral類的setNotifyValue:forCharacteristic:方法來訂閱感興趣的特性的值。如下所示:
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
當我們嘗試訂閱特性的值時,會調用peripheral對象的代理對象的peripheral:didUpdateNotificationStateForCharacteristic:error: 方法。如果訂閱失敗,我們可以實現該代理方法來訪問錯誤,如下所示:
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{ ... if (error) { NSLog(@"Error changing notification state: %@", [error localizedDescription]); }}
在成功訂閱特性的值後,當特性值改變時,peripheral裝置會通知我們的應用。
寫入特性的值
一些情境下,我們需要寫入特性的值。例如我們需要與BLE數字恒溫器互動時,可能需要給恒溫器提供一個值來設定房間的溫度。如果特性的值是可寫的,我們可以通過調用CBPeripheral執行個體的writeValue:forCharacteristic:type:方法來寫入值。
NSData *data = [NSData dataWithBytes:[@"test" UTF8String] length:@"test".length];[peripheral writeValue:data forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
當嘗試寫入特性值時,我們需要指定想要執行的寫入類型。上例指定了寫入類型是CBCharacteristicWriteWithResponse,表示peripheral讓我們的應用知道是否寫入成功。
指定寫入類型為CBCharacteristicWriteWithResponse的peripheral對象,在響應請求時會調用代理對象的peripheral:didWriteValueForCharacteristic:error:方法。如果寫入失敗,我們可以在這個方法中處理錯誤資訊。
小結
Core Bluetooth架構已經為我們封裝了藍芽通訊的底層實現,我們只需要做簡單的處理就可以在程式中實現基於藍芽的通訊。不過在遊戲中,一般使用Game Kit中內建的藍芽處理功能,以實現大資料量的通訊。Core Bluetooth架構還是比較適合小資料量的通訊。
【轉】Core Bluetooth架構之一:Central與Peripheral