Load libusb-1.0 shared library from libusb_package wheel

It would be nice to pip install bumble without having to first install
the libusb system dependency. Expecially on platforms like Windows and
Mac, without a default package manager.

The libusb_package Python package distributes prebuilt libusb-1.0 shared
libraries for each OS and architecture as binary wheels for the pyusb
project. Add this package as a dependency for bumble.

For the pyusb transport, the libusb_package.find() function is a drop-in
replacement for pyusb.core.find(). It searches the libusb_package
site-path before system paths and creates a pyusb backend.

For the usb transport, use libusb_package.get_library_path() to return a
path to the libusb-1.0 library in site-packages. If this path exists,
create a ctypes DLL and init the usb1 backend. This only needs to be
done once. All future calls to usb1 will use this opened library.
If the library path does not exist, do nothing, and usb1 will search
default system paths when the usb1.USBContext object is created.

This commit pins the libusb_package dependency at 1.0.26.0 to ensure
every bumble install uses the exact same version of the libusb library.
This commit is contained in:
Michael Mogenson
2022-12-15 10:06:16 -05:00
parent f5fe3d87f2
commit 86f9496575
5 changed files with 31 additions and 5 deletions

View File

@@ -31,6 +31,7 @@ import logging
import sys
import click
import usb1
from bumble.transport.usb import load_libusb
from colors import color
@@ -169,6 +170,7 @@ def is_bluetooth_hci(device):
def main(verbose):
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
load_libusb()
with usb1.USBContext() as context:
bluetooth_device_count = 0
devices = {}

View File

@@ -17,6 +17,7 @@
# -----------------------------------------------------------------------------
import asyncio
import logging
import libusb_package
import usb.core
import usb.util
import threading
@@ -203,13 +204,13 @@ async def open_pyusb_transport(spec):
# Find the device according to the spec moniker
if ':' in spec:
vendor_id, product_id = spec.split(':')
device = usb.core.find(
device = libusb_package.find(
idVendor=int(vendor_id, 16), idProduct=int(product_id, 16)
)
else:
device_index = int(spec)
devices = list(
usb.core.find(
libusb_package.find(
find_all=1,
bDeviceClass=USB_DEVICE_CLASS_WIRELESS_CONTROLLER,
bDeviceSubClass=USB_DEVICE_SUBCLASS_RF_CONTROLLER,

View File

@@ -17,9 +17,12 @@
# -----------------------------------------------------------------------------
import asyncio
import logging
import libusb_package
import usb1
import threading
import collections
import ctypes
import platform
from colors import color
from .common import Transport, ParserSource
@@ -33,6 +36,19 @@ logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
def load_libusb():
'''
Attempt to load the libusb-1.0 C library from libusb_package in site-packages.
If library exists, we create a DLL object and initialize the usb1 backend.
This only needs to be done once, but bufore a usb1.USBContext is created.
If library does not exists, do nothing and usb1 will search default system paths
when usb1.USBContext is created.
'''
if libusb_path := libusb_package.get_library_path():
dll_loader = ctypes.WinDLL if platform.system() == 'Windows' else ctypes.CDLL
libusb_dll = dll_loader(libusb_path, use_errno=True, use_last_error=True)
usb1.loadLibrary(libusb_dll)
async def open_usb_transport(spec):
'''
Open a USB transport.
@@ -305,6 +321,7 @@ async def open_usb_transport(spec):
self.context.close()
# Find the device according to the spec moniker
load_libusb()
context = usb1.USBContext()
context.open()
try:

View File

@@ -22,10 +22,10 @@ the first USB interface of the device will be used, regardless of the interface
This may be useful for some devices that use a custom class/subclass but may nonetheless work as-is.
!!! examples
`usb:04b4:f901`
`usb:04b4:f901`
The USB dongle with `<vendor>` equal to `04b4` and `<product>` equal to `f901`
`usb:0`
`usb:0`
The first Bluetooth HCI dongle that's declared as such by Class/Subclass/Protocol
`usb:04b4:f901/0016A45B05D8`
@@ -42,6 +42,11 @@ This may be useful for some devices that use a custom class/subclass but may non
The library includes two different implementations of the USB transport, implemented using different python bindings for `libusb`.
Using the transport prefix `pyusb:` instead of `usb:` selects the implementation based on [PyUSB](https://pypi.org/project/pyusb/), using the synchronous API of `libusb`, whereas the default implementation is based on [libusb1](https://pypi.org/project/libusb1/), using the asynchronous API of `libusb`. In order to use the alternative PyUSB-based implementation, you need to ensure that you have installed that python module, as it isn't installed by default as a dependency of Bumble.
## Libusb
The `libusb-1.0` shared library is required to use both `usb` and `pyusb` transports. This library should be installed automatically with Bumble, as part of the `libusb_package` Python package.
If your OS or architecture is not supported by `libusb_package`, you can install a system-wide library with `brew install libusb` for Mac or `apt install libusb-1.0-0` for Linux.
## Listing Available USB Devices
### With `usb_probe`
@@ -65,4 +70,4 @@ Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 003 Device 014: ID 0b05:17cb ASUSTek Computer, Inc. Broadcom BCM20702A0 Bluetooth
```
The device id for the Bluetooth interface in this case is `0b05:17cb`.
The device id for the Bluetooth interface in this case is `0b05:17cb`.

View File

@@ -37,6 +37,7 @@ install_requires =
cryptography == 35; platform_system!='Emscripten'
grpcio >= 1.46; platform_system!='Emscripten'
libusb1 >= 2.0.1; platform_system!='Emscripten'
libusb-package == 1.0.26.0; platform_system!='Emscripten'
prompt_toolkit >= 3.0.16; platform_system!='Emscripten'
protobuf >= 3.12.4
pyee >= 8.2.2