Skip to content

Instantly share code, notes, and snippets.

@rmeissn
Last active March 22, 2023 03:32
Show Gist options
  • Save rmeissn/961441e298e5063fff1ae1eab0d6bc0e to your computer and use it in GitHub Desktop.
Save rmeissn/961441e298e5063fff1ae1eab0d6bc0e to your computer and use it in GitHub Desktop.
#! /usr/bin/env bash
usage() {
echo "
Bind/unbind a USB device from a given vendor:product id
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-d|--dry-run] [--hdmi|microsd|usba|ssd250] [--detect=deviceid:vendorid] [on|off]
Example usage: $(basename "${BASH_SOURCE[0]}") --hdmi off
Example usage: $(basename "${BASH_SOURCE[0]}") --detect=27c6:609c
" >&2
exit 1
}
# hard coded framework modules
# (driver device-id vendor-id) from lsusb and sudo grep -iFl "PRODUCT=DEVICE-ID" /sys/bus/usb/drivers/**/*/uevent
hdmi=(usb 32ac 0002)
microsd=(usb-storage 090c 3350)
usba=(usb 27c6 609c)
ssd250=(usb 13fe 6500)
# Variables used in the script
## Command line arguments
driver_type= # The sub-folder in /sys/bus/usb/drivers/ which is responsible for the usb device
dry_run= # If set to 1, print the message but don't actually bind/unbind
# vendor_id and product_id combine to identify the USB device requested to modify
# Users can use lsusb to figure out the parameters: https://wiki.debian.org/HowToIdentifyADevice/USB
vendor_id=
product_id=
usb_command= # set to either "bind" or "unbind"
## local variables
TEMP= # Used for parsing the command line arguments. Copy/pasta from the getopt template
short_vendor_id= # same as vendor_id, but with leading 0's stripped off
short_product_id= # same as vendor_id, but with leading 0's stripped off
usb_id= # The bus/port/? identifier. The main work of the script is to take the vendor/product id and figure out which usb_id corresponds to this device for this particular boot.
detect=false
device= # helper variable for less code
# Step 1: Parse command line arguments into appropriate variables
TEMP=$(getopt -o 'dh' --longoptions 'dry-run,help,hdmi,microsd,usba,ssd250,detect:' -n "$0" -- "$@")
if [ $? -ne 0 ]; then
usage
fi
eval set -- "$TEMP"
unset TEMP
while true; do
case "$1" in
'-h'|'--help')
usage
shift 1
continue
;;
'-d'|'--dry-run')
dry_run=1
shift 1
continue
;;
'--hdmi')
device=("${hdmi[@]}")
shift 1
continue
;;
'--microsd')
device=("${microsd[@]}")
shift 1
continue
;;
'--usba')
device=("${usba[@]}")
shift 1
continue
;;
'--ssd250')
device=("${ssd250[@]}")
shift 1
continue
;;
'--detect')
tmp=($(echo "$2" | tr ":" " "))
vendor_id="${tmp[0]}"
product_id="${tmp[1]}"
detect=true
shift 2
continue
;;
'--')
shift
break
;;
*)
usage
;;
esac
done
if [ $detect = false ] && [ "${#device[@]}" != 0 ]; then
driver_type="${device[0]}"
vendor_id="${device[1]}"
product_id="${device[2]}"
fi
if [ -z "$vendor_id" ]; then # we only check for vendor_id, as this is only set when a device is specified
echo "missing device" >&2
exit 1
fi
if [ $detect = true ]; then # abort on wrong number of CLI arguments, depending on use-case
if [ "$#" -ne 0 ]; then
usage
fi
else
if [ "$#" -ne 1 ]; then
usage
fi
fi
# Strip leading zeros from the vendor:product id
short_vendor_id=$(printf "%X" "0x${vendor_id}")
short_product_id=$(printf "%X" "0x${product_id}")
# Step 2: Figure out the usb id for the device
# For both bound and unbound devices, the device will have at least one entry in /sys/bus/usb/devices/${usb_id}/uevent
# So, we begin by searching through all these files for the ones matching our USB device
# This will probably match multiple times. E.g. the device might be registered on /usb/2-2 and usb-storage/2-2:1-0
# We want to unbind at the lowest place in the tree, so loop through each match and choose the one with the longest usb_id
# EDIT Changed to use shortest id, as longest one didn't worked for me
while IFS= read -r line; do
path_parts=($(echo "$line" | cut -d "/" --output-delimiter=" " -f 1-))
if [ "${#path_parts[@]}" -lt 3 ]; then
# Usually this happens if you try to bind/unbind/rebind too quickly
continue
fi
new_usb_id="${path_parts[-2]}"
# echo $new_usb_id
if [ "${#new_usb_id}" -gt "${#usb_id}" ]; then
if [[ "$new_usb_id" == *":"* ]]; then # skip long usb-ids
continue
fi
usb_id=${new_usb_id}
if [[ -z $driver_type ]]; then # used to detect the driver of a previously unknown expension card
sys_array=($(echo "$line" | tr "/" " "))
new_driver_type=${sys_array[2]}
echo "Driver type: $new_driver_type"
echo "Add to script (hardcoded framework modules section) as: DEVICE_SHORT=($new_driver_type $vendor_id $product_id)"
echo "Example: usba=(usb 27c6 609c)"
echo "Also remember to add a new CLI argument and CLI argument evaluation"
exit 1
fi
# echo "driver type: $driver_type"
# echo "usb id: $usb_id"
fi
done <<< $(grep -iFl "PRODUCT=${short_vendor_id}/${short_product_id}" /sys/bus/usb/devices/*/uevent)
if [ -z "$usb_id" ]; then
echo "could not calculate the usb-id" >&2
echo "is the specified expansion card mounted?" >&2
exit 1
fi
if [ "$1" == "on" ]; then
usb_command="bind"
elif [ "$1" == "off" ]; then
usb_command="unbind"
else
echo "missing on/off" >&2
exit 1
fi
# Step 3: Check to see if the device has already been bound/unbound and just no-op if there's nothing to do
if [ -d "/sys/bus/usb/drivers/${driver_type}/${usb_id}" ]; then
if [ "$usb_command" == "bind" ]; then
echo "The device is already bound. Nothing to do"
exit 0
fi
elif [ "$usb_command" == "unbind" ]; then
echo "The device is already unbound. Nothing to do"
exit 0
fi
# Step 4; Bind/unbind the USB device by writing the usb_id to the correct bind or unbind file in sys/bus/usb
echo "Calling ${usb_command} for ${driver_type}/${usb_id}"
if [ -z "$dry_run" ]; then
echo -n "$usb_id" | sudo tee /sys/bus/usb/drivers/${driver_type}/${usb_command} > /dev/null
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment