iOS蓝牙
在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方法,在此方法可以获得发送数据并处理。下面是程序代码:
详细代码:
1 #import "ViewController.h" 2 #import <GameKit/GameKit.h> 3 4 @interface ViewController ()<GKPeerPickerControllerDelegate,UIImagePickerControllerDelegate,UINavigationBarDelegate> 5 6 @property (weak, nonatomic) IBOutlet UIImageView *imageView; 7 8 @property (nonatomic,strong) GKSession * session;//蓝牙连接会话 9 10 @end 11 12 @implementation ViewController 13 14 - (void)viewDidLoad { 15 [super viewDidLoad]; 16 17 GKPeerPickerController * pearPickerController = [[GKPeerPickerController alloc] init]; 18 pearPickerController.delegate = self; 19 20 [pearPickerController show]; 21 } 22 23 24 #pragma mark - UI事件 25 //选择相片 26 - (IBAction)selectClick:(id)sender { 27 28 UIImagePickerController * imagePickerController = [[UIImagePickerController alloc] init]; 29 imagePickerController.delegate = self; 30 31 [self presentViewController:imagePickerController animated:YES completion:nil]; 32 } 33 34 //发送相片 35 - (IBAction)sendClick:(id)sender { 36 37 NSData * data = UIImagePNGRepresentation(self.imageView.image); 38 NSError * error = nil; 39 [self.session sendDataToAllPeers:data withDataMode:GKSendDataReliable error:&error]; 40 41 if (error) { 42 NSLog(@"发送图片过程中发生错误,错误信息:%@",error.localizedDescription); 43 } 44 } 45 46 #pragma mark - GKPeerPickerController delegate 47 /** 48 *连接到某个设备 49 * 50 *@param picker 蓝牙点对点连接控制器 51 *@param peerID 连接设备蓝牙传输ID 52 *@param session 连接会话 53 */ 54 - (void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session{ 55 56 NSLog(@"已连接客户端设备:%@.",peerID); 57 58 //设置数据接收处理句炳,相当于代理,一旦数据接收完成调用它的-receiveData:fromPeer:inSession:context:方法处理数据 59 [self.session setDataReceiveHandler:self withContext:nil]; 60 61 [picker dismiss];//一旦连接成功关闭窗口 62 } 63 64 #pragma mark - 蓝牙数据接收发放 65 - (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context{ 66 67 UIImage *image=[UIImage imageWithData:data]; 68 69 self.imageView.image=image; 70 71 UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); 72 73 NSLog(@"数据发送成功!"); 74 } 75 76 #pragma mark - UIImagePickerController delegate 77 - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info{ 78 79 self.imageView.image = [info objectForKey:UIImagePickerControllerOriginalImage]; 80 81 [self dismissViewControllerAnimated:YES completion:nil]; 82 } 83 84 - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{ 85 [self dismissViewControllerAnimated:YES completion:nil]; 86 }
效果图
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真机上作为发现节点,并且实现两个节点的图片互传。
下面用两个不同的应用程序来演示使用MultipeerConnectivity的使用过程,其中一个应用运行在模拟器中作为广播节点,另一个运行在iPhone真机上作为发现节点,并且实现两个节点的图片互传。
首先看一下作为广播节点的程序:
界面
点击“开始广播”来发布服务,一旦有节点连接此服务就可以使用“选择照片”来从照片库中选取一张图片并发送到所有已连接节点。
详细代码
1 #import "ViewController.h" 2 #import <MultipeerConnectivity/MultipeerConnectivity.h> 3 4 @interface ViewController ()<MCSessionDelegate,MCAdvertiserAssistantDelegate,UIImagePickerControllerDelegate,UINavigationControllerDelegate> 5 6 @property (nonatomic,strong) MCSession * session; 7 @property (nonatomic,strong) MCAdvertiserAssistant * advertiserAssistant; 8 @property (nonatomic,strong) UIImagePickerController * imagePickerController; 9 @property (weak, nonatomic) IBOutlet UIImageView *photo; 10 11 12 @end 13 14 @implementation ViewController 15 16 - (void)viewDidLoad { 17 [super viewDidLoad]; 18 19 //创建节点,displayName是用于提供给周围设备查看和区分此服务的 20 MCPeerID * peerID = [[MCPeerID alloc] initWithDisplayName:@"heyifu_Adertiser"]; 21 _session = [[MCSession alloc] initWithPeer:peerID]; 22 _session.delegate = self; 23 24 //创建广播 25 _advertiserAssistant = [[MCAdvertiserAssistant alloc] initWithServiceType:@"hyf-stream" discoveryInfo:nil session:_session]; 26 _advertiserAssistant.delegate = self; 27 28 } 29 30 #pragma mark - UI事件 31 - (IBAction)advertiserClick:(id)sender { 32 33 //开始广播 34 [self.advertiserAssistant start]; 35 } 36 37 - (IBAction)selectClick:(id)sender { 38 39 //选取照片 40 _imagePickerController = [[UIImagePickerController alloc] init]; 41 _imagePickerController.delegate = self; 42 [self presentViewController:_imagePickerController animated:YES completion:nil]; 43 } 44 45 #pragma mark - MCSession代理方法 46 - (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{ 47 48 NSLog(@"didChangeState"); 49 switch (state) { 50 case MCSessionStateConnected: 51 NSLog(@"连接成功"); 52 break; 53 case MCSessionStateConnecting: 54 NSLog(@"正在连接"); 55 break; 56 default: 57 NSLog(@"连接失败"); 58 break; 59 } 60 } 61 62 //接收数据 63 - (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{ 64 65 NSLog(@"开始接收数据"); 66 UIImage * image = [UIImage imageWithData:data]; 67 [self.photo setImage:image]; 68 69 //保存到相册 70 UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); 71 } 72 73 #pragma mark - MCAdvertiserAssistant代理方法 74 75 #pragma mark - UIImagePickerController代理方法 76 - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info{ 77 78 UIImage * image = [info objectForKey:UIImagePickerControllerOriginalImage]; 79 [self.photo setImage:image]; 80 81 //发送数据给所有已连接设备 82 NSError * error = nil; 83 [self.session sendData:UIImagePNGRepresentation(image) toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error]; 84 NSLog(@"开始发送数据..."); 85 if (error) { 86 NSLog(@"发送数据过程中发生错误,错误信息:%@",error.localizedDescription); 87 } 88 [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; 89 } 90 91 92 - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{ 93 [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; 94 }
再看一下作为发现节点的程序:
界面:
点击“查找设备”浏览可用服务,点击服务建立连接;一旦建立了连接之后就可以点击“选择照片”会从照片库中选择一张图片并发送给已连接的节点。
详细代码:
1 #import "ViewController.h" 2 #import <MultipeerConnectivity/MultipeerConnectivity.h> 3 4 @interface ViewController ()<MCSessionDelegate,MCBrowserViewControllerDelegate,UIImagePickerControllerDelegate,UINavigationControllerDelegate> 5 6 @property (nonatomic,strong) MCSession * session; 7 @property (nonatomic,strong) MCBrowserViewController * browserController; 8 @property (nonatomic,strong) UIImagePickerController * imagePickerController; 9 @property (weak, nonatomic) IBOutlet UIImageView *photo; 10 11 @end 12 13 @implementation ViewController 14 15 - (void)viewDidLoad { 16 [super viewDidLoad]; 17 18 //创建节电 19 MCPeerID * peerID = [[MCPeerID alloc] initWithDisplayName:@"heyifu_browser"]; 20 21 //创建会话 22 _session = [[MCSession alloc] initWithPeer:peerID]; 23 _session.delegate = self; 24 } 25 26 #pragma mark - UI事件 27 - (IBAction)browserClick:(id)sender { 28 29 //发现广播:hyf-stream 30 _browserController = [[MCBrowserViewController alloc] initWithServiceType:@"hyf-stream" session:self.session]; 31 _browserController.delegate = self; 32 33 [self presentViewController:_browserController animated:YES completion:nil]; 34 } 35 - (IBAction)selectClick:(id)sender { 36 37 //选择图片 38 _imagePickerController = [[UIImagePickerController alloc] init]; 39 _imagePickerController.delegate = self; 40 [self presentViewController:_imagePickerController animated:YES completion:nil]; 41 } 42 43 #pragma mark - MCBrowserViewController代理方法 44 - (void)browserViewControllerDidFinish:(MCBrowserViewController *)browserViewController{ 45 NSLog(@"已经连接"); 46 [self.browserController dismissViewControllerAnimated:YES completion:nil]; 47 } 48 49 - (void)browserViewControllerWasCancelled:(MCBrowserViewController *)browserViewController{ 50 NSLog(@"取消浏览"); 51 [self.browserController dismissViewControllerAnimated:YES completion:nil]; 52 } 53 54 55 #pragma mark - MCSeesion代理方法 56 - (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{ 57 NSLog(@"didChangeState"); 58 switch (state) { 59 case MCSessionStateConnected: 60 NSLog(@"连接成功"); 61 break; 62 case MCSessionStateConnecting: 63 NSLog(@"正在连接"); 64 break; 65 default: 66 NSLog(@"连接失败"); 67 break; 68 } 69 } 70 71 //接收数据 72 - (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{ 73 NSLog(@"开始接收数据..."); 74 UIImage * image = [UIImage imageWithData:data]; 75 [self.photo setImage:image]; 76 77 //保存到相册 78 UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); 79 } 80 81 #pragma mark - UIImagePickkerController代理发放 82 - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info{ 83 UIImage * image = [info objectForKey:UIImagePickerControllerOriginalImage]; 84 [self.photo setImage:image]; 85 86 //发送数据给所有已连接设备 87 NSError * error = nil; 88 [self.session sendData:UIImagePNGRepresentation(image) toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error]; 89 NSLog(@"发送数据给所有已连接设备"); 90 if (error) { 91 NSLog(@"发送数据过程中发生错误,错误信息:%@",error.localizedDescription); 92 } 93 [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; 94 } 95 96 - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{ 97 [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; 98 }
在两个程序中无论是MCBrowserViewController还是MCAdvertiserAssistant在初始化的时候都指定了一个服务类型“hyf-stream”,这是唯一标识一个服务类型的标记,可以按照官方的要求命名,应该尽可能表达服务的作用。需要特别指出的是,如果广播命名为“hyf-stream”那么发现节点只有在MCBrowserViewController中指定为“hyf-stream”才能发现此服务。
运行效果图不在上传,自己复制代码去测试。。。
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进行交互。
下面是简单的程序示例,程序有两个按钮“启动”和“更新”,点击启动按钮则创建外围设备、添加服务和特征并开始广播,一旦发现有中央设备连接并订阅了此服务的特征则通过更新按钮更新特征数据,此时已订阅的中央设备就会收到更新数据。
界面设计:
1 // 2 // ViewController.m 3 // CBPerpheraIManager 4 // 5 // Created by HO on 16/3/12. 6 // Copyright © 2016年 HO. All rights reserved. 7 // 8 9 #import "ViewController.h" 10 #import <CoreBluetooth/CoreBluetooth.h> 11 12 #define kPeripheralName @"Kenshin Cui's Device" //外围设备名称 13 #define kServiceUUID @"C4FB2349-72FE-4CA2-94D6-1F3CB16331EE" //服务的UUID 14 #define kCharacteristicUUID @"6A3E4B28-522D-4B3B-82A9-D5E2004534FC" //特征的UUID 15 16 17 @interface ViewController ()<CBPeripheralManagerDelegate> 18 19 @property (nonatomic,strong) CBPeripheralManager * peripheralManager;//外围设备管理器 20 @property (nonatomic,strong) NSMutableArray * centralM;//订阅此外围设备特征的中心设备 21 @property (nonatomic,strong) CBMutableCharacteristic * characteristicM;//特征 22 23 @property (weak, nonatomic) IBOutlet UITextView *log; 24 25 @end 26 27 @implementation ViewController 28 29 - (void)viewDidLoad { 30 [super viewDidLoad]; 31 32 } 33 34 35 #pragma mark - UI事件 36 //创建外围设备 37 - (IBAction)startClick:(id)sender { 38 _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil]; 39 } 40 41 //更新数据 42 - (IBAction)transferClick:(id)sender { 43 [self updateCharacteristicValue]; 44 } 45 46 #pragma mark - CBPeripheralManager代理方法 47 //外围设备状态发生变化后调用 48 - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral{ 49 switch (peripheral.state) { 50 case CBPeripheralManagerStatePoweredOn: 51 NSLog(@"BLE已打开."); 52 [self writeToLog:@"BLE已打开."]; 53 //添加服务 54 [self setupService]; 55 break; 56 57 default: 58 NSLog(@"此设备不支持BLE或未打开蓝牙功能,无法作为外围设备."); 59 [self writeToLog:@"此设备不支持BLE或未打开蓝牙功能,无法作为外围设备."]; 60 break; 61 } 62 } 63 64 65 //外围设备添加服务后调用 66 - (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error{ 67 if (error) { 68 NSLog(@"向外围设备添加服务失败,错误详情:%@",error.localizedDescription); 69 [self writeToLog:[NSString stringWithFormat:@"向外围设备添加服务失败,错误详情:%@",error.localizedDescription]]; 70 return; 71 } 72 73 //添加服务后开始广播 74 NSDictionary * dic = @{CBAdvertisementDataLocalNameKey:kPeripheralName};//广播设置 75 [self.peripheralManager startAdvertising:dic];//开始广播 76 NSLog(@"向外围设备添加了服务并开始广播..."); 77 [self writeToLog:@"向外围设备添加了服务并开始广播..."]; 78 } 79 80 - (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error{ 81 if (error) { 82 NSLog(@"启动广播过程中发生错误,错误信息:%@",error.localizedDescription); 83 [self writeToLog:[NSString stringWithFormat:@"启动广播过程中发生错误,错误信息:%@",error.localizedDescription]]; 84 return; 85 } 86 NSLog(@"启动广播..."); 87 [self writeToLog:@"启动广播..."]; 88 } 89 90 //订阅特征 91 - (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic{ 92 93 [self writeToLog:[NSString stringWithFormat:@"中心设备:%@ 已订阅特征:%@.",central.identifier.UUIDString,characteristic.UUID]]; 94 //发现中心设备,并存储 95 if (![self.centralM containsObject:central]) { 96 [self.centralM addObject:central]; 97 } 98 99 /*中心设备订阅成功后外围设备可以更新特征值发送到中心设备,一旦更新特征值将会触发中心设备的代理发放: 100 -(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error 101 */ 102 103 // [self updateCharacteristicValue]; 104 } 105 106 //取消订阅特征 107 - (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic{ 108 NSLog(@"didUnsubscribeFromCharacteristic"); 109 } 110 111 -(void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(CBATTRequest *)request{ 112 NSLog(@"didReceiveWriteRequests"); 113 } 114 -(void)peripheralManager:(CBPeripheralManager *)peripheral willRestoreState:(NSDictionary *)dict{ 115 NSLog(@"willRestoreState"); 116 } 117 118 119 #pragma mark -属性 120 -(NSMutableArray *)centralM{ 121 if (!_centralM) { 122 _centralM=[NSMutableArray array]; 123 } 124 return _centralM; 125 } 126 127 #pragma mark - 私有方法 128 //创建特征、服务并添加服务到外围设备 129 -(void)setupService{ 130 /*1.创建特征*/ 131 //创建特征的UUID对象 132 CBUUID *characteristicUUID=[CBUUID UUIDWithString:kCharacteristicUUID]; 133 //特征值 134 // NSString *valueStr=kPeripheralName; 135 // NSData *value=[valueStr dataUsingEncoding:NSUTF8StringEncoding]; 136 //创建特征 137 /** 参数 138 * uuid:特征标识 139 * properties:特征的属性,例如:可通知、可写、可读等 140 * value:特征值 141 * permissions:特征的权限 142 */ 143 CBMutableCharacteristic *characteristicM=[[CBMutableCharacteristic alloc]initWithType:characteristicUUID properties:CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable]; 144 self.characteristicM=characteristicM; 145 // CBMutableCharacteristic *characteristicM=[[CBMutableCharacteristic alloc]initWithType:characteristicUUID properties:CBCharacteristicPropertyRead value:nil permissions:CBAttributePermissionsReadable]; 146 // characteristicM.value=value; 147 148 /*创建服务并且设置特征*/ 149 //创建服务UUID对象 150 CBUUID *serviceUUID=[CBUUID UUIDWithString:kServiceUUID]; 151 //创建服务 152 CBMutableService *serviceM=[[CBMutableService alloc]initWithType:serviceUUID primary:YES]; 153 //设置服务的特征 154 [serviceM setCharacteristics:@[characteristicM]]; 155 156 157 /*将服务添加到外围设备*/ 158 [self.peripheralManager addService:serviceM]; 159 } 160 161 162 //更新特征值 163 -(void)updateCharacteristicValue{ 164 //特征值 165 NSString *valueStr=[NSString stringWithFormat:@"%@ --%@",kPeripheralName,[NSDate date]]; 166 NSData *value=[valueStr dataUsingEncoding:NSUTF8StringEncoding]; 167 //更新特征值 168 [self.peripheralManager updateValue:value forCharacteristic:self.characteristicM onSubscribedCentrals:nil]; 169 [self writeToLog:[NSString stringWithFormat:@"更新特征值:%@",valueStr]]; 170 } 171 172 /** 173 * 记录日志 174 * 175 * @param info 日志信息 176 */ 177 -(void)writeToLog:(NSString *)info{ 178 self.log.text=[NSString stringWithFormat:@"%@rn%@",self.log.text,info]; 179 } 180 181 - (void)didReceiveMemoryWarning { 182 [super didReceiveMemoryWarning]; 183 // Dispose of any resources that can be recreated. 184 } 185 186 @end
上面程序运行的流程如下(图中蓝色代表外围设备操作,绿色部分表示中央设备操作):
B.中央设备
中央设备的创建一般可以分为如下几个步骤:
- 创建中央设备管理对象CBCentralManager并指定代理。
- 扫描外围设备,一般发现可用外围设备则连接并保存外围设备。
- 查找外围设备服务和特征,查找到可用特征则读取特征数据。
下面是一个简单的中央服务器端实现,点击“启动”按钮则开始扫描周围的外围设备,一旦发现了可用的外围设备则建立连接并设置外围设备的代理,之后开始查找其服务和特征。一旦外围设备的特征值做了更新,则可以在代理方法中读取更新后的特征值。
界面设计:
程序设计:
1 // 2 // ViewController.m 3 // CBCentralManager 4 // 5 // Created by HO on 16/3/28. 6 // Copyright © 2016年 HO. All rights reserved. 7 // 8 9 #import "ViewController.h" 10 #import <CoreBluetooth/CoreBluetooth.h> 11 #define kServiceUUID @"C4FB2349-72FE-4CA2-94D6-1F3CB16331EE" //服务的UUID 12 #define kCharacteristicUUID @"6A3E4B28-522D-4B3B-82A9-D5E2004534FC" //特征的UUID 13 14 15 @interface ViewController ()<CBCentralManagerDelegate,CBPeripheralDelegate> 16 17 @property (nonatomic,strong) CBCentralManager * centralManager;//中心设备管理器 18 @property (nonatomic,strong) NSMutableArray * peripherals;//连接的外围设备 19 @property (weak, nonatomic) IBOutlet UITextView *log;//日志记录 20 21 22 @end 23 24 @implementation ViewController 25 26 - (void)viewDidLoad { 27 [super viewDidLoad]; 28 29 } 30 31 #pragma mark - UI事件 32 - (IBAction)startClick:(id)sender { 33 //创建中心设备管理器并设置当前控制器视图为代理 34 _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]; 35 } 36 37 38 #pragma mark - CBCentralManager代理方法 39 //中心服务器状态更新后 40 - (void)centralManagerDidUpdateState:(CBCentralManager *)central{ 41 switch (central.state) { 42 case CBPeripheralManagerStatePoweredOn: 43 NSLog(@"BLE已打开."); 44 [self writeToLog:@"BLE已打开."]; 45 //扫描外围设备 46 // [central scanForPeripheralsWithServices:@[CBUUID UUIDWithString:kServiceUUID] options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@YES}]; 47 [central scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@YES}]; 48 break; 49 50 default: 51 NSLog(@"此设备不支持BLE或未打开蓝牙功能,无法作为外围设备."); 52 [self writeToLog:@"此设备不支持BLE或未打开蓝牙功能,无法作为外围设备."]; 53 break; 54 } 55 } 56 57 /** 58 * 发现外围设备 59 * 60 * @param central 中心设备 61 * @param peripheral 外围设置 62 * @param advertisementData 特征数据 63 * @param RSSI 信号质量(信号强度) 64 * 65 */ 66 - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI{ 67 NSLog(@"发现外围设备..."); 68 [self writeToLog:@"发现外围设备..."]; 69 //停止扫面 70 [self.centralManager stopScan]; 71 //连接外围设备 72 if (peripheral) { 73 //添加保存外围设备,注意如果这里不保存外围设备(或者说peripheral没有强引用,无法到达连接成功(或失败)的代理方法,因为在此方法调用完就回被销毁) 74 if (![self.peripherals containsObject:peripheral]) { 75 [self.peripherals addObject:peripheral]; 76 } 77 NSLog(@"开始连接外围设备..."); 78 [self writeToLog:@"开始连接外围设备..."]; 79 [self.centralManager connectPeripheral:peripheral options:nil]; 80 } 81 82 } 83 84 //连接到外围设备 85 - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(nonnull CBPeripheral *)peripheral{ 86 NSLog(@"连接外围设备成功!"); 87 [self writeToLog:@"连接外围设备成功!"]; 88 //设置外围设备的代理为当前视图控制器 89 peripheral.delegate = self; 90 //外围设备开始寻找服务 91 [peripheral discoverServices:@[[CBUUID UUIDWithString:kServiceUUID]]]; 92 93 } 94 95 //连接外围设备失败 96 - (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(nonnull CBPeripheral *)peripheral error:(nullable NSError *)error{ 97 NSLog(@"连接外围设备失败!"); 98 [self writeToLog:@"连接外围设备失败!"]; 99 } 100 101 102 #pragma mark - CBPeripheral 代理方法 103 //外围设备寻找到服务后 104 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error{ 105 NSLog(@"已发现可用服务..."); 106 [self writeToLog:@"已发现可用服务..."]; 107 if (error) { 108 NSLog(@"外围设备寻找服务过程中发生错误,错误信息:%@",error.localizedDescription); 109 [self writeToLog:[NSString stringWithFormat:@"外围设备寻找服务过程中发生错误,错误信息:%@",error.localizedDescription]]; 110 } 111 112 //遍历查找到的服务 113 CBUUID * serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; 114 CBUUID * characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID]; 115 for (CBService * service in peripheral.services) { 116 if ([service.UUID isEqual:serviceUUID]) { 117 //外围设备查找指定服务中的特征 118 [peripheral discoverCharacteristics:@[characteristicUUID] forService:service]; 119 } 120 } 121 } 122 123 124 //外围设备寻找到特征后 125 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(nonnull CBService *)service error:(nullable NSError *)error{ 126 NSLog(@"已发现可用特征..."); 127 [self writeToLog:@"已发现可用特征..."]; 128 if (error) { 129 NSLog(@"外围设备寻找特征过程中发生错误,错误信息:%@",error.localizedDescription); 130 [self writeToLog:[NSString stringWithFormat:@"外围设备寻找特征过程中发生错误,错误信息:%@",error.localizedDescription]]; 131 } 132 //遍历服务中的特征 133 CBUUID * serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; 134 CBUUID * characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID]; 135 if ([service.UUID isEqual:serviceUUID]) { 136 for (CBCharacteristic * characteristic in service.characteristics) { 137 if ([characteristic.UUID isEqual:characteristicUUID]) { 138 //情景一:通知 139 /*找到特征后设置外围设备为已通知状态(订阅特征): 140 *1.调用此方法会触发代理方法:-(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error 141 *2.调用此方法会触发外围设备的订阅代理方法 142 */ 143 [peripheral setNotifyValue:YES forCharacteristic:characteristic]; 144 145 //情景二:读取 146 // [peripheral readValueForCharacteristic:characteristic]; 147 // if (characteristic.value) { 148 // NSString * value = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding]; 149 // NSLog(@"读取到特征值:%@",value); 150 // } 151 } 152 } 153 } 154 } 155 156 //特征值被更新后 157 - (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error{ 158 NSLog(@"收到特征更新通知..."); 159 [self writeToLog:@"收到特征更新通知..."]; 160 if (error) { 161 NSLog(@"更新通知状态时发生错误,错误信息:%@",error.localizedDescription); 162 } 163 //给特征值设置新的值 164 CBUUID * characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID]; 165 if ([characteristic.UUID isEqual:characteristicUUID]) { 166 if (characteristic.isNotifying) { 167 if (characteristic.properties == CBCharacteristicPropertyNotify) { 168 NSLog(@"已订阅特征通知."); 169 [self writeToLog:@"已订阅特征通知."]; 170 return; 171 }else if (characteristic.properties == CBCharacteristicPropertyRead){ 172 //从外围设备读取新值,调用此方法会触发代理方法:-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error 173 [peripheral readValueForCharacteristic:characteristic]; 174 } 175 }else{ 176 NSLog(@"停止已停止."); 177 [self writeToLog:@"停止已停止."]; 178 //取消连接 179 [self.centralManager cancelPeripheralConnection:peripheral]; 180 } 181 } 182 } 183 184 //更新特征值后 (调用readValueForCharacteristic:方法或者外围设置在订阅新特征都会调用此代理方法) 185 - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error{ 186 if (error) { 187 NSLog(@"更新特征值时发生错误,错误信息:%@",error.localizedDescription); 188 [self writeToLog:[NSString stringWithFormat:@"更新特征值时发生错误,错误信息:%@",error.localizedDescription]]; 189 return; 190 } 191 if (characteristic.value) { 192 NSString * value = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding]; 193 NSLog(@"读取到特征值:%@",value); 194 [self writeToLog:[NSString stringWithFormat:@"读取到特征值:%@",value]]; 195 }else{ 196 NSLog(@"未发现特征值"); 197 [self writeToLog:@"未发现特征值"]; 198 } 199 } 200 201 #pragma mark - 属性 202 - (NSMutableArray *)peripherals{ 203 if (!_peripherals) { 204 _peripherals = [NSMutableArray array]; 205 } 206 return _peripherals; 207 } 208 209 #pragma mark - 私有方法 210 /** 211 * 记录日志 212 * 213 * @param info 日志信息 214 */ 215 - (void)writeToLog:(NSString *)info{ 216 self.log.text = [NSString stringWithFormat:@"%@rn%@",self.log.text,info]; 217 } 218 219 220 221 - (void)didReceiveMemoryWarning { 222 [super didReceiveMemoryWarning]; 223 // Dispose of any resources that can be recreated. 224 } 225 226 @end
有了上面两个程序就可以分别运行在两台支持BLE的iOS设备上,当两个应用建立连接后,一旦外围设备更新特征之后,中央设备就可以立即获取到更新后的值。需要强调的是使用CoreBluetooth开发的应用不仅仅可以和其他iOS设备进行蓝牙通信,还可以同其他第三方遵循BLE规范的设备进行蓝牙通讯,这里就不再赘述。