关于蓝牙

蓝牙简介

蓝牙( Bluetooth® ):是一种无线技术标准,可实现固定设备、移动设备和楼宇个人域网之间的短距离数据交换(使用2.4—2.485GHz的ISM波段的UHF无线电波)。蓝牙技术最初由电信巨头爱立信公司于1994年创制,当时是作为RS232数据线的替代方案。蓝牙可连接多个设备,克服了数据同步的难题。
如今蓝牙由蓝牙技术联盟(Bluetooth Special Interest Group,简称SIG)管理。蓝牙技术联盟在全球拥有超过25,000家成员公司,它们分布在电信、计算机、网络、和消费电子等多重领域。IEEE将蓝牙技术列为IEEE 802.15.1,但如今已不再维持该标准。蓝牙技术联盟负责监督蓝牙规范的开发,管理认证项目,并维护商标权益。制造商的设备必须符合蓝牙技术联盟的标准才能以“蓝牙设备”的名义进入市场。蓝牙技术拥有一套专利网络,可发放给符合标准的设备。

蓝牙发展

蓝牙1.1标准

1.1 为最早期版本,传输率约在748~810kb/s,因是早期设计,容易受到同频率之产品所干扰下影响通讯质量。

蓝牙1.2标准

1.2 同样是只有 748~810kb/s 的传输率,但在加上了(改善 Software)抗干扰跳频功能。

蓝牙2.0标准

2.0是1.2的改良提升版,传输率约在1.8M/s~2.1M/s,开始支持双工模式——即一面作语音通讯,同时亦可以传输档案/高质素图片,2.0版本当然也支持Stereo运作。
  应用最为广泛的是Bluetooth2.0+EDR标准,该标准在2004年已经推出,支持Bluetooth2.0+EDR标准的产品也于2006年大量出现。
  虽然Bluetooth2.0+EDR标准在技术上作了大量的改进,但从1.X标准延续下来的配置流程复杂和设备功耗较大的问题依然存在。

蓝牙2.1标准

2007年8月2日,蓝牙技术联盟今天正式批准了蓝牙2.1版规范,即“蓝牙2.1+EDR”,可供未来的设备自由使用。和2.0版本同时代产品,目前仍然占据蓝牙市场较大份额,相对2.0版本主要是提高了待机时间2倍以上,技术标准没有根本性变化。

蓝牙3.0标准

2009年4月21日,蓝牙技术联盟(BluetoothSIG)正式颁布了新一代标准规范“BluetoothCoreSpecificationVersion3.0HighSpeed”(蓝牙核心规范3.0版),蓝牙3.0的核心是“GenericAlternateMAC/PHY”(AMP),这是一种全新的交替射频技术,允许蓝牙协议栈针对任一任务动态地选择正确射频。
  蓝牙3.0的数据传输率提高到了大约24Mbps(即可在需要的时候调用802.11WI-FI用于实现高速数据传输)。在传输速度上,蓝牙3.0是蓝牙2.0的八倍,可以轻松用于录像机至高清电视、PC至PMP、UMPC至打印机之间的资料传输,但是需要双方都达到此标准才能实现功能。

蓝牙4.0标准

蓝牙4.0规范于2010年7月7日正式发布,新版本的最大意义在于低功耗,同时加强不同OEM厂商之间的设备兼容性,并且降低延迟,理论最高传输速度依然为24Mbps(即3MB/s),有效覆盖范围扩大到100米(之前的版本为10米)。该标准芯片被大量的手机、平板所采用,如苹果TheNewiPad平板电脑,以及苹果iPhone5、魅族MX4、HTCOneX等手机上带有蓝牙4.0功能。

蓝牙4.1标准

蓝牙4.1于2013年12月6日发布,与LTE无线电信号之间如果同时传输数据,那么蓝牙4.1可以自动协调两者的传输信息,理论上可以减少其它信号对蓝牙4.1的干扰。改进是提升了连接速度并且更加智能化,比如减少了设备之间重新连接的时间,意味着用户如果走出了蓝牙4.1的信号范围并且断开连接的时间不算很长,当用户再次回到信号范围中之后设备将自动连接,反应时间要比蓝牙4.0更短。最后一个改进之处是提高传输效率,如果用户连接的设备非常多,比如连接了多部可穿戴设备,彼此之间的信息都能即时发送到接接收设备上。
  除此之外,蓝牙4.1也为开发人员增加了更多的灵活性,这个改变对普通用户没有很大影响,但是对于软件开发者来说是很重要的,因为为了应对逐渐兴起的可穿戴设备,那么蓝牙必须能够支持同时连接多部设备。

蓝牙4.2标准

