testsAndMisc/linux_configuration/scripts/fixes/fix_bluetooth.sh

713 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
# Fix Bluetooth connectivity issues on Arch Linux
#
# Common issues addressed:
# 1. Bluetooth service not running or in bad state
# 2. Missing PipeWire/PulseAudio Bluetooth audio support (A2DP sink)
# 3. Stale pairing data causing connection hangs
# 4. Missing Broadcom firmware (.hcd files)
# 5. Stuck/unresponsive adapter requiring USB reset
# 6. USB autosuspend causing audio dropouts
# 7. Hung PipeWire/WirePlumber audio stack
# 8. Auto scan/pair/trust/connect when MAC is provided
# 9. SBC-XQ codec causing dropouts on older adapters
# 10. Stale HCI link state (link tx timeout) requiring btusb reload
# 11. A2DP ServicesResolved stuck at false after connect
# 12. PipeWire bluez audio card not appearing after connection
#
# Usage:
# ./fix_bluetooth.sh # Diagnose and fix + connect JBL Charge 5
# ./fix_bluetooth.sh --interactive # Prompt before each fix
# ./fix_bluetooth.sh <MAC> # Fix + auto-connect to device
# ./fix_bluetooth.sh --interactive <MAC> # Both
#
# Safe to re-run: all fixes are idempotent.
set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
parse_interactive_args "$@"
shift "$COMMON_ARGS_SHIFT"
TARGET_MAC="${1:-F8:5C:7E:0E:50:6B}"
require_root "$@"
print_setup_header "Bluetooth Troubleshooter"
FIXES_APPLIED=0
FIXES_SKIPPED=0
# ---------------------------------------------------------------------------
# Helper: run or skip a fix depending on --interactive
# ---------------------------------------------------------------------------
apply_fix() {
local description="$1"
shift
echo ""
log_info "$description"
if [[ $INTERACTIVE_MODE == "true" ]]; then
if ! ask_yes_no " Apply this fix?"; then
log_warn "Skipped."
((FIXES_SKIPPED++)) || true
return 0
fi
fi
if "$@"; then
log_ok "Done."
((FIXES_APPLIED++)) || true
else
log_error "Failed (non-fatal, continuing)."
fi
}
# ---------------------------------------------------------------------------
# Helper: run a bluetoothctl command reliably via stdin pipe.
# (bluetoothctl -- <cmd> returns empty when run non-interactively)
# ---------------------------------------------------------------------------
_btctl() {
echo "$*" | bluetoothctl 2>/dev/null
}
# ==========================================================================
# 1. Check Bluetooth service status
# ==========================================================================
check_bluetooth_service() {
echo ""
log_info "Checking bluetooth.service status..."
if ! systemctl is-active --quiet bluetooth.service; then
log_warn "bluetooth.service is not running."
apply_fix "Starting and enabling bluetooth.service" \
systemctl enable --now bluetooth.service
else
log_ok "bluetooth.service is active."
fi
# Also check if the adapter is soft/hard blocked
if has_cmd rfkill; then
local blocked
blocked=$(rfkill list bluetooth 2>/dev/null || true)
if echo "$blocked" | grep -qi "Soft blocked: yes"; then
apply_fix "Unblocking Bluetooth (rfkill)" rfkill unblock bluetooth
elif echo "$blocked" | grep -qi "Hard blocked: yes"; then
log_error "Bluetooth is HARD blocked (physical switch). Enable it manually."
else
log_ok "Bluetooth is not blocked by rfkill."
fi
fi
}
# ==========================================================================
# 2. Check for required packages (bluez, pipewire-pulse, etc.)
# ==========================================================================
check_packages() {
echo ""
log_info "Checking required Bluetooth packages..."
local missing=()
for pkg in bluez bluez-utils; do
if ! pacman -Qi "$pkg" &>/dev/null; then
missing+=("$pkg")
else
log_ok "$pkg is installed."
fi
done
# Detect audio backend and check for BT audio support
if pacman -Qi pipewire &>/dev/null; then
log_info "PipeWire detected as audio server."
if ! pacman -Qi pipewire-pulse &>/dev/null; then
missing+=("pipewire-pulse")
else
log_ok "pipewire-pulse is installed."
fi
elif pacman -Qi pulseaudio &>/dev/null; then
log_info "PulseAudio detected as audio server."
if ! pacman -Qi pulseaudio-bluetooth &>/dev/null; then
missing+=("pulseaudio-bluetooth")
else
log_ok "pulseaudio-bluetooth is installed."
fi
else
log_warn "No PipeWire or PulseAudio detected. Bluetooth audio may not work."
fi
if [[ ${#missing[@]} -gt 0 ]]; then
apply_fix "Installing missing packages: ${missing[*]}" \
pacman -S --noconfirm "${missing[@]}"
fi
}
# ==========================================================================
# 3. Check for missing Broadcom firmware
# ==========================================================================
check_firmware() {
echo ""
log_info "Checking for missing Bluetooth firmware..."
local missing_fw
missing_fw=$(dmesg 2>/dev/null | grep -o "brcm/BCM[^ ']*\.hcd" | sort -u || true)
if [[ -z $missing_fw ]]; then
log_ok "No missing firmware detected in dmesg."
return 0
fi
local fw_name
for fw_name in $missing_fw; do
local fw_path="/usr/lib/firmware/$fw_name"
if [[ -f $fw_path ]]; then
log_ok "Firmware $fw_name already installed."
continue
fi
local basename
basename=$(basename "$fw_name")
local url="https://github.com/winterheart/broadcom-bt-firmware/raw/master/brcm/$basename"
log_warn "Missing firmware: $fw_name"
apply_fix "Downloading $basename from broadcom-bt-firmware repo" \
_download_firmware "$url" "$fw_path"
done
}
_download_firmware() {
local url="$1"
local dest="$2"
mkdir -p "$(dirname "$dest")"
wget -q "$url" -O "$dest" || curl -sL "$url" -o "$dest"
}
# ==========================================================================
# 4. Reset stuck adapter via USB reset
# ==========================================================================
check_adapter_stuck() {
echo ""
log_info "Checking if Bluetooth adapter is responsive..."
# Test if bluetoothctl can see the adapter
local adapter_list
adapter_list=$(_btctl list | grep "^Controller" || true)
if [[ -n $adapter_list ]]; then
log_ok "Adapter is responsive: $adapter_list"
return 0
fi
log_warn "Adapter not responding to bluetoothctl."
# Try USB reset if usbreset is available
local bt_usb
bt_usb=$(lsusb 2>/dev/null | grep -i bluetooth | head -1 || true)
if [[ -n $bt_usb ]]; then
local usb_id
usb_id=$(echo "$bt_usb" | grep -oP 'ID \K[0-9a-f]{4}:[0-9a-f]{4}')
if [[ -n $usb_id ]] && has_cmd usbreset; then
apply_fix "USB-resetting Bluetooth adapter ($usb_id)" \
usbreset "$usb_id"
else
log_info "Falling back to btusb module reload..."
apply_fix "Reloading btusb kernel module" \
_reload_btusb
fi
else
log_error "No USB Bluetooth adapter found. Is the dongle plugged in?"
fi
}
_reload_btusb() {
modprobe -r btusb && sleep 1 && modprobe btusb && sleep 2
}
# ---------------------------------------------------------------------------
# Helper: check if A2DP services are resolved for a connected device
# Returns 0 if resolved, 1 otherwise.
# ---------------------------------------------------------------------------
_services_resolved() {
local mac="$1"
local dbus_path="/org/bluez/hci0/dev_${mac//:/_}"
local result
result=$(dbus-send --system --print-reply \
--dest=org.bluez "$dbus_path" \
org.freedesktop.DBus.Properties.Get \
string:"org.bluez.Device1" string:"ServicesResolved" 2>/dev/null || true)
echo "$result" | grep -q "boolean true"
}
# ---------------------------------------------------------------------------
# Helper: full reset cycle — btusb reload + service restarts + reconnect.
# Fixes stale HCI link state ("link tx timeout" / ServicesResolved stuck).
# ---------------------------------------------------------------------------
_full_adapter_reset_and_connect() {
local mac="$1"
log_info "Performing full adapter reset (btusb reload)..."
_btctl disconnect "$mac" >/dev/null 2>&1 || true
sleep 1
modprobe -r btusb && sleep 2 && modprobe btusb && sleep 5
systemctl restart bluetooth.service
sleep 3
_restart_pipewire_stack
sleep 3
log_info "Reconnecting to $mac after adapter reset..."
{ echo "power on"; sleep 1; echo "connect $mac"; sleep 20; } \
| bluetoothctl 2>/dev/null || true
}
# ---------------------------------------------------------------------------
# Helper: verify the Bluetooth audio sink appeared in PipeWire.
# ---------------------------------------------------------------------------
_verify_audio_sink() {
local mac="$1"
local card_name="bluez_card.${mac//:/_}"
if ! has_cmd pactl; then
return 0
fi
# Give PipeWire time to create the audio card
local _attempt
for _attempt in 1 2 3 4 5; do
if pactl list cards short 2>/dev/null | grep -q "$card_name"; then
log_ok "Bluetooth audio card detected in PipeWire."
return 0
fi
sleep 3
done
log_warn "Bluetooth audio card not found in PipeWire after connection."
return 1
}
# ==========================================================================
# 5. Remove stale pairing for target device (if specified)
# ==========================================================================
remove_stale_pairing() {
if [[ -z $TARGET_MAC ]]; then
return 0
fi
echo ""
log_info "Checking for stale pairing with $TARGET_MAC..."
local info
info=$(_btctl info "$TARGET_MAC" || true)
if echo "$info" | grep -q "Device $TARGET_MAC"; then
local paired
paired=$(echo "$info" | grep "Paired:" | awk '{print $2}')
local connected
connected=$(echo "$info" | grep "Connected:" | awk '{print $2}')
if [[ $paired == "yes" && $connected == "no" ]]; then
log_warn "Device is paired but NOT connected — may have stale pairing."
apply_fix "Removing stale pairing for $TARGET_MAC" \
_btctl remove "$TARGET_MAC"
elif [[ $paired == "no" ]]; then
log_info "Device is not currently paired."
else
log_ok "Device is paired and connected."
fi
else
log_info "Device $TARGET_MAC not found in bluetoothctl. Fresh pairing needed."
fi
}
# ==========================================================================
# 6. Restart Bluetooth service to apply changes
# ==========================================================================
restart_bluetooth() {
echo ""
if [[ $FIXES_APPLIED -gt 0 ]]; then
apply_fix "Restarting bluetooth.service to apply changes" \
systemctl restart bluetooth.service
else
log_info "No fixes applied — skipping service restart."
fi
}
# ==========================================================================
# 7. Disable USB autosuspend for Bluetooth adapter
# ==========================================================================
fix_usb_autosuspend() {
echo ""
log_info "Checking USB autosuspend for Bluetooth adapter..."
local bt_usb
bt_usb=$(lsusb 2>/dev/null | grep -i bluetooth | head -1 || true)
if [[ -z $bt_usb ]]; then
return 0
fi
local usb_id
usb_id=$(echo "$bt_usb" | grep -oP 'ID \K[0-9a-f]{4}:[0-9a-f]{4}' || true)
if [[ -z $usb_id ]]; then
return 0
fi
local vendor="${usb_id%%:*}"
local product="${usb_id##*:}"
# Find the sysfs device path
local sysfs_path=""
local dev_path
for dev_path in /sys/bus/usb/devices/*/; do
if [[ -f "${dev_path}idVendor" && -f "${dev_path}idProduct" ]]; then
local v p
v=$(cat "${dev_path}idVendor" 2>/dev/null || true)
p=$(cat "${dev_path}idProduct" 2>/dev/null || true)
if [[ $v == "$vendor" && $p == "$product" ]]; then
sysfs_path="$dev_path"
break
fi
fi
done
if [[ -z $sysfs_path ]]; then
log_warn "Could not find sysfs path for BT adapter."
return 0
fi
local power_control="${sysfs_path}power/control"
if [[ -f $power_control ]]; then
local current
current=$(cat "$power_control" 2>/dev/null || true)
if [[ $current != "on" ]]; then
log_warn "USB autosuspend is enabled ($current) — can cause audio dropouts."
apply_fix "Disabling USB autosuspend for BT adapter" \
_disable_usb_autosuspend "$power_control" "$vendor" "$product"
else
log_ok "USB autosuspend already disabled."
fi
fi
}
_disable_usb_autosuspend() {
local power_control="$1"
local vendor="$2"
local product="$3"
# Immediate fix
echo "on" > "$power_control"
# Persistent udev rule
local rule_file="/etc/udev/rules.d/50-bluetooth-no-autosuspend.rules"
local rule="ACTION==\"add\", SUBSYSTEM==\"usb\", ATTR{idVendor}==\"$vendor\", ATTR{idProduct}==\"$product\", ATTR{power/control}=\"on\""
if [[ ! -f $rule_file ]] || ! grep -qF "$vendor" "$rule_file" 2>/dev/null; then
echo "$rule" > "$rule_file"
udevadm control --reload-rules 2>/dev/null || true
log_info "Created persistent udev rule: $rule_file"
fi
}
# ==========================================================================
# 8. Check PipeWire/WirePlumber health (hung audio stack)
# ==========================================================================
check_pipewire_health() {
echo ""
log_info "Checking PipeWire/WirePlumber health..."
if ! has_cmd wpctl; then
log_info "wpctl not found — skipping PipeWire health check."
return 0
fi
# Test if PipeWire is responding within 3 seconds
if timeout 3 wpctl status &>/dev/null; then
log_ok "PipeWire is responsive."
return 0
fi
log_warn "PipeWire/WirePlumber appears hung (wpctl timed out)."
apply_fix "Restarting PipeWire + WirePlumber audio stack" \
_restart_pipewire_stack
}
_restart_pipewire_stack() {
# Restart as the calling user (these are user services)
local target_user
target_user="${SUDO_USER:-$USER}"
local target_uid
target_uid=$(id -u "$target_user")
sudo -u "$target_user" \
XDG_RUNTIME_DIR="/run/user/$target_uid" \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$target_uid/bus" \
systemctl --user restart pipewire pipewire-pulse wireplumber
sleep 3
log_info "Waiting for audio stack to initialize..."
}
# ==========================================================================
# 9. Auto-connect target device
# ==========================================================================
connect_device() {
if [[ -z $TARGET_MAC ]]; then
return 0
fi
echo ""
log_info "Attempting to connect to $TARGET_MAC..."
# Check if already connected
local info
info=$(_btctl info "$TARGET_MAC" || true)
if echo "$info" | grep -q "Connected: yes"; then
log_ok "Device $TARGET_MAC is already connected."
return 0
fi
# Power on adapter
_btctl power on >/dev/null || true
sleep 1
# ---- Attempt 1: direct connect (existing pairing) ----
if echo "$info" | grep -q "Paired: yes"; then
log_info "Device is already paired. Trying direct connect..."
{ echo "power on"; sleep 1; echo "connect $TARGET_MAC"; sleep 15; } \
| bluetoothctl 2>/dev/null || true
if _check_connection_health "$TARGET_MAC"; then
return 0
fi
# Direct connect failed — remove stale pairing and try fresh
log_warn "Direct connect failed. Removing stale pairing for fresh start."
_btctl remove "$TARGET_MAC" >/dev/null || true
sleep 2
fi
# ---- Attempt 2: scan + pair from scratch ----
log_info "Scanning for $TARGET_MAC (20 seconds)..."
log_info "Make sure the device is in pairing mode."
{ echo "power on"; sleep 1; echo "scan on"; sleep 20; echo "scan off"; sleep 2; } \
| bluetoothctl 2>/dev/null || true
# Check if device was found
local devices
devices=$(_btctl devices || true)
if ! echo "$devices" | grep -qi "$TARGET_MAC"; then
log_error "Device $TARGET_MAC not found during scan."
log_info "Put the device in pairing mode and re-run the script."
return 1
fi
log_ok "Device found during scan."
# Pair
log_info "Pairing..."
{ echo "power on"; sleep 1; echo "pair $TARGET_MAC"; sleep 5; } \
| bluetoothctl 2>/dev/null || true
# Trust (so it auto-reconnects in the future)
log_info "Trusting..."
{ echo "trust $TARGET_MAC"; sleep 2; } | bluetoothctl 2>/dev/null || true
# Connect
log_info "Connecting..."
{ echo "power on"; sleep 1; echo "connect $TARGET_MAC"; sleep 15; } \
| bluetoothctl 2>/dev/null || true
# Verify connection + services + audio
if _check_connection_health "$TARGET_MAC"; then
return 0
fi
log_error "Connection to $TARGET_MAC failed."
log_info "Try putting the device in pairing mode and re-run."
return 1
}
# ---------------------------------------------------------------------------
# Helper: verify connection is fully healthy (connected + services + audio).
# If connected but services stuck, triggers full adapter reset + retry.
# ---------------------------------------------------------------------------
_check_connection_health() {
local mac="$1"
local info
sleep 2
info=$(_btctl info "$mac" || true)
# Not connected at all
if ! echo "$info" | grep -q "Connected: yes"; then
return 1
fi
# Connected — check if A2DP services resolved
local _attempt
for _attempt in 1 2 3; do
if _services_resolved "$mac"; then
log_ok "Connected to $mac with A2DP services resolved."
_verify_audio_sink "$mac" || true
return 0
fi
sleep 3
done
# Connected but ServicesResolved stuck at false — stale HCI link state.
log_warn "Connected but A2DP services not resolved (stale HCI link state)."
apply_fix "Full adapter reset to fix stale link" \
_full_adapter_reset_and_connect "$mac"
# Verify after reset
sleep 3
if _services_resolved "$mac"; then
log_ok "Connected to $mac with A2DP services resolved after reset."
_verify_audio_sink "$mac" || true
return 0
fi
return 1
}
# ==========================================================================
# 10. Set audio profile (avoid SBC-XQ dropouts on older adapters)
# ==========================================================================
set_audio_profile() {
if [[ -z $TARGET_MAC ]]; then
return 0
fi
if ! has_cmd pactl; then
return 0
fi
echo ""
log_info "Checking audio profile..."
# Wait a moment for PipeWire to set up the audio card
sleep 3
local card_name="bluez_card.${TARGET_MAC//:/_}"
local card_info
card_info=$(pactl list cards 2>/dev/null || true)
if ! echo "$card_info" | grep -q "$card_name"; then
log_info "No PipeWire audio card found for device (may not be an audio device)."
return 0
fi
local current_profile
current_profile=$(echo "$card_info" | grep -A 50 "$card_name" | grep "Active Profile:" | head -1 | awk '{print $3}' || true)
if [[ $current_profile == *"sbc_xq"* ]]; then
log_warn "SBC-XQ codec active — may cause audio dropouts on older adapters."
apply_fix "Switching to standard SBC codec" \
pactl set-card-profile "$card_name" a2dp-sink
elif [[ -n $current_profile ]]; then
log_ok "Audio profile: $current_profile"
fi
}
# ==========================================================================
# 11. Show connection instructions
# ==========================================================================
show_instructions() {
echo ""
echo "==========================================="
echo " Next Steps"
echo "==========================================="
echo ""
echo "1. Put your Bluetooth device in PAIRING mode"
echo " (e.g., hold the Bluetooth button until LED blinks rapidly)"
echo ""
echo "2. In bluetoothctl:"
if [[ -n $TARGET_MAC ]]; then
cat <<EOF
scan on
# Wait for device to appear
pair $TARGET_MAC
trust $TARGET_MAC
connect $TARGET_MAC
EOF
else
cat <<EOF
scan on
# Wait for device to appear, note its MAC address
pair <MAC>
trust <MAC>
connect <MAC>
EOF
fi
echo ""
echo "3. If connection still fails, check logs:"
echo " journalctl -u bluetooth -f"
echo ""
}
# ==========================================================================
# 12. Dump diagnostic info
# ==========================================================================
dump_diagnostics() {
echo ""
log_info "=== Diagnostic Summary ==="
echo ""
echo "--- Bluetooth adapter ---"
_btctl show | grep -v '\[bluetoothctl\]' | head -20 || true
echo ""
echo "--- Known devices ---"
_btctl devices | grep '^Device' || true
echo ""
echo "--- Loaded Bluetooth kernel modules ---"
lsmod | grep -i bluetooth || echo "(none loaded)"
echo ""
echo "--- rfkill status ---"
rfkill list bluetooth 2>/dev/null || echo "(rfkill not available)"
if [[ -n $TARGET_MAC ]]; then
echo ""
echo "--- Device info: $TARGET_MAC ---"
_btctl info "$TARGET_MAC" || echo "(device not known)"
fi
echo ""
echo "--- Recent bluetooth journal entries ---"
journalctl -u bluetooth --no-pager -n 20 2>/dev/null || true
}
# ==========================================================================
# Main
# ==========================================================================
main() {
dump_diagnostics
check_bluetooth_service
check_packages
check_firmware
check_adapter_stuck
fix_usb_autosuspend
check_pipewire_health
restart_bluetooth
echo ""
echo "==========================================="
printf "Fixes applied: %d | Skipped: %d\n" "$FIXES_APPLIED" "$FIXES_SKIPPED"
echo "==========================================="
if ! connect_device; then
show_instructions
fi
set_audio_profile
}
main