UPDATE 14/08 - 3 - Found the real solution:
You can check out the solution in the answers below !
UPDATE 16/06 - 2 - May be the solution :
As Sandy Chapman suggested in comments in his answer, i'm able now to retrieve my peripheral at the start of a scan by using this method :
- (NSArray<CBPeripheral *> * nonnull)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> * nonnull)identifiers
I'm actually trying to make it work by getting my peripheral back at the start of a scan and launching a connection (even if it's not in range) when i need it. iOS will keep it alive until it finds the device i'm looking for.
Please also note that there might be a bug in iOS 8.x that keep an app with debug build from scanning (disappearing callbacks as i get) from time to time if there is another app in background with release build that is using Bluetooth.
UPDATE 16/06 :
So i checked with retrievePeripheralsWithServices if any device was connected while i'm starting the scan. When i get the bug, i launch the app and the first thing i do in
- (void) applicationDidBecomeActive:(UIApplication *)application
is to check the size of the returned array. It's always 0, each time i get the bug. The bug can happen also if my device hasn't made any connection earlier in the current run. I'm also able to see my device advertising and trigger a command with a second device while i got the bug with another device.
UPDATE 10/06 :
UPDATE 08/06 :
ORIGINAL QUESTION
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 :
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 AppDelegate init: 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.
// Init of my DeviceManager class that handles all BLE processing
- (id) init {
self = [super init];
// Flags creation
self.autoConnectTriggered = NO;
self.isDeviceReady = NO;
self.connectionUncomplete = NO;
self.currentCommand = NONE;
self.currentCommand_index = 0;
self.signalOkDetectionCount = 0; // Helps to find out if device is at a good range or too far
self.connectionFailedCount = 0; // Helps in a "try again" process if a command fails
self.main_uuid = [CBUUID UUIDWithString:MAINSERVICE_UUID];
self.peripheralsRetainer = [[NSMutableArray alloc] init];
self.lastDeviceDetection = nil;
// Ble items creation
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;
}
// The way i start the scan
- (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];
});
}
}
// The way i stop and restart the scan after i've found our device. Contains some of foreground (UI update) process that you can ignore
- (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];
});
// Avoid clearing the connection when waiting for notification (remote + learning)
if (!self.isWaitingNotifiy && !self.isSynchronizing && self.currentCommand == NONE ) {
// If no device found during scan, update view
if (self.deviceToReach == nil && !self.isBackground) {
// Check if any connected devices last
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];
}
// Reset var
self.deviceToReach = nil;
self.isDeviceInRange = NO;
self.signalOkDetectionCount = 0;
// Check if autotrigger needs to be done again - If time interval is higher enough,
// reset autoConnectTriggered to NO. If user has been away for <AUTOTRIGGER_INTERVAL>
// from the device, it will trigger again next time it will be detected.
if ([[NSDate date] timeIntervalSinceReferenceDate] - [self.lastDeviceDetection timeIntervalSinceReferenceDate] > AUTOTRIGGER_INTERVAL) {
CLS_LOG(@"### Auto trigger is enabled ###");
self.autoConnectTriggered = NO;
}
}
}
if (restart) {
[self startScanning];
}
}
// Here is my detection process, the flag "isInBackground" is set up each time the app goes background
- (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];
}
}
In your sample code, it doesn't look like you're calling either of these methods on your CBCentralManager:
- (NSArray<CBPeripheral *> * nonnull)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> * nonnull)serviceUUIDs
- (NSArray<CBPeripheral *> * nonnull)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> * nonnull)identifiers
It's possible that you're in a state where you're waiting on a didDiscoverPeripheral
that will never come because the system is already connected. After instantiating your CBCentralManager, first check to see if your peripheral is already connected by calling one of these methods.
The start-up flow for my central manager which uses state restoration is as follows:
One additional thing tip that you may have already discovered: iOS will cache the characteristics and UUIDs advertised by a peripheral. If these change, the only way to clear the cache is to toggle bluetooth off and on in the iOS system settings.
After many many tries, i may finally have found the real solution.
FYI, i had many talks with an engineer at Apple (via Technical Support). He led me in two ways :
The real solution may take part in these elements. I added and corrected things while i was searching to clean this bug. But the thing that made it work (i tested it for around 4-5h and it didn't froze at all) was about dispatch_queue.
What i did before :
// Initialisation
dispatch_queue_t queue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:queue options:@{CBCentralManagerOptionRestoreIdentifierKey:RESTORE_KEY}];
// Start scanning
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];
});
// Stop scanning
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.centralManager stopScan];
});
What i'm doing now :
// Initialisation
self.bluetoothQueue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:self.bluetoothQueue options:@{CBCentralManagerOptionRestoreIdentifierKey:RESTORE_KEY}];
// Start scanning
dispatch_async(self.bluetoothQueue, ^{
[self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];
});
// Stop scanning
dispatch_async(self.bluetoothQueue, ^{
[self.centralManager stopScan];
});
Please note that i added this line to my DeviceManager.h (my main Ble class) :
@property (atomic, strong) dispatch_queue_t bluetoothQueue;
As you can see it was a little messed up :)
So now i'm able to scan as long as needed. Thanks for your help ! I hope it might help someone one day.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With