2014年12月4日,最新的蓝牙4.2标准颁布。蓝牙4.2标准的公布,不仅改善了数据传输速度和隐私保护程度,还接入了该设备将可直接通过IPv6和6LoWPAN接入互联网。
  首先是速度方面变得更加快速。尽管蓝牙4.1版本已在之前的基础上提升了不少,但远远不能满足用户的需求,同Wi-Fi相比,显得优势不足。而蓝牙4.2标准通过蓝牙智能(Bluetooth Smart)数据包的容量提高,其可容纳的数据量相当于此前的10倍左右,两部蓝牙设备之间的数据传输速度提高了2.5倍。
  其次,隐私保护程度地加强也获得众多用户的好评。我们知道,蓝牙4.1以及其之前的版本在隐私安全上存在一定的隐患——连接一次之后便无需再确认便自动连接,容易造成隐私泄露。而在蓝牙4.2新的标准下,蓝牙信号想要连接或者追踪用户设备必须经过用户许可,否则蓝牙信号将无法连接和追踪用户设备。
  当然,最令人期待的还是新版本通过IPv6和6LoWPAN接入互联网的功能。早在蓝牙4.1版本时,蓝牙技术联盟便已经开始尝试接入,但由于之前版本传输率的限制以及网络芯片的不兼容新,并未完全实现这一功能。而据蓝牙技术联盟称,蓝牙4.2新标准已可直接通过IPv6和6LoWPAN接入互联网。相信在此基础上,一旦可IPv6和6LoWPAN广泛运用,此功能将会吸引更多的关注。
  另外不得不提的是,对较老的蓝牙适配器来说,蓝牙4.2的部分功能将可通过软件升级的方式获得,但并非所有功能都可获取。蓝牙技术联盟称:“隐私功能或可通过固件升级的方式获得,但要视制造商的安装启用而定。速度提升和数据包扩大的功能则将要求硬件升级才能做到。”而到目前为止,蓝牙4.0仍是消费者设备最常用的标准,不过Android Lollipop等移动平台已经开始添加对蓝牙4.1标准和蓝牙4.2标准的原生支持。

蓝牙5.0标准

美国时间2016年6月16日,蓝牙技术联盟(SIG)在华盛顿正式发布了第五代蓝牙技术(简称蓝牙5.0),不仅速度提升2倍、距离远4倍,还优化IoT物联网底层功能。
  性能方面,蓝牙5.0标准传输速度是之前4.2LE版本的两倍,有效距离则是上一版本的4倍,即蓝牙发射和接收设备之间的理论有效工作距离增至300米。
  另外,蓝牙5.0还允许无需配对接受信标的数据,比如广告、Beacon、位置信息等,传输率提高了8倍。同时蓝牙5.0标准还针对IoT物联网进行底层优化,更快更省电,力求以更低的功耗和更高的性能为智能家居服务。
  蓝牙技术联盟称,目前全球的蓝牙设备已经超过了82亿。并预计蓝牙5.0标准将于2016年年底或2017年年初正式推出,搭载蓝牙5.0芯片的旗舰级手机将于2017年问世,据称苹果将为成为第一批使用该项技术的厂商之一。

CoreBluetooth

Central与Peripheral

蓝牙通信中的角色

在BLE通信中,主要有两个角色:Central和Peripheral。类似于传统的客户端-服务端架构,一个Peripheral端是提供数据的一方(相当于服务端);而Central是使用Peripheral端提供的数据完成特定任务的一方(相当于客户端)。Central端可以扫描并监听其感兴趣的任何广播信息的Peripheral设备。数据的广播及接收需要以一定的数据结构来表示。而服务就是这样一种数据结构。Peripheral端可能包含一个或多个服务或提供关于连接信号强度的有用信息。一个服务是一个设备的数据的集合及数据相关的操作。而服务本身又是由特性或所包含的服务组成的。一个特性提供了关于服务的更详细的信息。在一个Central端与Peripheral端成功建立连接后,Central可以发现Peripheral端提供的完整的服务及特性的集合。一个Central也可以读写Peripheral端的服务特性的值。

Central、Peripherals及Peripheral数据的表示

当我们使用本地Central与Peripheral端交互时,我们会在BLE通信的Central端执行操作。除非我们设置了一个本地Peripheral设备,否则大部分蓝牙交互都是在Central端进行的。(下文也会讲Peripheral端的基本操作)。在Central端,本地Central设备由CBCentralManager对象表示。这个对象用于管理发现与连接Peripheral设备(CBPeripheral对象)的操作,包括扫描、查找和连接。当与peripheral设备交互时,我们主要是在处理它的服务及特性。在Core Bluetooth框架中,服务是一个CBService对象,特性是一个CBCharacteristic对象,下图演示了Central端的服务与特性的基本结构:


服务于特性关系.png

苹果在OS X 10.9和iOS 6版本后,提供了BLE外设(Peripheral)功能,可以将设备作为Peripheral来处理。在Peripheral端,本地Peripheral设备表示为一个CBPeripheralManager对象。这些对象用于管理将服务及特性发布到本地Peripheral设备数据库,并广告这些服务给Central设备。Peripheral管理器也用于响应来自Central端的读写请求。如下图展示了一个Peripheral端角色:


Peripheral端角色.png

