You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

348 lines
8.1 KiB

#!/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