蓝牙(CoreBluetooth)-外部设备(服务端)

主要内容

1. 创建外部管理器对象
2. 设置本地外设的服务和特征
3. 添加服务和特征到到你的设置的数据库中
4. 向外公布你的的服务
5. 相应来自连接上的中心设备的请求
6. 向订阅了特征值改变的中心设备发送通知

1. 创建外设管理器

首先你需要创建一个CBPeripheralManager 对象,通过CBPeripheralManagerinitWithDelegate:queue:options:,像这样:

  self.peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];

这里设置self来作为外设的任何事件的接受者.

queue:表示外设管理器分发事件的队列,当你指定为nil,外设管理器将在主队列中的分发外设事件.

当你创建CBPeripheralManager对象时,这个管理器对象会调用peripheralManagerDidUpdateState:方法,你必须实现这个来判断你设备是否支持并开启蓝牙4.0

2. 设置服务和特征

你必须按树形结构进行管理服务特征,树结构如图

外设,服务,特征结构图

1. 服务和特征通过UUID来标示的

外设的服务和和特征通过一个128位的蓝牙UUID来标示的,在在CoreBlueTooth框架中是通过CBUUID来表示的,尽管不是所有服务和特质的UUID都需要通过蓝牙特定兴趣组来(SIG[ Special Interest Group])重定义,但是通过SIG定义或分发的通用UUID可以被缩短为16位.例如:通过蓝牙SIG定义表示心率的服务的16为UUID180D,这个UUID是蓝牙4.0,特定的服务UUID0000180D-0000-1000-8000-00805F9B34FB简便形式.
CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString: @"180D"];
当你通过16为的简便形式创建一个UUID,核心蓝牙会把填满为128位的UUID通过基UUID

2. 创建你自己的服务和特征的UUID,

1. 你可以通过终端生成一个UUID
$ uuidgen
71DA3FD1-7E10-41C1-B16F-4430B506CDE7```

#####2. 你可以使用这个UUID,通过`CBUUID`的` UUIDWithString:`方法创建一个`CBUUID`对象,像这样

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

3. 构建你的服务和特征树

当你有了服务或特征标示你就可以按照上面的特征服务树来组织你的服务和特征.你可以通过CBMutableCharacteristicinitWithType:properties:value:permissions:来创建一个可变特征,像这样
myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead value:myValue permissions:CBAttributePermissionsReadable];

当你创建一个可变的特征的时候,你就可以指定他的属性,值和许可.你可以通过这些属性和权限来指定这个特征的值是可读的可写的和连接上的中心设备能否可以订阅这个值,在这个例子中,中心设备可以读取特征的值,更多信息参考CBMutableCharacteristic

注意: 当你给某个特征指定一个值,这个将被缓存 并且它的属性和许可被设置为可读.因此,如果你需要这个该特征的值是可写的或如果希望这个特征的值在其所属服务的生命周期中是可以改变的,你必须指定这个值为nil.这样做来确保这个值是动态的和当外设管理器接收来自中心设备的读和写请求是,可以被外设管理器请求.

现在你有一个可变特征后,你可以创建一个服务,然后关联上该特征.你可以通过CBMutableServiceinitWithType:primary:方法,如下:

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

在这个例子中,第二个参数设置为`YES` ,它说明这个服务是一个`主要`的服务而不是`次要`的,`主服务`描述了一个服务的主要功能,他可以`包含`其他的`服务`,`次要服务`必须关联在他所参照的其他服务的上下文中.比如:心率检测器的`主要服务`是提供心率的数据的,那么`次要服务`可能就是提供心率检测器的电量数据的.
    
在你创建服务之后,你可以关联那个特征到这个服务上,像这样:

myService.characteristics = @[myCharacteristic];```

3. 添加布服务和特征到外设数据库

