Objective-c bluetooth says it's connected but

2019-08-02 03:15发布

问题:

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.

回答1:

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 the CBPeripheral object, so that it can be re-used if a timeout happens. Here's the basic logic I would use:

  1. When you discover a new peripheral in didDiscoverPeripheral, save the object instance in a variable called peripheralForConnectionAttempt, then start a 5 second timer.
  2. If the connection completes successfully in didConnectPeripheral, set the value of peripheralForConnectionAttempt to nil.
  3. When the timer goes off, check if peripheralForConnectionAttempt is not nil, and if so, retry on this object just as if didDiscoverPeripheral 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.