2

I would like to set a ScanFilter to scan a BLE device based on its advertisement data: name, services UUID, and the first character of its manufacture data. The problem is: when I use a ScanFilter, there is no callback, which means the .onScanResult() is not called.

The device name is correct because I have tested by finding the device in callback. When I don't use a ScanFilter, there is callback and the device can be found by its name. And it can successfully connect to the device.

  1. Here is my problematic code. Can you help with indicating its problem?

  2. Also, I have another question: how to input the three parameters for .setManufacturerData()?

    protected void scan(){
    
        List<ScanFilter> filters = new ArrayList<>();
        ScanFilter filterName = new ScanFilter.Builder().setDeviceName(this.mDeviceName).build();
        filters.add(filterName);
        //ScanFilter filterUUID = new ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString("00001814-0000-1000-8000-00805F9B34FB")).build();
        //filters.add(filterUUID);
        //ScanFilter filterManu = new ScanFilter.Builder().setManufacturerData().build();
        //filters.add(filterManu);
    
        ScanSettings settings = (new Builder()).setScanMode(2).build();
        this.mBluetoothLeScanner.startScan(filters, settings, this);
        this.mScanning = true;
    }
    
    @Override
        public void onScanResult(int callbackType, ScanResult result) {
            super.onScanResult(callbackType, result);
            BluetoothDevice _device = result.getDevice();
                    Log.i("onScanResult", " callback function is being called");
    
            // find device by name
            if (_device != null && _device.getName() != null && _device.getName().matches(this.mDeviceName) ) {
                this.stopScan();
                this.mDevice = _device;
                Log.i(TAG, "mac address : " + this.mDevice.getAddress() + ", name : " + this.mDevice.getName());
                this.mDeviceFoundLatch.countDown();
            }
    
            long m = mDeviceFoundLatch.getCount();
            Log.i("onResultscallbackLatch", String.valueOf(m));
    
        }
    

I got these errors:

E/ScanRecord: unable to parse scan record: [2, 1, 6, 3, 2, 20, 24, 2, -1, 82, 20, 9, 83, 116, 114, 105, 100, 97, 108, 121, 122, 101, 114, 32, 73, 78, 83, 73, 71, 72, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

E/BluetoothServiceJni: An exception was thrown by callback 'btgattc_scan_result_cb'.

E/BluetoothServiceJni: java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object java.util.Map.get(java.lang.Object)' on a null object reference
        at android.bluetooth.le.ScanRecord.getServiceData(ScanRecord.java:121)
        at android.bluetooth.le.ScanFilter.matches(ScanFilter.java:305)
        at com.android.bluetooth.gatt.GattService.matchesFilters(GattService.java:659)
        at com.android.bluetooth.gatt.GattService.onScanResult(GattService.java:611)

Here is the correctly running code. I can use it to find the device. But I would like to filter the device by its manufacture data. Do I have to set the filter when scanning? Is there anyway to filter the device by its manufacture data here?

protected void scan(){    
        ScanSettings settings = (new Builder()).setScanMode(2).build();
        this.mBluetoothLeScanner.startScan(null, settings, this);
        this.mScanning = true;
    }

    @Override
        public void onScanResult(int callbackType, ScanResult result) {
            super.onScanResult(callbackType, result);
            BluetoothDevice _device = result.getDevice();
            if (_device != null && _device.getName() != null && _device.getName().matches(this.mDeviceName) ) {
                this.stopScan();
                this.mDevice = _device;
                this.mDeviceFoundLatch.countDown();
            }
        }
Tao
  • 366
  • 1
  • 3
  • 15
  • You don't happen to be scanning for one those *Bluetooth to Serial Transceiver Modules*? Some of them are known to send invalid advertisement data and Android 8 and higher will only find them if you scan without a filter. – Codo Jul 04 '19 at 14:59
  • Hi @Codo thanks for reply. Do you mean it might be the device's problem ? It's weird that second method (see code above) in callback is able to find the device by its name, but not able to find the device when setting a filter of its name – Tao Jul 04 '19 at 22:13

2 Answers2

2

Indeed the data sent by the device is invalid. The format is basically a sequence of:

  • data length (incl. field type)
  • field type
  • data

So if we split the data, we get:

2: length 2 bytes
1: field type "flags"
6: data (flags)

3: length 3 bytes
2: field type "partial 16 bit UUID"
20, 24: data (UUID bytes 0x14 0x18, that will be converted into the full UUID 00001814-0000-1000-8000-00805F9B34FB)

2: length 2 bytes
-1 (or 0xff): field type "manufacturer data"
82: data (manufacturer data)

20: length 20 bytes
9: field type "local name complete"
83, 116, 114, 105, 100, 97, 108, 121, 122, 101, 114, 32, 73, 78, 83, 73, 71, 72, 84: data (local name, decoded as "Stridalyzer INSIGHT")
0, 0, ...: ignored data

Unfortunately, the manufacturer data is invalid. It would need to be at least 3 bytes (4 with the type field) and start with 2 bytes for the manufacturer ID. So at this point, Android discard the parsing and creates a ScanResult with no data at all.

So you won't have any luck with filters. Instead, you will need to filter it yourself.

Codo
  • 75,595
  • 17
  • 168
  • 206
  • Dear Code! That's a great answer and I finally have an idea of this numbers. If I cannot use any filter, how would l filter it myself? I have tried to set a null filter and use check callback. `if (_device != null && _device.getName() != null && _device.getName().matches(this.mDeviceName) ) { ... }` But in this way, I can only filter by its name rather than manufacture data. – Tao Jul 05 '19 at 10:29
  • Because in the callback, there is no method to access the manufacture data. – Tao Jul 05 '19 at 10:35
  • 1
    Possibly checking the device name is the best thing you can do with this broken device. Another one would be to connect to the device and query the characteristics that many devices support to provide a longer name, model number, hardware revision, manufacturer name etc. Install the *nRF Connect* app from Nordic to check what your device supports. – Codo Jul 05 '19 at 10:38
  • 1
    Hi Code! Thanks a lot for your help! I managed to connect it by manually parse and filter the scanRecord! – Tao Jul 06 '19 at 16:25
  • 1
    Well done. That could be an approach for one of my projects as well. Make sure your code is robust. There are many other BLE devices with invalid advertising data out there... – Codo Jul 09 '19 at 14:33
1

Finally get it worked! Here is my solution:

Due to the invalid manufacturer data (see the explanation by @code above), we cannot directly use Android-provided function - scanRecord.getManufacturerSpecificData(). Instead, I manually parse the advertisement by using the following codes:

@Override
public void onScanResult(int callbackType, ScanResult result) {
    super.onScanResult(callbackType, result);


    BluetoothDevice _device = result.getDevice();
    ScanRecord scanRecord = result.getScanRecord();
    byte[] byte_ScanRocord = scanRecord.getBytes();
    int isLeft = byte_ScanRocord[9];//the 9th byte stores the information that can be used to filter the device
    String byte_ScanRocord_str = Arrays.toString(byte_ScanRocord);
    Log.i("onScanRecordAdv",byte_ScanRocord_str);
    Log.i("onScanRecordisLeft",String.valueOf(isLeft));

    if (_device != null && _device.getName() != null && _device.getName().matches(this.mDeviceName) && isLeft == 76 ) {
        this.stopScan();
        this.mDevice = _device;
        Log.i("InsoleScanner", "mac address : " + this.mDevice.getAddress() + ", name : " + this.mDevice.getName());
        this.mDeviceFoundLatch.countDown();
    }
}
Tao
  • 366
  • 1
  • 3
  • 15