Sebastien
Sebastien

Reputation: 4295

Scanning for BLE peripherals with a scan filter based on advertised service UUID

I have a custom BLE peripheral that advertises data like this:

enter image description here

In other words, my BLE peripheral advertises a service UUID associated with a unique identifier in advertised service data, but it does not add that service UUID to advertised service list because if I do that, I don't have room in the BLE frame to add battery level when I need to.

On iOS, I'm able to scan with a filter based on service UUID and see my peripheral. But on Android, with the following scan filter, I don't see my peripheral:

val scanSettingsBuilder = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .setReportDelay(0L)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    scanSettingsBuilder
                .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
                .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
                .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
}
bluetoothAdapter?.bluetoothLeScanner?.startScan(
    arrayListOf(ScanFilter.Builder().setServiceUuid(ParcelUuid(UUID.fromString("00004865-726f-6e54-7261-636b2d475053"))).build()),
    scanSettingsBuilder.build(),
    leScanCallback
)

Does anyone have more details about how the serviceUUID-based scan filter works, and what are the conditions a peripheral must meet in order to be accepted by the filter?

Upvotes: 8

Views: 5776

Answers (2)

Mujahed Ansari
Mujahed Ansari

Reputation: 469

Step 1)

let kServiceUART = CBUUID(string: "0x1800")

var peripheralHeartRateMonitor: CBPeripheral?`

Step 2)

cbManger = CBCentralManager(delegate: self, queue: .main)

Step 3)

extension GuidedStartOnPhoneVC: CBCentralManagerDelegate, CBPeripheralDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {

    switch central.state {
    case .unsupported:
        print("BLe Unsupported")
        break
    case .unauthorized:
         print("BLe unauthorized")
        break
    case .poweredOff:
        let alertMessgesInst = AlertMessages.sharedInstance
        CommonUtils.showAlert(alertMessgesInst.actofit_Title, message: alertMessgesInst.trun_On_blueTooth)
        break
    case .poweredOn:

        if isNewFirmWareOFImpulse {

            let uuidString = StorageServices.readFromDefaults(key: Constants.userDefaultKeys.impulseUUID)
            let uuid = UUID(uuidString:uuidString as! String )
            let device =  cbManger.retrievePeripherals(withIdentifiers: [uuid!])
            peripheralHeartRateMonitor = device.first
            peripheralHeartRateMonitor!.delegate = self
            cbManger?.connect(peripheralHeartRateMonitor!)

        }else {

            let option:[String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey: NSNumber(value: false)]
            cbManger.scanForPeripherals(withServices: nil, options: option)
        }

        break
    case .unknown:
         print("BLe unknown")
        break
    default:
        break
    } // End Swith

} // End 'centralManagerDidUpdateState' function.
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {

    if isNewFirmWareOFImpulse {

        peripheralHeartRateMonitor = peripheral
        print("UUid of band is :- \(peripheralHeartRateMonitor?.identifier.uuidString)")

        if impulseName == peripheral.name {


            peripheralHeartRateMonitor!.delegate = self


            cbManger.stopScan()

            // STEP 6: connect to the discovered peripheral of interest
            cbManger?.connect(peripheralHeartRateMonitor!)


        } // End impulse condition

    }else {

        let keysArray = advertisementData.keys

        if let tempImpulseName = peripheral.name {
            print(impulseName + " and " + tempImpulseName )
            if impulseName == tempImpulseName {
                for key in keysArray {
                    if key == "kCBAdvDataManufacturerData"{
                        let manufactureData = advertisementData[key]
                        if let stringValue = manufactureData.debugDescription as? String {

                            var heartValue: String = String()
                            heartValue = stringValue
                            heartValue.removeLast()
                            heartValue.removeLast()
                            let last = heartValue.removeLast()
                            let secondLast = heartValue.removeLast()

                            let hR = String([secondLast, last])
                            if let value = UInt8(hR, radix: 16){

                                if Int(value) > 60 {
                                    hrArray.append(Int(value))
                                }


                            } // End the value block

                        } // end of if 'stringValue' condition
                    } // end 'Key' if condition

                } // End for each loop
            } // End impulse condition

        } // End pheripheral if condition

    } // end version condition

} // End function 'didDiscover peripheral'.

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {


    // STEP 8: look for services of interest on peripheral

    peripheralHeartRateMonitor?.discoverServices(nil)

} // END func centralManager(... didConnect peripheral

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    if error != nil {

        print("didDiscoverService Error :- \(error!)")
    }
    for service in peripheral.services! {
        print("Service: \(service)")

        if service.uuid.uuidString ==  "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" {

            print("Service: \(service)")

            // STEP 9: look for characteristics of interest
            // within services of interest
            peripheral.discoverCharacteristics(nil, for: service)

        }

    }

} // END func peripheral(... didDiscoverServices


func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {

    for characteristic in service.characteristics! {
        print(characteristic)

        if characteristic.uuid.uuidString == "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" {


            peripheral.setNotifyValue(true, for: characteristic)

        }

        if characteristic.uuid.uuidString == "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" {


            peripheral.setNotifyValue(true, for: characteristic)


        }
        //
    } // END for

} // END func peripheral(... didDiscoverCharacteristicsFor service


func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {


    // print(characteristic.value!.hexString)
    if !isOnRestScreen{
        if   let batteryLevelValue = characteristic.value {
            var  buffer = [UInt8](batteryLevelValue)
            //  print(buffer)
            print("Array count is \(buffer.count) :-  \(buffer)")
            if buffer.count == 20 {
                buffer.removeFirst(4)

                //MARK:- get Frame Array from the Impulse.
                let array1 = Array(buffer.prefix(upTo: 8))
                //  let array2 = Array(buffer.suffix(from: 8))
                makeProceedArray(tempArray: array1)
                //  makeProceedArray(tempArray: array2)
            }else {
                print("\(characteristic.service)")
            }

        }
    }
} // END if characteristic.uuid

func decodePeripheralState(peripheralState: CBPeripheralState) {

    switch peripheralState {
    case .disconnected:
        print("Peripheral state: disconnected")
    case .connected:
        print("Peripheral state: connected")
    case .connecting:
        print("Peripheral state: connecting")
    case .disconnecting:
        print("Peripheral state: disconnecting")
    }

} // END func decodePeripheralState(peripheralState

func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {

    // print("Disconnected!")

    if error != nil {

        print("didDisconnectPeripheral Error :- \(error!)")
    }

    // STEP 16: in this use-case, start scanning
    // for the same peripheral or another, as long
    // as they're HRMs, to come back online
    cbManger?.scanForPeripherals(withServices: [kServiceUART])

} // END func centralManager(... didDisconnectPeripheral peripheral

} // End extension

Upvotes: -2

Sebastien
Sebastien

Reputation: 4295

I figured out how to make it work... sort of. The problem is that my filter was on serviceUuid, which I assume looks at peripherals that advertise the UUID in the advertisedServices collection. My peripheral only advertises the UUID as a key in its serviceData associative array, so I switched to the serviceData filter as follows, and now I can find my peripheral:

AsyncTask.execute {
    val scanFilters = Settings.scannedBleServices.values.map {
        ScanFilter.Builder().setServiceData(it, null).build()
    }
    val scanSettingsBuilder = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .setReportDelay(0L)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        scanSettingsBuilder
                .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
                .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
                .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
    }
    bluetoothAdapter?.bluetoothLeScanner?.startScan(
            scanFilters,
            scanSettingsBuilder.build(),
            leScanCallback
    )
}

The problem is that now the filter is too permissive, as I get a callback for every peripheral around, even those without any serviceData, just as if I had specified no filter at all. Maybe it's because I passed null as a second parameter to setServiceData in the filter because I didn't know what else to add there. And the documentation is not exactly helpful.

My guess is that it's enough for the scan to work in the background (I haven't tried yet), but it would make more sense if I could restrict the number of times the callback is called and I didn't have to filter by myself.

Upvotes: 5

Related Questions