iOS之藍牙開發(fā)

前言#

什么是藍牙?####

  • 隨著藍牙低功耗技術BLE(Bluetooth Low Energy)的發(fā)展,藍牙技術正在一步步成熟今阳,如今的大部分移動設備都配備有藍牙4.0麻掸,相比之前的藍牙技術耗電量大大降低织狐。從iOS的發(fā)展史也不難看出蘋果目前對藍牙技術也是越來越關注骗绕,例如蘋果于2013年9月發(fā)布的iOS7就配備了iBeacon技術,這項技術完全基于藍牙傳輸贮泞。但是眾所周知蘋果的設備對于權限要求也是比較高的楞慈,因此在iOS中并不能像Android一樣隨意使用藍牙進行文件傳輸(除非你已經(jīng)越獄)。知道什么是藍牙之后,那么在iOS中進行藍牙傳輸應用開發(fā)常用的框架有哪幾種呢?

藍牙在開發(fā)中的框架有哪些?####

  • GameKit.framework:iOS7之前的藍牙通訊框架啃擦,從iOS7開始過期囊蓝,但是目前多數(shù)應用還是基于此框架。
  • MultipeerConnectivity.framework:iOS7開始引入的新的藍牙通訊開發(fā)框架令蛉,用于取代GameKit慎颗。
  • CoreBluetooth.framework:功能強大的藍牙開發(fā)框架,要求設備必須支持藍牙4.0言询。

藍牙在開發(fā)中的框架優(yōu)缺點?####

  • 現(xiàn)在就給大家來總結下這三種框架的優(yōu)缺點.

前兩個框架使用起來比較簡單,但是缺點也比較明顯:僅僅支持iOS設備傲宜,傳輸內(nèi)容僅限于沙盒或者照片庫中用戶選擇的文件运杭,并且第一個框架只能在同一個應用之間進行傳輸(一個iOS設備安裝應用A,另一個iOS設備上安裝應用B是無法傳輸?shù)模┖洹.斎籆oreBluetooth就擺脫了這些束縛辆憔,它不再局限于iOS設備之間進行傳輸,你可以通過iOS設備向Android、Windows Phone以及其他安裝有藍牙4.0芯片的智能設備傳輸虱咧,因此也是目前智能家居熊榛、無線支付等熱門智能設備所推崇的技術。

藍牙框架之GameKit框架####

  • 其實從名稱來看這個框架并不是專門為了支持藍牙傳輸而設計的腕巡,它是為游戲設計的玄坦。而很多游戲中會用到基于藍牙的點對點信息傳輸,因此這個框架中集成了藍牙傳輸模塊绘沉。前面也說了這個框架本身有很多限制煎楣,但是在iOS7之前的很多藍牙傳輸都是基于此框架的,所以有必要對它進行了解车伞。GameKit中的藍牙使用設計很簡單择懂,并沒有給開發(fā)者留有太多的復雜接口,而多數(shù)連接細節(jié)開發(fā)者是不需要關注的另玖。GameKit中提供了兩個關鍵類來操作藍牙連接:

  • #####GKPeerPickerController:##### 
    

藍牙查找困曙、連接用的視圖控制器,通常情況下應用程序A打開后會調(diào)用此控制器的show方法來展示一個藍牙查找的視圖谦去,一旦發(fā)現(xiàn)了另一個同樣在查找藍牙連接的客戶客戶端B就會出現(xiàn)在視圖列表中慷丽,此時如果用戶點擊連接B,B客戶端就會詢問用戶是否允許A連接B哪轿,如果允許后A和B之間建立一個藍牙連接盈魁。

  • #####GKSession#####
    

