bleak icon indicating copy to clipboard operation
bleak copied to clipboard

Added support for Google´s Bumble Bluetooth Controller stack

Open vChavezB opened this issue 1 year ago • 26 comments

The backend supports direct use with Bumble. The HCI Controller is managed by the Bumble stack and the transport layer can be defined by the user (e.g. VHCI, Serial, TCP, android-netsim).

Use cases for this backend are:

  1. Bluetooth Functional tests without Hardware. Example of Bluetooth stacks that support virtualization are Android Emulator and Zephyr RTOS.
  2. Virtual connection between different HCI Controllers that are not in the same radio network (virtual or physical).

To enable this backend the env. variable BLEAK_BUMBLE must be set.

At the moment this is an experimental backend and would like to get some feedback from users and the devs of bleak :)

vChavezB avatar Nov 01 '24 19:11 vChavezB

Fixed some linting and doc issues.

vChavezB avatar Nov 01 '24 22:11 vChavezB

It looks like you may not have updated the lock file. Please run poetry lock --no-update, commit, and push. thanks!

JPHutchins avatar Nov 01 '24 22:11 JPHutchins

Re: the overall API. If this is merged, I think that it should be importable as a separate class. BleakClient can remain as an OS-selected class, but BumbleClient should be separately importable. BumbleClient would be fully compatible with BleakClient - that is, they implement the same interface, Protocol, ABC, whatever you want to call it.

JPHutchins avatar Nov 01 '24 23:11 JPHutchins

I agree, the Bumble client is largely OS independent (except for the transports that use the Linux HCI device). So it could be imported separately.

Thanks for the feedback on type safety! It really helps to remove ambiguity in Python and help other developers understand the intent of the code. I will apply the suggestions you mentioned.

vChavezB avatar Nov 01 '24 23:11 vChavezB

I agree, the Bumble client is largely OS independent (except for the transports that use the Linux HCI device). So it could be imported separately.

Thanks for the feedback on type safety! It really helps to remove ambiguity in Python and help other developers understand the intent of the code. I will apply the suggestions you mentioned.

Glad you like it! I was annoyed when I first saw it being required in projects but now I'm a "kool-aid drinker" 🤣! It does in fact eliminate an entire class of runtime bugs that Python applications tend to suffer from.

You'll need mypy or pyright LSPs/extensions running to really see anything. By default, if a function has no return type specified, the linters ignore the typing in the function body. Best to simply specify a return type for all functions, even if it is None.

JPHutchins avatar Nov 01 '24 23:11 JPHutchins

I have applied some of the suggestions from @JPHutchins. I will also check the typesafety of the changes in this PR.

vChavezB avatar Nov 02 '24 11:11 vChavezB

Resolved some of the reviews pending is:

  • Wait for Answer to https://github.com/hbldh/bleak/pull/1681#discussion_r1827566918
  • Wait for PR for Bleak adapter ? https://github.com/hbldh/bleak/issues/1060
  • Wait for PR (?) to get Rid of BleakGattChar, BleakGatt Svc , see https://github.com/hbldh/bleak/pull/1681#discussion_r1827081869
  • Wait for comment on detailed description of USB/Serial use case. See https://github.com/hbldh/bleak/pull/1681#discussion_r1827470772
  • ~Define exactly expected use cases for bumble https://github.com/hbldh/bleak/pull/1681#discussion_r1827656973~

vChavezB avatar Nov 04 '24 12:11 vChavezB

I disagree with this suggestion. I would like to be able to run any script with or without the Bumble backend, so no code changes should be required to use the Bumble backend. I also don't want a bunch of features being added that aren't supported on other backends, so having a separate class should not be necessary.

I think that this works for me. My application can simply specify the Bumble backend, right?

https://github.com/hbldh/bleak/blob/e01e2640994b99066552b6161f84799712c396fa/bleak/init.py#L517

Obviously I cannot ask users to be setting environment variables 🤣.

Note to self: investigate import times due to use of runtime vs compile time OS checks: https://github.com/hbldh/bleak/blob/e01e2640994b99066552b6161f84799712c396fa/bleak/backends/client.py#L250-L274

JPHutchins avatar Nov 04 '24 20:11 JPHutchins

