如何在iOS 7.1中在前台和后台使用蓝牙LE检测附近的设备?

20

我有一个应用程序需要检测附近(蓝牙LE范围内)运行相同应用程序和iOS 7.1的设备。我考虑了两种检测方法:

  1. 让设备作为iBeacon并检测范围内的iBeacon
  2. 使用CoreBluetooth(如Vicinity实现here)创建BLE外设,广告并扫描外设

选项1似乎不可行,因为:

  • 当应用程序在后台运行时,iOS可能需要至少15分钟才能检测到进入信标区域(iOS 7.1)

选项2似乎是可行的,但是实施上存在一些困难:

  • iOS似乎会在广告包中的一段时间后(大约15分钟?)更改外设UUID。这意味着无法直接从广告广播信号中识别广告设备。

关于此,我有以下问题:

  • 是否有其他实现附近设备检测的方法我没有考虑过?
  • 是否可以通过广告(或其他方式)识别设备,使选项2可行?
1个回答

24

我找到了一种方法让 Core Bluetooth(选项 2)能够正常工作,大致过程如下:

  • 应用程序使用编码的设备唯一标识符在 CBAdvertisementDataLocalNameKey 中进行自我广告(当广播应用程序在前台运行时),并通过 Bluetooth LE 服务提供设备唯一标识符的特征(当广播应用程序在后台运行时)。
  • 同时,应用程序会扫描具有相同服务的其他外设。

广告的工作方式如下:

  • 为了使其他设备能够识别此设备,我使用每个设备的唯一 UUID(我正在使用 Urban Airship 的 [UAUtils deviceID],因为它是程序中其他部分的设备标识符,但您也可以使用任何唯一的 ID 实现)。
  • 当应用程序在前台运行时,我可以通过使用 CBAdvertisementDataLocalNameKey 直接在广告包中传递设备唯一 ID。标准 UUID 表示过长,因此我使用缩短形式的 UUID,如下所示:

+ (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;
}
当应用程序在后台运行时,广告数据包会被剥离,CBAdvertisementDataLocalNameKey 不再传递。因此,应用程序需要发布一个提供唯一设备标识符的 特征值
  • 当应用程序在后台运行时,广告数据包会被剥离,CBAdvertisementDataLocalNameKey不再传递。因此,应用程序需要发布一个提供唯一设备标识符的特征值

  • - (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];
            }
        }
    }
    

    扫描的工作流程如下:

    • 你需要按照以下方式扫描带有特定服务UUID的外围设备(注意,你需要指定服务UUID,否则后台扫描将无法找到设备):

    [self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:DEVICE_IDENTIFIER_SERVICE_UUID]]
        options:scanOptions];
    
    当设备被发现时,在- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI中,你需要检查advertisementData[CBAdvertisementDataLocalNameKey]是否存在,并尝试将其转换回UUID格式,如下所示:
    + (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;
    }
    
    如果转换失败,则说明广播设备在后台运行,您需要连接到设备以读取提供唯一标识符的特征。为此,您需要使用 [self.central connectPeripheral:peripheral options:nil];(同时设置 peripheral.delegate = self; 并实现如下委托方法链:)
    - (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);
        }
    }
    

    1
    这篇文章非常有帮助,谢谢!需要注意的是,在 didDiscoverPeripheral 中,如果被发现的外设在后台运行,可以使用 CBAdvertisementDataOverflowServiceUUIDsKey 而不是 CBAdvertisementDataServiceUUIDsKey 在 advertisementData 中找到服务 UUID。 - Tron5000
    1
    在使用connectPeripheral连接之前,您需要保留对要连接的外设的引用。其次,请注意,在连接之前,您需要保证该外设已经处于可连接状态。 - Tron5000
    1
    我发现当应用在前台时,“LocalName”键的长度可靠地为8个字符。我发现传输的长度也会有所变化 - 可能是因为设备上其他BLE应用程序的原因。不要指望它超过8个字符,当然,在后台中也不可用。 - vaughan
    1
    有人能确认这段代码在最新的iOS上仍然可用吗?我一直在尝试让扫描和广告同时工作。谢谢。 - xeo
    1
    我在iOS 12+上做了完全相同的事情。可悲的是,在后台一段时间后,即使我尝试连接到外设,我也没有在didDiscoverServices委托中看到服务。@MarkusRautopuro,您是否已经测试过此代码,以便在设备处于后台且其服务在几分钟后可被发现时进行发现? - Andreas Papageorgiou
    显示剩余2条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接