『CoreBluetooth』6. 作为 Peripheral 时的请求响应

之前几篇都详细的介绍了当 iOS 设备作为 central 时的读写操作,这一章将会介绍下在 iOS 设备作为 peripheral 时的相关处理。即使你只需要 central 的部分,我也建议你看一下本章内容,它对你了解整个蓝牙通信有帮助。

本文将会介绍以下内容:

学习CBPeripheralManager。

配置 service 和 characteristic。

将构建的 service 和 characteristic 树形结构加入 peripheral。

广播拥有的 service。

在 central 写入数据时,做出相关响应。

在 characteristic 数据更新时,告诉订阅的 central。

本章会举一些例子,但这些例子都是抽象化的,具体应该怎么去解决,还需要和需求结合。当然,下一章《最佳实践》也会提供一些想法。

CBPeripheralManager

将设备作为 peripheral,第一步就是初始化CBPeripheralManager对象。可以通过调用CBPeripheralManager的initWithDelegate:queue:options:方法来进行初始化:

myPeripheralManager = [[CBPeripheralManager alloc] initWithDelegate:selfqueue:niloptions:nil];

上面的几个参数中,将self设为代理来接收相关回调,queue为nil表示在主线程。

当你调用上面这方法后,便会回调peripheralManagerDidUpdateState:。所以在此之前,你需要先遵循CBPeripheralManagerDelegate。这个代理方法能获取当前 iOS 设备能否作为 peripheral。

配置 service 和 characteristic

就像之前讲到的一样,peripheral 数据库是一个树形结构。

所以在创建 peripheral 的时候,也要像这种树形结构一样,将 service 和 characteristic 装进去。在此之前,我们需要做的是学会如何标识 service 和 characteristic。

使用 UUID 来标识 service 和 characteristic

service 和 characteristic 都通过 128 位的 UUID 来进行标识,Core Bluetooth 将 UUID 封装为了CBUUID。关于详细 UUID 的介绍,请参考CoreBluetooth3 作为 central 时的数据读写(补充)

为自定义的 service 和 characteristic 创建 UUID

你的 service 或者 characteristic 的 UUID 并没有公共的 UUID,这时你需要创建自己的 UUID。

使用命令行的uuidgen能很容易的生成 UUID。首先打开终端,为你的每一个 service 和 characteristic 创建 UUID。在终端输入uuidgen然后回车,具体如下:

$ uuidgen71DA3FD1-7E10-41C1-B16F-4430B506CDE7

可以通过UUIDWithString方法,将 UUID 生成CBUUID对象。

CBUUID *myCustomServiceUUID = [CBUUID UUIDWithString:@"71DA3FD1-7E10-41C1-B16F-4430B506CDE7"];

构建 service 和 characteristic 树形结构

在将 UUID 打包为CBUUID之后,就可以创建CBMutableService和CBMutableCharacteristic并把他们组成一个树形结构了。创建CBMutableCharacteristic对象可以通过该类的initWithType:properties:value:permissions:方法:

myCharacteristic =

[[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID

properties:CBCharacteristicPropertyRead

value:myValue permissions:CBAttributePermissionsReadable];

创建 characteristic 的时候,就为他设置了properties和permissions。这两个属性分别定义了 characteristic 的可读写状态和 central 连接后是否能订阅。上面这种初始化方式,代表着 characteristic 可读。更多的选项,可以去看看CBMutableCharacteristic Class Reference

如果给 characteristic 设置了value参数,那么这个value会被缓存,并且properties和permissions会自动设置为可读。如果想要 characteristic 可写,或者在其生命周期会改变它的值,那需要将value设置为nil。这样的话,就会动态的来处理value。

现在已经成功的创建了 characteristic,下一步就是创建一个 service,并将它们构成树形结构。

调用CBMutableService的initWithType:primary:方法来初始化 service:

myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];

第二个参数primary设置为YES表示该 service 为 primary service(主服务),与 secondary service(次服务)相对。primary service 描述了设备的主要功能,并且能包含其他 service。secondary service 描述的是引用它的那个 service 的相关信息。比如,一个心率监测器,primary service 描述的是当前心率数据,secondary service 描述描述的是当前电量。

创建了 service 之后,就可以包含 characteristic 了:

myService.characteristics = @[myCharacteristic];

发布 service 和 characteristic

构建好树形结构之后,接下来便需要将这结构加入设备的数据库。这一操作 Core Bluetooth 已经封装好了,调用CBPeripheralManager的addService:方法即可:

[myPeripheralManager addService:myService];

当调用以上方法时,便会回调CBPeripheralDelegate的peripheralManager:didAddService:error:回调。当有错误,或者当前 service 不能发布的时候,可以在这个代理中来进行检测:

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

当你发布 service 之后,service 就会缓存下来,并且无法再修改。

广播 service

搞定发布 service 和 characteristic 之后,就可以开始给正在监听的 central 发广播了。可以通过调用CBPeripheralManager的startAdvertising:方法并传入字典作为参数来进行广播:

[myPeripheralManager startAdvertising:@{CBAdvertisementDataServiceUUIDsKey : @[myFirstService.UUID, mySecondService.UUID]}];

上面的代码中,key 只用到了CBAdvertisementDataServiceUUIDsKey,对应的 value 是包含需要广播的 service 的CBUUID类型数组。除此之外,还有以下 key:

