Files
serv_benchmark/backend/app/utils/lsusb_parser.py
Gilles Soulier c67befc549 addon
2026-01-05 16:08:01 +01:00

247 lines
9.1 KiB
Python
Executable File

"""
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