美文网首页iOS蓝牙4.0(BLE )蓝牙BLEiOS蓝牙开发
CoreBluetooth 蓝牙开发(后台模式、状态保存与恢复)

CoreBluetooth 蓝牙开发(后台模式、状态保存与恢复)

作者: 望月Jarvis | 来源:发表于2018-09-08 00:38 被阅读11次

    最近新进一家公司,主要是做物联网这一块的的,项目需要用到蓝牙开发,讲真的,挑战还是挺大的,做了差不多四年的iOS开发,从没有接触过蓝牙开发这一领域,我是这样学习的。

    从网上找各种博客(国内的,国外的),借鉴别人写过的Demo以及官方文档,花了整整的一周时间,对iOS的CoreBluetooth这个框架的使用稍微有一些的了解,请听我一一道来;

    iOS 蓝牙

    简称:BLE(buletouch low energy),蓝牙 4.0 设备因为低耗电,所以也叫做 BLE,CoreBluetooth框架就是苹果公司为我们提供的一个库,我们可以使用这个库和其他支持蓝牙4.0的设备进行数据交互。值得注意的是在IOS10之后的APP中,我们需要在 info.plist文件中添加NSBluetoothPeripheralUsageDescription字段否则APP会崩溃

    工作模式:蓝牙通信中,首先需要提到的就是 central 和 peripheral 两个概念。这是设备在通信过程中扮演的两种角色。直译过来就是 [中心] 和 [周边(可以理解为外设)]。iOS 设备既可以作为 central,也可以作为 peripheral,这主要取决于通信需求。

    自己尝试的写了个Demo,实现的功能有:

    1、通过已知外围设备的服务UUID搜索(这个UUID是指被广播出来的服务UUID);
    2、连接指定的外围设备;
    3、获取指定的服务,发现需要订阅的特征;
    4、接收外围设备发送的数据;
    5、向外围设备写数据;
    6、实现蓝牙服务的后台模式;
    7、实现蓝牙服务的状态保存与恢复(应用被系统杀死的时候,系统会自动保存 central manager 的状态);
    

    中心角色的实现:(central)

    (1)、初始化中央管理器对象

    /**
    第一个参数:代理
    第二个参数:队列(nil为不指定队列,默认为主队列)
    第三个参数:实现状态保存的时候需要用到 eg:@{CBCentralManagerOptionRestoreIdentifierKey:@"centralManagerIdentifier"} 
    */  
    centerManager = [[CBCentralManager alloc]initWithDelegate:self queue:queue options:options];
    

    中央管理器会调用 centralManagerDidUpdateState:通知蓝牙的状态

    (2)、发现外围设备

    [centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:SERVICE_UUID]] options:nil];
    

    每次中央管理器发现外围设备时,它都会调用centralManager:didDiscoverPeripheral:advertisementData:RSSI:其委托对象的方法。

    (3)、发现想要的外围设备进行连接

    #pragma mark -- 扫描发现到任何一台设备都会通过这个代理方法回调
    - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI
    {
        //过滤掉无效的结果
        if (peripheral == nil||peripheral.identifier == nil/*||peripheral.name == nil*/)
        {
            return;
        }
        
        NSString *pername =[NSString stringWithFormat:@"%@",peripheral.name];
        NSLog(@"所有服务****:%@",peripheral.services);
    
        NSLog(@"蓝牙名字:%@  信号强弱:%@",pername,RSSI);
       //连接需要的外围设备
        [self connectPeripheral:peripheral];
        //将搜索到的设备添加到列表中
        [self.peripherals addObject:peripheral];
        
        if (_didDiscoverPeripheralBlock) {
            _didDiscoverPeripheralBlock(central,peripheral,advertisementData,RSSI);
        }
    }
    
    

    如果连接请求成功,则中央管理器调用centralManager:didConnectPeripheral:其委托对象的方法。

    (4)、发现所连接的外围设备的服务

    #pragma mark -- 连接成功、获取当前设备的服务和特征 并停止扫描
    - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
    {
        NSLog(@"%@",peripheral);
        
        // 设置设备代理
        [peripheral setDelegate:self];
        // 大概获取服务和特征
        [peripheral discoverServices:@[[CBUUID UUIDWithString:SERVICE_UUID]]];
        
        NSLog(@"Peripheral Connected");
        
        if (_centerManager.isScanning) {
            [_centerManager stopScan];
        }
        NSLog(@"Scanning stopped");
        
    }
    

    发现指定的服务时,外围设备(CBPeripheral你连接的对象)会调用peripheral:didDiscoverServices:其委托对象的方法。

    (5)、发现服务的特征

    #pragma mark -- 获取当前设备服务services
    - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
    {
        if (error) {
            NSLog(@"Error discovering services: %@", [error localizedDescription]);
            return;
        }
        NSLog(@"所有的servicesUUID%@",peripheral.services);   
        //遍历所有service
        for (CBService *service in peripheral.services)
        {
            NSLog(@"服务%@",service.UUID);
            //找到你需要的servicesuuid
            if ([[NSString stringWithFormat:@"%@",service.UUID] isEqualToString:SERVICE_UUID])
            {
                // 根据UUID寻找服务中的特征
                [peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:CHARACTERISTIC_UUID]] forService:service];
            }
        }
    }
    

    peripheral:didDiscoverCharacteristicsForService:error:当发现指定服务的特征时,外围设备调用其委托对象的方法。

    (6)、检索特征价值

    阅读特征的值 ()

     [peripheral readValueForCharacteristic:interestingCharacteristic];
    

    注意: 并非所有特征都是可读的。你可以通过检查其properties属性是否包含CBCharacteristicPropertyRead常量来确定特征是否可读。如果尝试读取不可读的特征值,则peripheral:didUpdateValueForCharacteristic:error:委托方法将返回合适的错误。

    订阅特征的值()

    虽然使用该readValueForCharacteristic:方法读取特征值对静态值有效,但它不是检索动态值的最有效方法。检索随时间变化的特征值 - 例如,你的心率 - 通过订阅它们。订阅特征值时,您会在值更改时收到外围设备的通知。

    [peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];
    

    注意: 并非所有特征都提供订阅。你可以通过检查特性是否properties包含其中一个CBCharacteristicPropertyNotify或多个CBCharacteristicPropertyIndicate常量来确定特征是否提供订阅。
    当你订阅(或取消订阅)特征的值时,外围设备会调用peripheral:didUpdateNotificationStateForCharacteristic:error:其委托对象的方法。

    写一个特征的值 ()

    有时写一个特征的值是有意义的。例如,如果你的应用程序与蓝牙低功耗数字恒温器交互,你可能需要为恒温器提供设置房间温度的值。如果特征值是可写的,则可以NSData通过调用外设writeValue:forCharacteristic:type:方法将数据值;

    [self.discoveredPeripheral writeValue:data forCharacteristic:self.characteristic1 type:CBCharacteristicWriteWithResponse];
    

    写入特征的值时,指定要执行的写入类型。在上面的示例中,写入类型CBCharacteristicWriteWithResponse指示外围设备通过调用peripheral:didWriteValueForCharacteristic:error:其委托对象的方法让您的应用程序知道写入是否成功。

    外围角色的实现

    (1)、初始化外围设备管理器

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

    创建外围设备管理器时,外围设备管理器会调用peripheralManagerDidUpdateState:其委托对象的方法。您必须实现此委托方法,以确保支持蓝牙低功耗并可在本地外围设备上使用。

    (2)、设置服务和特征

    为自定义服务和特征创建自己的UUID
    在终端使用 uuidgen 命令获取以ASCII字符串形式的128位值的UUID:71DA3FD1-7E10-41C1-B16F-4430B506CDE7

    构建服务树和特征

    myCharacteristic =[[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead value:myValue permissions:CBAttributePermissionsReadable];   //特征
     myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];    //与特征所关联的服务
    
    myService.characteristics = @ [myCharacteristic];        //设置服务的特征数组,将特征与其关联
    

    (3)、发布服务和特征

      [peripheralManager addService:myService];
    

    当调用此方法发布服务时,外围管理器将调用peripheralManager:didAddService:error:其委托对象的方法。通过error可以知道是否发布成功;
    将服务及其任何关联特性发布到外围设备的数据库后,该服务将被缓存,将无法再对其进行更改。

    (4)、广播服务

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

    当开始在本地外围设备上公布某些数据时,外围设备管理器会调用peripheralManagerDidStartAdvertising:error:其委托对象的方法。

    (5)、响应来自中央的读取和写入请求

    当连接的中央请求读取某个特征的值时,外围管理器会调用peripheralManager:didReceiveReadRequest:其委托对象的方法。

     [peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset]; 
    

    设置读取请求不要求从超出特征值的边界的索引位置读取

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

    将请求的特性属性(默认值为nil)的值设置为您在本地外围设备上创建的特征值,同时考虑读取请求的偏移量

    设置值后,响应远程中央以指示请求已成功完成。通过调用类的respondToRequest:withResult:方法CBPeripheralManager,传回请求(其更新的值)和请求的结果

    当连接的中心发送写入一个或多个特征值的请求时,外围管理器会调用peripheralManager:didReceiveWriteRequests:其委托对象的方法

    (6)、将更新的特征值发送到订阅的中心

    当连接的中心订阅某个特征的值时,外围管理器会调用peripheralManager:central:didSubscribeToCharacteristic:其委托对象的方法
    获取特征的更新值,并通过调用类的updateValue:forCharacteristic:onSubscribedCentrals:方法将其发送到中心CBPeripheralManager。

    处理常驻后台任务

    首先需要在Capabilities-->Background Modes申请中心角色的后台模式说明

    如图:


    中心角色后台模式.jpg

    (1)、状态保存与恢复

    因为状态的保存和恢复 Core Bluetooth 都为我们封装好了,所以我们只需要选择是否需要这个特性即可。系统会保存当前 central manager 或 peripheral manager,并且继续执行蓝牙相关事件(即使程序已经不再运行)。一旦事件执行完毕,系统会在后台重启 app,这时你有机会去存储当前状态,并且处理一些事物。在之前提到的 “门锁” 的例子中,系统会监视连接请求,并在 centralManager:didConnectPeripheral: 回调时,重启 app,在用户回家后,连接操作结束。

    Core Bluetooth 的状态保存与恢复在设备作为 central、peripheral 或者这两种角色时,都可用。在设备作为 central 并添加了状态保存与恢复支持后,如果 app 被强行关闭进程,系统会自动保存 central manager 的状态(如果 app 有多个 central manager,你可以选择哪一个需要系统保存)。

    对于 CBCentralManager,系统会保存以下信息:

    central 准备连接或已经连接的 peripheral
    central 需要扫描的 service(包括扫描时,配置的 options)
    central 订阅的 characteristic
    对于 peripheral 来说,情况也差不多。系统对 CBPeripheralManager 的处理方式如下:
    peripheral 在广播的数据
    peripheral 存入的 service 和 characteristic 的树形结构
    已经被 central 订阅了的 characteristic 的值
    当系统在后台重新加载程序后(可能是因为找到了要找的 peripheral),你可以重新实例化 central manager 或 peripheral 并恢复他们的状态。

    (2)、选择支持存储和恢复

    如果要支持存储和恢复,则需要在初始化 manager 的时候给一个 restoration identifier。restoration identifier 是 string 类型,并标识了 app 中的 central manager 或 peripheral manager。这个 string 很重要,它将会告诉 Core Bluetooth 需要存储状态,毕竟 Core Bluetooth 恢复有 identifier 的对象。

    例如,在 central 端,要想支持该特性,可以在调用 CBCentralManager 的初始化方法时,配置 CBCentralManagerOptionRestoreIdentifierKey:

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

    虽然以上代码没有展示出来,其实在 peripheral manager 中要设置 identifier 也是这样的。只是在初始化时,将 key 改成了 CBPeripheralManagerOptionRestoreIdentifierKey。
    因为程序可以有多个 CBCentralManager 和 CBPeripheralManager,所以要确保每个 identifier 都是唯一的。

    (3)、重新初始化 central manager 和 peripheral manager

    当系统重新在后台加载程序时,首先需要做的即根据存储的 identifier,重新初始化 central manager 或 peripheral manager。如果你只有一个 manager,并且 manager 存在于 app 生命周期中,那这个步骤就不需要做什么了。
    .
    如果 app 中包含多个 manager,或者 manager 不是在整个 app 生命周期中都存在的,那 app 就必须要区分你要重新初始化哪个 manager 了。你可以通过从 app delegate 中的 application:didFinishLaunchingWithOptions: 中取出 key(UIApplicationLaunchOptionsBluetoothCentralsKey 或 UIApplicationLaunchOptionsBluetoothPeripheralsKey)中的 value(数组类型)来得到程序退出之前存储的 manager identifier 列表:

    - (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    NSArray *centralManagerIdentifiers =
        launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
        if (centralManagerIdentifiers.count) {
            //重新初始化所有的 manager 
            for (NSString *identifier in centralManagerIdentifiers) {
                NSLog(@"系统启动项目");
                //在这里创建的蓝牙实例一定要被当前类持有,不然出了这个函数就被销毁了,蓝牙检测会出现“XPC connection invalid”
                self.bluetooth = [[MSBBlueTooth alloc]initWithQueue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey : identifier}];
                NSLog(@"");
            }
        }
    
    return YES;
    }
    

    (4)、实现恢复状态的代理方法

    在重新初始化 manager 之后,接下来需要同步 Core Bluetooth 存储的他们的状态。要想弄清楚在程序被退出时都在做些什么,就需要正确的实现代理方法。对于 central manager 来说,需要实现 centralManager:willRestoreState:;对于 peripheral manager 来说,需要实现 peripheralManager:willRestoreState:。
    .
    注意:如果选择存储和恢复状态,当系统在后台重新加载程序时,首先调用的方法是 centralManager:willRestoreState: 或 peripheralManager:willRestoreState:。如果没有选择存储的恢复状态(或者唤醒时没有什么内容需要恢复),那么首先调用的方法是 centralManagerDidUpdateState: 或 peripheralManagerDidUpdateState:。
    .
    无论是以上哪种代理方法,最后一个参数都是一个包含程序退出前状态的字典。字典中,可用的 key ,

    central 端有:
    NSString *const CBCentralManagerRestoredStatePeripheralsKey;
    NSString *const CBCentralManagerRestoredStateScanServicesKey;
    NSString *const CBCentralManagerRestoredStateScanOptionsKey;
    
    peripheral 端有:
    NSString *const CBPeripheralManagerRestoredStateServicesKey;
    NSString *const CBPeripheralManagerRestoredStateAdvertisementDataKey;
    

    要恢复 central manager 的状态,可以用 centralManager:willRestoreState: 返回字典中的 key 来得到。假如说 central manager 有想要或者已经连接的 peripheral,那么可以通过 CBCentralManagerRestoredStatePeripheralsKey 对应得到的 peripheral(CBPeripheral 对象)数组来得到。

    - (void)centralManager:(CBCentralManager *)central
    willRestoreState:(NSDictionary *)state {
    NSArray *peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey];
        //讲状态保存的设备加入列表,在蓝牙检测状态的回调里实现重连
        self.peripherals = [NSMutableArray arrayWithArray:peripherals];
    
    }
    

    具体要对拿到的 peripheral 数组做什么就要根据需求来了。如果这是个 central manager 搜索到的 peripheral 数组,那就可以存储这个数组的引用,并且开始建立连接了(注意给这些 peripheral 设置代理,否则连接后不会走 peripheral 的代理方法)。
    .
    恢复 peripheral manager 的状态和 central manager 的方式类似,就只是把代理方法换成了 peripheralManager:willRestoreState:,并且使用对应的 key 即可

    写的不是很好,也算是东拼西凑了,但也是花了时间去整理的,如果看不懂,可以下载我的Demo自己跑一遍;

    想要看实现效果,可以下载Demo,看的再多也不如项目跑一遍来的快,疗效是不骗人的;

    喜欢就点个赞,也可以在下方评论一起讨论讨论

    相关文章

      网友评论

      • 3c446647e9d1:iPhone作为中心设备,基本上搜索不到别的蓝牙设备,这怎么办

      本文标题:CoreBluetooth 蓝牙开发(后台模式、状态保存与恢复)

      本文链接:https://www.haomeiwen.com/subject/isrcgftx.html