diff --git a/Linux.py b/Linux.py
new file mode 100644
index 0000000..3d42582
--- /dev/null
+++ b/Linux.py
@@ -0,0 +1,940 @@
+#!/usr/bin/env python3
+"""
+Linux USB Port Mapping Tool for Hackintosh
+Part of USBToolBox - Linux Implementation
+
+This module provides USB controller and device detection on Linux systems
+for preparing USB port mappings for macOS Hackintosh installations.
+
+IMPORTANT LIMITATIONS:
+- This is a PREPARATION tool, not a final kext generator
+- Port connector types (Type-A vs Type-C) cannot be determined from Linux
+- Final USB mapping must be completed in macOS using Hackintool or similar
+
+Supported macOS versions: Sonoma, Sequoia, Tahoe 26
+
+STANDALONE VERSION: Does not require termcolor2 or base.py
+"""
+
+import json
+import os
+import re
+import subprocess
+import sys
+from dataclasses import dataclass, field
+from datetime import datetime
+from enum import IntEnum
+from pathlib import Path
+from typing import Optional, List, Dict, Any
+
+
+# Color Output (ANSI codes - no external dependency)
+
+class Colors:
+ """ANSI color codes for terminal output."""
+ BLUE = "\033[36;1m"
+ GREEN = "\033[32;1m"
+ YELLOW = "\033[33;1m"
+ RED = "\033[31;1m"
+ RESET = "\033[0m"
+
+ @classmethod
+ def disable(cls):
+ """Disable colors for non-TTY output."""
+ cls.BLUE = cls.GREEN = cls.YELLOW = cls.RED = cls.RESET = ""
+
+
+# Disable colors if not a TTY
+if not sys.stdout.isatty():
+ Colors.disable()
+
+
+# Enums (Self-contained, matching shared.py)
+
+class USBDeviceSpeeds(IntEnum):
+ """USB device speed classifications."""
+ LowSpeed = 0
+ FullSpeed = 1
+ HighSpeed = 2
+ SuperSpeed = 3
+ SuperSpeedPlus = 4
+ Unknown = 9999
+
+ def __str__(self) -> str:
+ speed_names = {
+ USBDeviceSpeeds.LowSpeed: "USB 1.1 (Low)",
+ USBDeviceSpeeds.FullSpeed: "USB 1.1 (Full)",
+ USBDeviceSpeeds.HighSpeed: "USB 2.0",
+ USBDeviceSpeeds.SuperSpeed: "USB 3.0",
+ USBDeviceSpeeds.SuperSpeedPlus: "USB 3.1+",
+ USBDeviceSpeeds.Unknown: "Unknown",
+ }
+ return speed_names.get(self, "Unknown")
+
+
+class USBPhysicalPortTypes(IntEnum):
+ """USB physical port/connector types (per ACPI spec)."""
+ USBTypeA = 0
+ USBTypeMiniAB = 1
+ ExpressCard = 2
+ USB3TypeA = 3
+ USB3TypeB = 4
+ USB3TypeMicroB = 5
+ USB3TypeMicroAB = 6
+ USB3TypePowerB = 7
+ USB3TypeC_USB2Only = 8
+ USB3TypeC_WithSwitch = 9
+ USB3TypeC_WithoutSwitch = 10
+ Internal = 255
+
+ def __str__(self) -> str:
+ type_names = {
+ USBPhysicalPortTypes.USBTypeA: "Type A (USB 2)",
+ USBPhysicalPortTypes.USB3TypeA: "USB 3 Type A",
+ USBPhysicalPortTypes.USB3TypeC_WithSwitch: "Type C (with switch)",
+ USBPhysicalPortTypes.USB3TypeC_WithoutSwitch: "Type C (no switch)",
+ USBPhysicalPortTypes.Internal: "Internal",
+ }
+ return type_names.get(self, f"Type {int(self)}")
+
+
+class USBControllerTypes(IntEnum):
+ """USB host controller types."""
+ UHCI = 0x00
+ OHCI = 0x10
+ EHCI = 0x20
+ XHCI = 0x30
+ Unknown = 9999
+
+ def __str__(self) -> str:
+ ctrl_names = {
+ USBControllerTypes.UHCI: "USB 1.1 (UHCI)",
+ USBControllerTypes.OHCI: "USB 1.1 (OHCI)",
+ USBControllerTypes.EHCI: "USB 2.0 (EHCI)",
+ USBControllerTypes.XHCI: "USB 3.0 (XHCI)",
+ USBControllerTypes.Unknown: "Unknown",
+ }
+ return ctrl_names.get(self, "Unknown")
+
+
+# Data Classes for USB Topology
+
+@dataclass
+class USBDevice:
+ """Represents a connected USB device."""
+ name: str
+ vendor_id: str = ""
+ product_id: str = ""
+ bus: int = 0
+ device: int = 0
+ port: int = 0
+ speed: str = ""
+ speed_class: Optional[USBDeviceSpeeds] = None
+ instance_id: str = ""
+ is_hub: bool = False
+ children: List['USBDevice'] = field(default_factory=list)
+
+ def to_dict(self) -> Dict[str, Any]:
+ result = {
+ "name": self.name,
+ "vendor_id": self.vendor_id,
+ "product_id": self.product_id,
+ "speed": self.speed,
+ "instance_id": self.instance_id,
+ }
+ if self.children:
+ result["devices"] = [c.to_dict() for c in self.children]
+ return result
+
+
+@dataclass
+class USBPort:
+ """Represents a USB port on a controller."""
+ index: int
+ name: str = ""
+ port_class: Optional[USBDeviceSpeeds] = None
+ devices: List[USBDevice] = field(default_factory=list)
+ comment: Optional[str] = None
+ guessed_type: Optional[USBPhysicalPortTypes] = None
+ companion_port: Optional[int] = None
+ is_internal: bool = False
+ type_c: bool = False
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "index": self.index,
+ "name": self.name,
+ "class": str(self.port_class) if self.port_class else "Unknown",
+ "comment": self.comment,
+ "guessed": str(self.guessed_type) if self.guessed_type else None,
+ "devices": [d.to_dict() for d in self.devices],
+ }
+
+
+@dataclass
+class USBController:
+ """Represents a USB host controller."""
+ name: str
+ pci_id: List[str] = field(default_factory=list)
+ acpi_path: str = ""
+ bus_number: int = 0
+ controller_class: Optional[USBControllerTypes] = None
+ ports: List[USBPort] = field(default_factory=list)
+ hub_name: str = ""
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "name": self.name,
+ "identifiers": {
+ "pci_id": self.pci_id,
+ "acpi_path": self.acpi_path,
+ },
+ "class": str(self.controller_class) if self.controller_class else "Unknown",
+ "hub_name": self.hub_name,
+ "ports": [p.to_dict() for p in self.ports],
+ }
+
+
+# Linux USB Detection Implementation
+
+class LinuxUSBMap:
+ """
+ Linux implementation of USB port mapping.
+
+ Detects USB controllers and devices using:
+ - lspci: PCI device enumeration
+ - lsusb: USB device listing
+ - lsusb -t: USB topology tree
+ - /sys/bus/usb/devices: Sysfs device information
+ """
+
+ SYSFS_USB_PATH = Path("/sys/bus/usb/devices")
+
+ # Known internal device patterns (vendor:product)
+ INTERNAL_DEVICE_PATTERNS = {
+ # Intel Bluetooth
+ ("8087", "0025"): "Intel Wireless Bluetooth",
+ ("8087", "0026"): "Intel Wireless Bluetooth",
+ ("8087", "0029"): "Intel Wireless Bluetooth",
+ ("8087", "002a"): "Intel Wireless Bluetooth",
+ ("8087", "0032"): "Intel Wireless Bluetooth",
+ ("8087", "0033"): "Intel Wireless Bluetooth",
+ ("8087", "0aaa"): "Intel Wireless Bluetooth",
+ # Realtek Bluetooth
+ ("0bda", "b00a"): "Realtek Bluetooth",
+ ("0bda", "b00b"): "Realtek Bluetooth",
+ ("0bda", "b00c"): "Realtek Bluetooth",
+ ("0bda", "8771"): "Realtek Bluetooth",
+ # Broadcom Bluetooth
+ ("0a5c", "21e6"): "Broadcom Bluetooth",
+ ("0a5c", "6412"): "Broadcom Bluetooth",
+ # Webcams (common integrated - vendor ID only)
+ ("0c45", ""): "Integrated Webcam", # Sonix
+ ("5986", ""): "Integrated Webcam", # Acer
+ ("04f2", ""): "Integrated Webcam", # Chicony
+ ("0408", ""): "Integrated Webcam", # Quanta
+ ("13d3", ""): "Integrated Webcam", # IMC Networks
+ # Fingerprint readers
+ ("06cb", ""): "Fingerprint Reader", # Synaptics
+ ("138a", ""): "Fingerprint Reader", # Validity
+ ("27c6", ""): "Fingerprint Reader", # Goodix
+ # Card readers
+ ("0bda", "0129"): "Card Reader", # Realtek
+ ("0bda", "0139"): "Card Reader", # Realtek
+ }
+
+ def __init__(self):
+ self.sysfs_devices: Dict[str, Dict] = {}
+ self.lsusb_devices: List[Dict] = []
+ self.usb_tree: Dict = {}
+ self.controllers: List[Dict] = []
+ self.controllers_historical: List[Dict] = []
+
+ # =========================================================================
+ # Command Execution Helpers
+ # =========================================================================
+
+ def _run_command(self, cmd: List[str], timeout: int = 10) -> Optional[str]:
+ """Execute a shell command and return stdout."""
+ try:
+ result = subprocess.run(
+ cmd,
+ capture_output=True,
+ text=True,
+ timeout=timeout
+ )
+ if result.returncode == 0:
+ return result.stdout
+ return None
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
+ return None
+
+ # =========================================================================
+ # USB Controller Detection (lspci)
+ # =========================================================================
+
+ def _parse_lspci_output(self) -> List[Dict]:
+ """Parse lspci output to find USB controllers."""
+ controllers = []
+
+ # Get detailed lspci output with numeric IDs
+ output = self._run_command(["lspci", "-nnvv"])
+ if not output:
+ output = self._run_command(["lspci", "-nn"])
+ if not output:
+ return controllers
+
+ current_device = {}
+ for line in output.split('\n'):
+ if not line.strip():
+ if current_device and 'usb' in current_device.get('class_name', '').lower():
+ controllers.append(current_device)
+ current_device = {}
+ continue
+
+ # Device header: "00:14.0 USB controller [0c03]: Intel Corporation..."
+ header_match = re.match(
+ r'^([0-9a-f:.]+)\s+(.+?)\s+\[([0-9a-f]+)\]:\s+(.+?)\s+\[([0-9a-f]+):([0-9a-f]+)\]',
+ line, re.IGNORECASE
+ )
+ if header_match:
+ current_device = {
+ 'bdf': header_match.group(1),
+ 'class_name': header_match.group(2),
+ 'class_id': header_match.group(3),
+ 'name': header_match.group(4),
+ 'vendor_id': header_match.group(5),
+ 'device_id': header_match.group(6),
+ }
+ continue
+
+ # Subsystem line
+ subsys_match = re.match(
+ r'^\s+Subsystem:\s+.+?\s+\[([0-9a-f]+):([0-9a-f]+)\]',
+ line, re.IGNORECASE
+ )
+ if subsys_match and current_device:
+ current_device['subsys_vendor'] = subsys_match.group(1)
+ current_device['subsys_device'] = subsys_match.group(2)
+ continue
+
+ # ProgIf for controller type
+ progif_match = re.match(r'^\s+Prog-?If:\s+([0-9a-f]+)', line, re.IGNORECASE)
+ if progif_match and current_device:
+ current_device['prog_if'] = int(progif_match.group(1), 16)
+
+ if current_device and 'usb' in current_device.get('class_name', '').lower():
+ controllers.append(current_device)
+
+ return controllers
+
+ def _get_controller_type(self, pci_info: Dict) -> USBControllerTypes:
+ """Determine USB controller type from PCI programming interface."""
+ prog_if = pci_info.get('prog_if', 0)
+ name_lower = pci_info.get('name', '').lower()
+
+ # Check programming interface first (most reliable)
+ if prog_if == 0x30:
+ return USBControllerTypes.XHCI
+ elif prog_if == 0x20:
+ return USBControllerTypes.EHCI
+ elif prog_if == 0x10:
+ return USBControllerTypes.OHCI
+ elif prog_if == 0x00 and prog_if != 0:
+ return USBControllerTypes.UHCI
+
+ # Fallback to name matching (for when lspci -nnvv not available)
+ # Check for USB 3.x indicators (XHCI)
+ if any(x in name_lower for x in ['xhci', 'usb3', 'usb 3', '3.0', '3.1', '3.2']):
+ return USBControllerTypes.XHCI
+ # Check for USB 2.x indicators (EHCI)
+ elif any(x in name_lower for x in ['ehci', 'usb2', 'usb 2', '2.0']):
+ return USBControllerTypes.EHCI
+ elif 'ohci' in name_lower:
+ return USBControllerTypes.OHCI
+ elif 'uhci' in name_lower:
+ return USBControllerTypes.UHCI
+
+ # If name contains "USB" but we couldn't determine type, assume XHCI for modern systems
+ if 'usb' in name_lower:
+ return USBControllerTypes.XHCI
+
+ return USBControllerTypes.Unknown
+
+ # =========================================================================
+ # USB Device Detection (lsusb)
+ # =========================================================================
+
+ def _parse_lsusb_output(self) -> List[Dict]:
+ """Parse lsusb output to list all USB devices."""
+ devices = []
+
+ output = self._run_command(["lsusb"])
+ if not output:
+ return devices
+
+ for line in output.strip().split('\n'):
+ # Format: "Bus 001 Device 002: ID 8087:0029 Intel Corp. ..."
+ match = re.match(
+ r'Bus\s+(\d+)\s+Device\s+(\d+):\s+ID\s+([0-9a-f]+):([0-9a-f]+)\s*(.*)',
+ line, re.IGNORECASE
+ )
+ if match:
+ devices.append({
+ 'bus': int(match.group(1)),
+ 'device': int(match.group(2)),
+ 'vendor_id': match.group(3),
+ 'product_id': match.group(4),
+ 'name': match.group(5).strip() or "Unknown Device",
+ })
+
+ return devices
+
+ def _parse_lsusb_tree(self) -> Dict:
+ """Parse lsusb -t output for USB topology tree."""
+ tree = {}
+
+ output = self._run_command(["lsusb", "-t"])
+ if not output:
+ return tree
+
+ current_bus = None
+
+ for line in output.split('\n'):
+ if not line.strip():
+ continue
+
+ # Root hub: "/: Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/12p, 480M"
+ root_match = re.match(
+ r'^/:\s+Bus\s+(\d+)\.Port\s+(\d+):\s+Dev\s+(\d+),\s+Class=(\w+),\s+Driver=([^,]+),\s+(\S+)',
+ line
+ )
+ if root_match:
+ bus_num = int(root_match.group(1))
+ tree[bus_num] = {
+ 'port': int(root_match.group(2)),
+ 'device': int(root_match.group(3)),
+ 'class': root_match.group(4),
+ 'driver': root_match.group(5),
+ 'speed': root_match.group(6),
+ 'children': {}
+ }
+ current_bus = bus_num
+ continue
+
+ # Device: " |__ Port 1: Dev 2, If 0, Class=Wireless, Driver=btusb, 12M"
+ device_match = re.match(
+ r'^(\s+)\|?__\s*Port\s+(\d+):\s+Dev\s+(\d+)',
+ line
+ )
+ if device_match and current_bus is not None:
+ port_num = int(device_match.group(2))
+ dev_num = int(device_match.group(3))
+
+ # Extract speed if present
+ speed_match = re.search(r',\s*(\d+[MGT])', line)
+ speed = speed_match.group(1) if speed_match else ""
+
+ tree[current_bus]['children'][port_num] = {
+ 'port': port_num,
+ 'device': dev_num,
+ 'speed': speed,
+ }
+
+ return tree
+
+ # =========================================================================
+ # Sysfs Parsing
+ # =========================================================================
+
+ def _parse_sysfs_devices(self) -> Dict[str, Dict]:
+ """Parse /sys/bus/usb/devices for detailed device information."""
+ devices = {}
+
+ if not self.SYSFS_USB_PATH.exists():
+ return devices
+
+ for device_path in self.SYSFS_USB_PATH.iterdir():
+ if not device_path.is_symlink():
+ continue
+
+ device_name = device_path.name
+ device_info = {'path': str(device_path.resolve())}
+
+ attrs = [
+ 'idVendor', 'idProduct', 'manufacturer', 'product',
+ 'speed', 'busnum', 'devnum', 'bDeviceClass', 'devpath'
+ ]
+
+ for attr in attrs:
+ attr_path = device_path / attr
+ if attr_path.exists():
+ try:
+ device_info[attr] = attr_path.read_text().strip()
+ except (PermissionError, OSError):
+ pass
+
+ # Parse device path to get port number
+ port_match = re.match(r'^(\d+)-(\d+)(?:\.(\d+))?', device_name)
+ if port_match:
+ device_info['bus'] = int(port_match.group(1))
+ device_info['port'] = int(port_match.group(2))
+ if port_match.group(3):
+ device_info['hub_port'] = int(port_match.group(3))
+
+ devices[device_name] = device_info
+
+ return devices
+
+ def _speed_to_class(self, speed: str) -> USBDeviceSpeeds:
+ """Convert sysfs speed string to USBDeviceSpeeds enum."""
+ speed_lower = speed.lower()
+ if '10000' in speed or ('super' in speed_lower and 'plus' in speed_lower):
+ return USBDeviceSpeeds.SuperSpeedPlus
+ elif '5000' in speed or 'super' in speed_lower:
+ return USBDeviceSpeeds.SuperSpeed
+ elif '480' in speed or 'high' in speed_lower:
+ return USBDeviceSpeeds.HighSpeed
+ elif '12' in speed or 'full' in speed_lower:
+ return USBDeviceSpeeds.FullSpeed
+ elif '1.5' in speed or 'low' in speed_lower:
+ return USBDeviceSpeeds.LowSpeed
+ return USBDeviceSpeeds.Unknown
+
+ def _is_internal_device(self, vendor_id: str, product_id: str) -> bool:
+ """Check if a device is likely internal based on vendor/product ID."""
+ vid = vendor_id.lower()
+ pid = product_id.lower()
+
+ if (vid, pid) in self.INTERNAL_DEVICE_PATTERNS:
+ return True
+ if (vid, "") in self.INTERNAL_DEVICE_PATTERNS:
+ return True
+ return False
+
+ # =========================================================================
+ # Controller and Port Assembly
+ # =========================================================================
+
+ def get_controllers(self):
+ """Main method to detect USB controllers and their ports."""
+ print(f"\n{Colors.BLUE}[*] Detecting USB controllers...{Colors.RESET}")
+
+ pci_controllers = self._parse_lspci_output()
+ self.usb_tree = self._parse_lsusb_tree()
+ self.lsusb_devices = self._parse_lsusb_output()
+ self.sysfs_devices = self._parse_sysfs_devices()
+
+ controllers = []
+
+ for pci_ctrl in pci_controllers:
+ controller = USBController(
+ name=pci_ctrl.get('name', 'Unknown USB Controller'),
+ pci_id=[
+ pci_ctrl.get('vendor_id', ''),
+ pci_ctrl.get('device_id', ''),
+ pci_ctrl.get('subsys_vendor', ''),
+ pci_ctrl.get('subsys_device', ''),
+ ],
+ controller_class=self._get_controller_type(pci_ctrl),
+ )
+ controllers.append(controller)
+
+ self._populate_ports(controllers)
+ self.controllers = [c.to_dict() for c in controllers]
+
+ if not self.controllers_historical:
+ self.controllers_historical = self.controllers.copy()
+
+ print(f"{Colors.GREEN}[+] Found {len(controllers)} USB controller(s){Colors.RESET}")
+
+ return self.controllers
+
+ def _populate_ports(self, controllers: List[USBController]):
+ """Populate port information for each controller."""
+
+ # Filter sysfs devices to only include actual device entries (not interface endpoints)
+ # Device entries are like "1-2", "3-2.1" but NOT like "1-2:1.0" (interface endpoints)
+ devices_by_bus: Dict[int, List[Dict]] = {}
+ for name, info in self.sysfs_devices.items():
+ # Skip interface endpoints (contain ':')
+ if ':' in name:
+ continue
+ # Skip root hubs (start with 'usb')
+ if name.startswith('usb'):
+ continue
+ # Must have vendor ID to be a real device
+ if not info.get('idVendor'):
+ continue
+
+ bus = info.get('bus', 0)
+ if bus not in devices_by_bus:
+ devices_by_bus[bus] = []
+ devices_by_bus[bus].append({'sysfs_name': name, **info})
+
+ lsusb_by_bus_dev = {(d['bus'], d['device']): d for d in self.lsusb_devices}
+
+ bus_num = 1
+ for controller in controllers:
+ if controller.controller_class == USBControllerTypes.XHCI:
+ tree_data = self.usb_tree.get(bus_num, {})
+ driver = tree_data.get('driver', '')
+ port_count_match = re.search(r'/(\d+)p', driver)
+ total_ports = int(port_count_match.group(1)) if port_count_match else 10
+
+ hs_ports = []
+ ss_ports = []
+ bus_devices = devices_by_bus.get(bus_num, [])
+
+ for port_num in range(1, min(total_ports + 1, 16)):
+ # Find device on this port (not through a hub)
+ port_device = None
+ for dev in bus_devices:
+ if dev.get('port') == port_num and 'hub_port' not in dev:
+ port_device = dev
+ break
+
+ hs_port = USBPort(
+ index=port_num,
+ name=f"HS{port_num:02d}",
+ port_class=USBDeviceSpeeds.HighSpeed,
+ )
+
+ ss_port = USBPort(
+ index=port_num,
+ name=f"SS{port_num:02d}",
+ port_class=USBDeviceSpeeds.SuperSpeed,
+ companion_port=port_num,
+ )
+
+ if port_device:
+ device_speed = self._speed_to_class(port_device.get('speed', ''))
+ bus = port_device.get('bus', 0)
+ devnum = int(port_device.get('devnum', 0))
+ lsusb_info = lsusb_by_bus_dev.get((bus, devnum), {})
+
+ device_name = (
+ port_device.get('product') or
+ lsusb_info.get('name') or
+ 'Unknown Device'
+ )
+
+ vendor_id = port_device.get('idVendor', lsusb_info.get('vendor_id', ''))
+ product_id = port_device.get('idProduct', lsusb_info.get('product_id', ''))
+
+ usb_device = USBDevice(
+ name=device_name,
+ vendor_id=vendor_id,
+ product_id=product_id,
+ bus=bus,
+ device=devnum,
+ port=port_num,
+ speed=port_device.get('speed', ''),
+ speed_class=device_speed,
+ instance_id=f"{bus}-{port_num}",
+ )
+
+ if device_speed in (USBDeviceSpeeds.SuperSpeed, USBDeviceSpeeds.SuperSpeedPlus):
+ ss_port.devices.append(usb_device)
+ else:
+ hs_port.devices.append(usb_device)
+
+ if self._is_internal_device(vendor_id, product_id):
+ if device_speed >= USBDeviceSpeeds.SuperSpeed:
+ ss_port.is_internal = True
+ ss_port.guessed_type = USBPhysicalPortTypes.Internal
+ else:
+ hs_port.is_internal = True
+ hs_port.guessed_type = USBPhysicalPortTypes.Internal
+
+ hs_ports.append(hs_port)
+ if port_num <= total_ports // 2 + 1:
+ ss_ports.append(ss_port)
+
+ controller.ports = hs_ports + ss_ports
+ controller.hub_name = f"usb{bus_num}"
+
+ elif controller.controller_class == USBControllerTypes.EHCI:
+ for port_num in range(1, 9):
+ port = USBPort(
+ index=port_num,
+ name=f"HP{port_num:02d}",
+ port_class=USBDeviceSpeeds.HighSpeed,
+ )
+ controller.ports.append(port)
+ controller.hub_name = f"usb{bus_num}"
+
+ bus_num += 1
+
+
+# Blueprint Generation
+
+def generate_blueprint(usb_map: LinuxUSBMap) -> Dict:
+ """Generate a USB mapping blueprint in JSON format."""
+ blueprint = {
+ "version": "1.0",
+ "tool": "USBToolBox-Linux",
+ "generated_on": "linux",
+ "generated_date": datetime.now().isoformat(),
+ "controllers": [],
+ "internal_devices": [],
+ "warnings": [
+ "Port connector types are GUESSES - verify in macOS",
+ "Companion port mapping may need adjustment",
+ "This is a PREPARATION blueprint, not a final mapping",
+ "Final USBPorts.kext must be created in macOS",
+ ],
+ }
+
+ internal_devices = []
+
+ for controller in (usb_map.controllers or []):
+ ctrl_blueprint = {
+ "name": controller.get('name', 'Unknown'),
+ "pci_id": controller.get('identifiers', {}).get('pci_id', []),
+ "acpi_path": controller.get('identifiers', {}).get('acpi_path', ''),
+ "type": controller.get('class', 'Unknown'),
+ "ports": [],
+ }
+
+ for port in controller.get('ports', []):
+ port_name = port.get('name', f"Port{port.get('index', 0)}")
+ is_ss = port_name.startswith('SS')
+
+ if port.get('guessed') == 'Internal':
+ port_type = 255
+ elif is_ss:
+ port_type = 3
+ else:
+ port_type = 0
+
+ port_blueprint = {
+ "port_name": port_name,
+ "port_number": port.get('index', 0),
+ "port_type": port_type,
+ "port_type_name": _port_type_to_name(port_type),
+ "speed_class": port.get('class', 'Unknown'),
+ "connector_type": "unknown",
+ "is_internal": port.get('guessed') == 'Internal',
+ "companion_port": f"{'HS' if is_ss else 'SS'}{port.get('index', 0):02d}" if port.get('guessed') != 'Internal' else None,
+ "devices": [],
+ }
+
+ for device in port.get('devices', []):
+ device_blueprint = {
+ "name": device.get('name', 'Unknown'),
+ "vendor_id": device.get('vendor_id', ''),
+ "product_id": device.get('product_id', ''),
+ "speed": device.get('speed', ''),
+ }
+ port_blueprint["devices"].append(device_blueprint)
+
+ if port.get('guessed') == 'Internal':
+ internal_devices.append({
+ "name": device.get('name', 'Unknown'),
+ "vendor_id": device.get('vendor_id', ''),
+ "product_id": device.get('product_id', ''),
+ "port_name": port_name,
+ "suggested_port_type": 255,
+ })
+
+ ctrl_blueprint["ports"].append(port_blueprint)
+
+ blueprint["controllers"].append(ctrl_blueprint)
+
+ blueprint["internal_devices"] = internal_devices
+ return blueprint
+
+
+def _port_type_to_name(port_type: int) -> str:
+ """Convert port type code to human-readable name."""
+ type_map = {
+ 0: "USB2 (Type-A)",
+ 3: "USB3 (Type-A)",
+ 8: "Type-C (USB2 only)",
+ 9: "Type-C (with switch)",
+ 10: "Type-C (without switch)",
+ 255: "Internal",
+ }
+ return type_map.get(port_type, "Unknown")
+
+
+# OpenCore Configuration Guidance
+
+def generate_opencore_guidance() -> str:
+ """Generate OpenCore configuration guidance for temporary USB setup."""
+ return """
+================================================================================
+OPENCORE TEMPORARY USB CONFIGURATION
+================================================================================
+
+⚠️ WARNING: These settings are TEMPORARY for initial USB discovery only!
+
+--------------------------------------------------------------------------------
+KERNEL > QUIRKS (config.plist)
+--------------------------------------------------------------------------------
+
+ Kernel
+
+ Quirks
+
+
+ XhciPortLimit
+
+
+
+
+⚠️ CRITICAL NOTES:
+
+1. XhciPortLimit is BROKEN in macOS 11.3+ (Big Sur and later)!
+ - May cause kernel panics or USB instability
+ - Use only if absolutely necessary
+
+2. DO NOT use USBInjectAll.kext - it is deprecated!
+
+3. After creating your final USB map, set XhciPortLimit to FALSE
+
+--------------------------------------------------------------------------------
+REQUIRED KEXTS
+--------------------------------------------------------------------------------
+
+Option A - USBToolBox Method:
+ - USBToolBox.kext (from github.com/USBToolBox/kext)
+ - UTBMap.kext (your custom port map)
+
+Option B - Native Apple Method:
+ - USBPorts.kext (created via Hackintool)
+ - No additional kexts required
+
+================================================================================
+"""
+
+# Display Functions
+
+def print_topology_report(usb_map: LinuxUSBMap):
+ """Print a human-readable USB topology report."""
+ print("\n" + "=" * 70)
+ print("USB TOPOLOGY REPORT")
+ print("=" * 70)
+
+ if not usb_map.controllers:
+ print(f"\n{Colors.RED}No USB controllers detected!{Colors.RESET}")
+ print("Make sure lspci and lsusb are installed.")
+ return
+
+ for controller in usb_map.controllers:
+ ctrl_class = controller.get('class', 'Unknown')
+ name = controller.get('name', 'Unknown Controller')
+ pci_id = controller.get('identifiers', {}).get('pci_id', [])
+ pci_str = ':'.join(pci_id[:2]) if len(pci_id) >= 2 else 'unknown'
+
+ print(f"\n{Colors.BLUE}{'─' * 68}{Colors.RESET}")
+ print(f"{Colors.GREEN}[{ctrl_class}] {name} ({pci_str}){Colors.RESET}")
+ print(f"{Colors.BLUE}{'─' * 68}{Colors.RESET}")
+
+ ports = controller.get('ports', [])
+ hs_ports = [p for p in ports if p.get('name', '').startswith('HS')]
+ ss_ports = [p for p in ports if p.get('name', '').startswith('SS')]
+
+ if hs_ports:
+ print(f"\n {Colors.YELLOW}High-Speed (USB 2.0) Ports:{Colors.RESET}")
+ for port in sorted(hs_ports, key=lambda x: x.get('index', 0)):
+ _print_port(port)
+
+ if ss_ports:
+ print(f"\n {Colors.YELLOW}SuperSpeed (USB 3.0) Ports:{Colors.RESET}")
+ for port in sorted(ss_ports, key=lambda x: x.get('index', 0)):
+ _print_port(port)
+
+ print("\n" + "=" * 70)
+
+
+def _print_port(port: Dict):
+ """Print a single port with its devices."""
+ name = port.get('name', f"Port {port.get('index', '?')}")
+ guessed = port.get('guessed', '')
+ devices = port.get('devices', [])
+
+ status = ""
+ if guessed == 'Internal':
+ status = f"{Colors.BLUE}[INTERNAL]{Colors.RESET}"
+ elif devices:
+ status = f"{Colors.GREEN}[DEVICE]{Colors.RESET}"
+
+ print(f" {name} {status}")
+
+ for device in devices:
+ device_name = device.get('name', 'Unknown Device')
+ vendor = device.get('vendor_id', '????')
+ product = device.get('product_id', '????')
+ print(f" └─ {device_name} [{vendor}:{product}]")
+
+
+# Main Entry Point
+
+def main():
+ """Main entry point for Linux USB mapping tool."""
+ print(f"\n{Colors.BLUE}{'=' * 60}{Colors.RESET}")
+ print(f"{Colors.GREEN} USBToolBox - Linux USB Port Mapper{Colors.RESET}")
+ print(f"{Colors.GREEN} For Hackintosh USB Preparation{Colors.RESET}")
+ print(f"{Colors.BLUE}{'=' * 60}{Colors.RESET}")
+
+ print(f"\n{Colors.YELLOW}⚠️ IMPORTANT LIMITATIONS:{Colors.RESET}")
+ print(" • This tool creates a PREPARATION blueprint only")
+ print(" • Final USB mapping must be completed in macOS")
+ print(" • Port connector types cannot be detected on Linux")
+
+ usb_map = LinuxUSBMap()
+
+ while True:
+ print(f"\n{Colors.BLUE}{'─' * 40}{Colors.RESET}")
+ print(" D. Discover/Refresh USB Ports")
+ print(" S. Show USB Topology Report")
+ print(" B. Generate USB Blueprint (JSON)")
+ print(" O. Show OpenCore Guidance")
+ print(" Q. Quit")
+ print(f"{Colors.BLUE}{'─' * 40}{Colors.RESET}")
+
+ choice = input("\nSelect option: ").strip().upper()
+
+ if choice == 'D':
+ usb_map.get_controllers()
+ print_topology_report(usb_map)
+
+ elif choice == 'S':
+ if not usb_map.controllers:
+ print(f"\n{Colors.YELLOW}No data yet. Running discovery...{Colors.RESET}")
+ usb_map.get_controllers()
+ print_topology_report(usb_map)
+
+ elif choice == 'B':
+ if not usb_map.controllers:
+ print(f"\n{Colors.YELLOW}No data yet. Running discovery...{Colors.RESET}")
+ usb_map.get_controllers()
+
+ blueprint = generate_blueprint(usb_map)
+
+ output_path = Path("usb_blueprint.json")
+ with open(output_path, 'w') as f:
+ json.dump(blueprint, f, indent=2)
+
+ print(f"\n{Colors.GREEN}✓ Blueprint saved to: {output_path.absolute()}{Colors.RESET}")
+ print(f"\n{Colors.YELLOW}Summary:{Colors.RESET}")
+ print(f" Controllers: {len(blueprint['controllers'])}")
+ total_ports = sum(len(c['ports']) for c in blueprint['controllers'])
+ print(f" Total Ports: {total_ports}")
+ print(f" Internal Devices: {len(blueprint['internal_devices'])}")
+
+ elif choice == 'O':
+ print(generate_opencore_guidance())
+
+ elif choice == 'Q':
+ print(f"\n{Colors.GREEN}Goodbye!{Colors.RESET}\n")
+ break
+
+ else:
+ print(f"\n{Colors.RED}Invalid option.{Colors.RESET}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/README.md b/README.md
index 77625c3..1905c78 100644
--- a/README.md
+++ b/README.md
@@ -2,15 +2,16 @@
*Making USB mapping simple(r)*
-The USBToolBox tool is a USB mapping tool supporting Windows and macOS. It allows for building a custom injector kext from Windows and macOS.
+The USBToolBox tool is a USB mapping tool supporting Windows, macOS, and Linux. It allows for building a custom injector kext from Windows and macOS, with Linux support for USB topology preparation.
## Features
-* Supports mapping from Windows and macOS
+* Supports mapping from Windows, macOS, and Linux (preparation mode)
* Can build a map using either the USBToolBox kext or native Apple kexts (AppleUSBHostMergeProperties)
* Supports multiple ways of matching
* Supports companion ports (on Windows)
* Make educated guesses for port types (on Windows)
+* Generate USB blueprints for macOS import (on Linux)
## Supported Methods
@@ -30,6 +31,38 @@ macOS is *not* recommended for several reasons. You won't have features like gue
If you still want to use USBToolBox on macOS, download `macOS.zip` from releases.
+### From Linux (Preparation Mode)
+
+Linux support allows you to **prepare** your USB mapping before installing macOS. This is useful for:
+
+* Documenting your USB topology before macOS installation
+* Identifying internal devices (Bluetooth, webcam, fingerprint readers)
+* Generating a blueprint to import into macOS later
+
+**Requirements:** Python 3.6+, `lspci` (pciutils), `lsusb` (usbutils)
+
+**Usage:**
+
+```bash
+python3 Linux.py
+```
+
+**Features:**
+
+* Detects XHCI/EHCI controllers via lspci
+* Enumerates USB devices and port topology
+* Generates JSON blueprint for macOS import
+* Provides OpenCore configuration guidance
+
+**Limitations:**
+
+* ⚠️ **Preparation only** - Cannot build final kext (must be done in macOS)
+* Cannot detect physical connector types (Type-A vs Type-C)
+* No live plug/unplug detection (snapshot-based)
+* Devices through hubs shown separately
+
+After generating your blueprint, see `docs/linux_to_macos_handoff.md` for instructions on finalizing your USB map in macOS using Hackintool.
+
## Usage
This is gonna be a very basic guide for now. A fully-fleshed guide will be released in the future.