BLE scan may freeze randomly

I'm currently working on an app that handle communication with a BLE device. One of my constraint is that i must connect to this device only when i have to send a command or read data. I must disconnect as soon as possible when it's done, to allow other potential users to do the same.


One of the features of the app is the following :

  • Users can enable an automatic command while the app is in the background. This automatic command triggers if the device hasn't been detected within 10 minutes.
  • My app scans until it finds my BLE device.
  • In order to keep it awake when i need it, i'm restarting the scan each time, because of the CBCentralManagerScanOptionAllowDuplicatesKey option ignorance.
  • When it's detected, i'm checking if the last detection was more than 10 minutes ago. If that's the case, i connect to the device and then write into the characteristic corresponding to the service i need.


The goal is to trigger this device when user come in range. It may happen a few minutes after being out of range such as a few hours, it depends on my users habits.

Everything is working fine this way, but sometimes (it seems like happening at random times), the scan sort of "freezes". My process is done well, but after a couple of time, i see my app scanning, but my didDiscoverPeripheral: call back is never called, even if my testing device is right in front of my BLE device. Sometimes it may take a while to detect it, but here, nothing happens after a couple of minutes.


I was thinking that iOS may have killed my app to claim back memory, but when i turn off and on Bluetooth, centralManagerDidUpdateState: is called the right way. If my app where killed, it shouldn't be the case right ? If i open my app, the scan is restarted and it's coming back to life. I also checked that iOS doesn't shutdown my app after 180 seconds of activity, but that's not the case because it's working well after this amount of time.


I've set up my .plist to have the right settings (bluetooth-central in UIBackgroundModes). My class managing all BLE processing is stored in my AppDelegate as a singleton accessible through all of my app. I've also tested to switch where i'm creating this object. Currently i'm creating it in the application:didFinishLaunchingWithOptions: method. I tried to put it in my AppDelegateinit: but the scans fails every time while i'm in background if i do so.

I don't know which part of my code i could show you to help you better understand my process. Here is some samples that might help.

Please note that " AT_appDelegate " is a maccro in order to access my AppDelegate.


UPDATE 08/06 :

  • Note that it's not an advertising issue since our BLE device is consistently powered, and we used the strongest BLE electronic card we could found.
  • It's also not an issue with iOS detection timing in background. I waited for a very long time(20~30min) to be sure that wasn't this issue.


// Init of my DeviceManager class that handles all BLE processing 
- (id) init {
     self = [super init];
     self.autoConnectTriggered = NO;
     self.isDeviceReady = NO;
     self.connectionUncomplete = NO;
     self.currentCommand = NONE;
     self.currentCommand_index = 0;
     self.signalOkDetectionCount = 0;
     self.connectionFailedCount = 0;
     self.main_uuid = [CBUUID UUIDWithString:MAINSERVICE_UUID];     
     self.peripheralsRetainer = [[NSMutableArray alloc] init];     
     self.lastDeviceDetection = nil;
     dispatch_queue_t queue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);     
     self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:queue];
     [self startScanning];
     return self; 
}

- (void) startScanning {
     if (!self.isScanning && self.centralManager.state == CBCentralManagerStatePoweredOn) {
         CLS_LOG(@"### Start scanning ###");
         self.isScanning = YES;

         NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:!self.isBackground] forKey:CBCentralManagerScanOptionAllowDuplicatesKey];
         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{             
               [self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];
         });     
     } 
} 


- (void) stopScanningAndRestart: (BOOL) restart {
     CLS_LOG(@"### Scanning terminated ###");     
     if (self.isScanning) {
         self.isScanning = NO;
         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{             
               [self.centralManager stopScan];
         });         
          
          if (!self.isWaitingNotifiy && !self.isSynchronizing && self.currentCommand == NONE ) {
             if (self.deviceToReach == nil && !self.isBackground) {
                  if (![self isDeviceStillConnected]) {                     
                     CLS_LOG(@"--- Device unreachable for view ---");              
                  } else {
                     self.isDeviceInRange = YES;
                     self.deviceToReach = AT_appDelegate.user.device.blePeripheral;
                  }                 

                  [self.delegate performSelectorOnMainThread:@selector(updateView) withObject:nil waitUntilDone:YES];
              }
              
              self.deviceToReach = nil;             
              self.isDeviceInRange = NO;             
              self.signalOkDetectionCount = 0;
              
              if ([[NSDate date] timeIntervalSinceReferenceDate] - [self.lastDeviceDetection timeIntervalSinceReferenceDate] > AUTOTRIGGER_INTERVAL) {                 
                    CLS_LOG(@"### Auto trigger is enabled ###");                     
                    self.autoConnectTriggered = NO;
              }
         }     
     }     


     if (restart) {
          [self startScanning];
     } 
}

