aboutsummaryrefslogtreecommitdiff
path: root/scripts/zz-bluetooth-resume
blob: 427333981280e6398b7a68ba4fd03900d1b38bfc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/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 restored. 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 its setting is
#      DEVICES_TO_ENABLE_ON_STARTUP: startup, not resume. TLP has no ON_RESUME
#      at all, so the resume edge has no owner. 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 orders it after TLP's own hook, so `tlp resume` has finished
# before this runs.
#
# 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