当在本地Peripheral设备上设置数据时,我们实际上处理的是服务与特性的可变版本。在Core Bluetooth框架中,本地Peripheral服务由CBMutableService对象表示,而特性由CBMutableCharacteristic对象表示,下图展示了本地Peripheral端服务与特性的基本结构:
Peripheral端服务与特性.png
Peripheral(Server)端操作

一个Peripheral端操作主要有以下步骤:

启动一个Peripheral管理对象
在本地Peripheral中设置服务及特性
将服务及特性发布给设备的本地数据库
广告我们的服务
针对连接的Central端的读写请求作出响应
发送更新的特性值到订阅Central端
我们将在下面结合代码对每一步分别进行讲解

启动一个Peripheral管理器

要在本地设备上实现一个Peripheral端,我们需要分配并初始化一个Peripheral管理器实例,如下代码所示

// 创建一个Peripheral管理器
// 我们将当前类作为peripheralManager,因此必须实现CBPeripheralManagerDelegate
// 第二个参数如果指定为nil,则默认使用主队列
peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];

创建Peripheral管理器后,Peripheral管理器会调用代理对象的peripheralManagerDidUpdateState:方法。我们需要实现这个方法来确保本地设备支持BLE。

- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
{
    NSLog(@"Peripheral Manager Did Update State");
    switch (peripheral.state) {
        case CBPeripheralManagerStatePoweredOn:
            NSLog(@"CBPeripheralManagerStatePoweredOn");
            break;
            
        case CBPeripheralManagerStatePoweredOff:
            NSLog(@"CBPeripheralManagerStatePoweredOff");
            break;
            
        case CBPeripheralManagerStateUnsupported:
            NSLog(@"CBPeripheralManagerStateUnsupported");
            break;
            
        default:
            break;
    }
}
设置服务及特性

一个本地Peripheral数据库以类似树的结构来组织服务及特性。所以,在设置服务及特性时,我们将其组织成树结构。

一个Peripheral的服务和特性通过128位的蓝牙指定的UUID来标识,该标识是一个CBUUID对象。虽然SIG组织没的预先定义所有的服务与特性的UUID,但是SIG已经定义并发布了一些通过的UUID,这些UUID被简化成16位以方便使用。例如,SIG定义了一个16位的UUID作为心跳服务的标识(180D)。

CBUUID类提供了方法,以从字符串中生成一个CBUUID对象。当字条串使用的是预定义的16位UUID时,Core Bluetooth使用它时会预先自动补全成128位的标识。

CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString:@"180D"];

当然我们也可以自己生成一个128位的UUID来标识我们的服务与特性。在命令行中使用uuidgen命令会生成一个128位的UUID字符串,然后我们可以使用它来生成一个CBUUID对象。

生成UUID对象后,我们就可以用这个对象来创建我们的服务及特性,然后再将它们组织成树状结构。

创建特性的代码如下所示:

CBUUID *characteristicUUID1 = [CBUUID UUIDWithString:@"C22D1ECA-0F78-463B-8C21-688A517D7D2B"];
CBUUID *characteristicUUID2 = [CBUUID UUIDWithString:@"632FB3C9-2078-419B-83AA-DBC64B5B685A"];
    
CBMutableCharacteristic *character1 = [[CBMutableCharacteristic alloc] initWithType:characteristicUUID1 properties:CBCharacteristicPropertyRead value:nil permissions:CBAttributePermissionsReadable];
    
CBMutableCharacteristic *character2 = [[CBMutableCharacteristic alloc] initWithType:characteristicUUID2 properties:CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsWriteable];

我们需要设置特性的属性、值及权限。属性及权限值确定了属性值是可读的还是可写的,及连接的Central端是否可以订阅特性的值。另外,如果我们指定了特性的值,则这个值会被缓存且其属性及权限被设置成可读的。如果我们要让特性的值是可写的,或者期望属性所属的服务的生命周期里这个值可以被修改,则必须指定值为nil。

创建的特性之后,我们便可以创建一个与特性相关的服务,然后将特性关联到服务上,如下代码所示:

CBUUID *serviceUUID = [CBUUID UUIDWithString:@"3655296F-96CE-44D4-912D-CD83F06E7E7E"];
CBMutableService *service = [[CBMutableService alloc] initWithType:serviceUUID primary:YES];
service.characteristics = @[character1, character2];    // 组织成树状结构

上例中primary参数传递的是YES,表示这是一个主服务,即描述了一个设备的主要功能且能被其它服务引用。与之相对的是次要服务(secondary service),其只在引用它的另一个服务的上下文中描述一个服务。

发布服务及特性

创建服务及特性后交将其组织成树状结构后,我们需要将这些服务发布到设备的本地数据库上。我们可以使用CBPeripheralManager的addService:方法来完成此工作。如下代码所示:

[peripheralManager addService:service];

在调用些方法发布服务时,CBPeripheralManager对象会调用它的代理的peripheralManager:didAddService:error:方法。如果发布过程中出现错误导致无法以布,则可以实现该代理方法来处理错误,如下代码所示:

- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error
{
    NSLog(@"Add Service");
    
    if (error)
    {
        NSLog(@"Error publishing service: %@", [error localizedDescription]);
    }
}

在将服务与特性发布到设备数据库后,服务将会被缓存,且我们不能再修改这个服务。

广告服务

处理完以上步骤,我们便可以将这些服务广告给对服务感兴趣的Central端。我们可以通过调用CBPeripheralManager实例的startAdvertising:方法来完成这一操作,如下代码所示:

[peripheralManager startAdvertising:@{CBAdvertisementDataServiceUUIDsKey: @[service.UUID]}];

startAdvertising:的参数是一个字典,Peripheral管理器支持且仅支持两个key值:CBAdvertisementDataLocalNameKey与CBAdvertisementDataServiceUUIDsKey。这两个值描述了数据的详情。key值所对应的value期望是一个表示多个服务的数组。

当广告服务时,CBPeripheralManager对象会调用代码对象的peripheralManagerDidStartAdvertising:error:方法,我们可以在此做相应的处理,如下代码所示:

- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error
{
    NSLog(@"Start Advertising");
    
    if (error)
    {
        NSLog(@"Error advertising: %@", [error localizedDescription]);
    }
}

广告服务之后,Central端便可以发现设备并初始化一个连接。

对Central端的读写请求作出响应

在与Central端进行连接后,可能需要从其接收读写请求,我们需要以适当的方式作出响应。

当连接的Central端请求读取特性的值时,CBPeripheralManager对象会调用代理对象的peripheralManager:didReceiveReadRequest:方法,代理方法提供一个CBATTRequest对象以表示Central端的请求,我们可以使用它的属性来填充请求。下面代码简单展示了这样一个过程:

- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request
{
    // 查看请求的特性是否是指定的特性
    if ([request.characteristic.UUID isEqual:cha1.UUID])
    {
        NSLog(@"Request character 1");
        
        // 确保读请求所请求的偏移量没有超出我们的特性的值的长度范围
        // offset属性指定的请求所要读取值的偏移位置
        if (request.offset > cha1.value.length)
        {
            [peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
            return;
        }
        
        // 如果读取位置未越界,则将特性中的值的指定范围赋给请求的value属性。
        request.value = [cha1.value subdataWithRange:(NSRange){request.offset, cha1.value.length - request.offset}];
        
        // 对请求作出成功响应
        [peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
    }
}

在每次调用代理对象的peripheralManager:didReceiveReadRequest:时调用respondToRequest:withResult:方法以对请求做出响应。

处理写请求类似于上述过程,此时会调用代理对象的peripheralManager:didReceiveWriteRequests:方法。不同的是代理方法会给我们一个包含一个或多个CBATTRequest对象的数组,每一个都表示一个写请求。我们可以使用请求对象的value属性来给我们的特性属性赋值,如下代码所示:

- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests
{
     CBATTRequest *request = requests[0];
        
     cha1.value = request.value;
        
     [peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
}

响应处理与请求类似。

发送更新的特性值给订阅的Central端

如果有一个或多个Central端订阅了我们的服务的特性时,当特性发生变化时,我们需要通知这些Central端。为此,代理对象需要实现peripheralManager:central:didSubscribeToCharacteristic:方法。如下所示:

- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic
{
    NSLog(@"Central subscribed to characteristic %@", characteristic);
    
    NSData *updatedData = characteristic.value;
    
    // 获取属性更新的值并调用以下方法将其发送到Central端
    // 最后一个参数指定我们想将修改发送给哪个Central端,如果传nil,则会发送给所有连接的Central
    // 将方法返回一个BOOL值,表示修改是否被成功发送,如果用于传送更新值的队列被填充满,则方法返回NO
    BOOL didSendValue = [peripheralManager updateValue:updatedData forCharacteristic:(CBMutableCharacteristic *)characteristic onSubscribedCentrals:nil];
    
    NSLog(@"Send Success ? %@", (didSendValue ? @"YES" : @"NO"));
}

在上述代码中,当传输队列有可用的空间时,CBPeripheralManager对象会调用代码对象的peripheralManagerIsReadyToUpdateSubscribers:方法。我们可以在这个方法中调用updateValue:forCharacteristic:onSubscribedCentrals:来重新发送值。

我们使用通知来将单个数据包发送给订阅的Central。当我们更新订阅的Central时,我们应该通过调用一次updateValue:forCharacteristic:onSubscribedCentrals:方法将整个更新的值放在一个通知中。

由于特性的值大小不一,所以不是所有值都会被通知传输。如果发生这种情况,需要在Central端调用CBPeripheral实例的readValueForCharacteristic:方法来处理,该方法可以获取整个值。

Central(Client)端操作

一个Central端主要包含以下操作:

启动一个Central端管理器对象
搜索并连接正在广告的Peripheral设备
在连接到Peripheral端后查询数据
发送一个对特性值的读写请求到Peripheral端
当Peripheral端特性值改变时接收通知
我们将在下面结合代码对每一步分别进行讲解

启动一个Central管理器

CBCentralManager对象在Core Bluetooth中表示一个本地Central设备,我们在执行任何BLE交互时必须分配并初始化一个Central管理器对象。创建代码如下所示:

// 指定当前类为代理对象,所以其需要实现CBCentralManagerDelegate协议
// 如果queue为nil,则Central管理器使用主队列来发送事件
centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];

在options字典中用于进行一些管理中心的初始化属性设置
NSString * const CBCentralManagerOptionShowPowerAlertKey 对应一个NSNumber类型的bool值,用于设置是否在关闭蓝牙时弹出用户提示
NSString * const CBCentralManagerOptionRestoreIdentifierKey 对应一个NSString对象,设置管理中心的标识符ID

创建Central管理器时,管理器对象会调用代理对象的centralManagerDidUpdateState:方法。我们需要实现这个方法来确保本地设备支持BLE。

- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    NSLog(@"Central Update State");
    
    switch (central.state) {
        case CBCentralManagerStatePoweredOn:
            NSLog(@"CBCentralManagerStatePoweredOn");
            break;
            
        case CBCentralManagerStatePoweredOff:
            NSLog(@"CBCentralManagerStatePoweredOff");
            break;
            
        case CBCentralManagerStateUnsupported:
            NSLog(@"CBCentralManagerStateUnsupported");
            break;
            
        default:
            break;
    }
}
发现正在广告的Peripheral设备

Central端的首要任务是发现正在广告的Peripheral设备,以备后续连接。我们可以调用CBCentralManager实例的scanForPeripheralsWithServices:options:方法来发现正在广告的Peripheral设备。如下代码所示:

// 查找Peripheral设备
// 如果第一个参数传递nil,则管理器会返回所有发现的Peripheral设备。
// 通常我们会指定一个UUID对象的数组,来查找特定的设备
[centralManager scanForPeripheralsWithServices:nil options:nil];

serviceUUIDs用于扫描一个特点ID的外设 options用于设置一些扫描属性 键值如下
是否允许重复扫描 对应NSNumber的bool值,默认为NO,会自动去重
NSString *const CBCentralManagerScanOptionAllowDuplicatesKey;
要扫描的设备UUID 数组 对应NSArray hovertree.com
NSString *const CBCentralManagerScanOptionSolicitedServiceUUIDsKey;

在调用上述方法后,CBCentralManager对象在每次发现设备时会调用代理对象的centralManager:didDiscoverPeripheral:advertisementData:RSSI:方法。

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
    NSLog(@"Discover name : %@", peripheral.name);
    
    // 当我们查找到Peripheral端时,我们可以停止查找其它设备,以节省电量
    [centralManager stopScan];
    
    NSLog(@"Scanning stop");
}
连接Peripheral设备

