Created
September 8, 2025 11:45
-
-
Save romen/8b8c532410dc6947aafdd7a68773c0b4 to your computer and use it in GitHub Desktop.
A script to reset USB controllers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# | |
# USB xHCI controller reset helper | |
# - Unbinds/rebinds the controller from/to xhci_hcd | |
# - Colors + Nerd Font icons chosen automatically by log level | |
# - --interactive: pick a controller from a list (uses fzf if available) | |
# | |
# Usage: | |
# sudo reset-usb.sh [--interactive] [CONTROLLER_ID] | |
# sudo reset-usb.sh --help | |
# | |
set -euo pipefail | |
# --- Config (defaults) --- | |
DRIVER="xhci_hcd" | |
CONTROLLER_DEFAULT="0000:00:14.0" | |
SLEEP_SECONDS=2 | |
DRIVER_DIR="/sys/bus/pci/drivers/${DRIVER}" | |
UNBIND_PATH="${DRIVER_DIR}/unbind" | |
BIND_PATH="${DRIVER_DIR}/bind" | |
# --- Colors (respect NO_COLOR and only if stderr is a TTY) --- | |
if [[ -t 2 && -z "${NO_COLOR:-}" ]]; then | |
COLOR_RED=$'\e[31m' | |
COLOR_GREEN=$'\e[32m' | |
COLOR_YELLOW=$'\e[33m' | |
COLOR_BLUE=$'\e[34m' | |
COLOR_DIM=$'\e[2m' | |
COLOR_RESET=$'\e[0m' | |
else | |
COLOR_RED=""; COLOR_GREEN=""; COLOR_YELLOW=""; COLOR_BLUE=""; COLOR_DIM=""; COLOR_RESET="" | |
fi | |
# --- Nerd Font Icons (fallback if not supported) --- | |
ICON_INFO="" | |
ICON_OK="" | |
ICON_WARN="" | |
ICON_ERR="" | |
ICON_USB="" | |
# crude fallback if glyphs won’t render | |
if ! printf "%b" "$ICON_OK" | LC_ALL=C grep -q . 2>/dev/null; then | |
ICON_INFO="i"; ICON_OK="OK"; ICON_WARN="!"; ICON_ERR="X"; ICON_USB="USB" | |
fi | |
# --- Helpers to map level -> color/icon --- | |
_color_for_level() { | |
case "${1^^}" in | |
SUCCESS) printf "%s" "$COLOR_GREEN" ;; | |
INFO) printf "%s" "$COLOR_BLUE" ;; | |
WARN) printf "%s" "$COLOR_YELLOW";; | |
ERROR) printf "%s" "$COLOR_RED" ;; | |
*) printf "%s" "$COLOR_DIM" ;; | |
esac | |
} | |
_icon_for_level() { | |
case "${1^^}" in | |
SUCCESS) printf "%s" "$ICON_OK" ;; | |
INFO) printf "%s" "$ICON_INFO" ;; | |
WARN) printf "%s" "$ICON_WARN" ;; | |
ERROR) printf "%s" "$ICON_ERR" ;; | |
*) printf "%s" "$ICON_INFO" ;; | |
esac | |
} | |
# --- Logging --- | |
log() { # usage: log LEVEL "message..." | |
local level="${1:-INFO}"; shift || true | |
local msg="${*:-}" | |
local color; color="$(_color_for_level "$level")" | |
local icon; icon="$(_icon_for_level "$level")" | |
# Timestamp dimmed, then restore the level color for the message | |
printf "%b%s %b[%s]%b %b%s%b\n" \ | |
"$color" "$icon" \ | |
"$COLOR_DIM" "$(date '+%H:%M:%S')" "$COLOR_RESET" \ | |
"$color" "$msg" "$COLOR_RESET" >&2 | |
} | |
die() { | |
log ERROR "$*" | |
exit 1 | |
} | |
usage() { | |
cat >&2 <<EOF | |
Reset an xHCI USB controller by unbinding/rebinding to ${DRIVER}. | |
${COLOR_DIM}Usage:${COLOR_RESET} | |
sudo ${0##*/} [--interactive] [CONTROLLER_ID] | |
sudo ${0##*/} --help | |
${COLOR_DIM}Examples:${COLOR_RESET} | |
sudo ${0##*/} # uses default ${CONTROLLER_DEFAULT} | |
sudo ${0##*/} 0000:03:00.0 # explicit controller | |
sudo ${0##*/} --interactive # pick from detected controllers | |
${COLOR_DIM}Notes:${COLOR_RESET} | |
- Must be run as root (or via sudo). | |
- You can whitelist this script in sudoers with NOPASSWD for convenience. | |
EOF | |
} | |
require_root() { | |
[[ "${EUID}" -eq 0 ]] || die "Please run as root (sudo)." | |
} | |
check_paths() { | |
[[ -w "$UNBIND_PATH" && -w "$BIND_PATH" ]] || die "Cannot access ${UNBIND_PATH} or ${BIND_PATH}. Is ${DRIVER} loaded?" | |
} | |
list_bound_controllers() { | |
shopt -s nullglob | |
local d | |
for d in "${DRIVER_DIR}"/0000:*; do | |
[[ -e "$d" ]] && basename "$d" | |
done | |
shopt -u nullglob | |
} | |
pick_controller_interactive() { | |
mapfile -t items < <(list_bound_controllers) | |
((${#items[@]})) || die "No controllers bound to ${DRIVER} were found." | |
if command -v fzf >/dev/null 2>&1; then | |
printf "%s\n" "${items[@]}" | fzf --prompt="Select controller > " --height=40% --reverse --border || true | |
else | |
log INFO "fzf not found, using bash menu." | |
local PS3="Select controller (1-${#items[@]} or 'q' to quit): " | |
local choice | |
select choice in "${items[@]}"; do | |
[[ -n "${choice:-}" ]] && { echo "$choice"; break; } | |
[[ "$REPLY" == "q" ]] && return 1 | |
echo "Invalid choice." >&2 | |
done | |
fi | |
} | |
reset_controller() { | |
local ctrl="$1" | |
log WARN "Unbinding ${ICON_USB} controller ${ctrl} from driver ${DRIVER}..." | |
echo "$ctrl" >"$UNBIND_PATH" || die "Failed to unbind ${ctrl}." | |
sleep "$SLEEP_SECONDS" | |
log INFO "Rebinding ${ICON_USB} controller ${ctrl} to driver ${DRIVER}..." | |
echo "$ctrl" >"$BIND_PATH" || die "Failed to bind ${ctrl}." | |
log SUCCESS "Controller ${ctrl} reset complete." | |
} | |
# --- Argument parsing --- | |
INTERACTIVE=false | |
CONTROLLER_ARG="" | |
while (( "$#" )); do | |
case "$1" in | |
-i|--interactive) INTERACTIVE=true ;; | |
-h|--help) usage; exit 0 ;; | |
--) shift; break ;; | |
-*) | |
die "Unknown option: $1. Use --help." | |
;; | |
*) | |
if [[ -z "$CONTROLLER_ARG" ]]; then | |
CONTROLLER_ARG="$1" | |
else | |
die "Unexpected extra argument: $1" | |
fi | |
;; | |
esac | |
shift | |
done | |
# --- Main --- | |
require_root | |
check_paths | |
if "$INTERACTIVE"; then | |
SELECTED="$(pick_controller_interactive || true)" | |
[[ -n "${SELECTED:-}" ]] || die "No controller selected." | |
CONTROLLER="$SELECTED" | |
else | |
CONTROLLER="${CONTROLLER_ARG:-$CONTROLLER_DEFAULT}" | |
fi | |
# Basic format hint (don't block if it doesn't match) | |
if [[ ! "$CONTROLLER" =~ ^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$ ]]; then | |
log WARN "Controller ID '$CONTROLLER' doesn't look like a PCI address (domain:bus:slot.func). Proceeding anyway..." | |
fi | |
reset_controller "$CONTROLLER" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment