Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scan peripherals and filter by service UUID #230

Closed
joelviel opened this issue Jun 27, 2020 · 10 comments · Fixed by #692
Closed

Scan peripherals and filter by service UUID #230

joelviel opened this issue Jun 27, 2020 · 10 comments · Fixed by #692
Assignees
Labels
asyncio Problems related to asyncio and multiple clients Backend: BlueZ Issues and PRs relating to the BlueZ backend Backend: Core Bluetooth Issues and PRs relating to the Core Bluetooth backend Backend: pythonnet Issues or PRs relating to the .NET/pythonnet backend Documentation Issues or PRs relating to RTD or documentation enhancement New feature or request scanning Issues about scanning/discovery
Milestone

Comments

@joelviel
Copy link

joelviel commented Jun 27, 2020

Hi,

Is it possible to scan peripherals and only consider those with a specific service UUID?
BLE addresses change often if using Android peripheral, what is your solution to detect compliant peripherals with your central in that case?

I am currenty using NodeJS noble ( noble.startScanning(uuid) ) but I am looking for alternative

Thanks

@bsiever
Copy link

bsiever commented Jun 29, 2020

By OS:

  • The Linux backend appears to allow this via an optional filters dictionary (I haven't tested it).
  • PR Updates to CoreBluetooth backend #209 should add identical support for CoreBluetooth too. (I have tested some of it)
  • I don't know much about the Windows backend, but it doesn't look like it does filtering yet.

The BlueZ documentation includes a section on SetDiscoveryFilters that gives the keywords allowed and their meaning. See: https://www.mideiros.net/doc/system/packages/bluez/dbus-apis/adapter-api.txt (Search for SetDiscoveryFilter)

Here's an example of scanning in the CoreBluetooth version:

    scanner = bleak.BleakScanner(filters={"UUIDs":["1d93af38-9239-11ea-bb37-0242ac130002"], "DuplicateData":False})
    scanner.register_detection_callback(detection_callback)
    await scanner.start()
    await asyncio.sleep(3.0)
    await scanner.stop()
    devices = await scanner.get_discovered_devices()

@dlech
Copy link
Collaborator

dlech commented Jun 29, 2020

Is there a compelling reason (like performance) that the filtering should be done at the OS level?

I've been thinking it would be nice to have an async iterator in the scanner. This could be used to implement filters in Python and it would not be restricted to the lowest common denominator of OS capabilities. (Off topic: this feature would also nicely replace register_detection_callback())

For example, get the first device with matching service UUID (ignore that it doesn't match the current API - this is more like pseudo-code):

MY_UUID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

async with BleakScanner() as scanner:
    # this has an iteration every time a new advertisement is seen
    async for advertisement in scanner.scan(timeout=5.0):
        if advertisement.service_uuids and MY_UUID in advertisement.service_uuids:
            my_device_id = advertisement.device_id
            # Breaking out of the for loop stops scanning. If there were any advertisements
            # already received after this one, they are ignored.
            break
    else:
        # The for loop completed without `break` so we must have reached timeout.
        print('device not found!')
        exit(1)

    my_device = await scanner.connect(my_device_id)
    ...

Or for all matching devices:

MY_UUID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

device_ids = []

async with BleakScanner() as scanner:
    async for advertisement in scanner.scan(timeout=5.0):
        if not advertisement.service_uuids:
            continue
        if MY_UUID not in advertisement.service_uuids:
            continue
        device_ids.append(advertisement.device_id)

    if not device_ids:
        print('no devices found!')
        exit(1)

    devices = await asyncio.gather(*(scanner.connect(x) for x in device_ids))
    ...

@hbldh hbldh self-assigned this Jun 30, 2020
@hbldh hbldh added Backend: pythonnet Issues or PRs relating to the .NET/pythonnet backend asyncio Problems related to asyncio and multiple clients Backend: BlueZ Issues and PRs relating to the BlueZ backend Backend: Core Bluetooth Issues and PRs relating to the Core Bluetooth backend enhancement New feature or request scanning Issues about scanning/discovery labels Jun 30, 2020
@hbldh
Copy link
Owner

hbldh commented Jun 30, 2020

As @bsiever says, there is OS level scan filtering available:

  • In Linux, this is set via the filters keyword in the discovery method or via filters keyword in the constructor or set_scanning_filter method on the BleakScannerBlueZDBus.
  • In Windows, it is available via filters keyword in the constructor or set_scanning_filter method on the BleakScannerDotNet class.
  • I will merge parts of Updates to CoreBluetooth backend #209 shortly, among other things the OS level scanning filter methods. It will be available on the corresponding objects in the Core Bluetooth backend.

@dlech My sole reason for leaving the scan filtering on the OS level is so that I would not have to support more code. Since it is already available there, I thought that it would be sufficient with providing access to those methods and leave it at that. The BleakScanner object is also meant for people to be able to implement their own filtering solutions, with user defined callbacks in which I thought one could evaluate if a detected device should be filtered or not.

Interesting that you put the connect method on the Scanner object. I have this morning thought that it would have been better to merge the scanning to the BleakClient and use that for everything, removing the costly double implementations of most things...

@bsiever
Copy link

bsiever commented Jun 30, 2020

@hbldh I thought that scanning may merit merging into the client too, but I haven't looked at all three clients. On the CoreBluetooth end this may make it easier to remove the singleton/cbapp.

It may also be worth considering people who are using bleak for observers that purely scan and process advertisements --- I don't know if there's a downside to folding scanning into clients for that use-case.

@dhalbert
Copy link

dhalbert commented Jun 30, 2020

One issue with bluez scanning is that the kernel support always turns on de-duplication of advertisements. This makes it difficult or impossible to use multiple advertisements to transmit changing data in a connectionless way. See https://marc.info/?l=linux-bluetooth&m=158225950522806&w=2. We are lobbying to allow control over this.

For now, we have gotten around this problem by using hcitool or bluetoothctl to initiate scanning, and hcidump to look at the raw data. However, this requires adding privileges to those commands. See
https:/adafruit/Adafruit_Blinka_bleio#support-for-duplicate-advertisement-scanning-on-linux
for more details, and
https:/adafruit/Adafruit_Blinka_bleio/blob/master/_bleio/adapter_.py
for some code, particularly _use_hcitool() and _start_scan_hcitool().

I do not expect bleak to provide this hack, but the restriction might be worth noting in the docs.

@hbldh hbldh added this to the Version 0.7.X milestone Jun 30, 2020
@hbldh hbldh added the Documentation Issues or PRs relating to RTD or documentation label Jun 30, 2020
@dlech
Copy link
Collaborator

dlech commented Jun 30, 2020

Interesting that you put the connect method on the Scanner object. I have this morning thought that it would have been better to merge the scanning to the BleakClient and use that for everything, removing the costly double implementations of most things...

Don't put any importance on that. I was just being lazy and didn't look up the existing API. 😄

@hbldh
Copy link
Owner

hbldh commented Jun 30, 2020

@dhalbert Even if bleak provided the hcitool / hcidump methods in python, it would still require some setcap solution in the system as well, doesn't it? A core idea of bleak initially was to provide a "pip only" solution to achieving BLE capability in Python, and I feel reluctant to add things that require tinkering outside of the Python program.

Please open a issue where this BlueZ scanning discussion can be had!

@dhalbert
Copy link

@dhalbert Even if bleak provided the hcitool / hcidump methods in python, it would still require some setcap solution in the system as well, doesn't it? A core idea of bleak initially was to provide a "pip only" solution to achieving BLE capability in Python, and I feel reluctant to add things that require tinkering outside of the Python program.

Right, I agree about that. We only did what we had to, after this unpleasant discovery. The setcap solution is not elegant or desired; the right thing is for bluez kernel support to be fixed.

@joelviel
Copy link
Author

joelviel commented Jul 1, 2020

Thank you for your numerous responses

I think on Windows the filters "UUIDs" when calling BleakScanner has no effect.
I am surprised about connecting performance (BleakClient.connect()), it takes always more than 4.5 seconds when I test, is it normal? (It is usually faster with other solutions)

import asyncio
import bleak
import uuid
import time

my_service_uuid = "0000fff0-0000-1000-8000-00805f9b34fb"
my_characteristic_uuid = '0000fff5-0000-1000-8000-00805f9b34fb'
my_peripheral_name = 'dtcBLE'
scanner = bleak.BleakScanner(filters={"UUIDs":[my_service_uuid], "DuplicateData":False})

def notification_handler(sender, data):
    print(f"{sender}: {data}")
    
def detection_callback(*args):
    print('LTPPP', args[1].Advertisement.LocalName)

async def run():
    
    scanner.register_detection_callback(detection_callback)
    target_peripheral_addr = None
    while(target_peripheral_addr is None):
        
        await scanner.start()
        await asyncio.sleep(0.7)
        await scanner.stop()
        devices = await scanner.get_discovered_devices()
        
        #devices = await bleak.discover() // slower alternative to detect peripherals
        
        for d in devices:
            print(d.address, d.name, d.metadata)
            if d.name==my_peripheral_name and my_service_uuid in d.metadata['uuids']:
                target_peripheral_addr = d.address
            else: print ('Target peripheral not found')
    
    print("compliant peripheral found!")
    t0 = time.clock()
    client = bleak.BleakClient(target_peripheral_addr, loop=loop)
    await client.connect()
    print('perf', time.clock() -t0)
    
loop = asyncio.get_event_loop()
loop.run_until_complete(run())

@hbldh
Copy link
Owner

hbldh commented Jul 2, 2020

@joelviel No, the UUIDs filter option does not exist in Windows. Look at the docstring for the Windows backend Scanner: https:/hbldh/bleak/blob/master/bleak/backends/dotnet/scanner.py#L51-L58

You have to do something like

from System import Guid
from Windows.Devices.Bluetooth.Advertisement import BluetoothLEAdvertisementFilter, BluetoothLEAdvertisement 

adv_filter = BluetoothLEAdvertisementFilter()
adv_filter.Advertisement = BluetoothLEAdvertisement()
adv_filter.Advertisement.ServiceUuids = [Guid.Parse("0000fff0-0000-1000-8000-00805f9b34fb")]

bleak.BleakScanner(AdvertisementFilter=adv_filter)

first.

I haven't tried the code above, merely writing it from documentation. Try it out.

Evidently, scanning filters need better documentation and some examples...

hbldh added a commit that referenced this issue Jul 2, 2020
@hbldh hbldh mentioned this issue Jul 2, 2020
@hbldh hbldh modified the milestones: Version 0.7.X, Version 0.8.0 Jul 9, 2020
@hbldh hbldh modified the milestones: Version 0.8.0, Version 0.X.Y Sep 2, 2020
dlech added a commit that referenced this issue Nov 27, 2021
This provides an implementation for the BleakScanner service_uuids
kwarg in the WinRT backend. As explained in the code comment, it is
not possible to use the OS-provided filter mechanism for technical
reasons.

Fixes #230
@dlech dlech linked a pull request Nov 27, 2021 that will close this issue
dlech added a commit that referenced this issue Dec 2, 2021
This provides an implementation for the BleakScanner service_uuids
kwarg in the WinRT backend. As explained in the code comment, it is
not possible to use the OS-provided filter mechanism for technical
reasons.

Fixes #230
dlech added a commit that referenced this issue Dec 6, 2021
This provides an implementation for the BleakScanner service_uuids
kwarg in the WinRT backend. As explained in the code comment, it is
not possible to use the OS-provided filter mechanism for technical
reasons.

Fixes #230
@dlech dlech closed this as completed in #692 Dec 7, 2021
dlech added a commit that referenced this issue Jan 8, 2022
Added
-----

* Added ``service_uuids`` kwarg to  ``BleakScanner``. This can be used to work around issue of scanning not working on macOS 12. Fixes #230. Works around #635.
* Added UUIDs for LEGO Powered Up Smart Hubs.

Changed
-------

* Changed WinRT backend to use GATT session status instead of actual device connection status.
* Changed handling of scan response data on WinRT backend. Advertising data and scan response data is now combined in callbacks like other platforms.
* Updated ``bleak-winrt`` dependency to v1.1.0. Fixes #698.

Fixed
-----

* Fixed ``InvalidStateError`` in CoreBluetooth backend when read and notification of the same characteristic are used. Fixes #675.
* Fixed reading a characteristic on CoreBluetooth backend also triggers notification callback.
* Fixed in Linux, scanner callback not setting metadata parameters. Merged #715.
@dlech dlech mentioned this issue Jan 8, 2022
dlech added a commit that referenced this issue Jan 10, 2022
Added
-----

* Added ``service_uuids`` kwarg to  ``BleakScanner``. This can be used to work around issue of scanning not working on macOS 12. Fixes #230. Works around #635.
* Added UUIDs for LEGO Powered Up Smart Hubs.

Changed
-------

* Changed WinRT backend to use GATT session status instead of actual device connection status.
* Changed handling of scan response data on WinRT backend. Advertising data and scan response data is now combined in callbacks like other platforms.
* Updated ``bleak-winrt`` dependency to v1.1.0. Fixes #698.

Fixed
-----

* Fixed ``InvalidStateError`` in CoreBluetooth backend when read and notification of the same characteristic are used. Fixes #675.
* Fixed reading a characteristic on CoreBluetooth backend also triggers notification callback.
* Fixed in Linux, scanner callback not setting metadata parameters. Merged #715.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
asyncio Problems related to asyncio and multiple clients Backend: BlueZ Issues and PRs relating to the BlueZ backend Backend: Core Bluetooth Issues and PRs relating to the Core Bluetooth backend Backend: pythonnet Issues or PRs relating to the .NET/pythonnet backend Documentation Issues or PRs relating to RTD or documentation enhancement New feature or request scanning Issues about scanning/discovery
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants