My Bose QuietComfort headphones kept powering themselves off while I was using them. A full shutdown, so a button press and a reconnect every time. A few times an hour.
I didn’t work this out myself. I handed the symptom to Claude Code running Opus 5, let it dig around the machine, and it came back with the cause and the fix. More on what that was worth at the end.
One card, two buses
The machine has a Realtek RTL8822CE, a combo WiFi and Bluetooth card. I’d assumed this a single device, it isn’t, each function sits on a completely different bus:
| Function | Bus | Driver | ID |
|---|---|---|---|
| WiFi | PCIe | rtw88_8822ce | 10ec:c822 |
| Bluetooth | internal USB | btusb + btrtl | 0bda:b00c |
The Bluetooth side is a USB device hanging off an internal port. It shows up under /sys/bus/usb/devices/3-10 and reports removable: fixed, so it’s soldered down and always there. It’s also subject to USB runtime power management, independent of WiFi.
The controller was asleep most of the time
/sys/bus/usb/devices/3-10/power/control = auto
/sys/bus/usb/devices/3-10/power/autosuspend_delay_ms = 2000
/sys/module/btusb/parameters/enable_autosuspend = Y
Two seconds idle and the radio powers down. The cumulative counters show what that adds up to over an uptime:
runtime_suspended_time: 16243761 ms (~4.5 hours)
runtime_active_time: 805499 ms (~13.4 minutes)
The radio was off about 95% of the time. RTL8822C doesn’t reliably hold an active ACL link across all that suspending and resuming. The link dies, the headphones see no host, and their own auto-off timer finishes the job.
ACL link?
ACL is the asynchronous connection-oriented logical transport, the general purpose data link between two paired devices. It gets created when a device joins a piconet and it carries Link Manager signalling as well as user data.
The name is a leftover:
For historical reasons this is known as the ACL logical transport. (Bluetooth Core Specification 5.4, “Architecture”).
It started life as asynchronous connection-less. The spec now defines it as connection-oriented and kept the acronym anyway.
The part that matters here is the layering. Profiles don’t each get a radio link of their own. A2DP, HFP and PBAP are L2CAP channels multiplexed over the one ACL transport, and A2DP audio streams over it too. The synchronous transports, SCO and eSCO, are the separate case, used for call audio.
So it’s one link with everything stacked on top of it. Lose the transport underneath and every profile goes in the same instant.
Reading the drop
The journal caught it twice, at 15:00:22 and again at 15:06:09:
wireplumber: spa.bluez5.sink.media: connection (.../fd0) terminated unexpectedly
wireplumber: spa.bluez5: Failure in Bluetooth audio transport .../fd0
bluetoothd: ext_io_disconnected() ... Hands-Free Voice gateway: not connected (107)
bluetoothd: ext_io_disconnected() ... Phone Book Access: not connected (107)
Hands-Free and Phone Book Access are unrelated profiles. Both dying in the same instant means the whole ACL link vanished. That points at the controller. A codec or profile negotiation problem would take out one of them and leave the other alone.
A few things I checked off along the way:
- The radio itself. Zero
hci0errors in the kernel log. No command timeouts, no resets, no firmware faults. - 2.4 GHz coexistence. WiFi was on channel 36, 5 GHz, so the other half of the combo card wasn’t colliding with Bluetooth.
- Power tuning daemons. No
tlp, nopowertop, nopower-profiles-daemon. The autosuspend came from kernel defaults. - BlueZ config.
/etc/bluetooth/main.confcompletely stock.
Fix one: stop the controller suspending
Writing on to power/control takes the device out of the autosuspend game entirely. The kernel docs put it nicely:
In effect, the kernel pretends the device is never idle. (Linux kernel documentation, “Power Management for USB”).
A udev rule makes that stick across reboots. Matching on the USB ID avoids hardcoding a port path, which can shift between boots:
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="0bda", ATTR{idProduct}=="b00c", TEST=="power/control", ATTR{power/control}="on"
Drop that in /etc/udev/rules.d/50-bluetooth-no-autosuspend.rules, run udevadm control --reload, and echo on into the live device to apply it without a reboot.
Fix two: WirePlumber’s five seconds
There’s a second, independent path to the same symptom. WirePlumber suspends an idle audio node after 5 seconds by default, set in session.suspend-timeout-seconds in suspend-node.lua. Suspending the node tears down the A2DP transport. No transport, and the headphones start counting toward their own auto-off. It also wakes the fragile controller more often than it needs to be woken.
A drop-in at ~/.config/wireplumber/wireplumber.conf.d/51-bluez-no-suspend.conf turns it off for Bluetooth nodes only:
monitor.bluez.rules = [
{
matches = [
{ node.name = "~bluez_output.*" }
{ node.name = "~bluez_input.*" }
]
actions = {
update-props = {
session.suspend-timeout-seconds = 0
}
}
}
]
Restart wireplumber, which drops the current Bluetooth connection, so reconnect afterwards.
Checking it took
cat /sys/bus/usb/devices/3-10/power/control # on
cat /sys/bus/usb/devices/3-10/power/runtime_status # active
cat /sys/bus/usb/devices/3-10/power/runtime_suspended_time # should stop rising
pw-dump | grep -A2 suspend-timeout # 0
runtime_suspended_time froze at 16397474 and runtime_active_time started climbing. No drop events since.
One thing that confused me for a minute: autosuspend_delay_ms still reads 2000 after the fix. That’s harmless. It’s the timer that would apply if autosuspend were on, and control: on takes it out of the picture.
The bit that saved me a day
Everything above took about five minutes. Left to myself it would have taken most of the day, and I’m not certain I’d have got there at all.
My search terms would have been the wrong ones. “Bose headphones disconnect Linux” gets you a wall of A2DP codec advice, LDAC versus SBC-XQ, pairing resets, people telling you to delete /var/lib/bluetooth and start again. Nothing about USB runtime power management, because from where I sat there was no reason to think a Bluetooth problem was a USB problem. That leap needed someone to notice the combo card splits across two buses.
Claude Code did the mechanical part fast. It walked /sys/bus/usb/devices to find the adapter, read the runtime counters, pulled the drop events out of the journal, checked the WiFi channel to rule out 2.4 GHz coexistence, checked for a tuning daemon, read main.conf to confirm it was stock. A dozen shell commands, each one trivial. Knowing which dozen to run, and what a 95% suspended ratio implies about a Realtek controller, is the whole job.
The ruled-out list above is the part I value most. Half of debugging is closing doors, and a tired human closes them one browser tab at a time.
It isn’t magic, and I checked its work. The counters, the log lines and the pw-dump output are all things I confirmed myself, and I’d not publish a udev rule I hadn’t watched take effect on the running machine. It also had real evidence off my actual laptop to go at. Given the same symptom and no shell access, it would have handed me the same codec advice as everyone else.
That’s the shift for me. It can look, cheaply, at fifteen things I’d never have thought to check, and it can do it in the time it takes me to make a coffee.
What I took from it
Both halves of this were power saving working exactly as designed. Nothing was broken. USB autosuspend assumes a device can suspend and resume cleanly, and this controller can’t quite manage it with a live audio link. WirePlumber assumes an idle sink is one nobody wants. Fair enough, until the sink is a pair of headphones with its own opinion about idleness.
The cost is a bit of battery, on the laptop and the headphones both. That’s the price of a link that stays up.