Core Bluetooth框架之一:Central与Peripheral

来源:互联网 发布:陕西大数据集团董事长 编辑:程序博客网 时间:2024/06/14 02:53


原文网址:http://southpeak.github.io/blog/2014/07/29/core-bluetoothkuang-jia-zhi-%5B%3F%5D-:centralyu-peripheral/

iOS和Mac应用使用Core Bluetooth framework来与BLE(低功耗蓝牙)设备通信。我们的程序可以发现、搜索并与低功耗外围(Peripheral)蓝牙设备通信,如心跳监听器、数字温控器、甚至是其它iOS设备。这个框架抽象了支持蓝牙4.0标准低功耗设备的基本操作,隐藏了4.0标准的底层实现细节,让我们可以方便的使用BLE设备。

蓝牙通信中的角色

在BLE通信中,主要有两个角色:Central和Peripheral。类似于传统的客户端-服务端架构,一个Peripheral端是提供数据的一方(相当于服务端);而Central是使用Peripheral端提供的数据完成特定任务的一方(相当于客户端)。下图以心跳监听器为例展示了这样一个架构:

Peripheral端以广告包的形式来广播一些数据。一个广告包(advertising packet)是一小束相关数据,可能包含Peripheral提供的有用的信息,如Peripheral名或主要功能。在BLE下,广告是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对象)的操作,包括扫描、查找和连接。下图本地Central端与peripheral对象

当与peripheral设备交互时,我们主要是在处理它的服务及特性。在Core Bluetooth框架中,服务是一个CBService对象,特性是一个CBCharacteristic对象,下图演示了Central端的服务与特性的基本结构:

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

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

Peripheral(Server)端操作

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

  1. 启动一个Peripheral管理对象
  2. 在本地Peripheral中设置服务及特性
  3. 将服务及特性发布给设备的本地数据库
  4. 广告我们的服务
  5. 针对连接的Central端的读写请求作出响应
  6. 发送更新的特性值到订阅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端主要包含以下操作:

  1. 启动一个Central端管理器对象
  2. 搜索并连接正在广告的Peripheral设备
  3. 在连接到Peripheral端后查询数据
  4. 发送一个对特性值的读写请求到Peripheral端
  5. 当Peripheral端特性值改变时接收通知

我们将在下面结合代码对每一步分别进行讲解

启动一个Central管理器

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

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

创建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];

在调用上述方法后,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];

如果连接成功,则会调用代码对象的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:方法。如果写入失败,我们可以在这个方法中处理错误信息。

小结

Core Bluetooth框架已经为我们封装了蓝牙通信的底层实现,我们只需要做简单的处理就可以在程序中实现基于蓝牙的通信。不过在游戏中,一般使用Game Kit中自带的蓝牙处理功能,以实现大数据量的通信。Core Bluetooth框架还是比较适合小数据量的通信。

Stay hungry, stay foolish!
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 洗澡的花洒漏水怎么办 高三了文科成绩很差怎么办 骑缝章最后一页没盖全怎么办 机票取早了没有登机口怎么办 机票早订比晚订贵怎么办? 孩子考差了父母怎么办 保险公司不给业务员办退司怎么办 我不习惯没有你我怎么办 锁坏了打不开了怎么办 要上班老人生病无人照顾怎么办 苹果手机一直说英文怎么办 公司很抠门怎么办英文怎么说 过了截港时间怎么办 截关日期是假日怎么办 恒温阀冷水进水堵塞怎么办 缺氧液泵管道堵塞怎么办 货物包装大集装箱装不下怎么办 微信收藏的视频格式错误怎么办 乙方被刑拘房租未付清怎么办 房贷银行卡号弄错怎么办 社保卡号弄错了怎么办 社保名字写错了怎么办 档案和身份证年龄姓名不一样怎么办 档案年龄与身份证年龄不一样怎么办 户口本身份证和档案不一样怎么办 如果档案姓名与身份证不符怎么办 感冒吃了白参怎么办 吃辣的嗓子疼怎么办 美团客户更改地址怎么办 忘记steam的账户名称怎么办 重置手机忘了密码怎么办 sp下行短信费扣怎么办 hr公司业务员招不到人怎么办 卖房中介被房倒压房子怎么办 电脑放不了dvd光盘怎么办 股东迟迟不交齐股本金怎么办 wps转pdf就乱了怎么办 被有用分期骗了怎么办 找不到以前有用分期的账号怎么办 打工去韩国不懂韩语怎么办? 想去韩国整容没钱怎么办