""" lsusb output parser for USB device detection and extraction. Parses output from 'lsusb -v' and extracts individual device information. """ import re from typing import List, Dict, Any, Optional def detect_usb_devices(lsusb_output: str) -> List[Dict[str, str]]: """ Detect all USB devices from lsusb -v output. Returns a list of devices with their Bus line and basic info. Args: lsusb_output: Raw output from 'lsusb -v' command Returns: List of dicts with keys: bus_line, bus, device, id, vendor_id, product_id, description Example: [ { "bus_line": "Bus 002 Device 003: ID 0781:55ab SanDisk Corp. ...", "bus": "002", "device": "003", "id": "0781:55ab", "vendor_id": "0x0781", "product_id": "0x55ab", "description": "SanDisk Corp. ..." }, ... ] """ devices = [] lines = lsusb_output.strip().split('\n') for line in lines: line_stripped = line.strip() # Match lines starting with "Bus" # Format: "Bus 002 Device 003: ID 0781:55ab SanDisk Corp. ..." match = re.match(r'^Bus\s+(\d+)\s+Device\s+(\d+):\s+ID\s+([0-9a-fA-F]{4}):([0-9a-fA-F]{4})\s*(.*)$', line_stripped) if match: bus = match.group(1) device_num = match.group(2) vendor_id = match.group(3).lower() product_id = match.group(4).lower() description = match.group(5).strip() devices.append({ "bus_line": line_stripped, "bus": bus, "device": device_num, "id": f"{vendor_id}:{product_id}", "vendor_id": f"0x{vendor_id}", "product_id": f"0x{product_id}", "description": description }) return devices def extract_device_section(lsusb_output: str, bus: str, device: str) -> Optional[str]: """ Extract the complete section for a specific device from lsusb -v output. Args: lsusb_output: Raw output from 'lsusb -v' command bus: Bus number (e.g., "002") device: Device number (e.g., "003") Returns: Complete section for the device, from its Bus line to the next Bus line (or end) """ lines = lsusb_output.strip().split('\n') # Build the pattern to match the target device's Bus line target_pattern = re.compile(rf'^Bus\s+{bus}\s+Device\s+{device}:') section_lines = [] in_section = False for line in lines: # Check if this is the start of our target device if target_pattern.match(line): in_section = True section_lines.append(line) continue # If we're in the section if in_section: # Check if we've hit the next device (new Bus line) if line.startswith('Bus '): # End of our section break # Add the line to our section section_lines.append(line) if section_lines: return '\n'.join(section_lines) return None def parse_device_info(device_section: str) -> Dict[str, Any]: """ Parse detailed information from a device section. Args: device_section: The complete lsusb output for a single device Returns: Dictionary with parsed device information including interface classes """ result = { "vendor_id": None, # idVendor "product_id": None, # idProduct "manufacturer": None, # iManufacturer (fabricant) "product": None, # iProduct (modele) "serial": None, "usb_version": None, # bcdUSB (declared version) "device_class": None, # bDeviceClass "device_subclass": None, "device_protocol": None, "interface_classes": [], # CRITICAL: bInterfaceClass from all interfaces "max_power": None, # MaxPower (in mA) "speed": None, # Negotiated speed (determines actual USB type) "usb_type": None, # Determined from negotiated speed "requires_firmware": False, # True if any interface is Vendor Specific (255) "is_bus_powered": None, "is_self_powered": None, "power_sufficient": None # Based on MaxPower vs port capacity } lines = device_section.split('\n') # Parse the first line (Bus line) - contains idVendor:idProduct and vendor name # Format: "Bus 002 Device 005: ID 0bda:8176 Realtek Semiconductor Corp." first_line = lines[0] if lines else "" bus_match = re.match(r'^Bus\s+\d+\s+Device\s+\d+:\s+ID\s+([0-9a-fA-F]{4}):([0-9a-fA-F]{4})\s*(.*)$', first_line) if bus_match: result["vendor_id"] = f"0x{bus_match.group(1).lower()}" result["product_id"] = f"0x{bus_match.group(2).lower()}" # Extract vendor name from first line (marque = text after IDs) vendor_name = bus_match.group(3).strip() if vendor_name: result["manufacturer"] = vendor_name # Parse detailed fields current_interface = False for line in lines[1:]: line_stripped = line.strip() # iManufacturer (fabricant) mfg_match = re.search(r'iManufacturer\s+\d+\s+(.+?)$', line_stripped) if mfg_match: result["manufacturer"] = mfg_match.group(1).strip() # iProduct (modele) prod_match = re.search(r'iProduct\s+\d+\s+(.+?)$', line_stripped) if prod_match: result["product"] = prod_match.group(1).strip() # iSerial serial_match = re.search(r'iSerial\s+\d+\s+(.+?)$', line_stripped) if serial_match: result["serial"] = serial_match.group(1).strip() # bcdUSB (declared version, not definitive) usb_ver_match = re.search(r'bcdUSB\s+([\d.]+)', line_stripped) if usb_ver_match: result["usb_version"] = usb_ver_match.group(1).strip() # bDeviceClass class_match = re.search(r'bDeviceClass\s+(\d+)\s+(.+?)$', line_stripped) if class_match: result["device_class"] = class_match.group(1).strip() # bDeviceSubClass subclass_match = re.search(r'bDeviceSubClass\s+(\d+)', line_stripped) if subclass_match: result["device_subclass"] = subclass_match.group(1).strip() # bDeviceProtocol protocol_match = re.search(r'bDeviceProtocol\s+(\d+)', line_stripped) if protocol_match: result["device_protocol"] = protocol_match.group(1).strip() # MaxPower (extract numeric value in mA) power_match = re.search(r'MaxPower\s+(\d+)\s*mA', line_stripped) if power_match: result["max_power"] = power_match.group(1).strip() # bmAttributes (to determine Bus/Self powered) attr_match = re.search(r'bmAttributes\s+0x([0-9a-fA-F]+)', line_stripped) if attr_match: attrs = int(attr_match.group(1), 16) # Bit 6: Self Powered, Bit 5: Remote Wakeup result["is_self_powered"] = bool(attrs & 0x40) result["is_bus_powered"] = not result["is_self_powered"] # CRITICAL: bInterfaceClass (this determines Mass Storage, not bDeviceClass) interface_class_match = re.search(r'bInterfaceClass\s+(\d+)\s+(.+?)$', line_stripped) if interface_class_match: class_code = int(interface_class_match.group(1)) class_name = interface_class_match.group(2).strip() result["interface_classes"].append({ "code": class_code, "name": class_name }) # Check for Vendor Specific (255) - requires firmware if class_code == 255: result["requires_firmware"] = True # Detect negotiated speed (determines actual USB type) # Format can be: "Device Qualifier (for other device speed):" or speed mentioned speed_patterns = [ (r'1\.5\s*Mb(?:it)?/s|Low\s+Speed', 'Low Speed', 'USB 1.1'), (r'12\s*Mb(?:it)?/s|Full\s+Speed', 'Full Speed', 'USB 1.1'), (r'480\s*Mb(?:it)?/s|High\s+Speed', 'High Speed', 'USB 2.0'), (r'5000\s*Mb(?:it)?/s|5\s*Gb(?:it)?/s|SuperSpeed(?:\s+USB)?(?:\s+Gen\s*1)?', 'SuperSpeed', 'USB 3.0'), (r'10\s*Gb(?:it)?/s|SuperSpeed\s+USB\s+Gen\s*2|SuperSpeed\+', 'SuperSpeed+', 'USB 3.1'), (r'20\s*Gb(?:it)?/s|SuperSpeed\s+USB\s+Gen\s*2x2', 'SuperSpeed Gen 2x2', 'USB 3.2'), ] for pattern, speed_name, usb_type in speed_patterns: if re.search(pattern, line_stripped, re.IGNORECASE): result["speed"] = speed_name result["usb_type"] = usb_type break # Determine power sufficiency based on USB type and MaxPower if result["max_power"]: max_power_ma = int(result["max_power"]) usb_type = result.get("usb_type", "USB 2.0") # Default to USB 2.0 # Normative port capacities if "USB 3" in usb_type: port_capacity = 900 # USB 3.x: 900 mA @ 5V = 4.5W else: port_capacity = 500 # USB 2.0: 500 mA @ 5V = 2.5W result["power_sufficient"] = max_power_ma <= port_capacity return result