CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-libusb1

Pure-python wrapper for libusb-1.0 providing comprehensive USB device access with support for all transfer types

Pending
Overview
Eval results
Files

descriptors.mddocs/

USB Descriptors

USB descriptors provide structured access to device configuration information including configurations, interfaces, alternate settings, and endpoints. These read-only objects mirror the USB descriptor hierarchy and allow applications to understand device capabilities and requirements.

Capabilities

Configuration Descriptors

Configuration descriptors represent complete device configurations containing all interfaces and their settings.

class USBConfiguration:
    def getNumInterfaces(self):
        """
        Get number of interfaces in this configuration.
        
        Returns:
            int: Number of interfaces
        """

    def getConfigurationValue(self):
        """
        Get configuration value used in SetConfiguration requests.
        
        Returns:
            int: Configuration value (1-based)
        """

    def getDescriptor(self):
        """
        Get string descriptor index for configuration description.
        
        Returns:
            int: String descriptor index (0 if none)
        """

    def getAttributes(self):
        """
        Get configuration attributes bitfield.
        
        Returns:
            int: Attributes (bit 7: bus-powered, bit 6: self-powered, bit 5: remote wakeup)
        """

    def getMaxPower(self):
        """
        Get maximum power consumption in milliamps.
        
        Returns:
            int: Power consumption in mA
            
        Note:
            Automatically scales descriptor value based on device speed
            (2mA units for high-speed, 8mA units for super-speed).
        """

    def getExtra(self):
        """
        Get extra (non-standard) descriptors associated with configuration.
        
        Returns:
            list: List of extra descriptor bytes
        """

    def __len__(self):
        """
        Get number of interfaces (same as getNumInterfaces).
        
        Returns:
            int: Number of interfaces
        """

    def __iter__(self):
        """
        Iterate over interfaces in this configuration.
        
        Yields:
            USBInterface: Interface objects
        """

    def __getitem__(self, interface):
        """
        Get interface by index.
        
        Args:
            interface (int): Interface index (0-based)
            
        Returns:
            USBInterface: Interface object
            
        Raises:
            IndexError: If interface index is invalid
            TypeError: If interface is not an integer
        """

Interface Descriptors

Interface descriptors group related endpoints that provide a specific function or service.

class USBInterface:
    def getNumSettings(self):
        """
        Get number of alternate settings for this interface.
        
        Returns:
            int: Number of alternate settings
        """

    def __len__(self):
        """
        Get number of alternate settings (same as getNumSettings).
        
        Returns:
            int: Number of alternate settings
        """

    def __iter__(self):
        """
        Iterate over alternate settings in this interface.
        
        Yields:
            USBInterfaceSetting: Interface setting objects
        """

    def __getitem__(self, alt_setting):
        """
        Get alternate setting by index.
        
        Args:
            alt_setting (int): Alternate setting index (0-based)
            
        Returns:
            USBInterfaceSetting: Interface setting object
            
        Raises:
            IndexError: If alternate setting index is invalid
            TypeError: If alt_setting is not an integer
        """

Interface Setting Descriptors

Interface setting descriptors define specific configurations of an interface with its endpoints and characteristics.

class USBInterfaceSetting:
    def getNumber(self):
        """
        Get interface number.
        
        Returns:
            int: Interface number (unique within configuration)
        """

    def getAlternateSetting(self):
        """
        Get alternate setting number.
        
        Returns:
            int: Alternate setting number (0-based)
        """

    def getNumEndpoints(self):
        """
        Get number of endpoints in this interface setting.
        
        Returns:
            int: Number of endpoints (excluding control endpoint 0)
        """

    def getClass(self):
        """
        Get interface class code.
        
        Returns:
            int: USB interface class (e.g., 3=HID, 9=Hub, 10=CDC Data)
        """

    def getSubClass(self):
        """
        Get interface subclass code.
        
        Returns:
            int: USB interface subclass
        """

    def getClassTuple(self):
        """
        Get (class, subclass) tuple for convenient matching.
        
        Returns:
            tuple[int, int]: (class, subclass) pair
        """

    def getProtocol(self):
        """
        Get interface protocol code.
        
        Returns:
            int: USB interface protocol
        """

    def getDescriptor(self):
        """
        Get string descriptor index for interface description.
        
        Returns:
            int: String descriptor index (0 if none)
        """

    def getExtra(self):
        """
        Get extra (non-standard) descriptors associated with interface.
        
        Returns:
            list: List of extra descriptor bytes
        """

    def __len__(self):
        """
        Get number of endpoints (same as getNumEndpoints).
        
        Returns:
            int: Number of endpoints
        """

    def __iter__(self):
        """
        Iterate over endpoints in this interface setting.
        
        Yields:
            USBEndpoint: Endpoint objects
        """

    def __getitem__(self, endpoint):
        """
        Get endpoint by index.
        
        Args:
            endpoint (int): Endpoint index (0-based)
            
        Returns:
            USBEndpoint: Endpoint object
            
        Raises:
            ValueError: If endpoint index is invalid
            TypeError: If endpoint is not an integer
        """

