In my app, I search for previously connected devices with the delegate method retrieveConnectedPeripheralsWithServices
first, then scan for nearby devices if nothing is found. Here is the code
- (void)searchForPeripherals {
NSLog(@"*** scanning for Bluetooth peripherals... ***");
//Check to see if any devices were connected already
if(self.ourPeripheral != nil) {
[self connectToDevice];
} else {
//Search for previously paired devices...
bool foundPairedDevices = [self checkForAndConnectToPreviouslyPairedDevices];
//None found, scan for new devices nearby...
if(!foundPairedDevices) {
NSLog(@"No paired boxes found, checking device powered state...");
//If the app user has "bluetooth" option turned on
if(self.centralManager.state == CBCentralManagerStatePoweredOn) {
NSLog(@"--- central manager powered on.");
NSLog(@"...begin scanning...");
[self.centralManager scanForPeripheralsWithServices:nil
options:@{CBCentralManagerScanOptionAllowDuplicatesKey: @(YES)}];
//No, user has "bluetooth" turned off
} else {
NSLog(@"--- central manager is NOT powered on.");
}
}
}
}
- (bool)checkForAndConnectToPreviouslyPairedDevices {
NSLog(@"our peripheral device: %@", self.ourPeripheral);
NSArray *dcBoxesFound = [self.centralManager retrieveConnectedPeripheralsWithServices:@[[CBUUID UUIDWithString:SERVICE_UUID]]];
NSLog(@"Previously paired DC boxes?: %@", dcBoxesFound);
//Are there any previously paired devices?
if(dcBoxesFound != nil && [dcBoxesFound count] > 0) {
CBPeripheral *newestBoxFound = [dcBoxesFound firstObject];
newestBoxFound.delegate = self;
self.ourPeripheral = newestBoxFound;
self.deviceUUID = [CBUUID UUIDWithString:[newestBoxFound.identifier UUIDString]];
[self connectToDevice];
return YES;
} else {
return NO;
}
}
- (void)connectToDevice {
if(self.ourPeripheral != nil) {
[self.centralManager connectPeripheral:self.ourPeripheral options:nil];
}
}
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData
RSSI:(NSNumber *)RSSI {
NSLog(@"scanned and found this peripheral: %@", peripheral);
NSLog(@"advertisment data: %@", advertisementData);
if(!self.isConnectedToDevice && [[advertisementData objectForKey:@"kCBAdvDataLocalName"] localizedCaseInsensitiveContainsString:@"dc-"]) {
NSLog(@"\n");
NSLog(@"-------------------------------------------");
NSLog(@"DC peripheral found! Attempting to connect to the following...: %@", peripheral);
peripheral.delegate = self;
self.ourPeripheral = peripheral;
self.deviceUUID = [CBUUID UUIDWithString:[peripheral.identifier UUIDString]];
[self connectToDevice];
NSLog(@"-------------------------------------------");
NSLog(@"\n");
}
}
- (void)centralManager:(CBCentralManager *)central
didConnectPeripheral:(CBPeripheral *)peripheral {
NSLog(@"--- didConnectPeripheral");
peripheral.delegate = self;
[peripheral discoverServices:@[[CBUUID UUIDWithString:SERVICE_UUID]]];
}
The Problem
When I start up the app it usually connects just fine. Everything is all good. Then at very unpredictable and odd times the BT connection gets "stuck" somehow. What I mean by stuck is that it never truely connects. The blue light on my box goes on as though the system has connected to it but in my app this is what happens.
1) The retrieveConnectedPeripheralsWithServices
method finds an empty array of previously connected devices, telling the method that called it that there are no previously connected devices found.
2) The scanForPeripheralsWithServices:nil
method is fired and begins scanning with the didDiscoverPeripheral
method.
3) The didDiscoverPeripheral
never even once discovers any boxes nearby with the prefix in kCBAdvDataLocalName
of "dc-" something or other
If I were to go into iOS settings -> bluetooth -> forget this device (have to hit it twice which makes me feel like it's "stuck" somehow) and then actually turn off the BT option all together... Wait 2 seconds and then turn it back on again... then re-launch the application, it all loads fine.
1) The app starts up
2) See's no previously connected devices
3) Scans for peripherals and finds my prefixed box name "dc-whatever"
4) Asks me to pair with BT device and then I have full functionality back
If I then shut down the app and re-launch it, the retrieveConnectedPeripheralsWithServices
finds my previously connected box without trouble and connects seamlessly...
So... what is going on here? Why does it seem to get "stuck" randomly some times?
EDIT:
I do realize that pairing and connecting are not the same thing so some method are named really poorly.
This is a common problem with Bluetooth LE programming, made harder by the fact that the APIs are all asynchronous.
The typical solution is to make the code to retry if something doesn't complete. And because you won't necessarily get error messages (you might just not get the next callback) what you end up having to do is set up timers to kick off a retry after a reasonable time period -- say 5-10 seconds.
As you've noticed, in some cases (like in the background, if the device connects, or otherwise stops advertising) you will only get one callback to
didDiscoverPeripheral
. So you really need to save off a copy of theCBPeripheral
object, so that it can be re-used if a timeout happens. Here's the basic logic I would use:didDiscoverPeripheral
, save the object instance in a variable calledperipheralForConnectionAttempt
, then start a 5 second timer.didConnectPeripheral
, set the value ofperipheralForConnectionAttempt
to nil.peripheralForConnectionAttempt
is not nil, and if so, retry on this object just as ifdidDiscoverPeripheral
had been called. You might want to have a counter to limit the retries to 10 times or something like that.Side Note:
When you say the blue light gets stuck on, this may mean that the BLE Peripheral device thinks it has established a BLE connection. Peripherals that do this typically stop advertising until the connection is broken. If the iOS device thinks it is not connected and the peripheral does, this puts you in a bad situation where you must break the connection somehow. Unfortunately, CoreLocation gives you no APIs to do this.
On one project, I have accomplished this in the bluetooth peripheral firmware by breaking the connection if a communication timeout happens there. But if you don't control the firmware, you obviously cannot do this.
Cycling power to bluetooth will break any active connections as you have seen. But on iOS, you cannot do this programmatically.
If you do not control the peripheral firmware, and cannot figure out any way to avoid locking up the connection with the device (perhaps by altering the timing of your operations?) then you may have no recourse other than to simply detect the problem, alert the user if appropriate, and instruct the user to cycle bluetooth if the use case warrants. This is hardly an ideal solution, I know.