藍牙
此文章轉載自Kenshin Cui’s Blog,此博主文章簡顯易懂,強烈推薦。爲方便查看,轉載至此。轉載界面凌亂,推薦進原博客查看。
隨着藍牙低功耗技術BLE(Bluetooth Low Energy)的發展,藍牙技術正在一步步成熟,如今的大部分移動設備都配備有藍牙4.0,相比之前的藍牙技術耗電量大大降低。從iOS的發展史也不難看出蘋果目前對藍牙技術也是越來越關注,例如蘋果於2013年9月發佈的iOS7就配備了iBeacon技術,這項技術完全基於藍牙傳輸。但是衆所周知蘋果的設備對於權限要求也是比較高的,因此在iOS中並不能像Android一樣隨意使用藍牙進行文件傳輸(除非你已經越獄)。在iOS中進行藍牙傳輸應用開發常用的框架有如下幾種:
GameKit.framework:iOS7之前的藍牙通訊框架,從iOS7開始過期,但是目前多數應用還是基於此框架。
MultipeerConnectivity.framework:iOS7開始引入的新的藍牙通訊開發框架,用於取代GameKit。
CoreBluetooth.framework:功能強大的藍牙開發框架,要求設備必須支持藍牙4.0。
前兩個框架使用起來比較簡單,但是缺點也比較明顯:僅僅支持iOS設備,傳輸內容僅限於沙盒或者照片庫中用戶選擇的文件,並且第一個框架只能在同一個應用之間進行傳輸(一個iOS設備安裝應用A,另一個iOS設備上安裝應用B是無法傳輸的)。當然CoreBluetooth就擺脫了這些束縛,它不再侷限於iOS設備之間進行傳輸,你可以通過iOS設備向Android、Windows Phone以及其他安裝有藍牙4.0芯片的智能設備傳輸,因此也是目前智能家居、無線支付等熱門智能設備所推崇的技術。
GameKit
其實從名稱來看這個框架並不是專門爲了支持藍牙傳輸而設計的,它是爲遊戲設計的。而很多遊戲中會用到基於藍牙的點對點信息傳輸,因此這個框架中集成了藍牙傳輸模塊。前面也說了這個框架本身有很多限制,但是在iOS7之前的很多藍牙傳輸都是基於此框架的,所以有必要對它進行了解。GameKit中的藍牙使用設計很簡單,並沒有給開發者留有太多的複雜接口,而多數連接細節開發者是不需要關注的。GameKit中提供了兩個關鍵類來操作藍牙連接:
GKPeerPickerController:藍牙查找、連接用的視圖控制器,通常情況下應用程序A打開後會調用此控制器的show方法來展示一個藍牙查找的視圖,一旦發現了另一個同樣在查找藍牙連接的客戶客戶端B就會出現在視圖列表中,此時如果用戶點擊連接B,B客戶端就會詢問用戶是否允許A連接B,如果允許後A和B之間建立一個藍牙連接。
GKSession:連接會話,主要用於發送和接受傳輸數據。一旦A和B建立連接GKPeerPickerController的代理方法會將A、B兩者建立的會話(GKSession)對象傳遞給開發人員,開發人員拿到此對象可以發送和接收數據。
其實理解了上面兩個類之後,使用起來就比較簡單了,下面就以一個圖片發送程序來演示GameKit中藍牙的使用。此程序一個客戶端運行在模擬器上作爲客戶端A,另一個運行在iPhone真機上作爲客戶端B(注意A、B必須運行同一個程序,GameKit藍牙開發是不支持兩個不同的應用傳輸數據的)。兩個程序運行之後均調用GKPeerPickerController來發現周圍藍牙設備,一旦A發現了B之後就開始連接B,然後iOS會詢問用戶是否接受連接,一旦接受之後就會調用GKPeerPickerController的-(void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session代理方法,在此方法中可以獲得連接的設備id(peerID)和連接會話(session);此時可以設置會話的數據接收句柄(相當於一個代理)並保存會話以便發送數據時使用;一旦一端(假設是A)調用會話的sendDataToAllPeers: withDataMode: error:方法發送數據,此時另一端(假設是B)就會調用句柄的- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context方法,在此方法可以獲得發送數據並處理。下面是程序代碼:
// // ViewController.m // GameKit // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" #import <GameKit/GameKit.h> @interface ViewController ()<GKPeerPickerControllerDelegate,UIImagePickerControllerDelegate,UINavigationBarDelegate> @property (weak, nonatomic) IBOutlet UIImageView *imageView;//照片顯示視圖 @property (strong,nonatomic) GKSession *session;//藍牙連接會話 @end @implementation ViewController #pragma mark - 控制器視圖方法 - (void)viewDidLoad { [super viewDidLoad]; GKPeerPickerController *pearPickerController=[[GKPeerPickerController alloc]init]; pearPickerController.delegate=self; [pearPickerController show]; } #pragma mark - UI事件 - (IBAction)selectClick:(UIBarButtonItem *)sender { UIImagePickerController *imagePickerController=[[UIImagePickerController alloc]init]; imagePickerController.delegate=self; [self presentViewController:imagePickerController animated:YES completion:nil]; } - (IBAction)sendClick:(UIBarButtonItem *)sender { NSData *data=UIImagePNGRepresentation(self.imageView.image); NSError *error=nil; [self.session sendDataToAllPeers:data withDataMode:GKSendDataReliable error:&error]; if (error) { NSLog(@"發送圖片過程中發生錯誤,錯誤信息:%@",error.localizedDescription); } } #pragma mark - GKPeerPickerController代理方法 /** * 連接到某個設備 * * @param picker 藍牙點對點連接控制器 * @param peerID 連接設備藍牙傳輸ID * @param session 連接會話 */ -(void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session{ self.session=session; NSLog(@"已連接客戶端設備:%@.",peerID); //設置數據接收處理句柄,相當於代理,一旦數據接收完成調用它的-receiveData:fromPeer:inSession:context:方法處理數據 [self.session setDataReceiveHandler:self withContext:nil]; [picker dismiss];//一旦連接成功關閉窗口 } #pragma mark - 藍牙數據接收方法 - (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context{ UIImage *image=[UIImage imageWithData:data]; self.imageView.image=image; UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); NSLog(@"數據發送成功!"); } #pragma mark - UIImagePickerController代理方法 -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{ self.imageView.image=[info objectForKey:UIImagePickerControllerOriginalImage]; [self dismissViewControllerAnimated:YES completion:nil]; } -(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{ [self dismissViewControllerAnimated:YES completion:nil]; } @end
運行效果(左側是真機,右側是模擬器,程序演示了兩個客戶端互發圖片的場景:首先是模擬器發送圖片給真機,然後真機發送圖片給模擬器):
MultipeerConnectivity
前面已經說了GameKit相關的藍牙操作類從iOS7已經全部過期,蘋果官方推薦使用MultipeerConnectivity代替。但是應該瞭解,MultipeerConnectivity.framework並不僅僅支持藍牙連接,準確的說它是一種支持Wi-Fi網絡、P2P Wi-Fi已經藍牙個人局域網的通信框架,它屏蔽了具體的連接技術,讓開發人員有統一的接口編程方法。通過MultipeerConnectivity連接的節點之間可以安全的傳遞信息、流或者其他文件資源而不必通過網絡服務。此外使用MultipeerConnectivity進行近場通信也不再侷限於同一個應用之間傳輸,而是可以在不同的應用之間進行數據傳輸(當然如果有必要的話你仍然可以選擇在一個應用程序之間傳輸)。
要了解MultipeerConnectivity的使用必須要清楚一個概念:廣播(Advertisting)和發現(Disconvering),這很類似於一種Client-Server模式。假設有兩臺設備A、B,B作爲廣播去發送自身服務,A作爲發現的客戶端。一旦A發現了B就試圖建立連接,經過B同意二者建立連接就可以相互發送數據。在使用GameKit框架時,A和B既作爲廣播又作爲發現,當然這種情況在MultipeerConnectivity中也很常見。
A.廣播
無論是作爲服務器端去廣播還是作爲客戶端去發現廣播服務,那麼兩個(或更多)不同的設備之間必須要有區分,通常情況下使用MCPeerID對象來區分一臺設備,在這個設備中可以指定顯示給對方查看的名稱(display name)。另外不管是哪一方,還必須建立一個會話MCSession用於發送和接受數據。通常情況下會在會話的-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state代理方法中跟蹤會話狀態(已連接、正在連接、未連接);在會話的-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID代理方法中接收數據;同時還會調用會話的-(void)sendData: toPeers:withMode: error:方法去發送數據。
廣播作爲一個服務器去發佈自身服務,供周邊設備發現連接。在MultipeerConnectivity中使用MCAdvertiserAssistant來表示一個廣播,通常創建廣播時指定一個會話MCSession對象將廣播服務和會話關聯起來。一旦調用廣播的start方法周邊的設備就可以發現該廣播並可以連接到此服務。在MCSession的代理方法中可以隨時更新連接狀態,一旦建立了連接之後就可以通過MCSession的connectedPeers獲得已經連接的設備。
B.發現
前面已經說過作爲發現的客戶端同樣需要一個MCPeerID來標誌一個客戶端,同時會擁有一個MCSession來監聽連接狀態併發送、接受數據。除此之外,要發現廣播服務,客戶端就必須要隨時查找服務來連接,在MultipeerConnectivity中提供了一個控制器MCBrowserViewController來展示可連接和已連接的設備(這類似於GameKit中的GKPeerPickerController),當然如果想要自己定製一個界面來展示設備連接的情況你可以選擇自己開發一套UI界面。一旦通過MCBroserViewController選擇一個節點去連接,那麼作爲廣播的節點就會收到通知,詢問用戶是否允許連接。由於初始化MCBrowserViewController的過程已經指定了會話MCSession,所以連接過程中會隨時更新會話狀態,一旦建立了連接,就可以通過會話的connected屬性獲得已連接設備並且可以使用會話發送、接受數據。
下面用兩個不同的應用程序來演示使用MultipeerConnectivity的使用過程,其中一個應用運行在模擬器中作爲廣播節點,另一個運行在iPhone真機上作爲發現節點,並且實現兩個節點的圖片互傳。
首先看一下作爲廣播節點的程序:
界面:
點擊“開始廣播”來發布服務,一旦有節點連接此服務就可以使用“選擇照片”來從照片庫中選取一張圖片併發送到所有已連接節點。
程序:
// // ViewController.m // MultipeerConnectivity_Advertiser // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "ViewController.h" #import <MultipeerConnectivity/MultipeerConnectivity.h> @interface ViewController ()<MCSessionDelegate,MCAdvertiserAssistantDelegate, UIImagePickerControllerDelegate,UINavigationControllerDelegate> @property (strong,nonatomic) MCSession *session; @property (strong,nonatomic) MCAdvertiserAssistant *advertiserAssistant; @property (strong,nonatomic) UIImagePickerController *imagePickerController; @property (weak, nonatomic) IBOutlet UIImageView *photo; @end @implementation ViewController #pragma mark - 控制器視圖事件 - (void)viewDidLoad { [super viewDidLoad]; //創建節點,displayName是用於提供給周邊設備查看和區分此服務的 MCPeerID *peerID=[[MCPeerID alloc]initWithDisplayName:@"KenshinCui_Advertiser"]; _session=[[MCSession alloc]initWithPeer:peerID]; _session.delegate=self; //創建廣播 _advertiserAssistant=[[MCAdvertiserAssistant alloc]initWithServiceType:@"cmj-stream" discoveryInfo:nil session:_session]; _advertiserAssistant.delegate=self; } #pragma mark - UI事件 - (IBAction)advertiserClick:(UIBarButtonItem *)sender { //開始廣播 [self.advertiserAssistant start]; } - (IBAction)selectClick:(UIBarButtonItem *)sender { _imagePickerController=[[UIImagePickerController alloc]init]; _imagePickerController.delegate=self; [self presentViewController:_imagePickerController animated:YES completion:nil]; } #pragma mark - MCSession代理方法 -(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{ NSLog(@"didChangeState"); switch (state) { case MCSessionStateConnected: NSLog(@"連接成功."); break; case MCSessionStateConnecting: NSLog(@"正在連接..."); break; default: NSLog(@"連接失敗."); break; } } //接收數據 -(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{ NSLog(@"開始接收數據..."); UIImage *image=[UIImage imageWithData:data]; [self.photo setImage:image]; //保存到相冊 UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); } #pragma mark - MCAdvertiserAssistant代理方法 #pragma mark - UIImagePickerController代理方法 -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{ UIImage *image=[info objectForKey:UIImagePickerControllerOriginalImage]; [self.photo setImage:image]; //發送數據給所有已連接設備 NSError *error=nil; [self.session sendData:UIImagePNGRepresentation(image) toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error]; NSLog(@"開始發送數據..."); if (error) { NSLog(@"發送數據過程中發生錯誤,錯誤信息:%@",error.localizedDescription); } [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; } -(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{ [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; } @end
再看一下作爲發現節點的程序:
界面:
點擊“查找設備”瀏覽可用服務,點擊服務建立連接;一旦建立了連接之後就可以點擊“選擇照片”會從照片庫中選擇一張圖片併發送給已連接的節點。
程序:
// // ViewController.m // MultipeerConnectivity // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "ViewController.h" #import <MultipeerConnectivity/MultipeerConnectivity.h> @interface ViewController ()<MCSessionDelegate,MCBrowserViewControllerDelegate,UIImagePickerControllerDelegate,UINavigationControllerDelegate> @property (strong,nonatomic) MCSession *session; @property (strong,nonatomic) MCBrowserViewController *browserController; @property (strong,nonatomic) UIImagePickerController *imagePickerController; @property (weak, nonatomic) IBOutlet UIImageView *photo; @end @implementation ViewController #pragma mark - 控制器視圖事件 - (void)viewDidLoad { [super viewDidLoad]; //創建節點 MCPeerID *peerID=[[MCPeerID alloc]initWithDisplayName:@"KenshinCui"]; //創建會話 _session=[[MCSession alloc]initWithPeer:peerID]; _session.delegate=self; } #pragma mark- UI事件 - (IBAction)browserClick:(UIBarButtonItem *)sender { _browserController=[[MCBrowserViewController alloc]initWithServiceType:@"cmj-stream" session:self.session]; _browserController.delegate=self; [self presentViewController:_browserController animated:YES completion:nil]; } - (IBAction)selectClick:(UIBarButtonItem *)sender { _imagePickerController=[[UIImagePickerController alloc]init]; _imagePickerController.delegate=self; [self presentViewController:_imagePickerController animated:YES completion:nil]; } #pragma mark - MCBrowserViewController代理方法 -(void)browserViewControllerDidFinish:(MCBrowserViewController *)browserViewController{ NSLog(@"已選擇"); [self.browserController dismissViewControllerAnimated:YES completion:nil]; } -(void)browserViewControllerWasCancelled:(MCBrowserViewController *)browserViewController{ NSLog(@"取消瀏覽."); [self.browserController dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - MCSession代理方法 -(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{ NSLog(@"didChangeState"); switch (state) { case MCSessionStateConnected: NSLog(@"連接成功."); [self.browserController dismissViewControllerAnimated:YES completion:nil]; break; case MCSessionStateConnecting: NSLog(@"正在連接..."); break; default: NSLog(@"連接失敗."); break; } } //接收數據 -(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{ NSLog(@"開始接收數據..."); UIImage *image=[UIImage imageWithData:data]; [self.photo setImage:image]; //保存到相冊 UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); } #pragma mark - UIImagePickerController代理方法 -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{ UIImage *image=[info objectForKey:UIImagePickerControllerOriginalImage]; [self.photo setImage:image]; //發送數據給所有已連接設備 NSError *error=nil; [self.session sendData:UIImagePNGRepresentation(image) toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error]; NSLog(@"開始發送數據..."); if (error) { NSLog(@"發送數據過程中發生錯誤,錯誤信息:%@",error.localizedDescription); } [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; } -(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{ [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; } @end
在兩個程序中無論是MCBrowserViewController還是MCAdvertiserAssistant在初始化的時候都指定了一個服務類型“cmj-photo”,這是唯一標識一個服務類型的標記,可以按照官方的要求命名,應該儘可能表達服務的作用。需要特別指出的是,如果廣播命名爲“cmj-photo”那麼發現節點只有在MCBrowserViewController中指定爲“cmj-photo”才能發現此服務。
運行效果:
CoreBluetooth
無論是GameKit還是MultipeerConnectivity,都只能在iOS設備之間進行數據傳輸,這就大大降低了藍牙的使用範圍,於是從iOS6開始蘋果推出了CoreBluetooth.framework,這個框架最大的特點就是完全基於BLE4.0標準並且支持非iOS設備。當前BLE應用相當廣泛,不再僅僅是兩個設備之間的數據傳輸,它還有很多其他應用市場,例如室內定位、無線支付、智能家居等等,這也使得CoreBluetooth成爲當前最熱門的藍牙技術。
CoreBluetooth設計同樣也是類似於客戶端-服務器端的設計,作爲服務器端的設備稱爲外圍設備(Peripheral),作爲客戶端的設備叫做中央設備(Central),CoreBlueTooth整個框架就是基於這兩個概念來設計的。
外圍設備和中央設備在CoreBluetooth中使用CBPeripheralManager和CBCentralManager表示。
CBPeripheralManager:外圍設備通常用於發佈服務、生成數據、保存數據。外圍設備發佈並廣播服務,告訴周圍的中央設備它的可用服務和特徵。
CBCentralManager:中央設備使用外圍設備的數據。中央設備掃描到外圍設備後會就會試圖建立連接,一旦連接成功就可以使用這些服務和特徵。
外圍設備和中央設備之間交互的橋樑是服務(CBService)和特徵(CBCharacteristic),二者都有一個唯一的標識UUID(CBUUID類型)來唯一確定一個服務或者特徵,每個服務可以擁有多個特徵,下面是他們之間的關係:
一臺iOS設備(注意iPhone4以下設備不支持BLE,另外iOS7.0、8.0模擬器也無法模擬BLE)既可以作爲外圍設備又可以作爲中央設備,但是不能同時即是外圍設備又是中央設備,同時注意建立連接的過程不需要用戶手動選擇允許,這一點和前面兩個框架是不同的,這主要是因爲BLE應用場景不再侷限於兩臺設備之間資源共享了。
A.外圍設備
創建一個外圍設備通常分爲以下幾個步驟:
- 創建外圍設備CBPeripheralManager對象並指定代理。
- 創建特徵CBCharacteristic、服務CBSerivce並添加到外圍設備
- 外圍設備開始廣播服務(startAdvertisting:)。
- 和中央設備CBCentral進行交互。
下面是簡單的程序示例,程序有兩個按鈕“啓動”和“更新”,點擊啓動按鈕則創建外圍設備、添加服務和特徵並開始廣播,一旦發現有中央設備連接並訂閱了此服務的特徵則通過更新按鈕更新特徵數據,此時已訂閱的中央設備就會收到更新數據。
界面設計:
程序設計:
// // ViewController.m // PeripheralApp // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // 外圍設備(周邊設備) #import "ViewController.h" #import <CoreBluetooth/CoreBluetooth.h> #define kPeripheralName @"Kenshin Cui's Device" //外圍設備名稱 #define kServiceUUID @"C4FB2349-72FE-4CA2-94D6-1F3CB16331EE" //服務的UUID #define kCharacteristicUUID @"6A3E4B28-522D-4B3B-82A9-D5E2004534FC" //特徵的UUID @interface ViewController ()<CBPeripheralManagerDelegate> @property (strong,nonatomic) CBPeripheralManager *peripheralManager;//外圍設備管理器 @property (strong,nonatomic) NSMutableArray *centralM;//訂閱此外圍設備特徵的中心設備 @property (strong,nonatomic) CBMutableCharacteristic *characteristicM;//特徵 @property (weak, nonatomic) IBOutlet UITextView *log; //日誌記錄 @end @implementation ViewController #pragma mark - 視圖控制器方法 - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - UI事件 //創建外圍設備 - (IBAction)startClick:(UIBarButtonItem *)sender { _peripheralManager=[[CBPeripheralManager alloc]initWithDelegate:self queue:nil]; } //更新數據 - (IBAction)transferClick:(UIBarButtonItem *)sender { [self updateCharacteristicValue]; } #pragma mark - CBPeripheralManager代理方法 //外圍設備狀態發生變化後調用 -(void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral{ switch (peripheral.state) { case CBPeripheralManagerStatePoweredOn: NSLog(@"BLE已打開."); [self writeToLog:@"BLE已打開."]; //添加服務 [self setupService]; break; default: NSLog(@"此設備不支持BLE或未打開藍牙功能,無法作爲外圍設備."); [self writeToLog:@"此設備不支持BLE或未打開藍牙功能,無法作爲外圍設備."]; break; } } //外圍設備添加服務後調用 -(void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error{ if (error) { NSLog(@"向外圍設備添加服務失敗,錯誤詳情:%@",error.localizedDescription); [self writeToLog:[NSString stringWithFormat:@"向外圍設備添加服務失敗,錯誤詳情:%@",error.localizedDescription]]; return; } //添加服務後開始廣播 NSDictionary *dic=@{CBAdvertisementDataLocalNameKey:kPeripheralName};//廣播設置 [self.peripheralManager startAdvertising:dic];//開始廣播 NSLog(@"向外圍設備添加了服務並開始廣播..."); [self writeToLog:@"向外圍設備添加了服務並開始廣播..."]; } -(void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error{ if (error) { NSLog(@"啓動廣播過程中發生錯誤,錯誤信息:%@",error.localizedDescription); [self writeToLog:[NSString stringWithFormat:@"啓動廣播過程中發生錯誤,錯誤信息:%@",error.localizedDescription]]; return; } NSLog(@"啓動廣播..."); [self writeToLog:@"啓動廣播..."]; } //訂閱特徵 -(void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic{ NSLog(@"中心設備:%@ 已訂閱特徵:%@.",central,characteristic); [self writeToLog:[NSString stringWithFormat:@"中心設備:%@ 已訂閱特徵:%@.",central.identifier.UUIDString,characteristic.UUID]]; //發現中心設備並存儲 if (![self.centralM containsObject:central]) { [self.centralM addObject:central]; } /*中心設備訂閱成功後外圍設備可以更新特徵值發送到中心設備,一旦更新特徵值將會觸發中心設備的代理方法: -(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error */ // [self updateCharacteristicValue]; } //取消訂閱特徵 -(void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic{ NSLog(@"didUnsubscribeFromCharacteristic"); } -(void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(CBATTRequest *)request{ NSLog(@"didReceiveWriteRequests"); } -(void)peripheralManager:(CBPeripheralManager *)peripheral willRestoreState:(NSDictionary *)dict{ NSLog(@"willRestoreState"); } #pragma mark -屬性 -(NSMutableArray *)centralM{ if (!_centralM) { _centralM=[NSMutableArray array]; } return _centralM; } #pragma mark - 私有方法 //創建特徵、服務並添加服務到外圍設備 -(void)setupService{ /*1.創建特徵*/ //創建特徵的UUID對象 CBUUID *characteristicUUID=[CBUUID UUIDWithString:kCharacteristicUUID]; //特徵值 // NSString *valueStr=kPeripheralName; // NSData *value=[valueStr dataUsingEncoding:NSUTF8StringEncoding]; //創建特徵 /** 參數 * uuid:特徵標識 * properties:特徵的屬性,例如:可通知、可寫、可讀等 * value:特徵值 * permissions:特徵的權限 */ CBMutableCharacteristic *characteristicM=[[CBMutableCharacteristic alloc]initWithType:characteristicUUID properties:CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable]; self.characteristicM=characteristicM; // CBMutableCharacteristic *characteristicM=[[CBMutableCharacteristic alloc]initWithType:characteristicUUID properties:CBCharacteristicPropertyRead value:nil permissions:CBAttributePermissionsReadable]; // characteristicM.value=value; /*創建服務並且設置特徵*/ //創建服務UUID對象 CBUUID *serviceUUID=[CBUUID UUIDWithString:kServiceUUID]; //創建服務 CBMutableService *serviceM=[[CBMutableService alloc]initWithType:serviceUUID primary:YES]; //設置服務的特徵 [serviceM setCharacteristics:@[characteristicM]]; /*將服務添加到外圍設備*/ [self.peripheralManager addService:serviceM]; } //更新特徵值 -(void)updateCharacteristicValue{ //特徵值 NSString *valueStr=[NSString stringWithFormat:@"%@ --%@",kPeripheralName,[NSDate date]]; NSData *value=[valueStr dataUsingEncoding:NSUTF8StringEncoding]; //更新特徵值 [self.peripheralManager updateValue:value forCharacteristic:self.characteristicM onSubscribedCentrals:nil]; [self writeToLog:[NSString stringWithFormat:@"更新特徵值:%@",valueStr]]; } /** * 記錄日誌 * * @param info 日誌信息 */ -(void)writeToLog:(NSString *)info{ self.log.text=[NSString stringWithFormat:@"%@\r\n%@",self.log.text,info]; } @end
上面程序運行的流程如下(圖中藍色代表外圍設備操作,綠色部分表示中央設備操作):
B.中央設備
中央設備的創建一般可以分爲如下幾個步驟:
- 創建中央設備管理對象CBCentralManager並指定代理。
- 掃描外圍設備,一般發現可用外圍設備則連接並保存外圍設備。
- 查找外圍設備服務和特徵,查找到可用特徵則讀取特徵數據。
下面是一個簡單的中央服務器端實現,點擊“啓動”按鈕則開始掃描周圍的外圍設備,一旦發現了可用的外圍設備則建立連接並設置外圍設備的代理,之後開始查找其服務和特徵。一旦外圍設備的特徵值做了更新,則可以在代理方法中讀取更新後的特徵值。
界面設計:
程序設計:
// // ViewController.m // CentralApp // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // 中心設備 #import "ViewController.h" #import <CoreBluetooth/CoreBluetooth.h> #define kServiceUUID @"C4FB2349-72FE-4CA2-94D6-1F3CB16331EE" //服務的UUID #define kCharacteristicUUID @"6A3E4B28-522D-4B3B-82A9-D5E2004534FC" //特徵的UUID @interface ViewController ()<CBCentralManagerDelegate,CBPeripheralDelegate> @property (strong,nonatomic) CBCentralManager *centralManager;//中心設備管理器 @property (strong,nonatomic) NSMutableArray *peripherals;//連接的外圍設備 @property (weak, nonatomic) IBOutlet UITextView *log;//日誌記錄 @end @implementation ViewController #pragma mark - 控制器視圖事件 - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - UI事件 - (IBAction)startClick:(UIBarButtonItem *)sender { //創建中心設備管理器並設置當前控制器視圖爲代理 _centralManager=[[CBCentralManager alloc]initWithDelegate:self queue:nil]; } #pragma mark - CBCentralManager代理方法 //中心服務器狀態更新後 -(void)centralManagerDidUpdateState:(CBCentralManager *)central{ switch (central.state) { case CBPeripheralManagerStatePoweredOn: NSLog(@"BLE已打開."); [self writeToLog:@"BLE已打開."]; //掃描外圍設備 // [central scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:kServiceUUID]] options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@YES}]; [central scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@YES}]; break; default: NSLog(@"此設備不支持BLE或未打開藍牙功能,無法作爲外圍設備."); [self writeToLog:@"此設備不支持BLE或未打開藍牙功能,無法作爲外圍設備."]; break; } } /** * 發現外圍設備 * * @param central 中心設備 * @param peripheral 外圍設備 * @param advertisementData 特徵數據 * @param RSSI 信號質量(信號強度) */ -(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{ NSLog(@"發現外圍設備..."); [self writeToLog:@"發現外圍設備..."]; //停止掃描 [self.centralManager stopScan]; //連接外圍設備 if (peripheral) { //添加保存外圍設備,注意如果這裏不保存外圍設備(或者說peripheral沒有一個強引用,無法到達連接成功(或失敗)的代理方法,因爲在此方法調用完就會被銷燬 if(![self.peripherals containsObject:peripheral]){ [self.peripherals addObject:peripheral]; } NSLog(@"開始連接外圍設備..."); [self writeToLog:@"開始連接外圍設備..."]; [self.centralManager connectPeripheral:peripheral options:nil]; } } //連接到外圍設備 -(void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{ NSLog(@"連接外圍設備成功!"); [self writeToLog:@"連接外圍設備成功!"]; //設置外圍設備的代理爲當前視圖控制器 peripheral.delegate=self; //外圍設備開始尋找服務 [peripheral discoverServices:@[[CBUUID UUIDWithString:kServiceUUID]]]; } //連接外圍設備失敗 -(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{ NSLog(@"連接外圍設備失敗!"); [self writeToLog:@"連接外圍設備失敗!"]; } #pragma mark - CBPeripheral 代理方法 //外圍設備尋找到服務後 -(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{ NSLog(@"已發現可用服務..."); [self writeToLog:@"已發現可用服務..."]; if(error){ NSLog(@"外圍設備尋找服務過程中發生錯誤,錯誤信息:%@",error.localizedDescription); [self writeToLog:[NSString stringWithFormat:@"外圍設備尋找服務過程中發生錯誤,錯誤信息:%@",error.localizedDescription]]; } //遍歷查找到的服務 CBUUID *serviceUUID=[CBUUID UUIDWithString:kServiceUUID]; CBUUID *characteristicUUID=[CBUUID UUIDWithString:kCharacteristicUUID]; for (CBService *service in peripheral.services) { if([service.UUID isEqual:serviceUUID]){ //外圍設備查找指定服務中的特徵 [peripheral discoverCharacteristics:@[characteristicUUID] forService:service]; } } } //外圍設備尋找到特徵後 -(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{ NSLog(@"已發現可用特徵..."); [self writeToLog:@"已發現可用特徵..."]; if (error) { NSLog(@"外圍設備尋找特徵過程中發生錯誤,錯誤信息:%@",error.localizedDescription); [self writeToLog:[NSString stringWithFormat:@"外圍設備尋找特徵過程中發生錯誤,錯誤信息:%@",error.localizedDescription]]; } //遍歷服務中的特徵 CBUUID *serviceUUID=[CBUUID UUIDWithString:kServiceUUID]; CBUUID *characteristicUUID=[CBUUID UUIDWithString:kCharacteristicUUID]; if ([service.UUID isEqual:serviceUUID]) { for (CBCharacteristic *characteristic in service.characteristics) { if ([characteristic.UUID isEqual:characteristicUUID]) { //情景一:通知 /*找到特徵後設置外圍設備爲已通知狀態(訂閱特徵): *1.調用此方法會觸發代理方法:-(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error *2.調用此方法會觸發外圍設備的訂閱代理方法 */ [peripheral setNotifyValue:YES forCharacteristic:characteristic]; //情景二:讀取 // [peripheral readValueForCharacteristic:characteristic]; // if(characteristic.value){ // NSString *value=[[NSString alloc]initWithData:characteristic.value encoding:NSUTF8StringEncoding]; // NSLog(@"讀取到特徵值:%@",value); // } } } } } //特徵值被更新後 -(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{ NSLog(@"收到特徵更新通知..."); [self writeToLog:@"收到特徵更新通知..."]; if (error) { NSLog(@"更新通知狀態時發生錯誤,錯誤信息:%@",error.localizedDescription); } //給特徵值設置新的值 CBUUID *characteristicUUID=[CBUUID UUIDWithString:kCharacteristicUUID]; if ([characteristic.UUID isEqual:characteristicUUID]) { if (characteristic.isNotifying) { if (characteristic.properties==CBCharacteristicPropertyNotify) { NSLog(@"已訂閱特徵通知."); [self writeToLog:@"已訂閱特徵通知."]; return; }else if (characteristic.properties ==CBCharacteristicPropertyRead) { //從外圍設備讀取新值,調用此方法會觸發代理方法:-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error [peripheral readValueForCharacteristic:characteristic]; } }else{ NSLog(@"停止已停止."); [self writeToLog:@"停止已停止."]; //取消連接 [self.centralManager cancelPeripheralConnection:peripheral]; } } } //更新特徵值後(調用readValueForCharacteristic:方法或者外圍設備在訂閱後更新特徵值都會調用此代理方法) -(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{ if (error) { NSLog(@"更新特徵值時發生錯誤,錯誤信息:%@",error.localizedDescription); [self writeToLog:[NSString stringWithFormat:@"更新特徵值時發生錯誤,錯誤信息:%@",error.localizedDescription]]; return; } if (characteristic.value) { NSString *value=[[NSString alloc]initWithData:characteristic.value encoding:NSUTF8StringEncoding]; NSLog(@"讀取到特徵值:%@",value); [self writeToLog:[NSString stringWithFormat:@"讀取到特徵值:%@",value]]; }else{ NSLog(@"未發現特徵值."); [self writeToLog:@"未發現特徵值."]; } } #pragma mark - 屬性 -(NSMutableArray *)peripherals{ if(!_peripherals){ _peripherals=[NSMutableArray array]; } return _peripherals; } #pragma mark - 私有方法 /** * 記錄日誌 * * @param info 日誌信息 */ -(void)writeToLog:(NSString *)info{ self.log.text=[NSString stringWithFormat:@"%@\r\n%@",self.log.text,info]; } @end
上面程序運行的流程圖如下:
有了上面兩個程序就可以分別運行在兩臺支持BLE的iOS設備上,當兩個應用建立連接後,一旦外圍設備更新特徵之後,中央設備就可以立即獲取到更新後的值。需要強調的是使用CoreBluetooth開發的應用不僅僅可以和其他iOS設備進行藍牙通信,還可以同其他第三方遵循BLE規範的設備進行藍牙通訊,這裏就不再贅述。
注意:本節部分圖片來自於互聯網,版權歸原作者所有。