| # Copyright 2021-2022 Google LLC | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # https://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # ----------------------------------------------------------------------------- | |
| # This tool lists all the USB devices, with details about each device. | |
| # For each device, the different possible Bumble transport strings that can | |
| # refer to it are listed. If the device is known to be a Bluetooth HCI device, | |
| # its identifier is printed in reverse colors, and the transport names in cyan color. | |
| # For other devices, regardless of their type, the transport names are printed | |
| # in red. Whether that device is actually a Bluetooth device or not depends on | |
| # whether it is a Bluetooth device that uses a non-standard Class, or some other | |
| # type of device (there's no way to tell). | |
| # ----------------------------------------------------------------------------- | |
| # ----------------------------------------------------------------------------- | |
| # Imports | |
| # ----------------------------------------------------------------------------- | |
| from typing import Any | |
| import click | |
| import usb1 | |
| import bumble.logging | |
| from bumble.colors import color | |
| from bumble.transport.usb import load_libusb | |
| # ----------------------------------------------------------------------------- | |
| # Constants | |
| # ----------------------------------------------------------------------------- | |
| USB_DEVICE_CLASS_DEVICE = 0x00 | |
| USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0 | |
| USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01 | |
| USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER = 0x01 | |
| USB_DEVICE_CLASSES = { | |
| 0x00: 'Device', | |
| 0x01: 'Audio', | |
| 0x02: 'Communications and CDC Control', | |
| 0x03: 'Human Interface Device', | |
| 0x05: 'Physical', | |
| 0x06: 'Still Imaging', | |
| 0x07: 'Printer', | |
| 0x08: 'Mass Storage', | |
| 0x09: 'Hub', | |
| 0x0A: 'CDC Data', | |
| 0x0B: 'Smart Card', | |
| 0x0D: 'Content Security', | |
| 0x0E: 'Video', | |
| 0x0F: 'Personal Healthcare', | |
| 0x10: 'Audio/Video', | |
| 0x11: 'Billboard', | |
| 0x12: 'USB Type-C Bridge', | |
| 0x3C: 'I3C', | |
| 0xDC: 'Diagnostic', | |
| USB_DEVICE_CLASS_WIRELESS_CONTROLLER: ( | |
| 'Wireless Controller', | |
| { | |
| 0x01: { | |
| 0x01: 'Bluetooth', | |
| 0x02: 'UWB', | |
| 0x03: 'Remote NDIS', | |
| 0x04: 'Bluetooth AMP', | |
| } | |
| }, | |
| ), | |
| 0xEF: 'Miscellaneous', | |
| 0xFE: 'Application Specific', | |
| 0xFF: 'Vendor Specific', | |
| } | |
| USB_ENDPOINT_IN = 0x80 | |
| USB_ENDPOINT_TYPES = ['CONTROL', 'ISOCHRONOUS', 'BULK', 'INTERRUPT'] | |
| USB_BT_HCI_CLASS_TUPLE = ( | |
| USB_DEVICE_CLASS_WIRELESS_CONTROLLER, | |
| USB_DEVICE_SUBCLASS_RF_CONTROLLER, | |
| USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER, | |
| ) | |
| # ----------------------------------------------------------------------------- | |
| def show_device_details(device): | |
| for configuration in device: | |
| print(f' Configuration {configuration.getConfigurationValue()}') | |
| for interface in configuration: | |
| for setting in interface: | |
| alternate_setting = setting.getAlternateSetting() | |
| suffix = ( | |
| f'/{alternate_setting}' if interface.getNumSettings() > 1 else '' | |
| ) | |
| (class_string, subclass_string) = get_class_info( | |
| setting.getClass(), setting.getSubClass(), setting.getProtocol() | |
| ) | |
| details = f'({class_string}, {subclass_string})' | |
| print(f' Interface: {setting.getNumber()}{suffix} {details}') | |
| for endpoint in setting: | |
| endpoint_type = USB_ENDPOINT_TYPES[endpoint.getAttributes() & 3] | |
| endpoint_direction = ( | |
| 'OUT' | |
| if (endpoint.getAddress() & USB_ENDPOINT_IN == 0) | |
| else 'IN' | |
| ) | |
| print( | |
| f' Endpoint 0x{endpoint.getAddress():02X}: ' | |
| f'{endpoint_type} {endpoint_direction}' | |
| ) | |
| # ----------------------------------------------------------------------------- | |
| def get_class_info(cls, subclass, protocol): | |
| class_info = USB_DEVICE_CLASSES.get(cls) | |
| protocol_string = '' | |
| if class_info is None: | |
| class_string = f'0x{cls:02X}' | |
| else: | |
| if isinstance(class_info, tuple): | |
| class_string = class_info[0] | |
| subclass_info = class_info[1].get(subclass) | |
| if subclass_info: | |
| protocol_string = subclass_info.get(protocol) | |
| if protocol_string is not None: | |
| protocol_string = f' [{protocol_string}]' | |
| else: | |
| class_string = class_info | |
| subclass_string = f'{subclass}/{protocol}{protocol_string}' | |
| return (class_string, subclass_string) | |
| # ----------------------------------------------------------------------------- | |
| def is_bluetooth_hci(device): | |
| # Check if the device class indicates a match | |
| if ( | |
| device.getDeviceClass(), | |
| device.getDeviceSubClass(), | |
| device.getDeviceProtocol(), | |
| ) == USB_BT_HCI_CLASS_TUPLE: | |
| return True | |
| # If the device class is 'Device', look for a matching interface | |
| if device.getDeviceClass() == USB_DEVICE_CLASS_DEVICE: | |
| for configuration in device: | |
| for interface in configuration: | |
| for setting in interface: | |
| if ( | |
| setting.getClass(), | |
| setting.getSubClass(), | |
| setting.getProtocol(), | |
| ) == USB_BT_HCI_CLASS_TUPLE: | |
| return True | |
| return False | |
| # ----------------------------------------------------------------------------- | |
| @click.command() | |
| @click.option('--verbose', is_flag=True, default=False, help='Print more details') | |
| @click.option('--hci-only', is_flag=True, default=False, help='only show HCI device') | |
| @click.option('--manufacturer', help='filter by manufacturer') | |
| @click.option('--product', help='filter by product') | |
| def main(verbose: bool, manufacturer: str, product: str, hci_only: bool): | |
| bumble.logging.setup_basic_logging('WARNING') | |
| load_libusb() | |
| with usb1.USBContext() as context: | |
| bluetooth_device_count = 0 | |
| devices: dict[tuple[Any, Any], list[str | None]] = {} | |
| for device in context.getDeviceIterator(skip_on_error=True): | |
| device_class = device.getDeviceClass() | |
| device_subclass = device.getDeviceSubClass() | |
| device_protocol = device.getDeviceProtocol() | |
| device_id = (device.getVendorID(), device.getProductID()) | |
| (device_class_string, device_subclass_string) = get_class_info( | |
| device_class, device_subclass, device_protocol | |
| ) | |
| try: | |
| device_serial_number = device.getSerialNumber() | |
| except usb1.USBError: | |
| device_serial_number = None | |
| try: | |
| device_manufacturer = device.getManufacturer() | |
| except usb1.USBError: | |
| device_manufacturer = None | |
| try: | |
| device_product = device.getProduct() | |
| except usb1.USBError: | |
| device_product = None | |
| device_is_bluetooth_hci = is_bluetooth_hci(device) | |
| if device_is_bluetooth_hci: | |
| bluetooth_device_count += 1 | |
| fg_color = 'black' | |
| bg_color = 'yellow' | |
| else: | |
| fg_color = 'yellow' | |
| bg_color = 'black' | |
| # Compute the different ways this can be referenced as a Bumble transport | |
| bumble_transport_names = [] | |
| basic_transport_name = ( | |
| f'usb:{device.getVendorID():04X}:{device.getProductID():04X}' | |
| ) | |
| if device_is_bluetooth_hci: | |
| bumble_transport_names.append(f'usb:{bluetooth_device_count - 1}') | |
| if device_id not in devices: | |
| bumble_transport_names.append(basic_transport_name) | |
| else: | |
| bumble_transport_names.append( | |
| f'{basic_transport_name}#{len(devices[device_id])}' | |
| ) | |
| if device_serial_number is not None: | |
| if ( | |
| device_id not in devices | |
| or device_serial_number not in devices[device_id] | |
| ): | |
| bumble_transport_names.append( | |
| f'{basic_transport_name}/{device_serial_number}' | |
| ) | |
| # Filter | |
| if product and device_product != product: | |
| continue | |
| if manufacturer and device_manufacturer != manufacturer: | |
| continue | |
| if not is_bluetooth_hci(device) and hci_only: | |
| continue | |
| # Print the results | |
| print( | |
| color( | |
| f'ID {device.getVendorID():04X}:{device.getProductID():04X}', | |
| fg=fg_color, | |
| bg=bg_color, | |
| ) | |
| ) | |
| if bumble_transport_names: | |
| print( | |
| color(' Bumble Transport Names:', 'blue'), | |
| ' or '.join( | |
| color(x, 'cyan' if device_is_bluetooth_hci else 'red') | |
| for x in bumble_transport_names | |
| ), | |
| ) | |
| print( | |
| color(' Bus/Device: ', 'green'), | |
| f'{device.getBusNumber():03}/{device.getDeviceAddress():03}', | |
| ) | |
| print(color(' Class: ', 'green'), device_class_string) | |
| print(color(' Subclass/Protocol: ', 'green'), device_subclass_string) | |
| if device_serial_number is not None: | |
| print(color(' Serial: ', 'green'), device_serial_number) | |
| if device_manufacturer is not None: | |
| print(color(' Manufacturer: ', 'green'), device_manufacturer) | |
| if device_product is not None: | |
| print(color(' Product: ', 'green'), device_product) | |
| if verbose: | |
| show_device_details(device) | |
| print() | |
| devices.setdefault(device_id, []).append(device_serial_number) | |
| # ----------------------------------------------------------------------------- | |
| if __name__ == '__main__': | |
| main() # pylint: disable=no-value-for-parameter |