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

2

8

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?

Effulgent answered 6/11, 2019 at 13:50 Comment(6)
If you apply no filter to the scan, and read all devices, are you able to detect the devices you are interested in after the fact by inspecting their services? (Not suggesting that's the only solution; just curious.) This would result in your BLE usage being flagged as unoptimized but could be a workable solution if that's not a problem for you.Almshouse
Yes, if I remove the scan filter I can see my devices and read the advertised data, but as you point it out, then this scan is marked as unoptimized and I need to be able to scan continuously in the background too, so I guess I need the scan filters to be able to remove some of the limits Android puts on background scanning.Effulgent
OK! I'm tracing through the method calls to see if I can find out anything more about how filtering is implemented...Almshouse
Hmm, I'm stuck at androidxref.com/9.0.0_r3/xref/system/bt/binder/android/…; not sure how to find the implementation of the generated stub.Almshouse
How your scanSettings look like? could you provide it too?Royo
@IbrahimAli I added my scan settings in my questionEffulgent
E
5

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.

Effulgent answered 11/11, 2019 at 12:17 Comment(0)
S
-2

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

Steele answered 13/2, 2020 at 13:7 Comment(1)
This is an example of an iOS solution. The problem is that the question was about Android.Hutment

© 2022 - 2024 — McMap. All rights reserved.