小引
在小兵整理蓝牙开发之前,小兵先万分感谢大神 KenshinCui,没有看大神的博客,小兵也不会对蓝牙有这么深得认识。
随着蓝牙低功耗技术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芯片的智能设备传输,因此也是目前智能家居、无线支付等热门智能设备所推崇的技术。
终上,小兵最后选择了CoreBluetooth.framework,这里小兵也着重整理了CoreBluetooth.framework
CoreBluetooth
无论是GameKit还是MultipeerConnectivity,都只能在iOS设备之间进行数据传输,这就大大降低了蓝牙的使用范围,于是从iOS6开始苹果推出了CoreBluetooth.framework,这个框架最大的特点就是完全基于BLE4.0标准并且支持非iOS设备。当前BLE应用相当广泛,不再仅仅是两个设备之间的数据传输,它还有很多其他应用市场,例如室内定位、无线支付、智能家居等等,这也使得CoreBluetooth成为当前最热门的蓝牙技术。
CoreBluetooth设计同样也是类似于客户端-服务器端的设计,作为服务器端的设备称为外围设备(Peripheral),作为客户端的设备叫做中央设备(Central),CoreBlueTooth整个框架就是基于这两个概念来设计的。
CoreBluetooth类似于客户端-服务器端.png外围设备和中央设备在CoreBluetooth中使用CBPeripheralManager和CBCentralManager表示。
CBPeripheralManager:外围设备通常用于发布服务、生成数据、保存数据。外围设备发布并广播服务,告诉周围的中央设备它的可用服务和特征。
CBCentralManager:中央设备使用外围设备的数据。中央设备扫描到外围设备后会就会试图建立连接,一旦连接成功就可以使用这些服务和特征。
外围设备和中央设备之间交互的桥梁是服务(CBService)和特征(CBCharacteristic),二者都有一个唯一的标识UUID(CBUUID类型)来唯一确定一个服务或者特征,每个服务可以拥有多个特征,下面是他们之间的关系:
UUID服务特征的关系.png一台iOS设备(注意iPhone4以下设备不支持BLE,另外iOS7.0、8.0模拟器也无法模拟BLE)既可以作为外围设备又可以作为中央设备,但是不能同时即是外围设备又是中央设备,同时注意建立连接的过程不需要用户手动选择允许,这一点和前面两个框架是不同的,这主要是因为BLE应用场景不再局限于两台设备之间资源共享了。
A.外围设备
创建一个外围设备通常分为以下几个步骤:
创建外围设备CBPeripheralManager对象并指定代理。
创建特征CBCharacteristic、服务CBSerivce并添加到外围设备
外围设备开始广播服务(startAdvertisting:)。
和中央设备CBCentral进行交互。
下面是简单的程序示例,程序有两个按钮“启动”和“更新”,点击启动按钮则创建外围设备、添加服务和特征并开始广播,一旦发现有中央设备连接并订阅了此服务的特征则通过更新按钮更新特征数据,此时已订阅的中央设备就会收到更新数据。
界面设计:
外围设备.png#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
上面程序运行的流程如下(图中蓝色代表外围设备操作,绿色部分表示中央设备操作):
外围设备.jpgB.中央设备
中央设备的创建一般可以分为如下几个步骤:
创建中央设备管理对象CBCentralManager并指定代理。
扫描外围设备,一般发现可用外围设备则连接并保存外围设备。
查找外围设备服务和特征,查找到可用特征则读取特征数据。
下面是一个简单的中央服务器端实现,点击“启动”按钮则开始扫描周围的外围设备,一旦发现了可用的外围设备则建立连接并设置外围设备的代理,之后开始查找其服务和特征。一旦外围设备的特征值做了更新,则可以在代理方法中读取更新后的特征值。
界面设计:
中央设备.png#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
上面程序运行的流程图如下:
中央设备.jpg有了上面两个程序就可以分别运行在两台支持BLE的iOS设备上,当两个应用建立连接后,一旦外围设备更新特征之后,中央设备就可以立即获取到更新后的值。需要强调的是使用CoreBluetooth开发的应用不仅仅可以和其他iOS设备进行蓝牙通信,还可以同其他第三方遵循BLE规范的设备进行蓝牙通讯。
补充:
一般情况下,服务(Service)的UUID根据功能来区分(假如有FFF0和FFFE0两种服务),比如FFF0的服务ID里的特征字节UUID通道用来作为传感器通讯,FFE0里放蓝牙设备的信息,名称啊电池啊等等。。
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
if (error)
{
NSLog(@"didDiscoverCharacteristicsForService error : %@", [error localizedDescription]);
return;
}
for (CBCharacteristic *c in service.characteristics)
{
NSLog(@"\n>>>\t特征UUID FOUND(in 服务UUID:%@): %@ (data:%@)",service.UUID.description,c.UUID,c.UUID.data);
/*
根据特征不同属性去读取或者写
if (c.properties==CBCharacteristicPropertyRead) {
}
if (c.properties==CBCharacteristicPropertyWrite) {
}
if (c.properties==CBCharacteristicPropertyNotify) {
}
*/
//假如你和硬件商量好了,某个UUID时写,某个读的,那就不用判断啦
/*
if([c.UUID isEqual:[CBUUID UUIDWithString:@"FFF1"]]){
self.writeCharacteristic = c;
}
if([c.UUID isEqual:[CBUUID UUIDWithString:@"FFF6"]]){
[peripheral readValueForCharacteristic:c];
}*/
if (c.properties==CBCharacteristicPropertyRead) {
[peripheral readValueForCharacteristic:c];
}
}
}
UUID
UUID 大家可以通过终端拿到
终端.png
网友评论
用这个框架是不是,外设必须是4.0?低于4.0就不能连接?还是???