1# Copyright 2021-2022 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15# ----------------------------------------------------------------------------- 16# This tool lists all the USB devices, with details about each device. 17# For each device, the different possible Bumble transport strings that can 18# refer to it are listed. If the device is known to be a Bluetooth HCI device, 19# its identifier is printed in reverse colors, and the transport names in cyan color. 20# For other devices, regardless of their type, the transport names are printed 21# in red. Whether that device is actually a Bluetooth device or not depends on 22# whether it is a Bluetooth device that uses a non-standard Class, or some other 23# type of device (there's no way to tell). 24# ----------------------------------------------------------------------------- 25 26# ----------------------------------------------------------------------------- 27# Imports 28# ----------------------------------------------------------------------------- 29import os 30import logging 31import click 32import usb1 33 34from bumble.colors import color 35from bumble.transport.usb import load_libusb 36 37 38# ----------------------------------------------------------------------------- 39# Constants 40# ----------------------------------------------------------------------------- 41USB_DEVICE_CLASS_DEVICE = 0x00 42USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0 43USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01 44USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER = 0x01 45 46USB_DEVICE_CLASSES = { 47 0x00: 'Device', 48 0x01: 'Audio', 49 0x02: 'Communications and CDC Control', 50 0x03: 'Human Interface Device', 51 0x05: 'Physical', 52 0x06: 'Still Imaging', 53 0x07: 'Printer', 54 0x08: 'Mass Storage', 55 0x09: 'Hub', 56 0x0A: 'CDC Data', 57 0x0B: 'Smart Card', 58 0x0D: 'Content Security', 59 0x0E: 'Video', 60 0x0F: 'Personal Healthcare', 61 0x10: 'Audio/Video', 62 0x11: 'Billboard', 63 0x12: 'USB Type-C Bridge', 64 0x3C: 'I3C', 65 0xDC: 'Diagnostic', 66 USB_DEVICE_CLASS_WIRELESS_CONTROLLER: ( 67 'Wireless Controller', 68 { 69 0x01: { 70 0x01: 'Bluetooth', 71 0x02: 'UWB', 72 0x03: 'Remote NDIS', 73 0x04: 'Bluetooth AMP', 74 } 75 }, 76 ), 77 0xEF: 'Miscellaneous', 78 0xFE: 'Application Specific', 79 0xFF: 'Vendor Specific', 80} 81 82USB_ENDPOINT_IN = 0x80 83USB_ENDPOINT_TYPES = ['CONTROL', 'ISOCHRONOUS', 'BULK', 'INTERRUPT'] 84 85USB_BT_HCI_CLASS_TUPLE = ( 86 USB_DEVICE_CLASS_WIRELESS_CONTROLLER, 87 USB_DEVICE_SUBCLASS_RF_CONTROLLER, 88 USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER, 89) 90 91 92# ----------------------------------------------------------------------------- 93def show_device_details(device): 94 for configuration in device: 95 print(f' Configuration {configuration.getConfigurationValue()}') 96 for interface in configuration: 97 for setting in interface: 98 alternate_setting = setting.getAlternateSetting() 99 suffix = ( 100 f'/{alternate_setting}' if interface.getNumSettings() > 1 else '' 101 ) 102 (class_string, subclass_string) = get_class_info( 103 setting.getClass(), setting.getSubClass(), setting.getProtocol() 104 ) 105 details = f'({class_string}, {subclass_string})' 106 print(f' Interface: {setting.getNumber()}{suffix} {details}') 107 for endpoint in setting: 108 endpoint_type = USB_ENDPOINT_TYPES[endpoint.getAttributes() & 3] 109 endpoint_direction = ( 110 'OUT' 111 if (endpoint.getAddress() & USB_ENDPOINT_IN == 0) 112 else 'IN' 113 ) 114 print( 115 f' Endpoint 0x{endpoint.getAddress():02X}: ' 116 f'{endpoint_type} {endpoint_direction}' 117 ) 118 119 120# ----------------------------------------------------------------------------- 121def get_class_info(cls, subclass, protocol): 122 class_info = USB_DEVICE_CLASSES.get(cls) 123 protocol_string = '' 124 if class_info is None: 125 class_string = f'0x{cls:02X}' 126 else: 127 if isinstance(class_info, tuple): 128 class_string = class_info[0] 129 subclass_info = class_info[1].get(subclass) 130 if subclass_info: 131 protocol_string = subclass_info.get(protocol) 132 if protocol_string is not None: 133 protocol_string = f' [{protocol_string}]' 134 135 else: 136 class_string = class_info 137 138 subclass_string = f'{subclass}/{protocol}{protocol_string}' 139 140 return (class_string, subclass_string) 141 142 143# ----------------------------------------------------------------------------- 144def is_bluetooth_hci(device): 145 # Check if the device class indicates a match 146 if ( 147 device.getDeviceClass(), 148 device.getDeviceSubClass(), 149 device.getDeviceProtocol(), 150 ) == USB_BT_HCI_CLASS_TUPLE: 151 return True 152 153 # If the device class is 'Device', look for a matching interface 154 if device.getDeviceClass() == USB_DEVICE_CLASS_DEVICE: 155 for configuration in device: 156 for interface in configuration: 157 for setting in interface: 158 if ( 159 setting.getClass(), 160 setting.getSubClass(), 161 setting.getProtocol(), 162 ) == USB_BT_HCI_CLASS_TUPLE: 163 return True 164 165 return False 166 167 168# ----------------------------------------------------------------------------- 169@click.command() 170@click.option('--verbose', is_flag=True, default=False, help='Print more details') 171def main(verbose): 172 logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper()) 173 174 load_libusb() 175 with usb1.USBContext() as context: 176 bluetooth_device_count = 0 177 devices = {} 178 179 for device in context.getDeviceIterator(skip_on_error=True): 180 device_class = device.getDeviceClass() 181 device_subclass = device.getDeviceSubClass() 182 device_protocol = device.getDeviceProtocol() 183 184 device_id = (device.getVendorID(), device.getProductID()) 185 186 (device_class_string, device_subclass_string) = get_class_info( 187 device_class, device_subclass, device_protocol 188 ) 189 190 try: 191 device_serial_number = device.getSerialNumber() 192 except usb1.USBError: 193 device_serial_number = None 194 195 try: 196 device_manufacturer = device.getManufacturer() 197 except usb1.USBError: 198 device_manufacturer = None 199 200 try: 201 device_product = device.getProduct() 202 except usb1.USBError: 203 device_product = None 204 205 device_is_bluetooth_hci = is_bluetooth_hci(device) 206 if device_is_bluetooth_hci: 207 bluetooth_device_count += 1 208 fg_color = 'black' 209 bg_color = 'yellow' 210 else: 211 fg_color = 'yellow' 212 bg_color = 'black' 213 214 # Compute the different ways this can be referenced as a Bumble transport 215 bumble_transport_names = [] 216 basic_transport_name = ( 217 f'usb:{device.getVendorID():04X}:{device.getProductID():04X}' 218 ) 219 220 if device_is_bluetooth_hci: 221 bumble_transport_names.append(f'usb:{bluetooth_device_count - 1}') 222 223 if device_id not in devices: 224 bumble_transport_names.append(basic_transport_name) 225 else: 226 bumble_transport_names.append( 227 f'{basic_transport_name}#{len(devices[device_id])}' 228 ) 229 230 if device_serial_number is not None: 231 if ( 232 device_id not in devices 233 or device_serial_number not in devices[device_id] 234 ): 235 bumble_transport_names.append( 236 f'{basic_transport_name}/{device_serial_number}' 237 ) 238 239 # Print the results 240 print( 241 color( 242 f'ID {device.getVendorID():04X}:{device.getProductID():04X}', 243 fg=fg_color, 244 bg=bg_color, 245 ) 246 ) 247 if bumble_transport_names: 248 print( 249 color(' Bumble Transport Names:', 'blue'), 250 ' or '.join( 251 color(x, 'cyan' if device_is_bluetooth_hci else 'red') 252 for x in bumble_transport_names 253 ), 254 ) 255 print( 256 color(' Bus/Device: ', 'green'), 257 f'{device.getBusNumber():03}/{device.getDeviceAddress():03}', 258 ) 259 print(color(' Class: ', 'green'), device_class_string) 260 print(color(' Subclass/Protocol: ', 'green'), device_subclass_string) 261 if device_serial_number is not None: 262 print(color(' Serial: ', 'green'), device_serial_number) 263 if device_manufacturer is not None: 264 print(color(' Manufacturer: ', 'green'), device_manufacturer) 265 if device_product is not None: 266 print(color(' Product: ', 'green'), device_product) 267 268 if verbose: 269 show_device_details(device) 270 271 print() 272 273 devices.setdefault(device_id, []).append(device_serial_number) 274 275 276# ----------------------------------------------------------------------------- 277if __name__ == '__main__': 278 main() # pylint: disable=no-value-for-parameter 279