• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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