在查找到Peripheral设备后,我们可以调用CBCentralManager实例的connectPeripheral:options:方法来连接Peripheral设备。如下代码所示

[centralManager connectPeripheral:peripheral options:nil];

options中可以设置一些连接设备的初始属性键值如下
对应NSNumber的bool值,设置当外设连接后是否弹出一个警告
NSString *const CBConnectPeripheralOptionNotifyOnConnectionKey;
对应NSNumber的bool值,设置当外设断开连接后是否弹出一个警告
NSString *const CBConnectPeripheralOptionNotifyOnDisconnectionKey;
对应NSNumber的bool值,设置当外设暂停连接后是否弹出一个警告
NSString *const CBConnectPeripheralOptionNotifyOnNotificationKey;

如果连接成功,则会调用代理对象的centralManager:didConnectPeripheral:方法,我们可以实现该方法以做相应处理。另外,在开始与Peripheral设备交互之前,我们需要设置peripheral对象的代理,以确保接收到合适的回调。

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    NSLog(@"Peripheral Connected");
    
    peripheral.delegate = self;
}
查找所连接Peripheral设备的服务

建立到Peripheral设备的连接后,我们就可以开始查询数据了。首先我们需要查找Peripheral设备中可用的服务。由于Peripheral设备可以广告的数据有限,所以Peripheral设备实际的服务可能比它广告的服务要多。我们可以调用peripheral对象的discoverServices:方法来查找所有的服务。如下代码所示:

[peripheral discoverServices:nil];

参数传递nil可以查找所有的服务,但一般情况下我们会指定感兴趣的服务。

当调用上述方法时,peripheral会调用代理对象的peripheral:didDiscoverServices:方法。Core Bluetooth创建一个CBService对象的数组,数组中的元素是peripheral中找到的服务。

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
    NSLog(@"Discover Service");
    
    for (CBService *service in peripheral.services)
    {
        NSLog(@"Discovered service %@", service);
    }
}
查找服务中的特性

假设我们已经找到感兴趣的服务,接下来就是查询服务中的特性了。为了查找服务中的特性,我们只需要调用CBPeripheral类的discoverCharacteristics:forService:方法,如下所示:

NSLog(@"Discovering characteristics for service %@", service);
[peripheral discoverCharacteristics:nil forService:service];