Endpoint Descriptors

Endpoint descriptors define communication endpoints with their transfer characteristics and capabilities.

class USBEndpoint:
    def getAddress(self):
        """
        Get endpoint address including direction bit.
        
        Returns:
            int: Endpoint address (bit 7: direction, bits 0-3: endpoint number)
        """

    def getAttributes(self):
        """
        Get endpoint attributes defining transfer type and characteristics.
        
        Returns:
            int: Attributes bitfield
                 Bits 1-0: Transfer type (0=control, 1=isochronous, 2=bulk, 3=interrupt)
                 For isochronous: bits 3-2: synchronization, bits 5-4: usage
        """

    def getMaxPacketSize(self):
        """
        Get maximum packet size for this endpoint.
        
        Returns:
            int: Maximum packet size in bytes
            
        Note:
            For high-speed endpoints, this includes additional transaction
            opportunities encoded in upper bits.
        """

    def getInterval(self):
        """
        Get polling interval for interrupt/isochronous endpoints.
        
        Returns:
            int: Interval value (interpretation depends on speed and transfer type)
        """

    def getRefresh(self):
        """
        Get refresh rate for audio isochronous endpoints.
        
        Returns:
            int: Refresh rate (audio endpoints only)
        """

    def getSyncAddress(self):
        """
        Get synchronization endpoint address for audio isochronous endpoints.
        
        Returns:
            int: Sync endpoint address (audio endpoints only)
        """

    def getExtra(self):
        """
        Get extra (non-standard) descriptors associated with endpoint.
        
        Returns:
            list: List of extra descriptor bytes
        """

Device Descriptor Navigation

Access configuration descriptors from device objects and navigate the descriptor hierarchy.

class USBDevice:
    def __len__(self):
        """
        Get number of configurations.
        
        Returns:
            int: Number of configurations
        """

    def __getitem__(self, index):
        """
        Get configuration by index.
        
        Args:
            index (int): Configuration index (0-based)
            
        Returns:
            USBConfiguration: Configuration object
        """

    def iterConfigurations(self):
        """
        Iterate over device configurations.
        
        Yields:
            USBConfiguration: Configuration objects
        """

    def iterSettings(self):
        """
        Iterate over all interface settings in all configurations.
        
        Yields:
            USBInterfaceSetting: Interface setting objects
        """

Usage Examples

Complete Descriptor Analysis

import usb1

def analyze_device_descriptors(device):
    """Perform complete analysis of device descriptor hierarchy."""
    print(f"\nDevice {device.getVendorID():04x}:{device.getProductID():04x}")
    print(f"  {device.getNumConfigurations()} configuration(s)")
    
    for config_idx, config in enumerate(device.iterConfigurations()):
        print(f"\n  Configuration {config_idx + 1}:")
        print(f"    Value: {config.getConfigurationValue()}")
        print(f"    Interfaces: {config.getNumInterfaces()}")
        print(f"    Attributes: 0x{config.getAttributes():02x}")
        print(f"    Max Power: {config.getMaxPower()} mA")
        
        # Check for configuration description
        desc_idx = config.getDescriptor()
        if desc_idx:
            try:
                desc = device.getASCIIStringDescriptor(desc_idx)
                if desc:
                    print(f"    Description: {desc}")
            except usb1.USBError:
                pass
        
        for interface in config:
            print(f"\n    Interface {interface.getNumber()}:")
            print(f"      Alternate settings: {interface.getNumSettings()}")
            
            for setting in interface:
                print(f"\n      Setting {setting.getAlternateSetting()}:")
                print(f"        Class: {setting.getClass()}")
                print(f"        Subclass: {setting.getSubClass()}")
                print(f"        Protocol: {setting.getProtocol()}")
                print(f"        Endpoints: {setting.getNumEndpoints()}")
                
                # Check for interface description
                desc_idx = setting.getDescriptor()
                if desc_idx:
                    try:
                        desc = device.getASCIIStringDescriptor(desc_idx)
                        if desc:
                            print(f"        Description: {desc}")
                    except usb1.USBError:
                        pass
                
                for endpoint in setting:
                    addr = endpoint.getAddress()
                    direction = "IN" if addr & 0x80 else "OUT"
                    ep_num = addr & 0x0f
                    attrs = endpoint.getAttributes()
                    transfer_type = ["Control", "Isochronous", "Bulk", "Interrupt"][attrs & 3]
                    
                    print(f"          Endpoint {ep_num} {direction}:")
                    print(f"            Type: {transfer_type}")
                    print(f"            Max Packet: {endpoint.getMaxPacketSize()}")
                    print(f"            Interval: {endpoint.getInterval()}")