連接會話,主要用于發(fā)送和接受傳輸數(shù)據(jù)窃诉。一旦A和B建立連接GKPeerPickerController的代理方法會將A杨耙、B兩者建立的會話(GKSession)對象傳遞給開發(fā)人員,開發(fā)人員拿到此對象可以發(fā)送和接收數(shù)據(jù)飘痛。

  • 其實理解了上面兩個類之后珊膜,使用起來就比較簡單了,下面就以一個圖片發(fā)送程序來演示GameKit中藍牙的使用宣脉。此程序一個客戶端運行在模擬器上作為客戶端A车柠,另一個運行在iPhone真機上作為客戶端B(注意A、B必須運行同一個程序塑猖,GameKit藍牙開發(fā)是不支持兩個不同的應用傳輸數(shù)據(jù)的)竹祷。

  • 兩個程序運行之后均調(diào)用GKPeerPickerController來發(fā)現(xiàn)周圍藍牙設備,一旦A發(fā)現(xiàn)了B之后就開始連接B羊苟,然后iOS會詢問用戶是否接受連接塑陵,一旦接受之后就會調(diào)用GKPeerPickerController的代理方法:
-(void)peerPickerController:(GKPeerPickerController )picker didConnectPeer:(NSString )peerID toSession:(GKSession *)session

在此方法中可以獲得連接的設備id(peerID)和連接會話(session);此時可以設置會話的數(shù)據(jù)接收句柄(相當于一個代理)并保存會話以便發(fā)送數(shù)據(jù)時使用蜡励;一旦一端(假設是A)調(diào)用會話的

sendDataToAllPeers: withDataMode: error:

方法發(fā)送數(shù)據(jù)令花,此時另一端(假設是B)就會調(diào)用句柄的

  - (void) receiveData:(NSData )data fromPeer:(NSString )peer   inSession: (GKSession )session context:(void )context

方法阻桅,在此方法可以獲得發(fā)送數(shù)據(jù)并處理。下面是程序代碼:

   #import "ViewController.h"
   #import <GameKit/GameKit.h>
   @interface ViewController ()

   @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(@"發(fā)送圖片過程中發(fā)生錯誤兼都,錯誤信    息:%@",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);
      //設置數(shù)據(jù)接收處理句柄嫂沉,相當于代理,一旦數(shù)據(jù)接收完成調(diào)用它的-receiveData:fromPeer:inSession:context:方法處理數(shù)據(jù)
      [self.session setDataReceiveHandler:self withContext:nil];

      [picker dismiss];//一旦連接成功關閉窗口
}
  #pragma mark - 藍牙數(shù)據(jù)接收方法
  - (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(@"數(shù)據(jù)發(fā)送成功扮碧!");
}

  #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
  • 運行效果(左側(cè)是真機趟章,右側(cè)是模擬器,程序演示了兩個客戶端互發(fā)圖片的場景:首先是模擬器發(fā)送圖片給真機芬萍,然后真機發(fā)送圖片給模擬器)


    2016050211201014.gif

藍牙框架之MultipeerConnectivity框架

  • 前面已經(jīng)說了GameKit相關的藍牙操作類從iOS7已經(jīng)全部過期尤揣,蘋果官方推薦使用MultipeerConnectivity代替。但是應該了解柬祠,MultipeerConnectivity.framework并不僅僅支持藍牙連接北戏,準確的說它是一種支持Wi-Fi網(wǎng)絡、P2P Wi-Fi以及藍牙個人局域網(wǎng)的通信框架漫蛔,它屏蔽了具體的連接技術嗜愈,讓開發(fā)人員有統(tǒng)一的接口編程方法。通過MultipeerConnectivity連接的節(jié)點之間可以安全的傳遞信息流或者其它文件資源而不必通過網(wǎng)絡服務莽龟。此外使用MultipeerConnectivity進行近場通信也不再局限于同一個應用之間傳輸蠕嫁,而是可以在不同的應用之間進行數(shù)據(jù)傳輸(當然如果有必要的話你仍然可以選擇在一個應用程序之間傳輸)。

  • 要了解MultipeerConnectivity的使用必須要清楚一個概念:廣播(Advertisting)和發(fā)現(xiàn)(Disconvering)毯盈,這很類似于一種Client-Server模式剃毒。假設有兩臺設備A、B搂赋,B作為廣播去發(fā)送自身服務赘阀,A作為發(fā)現(xiàn)的客戶端。一旦A發(fā)現(xiàn)了B就試圖建立連接脑奠,經(jīng)過B同意二者建立連接就可以相互發(fā)送數(shù)據(jù)基公。在使用GameKit框架時,A和B既作為廣播又作為發(fā)現(xiàn)宋欺,當然這種情況在MultipeerConnectivity中也很常見轰豆。

