iOS開發之藍芽4.0技術完美實現
前言
前端時間,同學在做專案過程中遇到關於藍芽方面的問題,今天我就給大家進行詳細的進行講解下藍芽在iOS開發中的具體實現.在介紹藍芽前,大家要搞清楚什麼是藍芽?
什麼是藍芽?
隨著藍芽低功耗技術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方法,在此方法可以獲得傳送資料並處理。下面是程式程式碼:
#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真機上作為發現節點,並且實現兩個節點的圖片互傳。
首先看一下作為廣播節點的程式:
介面:
點選“開始廣播”來發布服務,一旦有節點連線此服務就可以使用“選擇照片”來從照片庫中選取一張圖片併發送到所有已連線節點。
程式:
#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
再看一下作為發現節點的程式:
介面:
點選“查詢裝置”瀏覽可用服務,點選服務建立連線;一旦建立了連線之後就可以點選“選擇照片”會從照片庫中選擇一張圖片併發送給已連線的節點。
程式:
#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.外圍裝置
建立一個外圍裝置通常分為以下幾個步驟:
1. 建立外圍裝置CBPeripheralManager物件並指定代理。
2. 建立特徵CBCharacteristic、服務CBSerivce並新增到外圍裝置
3. 外圍裝置開始廣播服務(startAdvertisting:)。
4. 和中央裝置CBCentral進行互動。
下面是簡單的程式示例,程式有兩個按鈕“啟動”和“更新”,點選啟動按鈕則建立外圍裝置、新增服務和特徵並開始廣播,一旦發現有中央裝置連線並訂閱了此服務的特徵則通過更新按鈕更新特徵資料,此時已訂閱的中央裝置就會收到更新資料。
#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 *[email protected]{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並指定代理。
- 掃描外圍裝置,一般發現可用外圍裝置則連線並儲存外圍裝置。
- 查詢外圍裝置服務和特徵,查詢到可用特徵則讀取特徵資料。
下面是一個簡單的中央伺服器端實現,點選“啟動”按鈕則開始掃描周圍的外圍裝置,一旦發現了可用的外圍裝置則建立連線並設定外圍裝置的代理,之後開始查詢其服務和特徵。一旦外圍裝置的特徵值做了更新,則可以在代理方法中讀取更新後的特徵值。
#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];