How to detect nearby devices with Bluetooth LE in iOS 7.1 both in background and foreground?

Markus Rautopuro picture Markus Rautopuro · Jun 22, 2014 · Viewed 8.6k times · Source

I have an app that needs to detect a nearby (in range for Bluetooth LE) devices running the same application and iOS 7.1. I've considered two alternatives for the detection:

  1. Having the devices act as iBeacons and detect iBeacons in range
  2. Using CoreBluetooth (like in Vicinity implementation here) to create a BLE peripheral, advertise that and scan the peripherals

It seems that the option 1 is out of the question because:

  • It may take at least 15 minutes for iOS to detect entering a beacon region when the application is running background (iOS 7.1)

Option 2 seems the way to go, but there are some difficulties regarding the implementation:

  • iOS seems to change the peripheral UUID in advertisement packets after a certain period of time (around 15 minutes?). This means that it's not directly possible to identify the advertising device from the advertisement broadcast signal.

Regarding this, I have the following questions:

  • Are there any other methods of implementing the nearby device detection I haven't considered?
  • Is it possible to identify the device through advertising (or by some other means) so that option 2 would work?

Answer

Markus Rautopuro picture Markus Rautopuro · Jun 22, 2014

I found a way to make this work Core Bluetooth (option 2), the procedure is roughly the following:

  • The application advertises itself with an encoded device unique identifier in CBAdvertisementDataLocalNameKey (when the broadcasting application runs foreground) and a characteristic that provides the device unique identifier through a Bluetooth LE service (when the broadcasting application runs background)
  • At the same time, the application scans other peripherals with the same service.

The advertising works as follows:

  • For the other devices to be able to identify this device, I use a per-device unique UUID (I'm using Urban Airship's [UAUtils deviceID], because it's the device identifier in other parts of the program, also - but you might as well use any unique ID implementation).
  • When the application is running foreground, I can pass the device unique ID directly in the advertisement packet by using CBAdvertisementDataLocalNameKey. The standard UUID representation is too long, so I use a shortened form of the UUID as follows:

    + (NSString *)shortenedDeviceID
    {
        NSString *deviceID = [UAUtils deviceID];
    
        NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:deviceID];
        uuid_t uuidBytes;
        [uuid getUUIDBytes:uuidBytes];
    
        NSData *data = [NSData dataWithBytes:uuidBytes length:16];
        NSString *base64 = [data base64EncodedStringWithOptions:0];
        NSString *encoded = [[[base64
                               stringByReplacingOccurrencesOfString:@"/" withString:@"_"]
                              stringByReplacingOccurrencesOfString:@"+" withString:@"-"]
                             stringByReplacingOccurrencesOfString:@"=" withString:@""];
        return encoded;
    }
    
  • When the application is running background, the advertisement packet gets stripped and CBAdvertisementDataLocalNameKey is not passed along anymore. For this, the application needs to publish a characteristic that provides the unique device identifier:

    - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
    {
        if (peripheral.state == CBPeripheralManagerStatePoweredOn) {
            [self startAdvertising];
    
            if (peripheralManager) {
                CBUUID *serviceUUID = [CBUUID UUIDWithString:DEVICE_IDENTIFIER_SERVICE_UUID];
                CBUUID *characteristicUUID = [CBUUID UUIDWithString:DEVICE_IDENTIFIER_CHARACTERISTIC_UUID];
                CBMutableCharacteristic *characteristic =
                [[CBMutableCharacteristic alloc] initWithType:characteristicUUID
                                                   properties:CBCharacteristicPropertyRead
                                                        value:[[MyUtils shortenedDeviceID] dataUsingEncoding:NSUTF8StringEncoding]
                                                  permissions:CBAttributePermissionsReadable];
                CBMutableService *service = [[CBMutableService alloc] initWithType:serviceUUID primary:YES];
                service.characteristics = @[characteristic];
                [peripheralManager addService:service];
            }
        }
    }
    

The scanning works as follows:

  • You start to scan peripherals with the certain service UUID as follows (notice that you need to specify the service UUID, because otherwise background scan fails to find the device):

    [self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:DEVICE_IDENTIFIER_SERVICE_UUID]]
        options:scanOptions];
    
  • When a device is discovered at - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI you check that if advertisementData[CBAdvertisementDataLocalNameKey] exists and try to convert it back to UUID form like this:

    + (NSString *)deviceIDfromShortenedDeviceID:(NSString *)shortenedDeviceID
    {
        if (!shortenedDeviceID)
            return nil;
        NSString *decoded = [[[shortenedDeviceID
                               stringByReplacingOccurrencesOfString:@"_" withString:@"/"]
                              stringByReplacingOccurrencesOfString:@"-" withString:@"+"]
                             stringByAppendingString:@"=="];
    
        NSData *data = [[NSData alloc] initWithBase64EncodedString:decoded options:0];
        if (!data)
            return nil;
    
        NSUUID *uuid = [[NSUUID alloc] initWithUUIDBytes:[data bytes]];
    
        return uuid.UUIDString;
    }
    
  • If the conversion fails you know the broadcasting device is in background, and you need to connect to the device to read the characteristic that provides the unique identifier. For this you need to use [self.central connectPeripheral:peripheral options:nil]; (with peripheral.delegate = self; and implement a chain of delegate methods as follows:

    - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
    {
        [peripheral discoverServices:@[[CBUUID UUIDWithString:DEVICE_IDENTIFIER_SERVICE_UUID]]];
    }
    
    - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
    {
        if (!error) {
            for (CBService *service in peripheral.services) {
                if ([service.UUID.UUIDString isEqualToString:DEVICE_IDENTIFIER_SERVICE_UUID]) {
                    NSLog(@"Service found with UUID: %@", service.UUID);
                    [peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:DEVICE_IDENTIFIER_CHARACTERISTIC_UUID]] forService:service];
                }
            }
        }
    }
    
    - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
    {
        if (!error) {
            for (CBCharacteristic *characteristic in service.characteristics) {
                if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:DEVICE_IDENTIFIER_CHARACTERISTIC_UUID]]) {
                    [peripheral readValueForCharacteristic:characteristic];
                }
            }
        }
    }
    
    - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
    {
        if (!error) {
            NSString *shortenedDeviceID = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
            NSString *deviceId = [MyUtils deviceIDfromShortenedDeviceID:shortenedDeviceID];
            NSLog(@"Got device id: %@", deviceId);
        }
    }