对 BLE 外设(Peripheral)自动重连机制的逻辑梳理

欢迎访问我的博客 muhlenXi,该文章出自我的博客,欢迎转载,转载请注明来源: http://muhlenxi.com/2017/07/07/About-Ble-Auto-Connecting*

导语:

近期一直在做 BLE 蓝牙手环的一些项目,也积累了一些经验,为了不再被这些问题劳生伤神,还是记录下来比较靠谱。目前,CoreBluetooth 与有手环两种连接方式,一种是连接后就可以直接通信了,另一种则是,连接后需要配对,配对后就可以通信了。本文会针对两种不同的机制的自动重连方式进行探索和记录。

到目前为止,感受最深的是,敲代码跟写作文有点类似,有思路的时候,行云流水,一气呵成。没思路的时候,就跟挤牙膏似得,还问题不断。为了提高以后行云流水的能力,就把一些比较绕的逻辑记录下来。

【1】直接连接通信(无需配对的情况)

这种情况,就是每次都得 Scan Peripheral ,找到你想要的 peripheral 后,然后进行 connect 操作,connect 成功后,就可以为所欲为了。

这种情况下,当 peripheral 因为一些原因断开连接或者用户主动断开连接导致连接中断,CoreBluetooth 会调用 - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error 代理方法,你若你想要自动重连的话,需要进行以下操作。

步骤 1

创建一个 断开原因 标志位,用于标识断开的原因,用于判断断开原因是用户主动断开的?还是其他原因断开的?

比如:

@property (nonatomic,copy) NSString * disConnectedState; //!< 1-其他原因断开,自动重连 3-手动断开,不重连

当然,这种情况 enum 是最合理的方式。

步骤 2

创建一个 连接方式 标志位,用于标识连接的方式,用于判断连接方式是用户点击 Peripheral 列表连接的?还是自动重连以前连接过的 Peripheral ?

比如:

@property (nonatomic,copy) NSString * connectedMethod;  //!<  1 - 自动重连, 2 - 点击连接

当然,这种情况 enum 是最合理的方式。

步骤 3

通过 NSUserDefaults 记录曾经连接过的 Peripheral 的名字,前提是这个名字可以唯一标识这个 peripheral。

比如,在连接成功的代理方法中保存 peripheral 的名字。

// 保存外设名字
[[NSUserDefaults standardUserDefaults] setObject:peripheral.name forKey:@"PeripheralName"];
步骤 4

- (void)centralManagerDidUpdateState:(CBCentralManager *)central 代理方法中决定是否要调用 60秒重连方法自动重连方法,并且在自动重连方法中需要将连接方式标志位设为自动重连方式。

- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    switch (central.state) {
        case CBCentralManagerStateUnknown:
            NSLog(@">>> CBCentralManagerStateUnknown");
            break;
        case CBCentralManagerStateResetting:
            NSLog(@">>> CBCentralManagerStateResetting");
            break;
        case CBCentralManagerStateUnsupported:
            NSLog(@">>> CBCentralManagerStateUnsupported");
            break;
        case CBCentralManagerStateUnauthorized:
            NSLog(@">>> CBCentralManagerStateUnauthorized");
            break;
        case CBCentralManagerStatePoweredOff:
            NSLog(@">>> CBCentralManagerStatePoweredOff");
            break;
        case CBCentralManagerStatePoweredOn:
        {
            NSLog(@">>> CBCentralManagerStatePoweredOn");
            // 其他原因断开 并且 peripheral 的名字不为 nil
            NSString * existName = [[NSUserDefaults standardUserDefaults] objectForKey:@"PeripheralName"];
            if ([self.disConnectedState isEqualToString:@"1"] && existName != nil) {
                [self autoScanAndConnectedToExistPeripheralOnOneMinutes];
            }
        }
            break;
        default:
            break;
    }
}

以下为60S自动重连方法