My application can simply specify the Bumble backend, right?

Yes, if you have an application that requires the Bumble backend and doesn't work with other backend, that is fine.

Obviously I cannot ask users to be setting environment variables 🤣.

Of course! But your application could set the environment variable itself after it starts too. But the backend arg seems like the more straightforward way to do it.

dlech avatar Nov 04 '24 20:11 dlech

Well, in this case, if for bleak we want an easy way for users to replace the backend with bumble, then a variable like BLEAK_BUMBLE_TRANSPORT can be defined (or whatever name is more convenient).

And then if we don't want the env variable, then the user can pass the backend option 👍

I can then mention these two options in the docs.

vChavezB avatar Nov 04 '24 20:11 vChavezB

@vChavezB @dlech What do you think about committing FW images for some common platforms? If you'd rather not, we can create a repo that is solely for that.

JPHutchins avatar Nov 13 '24 20:11 JPHutchins

What do you think about committing FW images for some common platforms?

Sounds like a job for a separate repo.

dlech avatar Nov 13 '24 20:11 dlech

Build HCI USB sample for NRF52840DK:

west build -b nrf52840dk/nrf52840 zephyr/samples/bluetooth/hci_usb

Flash

west flash

NRF52840DK device shows up!

image

Confirmed poetry run python -m examples.discover running normally on the regular BT adapter. Now will set env variables to test the Bumble backend!

$env:BLEAK_BUMBLE="usb:2FE3:0008"
poetry run python -m examples.discover
scanning for 5 seconds, please wait...
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\jp\repos\bleak\examples\discover.py", line 51, in <module>
    asyncio.run(main(args))
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\asyncio\runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\asyncio\base_events.py", line 687, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "C:\Users\jp\repos\bleak\examples\discover.py", line 20, in main
    devices = await BleakScanner.discover(
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jp\repos\bleak\bleak\__init__.py", line 320, in discover
    async with cls(**kwargs) as scanner:
               ^^^^^^^^^^^^^
  File "C:\Users\jp\repos\bleak\bleak\__init__.py", line 158, in __aenter__
    await self._backend.start()
  File "C:\Users\jp\repos\bleak\bleak\backends\bumble\scanner.py", line 115, in start
    await start_transport(self._adapter)
  File "C:\Users\jp\repos\bleak\bleak\backends\bumble\__init__.py", line 112, in start_transport
    transports[transport_cmd] = await open_transport(transport_cmd)
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\bumble\transport\__init__.py", line 93, in open_transport
    transport = await _open_transport(scheme, spec)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\bumble\transport\__init__.py", line 165, in _open_transport
    return await open_usb_transport(spec)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\bumble\transport\usb.py", line 456, in open_usb_transport
    raise TransportInitError('device not found')
bumble.transport.common.TransportInitError: device not found

Tried lower case hex for VID/PID as well.

Reference is here: https://google.github.io/bumble/transports/usb.html

OK, updated to:

$env:BLEAK_BUMBLE="usb:0"

And now I get:

scanning for 5 seconds, please wait...
!!! failed to open USB device: LIBUSB_ERROR_NOT_SUPPORTED [-12]
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\jp\repos\bleak\examples\discover.py", line 51, in <module>
    asyncio.run(main(args))
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\asyncio\runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\asyncio\base_events.py", line 687, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "C:\Users\jp\repos\bleak\examples\discover.py", line 20, in main
    devices = await BleakScanner.discover(
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jp\repos\bleak\bleak\__init__.py", line 320, in discover
    async with cls(**kwargs) as scanner:
               ^^^^^^^^^^^^^
  File "C:\Users\jp\repos\bleak\bleak\__init__.py", line 158, in __aenter__
    await self._backend.start()
  File "C:\Users\jp\repos\bleak\bleak\backends\bumble\scanner.py", line 115, in start
    await start_transport(self._adapter)
  File "C:\Users\jp\repos\bleak\bleak\backends\bumble\__init__.py", line 112, in start_transport
    transports[transport_cmd] = await open_transport(transport_cmd)
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\bumble\transport\__init__.py", line 93, in open_transport
    transport = await _open_transport(scheme, spec)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\bumble\transport\__init__.py", line 165, in _open_transport
    return await open_usb_transport(spec)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\bumble\transport\usb.py", line 536, in open_usb_transport
    device = found.open()
             ^^^^^^^^^^^^
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\usb1\__init__.py", line 2055, in open
    mayRaiseUSBError(libusb1.libusb_open(self.device_p, byref(handle)))
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\usb1\__init__.py", line 127, in mayRaiseUSBError
    __raiseUSBError(value)
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\usb1\__init__.py", line 119, in raiseUSBError
    raise __STATUS_TO_EXCEPTION_DICT.get(value, __USBError)(value)
usb1.USBErrorNotSupported: LIBUSB_ERROR_NOT_SUPPORTED [-12]

Installing Zadig 🤣 ...

Tried libusb-win32 driver - discover script hangs until I unplug the board.

Tried libusbK driver - same

Tried WinUSB - same

Could test from linux first, for example, then try Windows.

JPHutchins avatar Nov 13 '24 21:11 JPHutchins

Looking good! Will be testing with an nrf52840dk momentarily!

Overall, a lot of time will be saved by getting mypy working in your editor.

Thanks for the review. I will definitely check again with the typesafety extensions in pycharm. It would be nice to get some field tests to check if everything works.

So far I have only tested with virtual devices (Zephyr posix and renode).

vChavezB avatar Nov 13 '24 21:11 vChavezB

Tried lower case hex for VID/PID as well.

Would you believe that the PID is 000B but I'd set 0008 every time? 😭. Will run through again ASAP.

JPHutchins avatar Nov 14 '24 02:11 JPHutchins

Would you believe that the PID is 000B but I'd set 0008 every time? 😭. Will run through again ASAP.

OK, picking up where I left off. Starting by trying to revert the drivers to Windows defaults. It's on libwdi now, after unisntalling and restart.

Set logging and correct USB VID:

$env:BLEAK_LOGGING=1
$env:BLEAK_BUMBLE="usb:2fe3:000B"
poetry run python -m examples.discover
scanning for 5 seconds, please wait...

Still hang. No Ctrl-C. At least it's find the USB device now. Can exit by removing the Bumble device.

Call stack reveals it's maybe doing some USB stuff (I didn't include whole stack)

!!! IN transfer not completed: status=4
Exception ignored on calling ctypes callback function: <bound method USBTransfer.__callbackWrapper of <class 'usb1.USBTransfer'>>
Traceback (most recent call last):
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\usb1\__init__.py", line 327, in __callbackWrapper
    callback(self)
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\bumble\transport\usb.py", line 280, in transfer_callback
    self.loop.call_soon_threadsafe(self.on_transport_lost)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\asyncio\base_events.py", line 840, in call_soon_threadsafe
    self._check_closed()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\asyncio\base_events.py", line 541, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
!!! IN transfer not completed: status=5
Exception ignored on calling ctypes callback function: <bound method USBTransfer.__callbackWrapper of <class 'usb1.USBTransfer'>>
Traceback (most recent call last):
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\usb1\__init__.py", line 327, in __callbackWrapper
    callback(self)
  File "C:\Users\jp\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\Local\pypoetry\Cache\virtualenvs\bleak-dSQ9Yv1H-py3.12\Lib\site-packages\bumble\transport\usb.py", line 280, in transfer_callback
    self.loop.call_soon_threadsafe(self.on_transport_lost)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\asyncio\base_events.py", line 840, in call_soon_threadsafe
    self._check_closed()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\asyncio\base_events.py", line 541, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
Exception in thread Thread-1 (run):
Traceback (most recent call last):
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2032.0_x64__qbz5n2kfra8p0\Lib\threading.py", line 1075, in _bootstrap_inner
    self.run()

So... maybe it's blocking on some USB transfer that never happens.

Adding logging to examples.discover:

logging.basicConfig(level=logging.DEBUG)

I mean... this looks promising!

poetry run python -m examples.discover
DEBUG:asyncio:Using proactor: IocpProactor
scanning for 5 seconds, please wait...
DEBUG:bumble.link:new controller: <bumble.controller.Controller object at 0x0000029E537B7EF0>
DEBUG:bumble.transport.usb:USB Device: Bus 004 Device 019: ID 2fe3:000b
DEBUG:bumble.transport.usb:selected endpoints: configuration=1, interface=0, setting=0, acl_in=0x82, acl_out=0x01, events_in=0x81,
DEBUG:bumble.transport.usb:current configuration = 1
DEBUG:bumble.transport.usb:starting USB event loop
DEBUG:bumble.link:new controller: <bumble.controller.Controller object at 0x0000029E532F6570>
DEBUG:bumble.drivers:Probing driver class: rtk
DEBUG:bumble.drivers.rtk:USB metadata not found
DEBUG:bumble.drivers:Probing driver class: intel
DEBUG:bumble.drivers.intel:USB metadata not sufficient
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_RESET_COMMAND
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_RESET_COMMAND
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_RESET_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_RESET_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_READ_LOCAL_SUPPORTED_COMMANDS_COMMAND
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_READ_LOCAL_SUPPORTED_COMMANDS_COMMAND
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_READ_LOCAL_SUPPORTED_COMMANDS_COMMAND
  return_parameters:       002000800000c000000000e4000000a822000000000000040000f7ffff7f00000030f0f9ff01008004000000000000000000000000000000000000000000000000
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_READ_LOCAL_SUPPORTED_COMMANDS_COMMAND
  return_parameters:
    status:             HCI_SUCCESS
    supported_commands: 2000800000c000000000e4000000a822000000000000040000f7ffff7f00000030f0f9ff01008004000000000000000000000000000000000000000000000000
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND
  return_parameters:       00ff49010000000000
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND
  return_parameters:
    status:      HCI_SUCCESS
    le_features: ff49010000000000
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND
  return_parameters:       0009000009ffff0000
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND
  return_parameters:
    status:             HCI_SUCCESS
    hci_version:        9
    hci_subversion:     0
    lmp_version:        9
    company_identifier: 65535
    lmp_subversion:     0
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND
  return_parameters:       000000000060000000
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND
  return_parameters:
    status:       HCI_SUCCESS
    lmp_features: 0000000060000000
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_SET_EVENT_MASK_COMMAND:
  event_mask: ff9ffbbf07f8bf3d
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_SET_EVENT_MASK_COMMAND:
  event_mask: ff9ffbbf07f8bf3d
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_SET_EVENT_MASK_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_SET_EVENT_MASK_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_LE_SET_EVENT_MASK_COMMAND:
  le_event_mask: fffff7ff07000000
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_LE_SET_EVENT_MASK_COMMAND:
  le_event_mask: fffff7ff07000000
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_SET_EVENT_MASK_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_SET_EVENT_MASK_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_READ_BUFFER_SIZE_COMMAND
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_READ_BUFFER_SIZE_COMMAND
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_READ_BUFFER_SIZE_COMMAND
  return_parameters:       001b000040000000
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_READ_BUFFER_SIZE_COMMAND
  return_parameters:
    status:                                HCI_SUCCESS
    hc_acl_data_packet_length:             27
    hc_synchronous_data_packet_length:     0
    hc_total_num_acl_data_packets:         64
    hc_total_num_synchronous_data_packets: 0
DEBUG:bumble.host:HCI ACL flow control: hc_acl_data_packet_length=27,hc_total_num_acl_data_packets=64
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_LE_READ_BUFFER_SIZE_COMMAND
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_LE_READ_BUFFER_SIZE_COMMAND
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_READ_BUFFER_SIZE_COMMAND
  return_parameters:       001b0040
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_READ_BUFFER_SIZE_COMMAND
  return_parameters:
    status:                           HCI_SUCCESS
    hc_le_acl_data_packet_length:     27
    hc_total_num_le_acl_data_packets: 64
DEBUG:bumble.host:HCI LE ACL flow control: hc_le_acl_data_packet_length=27,hc_total_num_le_acl_data_packets=64
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
  return_parameters:       001b004801
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
  return_parameters:
    status:                  HCI_SUCCESS
    suggested_max_tx_octets: 27
    suggested_max_tx_time:   328
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND:
  suggested_max_tx_octets: 251
  suggested_max_tx_time:   2120
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND:
  suggested_max_tx_octets: 251
  suggested_max_tx_time:   2120
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_READ_BD_ADDR_COMMAND
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_READ_BD_ADDR_COMMAND
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_READ_BD_ADDR_COMMAND
  return_parameters:       00000000000000
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_READ_BD_ADDR_COMMAND
  return_parameters:
    status:  HCI_SUCCESS
    bd_addr: 00:00:00:00:00:00/P
DEBUG:bumble.device:BD_ADDR: 00:00:00:00:00:00/P
DEBUG:bumble.device:LE Random Address: F0:F1:F2:F3:F4:F5
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_LE_SET_RANDOM_ADDRESS_COMMAND:
  random_address: F0:F1:F2:F3:F4:F5
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_LE_SET_RANDOM_ADDRESS_COMMAND:
  random_address: F0:F1:F2:F3:F4:F5
DEBUG:bumble.controller:new random address: F0:F1:F2:F3:F4:F5
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_SET_RANDOM_ADDRESS_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_SET_RANDOM_ADDRESS_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_LE_SET_SCAN_PARAMETERS_COMMAND:
  le_scan_type:           1
  le_scan_interval:       96
  le_scan_window:         96
  own_address_type:       RANDOM
  scanning_filter_policy: 0
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_LE_SET_SCAN_PARAMETERS_COMMAND:
  le_scan_type:           1
  le_scan_interval:       96
  le_scan_window:         96
  own_address_type:       RANDOM
  scanning_filter_policy: 0
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_SET_SCAN_PARAMETERS_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_SET_SCAN_PARAMETERS_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_LE_SET_SCAN_ENABLE_COMMAND:
  le_scan_enable:    1
  filter_duplicates: 0
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_LE_SET_SCAN_ENABLE_COMMAND:
  le_scan_enable:    1
  filter_duplicates: 0
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_SET_SCAN_ENABLE_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_SET_SCAN_ENABLE_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### HOST -> CONTROLLER: HCI_LE_SET_SCAN_ENABLE_COMMAND:
  le_scan_enable:    0
  filter_duplicates: 0
DEBUG:bumble.controller:<<< [Scanner] HOST -> CONTROLLER: HCI_LE_SET_SCAN_ENABLE_COMMAND:
  le_scan_enable:    0
  filter_duplicates: 0
DEBUG:bumble.controller:>>> [Scanner] CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_SET_SCAN_ENABLE_COMMAND
  return_parameters:       HCI_SUCCESS
DEBUG:bumble.host:### CONTROLLER -> HOST: HCI_COMMAND_COMPLETE_EVENT:
  num_hci_command_packets: 1
  command_opcode:          HCI_LE_SET_SCAN_ENABLE_COMMAND
  return_parameters:       HCI_SUCCESS

So maybe there are 2 problems left:

  • not exiting after 5 seconds as specified
  • no devices are discovered - maybe a callback is not set?

JPHutchins avatar Nov 17 '24 00:11 JPHutchins

So, it looks like Bumble uses a flexible event callback system. Unfortunately, they're using strings again, but here's the magic string: "advertisement". Found in source as

self.emit('advertisement', advertisement)

So it seems like intended usage may be with a closure, like this:

    async def start(self) -> None:

        def on_advertisement(advertisement: Advertisement):
            logger.debug(f"Received advertisement: {advertisement}")

            service_uuids: List[str] = []
            service_data: Dict[str, AdvertisingDataObject] = {}
            local_name = advertisement.data.get(AdvertisingData.COMPLETE_LOCAL_NAME)
            if not local_name:
                local_name = advertisement.data.get(
                    AdvertisingData.SHORTENED_LOCAL_NAME
                )
            manuf_data = advertisement.data.get(
                AdvertisingData.MANUFACTURER_SPECIFIC_DATA
            )
            for uuid_type in SERVICE_UUID_TYPES:
                adv_uuids = advertisement.data.get(uuid_type)
                if adv_uuids is None:
                    continue
                if not isinstance(adv_uuids, list):
                    continue
                for uuid in adv_uuids:  # type: UUID
                    if uuid not in service_uuids:
                        service_uuids.append(str(uuid))

            for service_data in advertisement.data.get_all(
                AdvertisingData.SERVICE_DATA
            ):
                service_uuid, data = service_data
                service_data[str(service_uuid)] = data

            advertisement_data = AdvertisementData(
                local_name=local_name,
                manufacturer_data=manuf_data,
                service_data=service_data,
                service_uuids=service_uuids,
                tx_power=advertisement.tx_power,
                rssi=advertisement.rssi,
                platform_data=(None, None),
            )

            device = self.create_or_update_device(
                str(advertisement.address),
                local_name,
                {},
                advertisement_data,
            )
            self.call_detection_callbacks(device, advertisement_data)

        self.device.on("advertisement", on_advertisement)

        await start_transport(self._adapter)
        await self.device.power_on()
        await self.device.start_scanning(active=self._scan_active)

That said, I still have no success. After adding some logging to bumble and pyee I can confirm that no events are being emitted, so I'm not really sure where to look next.

JPHutchins avatar Nov 17 '24 01:11 JPHutchins

Aw, I see this was supposed to work with multiple inheritance of Device.Listener. I would favor dependency injection instead, for clarity: self._listener = OurListenerImpl().

JPHutchins avatar Nov 17 '24 01:11 JPHutchins

Can confirm the scanner (Zephyr FW) is working well with Bumble's example!

python -m examples.run_scanner usb:2fe3:000B

JPHutchins avatar Nov 17 '24 02:11 JPHutchins

I think this can be refactored to align with the example more closely: https://github.com/google/bumble/blob/5e959d638e6a9c99e62536d0a3472cf4e6616ccf/examples/run_scanner.py#L36-L78

Specifically, class consrtuctors in Python async should not do any IO. So we can wait until some action is taken to open connections and do async IO, for example.

JPHutchins avatar Nov 17 '24 02:11 JPHutchins

The following scanner implementation is working well for me. Obviously would need to be adapted to fit with the rest of the API, but it's nice to see it running.

# SPDX-License-Identifier: MIT
# Copyright (c) 2024 Victor Chavez

import logging
import os
from typing import Dict, Final, List, Literal, Optional

from bumble.core import AdvertisingData, AdvertisingDataObject
from bumble.device import Advertisement, Device
from bumble.hci import Address
from bumble.transport import open_transport

from bleak.backends.scanner import (
    AdvertisementData,
    AdvertisementDataCallback,
    BaseBleakScanner,
)

logger = logging.getLogger(__name__)

SERVICE_UUID_TYPES = [
    AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
    AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
    AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
    AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
    AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
    AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
]

# Arbitrary BD_ADDR for the scanner device
SCANNER_BD_ADDR = "F0:F1:F2:F3:F4:F5"


class BleakScannerBumble(BaseBleakScanner):
    """
    Interface for Bleak Bluetooth LE Scanners

    Args:
        detection_callback:
            Optional function that will be called each time a device is
            discovered or advertising data has changed.
        service_uuids:
            Optional list of service UUIDs to filter on. Only advertisements
            containing this advertising data will be received.
    Keyword Args:
        adapter (BumbleTransport): Bumble transport adapter to use.
    """

    def __init__(
        self,
        detection_callback: Optional[AdvertisementDataCallback],
        service_uuids: Optional[List[str]],
        scanning_mode: Literal["active", "passive"],
        **kwargs,
    ):
        super().__init__(detection_callback, service_uuids)

        self._scanning_mode: Final = scanning_mode

        self._device: Optional[Device] = None

    async def on_connection(self, connection):
        pass

    async def start(self) -> None:

        def on_advertisement(advertisement: Advertisement):
            logger.debug(f"Received advertisement: {advertisement}")

            service_uuids: List[str] = []
            service_data: Dict[str, AdvertisingDataObject] = {}
            local_name = advertisement.data.get(AdvertisingData.COMPLETE_LOCAL_NAME)
            if not local_name:
                local_name = advertisement.data.get(
                    AdvertisingData.SHORTENED_LOCAL_NAME
                )
            manuf_data = advertisement.data.get(
                AdvertisingData.MANUFACTURER_SPECIFIC_DATA
            )
            for uuid_type in SERVICE_UUID_TYPES:
                adv_uuids = advertisement.data.get(uuid_type)
                if adv_uuids is None:
                    continue
                if not isinstance(adv_uuids, list):
                    continue
                for uuid in adv_uuids:
                    if uuid not in service_uuids:
                        service_uuids.append(str(uuid))

            for service_data in advertisement.data.get_all(
                AdvertisingData.SERVICE_DATA
            ):
                service_uuid, data = service_data
                service_data[str(service_uuid)] = data

            advertisement_data = AdvertisementData(
                local_name=local_name,
                manufacturer_data=manuf_data,
                service_data=service_data,
                service_uuids=service_uuids,
                tx_power=advertisement.tx_power,
                rssi=advertisement.rssi,
                platform_data=(None, None),
            )

            device = self.create_or_update_device(
                str(advertisement.address),
                local_name,
                {},
                advertisement_data,
            )
            self.call_detection_callbacks(device, advertisement_data)

        hci_transport: Final = await open_transport(os.environ["BLEAK_BUMBLE"])

        self._device = Device.with_hci(
            "scanner_dev",
            Address(SCANNER_BD_ADDR),
            hci_transport.source,
            hci_transport.sink,
        )

        self._device.on("advertisement", on_advertisement)

        await self._device.power_on()
        await self._device.start_scanning(active=self._scanning_mode == "active")

    async def stop(self) -> None:
        if self._device is None:
            raise RuntimeError("Scanner not started")

        await self._device.stop_scanning()
        await self._device.power_off()

        self._device = None

    def set_scanning_filter(self, **kwargs) -> None:
        # Implement scanning filter setup
        pass

JPHutchins avatar Nov 17 '24 07:11 JPHutchins

There's another wrinkle with Bumble. The library defines open_transport as an async function, but this is not necessary in the case of opening a USB transport - that is, open_usb_transport has no awaits.

Defining the USBTransport class in a factory function is a problem.

JPHutchins avatar Nov 17 '24 07:11 JPHutchins

Thanks for the tests and suggestions to make it work with usb 👍

vChavezB avatar Nov 17 '24 18:11 vChavezB

I have done some more type safety changes, and moved operation calls of the Bumble Controller stack to function calls.

In addition, I removed the listeners and use the on function to get access to the function calls through the emitters of Bumble.

vChavezB avatar Nov 17 '24 22:11 vChavezB

The following scanner implementation is working well for me. Obviously would need to be adapted to fit with the rest of the API, but it's nice to see it running.

I think the modifications you made were due to having an HCI controller (Zephyr MCU).

graph LR
    A[Bleak Bumble - HCI Host] <-->B[Zephyr - HCI Controller]<--> C[Bluetooth Radio]

For my use case its the other way around.

graph LR
    A[Bleak Bumble - HCI Controller] <-->B[Zephyr - HCI Host]

Perhaps there should be an env variable or setting to set the backend mode:

  1. Communicate with an HCI controller: Used when your native OS drivers does not support the HCI controller.
  2. Communicating with a HCI host: Used for virtualization and cross-platform functional tests with bluetooth.

vChavezB avatar Nov 17 '24 22:11 vChavezB

Just a short update. You can now set the HCI Mode (Host or controller).

To set as HCI Host env. variable BLEAK_BUMBLE_HOST can be set or via argument.

vChavezB avatar Dec 29 '24 12:12 vChavezB

Hey @vChavezB - thanks for your work on this! bumble is a tremendously valuable tool for Bluetooth development of all sorts, and it would be phenomenal to have this merged into bleak.

jpwright avatar Aug 15 '25 21:08 jpwright

@jpwright I would appreciate some user feedback if everything works, this PR is outdated as it uses bleak 0.22. The latest changes for bleak v1.0.1 are in this branch.

vChavezB avatar Aug 17 '25 04:08 vChavezB

Overall, it worked fine for me. The only feedback I would give is to consider the use case where you have both a BleakScanner and BleakClient alive at the same time. Right now you have both BleakScanner.start() and BleakClient.connect() open the bumble transport, but it can't be shared. I made a slight tweak just to externally manage the transport, and would suggest passing that as an optional argument.

jpwright avatar Sep 04 '25 05:09 jpwright

I have decided to make the bumble backend as an out of tree project. @jpwright if you want to contribute your changes feel free to do so at https://github.com/vChavezB/bleak-bumble/

vChavezB avatar Sep 04 '25 11:09 vChavezB