0 Replies
      Latest reply on Nov 14, 2017 10:46 AM by RonHansen
      portunes Level 1 Level 1 (0 points)

        When using state preservation in iOS 11, if a peripheral connection is not cancelled properly the CBCentralManager used to make the connection and any future CBCentralManagers with the same restoration ID will not be able to connect to peripherals. The problem can last for days. To resolve the issue the system bluetooth must be toggled on/off, the app force quit or the restorationID must be changed.

         

        I've posted a radar for the issue here: https://bugreport.apple.com/web/?problemID=33728133

         

        I'd be great to know if anyone else has seen this issue. Also Apple folks will this problem be addressed before the first public release of iOS 11?

         

        I believe these other posts are for the same issue:

        https://forums.developer.apple.com/message/244494#244494

        https://forums.developer.apple.com/thread/83852

        Details

        1. Create a CBCentralManager with a restoration ID (i.e. State preservation enabled)
        2. Attempt to connect to a peripheral. Hold a reference to the peripheral
        3. Upon successful connection: Set the peripheral reference to nil. Do not call cancelPeripheralConnection:
        4. Attempt to connect to the peripheral again


        Expected: CBCentralManager.state continues to return CBCentralManagerStatePoweredOn. Connection can successfully be made.


        Actual: CBCentralManager.state returns CBCentralManagerStatePoweredOff or CBCentralManagerStateUnknown. Connection cannot be made.


        Configuration:

        • OS: iOS 11 - public beta 3
        • Device: iPhone 6s ( Suspected to affect all iOS 11 public beta 2+ devices)


        Notes:

        • Toggling bluetooth on/off resolves the issue. Force quitting the app will resolve the issue.
        • Killing the app via the debugger will not resolve the issue. It's suspected that any other non-force quits of the application like termination due to mermory pressure will not resolve the issue.
        • When the peripheral is not cancelled correctly the following is observed:2017-08-03 19:00:45.840993-0700 StatePreservationTest[542:298206] [CoreBluetooth] API MISUSE: Cancelling connection for unused peripheral <CBPeripheral: 0x1c01078f0, identifier = YOUR_PERIPHERAL_UUID, name = PeripheralName, state = connecting>, Did you forget to keep a reference to it?
        • The issue is not observed on iOS 10

        The problem can be solved by calling cancelPeripheralConnection: before the reference is dropped. Ideally cancelPeripheralConnection: would always call but under certain circumstances it may difficult to practically do so. This bug is particularly severe because of how long it lasts – seems to last until the bluetooth system is restarted which may be for days for some users – and how badly it hinders the use of CoreBluetooth – cental managers using the affected restoration ID are rendered unusable. Finally even if cancelPeripheralConnection: is required for cancelling a single connection, failing to call it should not render the CBCentralManager unusable for other connections.

         

        For details see the code below which will cause the problem.

         

        //DemoViewController.m
        
        #import "DemoViewController.h"
        #import <CoreBluetooth/CoreBluetooth.h>
        
        NSString * const kDemoPeripheralUUID = @"YOUR_PERIPHERAL_UUID";
        
        
        @interface DemoViewController () <CBCentralManagerDelegate>
        
        @property (weak, nonatomic) IBOutlet UITextView *debugTextView;//A textview that shows the debug output
        @property (weak, nonatomic) IBOutlet UIButton *findPeripheralButton;//A button that starts a connection to the peripheral when tapped. Its disabled while the connection is being attempted
        
        @property(nonatomic)NSMutableString *mutableLogs;
        
        @property(nonatomic)CBCentralManager *centralManager;
        @property(nonatomic)CBPeripheral *peripheral;
        
        @end
        
        @implementation DemoViewController
        
        - (void)viewDidLoad {
            [super viewDidLoad];
            self.mutableLogs = [NSMutableString new];
            [self addLogEvent:@"View Loaded"];
        }
        
        #pragma mark - User Actions
        
        - (IBAction)findPeripheralTapped:(id)sender {
            self.findPeripheralButton.enabled = NO;
            [self addLogEvent:@"Find Peripheral Tapped"];
            [self startConnectingToPeripheral];
        }
        
        #pragma mark - CoreBluetooth
        
        -(void)startConnectingToPeripheral{
        
            if (!self.centralManager) {
                self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey:@"MyRestorationID"}];
            }
        
            if (self.centralManager.state == CBManagerStatePoweredOn) {
                NSArray *allPeripherals = [self.centralManager retrievePeripheralsWithIdentifiers:@[[[NSUUID alloc] initWithUUIDString:kDemoPeripheralUUID]]];
                CBPeripheral *peripheral = allPeripherals.firstObject;
                if (!peripheral) {
                    [self addLogEvent:@"Peripheral Not Found in Cache"];//For the sake of brevity in this demo, the peripheral must be in the BLE cache.
                    return;
                }
        
                self.peripheral = peripheral;
                [self.centralManager connectPeripheral:self.peripheral options:nil];
                [self addLogEvent:@"Connecting Peripheral"];
            }
        }
        
        -(void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary<NSString *,id> *)dict{
            //We dont't actually use this method but it must be implemented.
        }
        
        -(void)centralManagerDidUpdateState:(CBCentralManager *)central{
            if (central.state == CBManagerStatePoweredOn) {
                [self startConnectingToPeripheral];
            }
        }
        
        -(void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{
            [self addLogEvent:@"Found peripheral"];
            self.findPeripheralButton.enabled = YES;
        
            //COMMENTING OUT THE BELOW CAUSES THE BUG. If `cancelPeripheralConnection:` is not called, the CBCentralManager and all future CBCentralManagers with the same restoration ID will be broken (CBCentralManagerState will always be CBCentralManagerStateOff).
        //    [self.centralManager cancelPeripheralConnection:self.peripheral];
        
            self.centralManager = nil;
            self.peripheral = nil;
        }
        
        #pragma mark - Logging
        -(void)addLogEvent:(NSString *)event{
            NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
            [dateFormatter setDateFormat:@"HH:mm:ss.SSS"];
        
            NSString *lineToAdd = [dateFormatter stringFromDate:[NSDate date]];
            lineToAdd = [lineToAdd stringByAppendingString:@"| "];
            lineToAdd = [lineToAdd stringByAppendingString:event];
            [self.mutableLogs appendString:[NSString stringWithFormat:@"%@\n", lineToAdd]];
            self.debugTextView.text = self.mutableLogs;
        }
        
        @end