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];
}