Last active
March 22, 2023 03:32
-
-
Save rmeissn/961441e298e5063fff1ae1eab0d6bc0e to your computer and use it in GitHub Desktop.
Binds/unbinds devices. Used on framework laptops, see https://community.frame.work/t/guide-automatically-disable-usb-devices-for-battery-savings/20392
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 | |
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