iOS CoreBluetooth

CoreBluetooth

在iOS和Mac应用中,CoreBluetooth框架用来与BLE(低功耗蓝牙)设备通信,我们的程序可以搜索并与低功耗蓝牙设备通信,如手环设备,甚者是其他iOS设备。这个框架支持蓝牙4.0的基本操作,隐藏了实现细节,我们可以方便的使用它与BLE设备交互。

蓝牙通信中的角色

在BLE通信中,主要有两个角色: CentralPeripheralPeripheral端是提供数据的一方,类似于服务端,Central端是使用Peripheral端提供的数据完成特定任务,例如展示数据。其实相当于客户端-服务端架构。

在本文主要介绍iOS设备(手机)作为Central端,BLE设备(如手环)作为Peripheral端的使用。

Peripheral端可以以广告包的形式来广播一些数据,比如peripheral设备的名字,设备UUID等。通常都是以16进制数据形式,并且字节数较少。

Central端可以扫描到到带有广告包信息的Peripheral设备。同时一个Central端与Peripheral端建立成功建立连接后,数据的广播以及接收也需要一定的数据结构来表示。而服务就是这样一种数据结构。一个Peripheral端可能包含一个或多个服务,每个服务又是由一个或者多个特征组成的。Central端可以发现Peripheral端提供的完整的服务及特性的集合。一个Central也可以读写Peripheral端的服务特性的值。

iOS CoreBluetooth_第1张图片
TreeOfServicesAndCharacteristics_Remote_2x.png

手机作为Central端的操作

Central端,本地Central设备由CBCentralManager对象表示。这个对象用于管理发现与连接Peripheral设备(CBPeripheral对象)的操作,包括扫描、查找和连接。当与Peripheral设备交互时,我们主要是在处理它的服务及特性,下文中出现的Central端指iPhone,Peripheral端指BLE设备。

Central端的主要操作:

  1. 初始化一个Central端的管理对象(CBCentralManager)
  2. 搜索并连接正在广告的Peripheral设备(CBPeripheral)
  3. 连接成功后,获取所需的服务(CBService)和特征(CBCharacteristic)。
  4. 操作特征,如读取、写入、通知,来获取所需数据并进行处理。

初始化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);
    }
}

处理特征的值

通常我们获取特征之后,CBCharacteristicCBCharacteristicProperties属性对应的值,这个特征告诉我们需要怎么去获取值,一般来说是读取、写入、通知这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设备,并且也能处理相应的特征值。系统在CBCentralManagerDelegateCBPeripheralDelegate的代理方法被执行时,会唤醒应用来处理事件。

但是也有一些不同,在扫描Peripheral设备时,

  • CBCentralManagerScanOptionAllowDuplicatesKey扫描选项会被忽略,同一个Peripheral端的多个事件会变成一个。
  • 扫描间隔会增加。
  • 扫描的时候scanForPeripheralsWithServices: options:的第一个值,不能传nil以扫描周围所有设备,必须传入对应的UUID数组来扫描特定的设备,否则不会扫描。。

减少电池损耗

  • 只有当需要的时候才扫描设备,并且最好指定扫描设备的UUID就只扫描指定设备
  • 只有当扫描需要的时候才使用CBCentralManagerScanOptionAllowDuplicatesKey选项
  • 获取服务和特征的时候不要全部获取,应该只获取指定的特征和服务
  • 特征值经常改变的情况下,最好采用订阅的方式。
  • 不再需要数据的时候即使断开。

另外还有一点,但是有利弊,如果BLE设备电池容量不是很大的情况下,但是又需要使用很久,可以使用广告包的形式来发送数据,不用与手机建立连接,但是这个有一个缺点,手机需要一直扫描。。感觉手机电池应该损耗比较大吧,希望有清楚的能指导一二。

其他

目前这些知识,已经能满足我自己在项目的使用了,但是使用代理的方式来实现蓝牙,在多个页面的时候,始终感觉有点麻烦,看了BabyBluetooth的实现方式后,确实逻辑就比较清晰了,但是里面有很多也用不上,就自己简单实现了一个手机作为Central端一对一连接的库DSBluetooth用来满足自己的需求,后续有需求的话,再慢慢完善这个库的。

你可能感兴趣的:(iOS CoreBluetooth)