bleak
bleak copied to clipboard
Added support for Google´s Bumble Bluetooth Controller stack
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:
- Bluetooth Functional tests without Hardware. Example of Bluetooth stacks that support virtualization are Android Emulator and Zephyr RTOS.
- 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 :)
Fixed some linting and doc issues.
It looks like you may not have updated the lock file. Please run poetry lock --no-update, commit, and push. thanks!
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.
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.
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.
I have applied some of the suggestions from @JPHutchins. I will also check the typesafety of the changes in this PR.
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~
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
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.
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 @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.
What do you think about committing FW images for some common platforms?
Sounds like a job for a separate repo.
Build HCI USB sample for NRF52840DK:
west build -b nrf52840dk/nrf52840 zephyr/samples/bluetooth/hci_usb
Flash
west flash
NRF52840DK device shows up!
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.
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).
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.
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?
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.
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().
Can confirm the scanner (Zephyr FW) is working well with Bumble's example!
python -m examples.run_scanner usb:2fe3:000B
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.
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
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.
Thanks for the tests and suggestions to make it work with usb 👍
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.
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:
- Communicate with an HCI controller: Used when your native OS drivers does not support the HCI controller.
- Communicating with a HCI host: Used for virtualization and cross-platform functional tests with bluetooth.
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.
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 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.
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.
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/