with usb1.USBContext() as context:
    for device in context.getDeviceIterator(skip_on_error=True):
        analyze_device_descriptors(device)

Interface Class Detection

import usb1

# Common USB interface classes
USB_CLASSES = {
    0: "Per-interface",
    1: "Audio",
    2: "Communications",
    3: "HID",
    5: "Physical",
    6: "Image",
    7: "Printer",
    8: "Mass Storage",
    9: "Hub",
    10: "CDC Data",
    11: "Smart Card",
    13: "Content Security",
    14: "Video",
    15: "Personal Healthcare",
    16: "Audio/Video",
    17: "Billboard",
    18: "USB Type-C Bridge",
    220: "Diagnostic",
    224: "Wireless Controller",
    239: "Miscellaneous",
    254: "Application Specific",
    255: "Vendor Specific"
}

def find_devices_by_class(context, target_class):
    """Find all devices with interfaces of specified class."""
    matching_devices = []
    
    for device in context.getDeviceIterator(skip_on_error=True):
        # Check device class first
        if device.getDeviceClass() == target_class:
            matching_devices.append((device, "device"))
            continue
        
        # Check interface classes
        for config in device.iterConfigurations():
            for interface in config:
                for setting in interface:
                    if setting.getClass() == target_class:
                        matching_devices.append((device, f"interface {setting.getNumber()}"))
                        break
                else:
                    continue
                break
            else:
                continue
            break
    
    return matching_devices

def interface_class_example():
    """Find and display devices by interface class."""
    with usb1.USBContext() as context:
        # Find HID devices
        hid_devices = find_devices_by_class(context, 3)  # HID class
        print(f"Found {len(hid_devices)} HID devices:")
        
        for device, location in hid_devices:
            print(f"  {device.getVendorID():04x}:{device.getProductID():04x} ({location})")
            try:
                manufacturer = device.getManufacturer()
                product = device.getProduct()
                if manufacturer or product:
                    print(f"    {manufacturer or 'Unknown'} - {product or 'Unknown'}")
            except usb1.USBError:
                pass
        
        # Find mass storage devices  
        storage_devices = find_devices_by_class(context, 8)  # Mass Storage
        print(f"\nFound {len(storage_devices)} Mass Storage devices:")
        
        for device, location in storage_devices:
            print(f"  {device.getVendorID():04x}:{device.getProductID():04x} ({location})")

interface_class_example()

Endpoint Analysis and Selection

import usb1