// 自动去搜索之前连过的外设并尝试一分钟重连
- (void) autoScanAndConnectedToExistPeripheralOnOneMinutes
{
    // 如果之前连接的 peripheral 为 nil,则需要先搜索到这个 peripheral 后再连接,如果不为 nil ,则直接连接处理。
    if (self.peripheral == nil) {
         [self.centralManager scanForPeripheralsWithServices:nil options:nil];
    } else {
        [self.centralManager connectPeripheral:self.peripheral options:nil];
    }
    
    // 正在连接的加载动画
    [self.centralManager scanForPeripheralsWithServices:nil options:nil];
    [SVProgressHUD setDefaultMaskType:SVProgressHUDMaskTypeBlack];
    [SVProgressHUD setDefaultStyle:SVProgressHUDStyleCustom];
    [SVProgressHUD setBackgroundColor:[UIColor blackColor]];
    [SVProgressHUD setForegroundColor: [UIColor whiteColor]];
    [SVProgressHUD showWithStatus:NSLocalizedString(@"In the connection...", @"连接中")];
    
    // 连接方式为自动重连
    self.connectedMethod = @"1";
    
    // 60秒后超时处理
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(60 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        // 如果 60 S后还未找到连接过的 peripheral 则取消搜索和连接。
        if (self.peripheral == nil ) {
            
            // 停止搜索
            [self.centralManager stopScan];

            // 取消连接,为了避免死循环,该取消连接方式为主动断开连接
            self.disConnectedState = @"3";
            [self.centralManager cancelPeripheralConnection:self.peripheral];
            
            [SVProgressHUD dismiss];
            
        }    
    });
}

如果是用户点击连接的话,需要在 Cell 的 didSelectRowAtIndexPath 代理方法中进行连接并设置连接方式标志。

//点击方法
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 取消连接
    [[AppDelegate mainDelegate] cancelScanPeripherals];
    
    // 保存用户指定的 Peripheral
    CBPeripheral * per = [AppDelegate mainDelegate].bleArray[indexPath.row];
    [AppDelegate mainDelegate].peripheral = per;
    
    [SVProgressHUD showWithStatus:NSLocalizedString(@"Linking Bluetooth devices...", @"name")];
    
    // 设置连接方式为用户点击连接方式
    [AppDelegate mainDelegate].connectedMethod = @"2";
    
    // 连接外设
    [[AppDelegate mainDelegate] connectToPeripheralOnOneMinutes];
    
}
步骤 5

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI 扫描外设代理方法中区别对待不同连接方式的Peripheral,如果是自动重连模式下的,发现 Peripheral 后直接连接,如果仅仅是搜索模式下的,仅仅添加到外设列表数组就可以了。

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI
{
    // 自动重连发现设备直接连接
    if ([self.connectedMethod isEqualToString:@"1"]) {
        NSString * name = [[NSUserDefaults standardUserDefaults] objectForKey:@"PeripheralName"];
        if ([peripheral.name isEqualToString:name]) {
            
            NSLog(@"自动重连搜索到了 %@ ,正在重连。。。",name);
            self.peripheral = peripheral;
            [central connectPeripheral:peripheral options:nil];
        }
        
    }
    
    // 不是自动重连,则将满足条件的 peripheral 加入到外设列表数组中
    if ([peripheral.name hasPrefix:@"SMART_"]) {
        NSLog(@"DiscoverPeripheral--%@",peripheral.name);
        if (![self.bleArray containsObject:peripheral] ) {
            [self.bleArray addObject:peripheral];
            [[NSNotificationCenter defaultCenter] postNotificationName:@"BleArrayChanged" object:nil];
        }
    }
}
步骤 6

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral 连接成功代理方法中分别执行对应操作,如果是自动重连,不需要执行相应的界面跳转操作,如果是用户点击 Peripheral 列表连接成功,则需要进行相应界面跳转操作。

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    NSLog(@"--连接成功--%@",peripheral.name);
    
    // 保存外设名字
    [[NSUserDefaults standardUserDefaults] setObject:peripheral.name forKey:@"PeripheralName"];
    
    // 这是重连成功,不进行页面跳转
    if ([self.connectedMethod isEqualToString:@"1"]) {
    
        // 更新外设状态信息
 
    } else if ([self.connectedMethod isEqualToString:@"2"]) {
    
       // 用户点击点击Peripheral 列表连接成功
       
       // 执行相应界面跳转操作
    }
    
    self.peripheral.delegate = self;
    [self.peripheral discoverServices:nil];
      
}

步骤 7

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error 断开连接代理方法中执行对应操作,如果是用户断开连接,则不需要自动重连,如果是其他原因断开连接的,则需要断开连接。

// 外设断开连接
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    NSLog(@"--断开连接 -- %@",peripheral.name);
    
    // 更新 外设状态信息
     
    // 用户主动断开连接
    if ([self.disConnectedState isEqualToString:@"3"]) {
        // do nothing
        return;
    }
    
    [self connectToPeripheralOnOneMinutes];
}

步骤 8

在用户主动断开连接的方法中,保存断开方式标志,并断开连接。

//断开连接
- (void)StopConnectedButtonDidClicked:(id) sender
{
     // 手动断开方式
    [AppDelegate mainDelegate].disConnectedState = @"3";
    
    [[AppDelegate mainDelegate].centralManager cancelPeripheralConnection:[AppDelegate mainDelegate].peripheral];
    
}

经过以上这些步骤,一个自动重连机制就理清了。

【2】连接后需要配对的情况

思路:当你与 Peripheral 配对后,只要你的 Peripheral 在手机的连接范围内,系统会去自动连接你的手环,因此,你需要在连接成功后,保存 Peripheral 的UUIDString。如果想要在 APP 启动后能自动连接之前配对的 Peripheral,你需要执行以下操作。

代码如下:

- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    switch (central.state) {
        case CBCentralManagerStateUnknown:
            NSLog(@">>> CBCentralManagerStateUnknown");
            break;
        case CBCentralManagerStateResetting:
            NSLog(@">>> CBCentralManagerStateResetting");
            break;
        case CBCentralManagerStateUnsupported:
            NSLog(@">>> CBCentralManagerStateUnsupported");
            break;
        case CBCentralManagerStateUnauthorized:
            NSLog(@">>> CBCentralManagerStateUnauthorized");
            break;
        case CBCentralManagerStatePoweredOff:
            NSLog(@">>> CBCentralManagerStatePoweredOff");
            [[NSNotificationCenter defaultCenter] postNotificationName:DisconnectPeripheral object:nil];
            break;
        case CBCentralManagerStatePoweredOn:
        {
           NSLog(@">>> CBCentralManagerStatePoweredOn");
            
            // 1、获取之前配对设备的 UUID String
            NSString * uuidStr = [[NSUserDefaults standardUserDefaults] objectForKey:BleBindUUID];
            NSLog(@" bind uuidStr == %@",uuidStr);
            
            // 2、如果 UUID String 为 nil,说明之前没有配对过任何外设,则外设进行扫描操作,如果不为 nil 且不等于空,则根据 NSUUID 恢复之前配对的 Peripheral
            if (uuidStr != nil && ![uuidStr isEqualToString:@"nil"] && ![uuidStr isEqualToString:@""]) {
                
                // 根据 UUID String 生成 NSUUID 对象
                NSUUID * uuid = [[NSUUID alloc] initWithUUIDString:uuidStr];
                
                // 根据 NSUUID 恢复曾经配对过的设备
                NSArray * peripherals = [central retrievePeripheralsWithIdentifiers:@[uuid]];

                // 数组不为空,说明找到之前配对过的设备
                if (peripherals.count > 0) {
                
                        // 之前配对的 对象
                    CBPeripheral *peripheral = [peripherals firstObject];
                    
                    self.peripheral = peripheral;//**关键**需要转存外设值,才能发起连接
                    
                    self.peripheral.delegate = self;
                    NSLog(@"self.peripheral == %@",self.peripheral);
                   
                    // 60秒自动重连方法
                    [self connectingPeripheralDuringOneMinute];
                }
            }
            else {
            
                    // 开始搜索外设
                [central scanForPeripheralsWithServices:nil options:nil];
                  
                // 4s 后停止扫描
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    
                   // 停止搜索外设
                });
            }
           
        }
            break;
        default:
            break;
    } 
}

注意:当手环与外设配对连接后,通过 cancelPeripheralConnection 方法是有时是无法断开与外设的连接。你需要到系统设置里去忽略这个外设,方可消除配对状态。

结束语

欢迎在本文下面留言一起交流心得...

如果本文能给你带来一定的帮助,在自己有能力的情况下,不妨赞助一下,表示对博主辛勤耕作的支持!

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

推荐阅读更多精彩内容

  • iOS连接外设的代码实现流程 1. 建立中心角色 2. 扫描外设(discover) 3. 连接外设(connec...
    UILabelkell阅读 2,399评论 2 4
  • 这里我们具体说明一下中心模式的应用场景。主设备(手机去扫描连接外设,发现外设服务和属性,操作服务和属性的应用。一般...
    丶逝水流年阅读 2,236评论 3 4
  • 蓝牙简介 蓝牙( Bluetooth® ):是一种无线技术标准,可实现固定设备、移动设备和楼宇个人域网之间的短距离...
    Chefil阅读 2,030评论 2 19
  • 备注:下面说到的内容都基于蓝牙4.0标准以上,主要以实践为主。 ~ CoreBluetooth.framework...
    未_漆小七阅读 1,599评论 1 8
  • 本文主要以蓝牙4.0做介绍,因为现在iOS能用的蓝牙也就是只仅仅4.0的设备 用的库就是core bluetoot...
    暮雨飞烟阅读 824评论 0 2