#!/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 " 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