A.廣播
  • 無論是作為服務器端去廣播還是作為客戶端去發(fā)現(xiàn)廣播服務,那么兩個(或更多)不同的設備之間必須要有區(qū)分齿诞,通常情況下使用MCPeerID對象來區(qū)分一臺設備酸休,在這個設備中可以指定顯示給對方查看的名稱(display name)。另外不管是哪一方祷杈,還必須建立一個會話MCSession用于發(fā)送和接受數(shù)據(jù)斑司。通常情況下會在會話的
-(void)session:(MCSession )session peer:(MCPeerID )peerID didChangeState:(MCSessionState)state

代理方法中跟蹤會話狀態(tài)(已連接、正在連接吠式、未連接);在會話的

-(void)session:(MCSession )session didReceiveData:(NSData )data fromPeer:(MCPeerID *)peerID

代理方法中接收數(shù)據(jù);同時還會調(diào)用會話的

-(void)sendData: toPeers:withMode: error:

方法去發(fā)送數(shù)據(jù)陡厘。

  • 廣播作為一個服務器去發(fā)布自身服務,供周邊設備發(fā)現(xiàn)連接特占。在MultipeerConnectivity中使用MCAdvertiserAssistant來表示一個廣播糙置,通常創(chuàng)建廣播時指定一個會話MCSession對象將廣播服務和會話關聯(lián)起來。一旦調(diào)用廣播的start方法周邊的設備就可以發(fā)現(xiàn)該廣播并可以連接到此服務是目。在MCSession的代理方法中可以隨時更新連接狀態(tài)谤饭,一旦建立了連接之后就可以通過MCSession的connectedPeers獲得已經(jīng)連接的設備。

B.發(fā)現(xiàn)
  • 前面已經(jīng)說過作為發(fā)現(xiàn)的客戶端同樣需要一個MCPeerID來標志一個客戶端懊纳,同時會擁有一個MCSession來監(jiān)聽連接狀態(tài)并發(fā)送揉抵、接受數(shù)據(jù)。除此之外嗤疯,要發(fā)現(xiàn)廣播服務冤今,客戶端就必須要隨時查找服務來連接,在MultipeerConnectivity中提供了一個控制器MCBrowserViewController來展示可連接和已連接的設備(這類似于GameKit中的GKPeerPickerController)茂缚,當然如果想要自己定制一個界面來展示設備連接的情況你可以選擇自己開發(fā)一套UI界面戏罢。一旦通過MCBroserViewController選擇一個節(jié)點去連接,那么作為廣播的節(jié)點就會收到通知脚囊,詢問用戶是否允許連接龟糕。由于初始化MCBrowserViewController的過程已經(jīng)指定了會話MCSession,所以連接過程中會隨時更新會話狀態(tài)悔耘,一旦建立了連接讲岁,就可以通過會話的connected屬性獲得已連接設備并且可以使用會話發(fā)送、接受數(shù)據(jù)衬以。

  • 下面用兩個不同的應用程序來演示使用MultipeerConnectivity的使用過程缓艳,其中一個應用運行在模擬器中作為廣播節(jié)點,另一個運行在iPhone真機上作為發(fā)現(xiàn)節(jié)點泄鹏,并且實現(xiàn)兩個節(jié)點的圖片互傳郎任。

  • 首先看一下作為廣播節(jié)點的程序:


    廣播.png
  • 界面:
    點擊“開始廣播”來發(fā)布服務,一旦有節(jié)點連接此服務就可以使用“選擇照片”來從照片庫中選取一張圖片并發(fā)送到所有已連接節(jié)點备籽。

程序:
#import "ViewController.h"
#import 

