bluetoothbluetooth-lowenergycore-bluetoothcbperipheralcbcentralmanager

CBPeripheral advertisementData is different when discovering peripherals on OSX vs iOS (GAP/GATT)


I am hoping to port some of my CoreBluetooth code from iOS to OS X. I've set up a shared set of CoreBluetooth wrappers which are consumed by both an iOS app and an OS X app in exactly the same manner with the same BLE devices.

Scanning for peripherals:

override init() {
    super.init()
    let queue = DispatchQueue.global(qos: .background)
    centralManager = CBCentralManager(delegate: self, queue: queue)
}

func startScanning() {
    let options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey: true]
    let deviceUUID = CBUUID(string: Project.Service.Device)
    let recoveryUUID = CBUUID(string: Project.Service.DFURecovery)
    centralManager?.scanForPeripherals(withServices: [deviceUUID, recoveryUUID], options: options)
}

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber){
    // Inspect advertisementData here to decipher what kind of device
}

On my iOS app, didDiscoverPeripheral is fired. Then when I inspect the advertisement data I get all the keys/values that I am expecting:

{
    kCBAdvDataIsConnectable = 1;
    kCBAdvDataLocalName = "My Device";
    kCBAdvDataManufacturerData = <34045254 5877f283 43fdd12d ff530978 45000000 000050c2 6500>;
    kCBAdvDataServiceData =     {
        Battery = <64>;
    };
    kCBAdvDataServiceUUIDs =     (
        "My Inforamtion"
    );
}

However when this same code is run (scanning for the same devices) from an OS X app, the advertisement data is missing some of the fields.

{
    kCBAdvDataIsConnectable = 1;
    kCBAdvDataManufacturerData = <34045254 5877f36e 43fdd12d ff530978 45000000 000050c2 6500>;
}

The following key/value pairs are missing from advertisedData.

kCBAdvDataLocalName
kCBAdvDataServiceData
kCBAdvDataServiceUUIDs

I've tried adding those keys to the scanForPeripherals call like so:

    let options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey: true,
                                  CBAdvertisementDataLocalNameKey: true,
                                  CBAdvertisementDataServiceDataKey: true,
                                  CBAdvertisementDataServiceUUIDsKey: true]
    let deviceUUID = CBUUID(string: Nightlight.Service.Device)
    let recoveryUUID = CBUUID(string: Nightlight.Service.DFURecovery)
    centralManager?.scanForPeripherals(withServices: [deviceUUID, recoveryUUID], options: options)

With no effect.


Solution

  • OSX may call didDiscoverPeripheral multiple times per device, each call with different advertisementData. The solution I came up with is to write a cache.

    import Foundation
    import CoreBluetooth
        
    class AdvertisementDataCache {
        /// A dictionary of advertised data to peripheral.uuid
        private var cache: [UUID: [String: Any]] = [:]
        
        /// Appends advertisementData to our cache
        /// for each unique `peripheral.uuid`.
        func append(
            advertisementData: [String: Any],
            to peripheral: CBPeripheral
        ) -> [String: Any] {
            // Join our cached adverts (our `cache` ivar)
            // with new adverts (`advertisementData`)
            let joined = advertisementData.reduce(
                cache[peripheral.identifier] ?? [:]
            ) {
                var all = $0
                all[$1.key] = $1.value
                return all
            }
            
            // write back to our private iVar
            cache[peripheral.identifier] = joined
            
            // Return all cached averts for this peripheral
            return joined
        }
        
        /// Purges all cached avertisements for all peripherals
        func clear() {
            cache.removeAll()
        }
    }
    

    And then in didDiscoverPeripheral:

    private var advertisementDataCache = AdvertisementDataCache()
    
    public func centralManager(
        _ central: CBCentralManager,
        didDiscover peripheral: CBPeripheral,
        advertisementData adPart: [String: Any],
        rssi RSSI: NSNumber
    ) {
        let advertisementData = advertisementDataCache.append(
            advertisementData: adPart,
            to: peripheral
        )
        
        // advertisementData now contains all data contained in multiple callbacks
    }