def analyze_endpoints(device):
    """Analyze endpoints and suggest optimal transfer methods."""
    print(f"\nEndpoint analysis for {device.getVendorID():04x}:{device.getProductID():04x}")
    
    for config in device.iterConfigurations():
        print(f"\nConfiguration {config.getConfigurationValue()}:")
        
        for interface in config:
            for setting in interface:
                if setting.getNumEndpoints() == 0:
                    continue
                
                print(f"\n  Interface {setting.getNumber()}, Setting {setting.getAlternateSetting()}:")
                class_name = USB_CLASSES.get(setting.getClass(), f"Class {setting.getClass()}")
                print(f"    Class: {class_name}")
                
                bulk_in = []
                bulk_out = []
                int_in = []
                int_out = []
                iso_in = []
                iso_out = []
                
                for endpoint in setting:
                    addr = endpoint.getAddress()
                    attrs = endpoint.getAttributes()
                    transfer_type = attrs & 3
                    is_in = bool(addr & 0x80)
                    ep_num = addr & 0x0f
                    max_packet = endpoint.getMaxPacketSize()
                    interval = endpoint.getInterval()
                    
                    ep_info = {
                        'address': addr,
                        'number': ep_num,
                        'max_packet': max_packet,
                        'interval': interval
                    }
                    
                    if transfer_type == 2:  # Bulk
                        if is_in:
                            bulk_in.append(ep_info)
                        else:
                            bulk_out.append(ep_info)
                    elif transfer_type == 3:  # Interrupt
                        if is_in:
                            int_in.append(ep_info)
                        else:
                            int_out.append(ep_info)
                    elif transfer_type == 1:  # Isochronous
                        if is_in:
                            iso_in.append(ep_info)
                        else:
                            iso_out.append(ep_info)
                
                # Print endpoint summary and usage suggestions
                if bulk_in or bulk_out:
                    print(f"    Bulk endpoints:")
                    for ep in bulk_in:
                        print(f"      IN  0x{ep['address']:02x}: {ep['max_packet']} bytes")
                        print(f"          -> Use bulkRead(0x{ep['address']:02x}, length)")
                    for ep in bulk_out:
                        print(f"      OUT 0x{ep['address']:02x}: {ep['max_packet']} bytes")
                        print(f"          -> Use bulkWrite(0x{ep['address']:02x}, data)")
                
                if int_in or int_out:
                    print(f"    Interrupt endpoints:")
                    for ep in int_in:
                        print(f"      IN  0x{ep['address']:02x}: {ep['max_packet']} bytes, interval {ep['interval']}")
                        print(f"          -> Use interruptRead(0x{ep['address']:02x}, {ep['max_packet']})")
                    for ep in int_out:
                        print(f"      OUT 0x{ep['address']:02x}: {ep['max_packet']} bytes, interval {ep['interval']}")
                        print(f"          -> Use interruptWrite(0x{ep['address']:02x}, data)")
                
                if iso_in or iso_out:
                    print(f"    Isochronous endpoints:")
                    for ep in iso_in:
                        print(f"      IN  0x{ep['address']:02x}: {ep['max_packet']} bytes, interval {ep['interval']}")
                        print(f"          -> Use async transfer with setIsochronous()")
                    for ep in iso_out:
                        print(f"      OUT 0x{ep['address']:02x}: {ep['max_packet']} bytes, interval {ep['interval']}")
                        print(f"          -> Use async transfer with setIsochronous()")

with usb1.USBContext() as context:
    for device in context.getDeviceIterator(skip_on_error=True):
        if device.getNumConfigurations() > 0:
            analyze_endpoints(device)

Configuration Selection Helper

import usb1

def select_best_configuration(device, required_class=None, min_endpoints=0):
    """
    Select the best configuration based on criteria.
    
    Args:
        device: USBDevice to analyze
        required_class: Required interface class (None for any)
        min_endpoints: Minimum number of endpoints needed
        
    Returns:
        tuple: (config_value, interface_number, alt_setting) or None
    """
    best_config = None
    best_score = -1
    
    for config in device.iterConfigurations():
        config_score = 0
        
        for interface in config:
            for setting in interface:
                setting_score = 0
                
                # Check class requirement
                if required_class is not None:
                    if setting.getClass() == required_class:
                        setting_score += 100
                    else:
                        continue  # Skip if class doesn't match
                
                # Check endpoint requirement
                num_endpoints = setting.getNumEndpoints()
                if num_endpoints >= min_endpoints:
                    setting_score += num_endpoints * 10
                else:
                    continue  # Skip if not enough endpoints
                
                # Prefer interface 0, alternate setting 0
                if setting.getNumber() == 0:
                    setting_score += 5
                if setting.getAlternateSetting() == 0:
                    setting_score += 3
                
                # Prefer configurations with both IN and OUT endpoints
                has_in = False
                has_out = False
                for endpoint in setting:
                    if endpoint.getAddress() & 0x80:
                        has_in = True
                    else:
                        has_out = True
                
                if has_in and has_out:
                    setting_score += 20
                elif has_in or has_out:
                    setting_score += 10
                
                config_score = max(config_score, setting_score)
                
                if setting_score > best_score:
                    best_score = setting_score
                    best_config = (
                        config.getConfigurationValue(),
                        setting.getNumber(),
                        setting.getAlternateSetting()
                    )
    
    return best_config

def configuration_selection_example():
    """Demonstrate automatic configuration selection."""
    with usb1.USBContext() as context:
        for device in context.getDeviceIterator(skip_on_error=True):
            # Skip hubs and other system devices
            if device.getDeviceClass() == 9:  # Hub
                continue
            
            print(f"\nDevice {device.getVendorID():04x}:{device.getProductID():04x}")
            
            # Try to find best configuration for different use cases
            scenarios = [
                ("HID device", 3, 1),          # HID class, at least 1 endpoint
                ("Mass storage", 8, 2),        # Mass storage, at least 2 endpoints
                ("General communication", None, 2),  # Any class, at least 2 endpoints
                ("Any interface", None, 0),    # Any configuration
            ]
            
            for scenario_name, req_class, min_eps in scenarios:
                result = select_best_configuration(device, req_class, min_eps)
                if result:
                    config_val, interface_num, alt_setting = result
                    print(f"  {scenario_name}: Config {config_val}, Interface {interface_num}, Alt {alt_setting}")
                    break
            else:
                print(f"  No suitable configuration found")

