9

CoreBluetooth state preservation issue: willRestoreState not called in iOS 7.1

Hey all. I’ve been working on a Bluetooth LE project for the past few weeks, and hit a roadblock. I have been unable to get state restoration working properly in iOS 7 / 7.1. I’ve followed (I think) all of the steps Apple lays out, and got some clues on other stack overflow posts.

  1. I added the proper bluetooth permissions to the plist
  2. when I create my central manager, I give it a restore Identifier key.
  3. I always instantiate the CM with the same key
  4. I added the willRestoreState function to the CM delegate

My Test Case:

  1. Connect to peripheral
  2. Confirm connection
  3. Simulate Memory Eviction (kill(getpid(), SIGKILL);)
  4. Transmit Data

Results iOS 7:

The app would respond in the AppDelegate didFinishLaunchingWithOptions function, but the contents of the NSArray inside of launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey] was always an empty array.

Results on iOS 7.1:

Progress! I can see my CentralManager key in the UIApplicationLaunchOptionsBluetoothCentralsKey array 100% of the time, but willRestoreState is never called.

Code:

//All of this is in AppDelegate for testing

@import CoreBluetooth;
@interface AppDelegate () <CBCentralManagerDelegate, CBPeripheralDelegate>
@property (readwrite, nonatomic, strong) CBCentralManager *centralManager;
@end

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

    self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey:@“myCentralManager”}];

    //Used to debug CM restore only
    NSArray *centralManagerIdentifiers = launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
    NSString *str = [NSString stringWithFormat: @"%@ %lu", @"Manager Restores: ", (unsigned long)centralManagerIdentifiers.count];
    [self sendNotification:str];
    for(int i = 0;i<centralManagerIdentifiers.count;i++)
    {
        [self sendNotification:(NSString *)[centralManagerIdentifiers objectAtIndex:i]];
    }

    return YES;
}

- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary *)state {
    activePeripheral = [state[CBCentralManagerRestoredStatePeripheralsKey] firstItem];
    activePeripheral.delegate = self;

    NSString *str = [NSString stringWithFormat: @"%@ %lu", @"Device: ", activePeripheral.UUID];
    [self sendNotification:str];
}

//sendNotification is a func that creates a local notification for debugging when device connected to comp

When I run the tests, didFinishLaunchWithOptions is called 100% when my BLE device communicates to the phone when the app is not in memory, but willRestoreState is never called.

Any and all help would be great! thanks!

allprog
  • 16,540
  • 9
  • 56
  • 97
cookieman459
  • 91
  • 1
  • 2
  • Since the `UIApplicationLaunchOptionsBluetoothCentralsKey` exists it should be being called. Is it possible that `centralManager:willRestoreState:` is running on a different thread and your `sendNotification:` call just doesn't work in this context? Can you try wrapping it in a dispatch_async(dispatch_get_main_queue(), ^() { ... }); ? Alternatively, try just using an NSLog as well? – Sandy Chapman Mar 24 '14 at 10:37
  • Are you solved that problem? how call this willRestoreState: ? What is activePeripheral in willRestorestate and what it is doing here? – Adeel Ishaq Jan 28 '15 at 08:20

4 Answers4

15

Okay, so I've had to delete two of my answers already to this question. But I think I've finally figured it out.

This comment is the key to your problem. Essentially, this centralManager:willRestoreState: only gets called if it's force closed by the OS while an outstanding operation is in progress with a peripheral (this does not include scanning for peripherals On further investigation, if you're scanning for a service UUID and the app is killed in the same way, or you've already finished connecting, it will in fact call your delegate).

To replicate: I have a peripheral using CoreBluetooth set up on my MacBook. I advertise on the peripheral, and have the central on my iPhone discover it. Then, leaving the OSX peripheral app running, kill your BT connection on your Mac and then initiate a connect from the central on your iOS device. This obviously will continuously run as the peripheral is non-reachable (apparently, the connection attempt can last forever as Bluetooth LE has no timeout on connections). I then added a button to my gui and hooked it up to a function in my view controller:

- (IBAction)crash:(id)sender
{
    kill(getpid(), SIGKILL);
}

This will kill the app as if it was killed by the OS. Once you are attempting to connect tap the button to crash the app (sometimes it takes two taps).

Activating Bluetooth on your Mac will then result in iOS relaunching your app and calling the correct handlers (including centralManager:willRestoreState:).

If you want to debug the handlers (by setting a breakpoint), in Xcode, before turning BT on on your Mac, set a breakpoint and then select 'Debug > Attach to Process... > By Process Identifier or Name...'.

In the dialog that appears, type the name of your app (should be identical to your target) and click "Attach". Xcode will then say waiting for launch in the status window. Wait a couple seconds and then turn on BT on OSX. Make sure your peripheral is still advertising and then iOS will pick it up and relaunch your app to handle the connection.

There are likely other ways to test this (using notify on a characteristic maybe?) but this flow is 100% reproducible so will likely help you test you code most easily.

Community
  • 1
  • 1
Sandy Chapman
  • 11,133
  • 3
  • 58
  • 67
  • Solid detail here. All of my reading points to an active scan or an active connection should be "cached" (and thus trigger the willRestoreState Call). Bad news is I implemented the Kill command for testing and no luck . . . I'm using an arduino based BLE module for testing, and the only way I can get it to "work" (not really) is if the BLE module doesn't get confirmation from the phone that the data was received, then start repeating the call. that "works" but not in the way it should. – cookieman459 Mar 23 '14 at 03:01
  • Also, wasn't 100% clear, I added the Kill for initial debugging, so my issue has been there from the beginning of my project cycle. – cookieman459 Mar 23 '14 at 03:08
3

Had the same issue. From what I can work out, you need to use a custom dispatch queue when instantiating your CBCentralManager and your willRestoreState method will be triggered. I think this is due to async events not being handled by the default queue (when using "nil") when your app is started by the background recovery thread.

    ...
    dispatch_queue_t centralQueue = dispatch_queue_create("com.myco.cm", DISPATCH_QUEUE_SERIAL);

    cm = [[CBCentralManager alloc] initWithDelegate:self queue:centralQueue options:@{CBCentralManagerOptionRestoreIdentifierKey:@"cmRestoreID",CBCentralManagerOptionShowPowerAlertKey:@YES}];

    ...
miniman42
  • 43
  • 6
  • So, this appears to solve my problem. Downside is the responsiveness on the app side is greatly delayed, but the BLE peripheral gets the response more reliably. Thanks! I'll follow up if I have more issues / questions. – cookieman459 May 10 '14 at 05:38
0

You need to move the CentralManager to its own queue.

My1
  • 199
  • 1
  • 10
-2

You also need to instantiate your peripheral with a restore identifier.

akaru
  • 6,299
  • 9
  • 63
  • 102
  • Not fully clear on what you mean. When a peripheral is discovered there isn't a place to set a restore identifier for the peripheral itself. The only place I have found any references to restore identifiers are on the central and peripheral managers. Also, not sure how that would impact the "willRestoreState" function never being called. My understanding is that if there is a manager to restore, willRestoreState is called, and provides more data as to what the central manager has to restore. Do you have an code example for what you mean? – cookieman459 Mar 16 '14 at 05:32