前言
最近实验室做了一个IOS设备之间使用蓝牙进行数据交互的项目。中间遇到了很多坑,现在大致讲解一下蓝牙通讯的流程。干货请直接下翻到第四节。
iOS蓝牙基础知识
背景
iOS的蓝牙不能用来传输文件。
iOS与iOS设备之间进行数据通信,使用gameKit.framework
iOS与其他非iOS设备进行数据通信,使用coreBluetooth.framework
iOS中蓝牙的实现方案iOS中提供了4个框架用于实现蓝牙连接:
- GameKit.framework
用法简单只能用于iOS设备之间的连接,多用于游戏(比如五子棋对战),从iOS7开始过期
- MultipeerConnectivity.framework
只能用于iOS设备之间的连接,从iOS7开始引入,主要用于文件共享(仅限于沙盒的文件)
- ExternalAccessory.framework
可用于第三方蓝牙设备交互,但是蓝牙设备必须经过苹果MFi认证(国内较少)
- CoreBluetooth.framework(时下热门)
可用于第三方蓝牙设备交互,必须要支持蓝牙4.0硬件至少是4s,系统至少是iOS6蓝牙4.0以低功耗著称,一般也叫BLE(BluetoothLowEnergy)目前应用比较多的案例:运动手坏、嵌入式设备、智能家居
CoreBluetooth
什么是CoreBluetooth
The Core Bluetooth framework lets your iOS and Mac apps communicate with Bluetooth low energy devices. For example, your app can discover, explore, and interact with low energy peripheral devices, such as heart rate monitors, digital thermostats, and even other iOS devices.
The framework is an abstraction of the Bluetooth 4.0 specification for use with low energy devices. That said, it hides many of the low-level details of the specification from you, the developer, making it much easier for you to develop apps that interact with Bluetooth low energy devices. Because the framework is based on the specification, some concepts and terminology from the specification have been adopted. This chapter introduces you to the key terms and concepts that you need to know to begin developing great apps using the Core Bluetooth framework.
CoreBluetooth框架就是苹果公司为我们提供的一个库,我们可以使用这个库和其他支持蓝牙4.0的设备进行数据交互。值得注意的是在IOS10之后的APP中,我们需要在 info.plist
文件中添加NSBluetoothPeripheralUsageDescription字段
否则APP会崩溃
蓝牙通讯中的外围设备和中心设备
通常,设备之间进行通讯的时候都少不了中心设备和外围设备。一般来说,外围设备具有一些需要传递给中心设备的数据,而中心设备获取这些数据之后,可以进行相应的数据处理。同时,中心设备处理完数据之后,也可以向外围设备发送信息。例如,下图1-1中外围设备(心率感应器)将数据发送给IOS或者MAC上的APP中,即中心设备。
中心设备可以发现并连接到发送了广播的外围设备
外围设备向中心设备传递数据的主要方式是广播。广播中携带的数据有限(大致为31字节),里面包含了外围设备所想要向中心设备提供的数据。例如下图1-2中,一个温度监控仪可能会向外广播现在房间的温度,中心设备就能拿到温度数据进行相应的一些操作。所以在BLE技术中,广播是外围设备让其他设备能感知到它的主要方式
外围设备中的数据结构
外围设备中包含一个或者多个服务或提供关于连接信号强度的一些信息。服务是一个数据集合,它包含完成一个(或多个)设备某些功能或特性的相关行为。例如,一个心率感应器的服务可能携带从心率传感器发送而来的数据。服务本身是由特征或服务(即引用其它服务)组成。特征则为外围设备的服务提供了更详细的细节。例如,刚刚提到的心率服务中的特性就可能包含心率设备的位置和心率测量数据。图1-3展示了这个心率感应设备中的结构。
中心设备和外围设备进行数据交互
当中心设备成功连接到一个外围设备之后,它可以知道外围设备提供的所以服务和特征(广播的数据可能只包含一部分可用的服务)。
中心设备可以对外围设备的服务特征进行读写操作从而实现和外围设备的数据交互。例如,一个APP可能会通过温度感应器获取房间当前温度,并且通过给温度感应器传递数据(像操作空调一样)改变房间温度。
中心设备、外围设备、外围设备数据的表现形式
本地中心设备对象蓝牙数据交互方式主要都通过Core Bluetooth
框架进行。
当你使用中心端和 远程外围设备 进行交互的时候,你的蓝牙交互操作基本都在本地中心端执行。除非你需要设置一个远程外围设备并且通过它对中心设备作出响应,否则你的大部分蓝牙交互操作都在中心端进行。
在本地中心端,一个名叫
CBCentralManager
的对象起着举足轻重的作用。这个类被用来管理发现或者已经连接的远程外围设备(通常这些外围设备都以CBPeripheral
对象为存在方式,某种程度上可以说CBPeripheral
就是外围设备,CBCentralManager
就是中心设备),包括扫描、发现并且连接到广播的外围设备。图1-4展示了本地中心设备和远程外围设备在Core Bluetooth
中的表现形式。
- 远程外围设备的数据由
CBService
和CBCharacteristic
对象表示> 当你在和远程外围设备进行数据交互的时候,你实际上是在和其服务和特征进行交互。在Core Bluetooth
框架中,服务由CBService
对象代表。同样的,特征也由CBCharacteristic
对象代表。图1-5阐述了外围设备中的结构信息。
本地外围设备对象
在IOS6和OS X v10.9之后,Mac和IOS设备都能够作为 本地外围设备 给包括Mac、iPhone、和iPad的设备分享数据。当你实现本地外围设备角色功能,在蓝牙交互角色中,你就表现为外围设备。
- 在远程外围设备端,本地外围设备由
CBPeripheralManager
对象作为代表。这些对象被用来管理发布外围设备服务和特征数据并实现对中心设备的广播。它也用来回应中心设备的读写操作。图1-6展示了外围设备在蓝牙交互中的角色信息。
- 本地外围设备由
CBMutableService
和CBMutableCharacteristic
对象代表> 当你和本地外围设备进行数据处理的时候,你其实是在和服务和特征的可变版本打交道。在Core Bluetooth
框架中,本地外围设备的服务由CBMutableService
代表。同样,本地外围设备特种由CBMutableCharacteristic
代表。图1-7传输了本地外围设备服务和特征的树状结构。
蓝牙通讯应用实例
对蓝牙基础知识有了一个大致了解之后,我们现在利用CoreBluetooth
框架进行实战。我们需要外围设备和中心设备模式,所以创建两个分别名为BluetoothCentralTest
和BluetoothPeripheralTest
的项目。 在后期我们需要测试蓝牙的时候,如果只有一个IOS设备,我们可以下载一个名为 LightBlue
的软件用来模拟另一个蓝牙设备
创建外围设备
- 外围设备的工作流程为:
1、开启外围设备管理
2、设置服务、特征、描述等信息
3、开始广播
外围设备和中心设备的数据交互可以用通知的形式来进行或者通过
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request
和- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests
这两个方法来进行。 其中通知主要是中心设备订阅外围设备,一般用作外围设备单方面向中心设备发送数据我创建外围设备是以单例的方式来创建,代码如下,注意:在.h文件中需要声明协议CBPeripheralManagerDelegate
//
//LARPeripheralManager.m
//BlueToothPeripheralTest
//
//Created by柳钰柯on 2016/12/4.
//Copyright © 2016年柳钰柯. All rights reserved.
//
#import"LARPeripheralManager.h"
staticNSString *constServiceUUID1 =@"FFF0";
staticNSString *constnotiyCharacteristicUUID =@"FFF1";
staticNSString *constreadwriteCharacteristicUUID =@"FFF2";
staticNSString *constServiceUUID2 =@"FFE0";
staticNSString *constreadCharacteristicUUID =@"FFE1";
staticNSString *constLocalNameKey =@"iPhone";
@implementationLARPeripheralManager{
//外围设备管理器
CBPeripheralManager *peripheralManager;
//定时器
NSTimer *timer;
}
+ (instancetype)shareInstance
{
staticLARPeripheralManager *peripheral;
staticdispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
peripheral = [[selfalloc] init];
});
returnperipheral;
}
- (instancetype)init
{
if(self= [superinit]) {
NSLog(@"外围设备单例创建");
peripheralManager = [[CBPeripheralManager alloc]initWithDelegate:selfqueue:nil];
}
returnself;
}
//初始化一些UUID和特征信息,此处设置的一些信息,在中心设备中可以根据需要进行过滤
- (void)setUp
{
// characteristic字段描述
CBUUID *CBUUIDCharacteristicUserDescriptionStringUUID = [CBUUID UUIDWithString:CBUUIDCharacteristicUserDescriptionString];
/*
可以通知的Characteristic
properties:CBCharacteristicPropertyNotify
permissions: CBAttributePermissionsReadable
*/
CBMutableCharacteristic *notiyCharacteristic = [[CBMutableCharacteristic alloc]initWithType:[CBUUID UUIDWithString:notiyCharacteristicUUID] properties:CBCharacteristicPropertyNotify value:nilpermissions:CBAttributePermissionsReadable];
/*
可读写的characteristics
properties:CBCharacteristicPropertyWrite | CBCharacteristicPropertyRead
permissions CBAttributePermissionsReadable | CBAttributePermissionsWriteable
*/
CBMutableCharacteristic *readwriteCharacteristic = [[CBMutableCharacteristic alloc]initWithType:[CBUUID UUIDWithString:readwriteCharacteristicUUID] properties:CBCharacteristicPropertyWrite | CBCharacteristicPropertyRead value:nilpermissions:CBAttributePermissionsReadable | CBAttributePermissionsWriteable];
//设置descriptor
CBMutableDescriptor *readwriteCharacteristicDescription1 = [[CBMutableDescriptor alloc]initWithType: CBUUIDCharacteristicUserDescriptionStringUUID value:@"name"];
[readwriteCharacteristic setDescriptors:@[readwriteCharacteristicDescription1]];
/*
只读的Characteristic
properties:CBCharacteristicPropertyRead
permissions CBAttributePermissionsReadable
*/
CBMutableCharacteristic *readCharacteristic = [[CBMutableCharacteristic alloc]initWithType:[CBUUID UUIDWithString:readCharacteristicUUID] properties:CBCharacteristicPropertyRead value:nilpermissions:CBAttributePermissionsReadable];
//service1初始化并加入两个characteristics
CBMutableService *service1 = [[CBMutableService alloc]initWithType:[CBUUID UUIDWithString:ServiceUUID1] primary:YES];
[service1 setCharacteristics:@[notiyCharacteristic,readwriteCharacteristic]];
//service2初始化并加入一个characteristics
CBMutableService *service2 = [[CBMutableService alloc]initWithType:[CBUUID UUIDWithString:ServiceUUID2] primary:YES];
[service2 setCharacteristics:@[readCharacteristic]];
//添加后就会调用代理的- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error
[peripheralManager addService:service1];
[peripheralManager addService:service2];
}
#pragma mark -
//检测蓝牙状态变化,当蓝牙状态改变时,自动回调
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
{
switch(peripheral.state) {
//在这里判断蓝牙设别的状态当开启了则可调用setUp方法(自定义)
caseCBManagerStatePoweredOn:
NSLog(@"powered on");
//运行初始化方法
[selfsetUp];
break;
caseCBManagerStatePoweredOff:
NSLog(@"powered off");
break;
default:
break;
}
}
//添加了服务,添加服务后需要广播,一旦广播,外围设备就可以被中心设备发现,同样外围设备所携带的数据也能被中心设备捕获
- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error
{
if(error) {
NSLog(@"%@",[error localizedDescription]);
return;
}
//添加服务后,发送广播
//发送广播后会自动调用
// - (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error
[peripheralManager startAdvertising:@{
CBAdvertisementDataServiceUUIDsKey :@[[CBUUID UUIDWithString:ServiceUUID1],[CBUUID UUIDWithString:ServiceUUID2]],
CBAdvertisementDataLocalNameKey : LocalNameKey
}];
}
//通知发送了广播
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error
{
NSLog(@"已经开始广播");
}
//中心设备订阅特征后会调用这个方法
- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic
{
NSLog(@"订阅了%@的数据",characteristic.UUID);
//分配定时任务
timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(sendData:)
userInfo:characteristic
repeats:YES];
}
//中心设备取消订阅特征后调用这个方法
-(void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic{
NSLog(@"取消订阅%@的数据",characteristic.UUID);
//取消定时器
[timer invalidate];
}
//发送数据
- (void)sendData:(NSTimer *)t{
CBMutableCharacteristic *characteristic = t.userInfo;
if([peripheralManager updateValue:[[NSString stringWithFormat:@"Sending Data"] dataUsingEncoding:NSUTF8StringEncoding]forCharacteristic:characteristic onSubscribedCentrals:nil]) {
NSLog(@"发送数据成功");
}else{
NSLog(@"发送数据错误");
}
}
//中心设备读characteristics请求
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request{
NSLog(@"didReceiveReadRequest");
//判断是否有读数据的权限
if(request.characteristic.properties & CBCharacteristicPropertyRead) {
//NSData *data = request.characteristic.value;
NSData *data = [[NSString stringWithFormat:@"通过characteristics读请求"] dataUsingEncoding:NSUTF8StringEncoding];
[request setValue:data];
//对请求作出成功响应
[peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
}else{
[peripheralManager respondToRequest:request withResult:CBATTErrorWriteNotPermitted];
}
}
//外围设备写characteristics请求
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests{
NSLog(@"didReceiveWriteRequests");
CBATTRequest *request = requests[0];
//判断是否有写数据的权限
if(request.characteristic.properties & CBCharacteristicPropertyWrite) {
//需要转换成CBMutableCharacteristic对象才能进行写值
CBMutableCharacteristic *c =(CBMutableCharacteristic *)request.characteristic;
c.value = request.value;
NSLog(@"收到中心设备发送信息:%@",[[NSString alloc] initWithData:c.value encoding:NSUTF8StringEncoding]);
[peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
}else{
[peripheralManager respondToRequest:request withResult:CBATTErrorWriteNotPermitted];
}
}
//外围设备更新描述后调用
- (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral{
NSLog(@"peripheralManagerIsReadyToUpdateSubscribers");
}
@end
- 我们尝试用
lighblue
来链接一下外围设备试试,结果如图
创建中心设备
- 中心设备的工作流程为:
1、创建中心设备
2、发现外围设备
3、链接外围设备
4、发现服务和特征4.1 获取服务
4.2 获取特征值
4.3 获取特征描述
5、 数据交互
6、 订阅通知
7、 断开链接
中心设备中读取外围设备的数据可以使用订阅外围设备通知或者是调用
[peripheral readValueForCharacteristic:<#(nonnull CBCharacteristic *)#>]
和[peripheral readValueForDescriptor:<#(nonnull CBDescriptor *)#>]
方法来获取特征或者描述的数据,至于需要获取那一层的,则需要自己定义。* 我是在特征值这一层通过订阅外围设备通知的方式获取数据,所以需要在- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
扫描到匹配的特征后,进行数据订阅。值得注意的是,中心设备扫描到外围设备后,想要保持连接,需要将外围设备保留下来。可以使用一个强引用变量引用,也可以将外围设备添加到数组。推荐使用后者
同样,也需要遵守
CBCentralManagerDelegate
和CBPeripheralDelegate协议
//
//LARCentralBlueTooth.m
//Unity-iPhone
//
//Created by柳钰柯on 2016/11/18.
//
//
#import"LARCentralBlueTooth.h"
staticNSString *constServiceUUID1 =@"FFF0";
staticNSString *constnotiyCharacteristicUUID =@"FFF1";
staticNSString *constreadwriteCharacteristicUUID =@"FFF2";
@interfaceLARCentralBlueTooth ()
/**系统蓝牙管理对象*/
@property(strong,nonatomic,readwrite) CBCentralManager *manager;
/**扫描到的设备*/
@property(strong,nonatomic,readwrite) NSMutableArray *discoverPeripheral;
/**当前连接设备*/
@property(strong,nonatomic) CBPeripheral *currentPeripheral;
@end
@implementationLARCentralBlueTooth
+ (instancetype)shareInstance{
staticLARCentralBlueTooth *blueTooth;
staticdispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
blueTooth = [[selfalloc] init];
});
returnblueTooth;
}
- (instancetype)init
{
if(self= [superinit]){
NSLog(@"单例实例化");
_manager = [[CBCentralManager alloc] initWithDelegate:selfqueue:dispatch_get_global_queue(0,0)];
_discoverPeripheral = [[NSMutableArray alloc] init];
}
returnself;
}
#pragma mark -
//检查设备蓝牙开关的状态
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
if(central.state == CBManagerStatePoweredOn) {
NSLog(@"蓝牙已打开");
//开始扫描设备
//扫描到设备之后会进入
//-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI方法
[_manager scanForPeripheralsWithServices:niloptions:nil];
}else{
NSLog(@"蓝牙已关闭");
}
}
//发现设备后,根据过滤设置进行连接
-(void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData
RSSI:(NSNumber *)RSSI
{
NSLog(@"搜索到了设备%@",peripheral.name);
//我的另外一个设备的名字以闫开头,可以在这里自行设置过滤
if([peripheral.name hasPrefix:@"闫"]) {
//持有设备
if(![self.discoverPeripheral containsObject:peripheral]) {
//数组或者变量保留外围设备二者选其一
//使用数组保留外围设备引用
// [self.discoverPeripheral addObject:peripheral];
//使用一个strong变量强引用持有外围设备
self.currentPeripheral = peripheral;
//连接设备
[_manager connectPeripheral:peripheral options:nil];
NSLog(@"准备连接设备%@",peripheral.name);
}
}
}
#pragma mark -连接状态
//连接设备失败自动调用
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
NSLog(@">>>连接%@失败>>>错误:%@",peripheral.name,[error localizedDescription]);
}
//断开连接自动调用
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral
{
NSLog(@">>>断开%@连接",peripheral.name);
}
//连接成功自动调用
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
NSLog(@">>>连接%@成功",peripheral.name);
//设置外围设备代理
[peripheral setDelegate:self];
//开始扫描外围设备的服务
//扫描到服务会进入
// - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
[peripheral discoverServices:nil];
}
#pragma mark -
//扫描到设备的服务后会进入这个方法
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
if(error){
NSLog(@"扫描服务错误:%@",[error localizedDescription]);
}else{
for(CBService *serverinperipheral.services) {
//扫描服务的特征
//扫描到后会进入
// - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
[peripheral discoverCharacteristics:nilforService:server];
NSLog(@"搜索到了一个服务:%@",server.UUID.UUIDString);
}
}
}
//扫描到服务的特征值
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
if(error) {
NSLog(@"扫描服务:%@的特征值错误:%@",service.UUID,[error localizedDescription]);
return;
}
//获取到特征值
for(CBCharacteristic *characteristicinservice.characteristics){
if([characteristic.UUID isEqual:[CBUUID UUIDWithString:notiyCharacteristicUUID]]) {
//订阅特征值的数据
[selfnotifyCharacteristic:peripheral characteristic:characteristic];
//读取发送的数据
[peripheral readValueForCharacteristic:characteristic];
}
}
}
//当获取到外围设备更新的描述信息后(即数据)会调用此方法
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
if(error)
{
NSLog(@"更新特征值数据: %@错误: %@", characteristic.UUID,[error localizedDescription]);
}
//这里对收到的数据进行处理,需要和发送端一致
if(characteristic.value !=nil) {
NSString *data = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
NSLog(@"收到外围设备发送的信息:%@",data);
[peripheral writeValue:[[NSString stringWithFormat:@"get"] dataUsingEncoding:NSUTF8StringEncoding] forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
}
}
//设置通知
- (void)notifyCharacteristic:(CBPeripheral *)peripheral
characteristic:(CBCharacteristic *)characteristic{
NSLog(@"订阅通知成功");
//设置通知
[peripheral setNotifyValue:YESforCharacteristic:characteristic];
}
//取消通知
- (void)cancelNotifyCharacteristic:(CBPeripheral *)peripheral
characteristic:(CBCharacteristic *)characteristic{
[peripheral setNotifyValue:NOforCharacteristic:characteristic];
}
//停止扫描并断开连接
-(void)disconnectPeripheral:(CBCentralManager *)centralManager
peripheral:(CBPeripheral *)peripheral{
//停止扫描
[centralManager stopScan];
//断开连接
[centralManager cancelPeripheralConnection:peripheral];
}
@end
- 测试结果如下图所示:
结语
- 写了几个小时终于写完了,觉得不错的话,请点个赞~
- 这个也是我自己琢磨、学习了几天之后学会的,难免有错误,请发现错误后联系我,我会及时纠正。谢谢!
- 如果有疑问,欢迎留言或者给我发邮件:[email protected]
- 项目地址:蓝牙Demo
- 不要脸的安利一下自己的博客地址,博客搭建好没多久,后续会平均一周更新一次:Larry代码成诗
参考
Core Bluetooth Overview