From 0624be7f5ff460c0ac700ee6048f48154fec97af Mon Sep 17 00:00:00 2001 From: Christophe Vanlancker Date: Tue, 19 May 2026 14:04:02 +0200 Subject: [PATCH 1/9] fix: connect A2DP after pairing and enable JustWorksRepairing for headsets The XRadio BT controller firmware generates link keys with store_hint=0, meaning BlueZ never persists the key to disk. This caused two symptoms: 1. Earbuds disconnect ~2s after pairing: after the BR/EDR bond completes, nothing initiates the A2DP audio profile connection. The earbuds time out waiting for the host to open the audio channel and disconnect (reason 2 = remote terminated). Fixed by calling `bluetoothctl connect` immediately after `bluetoothctl pair` in PLAT_bluetoothPair(). 2. No sound after sleep/wake (fixes #467): bt_init.sh stop/start clears the controller's volatile key memory. On wake, earbuds try to reconnect but BlueZ rejects them (no stored key, JustWorksRepairing=never default). Fixed by patching main.conf to set JustWorksRepairing=always before bluetoothd starts, allowing earbuds to re-initiate the bond from their side without user interaction. Co-Authored-By: Claude Sonnet 4.6 --- skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh | 8 ++++++++ skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh | 8 ++++++++ workspace/all/common/generic_bt.c | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh b/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh index 3b3bda444..180e21f01 100755 --- a/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh +++ b/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh @@ -44,6 +44,14 @@ start_bt() { start_hci_attach fi + # Allow headsets to auto-reconnect without user re-pairing. + # XRadio BT firmware sets store_hint=0, so link keys are never + # persisted; JustWorksRepairing=always lets earbuds re-initiate + # the bond from their side after a reboot. + if ! grep -q 'JustWorksRepairing = always' /etc/bluetooth/main.conf 2>/dev/null; then + sed -i 's/#JustWorksRepairing = never/JustWorksRepairing = always/' /etc/bluetooth/main.conf 2>/dev/null + fi + # Start bluetooth daemon if not running d=`ps | grep bluetoothd | grep -v grep` [ -z "$d" ] && { diff --git a/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh b/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh index e7a49309c..a94dcd428 100755 --- a/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh +++ b/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh @@ -52,6 +52,14 @@ start_bt() { start_hci_attach fi + # Allow headsets to auto-reconnect without user re-pairing. + # XRadio BT firmware sets store_hint=0, so link keys are never + # persisted; JustWorksRepairing=always lets earbuds re-initiate + # the bond from their side after a reboot. + if ! grep -q 'JustWorksRepairing = always' /etc/bluetooth/main.conf 2>/dev/null; then + sed -i 's/#JustWorksRepairing = never/JustWorksRepairing = always/' /etc/bluetooth/main.conf 2>/dev/null + fi + # Start bluetooth daemon if not running d=`ps | grep bluetoothd | grep -v grep` [ -z "$d" ] && { diff --git a/workspace/all/common/generic_bt.c b/workspace/all/common/generic_bt.c index 1f017f4e2..71fac47a8 100644 --- a/workspace/all/common/generic_bt.c +++ b/workspace/all/common/generic_bt.c @@ -479,6 +479,12 @@ void PLAT_bluetoothPair(char *addr) { } } + // Connect after pairing to initiate A2DP audio profile. + // Some headsets (store_hint=0 controllers) disconnect ~2s after + // pairing if the host doesn't open the audio channel first. + snprintf(cmd, sizeof(cmd), "bluetoothctl connect %s 2>/dev/null", addr); + system(cmd); + // Remove from discovered list since it's now paired bt_remove_discovered_device(addr); } From 2afb3086a588c1b5c5e9e4d9d6ca00e361e92cc8 Mon Sep 17 00:00:00 2001 From: Christophe Vanlancker Date: Tue, 19 May 2026 14:31:13 +0200 Subject: [PATCH 2/9] fix: correct tg5050 JustWorksRepairing comment (AIC8800, not XRadio) tg5050 (TrimUI Smart Pro S) uses an AIC8800 BT chip, not XRadio. Remove the chip-specific attribution from the comment so it accurately describes the symptom rather than incorrectly referencing XRadio. Co-Authored-By: Claude Sonnet 4.6 --- skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh b/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh index a94dcd428..dc617c2f4 100755 --- a/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh +++ b/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh @@ -53,9 +53,9 @@ start_bt() { fi # Allow headsets to auto-reconnect without user re-pairing. - # XRadio BT firmware sets store_hint=0, so link keys are never - # persisted; JustWorksRepairing=always lets earbuds re-initiate - # the bond from their side after a reboot. + # Some BT controller firmware never persists link keys to disk; + # JustWorksRepairing=always lets earbuds re-initiate the bond + # from their side after a reboot without user interaction. if ! grep -q 'JustWorksRepairing = always' /etc/bluetooth/main.conf 2>/dev/null; then sed -i 's/#JustWorksRepairing = never/JustWorksRepairing = always/' /etc/bluetooth/main.conf 2>/dev/null fi From fc77705bccf2d991b16814cfdecd6bf4fcb525cc Mon Sep 17 00:00:00 2001 From: Christophe Vanlancker Date: Tue, 19 May 2026 17:42:33 +0200 Subject: [PATCH 3/9] fix: cache A2DP MAC to reliably detect BT headset disconnect When earbuds power off abruptly, BlueZ may have already cleared the device's service cache by the time audiomon processes the disconnect signal. The previous hasUUID() call on disconnect would return false in that race, silently skipping the audio switch-back to the internal speaker. Fix by caching the connected A2DP device MAC at connect time and comparing against it on disconnect, avoiding the BlueZ query entirely. Co-Authored-By: Claude Sonnet 4.6 --- workspace/all/audiomon/audiomon.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/workspace/all/audiomon/audiomon.cpp b/workspace/all/audiomon/audiomon.cpp index 6fafd66f7..43b1dd8cc 100644 --- a/workspace/all/audiomon/audiomon.cpp +++ b/workspace/all/audiomon/audiomon.cpp @@ -33,6 +33,7 @@ enum DeviceType { bool use_syslog = false; bool running = true; +static std::string connected_a2dp_mac; void log(const std::string& msg) { if (use_syslog) syslog(LOG_INFO, "%s", msg.c_str()); @@ -203,6 +204,7 @@ void handleDeviceConnected(DBusConnection* conn, const std::string& path) { std::string mac = pathToMac(path); if (hasUUID(conn, path, UUID_A2DP)) { log("Audio device connected: " + mac); + connected_a2dp_mac = mac; writeAudioFile(mac, DEVICE_BLUETOOTH); SetAudioSink(AUDIO_SINK_BLUETOOTH); } else { @@ -212,8 +214,12 @@ void handleDeviceConnected(DBusConnection* conn, const std::string& path) { void handleDeviceDisconnected(DBusConnection* conn, const std::string& path) { std::string mac = pathToMac(path); - if (hasUUID(conn, path, UUID_A2DP)) { + // Use cached MAC rather than querying BlueZ: after an abrupt power-off the + // device's service cache may already be gone, causing hasUUID to return false + // and silently skip the audio switch-back. + if (!connected_a2dp_mac.empty() && mac == connected_a2dp_mac) { log("Audio device disconnected: " + mac); + connected_a2dp_mac.clear(); clearAudioFile(); // TODO: we could maintain a stack here, if USBC was connected before and restore that instead SetAudioSink(AUDIO_SINK_DEFAULT); From f1a7932135bbb57f1c9cacb96afc44efd61c0dec Mon Sep 17 00:00:00 2001 From: Christophe Vanlancker Date: Tue, 19 May 2026 22:56:13 +0200 Subject: [PATCH 4/9] fix: use explicit ALSA device names to bypass config cache on audio switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ALSA caches its PCM configuration. After BT disconnects and .asoundrc is deleted, setting AUDIODEV=default still resolves through the cached config which may still point to bluealsa — causing SDL to open a dead PCM and producing no or garbled audio from the speaker. Fix by naming both endpoints explicitly, matching the same pattern already used for BT (bluealsa): - Speaker: plughw:0 (card 0, with format/rate conversion) - USB DAC: plughw:1 (card 1, with format/rate conversion) - Bluetooth: bluealsa (unchanged) Co-Authored-By: Claude Sonnet 4.6 --- workspace/all/minarch/ma_audio.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/workspace/all/minarch/ma_audio.c b/workspace/all/minarch/ma_audio.c index a8f66e765..69d7fdfc3 100644 --- a/workspace/all/minarch/ma_audio.c +++ b/workspace/all/minarch/ma_audio.c @@ -17,11 +17,14 @@ void Audio_onSinkChanged(int device, int watch_event) { resetAudio = true; - // FIXME: This shouldnt be necessary, alsa should just read .asoundrc for the changed default device. + // ALSA caches its config, so "default" may still resolve to bluealsa after + // .asoundrc is deleted. Name both endpoints explicitly to bypass the cache. if (device == AUDIO_SINK_BLUETOOTH) SDL_setenv("AUDIODEV", "bluealsa", 1); + else if (device == AUDIO_SINK_USBDAC) + SDL_setenv("AUDIODEV", "plughw:1", 1); else - SDL_setenv("AUDIODEV", "default", 1); + SDL_setenv("AUDIODEV", "plughw:0", 1); } void Audio_checkAndResetIfNeeded(void) { From fe081b10cbda1e9ccc652c1025129b9b98e4a2ae Mon Sep 17 00:00:00 2001 From: Christophe Vanlancker Date: Tue, 19 May 2026 23:53:22 +0200 Subject: [PATCH 5/9] Revert "fix: use explicit ALSA device names to bypass config cache on audio switch" This reverts commit f1a7932135bbb57f1c9cacb96afc44efd61c0dec. --- workspace/all/minarch/ma_audio.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/workspace/all/minarch/ma_audio.c b/workspace/all/minarch/ma_audio.c index 69d7fdfc3..a8f66e765 100644 --- a/workspace/all/minarch/ma_audio.c +++ b/workspace/all/minarch/ma_audio.c @@ -17,14 +17,11 @@ void Audio_onSinkChanged(int device, int watch_event) { resetAudio = true; - // ALSA caches its config, so "default" may still resolve to bluealsa after - // .asoundrc is deleted. Name both endpoints explicitly to bypass the cache. + // FIXME: This shouldnt be necessary, alsa should just read .asoundrc for the changed default device. if (device == AUDIO_SINK_BLUETOOTH) SDL_setenv("AUDIODEV", "bluealsa", 1); - else if (device == AUDIO_SINK_USBDAC) - SDL_setenv("AUDIODEV", "plughw:1", 1); else - SDL_setenv("AUDIODEV", "plughw:0", 1); + SDL_setenv("AUDIODEV", "default", 1); } void Audio_checkAndResetIfNeeded(void) { From ce34d34c78a8bbee9c70a021e82ecdfe093e1650 Mon Sep 17 00:00:00 2001 From: Christophe Vanlancker Date: Fri, 22 May 2026 12:08:57 +0200 Subject: [PATCH 6/9] fix: proactively reconnect trusted A2DP devices after BT restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BlueZ loads paired device records on startup but never reaches out to them — it waits for the remote device to initiate. Budget earbuds (Raycon) handle this by connecting from their side; premium headphones (Sony XM-6s, etc.) expect the host to initiate and won't fall back to JustWorks re-pairing when authentication fails. Fix by scanning /var/lib/bluetooth/ after BT stack init and calling bluetoothctl connect for each trusted device advertising the A2DP Sink UUID (0000110b). Runs in background with a 5s delay to avoid blocking startup. Uses stored link keys, so no JustWorks re-pairing is needed. Co-Authored-By: Claude Sonnet 4.6 --- .../SYSTEM/tg5040/etc/bluetooth/bt_init.sh | 17 +++++++++++++++++ .../SYSTEM/tg5050/etc/bluetooth/bt_init.sh | 19 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh b/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh index 180e21f01..a09970472 100755 --- a/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh +++ b/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh @@ -77,6 +77,23 @@ start_bt() { # Set adapter name bluetoothctl system-alias "$DEVICE_NAME" 2>/dev/null + + # Proactively reconnect trusted A2DP audio devices after BT restart. + # Some headphones (e.g. Sony) won't fall back to JustWorks re-pairing + # when authentication fails; they expect the host to initiate using the + # stored link key. Runs in background to avoid blocking startup. + { + sleep 5 + for dev_dir in /var/lib/bluetooth/*/; do + for paired_dir in "${dev_dir}"*/; do + [ -f "${paired_dir}info" ] || continue + grep -q "^Trusted=true" "${paired_dir}info" || continue + grep -q "0000110b" "${paired_dir}info" || continue + mac=$(basename "${paired_dir%/}") + bluetoothctl connect "$mac" >/dev/null 2>&1 + done + done + } & } } diff --git a/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh b/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh index dc617c2f4..402f64d8c 100755 --- a/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh +++ b/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh @@ -85,8 +85,25 @@ start_bt() { # Set adapter name bluetoothctl system-alias "$DEVICE_NAME" 2>/dev/null + + # Proactively reconnect trusted A2DP audio devices after BT restart. + # Some headphones (e.g. Sony) won't fall back to JustWorks re-pairing + # when authentication fails; they expect the host to initiate using the + # stored link key. Runs in background to avoid blocking startup. + { + sleep 5 + for dev_dir in /var/lib/bluetooth/*/; do + for paired_dir in "${dev_dir}"*/; do + [ -f "${paired_dir}info" ] || continue + grep -q "^Trusted=true" "${paired_dir}info" || continue + grep -q "0000110b" "${paired_dir}info" || continue + mac=$(basename "${paired_dir%/}") + bluetoothctl connect "$mac" >/dev/null 2>&1 + done + done + } & } - + } stop_bt() { From fa90e3f5a57721610f08586713b8ea1f552d2f93 Mon Sep 17 00:00:00 2001 From: Christophe Vanlancker Date: Fri, 22 May 2026 12:31:12 +0200 Subject: [PATCH 7/9] fix: harden BT reconnect across all corner cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bt_init.sh: - Remove Trusted=true filter from proactive reconnect loop. The bluetoothctl trust call was only added in this branch, so any device paired on a previous version would be silently skipped. The A2DP Sink UUID (0000110b) is sufficient to identify audio devices. - Use '.*JustWorksRepairing.*' instead of exact '#JustWorksRepairing = never' in sed, so the patch applies regardless of whether the line is commented, already set to a different value, or has different spacing. audiomon.cpp: - Restore connected_a2dp_mac from .asoundrc at startup. audiomon only tracked BT state via D-Bus events, so if it started (or restarted) after a BT device was already connected it would never populate the MAC cache, causing a subsequent abrupt disconnect to go undetected and leaving audio stuck on Bluetooth. .asoundrc already contains the MAC because audiomon wrote it — reading it back on init closes this gap. Co-Authored-By: Claude Sonnet 4.6 --- skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh | 3 +-- skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh | 3 +-- workspace/all/audiomon/audiomon.cpp | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh b/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh index a09970472..a5d89a031 100755 --- a/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh +++ b/skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh @@ -49,7 +49,7 @@ start_bt() { # persisted; JustWorksRepairing=always lets earbuds re-initiate # the bond from their side after a reboot. if ! grep -q 'JustWorksRepairing = always' /etc/bluetooth/main.conf 2>/dev/null; then - sed -i 's/#JustWorksRepairing = never/JustWorksRepairing = always/' /etc/bluetooth/main.conf 2>/dev/null + sed -i 's/.*JustWorksRepairing.*/JustWorksRepairing = always/' /etc/bluetooth/main.conf 2>/dev/null fi # Start bluetooth daemon if not running @@ -87,7 +87,6 @@ start_bt() { for dev_dir in /var/lib/bluetooth/*/; do for paired_dir in "${dev_dir}"*/; do [ -f "${paired_dir}info" ] || continue - grep -q "^Trusted=true" "${paired_dir}info" || continue grep -q "0000110b" "${paired_dir}info" || continue mac=$(basename "${paired_dir%/}") bluetoothctl connect "$mac" >/dev/null 2>&1 diff --git a/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh b/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh index 402f64d8c..20c74dab0 100755 --- a/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh +++ b/skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh @@ -57,7 +57,7 @@ start_bt() { # JustWorksRepairing=always lets earbuds re-initiate the bond # from their side after a reboot without user interaction. if ! grep -q 'JustWorksRepairing = always' /etc/bluetooth/main.conf 2>/dev/null; then - sed -i 's/#JustWorksRepairing = never/JustWorksRepairing = always/' /etc/bluetooth/main.conf 2>/dev/null + sed -i 's/.*JustWorksRepairing.*/JustWorksRepairing = always/' /etc/bluetooth/main.conf 2>/dev/null fi # Start bluetooth daemon if not running @@ -95,7 +95,6 @@ start_bt() { for dev_dir in /var/lib/bluetooth/*/; do for paired_dir in "${dev_dir}"*/; do [ -f "${paired_dir}info" ] || continue - grep -q "^Trusted=true" "${paired_dir}info" || continue grep -q "0000110b" "${paired_dir}info" || continue mac=$(basename "${paired_dir%/}") bluetoothctl connect "$mac" >/dev/null 2>&1 diff --git a/workspace/all/audiomon/audiomon.cpp b/workspace/all/audiomon/audiomon.cpp index 43b1dd8cc..0f5a0f88c 100644 --- a/workspace/all/audiomon/audiomon.cpp +++ b/workspace/all/audiomon/audiomon.cpp @@ -35,6 +35,19 @@ bool use_syslog = false; bool running = true; static std::string connected_a2dp_mac; +static void initBtStateFromAsoundrc() { + std::ifstream f(AUDIO_FILE); + if (!f) return; + std::string content((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + auto pos = content.find("defaults.bluealsa.device \""); + if (pos == std::string::npos) return; + pos += strlen("defaults.bluealsa.device \""); + auto end = content.find("\"", pos); + if (end == std::string::npos) return; + connected_a2dp_mac = content.substr(pos, end - pos); + log("Restored BT state from .asoundrc: " + connected_a2dp_mac); +} + void log(const std::string& msg) { if (use_syslog) syslog(LOG_INFO, "%s", msg.c_str()); else std::cout << msg << std::endl; @@ -291,6 +304,7 @@ int main(int argc, char* argv[]) { InitSettings(); // This will be updated as soon as something connects SetAudioSink(AUDIO_SINK_DEFAULT); + initBtStateFromAsoundrc(); signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); From c7e8813fdae1105f22a372ee83bb19a7d7517a30 Mon Sep 17 00:00:00 2001 From: Christophe Vanlancker Date: Fri, 22 May 2026 12:46:34 +0200 Subject: [PATCH 8/9] fix: move initBtStateFromAsoundrc after log() to fix build The function was placed before log() causing 'not declared in this scope' compile error. Co-Authored-By: Claude Sonnet 4.6 --- workspace/all/audiomon/audiomon.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/workspace/all/audiomon/audiomon.cpp b/workspace/all/audiomon/audiomon.cpp index 0f5a0f88c..7d1b791db 100644 --- a/workspace/all/audiomon/audiomon.cpp +++ b/workspace/all/audiomon/audiomon.cpp @@ -35,6 +35,11 @@ bool use_syslog = false; bool running = true; static std::string connected_a2dp_mac; +void log(const std::string& msg) { + if (use_syslog) syslog(LOG_INFO, "%s", msg.c_str()); + else std::cout << msg << std::endl; +} + static void initBtStateFromAsoundrc() { std::ifstream f(AUDIO_FILE); if (!f) return; @@ -48,11 +53,6 @@ static void initBtStateFromAsoundrc() { log("Restored BT state from .asoundrc: " + connected_a2dp_mac); } -void log(const std::string& msg) { - if (use_syslog) syslog(LOG_INFO, "%s", msg.c_str()); - else std::cout << msg << std::endl; -} - void ensureDirExists(const std::string& path) { mkdir(path.c_str(), 0755); } From 0df05e985bebc1f7211581a8dc6fc030369549c7 Mon Sep 17 00:00:00 2001 From: Christophe Vanlancker Date: Tue, 26 May 2026 19:53:54 +0200 Subject: [PATCH 9/9] add SetAudioSink(AUDIO_SINK_BLUETOOTH) at the end of initBtStateFromAsoundrc() when a MAC is successfully restored Signed-off-by: Christophe Vanlancker --- workspace/all/audiomon/audiomon.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/workspace/all/audiomon/audiomon.cpp b/workspace/all/audiomon/audiomon.cpp index 7d1b791db..79247ce62 100644 --- a/workspace/all/audiomon/audiomon.cpp +++ b/workspace/all/audiomon/audiomon.cpp @@ -51,6 +51,7 @@ static void initBtStateFromAsoundrc() { if (end == std::string::npos) return; connected_a2dp_mac = content.substr(pos, end - pos); log("Restored BT state from .asoundrc: " + connected_a2dp_mac); + SetAudioSink(AUDIO_SINK_BLUETOOTH); } void ensureDirExists(const std::string& path) {