当发现特定服务的特性时,peripheral对象会调用代理对象的peripheral:didDiscoverCharacteristicsForService:error:方法。在这个方法中,Core Bluetooth会创建一个CBCharacteristic对象的数组,每个元素表示一个查找到的特性对象。如下代码所示:

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
    NSLog(@"Discover Characteristics");
    for (CBCharacteristic *characteristic in service.characteristics)
    {
        NSLog(@"Discovered characteristic %@", characteristic);
    }
}
获取特性的值

一个特性包含一个单一的值,这个值包含了Peripheral服务的信息。在获取到特性之后,我们就可以从特性中获取这个值。只需要调用CBPeripheral实例的readValueForCharacteristic:方法即可。如下所示:

NSLog(@"Reading value for characteristic %@", characteristic);
[peripheral readValueForCharacteristic:characteristic];

当我们读取特性中的值时,peripheral对象会调用代理对象的peripheral:didUpdateValueForCharacteristic:error:方法来获取该值。如果获取成功,我们可以通过特性的value属性来访问它,如下所示:

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    NSData *data = characteristic.value;
    
    NSLog(@"Data = %@", data);
}
订阅特性的值

虽然使用readValueForCharacteristic:方法读取特性值对于一些使用场景非常有效,但对于获取改变的值不太有效。对于大多数变动的值来讲,我们需要通过订阅来获取它们。当我们订阅特性的值时,在值改变时,我们会从peripheral对象收到通知。

我们可以调用CBPeripheral类的setNotifyValue:forCharacteristic:方法来订阅感兴趣的特性的值。如下所示:

[peripheral setNotifyValue:YES forCharacteristic:characteristic];

当我们尝试订阅特性的值时,会调用peripheral对象的代理对象的peripheral:didUpdateNotificationStateForCharacteristic:error: 方法。如果订阅失败,我们可以实现该代理方法来访问错误,如下所示:

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    ...
    
    if (error)
    {
        NSLog(@"Error changing notification state: %@", [error localizedDescription]);
    }
}

在成功订阅特性的值后,当特性值改变时,peripheral设备会通知我们的应用。

写入特性的值

一些场景下,我们需要写入特性的值。例如我们需要与BLE数字恒温器交互时,可能需要给恒温器提供一个值来设定房间的温度。如果特性的值是可写的,我们可以通过调用CBPeripheral实例的writeValue:forCharacteristic:type:方法来写入值。

NSData *data = [NSData dataWithBytes:[@"test" UTF8String] length:@"test".length];
[peripheral writeValue:data forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];

当尝试写入特性值时,我们需要指定想要执行的写入类型。上例指定了写入类型是CBCharacteristicWriteWithResponse,表示peripheral让我们的应用知道是否写入成功。

指定写入类型为CBCharacteristicWriteWithResponse的peripheral对象,在响应请求时会调用代理对象的peripheral:didWriteValueForCharacteristic:error:方法。如果写入失败,我们可以在这个方法中处理错误信息。

后台处理

在开发BLE相关应用时,由于应用在后台时会有诸多资源限制,需要考虑应用的后台处理问题。默认情况下,当程序位于后台或挂起时,大多数普通的Core Bluetooth任务都无法使用,不管是Central端还是Peripheral端。但我们可以声明我们的应用支持Core Bluetooth后台执行模式,以允许程序从挂起状态中被唤醒以处理蓝牙相关的事件。

然而,即使我们的应用支持两端的Core Bluetooth后台执行模式,它也不能一直运行。在某些情况下,系统可能会关闭我们的应用来释放内存,以为当前前台的应用提供更多的内存空间。在iOS7及后续版本中,Core Bluetooth支持保存Central及Peripheral管理器对象的状态信息,并在程序启动时恢复这些信息。我们可以使用这个特性来支持与蓝牙设备相关的长时间任务。

下面我们将详细讨论下这些问题。

只支持前台操作(Foreground-Only)的应用

大多数应用在进入到后台后都会在短时间内进入挂起状态,除非我们请求执行一些特定的后台任务。当处理挂起状态时,我们的应用无法继续执行蓝牙相关的任务。

在Central端,Foreground-Only应用在进入后台或挂起时,无法继续扫描并发现下在广告的Peripheral设备。而在Peripheral端,无法广告自身,同时Central端对其的任何访问操作都会返回一个错误。

Foreground-Only应用挂起时,所有蓝牙相关的事件都会被系统放入一个队列,当应用进入前台后,系统会将这些事件发送给我们的应用。也就是说,当某些事件发生时,Core Bluetooth提供了一种方法来提示用户。用户可以使用这些提示来决定是否将应用放到前台。在Central与Peripheral中我们介绍了connectPeripheral:options:方法,在调用这个方法时,我们可以设备options参数来设置这些提示:

