# 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 # ----------------------------------------------------------------------------- import os import logging import click import usb1 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') 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 = {} 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}' ) # 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