Skip to content

fix: connect A2DP after pairing and enable JustWorksRepairing for BT headsets#730

Open
carroarmato0 wants to merge 9 commits into
LoveRetro:mainfrom
carroarmato0:fix/bluetooth-headset-disconnect
Open

fix: connect A2DP after pairing and enable JustWorksRepairing for BT headsets#730
carroarmato0 wants to merge 9 commits into
LoveRetro:mainfrom
carroarmato0:fix/bluetooth-headset-disconnect

Conversation

@carroarmato0
Copy link
Copy Markdown

@carroarmato0 carroarmato0 commented May 19, 2026

Summary

  • PLAT_bluetoothPair() now calls bluetoothctl connect after bluetoothctl pair so the A2DP audio profile is opened immediately after bonding. Without this, earbuds time out (~2 s) waiting for the host to initiate the audio channel and disconnect themselves (BlueZ reason 2 = remote terminated).
  • bt_init.sh now sets JustWorksRepairing = always in main.conf before starting bluetoothd. The XRadio BT controller firmware generates link keys with store_hint=0, so BlueZ never persists the key to disk. After a sleep/wake cycle the key is gone; with JustWorksRepairing=never (the BlueZ default) earbuds can't reconnect without the user re-pairing manually. Setting it to always lets the earbuds re-initiate the bond from their side on wake.

Fixes #467 (no sound after sleep when using BT headphones).

Root cause

The XRadio chip used in TrimUI devices sets store_hint=0 in the MGMT NEW_LINK_KEY event. BlueZ respects this flag and skips writing [LinkKey] to the device info file, so the device is never considered Paired=true. This means:

  1. After pairing, nothing triggers an A2DP connection → earbuds disconnect in ~2 s.
  2. After sleep/wake, the controller's volatile key memory is cleared and BlueZ rejects reconnect attempts from the earbuds.

Files changed

File Change
workspace/all/common/generic_bt.c Add bluetoothctl connect after bluetoothctl pair in PLAT_bluetoothPair()
skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh Patch main.conf to JustWorksRepairing=always before bluetoothd starts
skeleton/SYSTEM/tg5050/etc/bluetooth/bt_init.sh Same patch for tg5050

I've tested this using my Raycon Earbuds and seems to work reliably. Before this fix the earbuds would simply not connect (or get disconnected immediately)

carroarmato0 and others added 2 commits May 19, 2026 14:04
…dsets

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 LoveRetro#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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@carroarmato0 carroarmato0 marked this pull request as draft May 19, 2026 13:19
@carroarmato0 carroarmato0 marked this pull request as ready for review May 19, 2026 13:19
carroarmato0 and others added 2 commits May 19, 2026 17:42
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 <noreply@anthropic.com>
…witch

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 <noreply@anthropic.com>
Comment thread workspace/all/minarch/ma_audio.c Outdated
@frysee
Copy link
Copy Markdown
Member

frysee commented May 21, 2026

I'm not sure this is working as expected. BT is always a bit of a clusterfuck, but at least with my XM-6s I do not get automatic reconnect after sleep with this patch.

@carroarmato0
Copy link
Copy Markdown
Author

carroarmato0 commented May 21, 2026

I'm not sure this is working as expected. BT is always a bit of a clusterfuck, but at least with my XM-6s I do not get automatic reconnect after sleep with this patch.

I've recently come in possession of a Smart Pro thx for a benefactor. I'll perform some extra tests and see what comes up. 😄

@carroarmato0
Copy link
Copy Markdown
Author

As I mentioned yesterday on Discord, I started fresh without this PR:

  • Brick: unable to pair
  • Smart Pro: unable to pair

After reapplying my PR: both devices were able to pair with the Raycon earbuds.

On both devices, while playing a game (GB game through Gambatte), the sound works through BLE.
I put the device in sleep mode, wait a few minutes, and then turned it back on, the earbuds did not disconnect, and the sound continued to resume when the game was resumed.

When I disconnect the earbuds while the game is still on, I can see visually on the both devices that the disconnect happened (the volume bar appears, and shows the regular AMP symbol rather than bluetooth).
However, the game does not switch to the internal speakers unless the emulator is restarted.