@interface ViewController ()
@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];
    //創(chuàng)建節(jié)點舶治,displayName是用于提供給周邊設備查看和區(qū)分此服務的
    MCPeerID *peerID=[[MCPeerID alloc]initWithDisplayName:@"KenshinCui_Advertiser"];
    _session=[[MCSession alloc]initWithPeer:peerID];
    _session.delegate=self;
    //創(chuàng)建廣播
    _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;
    }
}
//接收數(shù)據(jù)
-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{
    NSLog(@"開始接收數(shù)據(jù)...");
    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];
    //發(fā)送數(shù)據(jù)給所有已連接設備
    NSError *error=nil;
    [self.session sendData:UIImagePNGRepresentation(image) toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error];
    NSLog(@"開始發(fā)送數(shù)據(jù)...");
    if (error) {
        NSLog(@"發(fā)送數(shù)據(jù)過程中發(fā)生錯誤,錯誤信息:%@",error.localizedDescription);
    }
    [self.imagePickerController dismissViewControllerAnimated:YES completion:nil];
}
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{
    [self.imagePickerController dismissViewControllerAnimated:YES completion:nil];
}
@end
  • 界面:
    點擊“查找設備”瀏覽可用服務车猬,點擊服務建立連接霉猛;一旦建立了連接之后就可以點擊“選擇照片”會從照片庫中選擇一張圖片并發(fā)送給已連接的節(jié)點。


    發(fā)現(xiàn).png
  • 再看一下作為發(fā)現(xiàn)節(jié)點的程序:
    
  #import "ViewController.h"
  #import 

  @interface ViewController ()
  @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];
    //創(chuàng)建節(jié)點
    MCPeerID *peerID=[[MCPeerID alloc]initWithDisplayName:@"KenshinCui"];
    //創(chuàng)建會話
    _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;
    }
}
//接收數(shù)據(jù)
-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{
    NSLog(@"開始接收數(shù)據(jù)...");
    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];
    //發(fā)送數(shù)據(jù)給所有已連接設備
    NSError *error=nil;
    [self.session sendData:UIImagePNGRepresentation(image) toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error];
    NSLog(@"開始發(fā)送數(shù)據(jù)...");
    if (error) {
        NSLog(@"發(fā)送數(shù)據(jù)過程中發(fā)生錯誤珠闰,錯誤信息:%@",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”那么發(fā)現(xiàn)節(jié)點只有在MCBrowserViewController中指定為“cmj-photo”才能發(fā)現(xiàn)此服務。

  • 運行效果:


    廣播發(fā)現(xiàn).gif

藍牙框架之CoreBluetooth框架

  • 無論是GameKit還是MultipeerConnectivity裸影,都只能在iOS設備之間進行數(shù)據(jù)傳輸挣轨,這就大大降低了藍牙的使用范圍,于是從iOS6開始蘋果推出了CoreBluetooth.framework,這個框架最大的特點就是完全基于BLE4.0標準并且支持非iOS設備轩猩。當前BLE應用相當廣泛卷扮,不再僅僅是兩個設備之間的數(shù)據(jù)傳輸,它還有很多其他應用市場均践,例如室內(nèi)定位晤锹、無線支付、智能家居等等彤委,這也使得CoreBluetooth成為當前最熱門的藍牙技術鞭铆。

  • CoreBluetooth設計同樣也是類似于客戶端-服務器端的設計,作為服務器端的設備稱為外圍設備(Peripheral)葫慎,作為客戶端的設備叫做中央設備(Central)衔彻,CoreBlueTooth整個框架就是基于這兩個概念來設計的.


    CoreBluetooth.png
  • 外圍設備和中央設備在CoreBluetooth中使用CBPeripheralManager和CBCentralManager表示。

  • CBPeripheralManager:外圍設備通常用于發(fā)布服務偷办、生成數(shù)據(jù)艰额、保存數(shù)據(jù)。外圍設備發(fā)布并廣播服務椒涯,告訴周圍的中央設備它的可用服務和特征柄沮。

  • CBCentralManager:中央設備使用外圍設備的數(shù)據(jù)。中央設備掃描到外圍設備后會就會試圖建立連接废岂,一旦連接成功就可以使用這些服務和特征祖搓。
    
  • 一臺iOS設備(注意iPhone4以下設備不支持BLE,另外iOS7.0湖苞、8.0模擬器也無法模擬BLE)既可以作為外圍設備又可以作為中央設備拯欧,但是不能同時即是外圍設備又是中央設備,同時注意建立連接的過程不需要用戶手動選擇允許财骨,這一點和前面兩個框架是不同的镐作,這主要是因為BLE應用場景不再局限于兩臺設備之間資源共享了。

A.外圍設備
創(chuàng)建一個外圍設備通常分為以下幾個步驟:
  1. 創(chuàng)建外圍設備CBPeripheralManager對象并指定代理隆箩。
  2. 創(chuàng)建特征CBCharacteristic该贾、服務CBSerivce并添加到外圍設備
  3. 外圍設備開始廣播服務(startAdvertisting:)剧董。
  4. 和中央設備CBCentral進行交互汹押。
  • 下面是簡單的程序示例,程序有兩個按鈕“啟動”和“更新”错览,點擊啟動按鈕則創(chuàng)建外圍設備、添加服務和特征并開始廣播逞力,一旦發(fā)現(xiàn)有中央設備連接并訂閱了此服務的特征則通過更新按鈕更新特征數(shù)據(jù)曙寡,此時已訂閱的中央設備就會收到更新數(shù)據(jù)。


    外圍設備.png
  • 程序
#import "ViewController.h"
#import 
#define kPeripheralName @"Kenshin Cui's Device" //外圍設備名稱
#define kServiceUUID @"C4FB2349-72FE-4CA2-94D6-1F3CB16331EE" //服務的UUID
#define kCharacteristicUUID @"6A3E4B28-522D-4B3B-82A9-D5E2004534FC" //特征的UUID

@interface ViewController ()

@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事件
//創(chuàng)建外圍設備
- (IBAction)startClick:(UIBarButtonItem *)sender {
    _peripheralManager=[[CBPeripheralManager alloc]initWithDelegate:self queue:nil];
}
//更新數(shù)據(jù)
- (IBAction)transferClick:(UIBarButtonItem *)sender {
    [self updateCharacteristicValue];
}

#pragma mark - CBPeripheralManager代理方法
//外圍設備狀態(tài)發(fā)生變化后調(diào)用
-(void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral{
    switch (peripheral.state) {
        case CBPeripheralManagerStatePoweredOn:
            NSLog(@"BLE已打開.");
            [self writeToLog:@"BLE已打開."];
            //添加服務
            [self setupService];
            break;

        default:
            NSLog(@"此設備不支持BLE或未打開藍牙功能寇荧,無法作為外圍設備.");
            [self writeToLog:@"此設備不支持BLE或未打開藍牙功能卵皂,無法作為外圍設備."];
            break;
    }
}
//外圍設備添加服務后調(diào)用
-(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(@"啟動廣播過程中發(fā)生錯誤,錯誤信息:%@",error.localizedDescription);
        [self writeToLog:[NSString stringWithFormat:@"啟動廣播過程中發(fā)生錯誤殴玛,錯誤信息:%@",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]];
    //發(fā)現(xiàn)中心設備并存儲
    if (![self.centralM containsObject:central]) {
        [self.centralM addObject:central];
    }
    /*中心設備訂閱成功后外圍設備可以更新特征值發(fā)送到中心設備,一旦更新特征值將會觸發(fā)中心設備的代理方法:
     -(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 - 私有方法
//創(chuàng)建特征捅膘、服務并添加服務到外圍設備
-(void)setupService{
    /*1.創(chuàng)建特征*/
    //創(chuàng)建特征的UUID對象
    CBUUID *characteristicUUID=[CBUUID UUIDWithString:kCharacteristicUUID];
    //特征值
//    NSString *valueStr=kPeripheralName;
//    NSData *value=[valueStr dataUsingEncoding:NSUTF8StringEncoding];
    //創(chuàng)建特征
    /** 參數(shù)
     * 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;

    /*創(chuàng)建服務并且設置特征*/
    //創(chuàng)建服務UUID對象
    CBUUID *serviceUUID=[CBUUID UUIDWithString:kServiceUUID];
    //創(chuàng)建服務
    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
  • 流程如下(圖中藍色代表外圍設備操作,綠色部分表示中央設備操作):


    流程圖.jpeg
B.中央設備
  • 中央設備的創(chuàng)建一般可以分為如下幾個步驟:

  • 創(chuàng)建中央設備管理對象CBCentralManager并指定代理凡壤。 掃描外圍設備署尤,一般發(fā)現(xiàn)可用外圍設備則連接并保存外圍設備。 查找外圍設備服務和特征亚侠,查找到可用特征則讀取特征數(shù)據(jù)曹体。

  • 下面是一個簡單的中央服務器端實現(xiàn),點擊“啟動”按鈕則開始掃描周圍的外圍設備硝烂,一旦發(fā)現(xiàn)了可用的外圍設備則建立連接并設置外圍設備的代理箕别,之后開始查找其服務和特征。一旦外圍設備的特征值做了更新滞谢,則可以在代理方法中讀取更新后的特征值串稀。


    中心服務器.png
  • 程序

#import "ViewController.h"
#import 
#define kServiceUUID @"C4FB2349-72FE-4CA2-94D6-1F3CB16331EE" //服務的UUID
#define kCharacteristicUUID @"6A3E4B28-522D-4B3B-82A9-D5E2004534FC" //特征的UUID

@interface ViewController ()

@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 {
    //創(chuàng)建中心設備管理器并設置當前控制器視圖為代理
    _centralManager=[[CBCentralManager alloc]initWithDelegate:self queue:nil];
}

#pragma mark - CBCentralManager代理方法
//中心服務器狀態(tài)更新后
-(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;
    }
}
/**
 *  發(fā)現(xiàn)外圍設備
 *
 *  @param central           中心設備
 *  @param peripheral        外圍設備
 *  @param advertisementData 特征數(shù)據(jù)
 *  @param RSSI              信號質(zhì)量(信號強度)
 */
-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{
    NSLog(@"發(fā)現(xiàn)外圍設備...");
    [self writeToLog:@"發(fā)現(xiàn)外圍設備..."];
    //停止掃描
    [self.centralManager stopScan];
    //連接外圍設備
    if (peripheral) {
        //添加保存外圍設備母截,注意如果這里不保存外圍設備(或者說peripheral沒有一個強引用,無法到達連接成功(或失旈辖獭)的代理方法清寇,因為在此方法調(diào)用完就會被銷毀
        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(@"已發(fā)現(xiàn)可用服務...");
    [self writeToLog:@"已發(fā)現(xiàn)可用服務..."];
    if(error){
        NSLog(@"外圍設備尋找服務過程中發(fā)生錯誤,錯誤信息:%@",error.localizedDescription);
        [self writeToLog:[NSString stringWithFormat:@"外圍設備尋找服務過程中發(fā)生錯誤颤陶,錯誤信息:%@",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(@"已發(fā)現(xiàn)可用特征...");
    [self writeToLog:@"已發(fā)現(xiàn)可用特征..."];
    if (error) {
        NSLog(@"外圍設備尋找特征過程中發(fā)生錯誤颗管,錯誤信息:%@",error.localizedDescription);
        [self writeToLog:[NSString stringWithFormat:@"外圍設備尋找特征過程中發(fā)生錯誤,錯誤信息:%@",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]) {
                //情景一:通知
                /*找到特征后設置外圍設備為已通知狀態(tài)(訂閱特征):
                 *1.調(diào)用此方法會觸發(fā)代理方法:-(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
                 *2.調(diào)用此方法會觸發(fā)外圍設備的訂閱代理方法
                 */
                [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(@"更新通知狀態(tài)時發(fā)生錯誤滓走,錯誤信息:%@",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) {
                //從外圍設備讀取新值,調(diào)用此方法會觸發(fā)代理方法:-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
                [peripheral readValueForCharacteristic:characteristic];
            }

        }else{
            NSLog(@"停止已停止.");
            [self writeToLog:@"停止已停止."];
            //取消連接
            [self.centralManager cancelPeripheralConnection:peripheral];
        }
    }
}
//更新特征值后(調(diào)用readValueForCharacteristic:方法或者外圍設備在訂閱后更新特征值都會調(diào)用此代理方法)
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
    if (error) {
        NSLog(@"更新特征值時發(fā)生錯誤垦江,錯誤信息:%@",error.localizedDescription);
        [self writeToLog:[NSString stringWithFormat:@"更新特征值時發(fā)生錯誤,錯誤信息:%@",error.localizedDescription]];
        return;
    }
    if (characteristic.value) {
        NSString *value=[[NSString alloc]initWithData:characteristic.value encoding:NSUTF8StringEncoding];
        NSLog(@"讀取到特征值:%@",value);
        [self writeToLog:[NSString stringWithFormat:@"讀取到特征值:%@",value]];
    }else{
        NSLog(@"未發(fā)現(xiàn)特征值.");
        [self writeToLog:@"未發(fā)現(xiàn)特征值."];
    }
}

#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

上面程序運行的流程圖如下:

外圍設備.png
  • 有了上面兩個程序就可以分別運行在兩臺支持BLE的iOS設備上,當兩個應用建立連接后比吭,一旦外圍設備更新特征之后绽族,中央設備就可以立即獲取到更新后的值。需要強調(diào)的是使用CoreBluetooth開發(fā)的應用不僅僅可以和其他iOS設備進行藍牙通信衩藤,還可以同其他第三方遵循BLE規(guī)范的設備進行藍牙通訊吧慢。
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赏表,隨后出現(xiàn)的幾起案子检诗,更是在濱河造成了極大的恐慌,老刑警劉巖瓢剿,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逢慌,死亡現(xiàn)場離奇詭異,居然都是意外死亡间狂,警方通過查閱死者的電腦和手機攻泼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鉴象,“玉大人忙菠,你說我怎么就攤上這事》谋祝” “怎么了牛欢?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長淆游。 經(jīng)常有香客問我氢惋,道長,這世上最難降的妖魔是什么稽犁? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任焰望,我火速辦了婚禮,結果婚禮上已亥,老公的妹妹穿的比我還像新娘熊赖。我一直安慰自己,他們只是感情好虑椎,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布震鹉。 她就那樣靜靜地躺著,像睡著了一般捆姜。 火紅的嫁衣襯著肌膚如雪传趾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天泥技,我揣著相機與錄音浆兰,去河邊找鬼。 笑死,一個胖子當著我的面吹牛簸呈,可吹牛的內(nèi)容都是我干的榕订。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼蜕便,長吁一口氣:“原來是場噩夢啊……” “哼劫恒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起轿腺,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤两嘴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后族壳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溶诞,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年决侈,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片喧务。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡赖歌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出功茴,到底是詐尸還是另有隱情庐冯,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布坎穿,位于F島的核電站展父,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏玲昧。R本人自食惡果不足惜栖茉,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望孵延。 院中可真熱鬧吕漂,春花似錦、人聲如沸尘应。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽犬钢。三九已至苍鲜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間玷犹,已是汗流浹背混滔。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人遍坟。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓拳亿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親愿伴。 傳聞我的和親對象是個殘疾皇子肺魁,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 什么是藍牙? 隨著藍牙低功耗技術BLE(Bluetooth Low Energy)的發(fā)展,藍牙技術正在一步步成熟隔节,...
    一字碼閱讀 1,740評論 0 11
  • 一鹅经、項目背景 二、IOS 藍牙介紹 ios中藍牙有四個框架怎诫,其中兩個支持與外設連接瘾晃。一個是 ExternalAcc...
    Mr_Victory閱讀 5,836評論 27 69
  • 和往常的日子一樣,談不上悶熱和寒冷幻妓。即便已經(jīng)來到新的一天蹦误,其他房間都還有著不同的聲音,客廳昏黃的電燈一直沒關肉津。比起...
    199x20xx閱讀 217評論 0 0
  • 前些天强胰,聽聞高中同學酒駕撞車,發(fā)生重大車禍妹沙,搶救無效離開了偶洋,留下未滿月的兒子和剛分娩完的妻子,而他喝酒的原因距糖,是與...
    靜子sugar閱讀 850評論 0 0
  • 喂悍引,爺爺 喂恩脂,誰啊 爺,是我 哦趣斤,你好 聽到這东亦,我一愣,你好唬渗,為什么是你好典阵,不知道為什...
    日亻匕十閱讀 380評論 0 2