- (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
     CLS_LOG(@"### : %@ -- %@", peripheral.name, RSSI);
    BOOL deviceAlreadyShown = [AT_appDelegate isDeviceAvailable];
   
    // If current device has no UUID set, check if peripheral is the right one
    // with its name, containing his serial number (macaddress) returned by
    // the server on remote adding
    NSString *p1 = [[[peripheral.name stringByReplacingOccurrencesOfString:@":" withString:@""] stringByReplacingOccurrencesOfString:@"Extel " withString:@""] uppercaseString];
    NSString *p2 = [AT_appDelegate.user.device.serial uppercaseString];
    if ([p1 isEqualToString:p2]) {
        AT_appDelegate.user.device.scanUUID = peripheral.identifier;
    }
   
    // Filter peripheral connection with uuid
    if ([AT_appDelegate.user.device.scanUUID isEqual:peripheral.identifier]) {
   
        if (([RSSI intValue] > REQUIRED_SIGNAL_STRENGTH && [RSSI intValue] < 0) || self.isBackground) {
       
            self.signalOkDetectionCount++;
            self.deviceToReach = peripheral;
            self.isDeviceInRange = (self.signalOkDetectionCount >= REQUIRED_SIGNAL_OK_DETECTIONS);
           
            [peripheral setDelegate:self];


            // Reset blePeripheral if daughter board has been switched and there were
            // not enough time for the software to notice connection has been lost.
            // If that was the case, the device.blePeripheral has not been reset to nil,
            // and might be different than the new peripheral (from the new daugtherboard)
           
            if (AT_appDelegate.user.device.blePeripheral != nil) {
                if (![AT_appDelegate.user.device.blePeripheral.name isEqualToString:peripheral.name]) {
                    AT_appDelegate.user.device.blePeripheral = nil;
                }
            }
           
            if (self.lastDeviceDetection == nil ||
                ([[NSDate date] timeIntervalSinceReferenceDate] - [self.lastDeviceDetection timeIntervalSinceReferenceDate] > AUTOTRIGGER_INTERVAL)) {
                self.autoConnectTriggered = NO;
            }
           
            [peripheral readRSSI];
            AT_appDelegate.user.device.blePeripheral = peripheral;
            self.lastDeviceDetection = [NSDate date];
           
            if (AT_appDelegate.user.device.autoconnect) {
                if (!self.autoConnectTriggered && !self.autoTriggerConnectionLaunched) {
                    CLS_LOG(@"--- Perform trigger ! ---");
                    self.autoTriggerConnectionLaunched = YES;
                    [self executeCommand:W_TRIGGER onDevice:AT_appDelegate.user.device]; // trigger !
                    return;
                }
            }
        }
       
        if (deviceAlreadyShown) {
            [self.delegate performSelectorOnMainThread:@selector(updateView) withObject:nil waitUntilDone:YES];
        }
    }
   
    if (self.isBackground && AT_appDelegate.user.device.autoconnect) {
        CLS_LOG(@"### Relaunch scan ###");
        [self stopScanningAndRestart:YES];
       
    }

Do you have anything else running that is using BLE?


I've found an bug in iOS 8 (which is still there in the current minor-release) wherein if you have both a Debug build application trying to scan, and a Release build (i.e. App Store) application, the Debug application will stop receiving scans; this presents as the callback no longer being called, precisely the way you describe.

I almost thought it was the solution ! I had an another app running (but i don't think it was using BLE), Light Blue, that i use for testing purpose (it's a little scanner that can let you read and write characteristics. I closed it, then i tryed to reproduce the scan freeze. It took a while, but i was able to make it happen again, a several times (i closed any other apps, i also uninstalled this test app to be sure).


You may be right, because it takes me much more time to get the freeze again, but unfortunately it wasn't the solution here.

Keep in mind that being connected via BLE to a desktop/laptop for Handoff or an Apple Watch will, in my experience, also potentially trigger this same issue for a debug build. If you have a secondary device, one you can guarantee isn't going to be doing Handoff or be tied to an Apple Watch, I would try the application on that and see if you get the same freeze.

My experience with CoreBluetooth has been that you can't just pick a specific connection workflow (here, scanning followed by discovery and connection) that you want to use, and then implement only that one path. You should also:


1) Register for CoreBluetooth state restoration and recover peripherals on startup.

2) Save the IDs of specific devices you are interested in and attempt to connect with them directly. You can do this at the same time you are scanning.

3) Periodically retrieve the devices offering your specific service ID that are already connected to the phone, and attempt to connect to them directly. You can do this at the same time you are scanning.


It's hard to know to for sure, but my guess is that you are running into some headbutt with #3. Your app never really connects directly to a device; CoreBluetooth connects on your behalf and multiplexes traffic among (potentially) multiple applications. So "connected to the phone" and "connected to your app" are really two different things, even though in most cases they go together.


Whenever a device is connected to the phone, it's unable to broadcast and so is unscannable. Conversely, CoreBluetooth sometimes seems to count any connected device as "already discovered" for scanning purposes, so even if the phone/app race condition is transient and momentary, it may leave the app not receiving didDiscoverPeripheral messages for a device.


I have a similar "always looking to connect with a particular device" application, and with this multi-pronged connection strategy, it is pretty much bulletproof. Here's my basic Bluetooth connector class you can use as a model (though I don't hold it up as any sort of example).

Hello,


Sorry for my late answer, i was working on something else.


I ended up implementing your step 1 & 2. I was able to check that 3 wasn't part of my issue : when i see my scan "freeze", i try to scan with a BLE tool (Light BLE on iOS) to see if my device is advertising. Each time i saw my scan "freeze", my device was advertising. So it's not something related to connection state of my BLE device. I didn't get through -(void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary *)state while scanning in the background, even after a "freeze". My aim here is to get the didDiscover callback each time i'm close to my device. The trick here is i get to relaunch the scan after i discover my device in order to keep scanning active. The use case is the following : when my device is not in range for more than 10 minutes, the next time i get close to it (my app must always be scanning for it), i send automatically a command to my device.


I also try, as apple suggested to me by their technical support, to change the advertising interval of my device with values suggested in Bluetooth Design Guidelines (part 3.5). I did a whole bunch of tests and my scan was freezing each time after 10s to 3min being in background.

BLE scan may freeze randomly
 
 
Q