diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3096596 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +obj-m += usb_bootmouse.o + +KVER := $(shell uname -r) + +# Most distros expose headers at /lib/modules/$(KVER)/build, but Arch/Manjaro +# often uses /usr/lib/modules/$(KVER)/build. +KDIR ?= $(firstword \ + $(wildcard /lib/modules/$(KVER)/build) \ + $(wildcard /usr/lib/modules/$(KVER)/build)) + +PWD := $(shell pwd) + +OUT := $(PWD)/out + +all: + @if [ -z "$(KDIR)" ]; then \ + echo "ERROR: kernel build dir not found for $(KVER). Install kernel headers (e.g. linux-headers)"; \ + exit 2; \ + fi + mkdir -p $(OUT) + $(MAKE) -C $(KDIR) M=$(PWD) modules + -mv -f -- *.ko *.mod.c *.o *.mod modules.order .*.cmd $(OUT) 2>/dev/null || true + @echo "Build outputs moved to $(OUT)" + +clean: + @if [ -z "$(KDIR)" ]; then \ + echo "ERROR: kernel build dir not found for $(KVER)."; \ + exit 2; \ + fi + $(MAKE) -C $(KDIR) M=$(PWD) clean + @rm -rf $(OUT) diff --git a/README.md b/README.md index 18d6ff0..d5e8064 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,116 @@ Device Driver ============ + +This folder contains a minimal **out-of-tree** Linux kernel module that acts as a USB **boot-protocol mouse** driver. + +It supports: +- Left and right buttons +- Scroll wheel +- Relative cursor motion (X/Y) + +Important notes +--------------- + +- This is an **educational** example. Real USB mice are normally handled by the kernel HID stack (e.g. `usbhid` / `hid-generic`). +- This driver binds to HID **Boot Mouse** interfaces (class=HID, subclass=BOOT, protocol=MOUSE). Many mice work, but not all. +- To use it on a running system you typically must **unbind** the existing driver from that USB interface first. + +Files +----- + +- `usb_bootmouse.c` – kernel module (USB driver + input device) +- `Makefile` – builds against your running kernel headers + +Build +----- + +Install kernel headers/build deps (examples): + +- Debian/Ubuntu: `sudo apt-get install build-essential linux-headers-$(uname -r)` +- Fedora: `sudo dnf install @development-tools kernel-devel-$(uname -r)` + +Then build: + +```bash +cd Device-Driver +make +``` + +Load +---- + +```bash +sudo insmod usb_bootmouse.ko +dmesg | tail -n 50 +``` + +If you want to restrict binding to a specific device: + +```bash +sudo insmod usb_bootmouse.ko match_vendor=0x046d match_product=0xc077 +``` + +(Replace IDs with your mouse vendor/product from `lsusb`.) + +Bind it to your mouse (unbind/bind) +--------------------------------- + +1) Find the USB interface path. + +You can use `dmesg` when plugging the mouse in, or inspect: + +```bash +ls -l /sys/bus/usb/devices/ +``` + +Typical interface names look like `1-2:1.0` (bus-port:config.interface). + +2) Unbind the existing HID driver (commonly `usbhid`) from that interface: + +```bash +DEVIF="1-2:1.0" # <- change this +echo -n "$DEVIF" | sudo tee /sys/bus/usb/drivers/usbhid/unbind +``` + +3) Bind this module to the interface: + +```bash +echo -n "$DEVIF" | sudo tee /sys/bus/usb/drivers/usb_bootmouse/bind +``` + +At this point, the driver should create an input device (via `evdev`). + +Test +---- + +List input devices and find the new one: + +```bash +cat /proc/bus/input/devices +``` + +Or use `evtest`: + +```bash +sudo apt-get install evtest # or your distro equivalent +sudo evtest +``` + +You should see events: +- `BTN_LEFT`, `BTN_RIGHT` +- `REL_X`, `REL_Y` +- `REL_WHEEL` + +Unload +------ + +```bash +sudo rmmod usb_bootmouse +``` + +If you want the original HID driver back, bind it again: + +```bash +echo -n "$DEVIF" | sudo tee /sys/bus/usb/drivers/usbhid/bind +``` diff --git a/bind_usb_bootmouse.sh b/bind_usb_bootmouse.sh new file mode 100755 index 0000000..2a15e8a --- /dev/null +++ b/bind_usb_bootmouse.sh @@ -0,0 +1,111 @@ +#!/bin/bash +# bind_usb_bootmouse.sh +# Usage: sudo ./bind_usb_bootmouse.sh [VID] [PID] +# Example: sudo ./bind_usb_bootmouse.sh 0x2516 0x012f + +set -euo pipefail + +if [ "$EUID" -ne 0 ]; then + echo "This script must be run as root (sudo)." >&2 + exit 1 +fi + +VID_IN=${1:-0x2516} +PID_IN=${2:-0x012f} + +# normalize (strip 0x, lowercase, pad to 4 hex digits) +norm_hex() { + local v=${1,,} + v=${v#0x} + printf "%04x" $((16#$v)) +} + +VID=$(norm_hex "$VID_IN") +PID=$(norm_hex "$PID_IN") + +MAPFILE="/var/run/usb_bootmouse_bind_${VID}_${PID}.map" + +echo "Looking for USB device VID=$VID PID=$PID..." + +found=0 +for d in /sys/bus/usb/devices/*; do + if [ -f "$d/idVendor" ] && [ -f "$d/idProduct" ]; then + v=$(cat "$d/idVendor") + p=$(cat "$d/idProduct") + if [ "$v" = "$VID" ] && [ "$p" = "$PID" ]; then + found=1 + devdir="$d" + break + fi + fi +done + +if [ $found -ne 1 ]; then + echo "Device not found in sysfs for VID=$VID PID=$PID" >&2 + exit 2 +fi + +echo "Device found at $devdir" + +# Ensure our module is loaded +if ! lsmod | grep -q '^usb_bootmouse'; then + if ! modprobe usb_bootmouse 2>/dev/null; then + echo "Attempting to insmod from module file..." + if [ -f "$(pwd)/usb_bootmouse.ko" ]; then + insmod "$(pwd)/usb_bootmouse.ko" + else + echo "usb_bootmouse module not found. Build the module first." >&2 + exit 3 + fi + fi +fi + +echo "Module usb_bootmouse loaded" + +# Prepare map file +rm -f "$MAPFILE" +mkdir -p "$(dirname "$MAPFILE")" + +# Iterate over interfaces under the device (like 1-2:1.0) +for ifpath in "$devdir"/*:*; do + [ -e "$ifpath" ] || continue + IFNAME=$(basename "$ifpath") + + # find the current driver for this interface + orig_driver="" + for drv in /sys/bus/usb/drivers/*; do + [ -e "$drv/$IFNAME" ] || continue + orig_driver=$(basename "$drv") + break + done + + if [ -z "$orig_driver" ]; then + echo "Interface $IFNAME has no driver currently; will try to bind usb_bootmouse directly" + else + echo "Interface $IFNAME currently bound to driver '$orig_driver'" + # record original driver + echo "$IFNAME:$orig_driver" >> "$MAPFILE" + + # unbind original driver + echo -n "$IFNAME" > "/sys/bus/usb/drivers/$orig_driver/unbind" || { + echo "Failed to unbind $IFNAME from $orig_driver" >&2 + continue + } + echo "Unbound $IFNAME from $orig_driver" + fi + + # bind our driver + echo -n "$IFNAME" > /sys/bus/usb/drivers/usb_bootmouse/bind || { + echo "Failed to bind $IFNAME to usb_bootmouse" >&2 + continue + } + echo "Bound $IFNAME to usb_bootmouse" +done + +if [ -f "$MAPFILE" ]; then + echo "Saved mapping to $MAPFILE" +else + echo "No original driver mappings were saved (device may have had no drivers)." +fi + +exit 0 diff --git a/restore_hid.sh b/restore_hid.sh new file mode 100755 index 0000000..fbb73a8 --- /dev/null +++ b/restore_hid.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# restore_hid.sh +# Usage: sudo ./restore_hid.sh [VID] [PID] +# Restores original driver bindings saved by bind_usb_bootmouse.sh + +set -euo pipefail + +if [ "$EUID" -ne 0 ]; then + echo "This script must be run as root (sudo)." >&2 + exit 1 +fi + +VID_IN=${1:-0x2516} +PID_IN=${2:-0x012f} + +norm_hex() { + local v=${1,,} + v=${v#0x} + printf "%04x" $((16#$v)) +} + +VID=$(norm_hex "$VID_IN") +PID=$(norm_hex "$PID_IN") + +MAPFILE="/var/run/usb_bootmouse_bind_${VID}_${PID}.map" + +if [ ! -f "$MAPFILE" ]; then + echo "Mapping file $MAPFILE not found. Nothing to restore." >&2 + exit 2 +fi + +echo "Restoring bindings from $MAPFILE" + +while IFS=: read -r IFNAME ORIG; do + [ -n "$IFNAME" ] || continue + [ -n "$ORIG" ] || continue + + # unbind our driver if it is bound + if [ -e "/sys/bus/usb/drivers/usb_bootmouse/$IFNAME" ]; then + echo -n "$IFNAME" > /sys/bus/usb/drivers/usb_bootmouse/unbind || { + echo "Failed to unbind $IFNAME from usb_bootmouse" >&2 + } + echo "Unbound $IFNAME from usb_bootmouse" + else + echo "usb_bootmouse not bound to $IFNAME" + fi + + # bind the original driver back + if [ -d "/sys/bus/usb/drivers/$ORIG" ]; then + echo -n "$IFNAME" > /sys/bus/usb/drivers/$ORIG/bind || { + echo "Failed to bind $IFNAME to $ORIG" >&2 + continue + } + echo "Bound $IFNAME to $ORIG" + else + echo "Original driver $ORIG not present on this system; skipping bind for $IFNAME" >&2 + fi + +done < "$MAPFILE" + +rm -f "$MAPFILE" + +echo "Restoration complete." +exit 0 diff --git a/usb_bootmouse.c b/usb_bootmouse.c new file mode 100644 index 0000000..9fcc3d9 --- /dev/null +++ b/usb_bootmouse.c @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include +#include +#include +#include +#include +#include + +#define DRV_NAME "usb_bootmouse" + +static ushort match_vendor; +module_param(match_vendor, ushort, 0444); +MODULE_PARM_DESC(match_vendor, "If non-zero, only bind to this USB vendor id (hex or dec)"); + +static ushort match_product; +module_param(match_product, ushort, 0444); +MODULE_PARM_DESC(match_product, "If non-zero, only bind to this USB product id (hex or dec)"); + +/* Runtime tuning parameters for devices with nonstandard report layouts */ +static int force_offset = -1; /* -1 = auto, 0 = no report-id, 1 = report-id present */ +module_param(force_offset, int, 0444); +MODULE_PARM_DESC(force_offset, "Force report-data offset (0 or 1). -1 = autodetect"); + +static bool swap_xy = false; +module_param(swap_xy, bool, 0444); +MODULE_PARM_DESC(swap_xy, "Swap X/Y axes (try true if horizontal/vertical are inverted)"); + +static int wheel_index = -1; /* -1 = autodetect (3+off) */ +module_param(wheel_index, int, 0444); +MODULE_PARM_DESC(wheel_index, "Force wheel byte index in report (0-based). -1 = autodetect"); + +struct bootmouse { + struct usb_device *udev; + struct usb_interface *intf; + struct input_dev *input; + + struct urb *irq_urb; + u8 *irq_data; + dma_addr_t irq_dma; + unsigned int irq_len; + unsigned int irq_interval; + unsigned int irq_ep; + + char phys[64]; + atomic_t opened; +}; + +static void bootmouse_irq(struct urb *urb) +{ + struct bootmouse *m = urb->context; + int status = urb->status; + u8 *data; + s8 dx, dy, wheel; + + if (!m) + return; + + if (!atomic_read(&m->opened)) + return; + + if (status) { + /* + * -ENOENT/-ECONNRESET typically come from disconnect/kill/unlink. + * Avoid noisy logs; just stop resubmitting. + */ + if (status == -ENOENT || status == -ECONNRESET || status == -ESHUTDOWN) + return; + + dev_dbg(&m->intf->dev, "irq urb status %d\n", status); + goto resubmit; + } + + data = m->irq_data; + + /* + * Many HID mice use the Boot protocol layout: + * [0]=buttons [1]=dx [2]=dy [3]=wheel + * Some devices prepend a report-id, shifting fields: + * [0]=rid [1]=buttons [2]=dx [3]=dy ... + * Use a small heuristic to pick the correct offset so axes don't get swapped. + */ + { + int off = 0; + int sA = 0, sB = 0; + /* score no-report-id layout: sum(|dx|+|dy|) at indexes 1,2 */ + if (m->irq_len >= 3) + sA = abs((s8)data[1]) + abs((s8)data[2]); + /* score report-id layout: sum at indexes 2,3 */ + if (m->irq_len >= 4) + sB = abs((s8)data[2]) + abs((s8)data[3]); + /* choose offset=1 if sB noticeably larger than sA */ + if (sB > sA * 2) + off = 1; + + /* buttons */ + input_report_key(m->input, BTN_LEFT, !!(data[0 + off] & 0x01)); + input_report_key(m->input, BTN_RIGHT, !!(data[0 + off] & 0x02)); + + /* motion */ + dx = (s8)data[1 + off]; + dy = (s8)data[2 + off]; + if (swap_xy) { + s8 tmp = dx; dx = dy; dy = tmp; + } + input_report_rel(m->input, REL_X, dx); + input_report_rel(m->input, REL_Y, dy); + + /* wheel if present -- allow override */ + if (wheel_index >= 0) { + if ((unsigned)wheel_index < m->irq_len) { + wheel = (s8)data[wheel_index]; + input_report_rel(m->input, REL_WHEEL, wheel); + } + } else if (m->irq_len >= 4 + off) { + wheel = (s8)data[3 + off]; + input_report_rel(m->input, REL_WHEEL, wheel); + } + } + + input_sync(m->input); + +resubmit: + usb_submit_urb(m->irq_urb, GFP_ATOMIC); +} + +static int bootmouse_open(struct input_dev *dev) +{ + struct bootmouse *m = input_get_drvdata(dev); + int ret; + + if (!m) + return -ENODEV; + + atomic_set(&m->opened, 1); + + ret = usb_submit_urb(m->irq_urb, GFP_KERNEL); + if (ret) { + atomic_set(&m->opened, 0); + return ret; + } + + return 0; +} + +static void bootmouse_close(struct input_dev *dev) +{ + struct bootmouse *m = input_get_drvdata(dev); + + if (!m) + return; + + atomic_set(&m->opened, 0); + usb_kill_urb(m->irq_urb); +} + +static int bootmouse_probe(struct usb_interface *intf, const struct usb_device_id *id) +{ + struct usb_device *udev = interface_to_usbdev(intf); + struct usb_host_interface *alts; + struct usb_endpoint_descriptor *epd = NULL; + struct bootmouse *m; + struct input_dev *input; + int i, error; + + if (match_vendor && le16_to_cpu(udev->descriptor.idVendor) != match_vendor) + return -ENODEV; + if (match_product && le16_to_cpu(udev->descriptor.idProduct) != match_product) + return -ENODEV; + + alts = intf->cur_altsetting; + for (i = 0; i < alts->desc.bNumEndpoints; i++) { + struct usb_endpoint_descriptor *d = &alts->endpoint[i].desc; + + if (usb_endpoint_is_int_in(d)) { + epd = d; + break; + } + } + if (!epd) + return -ENODEV; + + m = kzalloc(sizeof(*m), GFP_KERNEL); + if (!m) + return -ENOMEM; + + m->udev = usb_get_dev(udev); + m->intf = intf; + atomic_set(&m->opened, 0); + + m->irq_ep = usb_endpoint_num(epd); + m->irq_len = usb_endpoint_maxp(epd); + m->irq_interval = epd->bInterval; + + m->irq_urb = usb_alloc_urb(0, GFP_KERNEL); + if (!m->irq_urb) { + error = -ENOMEM; + goto err_free; + } + + m->irq_data = usb_alloc_coherent(udev, m->irq_len, GFP_KERNEL, &m->irq_dma); + if (!m->irq_data) { + error = -ENOMEM; + goto err_free_urb; + } + + input = input_allocate_device(); + if (!input) { + error = -ENOMEM; + goto err_free_buf; + } + + m->input = input; + + usb_make_path(udev, m->phys, sizeof(m->phys)); + strlcat(m->phys, "/input0", sizeof(m->phys)); + + input->name = "USB Boot Mouse (example driver)"; + input->phys = m->phys; + usb_to_input_id(udev, &input->id); + input->dev.parent = &intf->dev; + + input->open = bootmouse_open; + input->close = bootmouse_close; + + __set_bit(EV_KEY, input->evbit); + __set_bit(EV_REL, input->evbit); + + __set_bit(BTN_LEFT, input->keybit); + __set_bit(BTN_RIGHT, input->keybit); + + __set_bit(REL_X, input->relbit); + __set_bit(REL_Y, input->relbit); + __set_bit(REL_WHEEL, input->relbit); + + input_set_drvdata(input, m); + + usb_fill_int_urb( + m->irq_urb, + udev, + usb_rcvintpipe(udev, epd->bEndpointAddress), + m->irq_data, + m->irq_len, + bootmouse_irq, + m, + m->irq_interval); + + m->irq_urb->transfer_dma = m->irq_dma; + m->irq_urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + usb_set_intfdata(intf, m); + + error = input_register_device(input); + if (error) + goto err_clear_intfdata; + + dev_info(&intf->dev, + "bound to %04x:%04x, int-in ep 0x%02x maxp %u interval %u\n", + le16_to_cpu(udev->descriptor.idVendor), + le16_to_cpu(udev->descriptor.idProduct), + epd->bEndpointAddress, + m->irq_len, + m->irq_interval); + + return 0; + +err_clear_intfdata: + usb_set_intfdata(intf, NULL); + input_free_device(input); + m->input = NULL; +err_free_buf: + usb_free_coherent(udev, m->irq_len, m->irq_data, m->irq_dma); +err_free_urb: + usb_free_urb(m->irq_urb); +err_free: + usb_put_dev(m->udev); + kfree(m); + return error; +} + +static void bootmouse_disconnect(struct usb_interface *intf) +{ + struct bootmouse *m = usb_get_intfdata(intf); + + usb_set_intfdata(intf, NULL); + if (!m) + return; + + if (m->input) { + /* Triggers ->close if opened */ + input_unregister_device(m->input); + m->input = NULL; + } + + usb_kill_urb(m->irq_urb); + usb_free_coherent(m->udev, m->irq_len, m->irq_data, m->irq_dma); + usb_free_urb(m->irq_urb); + usb_put_dev(m->udev); + kfree(m); + + dev_info(&intf->dev, "disconnected\n"); +} + +/* Match HID Boot Mouse interfaces. */ +static const struct usb_device_id bootmouse_id_table[] = { + /* HID Boot Mouse: bInterfaceClass = 3 (HID), bInterfaceSubClass = 1 (BOOT), bInterfaceProtocol = 2 (MOUSE) */ + { USB_INTERFACE_INFO(3, 1, 2) }, + { } +}; +MODULE_DEVICE_TABLE(usb, bootmouse_id_table); + +static struct usb_driver bootmouse_driver = { + .name = DRV_NAME, + .probe = bootmouse_probe, + .disconnect = bootmouse_disconnect, + .id_table = bootmouse_id_table, +}; + +module_usb_driver(bootmouse_driver); + +MODULE_AUTHOR("Example / educational"); +MODULE_DESCRIPTION("Minimal USB HID boot-protocol mouse driver (buttons, motion, wheel)"); +MODULE_LICENSE("GPL"); diff --git a/usb_driver_manager.py b/usb_driver_manager.py new file mode 100755 index 0000000..a1911f6 --- /dev/null +++ b/usb_driver_manager.py @@ -0,0 +1,610 @@ +#!/usr/bin/env python3 +""" +USB Driver Manager - CLI tool for managing USB device driver bindings +Helps with finding USB devices, unloading current drivers, loading new drivers, +and binding USB devices to new drivers. + +Note: USB drivers typically bind to interfaces, not devices. This script +handles both device-level and interface-level driver binding. +""" + +import os +import sys +import subprocess +import glob +import re +import time +from pathlib import Path + + +class Colors: + """ANSI color codes for terminal output""" + HEADER = '\033[95m' + BLUE = '\033[94m' + CYAN = '\033[96m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + END = '\033[0m' + BOLD = '\033[1m' + + +def print_header(text): + """Print colored header""" + print(f"\n{Colors.BOLD}{Colors.HEADER}{text}{Colors.END}") + + +def print_success(text): + """Print success message""" + print(f"{Colors.GREEN}✓ {text}{Colors.END}") + + +def print_error(text): + """Print error message""" + print(f"{Colors.RED}✗ {text}{Colors.END}") + + +def print_warning(text): + """Print warning message""" + print(f"{Colors.YELLOW}⚠ {text}{Colors.END}") + + +def check_root(): + """Check if script is running with root privileges""" + if os.geteuid() != 0: + print_error("This tool requires root privileges.") + print("Please run with sudo:") + print(f" sudo {' '.join(sys.argv)}") + sys.exit(1) + + +def get_usb_devices(): + """Get list of USB devices with their information""" + devices = [] + usb_devices_path = Path("/sys/bus/usb/devices") + + if not usb_devices_path.exists(): + print_error("USB devices path not found. Is USB subsystem available?") + return devices + + # Get lsusb output for human-readable names + lsusb_output = {} + try: + result = subprocess.run(['lsusb'], capture_output=True, text=True) + for line in result.stdout.splitlines(): + # Format: Bus 001 Device 005: ID 046d:c52b Logitech, Inc. Unifying Receiver + match = re.match(r'Bus (\d+) Device (\d+): ID ([0-9a-f]{4}):([0-9a-f]{4})\s+(.*)', line) + if match: + bus, dev, vendor, product, name = match.groups() + key = f"{int(bus)}-{int(dev)}" + lsusb_output[key] = { + 'vendor_id': vendor, + 'product_id': product, + 'name': name.strip() + } + except Exception as e: + print_warning(f"Could not run lsusb: {e}") + + # Iterate through USB devices + for device_path in usb_devices_path.iterdir(): + if not device_path.is_dir(): + continue + + # Only process actual device entries (format: busnum-devnum or busnum-port.port...) + device_name = device_path.name + if not re.match(r'\d+-[\d.]+', device_name): + continue + + # Skip root hubs + devpath_file = device_path / "devpath" + if not devpath_file.exists(): + continue + + try: + # Read device information + vendor_id = (device_path / "idVendor").read_text().strip() if (device_path / "idVendor").exists() else "unknown" + product_id = (device_path / "idProduct").read_text().strip() if (device_path / "idProduct").exists() else "unknown" + manufacturer = (device_path / "manufacturer").read_text().strip() if (device_path / "manufacturer").exists() else "Unknown" + product = (device_path / "product").read_text().strip() if (device_path / "product").exists() else "Unknown" + busnum = (device_path / "busnum").read_text().strip() if (device_path / "busnum").exists() else "?" + devnum = (device_path / "devnum").read_text().strip() if (device_path / "devnum").exists() else "?" + + # Get current driver + driver = "none" + driver_link = device_path / "driver" + if driver_link.exists() and driver_link.is_symlink(): + driver = driver_link.resolve().name + + # Try to get better name from lsusb + lsusb_key = f"{int(busnum)}-{int(devnum)}" + if lsusb_key in lsusb_output: + product = lsusb_output[lsusb_key]['name'] + + # Check if it's an input device by looking at device class or interfaces + is_input = False + device_class = (device_path / "bDeviceClass").read_text().strip() if (device_path / "bDeviceClass").exists() else "00" + + # Class 03 is HID (Human Interface Device) + if device_class == "03": + is_input = True + + # Check interfaces for HID class and collect interface information + interfaces = [] + for interface_path in device_path.glob("*:*.*"): + if not interface_path.is_dir(): + continue + try: + iface_class = (interface_path / "bInterfaceClass").read_text().strip() if (interface_path / "bInterfaceClass").exists() else "00" + iface_subclass = (interface_path / "bInterfaceSubClass").read_text().strip() if (interface_path / "bInterfaceSubClass").exists() else "00" + iface_protocol = (interface_path / "bInterfaceProtocol").read_text().strip() if (interface_path / "bInterfaceProtocol").exists() else "00" + + # Get interface driver + iface_driver = "none" + iface_driver_link = interface_path / "driver" + if iface_driver_link.exists() and iface_driver_link.is_symlink(): + iface_driver = iface_driver_link.resolve().name + + interfaces.append({ + 'name': interface_path.name, + 'path': str(interface_path), + 'class': iface_class, + 'subclass': iface_subclass, + 'protocol': iface_protocol, + 'driver': iface_driver + }) + + # Class 03 is HID (Human Interface Device) + if iface_class == "03": + is_input = True + except Exception: + continue + + devices.append({ + 'path': str(device_path), + 'name': device_name, + 'vendor_id': vendor_id, + 'product_id': product_id, + 'manufacturer': manufacturer, + 'product': product, + 'bus': busnum, + 'device': devnum, + 'driver': driver, + 'is_input': is_input, + 'interfaces': interfaces, + 'display_name': f"{manufacturer} {product}" + }) + + except Exception as e: + # Skip devices that can't be read + continue + + return devices + + +def display_usb_devices(devices, filter_input=True): + """Display USB devices in a formatted list""" + if filter_input: + devices = [d for d in devices if d['is_input']] + + if not devices: + print_warning("No USB devices found.") + return None + + print_header("Available USB Devices:") + print(f"\n{'#':<4} {'Device':<15} {'Vendor:Product':<15} {'Name':<40} {'Interfaces':<15}") + print("-" * 100) + + for idx, device in enumerate(devices, 1): + vendor_product = f"{device['vendor_id']}:{device['product_id']}" + + display_name = device['display_name'] + if len(display_name) > 40: + display_name = display_name[:37] + "..." + + iface_info = f"{len(device.get('interfaces', []))} interface(s)" + + print(f"{idx:<4} {device['name']:<15} {vendor_product:<15} {display_name:<40} {iface_info}") + + return devices + + +def get_kernel_modules(directory="."): + """Get list of available kernel modules (.ko files)""" + modules = [] + + # Search for .ko files in the specified directory + for ko_file in glob.glob(os.path.join(directory, "*.ko")): + module_name = os.path.basename(ko_file) + module_path = os.path.abspath(ko_file) + + # Get module info if possible + try: + result = subprocess.run(['modinfo', module_path], + capture_output=True, text=True) + description = "No description" + for line in result.stdout.splitlines(): + if line.startswith("description:"): + description = line.split(":", 1)[1].strip() + break + + modules.append({ + 'name': module_name, + 'path': module_path, + 'description': description + }) + except Exception: + modules.append({ + 'name': module_name, + 'path': module_path, + 'description': "No description available" + }) + + return modules + + +def display_kernel_modules(modules): + """Display available kernel modules""" + if not modules: + print_warning("No kernel modules (.ko files) found in current directory.") + return None + + print_header("Available Kernel Modules:") + print(f"\n{'#':<4} {'Module Name':<30} {'Description':<50}") + print("-" * 90) + + for idx, module in enumerate(modules, 1): + desc = module['description'] + if len(desc) > 50: + desc = desc[:47] + "..." + print(f"{idx:<4} {module['name']:<30} {desc}") + + return modules + + +def get_user_choice(prompt, max_choice): + """Get user input for selection""" + while True: + try: + choice = input(f"\n{prompt} (1-{max_choice}, or 'q' to quit): ").strip() + if choice.lower() == 'q': + return None + choice = int(choice) + if 1 <= choice <= max_choice: + return choice - 1 # Return 0-indexed + else: + print_error(f"Please enter a number between 1 and {max_choice}") + except ValueError: + print_error("Invalid input. Please enter a number.") + except KeyboardInterrupt: + print("\n") + return None + + +def unbind_device(device, interface=None): + """Unbind device interface from current driver""" + # If specific interface provided, unbind that interface + if interface: + if interface['driver'] == "none": + print_warning(f"Interface {interface['name']} is not bound to any driver.") + return True + + driver_path = Path(f"/sys/bus/usb/drivers/{interface['driver']}") + unbind_path = driver_path / "unbind" + + if not unbind_path.exists(): + print_error(f"Cannot unbind: {unbind_path} not found") + return False + + try: + print(f"Unbinding interface {interface['name']} from driver {interface['driver']}...") + unbind_path.write_text(interface['name']) + print_success(f"Successfully unbound from {interface['driver']}") + return True + except Exception as e: + print_error(f"Failed to unbind interface: {e}") + return False + + # Otherwise unbind all interfaces + success = True + for iface in device.get('interfaces', []): + if iface['driver'] != "none": + if not unbind_device(device, iface): + success = False + + if not device.get('interfaces'): + # Fallback to old behavior for device-level driver + if device['driver'] == "none": + print_warning("Device is not bound to any driver.") + return True + + driver_path = Path(f"/sys/bus/usb/drivers/{device['driver']}") + unbind_path = driver_path / "unbind" + + if not unbind_path.exists(): + print_error(f"Cannot unbind: {unbind_path} not found") + return False + + try: + print(f"Unbinding device {device['name']} from driver {device['driver']}...") + unbind_path.write_text(device['name']) + print_success(f"Successfully unbound from {device['driver']}") + return True + except Exception as e: + print_error(f"Failed to unbind device: {e}") + return False + + return success + + +def load_module(module): + """Load kernel module""" + try: + print(f"Loading module {module['name']}...") + result = subprocess.run(['insmod', module['path']], + capture_output=True, text=True) + if result.returncode == 0: + print_success(f"Successfully loaded {module['name']}") + return True + else: + # Module might already be loaded + if "File exists" in result.stderr or "already" in result.stderr.lower(): + print_warning(f"Module {module['name']} is already loaded") + return True + print_error(f"Failed to load module: {result.stderr}") + return False + except Exception as e: + print_error(f"Failed to load module: {e}") + return False + + +def get_module_driver_name(module): + """Get the driver name from module""" + # Extract driver name from module (remove .ko extension) + driver_name = os.path.splitext(module['name'])[0] + + # Check if driver exists in /sys/bus/usb/drivers/ + driver_path = Path(f"/sys/bus/usb/drivers/{driver_name}") + if driver_path.exists(): + return driver_name + + # Try to get it from loaded modules + try: + result = subprocess.run(['lsmod'], capture_output=True, text=True) + for line in result.stdout.splitlines(): + if line.startswith(driver_name): + return driver_name + except Exception: + pass + + return driver_name + + +def bind_device(device, module, interface=None): + """Bind device interface to new driver""" + driver_name = get_module_driver_name(module) + driver_path = Path(f"/sys/bus/usb/drivers/{driver_name}") + bind_path = driver_path / "bind" + + if not driver_path.exists(): + print_error(f"Driver path not found: {driver_path}") + print_warning("The driver might not be loaded or might use a different name.") + return False + + if not bind_path.exists(): + print_error(f"Bind interface not found: {bind_path}") + return False + + # If specific interface provided, bind that interface + if interface: + # Check if already bound to target driver + iface_path = Path(interface['path']) + driver_link = iface_path / "driver" + if driver_link.exists() and driver_link.is_symlink(): + current_driver = driver_link.resolve().name + if current_driver == driver_name: + print_success(f"Interface {interface['name']} already bound to {driver_name}") + return True + + try: + print(f"Binding interface {interface['name']} to driver {driver_name}...") + bind_path.write_text(interface['name']) + print_success(f"Successfully bound to {driver_name}") + return True + except Exception as e: + # Check again if it got bound (might be EBUSY because it auto-bound) + if driver_link.exists() and driver_link.is_symlink(): + current_driver = driver_link.resolve().name + if current_driver == driver_name: + print_success(f"Interface {interface['name']} bound to {driver_name} (auto-probed)") + return True + print_error(f"Failed to bind interface: {e}") + return False + + # Otherwise try to bind all HID mouse interfaces (class 03, subclass 01, protocol 02) + target_interfaces = [] + for iface in device.get('interfaces', []): + # Look for HID Boot Mouse interfaces + if iface['class'] == "03" and iface['subclass'] == "01" and iface['protocol'] == "02": + target_interfaces.append(iface) + + if not target_interfaces: + # Fallback: try to bind the device itself + try: + print(f"Binding device {device['name']} to driver {driver_name}...") + bind_path.write_text(device['name']) + print_success(f"Successfully bound to {driver_name}") + return True + except Exception as e: + print_error(f"Failed to bind device: {e}") + return False + + # Bind each target interface + success = True + for iface in target_interfaces: + # Check if already bound to target driver + iface_path = Path(iface['path']) + driver_link = iface_path / "driver" + if driver_link.exists() and driver_link.is_symlink(): + current_driver = driver_link.resolve().name + if current_driver == driver_name: + print_success(f"Interface {iface['name']} already bound to {driver_name}") + continue + + try: + print(f"Binding interface {iface['name']} to driver {driver_name}...") + bind_path.write_text(iface['name']) + print_success(f"Successfully bound interface {iface['name']} to {driver_name}") + except Exception as e: + # Check again if it got bound (might be EBUSY because it auto-bound) + if driver_link.exists() and driver_link.is_symlink(): + current_driver = driver_link.resolve().name + if current_driver == driver_name: + print_success(f"Interface {iface['name']} bound to {driver_name} (auto-probed)") + continue + print_error(f"Failed to bind interface {iface['name']}: {e}") + success = False + + return success + + +def unload_module(module): + """Unload kernel module""" + driver_name = get_module_driver_name(module) + try: + print(f"Unloading module {driver_name}...") + result = subprocess.run(['rmmod', driver_name], + capture_output=True, text=True) + if result.returncode == 0: + print_success(f"Successfully unloaded {driver_name}") + return True + else: + print_warning(f"Could not unload module: {result.stderr}") + return False + except Exception as e: + print_warning(f"Could not unload module: {e}") + return False + + +def main(): + """Main function""" + print(f"{Colors.BOLD}{Colors.CYAN}") + print("=" * 60) + print(" USB Driver Manager") + print("=" * 60) + print(Colors.END) + + # Check root privileges + check_root() + + # Step 1: List USB devices + devices = get_usb_devices() + displayed_devices = display_usb_devices(devices, filter_input=True) + + if not displayed_devices: + print_error("No input devices found. Showing all USB devices...") + displayed_devices = display_usb_devices(devices, filter_input=False) + if not displayed_devices: + sys.exit(1) + + # Step 2: Select device + device_idx = get_user_choice("Select USB device", len(displayed_devices)) + if device_idx is None: + print("\nOperation cancelled.") + sys.exit(0) + + selected_device = displayed_devices[device_idx] + print(f"\n{Colors.CYAN}Selected device: {selected_device['display_name']}{Colors.END}") + print(f" Device: {selected_device['name']}") + print(f" Current driver: {selected_device['driver']}") + + # Show interfaces + if selected_device.get('interfaces'): + print(f"\n Interfaces:") + for iface in selected_device['interfaces']: + driver_color = Colors.GREEN if iface['driver'] != "none" else Colors.YELLOW + print(f" {iface['name']}: class={iface['class']} subclass={iface['subclass']} protocol={iface['protocol']} driver={driver_color}{iface['driver']}{Colors.END}") + + # Step 3: List available kernel modules + modules = get_kernel_modules() + displayed_modules = display_kernel_modules(modules) + + if not displayed_modules: + sys.exit(1) + + # Step 4: Select module + module_idx = get_user_choice("Select kernel module", len(displayed_modules)) + if module_idx is None: + print("\nOperation cancelled.") + sys.exit(0) + + selected_module = displayed_modules[module_idx] + print(f"\n{Colors.CYAN}Selected module: {selected_module['name']}{Colors.END}") + + # Step 5: Confirm operation + print(f"\n{Colors.YELLOW}This will:{Colors.END}") + if selected_device.get('interfaces'): + print(f" 1. Unbind interface(s) from current driver(s)") + else: + print(f" 1. Unbind {selected_device['name']} from {selected_device['driver']}") + print(f" 2. Load module {selected_module['name']}") + print(f" 3. Bind interface(s) to the new driver") + + confirm = input(f"\n{Colors.BOLD}Proceed? (yes/no): {Colors.END}").strip().lower() + if confirm not in ['yes', 'y']: + print("\nOperation cancelled.") + sys.exit(0) + + # Step 6: Perform operations + print_header("\nExecuting operations...") + + # Unbind from current driver + if not unbind_device(selected_device): + print_error("Failed to unbind device. Aborting.") + sys.exit(1) + + # Load new module + if not load_module(selected_module): + print_error("Failed to load module. Attempting to restore...") + # Try to rebind to original driver if available + if selected_device['driver'] != "none": + bind_device(selected_device, {'name': selected_device['driver'] + '.ko'}) + sys.exit(1) + + # Give kernel time to auto-probe and bind + print("Waiting for kernel to probe interfaces...") + time.sleep(0.5) + + # Bind to new driver + if not bind_device(selected_device, selected_module): + print_error("Failed to bind device to new driver.") + print_warning("Device may be unbound. You might need to reconnect it or bind manually.") + sys.exit(1) + + # Success + print_header("\nOperation completed successfully!") + print(f"{Colors.GREEN}Device {selected_device['name']} is now using the new driver{Colors.END}") + + # Offer to show new device status + print("\nVerifying device status...") + new_devices = get_usb_devices() + for dev in new_devices: + if dev['name'] == selected_device['name']: + for iface in dev.get('interfaces', []): + driver_color = Colors.GREEN if iface['driver'] != "none" else Colors.YELLOW + print(f" Interface {iface['name']}: {driver_color}{iface['driver']}{Colors.END}") + if not dev.get('interfaces') and dev['driver'] != 'none': + print(f" Current driver: {Colors.GREEN}{dev['driver']}{Colors.END}") + break + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print(f"\n\n{Colors.YELLOW}Operation cancelled by user.{Colors.END}") + sys.exit(0) + except Exception as e: + print_error(f"Unexpected error: {e}") + import traceback + traceback.print_exc() + sys.exit(1)