When I turn the ear buds back on, they stay in pairing mode. They do not attempt to reconnect with the device, and the device does not attempt to reconnect as well. Which I think makes sense because when the device disconnects, it's removed from the cache code that I introduced. Maybe I should look into that.

@carroarmato0
Copy link
Copy Markdown
Author

carroarmato0 commented May 22, 2026

Two remaining issues found during testing that are out of scope for this PR:

Audio doesn't switch back to internal speakers mid-game

The disconnect is detected correctly (the volume bar updates to show the amp icon), so audiomon is doing its thing. The problem is that minarch opens the ALSA device at startup and holds onto it — when audiomon rewrites .asoundrc and flips the sink, minarch has no idea and keeps writing to the old device. You'd need minarch to either poll for audio config changes or handle a signal to reinitialize its audio output. Not something we should tackle here.

Earbuds don't reconnect after manual power-off/on

Sleep/resume works because bt_init.sh restarts bluetoothd on wake, which sets up the JustWorksRepairing context fresh and lets the earbuds re-initiate pairing from their side. Manual power cycle is different — bluetoothd stays running, the earbuds come back up and wait for the device to reach out, time out, and fall into fresh pairing mode instead of reconnecting. The device never proactively tries to connect to paired audio devices. Fixing this would need something like a background bluetoothctl monitor that watches for a trusted device to reappear and calls bluetoothctl connect <addr> for it.

Both are worth tracking as follow-up issues, but the current PR does it's job to pair successfully with my ear buds.
I think in the case of XM-6s because they operate differently, so additional patches might be needed

@carroarmato0
Copy link
Copy Markdown
Author

@frysee what's in your /etc/bluetooth/main.conf and which device are you on?

@frysee
Copy link
Copy Markdown
Member

frysee commented May 22, 2026

@frysee what's in your /etc/bluetooth/main.conf and which device are you on?

Brick, and the defaults.

@carroarmato0
Copy link
Copy Markdown
Author

Brick, and the defaults.

Ok, so same chip (XRadio), same device, patch applied, pairing works, but sleep/resume reconnect fails specifically with the XM-6s.

According to Jean-Claude:

The most likely explanation is that Sony XM-6s simply won't do JustWorks re-pairing after authentication fails. Here's what probably happens:

  1. Device wakes from sleep → bluetoothd restarts → volatile key memory cleared
  2. XM-6s come back and present their stored link key
  3. BlueZ rejects it (no matching key)
  4. Sony firmware treats this as a security failure and refuses to fall back to JustWorks — they either wait for user-initiated pairing or give up entirely

  The Raycon earbuds don't care — when they get rejected they just re-initiate as JustWorks and BlueZ accepts it via JustWorksRepairing=always. Sony XM-6s are premium headphones built with security in mind; they're designed to not silently re-pair without user intent.

  The actual fix for XM-6s (and likely other quality headphones) would be to preserve the link key across sleep despite store_hint=0. BlueZ by default respects that hint from the XRadio firmware and refuses to persist the key to disk. If we could override that,  essentially force BlueZ to store the key anyway — the XM-6s would reconnect fine because authentication would succeed on both sides.

@frysee
Copy link
Copy Markdown
Member

frysee commented May 22, 2026

BT just is a joy to work on, everything is so consistent.

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 <noreply@anthropic.com>
@carroarmato0
Copy link
Copy Markdown
Author

@frysee I submitted another patch. It looks like the device does not attempt itself to reconnect.
It looks like BlueZ loads all pairing data after restart but never initiates outgoing connections, it passively waits. Raycon-style earbuds reach out from their side (and JustWorks catches any key mismatch). Sony XM-6s and other premium headphones either expect the host to connect first, or give up rather than falling back to JustWorks re-pairing.

So give this a try, hopefully works for you

Comment thread skeleton/SYSTEM/tg5040/etc/bluetooth/bt_init.sh
carroarmato0 and others added 2 commits May 22, 2026 12:31
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 <noreply@anthropic.com>
The function was placed before log() causing 'not declared in this
scope' compile error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
frysee added a commit that referenced this pull request May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No sound after sleep mode when using bt headphones.

2 participants