#!/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 # Fix + auto-connect to device # ./fix_bluetooth.sh --interactive # 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 -- 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 "agent on"; echo "default-agent"; sleep 1; 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 _run_as_user 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 _run_as_user 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 } # --------------------------------------------------------------------------- # Helper: run a command as the actual (non-root) user with PipeWire env. # Needed because pactl/wpctl/systemctl --user talk to the user session. # --------------------------------------------------------------------------- _run_as_user() { 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" \ "$@" } _restart_pipewire_stack() { _run_as_user 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 "agent on"; echo "default-agent"; sleep 1; 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 "agent on"; echo "default-agent"; sleep 1; 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 "agent on"; echo "default-agent"; sleep 1; 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=$(_run_as_user 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" \ _run_as_user 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 < trust connect 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 remove_stale_pairing 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