CBConnectPeripheralOptionNotifyOnConnectionKey:当应用挂起时,如果有一个连接成功时,如果我们想要系统为指定的peripheral显示一个提示时,就使用这个key值。
CBConnectPeripheralOptionNotifyOnDisconnectionKey:当应用挂起时,如果连接断开时,如果我们想要系统为指定的peripheral显示一个断开连接的提示时,就使用这个key值。
CBConnectPeripheralOptionNotifyOnNotificationKey:当应用挂起时,使用该key值表示只要接收到给定peripheral端的通知就显示一个提示。

Core Bluetooth后台执行模式

我们可以在Info.plist文件中设置Core Bluetooth后台执行模式,以让应用支持在后台执行一些蓝牙相关的任务。当应用声明了这一功能时,系统会将应用唤醒以允许它处理蓝牙相关的任务。这个特性对于与那种定时发送数据的BLE交互的应用非常有用。

有两种Core Bluetooth后台执行模式,一种用于实现Central端操作,一种用于实现Peripheral端操作。如果我们的应用同时实现了这两端的功能,则需要声明同时支持两种模式。我们需要在Info.plist文件添加UIBackgroundModes键,同时添加以下两个值或其中之一:

bluetooth-central(App communicates using CoreBluetooth)
bluetooth-peripheral(App shares data using CoreBluetooth)

  • bluetooth-central模式

如果设置了bluetooth-central值,则我们的应用在后台时,仍然可以查找并连接到Peripheral设备,以及查找相应数据。另外,系统会在CBCentralManagerDelegate或CBPeripheralDelegate代理方法被调用时唤醒我们的应用,允许应用处理事件,如建立连接或断开连接等等。

虽然应用在后台时,我们可以执行很多蓝牙相关任务,但需要记住应用在前后台扫描Peripheral设备时还是不一样的。当我们的应用在后台扫描Peripheral设备时,

CBCentralManagerScanOptionAllowDuplicatesKey扫描选项会被忽略,同一个Peripheral端的多个发现事件会被聚合成一个发现事件。
如果扫描Peripheral设备的多个应用都在后台,则Central设备扫描广告数据的时间间隔会增加。结果是发现一个广告的Peripheral设备可能需要很长时间。
这些处理在iOS设备中最小化无线电的使用及改善电池的使用寿命非常有用。

  • bluetooth-peripheral模式

如果设置了bluetooth-peripheral值,则我们的应用在后台时,应用会被唤醒以处理来自于连接的Central端的读、写及订阅请求,Core Bluetooth还允许我们在后台进行广告。与Central端类似,也需要注意前后台的操作区别。特别是在广告时,有以下几点区别:

CBAdvertisementDataLocalNameKey广告key值会被忽略,Peripheral端的本地名不会被广告
CBAdvertisementDataServiceUUIDsKey键的所有服务的UUID都被放在一个”overflow”区域中,它们只能被那些显示要扫描它们的网络设备发现。
如果多个应用在后台广告,则Peripheral设备发送广告包的时间间隔会变长。

在后台执行长(Long-Term)任务

虽然建议尽快完成后台任务,但有些应该仍然需要使用Core Bluetooth来执行一个长任务。这时就涉及到状态的保存与恢复操作。

状态保存与恢复

因为状态保存与恢复是内置于Core Bluetooth的,我们的程序可以选择这个特性,让系统保存Central和Peripheral管理器的状态并继续执行一些蓝牙相关的任务,即使此时程序不再运行。当这些任务中的一个完成时,系统会在后台重启程序,程序可以恢复先前的状态以处理事件。Core Bluetooth支持Central端、Peripheral端的状态保存与恢复,也可以同时支持两者。

在Central端,系统会在关闭程序释放内存时保存Central管理器对象的状态(如果有多个Central管理器,我们可以选择系统跟踪哪个管理器)。对于给定的CBCentralManager对象,系统会跟踪如下信息:

Central管理器扫描的服务
Central管理器尝试或已经连接的Peripheral
Central管理器订阅的特性
在Peripheral端,对于给定的CBPeripheralManager对象,系统会跟踪以下信息:

Peripheral管理器广告的数据
Peripheral管理器发布到设备数据库的服务和特性
订阅Peripheral管理器的特性值的Central端
当系统将程序重启到后台后,我们可以重新重新初始化我们程序的Central和Peripheral管理器并恢复状态。我们接下来将详细介绍一下如何使用状态保存与恢复。

添加状态保存和恢复支持

Core Bluetooth中的状态保存与恢复是可选的特性,需要程序的支持才能工作。我们可以按照以下步骤来添加这一特性的支持:

(必要步骤)当分配及初始化Central或Peripheral管理器对象时,选择性加入状态保存与恢复。
(必要步骤)在系统重启程序时,重新初始化Central或Peripheral管理器对象
(必要步骤)实现适当的恢复代理方法
(可选步骤)更新Central或Peripheral管理器初始化过程
选择性加入状态保存与恢复

为了选择性加入状态保存与恢复特性,在分配及初始化Central或Peripheral管理器时提供一个一个唯一恢复标识。恢复标识是一个字条串,用来让Core Bluetooth和程序标识Central或Peripheral管理器。字符串的值只在自己的代码中有意义,但这个字符串告诉Core Bluetooth我们需要保存对象的状态。Core Bluetooth只保存那些有恢复标识的对象。