configuration_selection_example()

Descriptor Caching and Comparison

import usb1
import json

def cache_device_descriptors(device):
    """Cache device descriptor information as JSON-serializable data."""
    cache = {
        'vendor_id': device.getVendorID(),
        'product_id': device.getProductID(),
        'device_class': device.getDeviceClass(),
        'device_subclass': device.getDeviceSubClass(),
        'device_protocol': device.getDeviceProtocol(),
        'usb_version': device.getbcdUSB(),
        'device_version': device.getbcdDevice(),
        'speed': device.getDeviceSpeed(),
        'configurations': []
    }
    
    # Cache string descriptors
    try:
        cache['manufacturer'] = device.getManufacturer()
        cache['product'] = device.getProduct()
        cache['serial'] = device.getSerialNumber()
    except usb1.USBError:
        pass
    
    # Cache configuration information
    for config in device.iterConfigurations():
        config_data = {
            'value': config.getConfigurationValue(),
            'attributes': config.getAttributes(),
            'max_power': config.getMaxPower(),
            'interfaces': []
        }
        
        for interface in config:
            interface_data = {
                'number': interface[0].getNumber(),  # All settings have same number
                'settings': []
            }
            
            for setting in interface:
                setting_data = {
                    'alt_setting': setting.getAlternateSetting(),
                    'class': setting.getClass(),
                    'subclass': setting.getSubClass(),
                    'protocol': setting.getProtocol(),
                    'endpoints': []
                }
                
                for endpoint in setting:
                    endpoint_data = {
                        'address': endpoint.getAddress(),
                        'attributes': endpoint.getAttributes(),
                        'max_packet_size': endpoint.getMaxPacketSize(),
                        'interval': endpoint.getInterval()
                    }
                    setting_data['endpoints'].append(endpoint_data)
                
                interface_data['settings'].append(setting_data)
            
            config_data['interfaces'].append(interface_data)
        
        cache['configurations'].append(config_data)
    
    return cache

def compare_descriptors(cache1, cache2):
    """Compare two descriptor caches and report differences."""
    differences = []
    
    # Compare basic device info
    basic_fields = ['vendor_id', 'product_id', 'device_class', 'usb_version']
    for field in basic_fields:
        if cache1.get(field) != cache2.get(field):
            differences.append(f"{field}: {cache1.get(field)} -> {cache2.get(field)}")
    
    # Compare configuration count
    if len(cache1['configurations']) != len(cache2['configurations']):
        differences.append(f"Configuration count: {len(cache1['configurations'])} -> {len(cache2['configurations'])}")
    
    # Compare configurations
    for i, (config1, config2) in enumerate(zip(cache1['configurations'], cache2['configurations'])):
        if config1['max_power'] != config2['max_power']:
            differences.append(f"Config {i+1} max power: {config1['max_power']} -> {config2['max_power']}")
        
        if len(config1['interfaces']) != len(config2['interfaces']):
            differences.append(f"Config {i+1} interface count: {len(config1['interfaces'])} -> {len(config2['interfaces'])}")
    
    return differences

def descriptor_caching_example():
    """Demonstrate descriptor caching and comparison."""
    device_cache = {}
    
    with usb1.USBContext() as context:
        print("Caching device descriptors...")
        
        for device in context.getDeviceIterator(skip_on_error=True):
            device_id = f"{device.getVendorID():04x}:{device.getProductID():04x}"
            cache = cache_device_descriptors(device)
            device_cache[device_id] = cache
            
            print(f"Cached {device_id}: {len(cache['configurations'])} configurations")
    
    # Save cache to file
    with open('usb_device_cache.json', 'w') as f:
        json.dump(device_cache, f, indent=2)
    print(f"Saved cache for {len(device_cache)} devices")
    
    # Example: Load and compare (in real use, this would be from a previous run)
    print("\nComparison example (comparing cache with itself):")
    for device_id, cache in list(device_cache.items())[:3]:  # Just first 3 devices
        differences = compare_descriptors(cache, cache)
        if differences:
            print(f"{device_id}: {len(differences)} differences found")
        else:
            print(f"{device_id}: No differences (as expected)")

descriptor_caching_example()

Install with Tessl CLI

npx tessl i tessl/pypi-libusb1

docs

async-transfers.md

descriptors.md

device-access.md

index.md

sync-transfers.md

usb-context.md

tile.json