在你创建了服务和特征之后,下一步就是添加服务到设备的服务和特征数据库中,在蓝牙核心框架中很容易做.你只需要调用CBPeripheralManageraddService: 方法即可,像这样:

 [myPeripheralManager addService:myService];```
 
当你通过这个方法添加服务到服务特征数据库中时,外设管理器会调用代理的` peripheralManager:didAddService:error: `方法,当分发失败了会调用这个方法,实现这个方法可以查看错误原因
  • (void)peripheralManager:(CBPeripheralManager *)peripheral
    didAddService:(CBService *)service
    error:(NSError *)error {

if (error) {
NSLog(@"Error publishing service: %@", [error localizedDescription]);
}
...```

注意:当你发布一个关联在外设数据库的服务和特征时,这个服务将被缓存,并且你以后再也不能在修改它了.

4. 公布你的服务给监听的中心管理器

当你已经添加服务和特征到外设的服务特征数据库中后,你已经公布给服务或特征给中心管理器做好准备,你可以通过你的CBPeripheralManagerstartAdvertising:,传入一个包含需要公布数据的字典.

 [myPeripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey :
    @[myFirstService.UUID, mySecondService.UUID] }];```
    
这个例子中通过`CBAdvertisementDataServiceUUIDsKey` 这个`key`来指定的需要公布分服务的 `UUID`,可能允许使用的`key`,在`CBCentralManagerDelegate Protocol Reference. `的`Advertisement Data Retrieval Keys` 中描述:
    1. CBAdvertisementDataLocalNameKey 对应的值是一个字符串,描述外设的名称
    2.CBAdvertisementDataManufacturerDataKey 对应的值是一个NSData对象,包含外设的产生的数据
    3. CBAdvertisementDataServiceDataKey 包含特定服务的分发数据,该字典的key为代表着该服务的CBUUID对象.值为NSData对象
    4. CBAdvertisementDataServiceUUIDsKey 需要公布的服务的`UUID`数组
    5. CBAdvertisementDataOverflowServiceUUIDsKey 代表着在公布数据的"overflow"区域能够被发现的服务的UUID的数组,因为存储在这个`UUID`列表是`最大努力的` 并且不总是精确的.如果设备资源不足这些属性可能不会被公布.
    6. CBAdvertisementDataTxPowerLevelKey 一个包含外设发射功率NSNumber的数字,如果外设在广播的数据包中,提供了他的`Tx`功率级别时候,这个属性是可用的. 使用这个`RSSI` 值和电台功率,计算出路径损耗是有可能.
    7. CBAdvertisementDataIsConnectable 一个布尔值,标示公布事件类型是否为可连接的,对应这个Key是一个 NSNumber对象,你可用使用这个值来检查一个外设当前是否为连接状态
    8. CBAdvertisementDataSolicitedServiceUUIDsKey 一个代表着一个或多个服务的`UUID`

但是对于外设管理器只支持两个key  `CBAdvertisementDataLocalNameKey`和 `CBAdvertisementDataServiceUUIDsKey`. 

当你在你的本地外设开始公布公布你的外设数据的时候,会调用外设管理器代理的`peripheralManagerDidStartAdvertising:error: `,你可以通过实现这个方法,来查看你公布数据时的错误
    
  • (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
    error:(NSError *)error {

if (error) {
NSLog(@"Error advertising: %@", [error localizedDescription]);
}
...


> `注意:` 数据发布是基于`最大努力`的,由于带宽是有限并且多个app可能同时发布,更多信息参考`CBPeripheralManager `的`startAdvertising: ` 的注释.
'当你应用后台也可以进行发布行为,这个话题参照`core bluetooth`文档的` Core Bluetooth Background Processing for iOS Apps`

一旦你开始公布数据,远程的中心设备就可用发现并连接上你.

###5. 响应来自`中心设备`的`读`和`写`请求

当你连接上一个或多个中心设备后,你便开始接受来自他们的读或写请求,请你确保以合适方式来响应的这些请求,下面的例子来描述如何相应这些请求

当一个中心管理器对去某个特征中的值的时候,会调用外设管理器的代理方法`peripheralManager:didReceiveReadRequest: `,这个方法传入了一个`CBATTRequest`的请求,他提供很多属来满足这个请求

例如当你接受到一个单独读取某个特征值的请求,代理方法传入的`CBATTRequest`对象的属性课用来判断你设备数据中的特征是否与远程中心设备请求的特征是否匹配,你可以向这样的来实现这个方法
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveReadRequest:(CBATTRequest *)request {

if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
    ...

如果特征是匹配的,那么下一步是确保请求读取数据的索引没有超出特征值的范围.下面的例子向你展示如何使用`CBATTRequest`对象的`offset` 来确保取得的索引没有超出特征值的边界

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


假定已经验证通过,你可以通过本地外设的`特征的值`给`请求`设置值(它默认是nil)
request.value = [myCharacteristic.value
subdataWithRange:NSMakeRange(request.offset,
myCharacteristic.value.length - request.offset)];```