例如,在实现Central端时,为了选择性加入状态保存与恢复特性,在初始化CBCentralManager对象时,可以指定初始化选项CBCentralManagerOptionRestoreIdentifierKey,并提供一个恢复标识,如下代码所示:

centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey: @"restoreIdentifier"}];

实现Peripheral端时的操作也类似,只不过我们使用选项CBPeripheralManagerOptionRestoreIdentifierKey键。

因为程序可以有多个Central或Peripheral管理器,所以需要确保恢复标识是唯一的,这样系统可以区分这些管理器对象。

重新初始化Central或Peripheral管理器对象

当系统重启程序到后台后,我们所需要做的第一件事就是使用恢复标识来重新初始化这些对象。如果我们的应用只有一个Central管理器或Peripheral管理器,且管理器在程序的整个生命周期都存在,则后续我们便不需要再做更多的处理。但如果我们有多个管理器,或者管理器不是存在于程序的整个生命周期,则系统重启应用时,我们需要知道重新初始化哪一个管理器。我们可以通过在程序代理对象的application:didFinishLaunchingWithOptions:方法中,使用合适的启动选项键来访问管理器对象的列表(这个列表是程序关闭是系统为程序保存的)。

下面代码展示了程序重新启动时,我们获取所有Central管理器对象的恢复标识:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    
    NSArray *centralManagerIdentifiers = launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
    
    // TODO: ...
    
    return YES;
}

有了这个恢复标识的列表后,我们就可以重新初始化我们所需要的管理器对象了。

实现适当的恢复代理方法

重新初始化Central或Peripheral管理器对象后,我们通过使用蓝牙系统的状态同步这些对象的状态来恢复它们。此时,我们需要实现一些恢复代理方法。对于Central管理器,我们实现centralManager:willRestoreState:代理方法;对于Peripheral管理器管理器,我们实现peripheralManager:willRestoreState:代理方法。

对于选择性加入保存与恢复特性的应用来说,这些方法是程序启动到后台以完成一些蓝牙相关任务所调用的第一个方法。而对于非选择性加入特性的应用来说,会首先调用centralManagerDidUpdateState:和peripheralManagerDidUpdateState:代理方法。

在上述两个代理方法中,最后一个参数是一个字典,包含程序关闭时保存的关于管理器的信息。如下代码所示,我们可以使用CBCentralManagerRestoredStatePeripheralsKey键来获取Central管理器已连接的或尝试连接的所有Peripheral设备的列表:

- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary *)state
{
    NSArray *peripherals = state[CBCentralManagerRestoredStatePeripheralsKey];
    
    // TODO: ...
}

获取到这个列表后,我们便可以根据需要来做处理。

更新初始化过程

在实现了前面的三个步骤后,我们可能想更新我们的管理器的初始化过程。虽然这一步是可选的,但如果要确认任务是否运行正常时,非常有用。例如,我们的程序可能在解析所连接的Peripheral设备的数据的过程中被关闭。当程序使用这个Peripheral设备作恢复操作时,无法知道数据处理到哪了。我们需要确保程序从数据操作停止的位置继续开始操作。

又如下面的代码展示了在centralManagerDidUpdateState:代理方法中初始化程序操作时,我们可以找出是否成功发现了被恢复的Peripheral设备的指定服务:

NSUInteger serviceUUIDIndex = [peripheral.services indexOfObjectPassingTest:^BOOL(CBService *obj, NSUInteger index, BOOL *stop) {
        return [obj.UUID isEqual:myServiceUUIDString];
    }];


    if (serviceUUIDIndex == NSNotFound) {
        [peripheral discoverServices:@[myServiceUUIDString]];
        ...
}

如上例所述,如果系统在程序完成搜索服务时关闭了应用,则通过调用discoverServices:方法在关闭的那个点开始解析恢复的Peripheral数据。如果程序成功搜索到服务,我们可以确认是否搜索到正确的特性。通过更新初始化过程,我们可以确保在正确的时间调用正确的方法。

虽然我们可能需要声明应用支持Core Bluetooth后台执行模式,以完成特定的任务,但总是应该慎重考虑执行后台操作。因为执行太多的蓝牙相关任务需要使用iOS设备的内置无线电,而无线电的使用会影响到电池的寿命,所以尽量减少在后台执行的任务。任何会被蓝牙相关任务唤醒的应用应该尽快处理任务并在完成时重新挂起。

下面是一些基础的准则:

应用应该是基于会话的,并提供接口以允许用户决定什么时候开始及结束蓝牙相关事件的分发。
一旦被唤醒,一个应用大概有10s的时间来完成任务。理想情况下,应用应该尽快完成任务并重新挂起。系统可以对执行时间太长的后台任务进行限流甚至杀死。
应用被唤醒时,不应该执行一些无关紧要的操作。

推荐阅读更多精彩内容