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.
787 lines
21 KiB
787 lines
21 KiB
#!/usr/bin/env bash |
|
set -u |
|
|
|
# ------------------------------------------------- |
|
# CONFIGURATION |
|
# ------------------------------------------------- |
|
WIFI_5G="cintra-5g" |
|
WIFI_24G="cintra-5g" |
|
|
|
BT_CONFIG_DIR="$HOME/.config/btswitch" |
|
BT_CONFIG_FILE="$BT_CONFIG_DIR/devices.conf" |
|
|
|
BT_SERVICE="bluetooth" |
|
BT_RETRIES=6 |
|
BT_DELAY=3 |
|
BT_SCAN_SECONDS=15 |
|
|
|
# Defaults written to devices.conf on first run. Afterwards the config |
|
# file is the single source of truth (manage it with add/remove/auto). |
|
BT_DEFAULT_DEVICES=( |
|
"TAH8506 00:1E:7C:CA:77:00" |
|
"SHB7150 00:1E:7C:30:F3:69" |
|
"HD450BT 00:16:94:33:91:51" |
|
"XTREME 41:42:5D:FA:26:0A" |
|
"EM03 F1:5A:D5:16:57:9D" |
|
"X5 47:0D:FF:C9:62:E1" |
|
) |
|
BT_DEFAULT_AUTO=("TAH8506" "SHB7150") |
|
|
|
# Sentinel file to track that the sudoers rule was installed |
|
BT_SUDOERS_SENTINEL="$BT_CONFIG_DIR/.sudoers_rule_installed" |
|
|
|
declare -A BT_DEVICES |
|
BT_AUTO_DEVICES=() |
|
|
|
DEVICES=() |
|
CMD_ARGS=() |
|
COMMAND="" |
|
TIMED_MINUTES=0 |
|
HARD_MODE=0 |
|
|
|
# ------------------------------------------------- |
|
# HELPERS |
|
# ------------------------------------------------- |
|
|
|
wifi_switch_needed() { |
|
[[ -n "$1" && -n "$2" && "$1" != "$2" ]] |
|
} |
|
|
|
wifi_switch() { |
|
local target="$1" |
|
if ! wifi_switch_needed "$WIFI_5G" "$WIFI_24G"; then |
|
return 0 |
|
fi |
|
echo "Switching Wi-Fi to '$target'..." |
|
nmcli connection up "$target" > /dev/null 2>&1 || echo "⚠ nmcli: could not switch to '$target'" >&2 |
|
} |
|
|
|
ensure_sudoers_rule() { |
|
if [[ -f "$BT_SUDOERS_SENTINEL" ]] && grep -q "^SUDOERS_RULE_VERSION=2" "$BT_SUDOERS_SENTINEL"; then |
|
return 0 |
|
fi |
|
|
|
cat >&2 << EOF |
|
|
|
NOTE: 'stop --hard' (full system-wide shutdown) and 'restore' after it |
|
need rights to control the bluetooth service and rfkill. You will be |
|
prompted for your password ONCE to install a passwordless sudoers rule: |
|
|
|
/etc/sudoers.d/49-btswitch |
|
|
|
Regular start/stop/scan/list do NOT need sudo while Bluetooth is |
|
already running. |
|
|
|
EOF |
|
read -r -p "Install the sudoers rule now? [y/N] " answer |
|
case "$answer" in |
|
y | Y | yes | YES) ;; |
|
*) |
|
echo "Skipping. 'stop --hard' and 'restore' will not work 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 $BT_SERVICE.service without the sudoers rule." >&2 |
|
exit 1 |
|
} |
|
if ! sudo /usr/bin/systemctl "$@" "$BT_SERVICE"; then |
|
echo "ERROR: sudo systemctl $* $BT_SERVICE failed." >&2 |
|
return 1 |
|
fi |
|
} |
|
|
|
rfkill_bt() { |
|
ensure_sudoers_rule || return 1 |
|
sudo /usr/bin/rfkill "$1" bluetooth > /dev/null 2>&1 |
|
} |
|
|
|
bt_start_service() { |
|
ensure_sudoers_rule || return 1 |
|
if sudo /usr/bin/systemctl start "$BT_SERVICE" 2>/dev/null; then |
|
return 0 |
|
fi |
|
sudo /usr/bin/systemctl unmask "$BT_SERVICE" || return 1 |
|
sudo /usr/bin/systemctl start "$BT_SERVICE" |
|
} |
|
|
|
# All bluetoothctl queries go through this: if bluetoothd is not running, |
|
# bluetoothctl would block forever ("Waiting to connect to bluetoothd..."). |
|
bt_info() { |
|
bt_service_active || return 1 |
|
timeout 5 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 |
|
} |
|
|
|
bt_name_for_mac() { |
|
local mac="${1^^}" name |
|
for name in "${!BT_DEVICES[@]}"; do |
|
if [[ "${BT_DEVICES[$name]}" == "$mac" ]]; then |
|
echo "$name" |
|
return 0 |
|
fi |
|
done |
|
echo "$mac" |
|
} |
|
|
|
auto_contains() { |
|
local dev |
|
for dev in "${BT_AUTO_DEVICES[@]}"; do |
|
[[ "$dev" == "$1" ]] && return 0 |
|
done |
|
return 1 |
|
} |
|
|
|
# State checks that do NOT need sudo |
|
bt_rfkill_blocked() { |
|
rfkill list bluetooth 2>/dev/null | grep -qi "blocked: yes" |
|
} |
|
|
|
bt_service_masked() { |
|
[[ "$(systemctl is-enabled "$BT_SERVICE" 2>/dev/null)" == "masked" ]] |
|
} |
|
|
|
bt_service_active() { |
|
systemctl is-active --quiet "$BT_SERVICE" 2>/dev/null |
|
} |
|
|
|
# Devices to act on: explicit -d/positional list, or the auto list |
|
target_devices() { |
|
if [[ ${#DEVICES[@]} -gt 0 ]]; then |
|
printf '%s\n' "${DEVICES[@]}" |
|
elif [[ ${#BT_AUTO_DEVICES[@]} -gt 0 ]]; then |
|
printf '%s\n' "${BT_AUTO_DEVICES[@]}" |
|
fi |
|
} |
|
|
|
# ------------------------------------------------- |
|
# DEVICE CONFIG FILE |
|
# ------------------------------------------------- |
|
|
|
config_seed() { |
|
mkdir -p "$BT_CONFIG_DIR" |
|
{ |
|
echo "# btswitch device configuration" |
|
echo "# Format: dev NAME MAC (a known device)" |
|
echo "# auto NAME (member of the auto-connect list)" |
|
echo "# Prefer managing this via: btswitch add/remove/auto" |
|
local entry |
|
for entry in "${BT_DEFAULT_DEVICES[@]}"; do echo "dev $entry"; done |
|
for entry in "${BT_DEFAULT_AUTO[@]}"; do echo "auto $entry"; done |
|
} > "$BT_CONFIG_FILE" || { |
|
echo "ERROR: cannot write $BT_CONFIG_FILE" >&2 |
|
exit 1 |
|
} |
|
echo "Created device configuration: $BT_CONFIG_FILE" >&2 |
|
} |
|
|
|
config_load() { |
|
if [[ ! -f "$BT_CONFIG_FILE" ]]; then |
|
config_seed |
|
fi |
|
BT_DEVICES=() |
|
BT_AUTO_DEVICES=() |
|
local line _ name mac |
|
while read -r line || [[ -n "$line" ]]; do |
|
case "$line" in |
|
dev\ *) |
|
read -r _ name mac <<< "$line" |
|
[[ -n "$name" && -n "$mac" ]] && BT_DEVICES["$name"]="$mac" |
|
;; |
|
esac |
|
done < "$BT_CONFIG_FILE" |
|
while read -r line || [[ -n "$line" ]]; do |
|
case "$line" in |
|
auto\ *) |
|
read -r _ name <<< "$line" |
|
if [[ -n "${BT_DEVICES[$name]:-}" ]] && ! auto_contains "$name"; then |
|
BT_AUTO_DEVICES+=("$name") |
|
fi |
|
;; |
|
esac |
|
done < "$BT_CONFIG_FILE" |
|
} |
|
|
|
config_save() { |
|
local tmp name |
|
tmp=$(mktemp) || return 1 |
|
{ |
|
echo "# btswitch device configuration" |
|
echo "# Format: dev NAME MAC (a known device)" |
|
echo "# auto NAME (member of the auto-connect list)" |
|
echo "# Prefer managing this via: btswitch add/remove/auto" |
|
for name in "${!BT_DEVICES[@]}"; do echo "dev $name ${BT_DEVICES[$name]}"; done |
|
for name in "${BT_AUTO_DEVICES[@]}"; do echo "auto $name"; done |
|
} > "$tmp" || { rm -f "$tmp"; return 1; } |
|
mv "$tmp" "$BT_CONFIG_FILE" || { rm -f "$tmp"; return 1; } |
|
} |
|
|
|
# ------------------------------------------------- |
|
# BLUETOOTH CONTROL |
|
# ------------------------------------------------- |
|
|
|
# Makes Bluetooth usable again (after a hard stop or if it was off). |
|
# Only invokes sudo for the steps that are actually needed. |
|
bt_ensure_available() { |
|
if bt_rfkill_blocked; then |
|
echo "Unblocking Bluetooth radio..." |
|
rfkill_bt unblock || return 1 |
|
fi |
|
if bt_service_masked; then |
|
echo "Unmasking $BT_SERVICE service (it was hard-stopped)..." |
|
systemctl_bt unmask || return 1 |
|
fi |
|
if ! bt_service_active; then |
|
echo "Starting $BT_SERVICE service..." |
|
bt_start_service || { |
|
echo "ERROR: could not start $BT_SERVICE.service." >&2 |
|
return 1 |
|
} |
|
fi |
|
if ! timeout 5 bluetoothctl show 2>/dev/null | grep -q "Powered: yes"; then |
|
echo "Powering on adapter..." |
|
local i |
|
for i in {1..10}; do |
|
timeout 5 bluetoothctl power on > /dev/null 2>&1 |
|
sleep 1 |
|
if timeout 5 bluetoothctl show 2>/dev/null | grep -q "Powered: yes"; then |
|
return 0 |
|
fi |
|
done |
|
echo "ERROR: Bluetooth adapter not found or could not be powered on." >&2 |
|
return 1 |
|
fi |
|
return 0 |
|
} |
|
|
|
# Full system-wide shutdown (opt-in via --hard) |
|
bt_hard_off() { |
|
timeout 5 bluetoothctl power off > /dev/null 2>&1 |
|
bt_service_active && systemctl_bt stop |
|
bt_service_masked || systemctl_bt mask |
|
bt_rfkill_blocked || rfkill_bt block |
|
} |
|
|
|
bt_pair_device() { |
|
local mac="$1" |
|
if bt_is_paired "$mac"; then |
|
return 0 |
|
fi |
|
echo "✖ Device $mac is not paired. Pair it once via your desktop Bluetooth settings or 'bluetoothctl pair $mac' (use 'btswitch scan' to discover devices)." >&2 |
|
return 1 |
|
} |
|
|
|
bt_ensure_connected() { |
|
local mac="$1" |
|
echo "----------------------------------------" |
|
echo "Checking device: $mac" |
|
|
|
timeout 5 bluetoothctl unblock "$mac" > /dev/null 2>&1 |
|
|
|
if ! bt_is_trusted "$mac"; then |
|
echo "→ Trusting $mac" |
|
timeout 5 bluetoothctl trust "$mac" > /dev/null 2>&1 |
|
fi |
|
|
|
local i |
|
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 |
|
|
|
timeout $((BT_DELAY * 4)) 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 |
|
} |
|
|
|
# Disconnects and blocks the device so it cannot auto-reconnect, |
|
# while leaving Bluetooth running for everything else. |
|
bt_safe_disconnect() { |
|
local mac="$1" |
|
if ! bt_is_connected "$mac"; then |
|
return 0 |
|
fi |
|
echo "Disconnecting $mac..." |
|
timeout 5 bluetoothctl disconnect "$mac" > /dev/null 2>&1 |
|
sleep 1 |
|
if bt_is_connected "$mac"; then |
|
echo "⚠ $mac is still connected." >&2 |
|
return 0 |
|
fi |
|
echo "Blocking $mac to prevent auto-reconnect ('btswitch start' unblocks it automatically)." |
|
timeout 5 bluetoothctl block "$mac" > /dev/null 2>&1 |
|
} |
|
|
|
bt_start_all() { |
|
local dev mac failed=0 |
|
while read -r dev; do |
|
[[ -n "$dev" ]] || continue |
|
mac=$(bt_resolve_device "$dev") |
|
if [[ -z "$mac" ]]; then |
|
echo "⚠ Unknown device '$dev' (see 'btswitch list')" >&2 |
|
continue |
|
fi |
|
bt_ensure_connected "$mac" || failed=1 |
|
done < <(target_devices) |
|
return "$failed" |
|
} |
|
|
|
bt_stop_all() { |
|
local dev mac |
|
while read -r dev; do |
|
[[ -n "$dev" ]] || continue |
|
mac=$(bt_resolve_device "$dev") |
|
if [[ -z "$mac" ]]; then |
|
echo "⚠ Unknown device '$dev' (see 'btswitch list')" >&2 |
|
continue |
|
fi |
|
bt_safe_disconnect "$mac" |
|
done < <(target_devices) |
|
} |
|
|
|
bt_status_one() { |
|
local dev="$1" mac |
|
mac=$(bt_resolve_device "$dev") |
|
if [[ -z "$mac" ]]; then |
|
echo "⚠ Unknown device '$dev' (see 'btswitch list')" >&2 |
|
return 0 |
|
fi |
|
echo "--- $(bt_name_for_mac "$mac") ($mac) ---" |
|
if ! bt_service_active; then |
|
echo "Status: unknown (Bluetooth service is off)" |
|
return 0 |
|
fi |
|
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 |
|
} |
|
|
|
# ------------------------------------------------- |
|
# COMMANDS |
|
# ------------------------------------------------- |
|
|
|
cmd_start() { |
|
if [[ ${#DEVICES[@]} -eq 0 && ${#BT_AUTO_DEVICES[@]} -eq 0 ]]; then |
|
echo "No devices selected and the auto list is empty (see 'btswitch auto NAME on')." |
|
fi |
|
wifi_switch "$WIFI_5G" |
|
bt_ensure_available || exit 1 |
|
bt_start_all |
|
|
|
if [[ "$TIMED_MINUTES" -gt 0 ]]; then |
|
local action="disconnect devices" |
|
[[ "$HARD_MODE" -eq 1 ]] && action="hard stop (full Bluetooth shutdown)" |
|
echo "Timed mode: will $action in $TIMED_MINUTES minute(s)..." |
|
sleep $((TIMED_MINUTES * 60)) |
|
echo "Time elapsed. Stopping..." |
|
cmd_stop |
|
fi |
|
} |
|
|
|
cmd_stop() { |
|
if ! bt_service_active; then |
|
echo "Bluetooth service is not running." |
|
if [[ "$HARD_MODE" -eq 1 ]]; then |
|
bt_hard_off |
|
wifi_switch "$WIFI_24G" |
|
else |
|
echo "If it was hard-stopped earlier, run 'btswitch restore' to re-enable it." |
|
fi |
|
return 0 |
|
fi |
|
|
|
bt_stop_all |
|
|
|
if [[ "$HARD_MODE" -ne 1 ]]; then |
|
echo "Bluetooth left running so other applications can keep using it." |
|
echo "(For a full system-wide shutdown use: btswitch stop --hard)" |
|
return 0 |
|
fi |
|
|
|
echo "Hard stop: powering off adapter, stopping/masking service and blocking radio." |
|
echo "Bluetooth is disabled system-wide until 'btswitch start' or 'btswitch restore'." |
|
bt_hard_off |
|
wifi_switch "$WIFI_24G" |
|
} |
|
|
|
cmd_restore() { |
|
bt_ensure_available && echo "✔ Bluetooth restored, other applications can use it again." |
|
} |
|
|
|
cmd_status() { |
|
echo "=== Bluetooth adapter ===" |
|
if bt_rfkill_blocked; then |
|
echo "Radio: blocked (rfkill)" |
|
fi |
|
if bt_service_masked; then |
|
echo "Service: masked (hard-stopped; run 'btswitch restore' to re-enable)" |
|
elif bt_service_active; then |
|
timeout 5 bluetoothctl show 2>/dev/null | grep -E "Name|Powered|Discoverable" | sed 's/^[[:space:]]*/ /' |
|
else |
|
echo "Service: inactive (run 'btswitch start' or 'btswitch restore')" |
|
fi |
|
|
|
echo "=== Devices ===" |
|
local devs=("${DEVICES[@]}") dev |
|
if [[ ${#devs[@]} -eq 0 ]]; then |
|
devs=("${!BT_DEVICES[@]}") |
|
fi |
|
if [[ ${#devs[@]} -eq 0 ]]; then |
|
echo "(no devices configured; add one with 'btswitch add NAME MAC')" |
|
return 0 |
|
fi |
|
for dev in "${devs[@]}"; do |
|
bt_status_one "$dev" |
|
done |
|
} |
|
|
|
cmd_scan() { |
|
local duration="${CMD_ARGS[0]:-$BT_SCAN_SECONDS}" |
|
if ! [[ "$duration" =~ ^[0-9]+$ ]] || (( duration < 1 || duration > 300 )); then |
|
echo "ERROR: scan duration must be between 1 and 300 seconds." >&2 |
|
exit 1 |
|
fi |
|
|
|
bt_ensure_available || exit 1 |
|
|
|
echo "Scanning for $duration seconds (put devices in range/pairing mode)..." |
|
timeout $((duration + 10)) bluetoothctl --timeout "$duration" scan on > /dev/null 2>&1 |
|
timeout 5 bluetoothctl scan off > /dev/null 2>&1 |
|
|
|
local listing |
|
listing=$(timeout 5 bluetoothctl devices 2>/dev/null | sort -k3) |
|
if [[ -z "$listing" ]]; then |
|
echo "No devices found. Make sure they are turned on and in range." |
|
return 0 |
|
fi |
|
|
|
local -A mac_to_name=() |
|
local name |
|
for name in "${!BT_DEVICES[@]}"; do |
|
mac_to_name["${BT_DEVICES[$name]}"]="$name" |
|
done |
|
|
|
printf "%-24s %-19s %s\n" "NAME" "MAC" "FLAGS" |
|
local _ mac flags flagstr |
|
while read -r _ mac name; do |
|
[[ -n "$mac" ]] || continue |
|
flags=() |
|
[[ -n "${mac_to_name[$mac]:-}" ]] && flags+=("configured") |
|
bt_is_paired "$mac" && flags+=("paired") |
|
bt_is_connected "$mac" && flags+=("connected") |
|
flagstr="new" |
|
[[ ${#flags[@]} -gt 0 ]] && flagstr=$(IFS=,; echo "${flags[*]}") |
|
printf "%-24s %-19s %s\n" "${name:-(unknown)}" "$mac" "$flagstr" |
|
done <<< "$listing" |
|
|
|
echo |
|
echo "To manage a found device: btswitch add NAME MAC" |
|
} |
|
|
|
cmd_list() { |
|
if [[ ${#BT_DEVICES[@]} -eq 0 ]]; then |
|
echo "No devices configured. Add one with: btswitch add NAME MAC" |
|
return 0 |
|
fi |
|
local online=1 |
|
bt_service_active || online=0 |
|
printf "%-12s %-19s %-18s %s\n" "NAME" "MAC" "STATUS" "AUTO" |
|
local names name mac status auto |
|
names=$(printf '%s\n' "${!BT_DEVICES[@]}" | sort) |
|
while read -r name; do |
|
mac="${BT_DEVICES[$name]}" |
|
if [[ "$online" -eq 1 ]]; then |
|
if bt_is_connected "$mac"; then |
|
status="connected" |
|
elif bt_is_paired "$mac"; then |
|
status="paired (offline)" |
|
else |
|
status="not paired" |
|
fi |
|
else |
|
status="unknown (bt off)" |
|
fi |
|
if auto_contains "$name"; then auto="yes"; else auto="-"; fi |
|
printf "%-12s %-19s %-18s %s\n" "$name" "$mac" "$status" "$auto" |
|
done <<< "$names" |
|
} |
|
|
|
cmd_add() { |
|
if [[ ${#CMD_ARGS[@]} -ne 2 ]]; then |
|
echo "Usage: btswitch add NAME MAC" >&2 |
|
exit 1 |
|
fi |
|
local name="${CMD_ARGS[0]}" mac="${CMD_ARGS[1]^^}" |
|
if ! [[ "$name" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]]; then |
|
echo "ERROR: invalid NAME '$name' (use letters, digits, '_' and '-')." >&2 |
|
exit 1 |
|
fi |
|
if ! bt_is_mac_address "$mac"; then |
|
echo "ERROR: invalid MAC address '${CMD_ARGS[1]}'." >&2 |
|
exit 1 |
|
fi |
|
if [[ -n "${BT_DEVICES[$name]:-}" ]]; then |
|
echo "ERROR: device '$name' is already configured (${BT_DEVICES[$name]})." >&2 |
|
exit 1 |
|
fi |
|
local other |
|
other=$(bt_name_for_mac "$mac") |
|
if [[ "$other" != "$mac" ]]; then |
|
echo "ERROR: MAC $mac is already configured as '$other'." >&2 |
|
exit 1 |
|
fi |
|
|
|
BT_DEVICES["$name"]="$mac" |
|
config_save || exit 1 |
|
echo "✔ Added $name ($mac)" |
|
if bt_service_active && ! bt_is_paired "$mac"; then |
|
echo "Note: not paired yet. Pair it once (desktop Bluetooth settings or 'bluetoothctl pair $mac') while it is in pairing mode." |
|
elif ! bt_service_active; then |
|
echo "Note: Bluetooth service is off; pairing status could not be checked." |
|
fi |
|
echo "Add it to the auto-connect list with: btswitch auto $name on" |
|
} |
|
|
|
cmd_remove() { |
|
if [[ ${#CMD_ARGS[@]} -ne 1 ]]; then |
|
echo "Usage: btswitch remove NAME" >&2 |
|
exit 1 |
|
fi |
|
local name="${CMD_ARGS[0]}" |
|
if [[ -z "${BT_DEVICES[$name]:-}" ]]; then |
|
echo "ERROR: device '$name' is not configured. See 'btswitch list'." >&2 |
|
exit 1 |
|
fi |
|
local mac="${BT_DEVICES[$name]}" |
|
unset "BT_DEVICES[$name]" |
|
local filtered=() dev |
|
for dev in "${BT_AUTO_DEVICES[@]}"; do |
|
[[ "$dev" != "$name" ]] && filtered+=("$dev") |
|
done |
|
BT_AUTO_DEVICES=("${filtered[@]}") |
|
config_save || exit 1 |
|
echo "✔ Removed $name ($mac) from the btswitch config." |
|
echo "It stays paired in BlueZ; to fully remove it: bluetoothctl remove $mac" |
|
} |
|
|
|
cmd_auto() { |
|
if [[ ${#CMD_ARGS[@]} -eq 0 ]]; then |
|
echo "Auto-connect devices: ${BT_AUTO_DEVICES[*]:-<none>}" |
|
return 0 |
|
fi |
|
if [[ ${#CMD_ARGS[@]} -ne 2 ]]; then |
|
echo "Usage: btswitch auto NAME on|off" >&2 |
|
exit 1 |
|
fi |
|
local name="${CMD_ARGS[0]}" state="${CMD_ARGS[1]}" |
|
if [[ -z "${BT_DEVICES[$name]:-}" ]]; then |
|
echo "ERROR: device '$name' is not configured (add it first)." >&2 |
|
exit 1 |
|
fi |
|
case "$state" in |
|
on) |
|
if auto_contains "$name"; then |
|
echo "'$name' is already in the auto list." |
|
return 0 |
|
fi |
|
BT_AUTO_DEVICES+=("$name") |
|
;; |
|
off) |
|
local filtered=() dev |
|
for dev in "${BT_AUTO_DEVICES[@]}"; do |
|
[[ "$dev" != "$name" ]] && filtered+=("$dev") |
|
done |
|
BT_AUTO_DEVICES=("${filtered[@]}") |
|
;; |
|
*) |
|
echo "Usage: btswitch auto NAME on|off" >&2 |
|
exit 1 |
|
;; |
|
esac |
|
config_save || exit 1 |
|
echo "✔ Auto-connect list: ${BT_AUTO_DEVICES[*]:-<none>}" |
|
} |
|
|
|
usage() { |
|
cat << EOF |
|
Usage: btswitch [options] COMMAND [args...] |
|
|
|
Connect and manage predefined Bluetooth devices. |
|
|
|
Commands: |
|
start [devices...] Make Bluetooth available and connect the auto devices |
|
(or only the given devices: names or MACs) |
|
stop [devices...] Disconnect the devices and block them so they do not |
|
auto-reconnect. Bluetooth stays on for other apps. |
|
stop --hard Additionally: power off adapter, stop+mask service, |
|
rfkill block (Bluetooth off system-wide) |
|
restore Undo a hard stop (unblock radio, unmask/start service) |
|
status [devices...] Adapter state + device status (default: all configured) |
|
list List configured devices and their status |
|
scan [seconds] Scan for nearby devices (default: $BT_SCAN_SECONDS s) |
|
add NAME MAC Add a device to the configuration |
|
remove NAME Remove a device from the configuration |
|
auto [NAME on|off] Show the auto-connect list, or modify it |
|
help Show this help |
|
|
|
Options: |
|
-d, --device NAME|MAC Act only on this device (repeatable) |
|
-t, --timed MINUTES Stop automatically after MINUTES minutes |
|
--hard With stop/-t: full system-wide Bluetooth shutdown |
|
-h, --help Show this help |
|
|
|
Devices are stored in $BT_CONFIG_FILE. |
|
Devices must be paired once (e.g. via desktop Bluetooth settings) before |
|
btswitch can connect them. Sudo is only needed for --hard/restore, and |
|
only when the service or radio state actually has to be changed. |
|
EOF |
|
} |
|
|
|
# ------------------------------------------------- |
|
# MAIN |
|
# ------------------------------------------------- |
|
|
|
parse_args() { |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-d | --device) |
|
DEVICES+=("$2") |
|
shift 2 |
|
;; |
|
-t | --timed) |
|
TIMED_MINUTES="$2" |
|
shift 2 |
|
;; |
|
--hard) |
|
HARD_MODE=1 |
|
shift |
|
;; |
|
-h | --help | help) |
|
COMMAND="help" |
|
shift |
|
;; |
|
start | stop | status | scan | list | add | remove | auto | restore) |
|
if [[ -z "$COMMAND" || "$COMMAND" == "help" ]]; then |
|
COMMAND="$1" |
|
else |
|
CMD_ARGS+=("$1") |
|
fi |
|
shift |
|
;; |
|
*) |
|
if [[ -z "$COMMAND" ]]; then |
|
echo "Unknown option or command: $1" >&2 |
|
usage >&2 |
|
exit 1 |
|
fi |
|
CMD_ARGS+=("$1") |
|
shift |
|
;; |
|
esac |
|
done |
|
} |
|
|
|
parse_args "$@" |
|
|
|
if [[ -z "$COMMAND" ]]; then |
|
usage >&2 |
|
exit 1 |
|
fi |
|
if [[ "$COMMAND" == "help" ]]; then |
|
usage |
|
exit 0 |
|
fi |
|
if ! [[ "$TIMED_MINUTES" =~ ^[0-9]+$ ]]; then |
|
echo "ERROR: --timed expects a number of minutes." >&2 |
|
exit 1 |
|
fi |
|
|
|
case "$COMMAND" in |
|
start | stop | status) |
|
DEVICES+=("${CMD_ARGS[@]}") |
|
;; |
|
esac |
|
|
|
config_load |
|
|
|
case "$COMMAND" in |
|
start) cmd_start ;; |
|
stop) cmd_stop ;; |
|
restore) cmd_restore ;; |
|
status) cmd_status ;; |
|
scan) cmd_scan ;; |
|
list) cmd_list ;; |
|
add) cmd_add ;; |
|
remove) cmd_remove ;; |
|
auto) cmd_auto ;; |
|
esac
|
|
|