279 lines
10 KiB
Python
279 lines
10 KiB
Python
# 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
|