CoreBluetooth
在iOS和Mac应用中,CoreBluetooth
框架用来与BLE(低功耗蓝牙)设备通信,我们的程序可以搜索并与低功耗蓝牙设备通信,如手环设备,甚者是其他iOS设备。这个框架支持蓝牙4.0的基本操作,隐藏了实现细节,我们可以方便的使用它与BLE设备交互。
蓝牙通信中的角色
在BLE通信中,主要有两个角色: Central
和Peripheral
。Peripheral
端是提供数据的一方,类似于服务端,Central
端是使用Peripheral
端提供的数据完成特定任务,例如展示数据。其实相当于客户端-服务端架构。
在本文主要介绍iOS设备(手机)作为Central
端,BLE设备(如手环)作为Peripheral
端的使用。
Peripheral
端可以以广告包的形式来广播一些数据,比如peripheral设备的名字,设备UUID等。通常都是以16进制数据形式,并且字节数较少。
Central端可以扫描到到带有广告包信息的Peripheral设备。同时一个Central端与Peripheral端建立成功建立连接后,数据的广播以及接收也需要一定的数据结构来表示。而服务就是这样一种数据结构。一个Peripheral端可能包含一个或多个服务,每个服务又是由一个或者多个特征组成的。Central端可以发现Peripheral端提供的完整的服务及特性的集合。一个Central也可以读写Peripheral端的服务特性的值。
手机作为Central端的操作
在Central
端,本地Central
设备由CBCentralManager
对象表示。这个对象用于管理发现与连接Peripheral
设备(CBPeripheral
对象)的操作,包括扫描、查找和连接。当与Peripheral
设备交互时,我们主要是在处理它的服务及特性,下文中出现的Central端
指iPhone,Peripheral端
指BLE设备。
Central
端的主要操作:
- 初始化一个
Central
端的管理对象(CBCentralManager
) - 搜索并连接正在广告的
Peripheral
设备(CBPeripheral
) - 连接成功后,获取所需的服务(
CBService
)和特征(CBCharacteristic
)。 - 操作特征,如读取、写入、通知,来获取所需数据并进行处理。
初始化Central端管理对象
/*
指定当前类为代理对象,需要实现CBCentralManagerDelegate
queue为nil 代表使用主队列来发送事件
options为管理器的配置,可以在CBCentralManagerConstants.h看到所需key
*/
_centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];
当初始化CBCentralManager
后,会调用代理方法centralManagerDidUpdateState:
来判断当前的Central
端是否支持BLE
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
//其他状态可以在CBManager.h查看
switch (central.state) {
case CBCentralManagerStatePoweredOn:
NSLog(@"蓝牙可以使用");
break;
default:
break;
}
}
搜索正在广告的Peripheral设备
Central
端要发现Peripheral
设备需要调用CBCentralManager
实例的scanForPeripheralsWithServices:options:
方法来发现正在广告的Peripheral
设备
/*
扫描周围Peripheral设备
services:传nil则代表扫描周围所有正在广告的设备,传UUID对象数组则扫描指定的设备
options:扫描的配置 详情见CBCentralManagerConstants.h
*/
[self.centralManager scanForPeripheralsWithServices:nil options:nil];
调用这个方法后,CBCentralManager
对象在每次发现设备时都会调用代理对象的centralManager:didDiscoverPeripheral:advertisementData:RSSI:
/*
发现设备回调
peripheral:发现的设备
advertisementData:广告包数据
RSSI:信号强度
*/
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
NSLog(@"发现设备: %@", peripheral);
}
连接Peripheral设备
现在我们已经发现了周围正在广告的设备了,所以需要连接到我们需要的设备了。
可以调用CBCentralManager
实例的connectPeripheral:options:
方法来连接Peripheral
设备。
/*
连接Peripheral设备
peripheral:你需要的设备
options:连接配置 详见CBCentralManagerConstants.h
*/
[self.centralManager connectPeripheral:peripheral options:nil];
如果连接成功则代理对象会调用centralManager:didConnectPeripheral:
方法,在这个方法里我们可以来处理与Peripheral
设备交互之前的一些操作,比如设置Peripheral
对象的代理(CBPeripheralDelegate
),确保可以接收到回调。
//设备连接成功
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
[self.centralManager stopScan];//停止扫描
self.peripheral = peripheral;
self.peripheral.delegate = self;
}
发现所连接的Peripheral设备的服务和特征
Central
端与Peripheral
端连接后,我们可以通过Peripheral
设备提供的服务和特征来获取更多数据,这个数据比Peripheral
设备的广告数据要多。调用peripheral
实例的discoverServices:
方法来查找服务
/*
传nil表示查找peripheral的所有服务
一般情况下我们会指定感兴趣的服务的UUID数组
*/
[peripheral discoverServices:nil];
当上面的方法被调用了,peripheral
会调用代理对象的peripheral:didDiscoverServices:
方法
//peripheral.services数组中是所有找到的服务
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
for (CBService *service in peripheral.services) {
NSLog(@"发现服务: %@", service);
}
}
现在我们已经找到了服务,接下来就是查找服务中的特征了,我们需要调用peripheral
实例的discoverCharacteristics:forService:
方法
//查找该服务service的特征
[peripheral discoverCharacteristics:nil forService:service];
当调用这个方法后,peripheral
会调用代理对象的peripheral:didDiscoverCharacteristicsForService:error:
方法
//service.characteristics数组是该服务下找到的特征
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
for (CBCharacteristic *characteristic in service.characteristics)
{
NSLog(@"发现特征:%@", characteristic);
}
}
处理特征的值
通常我们获取特征之后,CBCharacteristic
的CBCharacteristicProperties
属性对应的值,这个特征告诉我们需要怎么去获取值,一般来说是读取、写入、通知这3种形式最多。可以在CBCharacteristic.h
文件查看更多内容
typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) {
CBCharacteristicPropertyBroadcast = 0x01,
CBCharacteristicPropertyRead = 0x02,
CBCharacteristicPropertyWriteWithoutResponse = 0x04,
CBCharacteristicPropertyWrite = 0x08,
CBCharacteristicPropertyNotify = 0x10,
CBCharacteristicPropertyIndicate = 0x20,
CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40,
CBCharacteristicPropertyExtendedProperties = 0x80,
CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(10_9, 6_0) = 0x100,
CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(10_9, 6_0) = 0x200
};
读取特征的值
一个特征包含一个单一的值,我们要从特征中获取这个值,需要调用peripheral
实例的readValueForCharacteristic:
方法
[peripheral readValueForCharacteristic:characteristic];
获取成功后,peripheral
会调用代理对象的peripheral:didUpdateValueForCharacteristic:error:
方法来获取characteristic
的值。
//获取读取的特征值,通常来说这个方法是获取值的唯一途径。
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
NSLog(@"characteristic.value = %@", characteristic.value);
}
写入特征的值
有些特征是允许写入值的,可以调用peripheral
实例的writeValue:forCharacteristic:type:
方法来写入值。
Byte byte[] = {0x01,0x02,0x03,0x04};
NSData *data = [NSData dataWithBytes:byte length:4];
[peripheral writeValue:data forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
写入特征值的时候我们需要指定是否有返回值,比如上面是CBCharacteristicWriteWithResponse
代表我们需要有返回值。这样peripheral
会调用代理对象的peripheral:didWriteValueForCharacteristic:error:
方法
//写入特征,如果写入失败可以在这个方法中处理
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
NSLog(@"characteristic.value = %@",characteristic.value);
}
订阅特征值
特征值有一些是会改变的,我们需要通过订阅的方式来获取它们,peripheral
实例调用setNotifyValue:forCharacteristic:
方法来订阅需要的特征。
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
当我们订阅具有通知属性的特征时,peripheral
会调用代理对象的peripheral:didUpdateNotificationStateForCharacteristic:error:
方法来获取值。
//获取订阅的特征值,通常来说这个方法是获取值的唯一途径。
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
NSLog(@"characteristic.value = %@", characteristic.value);
}
Core Bluetooth
框架已经为我们封装了蓝牙通信的底层实现,我们只需要按照上面的步骤就可以来构建一个基本的蓝牙通讯过程(其实过程还是比较复杂的...)
后台执行模式
应用在后台时会有诸多资源的限制,蓝牙也不例外。我们可以在Info.plist
文件中设置Core Bluetooth
后台执行模式,让应用在后台可以继续执行一些蓝牙相关的任务。我们需要在Info.plist
文件添加UIBackgroundModes
键,同时添加以下两个值或其中之一:
bluetooth-central
bluetooth-peripheral
bluetooth-central模式
设置了bluetooth-central
值,则我们的应用在后台时,仍然可以使用连接到Peripheral
设备,并且也能处理相应的特征值。系统在CBCentralManagerDelegate
或CBPeripheralDelegate
的代理方法被执行时,会唤醒应用来处理事件。
但是也有一些不同,在扫描Peripheral
设备时,
-
CBCentralManagerScanOptionAllowDuplicatesKey
扫描选项会被忽略,同一个Peripheral端的多个事件会变成一个。 - 扫描间隔会增加。
- 扫描的时候
scanForPeripheralsWithServices: options:
的第一个值,不能传nil
以扫描周围所有设备,必须传入对应的UUID数组
来扫描特定的设备,否则不会扫描。。
减少电池损耗
- 只有当需要的时候才扫描设备,并且最好指定扫描设备的
UUID
就只扫描指定设备 - 只有当扫描需要的时候才使用
CBCentralManagerScanOptionAllowDuplicatesKey
选项 - 获取服务和特征的时候不要全部获取,应该只获取指定的特征和服务
- 特征值经常改变的情况下,最好采用订阅的方式。
- 不再需要数据的时候即使断开。
另外还有一点,但是有利弊,如果BLE设备电池容量不是很大的情况下,但是又需要使用很久,可以使用广告包的形式来发送数据,不用与手机建立连接,但是这个有一个缺点,手机需要一直扫描。。感觉手机电池应该损耗比较大吧,希望有清楚的能指导一二。
其他
目前这些知识,已经能满足我自己在项目的使用了,但是使用代理的方式来实现蓝牙,在多个页面的时候,始终感觉有点麻烦,看了BabyBluetooth的实现方式后,确实逻辑就比较清晰了,但是里面有很多也用不上,就自己简单实现了一个手机作为Central端
一对一连接的库DSBluetooth用来满足自己的需求,后续有需求的话,再慢慢完善这个库的。