diff --git a/docs/superpowers/contracts/linux-config-bluetooth-split-2026-08-16.json b/docs/superpowers/contracts/linux-config-bluetooth-split-2026-08-16.json new file mode 100644 index 00000000..477710ac --- /dev/null +++ b/docs/superpowers/contracts/linux-config-bluetooth-split-2026-08-16.json @@ -0,0 +1,18 @@ +{ + "title": "Split fix_bluetooth.sh under the cap, deferring steam_compatibility.sh", + "objective": "Bring fix_bluetooth.sh (720 lines) under the 250-line cap using a single multi-lib pass, and confirm that a seam crossing shared mutable state must be abandoned rather than suppressed.", + "acceptance_criteria": [ + "The entry script and all four new libs are at most 250 lines.", + "The pre-commit shellcheck hook passes on every touched file, with no new suppression and no disable directive.", + "All four libs are written in one pass, so the entry script sources every lib directly and no lib sources a sibling.", + "The stubbed run reports every moved function as defined.", + "No script is executed. fix_bluetooth.sh reloads btusb and restarts bluetooth and PipeWire, which would drop live audio.", + "steam_compatibility.sh is fully restored to 663 lines, with the unrelated libs already present in the same lib/ directory left untouched." + ], + "out_of_scope": [ + "Executing any bluetooth or audio fix.", + "steam_compatibility.sh, whose CACHE_MAP associative array is written in one prospective lib and read by main.", + "Changing what any script does when run." + ], + "verifier": "meta/scripts/verify_shell_split.sh, plus pre-commit run --files over all touched files, plus check_file_length.sh, plus ls of the shared lib/ directory to confirm the revert removed only what it should." +} diff --git a/docs/superpowers/evidence/linux-config-bluetooth-split-2026-08-16.json b/docs/superpowers/evidence/linux-config-bluetooth-split-2026-08-16.json new file mode 100644 index 00000000..8b575d16 --- /dev/null +++ b/docs/superpowers/evidence/linux-config-bluetooth-split-2026-08-16.json @@ -0,0 +1,44 @@ +{ + "intent": "Split fix_bluetooth.sh (720 lines) under the cap in a single multi-lib pass, and defer steam_compatibility.sh whose seam crosses a shared associative array.", + "scope": [ + "linux_configuration/scripts/single_use/fixes/fix_bluetooth.sh (720 -> 186)", + "lib/bt_adapter.sh, bt_pairing.sh, bt_audio.sh, bt_report.sh (new)", + "Deferred and fully restored: single_use/utils/steam_compatibility.sh (663)", + "Non-goal: no script executed" + ], + "changes": [ + "fix_bluetooth.sh split into four libs in one pass rather than four sequential passes. Writing all the libs at once is what avoids the chaining bug seen twice before: the entry gets a single source block naming every lib, and no lib sources a sibling.", + "The four libs follow the script's own phases: adapter and firmware recovery, pairing and USB autosuspend, PipeWire and audio, then instructions and diagnostics.", + "steam_compatibility.sh was reverted. Its CACHE_MAP associative array is declared and populated in what would be the ProtonDB lib but read by main, so the lib fails SC2034 when linted alone. Same class as the pacman stall script: the seam does not exist without passing that state explicitly." + ], + "verification": [ + { + "command": "meta/scripts/verify_shell_split.sh fix_bluetooth.sh check_firmware remove_stale_pairing check_pipewire_health show_instructions dump_diagnostics connect_device", + "result": "pass", + "evidence": "'libs sourced, functions defined' across all four libs, first try - no chaining and no SCRIPT_DIR collision." + }, + { + "command": "pre-commit run --files ", + "result": "pass", + "evidence": "shellcheck (which lints each lib in isolation), shfmt, the shebang hook and jscpd all pass." + }, + { + "command": "git checkout HEAD -- steam_compatibility.sh, rm its libs, shellcheck -x", + "result": "pass", + "evidence": "Restored to 663 lines and clean, with the unrelated libs in that same lib/ directory left in place - checked by listing the directory rather than assuming." + }, + { + "command": "bash ~/utils/scripts/check_file_length.sh --all", + "result": "pass", + "evidence": "fix_bluetooth.sh and its four libs are all under the cap; the largest is 214 lines." + } + ], + "risks": [ + "fix_bluetooth.sh was not run: it reloads btusb, restarts bluetooth and PipeWire, and reconnects devices. Running it would disconnect live audio, which is exactly the mutation decision 6 excludes.", + "steam_compatibility.sh joins the deferred list at 663 lines - the largest remaining single_use file. Splitting it needs CACHE_MAP passed or rebuilt rather than shared across the seam." + ], + "rollback": [ + "git revert the commit; fix_bluetooth.sh returns to 720 lines and the four libs disappear.", + "After rollback, re-run bash -n and shellcheck on the restored script." + ] +} diff --git a/linux_configuration/scripts/single_use/fixes/fix_bluetooth.sh b/linux_configuration/scripts/single_use/fixes/fix_bluetooth.sh index 18c1074a..d93d870b 100755 --- a/linux_configuration/scripts/single_use/fixes/fix_bluetooth.sh +++ b/linux_configuration/scripts/single_use/fixes/fix_bluetooth.sh @@ -147,549 +147,15 @@ check_packages() { fi } -# ========================================================================== -# 3. Check for missing Broadcom firmware -# ========================================================================== -check_firmware() { - echo "" - log_info "Checking for missing Bluetooth firmware..." +# shellcheck source=lib/bt_adapter.sh +source "$SCRIPT_DIR/lib/bt_adapter.sh" +# shellcheck source=lib/bt_pairing.sh +source "$SCRIPT_DIR/lib/bt_pairing.sh" +# shellcheck source=lib/bt_audio.sh +source "$SCRIPT_DIR/lib/bt_audio.sh" +# shellcheck source=lib/bt_report.sh +source "$SCRIPT_DIR/lib/bt_report.sh" - 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 diff --git a/linux_configuration/scripts/single_use/fixes/lib/bt_adapter.sh b/linux_configuration/scripts/single_use/fixes/lib/bt_adapter.sh new file mode 100755 index 00000000..0cf0211d --- /dev/null +++ b/linux_configuration/scripts/single_use/fixes/lib/bt_adapter.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# Firmware download and stuck-adapter recovery. +# +# Sourced by fix_bluetooth.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# ========================================================================== +# 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 +} diff --git a/linux_configuration/scripts/single_use/fixes/lib/bt_audio.sh b/linux_configuration/scripts/single_use/fixes/lib/bt_audio.sh new file mode 100755 index 00000000..378d3faa --- /dev/null +++ b/linux_configuration/scripts/single_use/fixes/lib/bt_audio.sh @@ -0,0 +1,214 @@ +#!/bin/bash +# PipeWire health, device connection and audio profile. +# +# Sourced by fix_bluetooth.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# ========================================================================== +# 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 +} diff --git a/linux_configuration/scripts/single_use/fixes/lib/bt_pairing.sh b/linux_configuration/scripts/single_use/fixes/lib/bt_pairing.sh new file mode 100755 index 00000000..6791893b --- /dev/null +++ b/linux_configuration/scripts/single_use/fixes/lib/bt_pairing.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# Stale pairing removal, service restart and USB autosuspend. +# +# Sourced by fix_bluetooth.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# ========================================================================== +# 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 +} diff --git a/linux_configuration/scripts/single_use/fixes/lib/bt_report.sh b/linux_configuration/scripts/single_use/fixes/lib/bt_report.sh new file mode 100755 index 00000000..acb6fd49 --- /dev/null +++ b/linux_configuration/scripts/single_use/fixes/lib/bt_report.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# User instructions and the diagnostics dump. +# +# Sourced by fix_bluetooth.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# ========================================================================== +# 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 +}