247 lines
9.1 KiB
Python
Executable File
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
|