aboutsummaryrefslogtreecommitdiff
path: root/scripts/zz-bluetooth-resume
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/zz-bluetooth-resume')
-rwxr-xr-xscripts/zz-bluetooth-resume95
1 files changed, 95 insertions, 0 deletions
diff --git a/scripts/zz-bluetooth-resume b/scripts/zz-bluetooth-resume
new file mode 100755
index 0000000..4b51dae
--- /dev/null
+++ b/scripts/zz-bluetooth-resume
@@ -0,0 +1,95 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-3.0-or-later
+# zz-bluetooth-resume - put bluetooth back after a sleep cycle.
+#
+# A systemd-sleep hook. Two things break bluetooth across sleep on a TLP
+# laptop, and nothing else on the machine fixes either one.
+#
+# 1. The rfkill soft-block is not cleared. systemd-rfkill would do it, and
+# it is masked here deliberately -- it fights TLP's radio handling, so
+# configure_tlp_power masks it and TLP owns radios instead. TLP's own
+# sleep hook runs `tlp resume`, but that only puts bluetooth back where
+# it was at suspend and applies no policy: its enable setting is
+# DEVICES_TO_ENABLE_ON_STARTUP, startup not resume, and TLP has no
+# ON_RESUME at all. So a block present at suspend stays, and so does one
+# that a controller re-probed after that replay comes up with. WiFi
+# survives only because NetworkManager unblocks itself; bluetooth has no
+# equivalent.
+#
+# 2. The controller comes back wedged from a hibernate. It reports powered
+# and unblocked while scanning finds nothing whatever -- zero devices
+# where the same room gave seventeen a minute later -- and bluetoothd
+# logs "Failed to set mode" and "Failed to add device <mac>" at the
+# instant of resume. Reloading btusb clears it.
+#
+# Both observed on velox 2026-08-21, on the first suspend-then-hibernate cycle
+# after hibernate was switched back on. The second symptom is why unblocking
+# alone is not enough: rfkill was cleared by hand and scanning still returned
+# nothing until the driver was reloaded.
+#
+# The hook re-asserts TLP's own declared intent rather than inventing a policy.
+# A machine whose TLP config does not ask for bluetooth keeps it off, which is
+# what stops this from overriding a deliberate block at every wakeup.
+#
+# The zz- prefix buys no ordering: systemd-sleep runs every hook here in
+# parallel (man systemd-sleep), so `tlp resume` may still be running when
+# this starts, and its first act is to replay the rfkill state it saved at
+# suspend. On a hibernate-class wake the settle between unload and load below
+# usually lands the final unblock after that replay. A plain suspend has no
+# settle and unblocks at once, so that race is open. The tell is `rfkill list
+# bluetooth` reading "Soft blocked: yes" after a suspend cycle that was entered
+# with bluetooth already blocked (a hook that never ran reads the same way).
+#
+# Test seams: BTR_RFKILL, BTR_MODPROBE, BTR_TLP_CONF, BTR_TLP_CONF_DIR,
+# BTR_SETTLE (seconds to wait between driver unload and load).
+
+set -u
+
+RFKILL="${BTR_RFKILL:-rfkill}"
+MODPROBE="${BTR_MODPROBE:-modprobe}"
+TLP_CONF="${BTR_TLP_CONF:-/etc/tlp.conf}"
+TLP_CONF_DIR="${BTR_TLP_CONF_DIR:-/etc/tlp.d}"
+SETTLE="${BTR_SETTLE:-1}"
+
+# post only. The pre phase has nothing to do, and acting there would fight the
+# suspend it is about to run.
+[ "${1:-}" = "post" ] || exit 0
+
+# Does TLP ask for bluetooth on this machine? Comments are stripped first, so a
+# commented-out example in the stock config cannot be read as a policy. Both
+# the main file and any drop-in count, and the last assignment wins the same
+# way TLP itself resolves them.
+wants_bluetooth() {
+ cat "$TLP_CONF" "$TLP_CONF_DIR"/*.conf 2>/dev/null \
+ | sed 's/#.*//' \
+ | awk -F= '/DEVICES_TO_ENABLE_ON_STARTUP/ { v = $2 } END { print v }' \
+ | tr -d '"' \
+ | tr ' ' '\n' \
+ | grep -qx "bluetooth"
+}
+
+wants_bluetooth || exit 0
+
+# The wedge follows a hibernate, which reinitialises the controller from a
+# saved image. A plain suspend brings USB back intact, so reloading there would
+# tear down a working adapter for nothing.
+#
+# suspend-then-hibernate reports that name whether or not it reached the
+# hibernate stage, so this reloads on a cycle that only suspended. That is the
+# cheap side of the trade: a couple of seconds against an adapter that answers
+# nothing until someone notices and reloads it by hand.
+case "${2:-}" in
+ hibernate|suspend-then-hibernate)
+ "$MODPROBE" -r btusb 2>/dev/null || true
+ [ "$SETTLE" = "0" ] || sleep "$SETTLE"
+ "$MODPROBE" btusb 2>/dev/null || true
+ ;;
+esac
+
+# After the reload, not before: a freshly loaded btusb can come up soft-blocked
+# and would undo an earlier unblock.
+"$RFKILL" unblock bluetooth 2>/dev/null || true
+
+# Never fail. systemd-sleep logs a failing hook, and that noise outlives the
+# cause it describes; nothing here is worth alarming a resume over.
+exit 0