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:
It seems that the option 1 is out of the question because:
Option 2 seems the way to go, but there are some difficulties regarding the implementation:
Regarding this, I have the following questions:
I found a way to make this work Core Bluetooth (option 2), the procedure is roughly the following:
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)The advertising works as follows:
[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);
}
}