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端的服务特性的值。

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用来满足自己的需求,后续有需求的话,再慢慢完善这个库的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 蓝牙简介 蓝牙( Bluetooth® ):是一种无线技术标准,可实现固定设备、移动设备和楼宇个人域网之间的短距离...
    Chefil阅读 1,984评论 2 19
  • 最近公司的项目中提到了蓝牙开发,而且现在市面上的蓝牙分为普通蓝牙和低功耗蓝牙(BLE)也就是蓝牙4.0 iOS 提...
    CoderSJun阅读 1,048评论 0 0
  • 关于CoreBluetooth CoreBluetooth框架提供了你的iOS或Mac的应用在和配备了BLE(低功...
    原鸣清阅读 716评论 0 3
  • 1 泡脚 拍打 健康 2 看《合伙中国人》,看到确实有的创业者受到大多数投资者的喜爱,而有的创业者就没人喜欢,我要...
    cwzyzh阅读 126评论 0 1
  • 在行动营的二十多天,我把我二十多年的人生串起来了。 我把自己的这些年按照状态分成了两种,一种是节制状态的人生,一种...
    宫鹏超阅读 334评论 2 2