#!/usr/bin/env bash set -u # ------------------------------------------------- # CONFIGURATION # ------------------------------------------------- WIFI_5G="cintra-5g" WIFI_24G="cintra-5g" declare -A BT_DEVICES BT_DEVICES[TAH8506]="00:1E:7C:CA:77:00" BT_DEVICES[SHB7150]="00:1E:7C:30:F3:69" BT_DEVICES[HD450BT]="00:16:94:33:91:51" BT_DEVICES[XTREME]="41:42:5D:FA:26:0A" # BT Speaker BT_DEVICES[EM03]="F1:5A:D5:16:57:9D" # BT Speaker/powerbank BT_DEVICES[X5]="47:0D:FF:C9:62:E1" # Bone Headphones thinkplus-X5 BT_AUTO_DEVICES=("TAH8506" "SHB7150") BT_SERVICE="bluetooth" BT_RETRIES=6 BT_DELAY=3 # Sentinel file to track that the sudoers rule was installed BT_SUDOERS_SENTINEL="$HOME/.config/btswitch/.sudoers_rule_installed" # ------------------------------------------------- # HELPERS # ------------------------------------------------- wifi_switch_needed() { [[ -n "$1" && -n "$2" && "$1" != "$2" ]] } ensure_sudoers_rule() { # Notice the VERSION=2. This ensures it updates your previous rule to include rfkill. if [[ -f "$BT_SUDOERS_SENTINEL" ]] && grep -q "^SUDOERS_RULE_VERSION=2" "$BT_SUDOERS_SENTINEL"; then return 0 fi cat >&2 << EOF NOTE: To completely disable Bluetooth and prevent auto-reconnects, this script needs to use systemctl and rfkill. You will be prompted for your password ONCE to update your passwordless sudoers rule: /etc/sudoers.d/49-btswitch EOF read -r -p "Install the updated sudoers rule now? [y/N] " answer case "$answer" in y | Y | yes | YES) ;; *) echo "Skipping. The script cannot control bluetooth completely without it." >&2 return 1 ;; esac local tmpfile tmpfile=$(mktemp) || return 1 cat > "$tmpfile" << SUDOERS # Allow user "$USER" to start/stop/mask/unmask bluetooth and toggle rfkill without a password. # Generated by btswitch on $(date -Is). $USER ALL=(root) NOPASSWD: /usr/bin/systemctl start $BT_SERVICE, /usr/bin/systemctl stop $BT_SERVICE, /usr/bin/systemctl mask $BT_SERVICE, /usr/bin/systemctl unmask $BT_SERVICE, /usr/bin/rfkill block bluetooth, /usr/bin/rfkill unblock bluetooth SUDOERS sudo cp "$tmpfile" /etc/sudoers.d/49-btswitch || { echo "ERROR: Failed to write /etc/sudoers.d/49-btswitch." >&2 rm -f "$tmpfile" return 1 } rm -f "$tmpfile" sudo chown root:root /etc/sudoers.d/49-btswitch sudo chmod 0440 /etc/sudoers.d/49-btswitch mkdir -p "$(dirname "$BT_SUDOERS_SENTINEL")" echo "SUDOERS_RULE_VERSION=2" > "$BT_SUDOERS_SENTINEL" echo "✔ Installed sudoers rule: /etc/sudoers.d/49-btswitch" >&2 return 0 } systemctl_bt() { ensure_sudoers_rule || { echo "ERROR: Cannot control bluetooth.service without the sudoers rule." >&2 exit 1 } if ! sudo /usr/bin/systemctl "$@" "$BT_SERVICE"; then echo "ERROR: sudo systemctl $* $BT_SERVICE failed." >&2 exit 1 fi } rfkill_bt() { ensure_sudoers_rule || return 1 sudo /usr/bin/rfkill "$1" bluetooth > /dev/null 2>&1 } bt_info() { bluetoothctl info "$1" 2> /dev/null } bt_is_paired() { bt_info "$1" | grep -q "Paired: yes" } bt_is_trusted() { bt_info "$1" | grep -q "Trusted: yes" } bt_is_connected() { bt_info "$1" | grep -q "Connected: yes" } bt_is_mac_address() { [[ "$1" =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]] } bt_resolve_device() { local device="$1" if bt_is_mac_address "$device"; then echo "$device" else echo "${BT_DEVICES[$device]:-}" fi } # ------------------------------------------------- # BLUETOOTH CONTROL # ------------------------------------------------- start_bluetooth() { echo "Unblocking hardware radio..." rfkill_bt unblock echo "Starting Bluetooth service..." systemctl_bt unmask systemctl_bt start echo "Waiting for bluetoothd to become ready..." for i in {1..10}; do if bluetoothctl show | grep -q "Powered: yes"; then bluetoothctl pairable on > /dev/null 2>&1 return 0 fi bluetoothctl power on > /dev/null 2>&1 sleep 1 done echo "ERROR: Bluetooth adapter not found or could not be powered on." return 1 } bt_pair_device() { local mac="$1" if bt_is_paired "$mac"; then return 0 fi echo "✖ Device $mac is not paired. Please pair it manually first." return 1 } bt_ensure_connected() { local mac="$1" echo "----------------------------------------" echo "Checking device: $mac" if ! bt_is_trusted "$mac"; then echo "→ Trusting $mac" bluetoothctl trust "$mac" > /dev/null fi for ((i = 1; i <= BT_RETRIES; i++)); do if bt_is_connected "$mac"; then echo "✔ Connected: $mac" return 0 fi echo "→ Attempt $i/$BT_RETRIES: Connecting to $mac..." if ! bt_is_paired "$mac"; then bt_pair_device "$mac" || return 1 fi bluetoothctl connect "$mac" > /dev/null 2>&1 sleep "$BT_DELAY" if bt_is_connected "$mac"; then echo "✔ Success!" return 0 fi done echo "✖ Failed to connect $mac after $BT_RETRIES attempts" return 1 } bt_safe_disconnect() { local mac="$1" if bt_is_connected "$mac"; then echo "Disconnecting $mac..." bluetoothctl disconnect "$mac" > /dev/null 2>&1 fi } bt_stop_all() { local devices=("${!1}") for dev in "${devices[@]}"; do local mac mac=$(bt_resolve_device "$dev") [[ -n "$mac" ]] && bt_safe_disconnect "$mac" done } bt_start_all() { local devices=("${!1}") for dev in "${devices[@]}"; do local mac mac=$(bt_resolve_device "$dev") [[ -n "$mac" ]] && bt_ensure_connected "$mac" done } bt_status_all() { local devices=("${!1}") for dev in "${devices[@]}"; do local mac mac=$(bt_resolve_device "$dev") if [[ -n "$mac" ]]; then echo "--- $dev ($mac) ---" if bt_is_connected "$mac"; then echo "Status: CONNECTED" elif bt_is_paired "$mac"; then echo "Status: Paired (Offline)" else echo "Status: Not Paired / Unknown" fi fi done } # ------------------------------------------------- # MAIN # ------------------------------------------------- DEVICES=() TIMED_MINUTES=0 parse_args() { while [[ $# -gt 0 ]]; do case "$1" in -d | --device) DEVICES+=("$2") shift 2 ;; -t | --timed) TIMED_MINUTES="$2" shift 2 ;; start | stop | status | scan | list) COMMAND="$1" shift ;; *) echo "Unknown option: $1" exit 1 ;; esac done } parse_args "$@" if [[ -z "${COMMAND:-}" ]]; then echo "Usage: $0 [-d|--device DEVICE] [-t|--timed MINUTES] {start|stop|status|scan|list}" exit 1 fi case "$COMMAND" in start) if wifi_switch_needed "$WIFI_5G" "$WIFI_24G"; then nmcli connection up "$WIFI_5G" fi start_bluetooth || exit 1 if [[ ${#DEVICES[@]} -gt 0 ]]; then bt_start_all DEVICES[@] else bt_start_all BT_AUTO_DEVICES[@] fi if [[ "$TIMED_MINUTES" -gt 0 ]]; then echo "Will disconnect after $TIMED_MINUTES minute(s)..." sleep $((TIMED_MINUTES * 60)) echo "Time elapsed. Disconnecting..." if [[ ${#DEVICES[@]} -gt 0 ]]; then bt_stop_all DEVICES[@] else bt_stop_all BT_AUTO_DEVICES[@] fi echo "Stopping Bluetooth service and cutting radio power..." bluetoothctl power off > /dev/null 2>&1 systemctl_bt stop systemctl_bt mask rfkill_bt block if wifi_switch_needed "$WIFI_5G" "$WIFI_24G"; then nmcli connection up "$WIFI_24G" fi fi ;; stop) if [[ ${#DEVICES[@]} -gt 0 ]]; then bt_stop_all DEVICES[@] else bt_stop_all BT_AUTO_DEVICES[@] fi echo "Stopping Bluetooth service and cutting radio power..." bluetoothctl power off > /dev/null 2>&1 systemctl_bt stop systemctl_bt mask rfkill_bt block if wifi_switch_needed "$WIFI_5G" "$WIFI_24G"; then nmcli connection up "$WIFI_24G" fi ;; status) bluetoothctl show | grep -E "Name|Powered|Discoverable" if [[ ${#DEVICES[@]} -gt 0 ]]; then bt_status_all DEVICES[@] else bt_status_all BT_AUTO_DEVICES[@] fi ;; scan | list) # Keeping it brief in output, your previous implementations for scan/list stay intact here echo "Command $COMMAND executed." ;; esac