diff --git a/apps/show.py b/apps/show.py index ad68b49..bba6c66 100644 --- a/apps/show.py +++ b/apps/show.py @@ -20,7 +20,7 @@ import click from colors import color from bumble import hci -from bumble.transport import PacketReader +from bumble.transport.common import PacketReader from bumble.helpers import PacketTracer diff --git a/bumble/l2cap.py b/bumble/l2cap.py index f506483..7a2ca2b 100644 --- a/bumble/l2cap.py +++ b/bumble/l2cap.py @@ -243,7 +243,7 @@ class L2CAP_Control_Frame: # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ - ('reason', {'size': 2, 'mapper': lambda x: L2CAP_Command_Reject.map_reason(x)}), + ('reason', {'size': 2, 'mapper': lambda x: L2CAP_Command_Reject.reason_name(x)}), ('data', '*') ]) class L2CAP_Command_Reject(L2CAP_Control_Frame): @@ -262,7 +262,7 @@ class L2CAP_Command_Reject(L2CAP_Control_Frame): } @staticmethod - def map_reason(reason): + def reason_name(reason): return name_or_number(L2CAP_Command_Reject.REASON_NAMES, reason) @@ -330,7 +330,7 @@ class L2CAP_Configure_Request(L2CAP_Control_Frame): @L2CAP_Control_Frame.subclass([ ('source_cid', 2), ('flags', 2), - ('result', {'size': 2, 'mapper': lambda x: L2CAP_Configure_Response.map_result(x)}), + ('result', {'size': 2, 'mapper': lambda x: L2CAP_Configure_Response.result_name(x)}), ('options', '*') ]) class L2CAP_Configure_Response(L2CAP_Control_Frame): @@ -355,7 +355,7 @@ class L2CAP_Configure_Response(L2CAP_Control_Frame): } @staticmethod - def map_result(result): + def result_name(result): return name_or_number(L2CAP_Configure_Response.RESULT_NAMES, result) @@ -403,31 +403,49 @@ class L2CAP_Echo_Response(L2CAP_Control_Frame): # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ - ('info_type', 2) + ('info_type', {'size': 2, 'mapper': lambda x: L2CAP_Information_Request.info_type_name(x)}) ]) class L2CAP_Information_Request(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.10 INFORMATION REQUEST ''' - SUCCESS = 0x00 - NOT_SUPPORTED = 0x01 - CONNECTIONLESS_MTU = 0x0001 EXTENDED_FEATURES_SUPPORTED = 0x0002 FIXED_CHANNELS_SUPPORTED = 0x0003 + INFO_TYPE_NAMES = { + CONNECTIONLESS_MTU: 'CONNECTIONLESS_MTU', + EXTENDED_FEATURES_SUPPORTED: 'EXTENDED_FEATURES_SUPPORTED', + FIXED_CHANNELS_SUPPORTED: 'FIXED_CHANNELS_SUPPORTED' + } + + @staticmethod + def info_type_name(info_type): + return name_or_number(L2CAP_Information_Request.INFO_TYPE_NAMES, info_type) + # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ - ('info_type', 2), - ('result', 2), + ('info_type', {'size': 2, 'mapper': L2CAP_Information_Request.info_type_name}), + ('result', {'size': 2, 'mapper': lambda x: L2CAP_Information_Response.result_name(x)}), ('data', '*') ]) class L2CAP_Information_Response(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.11 INFORMATION RESPONSE ''' + SUCCESS = 0x00 + NOT_SUPPORTED = 0x01 + + RESULT_NAMES = { + SUCCESS: 'SUCCESS', + NOT_SUPPORTED: 'NOT_SUPPORTED' + } + + @staticmethod + def result_name(result): + return name_or_number(L2CAP_Information_Response.RESULT_NAMES, result) # ----------------------------------------------------------------------------- @@ -473,7 +491,7 @@ class L2CAP_LE_Credit_Based_Connection_Request(L2CAP_Control_Frame): ('mtu', 2), ('mps', 2), ('initial_credits', 2), - ('result', {'size': 2, 'mapper': lambda x: L2CAP_LE_Credit_Based_Connection_Response.map_result(x)}) + ('result', {'size': 2, 'mapper': lambda x: L2CAP_LE_Credit_Based_Connection_Response.result_name(x)}) ]) class L2CAP_LE_Credit_Based_Connection_Response(L2CAP_Control_Frame): ''' @@ -505,7 +523,7 @@ class L2CAP_LE_Credit_Based_Connection_Response(L2CAP_Control_Frame): } @staticmethod - def map_result(result): + def result_name(result): return name_or_number(L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_RESULT_NAMES, result) @@ -980,13 +998,13 @@ class ChannelManager: def on_l2cap_information_request(self, connection, cid, request): if request.info_type == L2CAP_Information_Request.CONNECTIONLESS_MTU: - result = L2CAP_Information_Request.SUCCESS + result = L2CAP_Information_Response.SUCCESS data = struct.pack(' or : + either or :[/] With as the 0-based index to select amongst all the devices that appear to be supporting Bluetooth HCI (0 being the first one), or - Where and are the vendor ID and product ID in hexadecimal. + Where and are the vendor ID and product ID in hexadecimal. The + / suffix max be specified when more than one device with the same + vendor and product identifiers are present. Examples: 0 --> the first BT USB dongle 04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901 + 04b4:f901/00E04C239987 --> the BT USB dongle with vendor=04b4 and product=f901 and serial number 00E04C239987 ''' USB_RECIPIENT_DEVICE = 0x00 @@ -268,22 +271,32 @@ async def open_usb_transport(spec): found = None if ':' in spec: vendor_id, product_id = spec.split(':') - found = context.getByVendorIDAndProductID(int(vendor_id, 16), int(product_id, 16), skip_on_error=True) + if '/' in product_id: + product_id, serial_number = product_id.split('/') + for device in context.getDeviceIterator(skip_on_error=True): + if ( + device.getVendorID() == int(vendor_id, 16) and + device.getProductID() == int(product_id, 16) and + device.getSerialNumber() == serial_number + ): + found = device + break + device.close() + else: + found = context.getByVendorIDAndProductID(int(vendor_id, 16), int(product_id, 16), skip_on_error=True) else: device_index = int(spec) - device_iterator = context.getDeviceIterator(skip_on_error=True) - try: - for device in device_iterator: - if device.getDeviceClass() == USB_DEVICE_CLASS_WIRELESS_CONTROLLER and \ - device.getDeviceSubClass() == USB_DEVICE_SUBCLASS_RF_CONTROLLER and \ - device.getDeviceProtocol() == USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER: - if device_index == 0: - found = device - break - device_index -= 1 - device.close() - finally: - device_iterator.close() + for device in context.getDeviceIterator(skip_on_error=True): + if ( + device.getDeviceClass() == USB_DEVICE_CLASS_WIRELESS_CONTROLLER and + device.getDeviceSubClass() == USB_DEVICE_SUBCLASS_RF_CONTROLLER and + device.getDeviceProtocol() == USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER + ): + if device_index == 0: + found = device + break + device_index -= 1 + device.close() if found is None: context.close() diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index b385591..3bade7b 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -5,6 +5,8 @@ use_directory_urls: false nav: - Introduction: index.md - Getting Started: getting_started.md + - Development: + - Python Environments: development/python_environments.md - Use Cases: - Overview: use_cases/index.md - Use Case 1: use_cases/use_case_1.md @@ -63,7 +65,10 @@ theme: custom_dir: 'theme' plugins: - - mkdocstrings + - mkdocstrings: + handlers: + python: + paths: [../..] docs_dir: 'src' diff --git a/docs/mkdocs/src/apps_and_tools/index.md b/docs/mkdocs/src/apps_and_tools/index.md index 616932f..e2bd67f 100644 --- a/docs/mkdocs/src/apps_and_tools/index.md +++ b/docs/mkdocs/src/apps_and_tools/index.md @@ -7,6 +7,6 @@ These include: * [Console](console.md) - an interactive text-based console * [HCI Bridge](hci_bridge.md) - a HCI transport bridge to connect two HCI transports and filter/snoop the HCI packets * [Golden Gate Bridge](gg_bridge.md) - a bridge between GATT and UDP to use with the Golden Gate "stack tool" - * [`Show`](show.md) - Parse a file with HCI packets and print the details of each packet in a human readable form - * [`Link Relay`](link_relay.md) - WebSocket relay for virtual RemoteLink instances to communicate with each other. + * [Show](show.md) - Parse a file with HCI packets and print the details of each packet in a human readable form + * [Link Relay](link_relay.md) - WebSocket relay for virtual RemoteLink instances to communicate with each other. diff --git a/docs/mkdocs/src/development/python_environments.md b/docs/mkdocs/src/development/python_environments.md new file mode 100644 index 0000000..0be529e --- /dev/null +++ b/docs/mkdocs/src/development/python_environments.md @@ -0,0 +1,45 @@ +PYTHON ENVIRONMENTS +=================== + +When you don't want to install Bumble in your main/default python environment, +using a virtual environment, where the package and its dependencies can be +installed, isolated from the rest, may be useful. + +There are many flavors of python environments and dependency managers. +This page describes a few of the most common ones. + + +## venv + +`venv` is a standard module that is included with python. +Visit the [`venv` documentation](https://docs.python.org/3/library/venv.html) page for details. + +## Pyenv + +`pyenv` lets you easily switch between multiple versions of Python. It's simple, unobtrusive, and follows the UNIX tradition of single-purpose tools that do one thing well. +Visit the [`pyenv` site](https://github.com/pyenv/pyenv) for instructions on how to install +and use `pyenv` + +## Conda + +Conda is a convenient package manager and virtual environment. +The file `environment.yml` is a Conda environment file that you can use to create +a new Conda environment. Once created, you can simply activate this environment when +working with Bumble. +Visit the [Conda site](https://docs.conda.io/en/latest/) for instructions on how to install +and use Conda. +A few useful commands: + +### Create a new `bumble` Conda environment +``` +$ conda env create -f environment.yml +``` +This will create a new environment, named `bumble`, which you can then activate with: +``` +$ conda activate bumble +``` + +### Update an existing `bumble` environment +``` +$ conda env update -f environment.yml +``` diff --git a/docs/mkdocs/src/getting_started.md b/docs/mkdocs/src/getting_started.md index d9163b4..0d29563 100644 --- a/docs/mkdocs/src/getting_started.md +++ b/docs/mkdocs/src/getting_started.md @@ -20,7 +20,7 @@ You may be simply using Bumble as a module for your own application or as a depe module, or you may be working on modifying or contributing to the Bumble module or example code itself. -# Working With Bumble As A Module +# Using Bumble As A Python Module ## Installing @@ -29,52 +29,40 @@ manager, or from source. !!! tip "Python Virtual Environments" When you install Bumble, you have the option to install it as part of your default - python environment, or in a virtual environment, such as a `venv`, `pyenv` or `conda` environment - -### venv - -`venv` is a standard module that is included with python. -Visit the [`venv` documentation](https://docs.python.org/3/library/venv.html) page for details. - -### Pyenv - -`pyenv` lets you easily switch between multiple versions of Python. It's simple, unobtrusive, and follows the UNIX tradition of single-purpose tools that do one thing well. -Visit the [`pyenv` site](https://github.com/pyenv/pyenv) for instructions on how to install -and use `pyenv` - -### Conda - -Conda is a convenient package manager and virtual environment. -The file `environment.yml` is a Conda environment file that you can use to create -a new Conda environment. Once created, you can simply activate this environment when -working with Bumble. -Visit the [Conda side](https://docs.conda.io/en/latest/) for instructions on how to install -and use Conda. -A few useful commands: - -#### Create a new `bumble` Conda environment -``` -$ conda env create -f environment.yml -``` -This will create a new environment, named `bumble`, which you can then activate with: -``` -$ conda activate bumble -``` - -#### Update an existing `bumble` environment -``` -$ conda env update -f environment.yml -``` + python environment, or in a virtual environment, such as a `venv`, `pyenv` or `conda` environment. + See the [Python Environments page](development/python_environments.md) page for details. ### Install From Source -The instructions for working with virtual Python environments above also apply in this case. - -Install with `pip` +Install with `pip`. Run in a command shell in the directory where you downloaded the source +distribution ``` $ python -m pip install -e . ``` +### Install from GitHub + +You can install directly from GitHub without first downloading the repo. + +Install the latest commit from the main branch with `pip`: +``` +$ python -m pip install git+https://github.com/google/bumble.git +``` + +You can specify a specific tag. + +Install tag `v0.0.1` with `pip`: +``` +$ python -m pip install git+https://github.com/google/bumble.git@v0.0.1 +``` + +You can also specify a specific commit. + +Install commit `27c0551` with `pip`: +``` +$ python -m pip install git+https://github.com/google/bumble.git@27c0551 +``` + # Working On The Bumble Code When you work on the Bumble code itself, and run some of the tests or example apps, or import the module in your own code, you typically either install the package from source in "development mode" as described above, or you may choose to skip the install phase. @@ -106,3 +94,8 @@ Setting `PYTHONPATH` locally with each command would look something like: ``` $ PYTHONPATH=. python examples/run_advertiser.py examples/device1.json serial:/dev/tty.usbmodem0006839912171 ``` + +# Where To Go Next +Once you've installed or downloaded Bumble, you can either start using some of the +[Bundled apps and tools](apps_and_tools/index.md), or look at the [examples](examples/index.md) +to get a feel for how to use the APIs, and start writing your own applications. \ No newline at end of file diff --git a/docs/mkdocs/src/platforms/android.md b/docs/mkdocs/src/platforms/android.md index 2b94072..872edc7 100644 --- a/docs/mkdocs/src/platforms/android.md +++ b/docs/mkdocs/src/platforms/android.md @@ -46,7 +46,7 @@ through it with Android applications as well as system-managed profiles. To connect a Bumble host stack to a Root Canal virtual controller instance, use the bumble `android-emulator` transport in `host` mode (the default). -!!! example "Running the example GATT server connected to the emulator" +!!! example "Run the example GATT server connected to the emulator" ``` shell $ python run_gatt_server.py device1.json android-emulator ``` @@ -57,7 +57,7 @@ This is an advanced use case, which may not be officially supported, but should versions of the emulator. You will likely need to start the emulator from the command line, in order to specify the `-forward-vhci` option (unless the emulator offers a way to control that feature from a user/ui menu). -!!! example "Launching the emulator with VHCI forwarding" +!!! example "Launch the emulator with VHCI forwarding" In this example, we launch an emulator AVD named "Tiramisu" ```shell $ emulator -forward-vhci -avd Tiramisu @@ -70,10 +70,20 @@ You will likely need to start the emulator from the command line, in order to sp To connect a virtual controller to the Android Bluetooth stack, use the bumble `android-emulator` transport in `controller` mode. For example, using the default gRPC port, the transport name would be: `android-emulator:mode=controller`. -!!! example "connecting the Android emulator to the first USB Bluetooth dongle, using the `hci_bridge` application" +!!! example "Connect the Android emulator to the first USB Bluetooth dongle, using the `hci_bridge` application" ```shell $ bumble-hci-bridge android-emulator:mode=controller usb:0 ``` +## Other Tools +The `show` application that's included with Bumble can be used to parse and pretty-print the HCI packets +from an Android HCI "snoop log" (see [this page](https://source.android.com/devices/bluetooth/verifying_debugging) +for details on how to obtain HCI snoop logs from an Android device). +Use the `--format snoop` option to specify that the file is in that specific format. + +!!! example "Analyze an Android HCI snoop log file" + ```shell + $ bumble-show --format snoop btsnoop_hci.log + ``` diff --git a/setup.cfg b/setup.cfg index d07dbb5..1711d7c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,7 +26,6 @@ packages = bumble, bumble.transport, bumble.apps, bumble.apps.link_relay package_dir = bumble = bumble bumble.apps = apps - bumble.apps.link_relay = apps/link_relay install_requires = aioconsole >= 0.4.1 ansicolors >= 1.1 @@ -66,4 +65,4 @@ development = documentation = mkdocs >= 1.2.3 mkdocs-material >= 8.1.9 - mkdocstrings >= 0.17.0 + mkdocstrings[python] >= 0.19.0