NSString*constCBAdvertisementDataLocalNameKey;NSString*constCBAdvertisementDataManufacturerDataKey;NSString*constCBAdvertisementDataServiceDataKey;NSString*constCBAdvertisementDataServiceUUIDsKey;NSString*constCBAdvertisementDataOverflowServiceUUIDsKey;NSString*constCBAdvertisementDataTxPowerLevelKey;NSString*constCBAdvertisementDataIsConnectable;NSString*constCBAdvertisementDataSolicitedServiceUUIDsKey;

但是只有CBAdvertisementDataLocalNameKey和CBAdvertisementDataServiceUUIDsKey才是 peripheral Manager 支持的。

当开始广播时,peripheral Manager 会回调peripheralManagerDidStartAdvertising:error:方法。如果有错或者 service 无法进行广播,则可以在该该方法中检测:

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

因为空间的限制,并且还可能有多个 app 在同时发起广播,所以数据广播基于best effort(即在接口发生拥塞时,立即丢包,知道业务量减小)。关于数据大小的限制,会在下一章《最佳实践》中谈到。

广播服务在程序挂起时依然可用,详细会在之后讲到。

响应 central 的读写操作

在连接到一个或多个 central 之后,peripheral 有可能会收到读写请求。此时,你应该根据请求作出相应的响应,接下来便会提到这方面的处理。

读取请求

当收到读请求时,会回调peripheralManager:didReceiveReadRequest:方法。该回调将请求封装为了CBATTRequest对象,在该对象中,包含很多可用的属性。

其中一种用法是在收到读请求时,可以通过CBATTRequest的characteristic属性来判断当前被读的 characteristic 是哪一个 characteristic:

- (void)peripheralManager:(CBPeripheralManager *)peripheral    didReceiveReadRequest:(CBATTRequest *)request {if([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {    }}

匹配上 UUID 之后,接下来需要确保读取数据的offset(偏移量)不会超过 characteristic 数据的总长度:

if(request.offset > myCharacteristic.value.length) {    [myPeripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];return;}

假设偏移量验证通过,下面需要截取 characteristic 中的数据,并赋值给request.value。注意,offset也要参与计算:

request.value = [myCharacteristic.value subdataWithRange:NSMakeRange(request.offset, myCharacteristic.value.length - request.offset)];

读取完成后,记着调用CBPeripheralManager的respondToRequest:withResult:方法,告诉 central 已经读取成功了:

[myPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];

如果 UUID 匹配不上,或者是因为其他原因导致读取失败,那么也应该调用respondToRequest:withResult:方法,并返回失败原因。官方提供了一个失败原因枚举,可能有你需要的。

写入请求

写入请求和读取请求一样简单。当 central 想要写入一个或多个 characteristic 时,CBPeripheralManager回调peripheralManager:didReceiveWriteRequests:。该方法会获得一个CBATTRequest数组,包含所有写入请求。当确保一切验证没问题后(与读取操作验证类似:UUID 与offset),便可以进行写入:

myCharacteristic.value = request.value;

成功后,同样去调用respondToRequest:withResult:。但是和读取操作不同的是,读取只有一个CBATTRequest,但是写入是一个CBATTRequest数组,所以这里直接传入第一个 request 就行:

[myPeripheralManager respondToRequest:[requests objectAtIndex:0] withResult:CBATTErrorSuccess];

因为收到的是一个请求数组,所以,当他们其中有任何一个不满足条件,那就不必再处理下去了,直接调用respondToRequest:withResult:方法返回相应的错误。

发送更新数据给订阅了的 central

central 可能会订阅了一个或多个 characteristic,当数据更新时,需要给他们发送通知。下面就来详细介绍下:

当 central 订阅 characteristic 的时候,会回调CBPeripheralManager的peripheralManager:central:didSubscribeToCharacteristic:方法:

- (void)peripheralManager:(CBPeripheralManager *)peripheral                  central:(CBCentral *)centraldidSubscribeToCharacteristic:(CBCharacteristic *)characteristic {NSLog(@"Central subscribed to characteristic %@", characteristic);}

通过上面这个代理,可以用个数组来保存被订阅的 characteristic,并在它们的数据更新时,调用CBPeripheralManager的updateValue:forCharacteristic:onSubscribedCentrals:方法来告诉 central  有新的数据:

NSData*updatedValue =// fetch the characteristic's new valueBOOLdidSendValue = [myPeripheralManager updateValue:updatedValue forCharacteristic:characteristic onSubscribedCentrals:nil];

这个方法的最后一个参数能指定要通知的 central。如果参数为nil,则表示想所有订阅了的 central 发送通知。

同时,updateValue:forCharacteristic:onSubscribedCentrals:方法会返回一个BOOL标识是否发送成功。如果发送队列任务是满的,则会返回NO。当有可用的空间时,会回调peripheralManagerIsReadyToUpdateSubscribers:方法。所以你可以在这个回调用调用updateValue:forCharacteristic:onSubscribedCentrals:重新发送数据。

发送数据使用到的是通知,当你更新订阅的 central 时,应该调用一次updateValue:forCharacteristic:onSubscribedCentrals:。

因为 characteristic 数据大小的关系,不是所有的更新都能发送成功,这种问题应该由 central 端来处理。调用CBPeripheral的readValueForCharacteristic:方法,来主动获取数据(关于 central 读取数据,可以参考《设备作为 central 时的数据读写》

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

推荐阅读更多精彩内容