在你设置值之后,你需要告知远程的中心设置已经成功设置,通过明确调用CBPeripheralManagerrespondToRequest:withResult:方法,像这样:

[myPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
    ...```

每次在代理对象的`peripheralManager:didReceiveReadRequest:`方法中明确调用` respondToRequest:withResult: `

> `注意`: 如果特征的`UUID`不匹配或因任何原因导致的读不能完成.你不要试图来填充请求的值,而是立即调用` respondToRequest:withResult:`方法提供一个失败的原因.详情参照`CBATTError`枚举
    

处理来自中央设备的写请求是简单的,当一个中心设备发送对象一个或多个特征值的写请求时,外设管理器就会调用其代理`peripheralManager:didReceiveWriteRequests: `方法,在这个方法中传入了一个`CBATTRequest `对象的数组.每一个都代表着一个写请求.在确认请示是可以被满足的后,你可以写特征的值,像这样:
myCharacteristic.value = request.value;```

尽管上面的例子中没有演示,当写入你的特征值时,如何确保写入的偏移量.但是在你真实的app中需要进行处理.

和是响应读请求一样,在代理方法peripheralManager:didReceiveWriteRequests:明确调用一次respondToRequest:withResult: 方法.这也就是说第一个参数是单独一个请求对象,尽管你可能在peripheralManager:didReceiveWriteRequests:方法中受到了多个请求对象.你应该传入数组中的第一个请求作为参数.像这样:

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

> `注意`:对于多个请求对象只有一次请求,只要任意的一个请求对象得不到满足,你任意一个都不要满足,而是立即调用`respondToRequest:withResult:`方法提供一个结果,告诉失败的原因.

###6. 发送更新特征的值给已经订阅的中心设备

通常情况,连接上的中央设备将订阅你的一个或多个特征值, 当他们这么做的时候,你有责任当某个订阅特征的值发生改变了通知他们.下面的例子描述如何做.

当一个连接上的中心管理器订阅了你的特征上的某个值,那么外设管理器就会调用其代理的`peripheralManager:central:didSubscribeToCharacteristic:`方法调用
- (void)peripheralManager:(CBPeripheralManager *)peripheral
              central:(CBCentral *)central

didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {

NSLog(@"Central subscribed to characteristic %@", characteristic);
...
   
使用上面的方法作为一个开始发送更新中心设备数据的引子,接下获取特征的更新值并调用` CBPeripheralManager` 的 `updateValue:forCharacteristic:onSubscribedCentrals: `把更新数据发送给中心设备

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

当你通过上面的方法给订阅的中心设备发送更新数据时,你可以通过最后一个参数指定更新那些中心设备,如果传入nil,表示所有订阅的中心设备都更新数据(连接上没有订阅的中心设备讲被忽略).该方法返回一个Bool,来指示是否发送成功,如果用于传入更新值的队列是满的,那么该方法返回NO,当传输队列可用的时,会调用外设管理器的代理peripheralManagerIsReadyToUpdateSubscribers:,你可以实现这个方法再次传送数据

注意:使用这个方法发送通知给订阅的中心设备管理器一个单独的数据包,当你更新数据的应该把整个数据作为一个通知发送.只调用一次updateValue:forCharacteristic:onSubscribedCentrals:方法,如果你的数据不能通过一次通知传递过去,解决方案就是在中心设备一边,通过 CBPeripheralreadValueForCharacteristic:方法,来获取数据,这个方法可以取回整个数据.

外设流程流程图

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

推荐阅读更多精彩内容