Skip to content

web: Support BLE (GATT) advertisement for LE Audio enabled connections#44

Open
mos9527 wants to merge 2 commits into
rewritefrom
web-ble
Open

web: Support BLE (GATT) advertisement for LE Audio enabled connections#44
mos9527 wants to merge 2 commits into
rewritefrom
web-ble

Conversation

@mos9527
Copy link
Copy Markdown
Owner

@mos9527 mos9527 commented Apr 21, 2026

Work in progress. Browser support is once again very spotty, thanks to it being mostly a Chromium exclusive

Still testing with the setup I had in #43 (comment)

Does _not_ work yet. When prompted, no device actually show up despite the correct UUIDs being provided.
@mos9527
Copy link
Copy Markdown
Owner Author

mos9527 commented Apr 25, 2026

Sigh.

Closed as not implementable with the current, (((secure))) state of Web Bluetooth API.

It cannot open an OS-paired BLE device by address or by known GATT UUID. Chromium must first discover the device via GAP advertising in its chooser, and with at least the XM6 does not appear to advertise in a way that makes it visible there.

Forget about BLE. Half-assed RFCOMM support is all we got for the Web platform.

@mos9527 mos9527 closed this Apr 25, 2026
@mos9527 mos9527 deleted the web-ble branch April 25, 2026 14:02
@morgenstern09
Copy link
Copy Markdown

morgenstern09 commented Apr 25, 2026

WH-1000XM5/XM6 and WF variants will be discoverable in the Chromium device chooser, though I think that the device should be already paired first (doesn't matter if it's paired via classic BT).

Try this, it works via BLE: https://moderat.ddns.net/

@mos9527 mos9527 restored the web-ble branch April 25, 2026 15:59
@mos9527
Copy link
Copy Markdown
Owner Author

mos9527 commented Apr 25, 2026

@morgenstern09 Interesting! So we got an actual self-contained flasher now?
Poked around a bit. Seems like this is how they'd connect to the device via GATT.

...
document[_0x28523b(0x222)](_0x28523b(0x232))[_0x28523b(0x235)] = async () => {
            const _0x214de3 = _0x28523b;
            try {
                log(_0x214de3(0x22a)),
                btDevice = await navigator['bluetooth']['requestDevice']({
                    'filters': [{
                        'services': [SVC_UUID]
                    }],
                    'optionalServices': [SVC_UUID]
                }),
 ...

With SVC_UUID being:

 const SVC_UUID = _0x28523b(0x1c7)
          , TX_UUID = 'bfd869fa-a3f2-4c2f-bcff-3eb1ec80cead'
          , RX_UUID = '2a6b6575-faf6-418c-923f-ccd63a56d955'
// Where..
_0x28523b(0x1c7) == 'dc405470-a351-4a59-97d8-2e2e3b207fbb'

I knew I saw this from the RACE protocol expolit - so this is what they'd use to flash?

And for reference, this is how we'd handle it:

// Prompt the user to pick a device that advertises the target service.
// Must be called synchronously from a user-gesture callstack (we are).
console.log(`Requesting device ${uuid}`);
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [uuid] }]
});
navigator.em_js_ble_device = device;
navigator.ble_set_last_error(`Connecting GATT on "${device.name || device.id}"`);

...with the uuid being:

#define MDR_BLE_SERVICE_UUID_TANDEM_OVER_BLE_HPC "5B833E20-6BC7-4802-8E9A-723CECA4BD8F"

Quite unfortunately while the Service UUID is being boardcasted (seen in GATT Browser from my mobile):
image

The one that's used for the MDR protocol seemingly isn't. Also curiously with the Windows impl by @kdpkke I got a different MAC address for the device

[BLE] Device #0: name="WH-1000XM6" id="BluetoothLE#BluetoothLE03:22:56:77:03:15-58:18:62:36:82:1e"
[BLE]   BLE address: 58:18:62:36:82:1E
[BLE] GetDevicesList returning 1 BLE device(s)
[BLE] Connect called: mac=58:18:62:36:82:1E serviceUUID=5B833E20-6BC7-4802-8E9A-723CECA4BD8F
[BLE] Connect thread started (MTA), addr=0x58186236821E
[BLE] BLE device opened: WH-1000XM6
[BLE] Looking for GATT service: 5B833E20-6BC7-4802-8E9A-723CECA4BD8F
[BLE] GetGattServicesAsync status: 0, count: 18
[BLE]   Found service: 00001800-0000-1000-8000-00805F9B34FB
[BLE]   Found service: 00001801-0000-1000-8000-00805F9B34FB
[BLE]   Found service: 5B833E20-6BC7-4802-8E9A-723CECA4BD8F
[BLE]   -> Matched target service!

Quite confusing, I'm still not sure if we can have MDR/TANDEM over BLE in Web yet. Cc @Amrsatrio @kdpkke - please check this out if you'd like to take a shot as well.

Reopening the PR. Thanks @morgenstern09 for the find!

@mos9527 mos9527 reopened this Apr 25, 2026
@morgenstern09
Copy link
Copy Markdown

Yeah, the flashing works, but only for devices that also advertise as BLE, even if they don't support LE audio - for example, the WH-1000XM5.

The flashing flow would be the same for MT2822/WF-1000MX4, but that device doesn't have BLE, so it won't be found with the flasher.

By the way, you can try just connecting your XM5 on the flasher page, it works even from an Android device on Chrome.

@mos9527
Copy link
Copy Markdown
Owner Author

mos9527 commented Apr 25, 2026

By the way, you can try just connecting your XM5 on the flasher page, it works even from an Android device on Chrome.

@morgenstern09 That I've tried. My WH-1000XM6 does show up on my Samsung there too, very cool project! ..Albeit reported as unsupported after connection with a werid, truncated name.

34d7a019a37c3d7df7abf0600bee457d

I think I've seen this back when I was trying to dump FW via modified https://github.com/kmantel/sony-vp-extract (which sadly did not work - reads are returning same, low entropy values that doesn't make sense. Not sure why.)

@morgenstern09
Copy link
Copy Markdown

By the way, you can try just connecting your XM5 on the flasher page, it works even from an Android device on Chrome.

@morgenstern09 That I've tried. My WH-1000XM6 does show up on my Samsung there too, very cool project! ..Albeit reported as unsupported after connection with a werid, truncated name.

34d7a019a37c3d7df7abf0600bee457d I think I've seen this back when I was trying to dump FW via modified https://github.com/kmantel/sony-vp-extract (which sadly did not work - reads are returning same, low entropy values that doesn't make sense. Not sure why.

Ah, I think it's because it expects earbuds, and not headsets.

{E9E064EE-3108-4AA8-861B-046824670AC2}

For earbuds, it will transfer the firmware to one bud (the main bud), then will send a command to that bud to switch roles with the other (partner) bud, then it will also transfer the firmware to that other bud, then will send the command to commit the firmware.

It probably doesn't have the logic to separate between earbuds and headsets yet, and I think that it uses the device's response (MT2833_Earbuds or MT2833_Headset) to differentiate between them.

By the way, not sure if it helps in any way, but here are dumps of the XM5 earbuds, and XM6 headphones' flash: flash_dumps.zip

@mos9527
Copy link
Copy Markdown
Owner Author

mos9527 commented Apr 26, 2026

For earbuds, it will transfer the firmware to one bud (the main bud), then will send a command to that bud to switch roles with the other (partner) bud, then it will also transfer the firmware to that other bud, then will send the command to commit the firmware.

Thanks for the explanation. Though I suppose implementing it for a single unit would be quite feasible too?

BTW the firmware stuff is quite unrelated to this PR - but I'm quite interested in its impl (and perhaps in a not-so-distant future, implement it here as well). @morgenstern09 Think we can move this to a seperate Disccussion if you'd like to share?

By the way, not sure if it helps in any way, but here are dumps of the XM5 earbuds, and XM6 headphones' flash: flash_dumps.zip

As for the flash dump...well, it does contain the complete Airoha AES key/IV pair needed to decrypt the offical FW which I've managed to nab. This is another topic on its own - I've opened a new Disccussion for this here:

Linking to:

@kdpkke
Copy link
Copy Markdown

kdpkke commented May 5, 2026

Hi guys, sorry to answer you late. Took a swing at this on a WH-1000XM6 + Windows 11 using Chrome desktop setup. tldr: the diagnosis matches what you suspected, the chooser side is workable once we change the filter, but the BLE session has a stability ceiling that doesn't seem to be on our side.

Findings on the BLE adv

The XM6 advertises BLE under the name LE_WH-1000XM6 (Sony uses an LE_ prefix in BLE mode), with a Resolvable Private Address (e.g. 4F:22:98:22:8E:7B, top two bits of the first byte are 01, the standard RPA encoding). That explains the MAC discrepancy you saw versus my Windows backend, which gets the resolved identity address (58:18:62:36:82:1E) via the IRK exchanged at classic-pairing time. Web Bluetooth has no way to feed an OS-known address back into the chooser, so we have to go through the chooser regardless.

The key finding from chrome://bluetooth-internals/ is that the Services UUIDs field of the adv payload is empty. The MDR service UUID 5B833E20-6BC7-4802-8E9A-723CECA4BD8F is exposed via GATT enumeration once connected, but not advertised. So requestDevice({ filters: [{ services: [MDR_UUID] }] }) will never match this device.

Code change

In libmdr/src/Platform/Emscripten/PlatformEmscriptenBLE.cpp I replaced the service-filtered requestDevice with acceptAllDevices: true plus the MDR UUID in optionalServices:

const device = await navigator.bluetooth.requestDevice({
    acceptAllDevices: true,
    optionalServices: [uuid]
});

getPrimaryService(uuid) after gatt.connect() still resolves it from the post-connection GATT cache, same path the WinRT backend takes. Kept it product-agnostic rather than filtering on namePrefix: 'LE_' because not every Sony product family follows that prefix and adding more felt brittle.

What I tested

Hardware: WH-1000XM6 on latest firmware, Windows 11, Chrome stable.

BLE path (Web Bluetooth):

  • With the XM6 idle and classic-paired, no live BLE adv reaches the chooser even with acceptAllDevices (the RPA shows up in chrome://bluetooth-internals/ but with Latest RSSI: Unknown, so it's cached only).
  • Power-7s into pairing mode makes the device advertise BLE actively. The chooser then shows LE_WH-1000XM6, gatt.connect() succeeds, and getPrimaryService(MDR_UUID) finds the service.
  • The session drops after ~10s. The headphones stay in pairing mode (the pairing voice prompt keeps playing), so the device side hasn't timed out GAP, but the GATT link is closed. My read is that Web Bluetooth can't bond/encrypt, and Sony's MDR service expects a bonded link or some session keepalive that we never send. Open question whether this is fixable at all from the web side.

Classic path (Web Serial over Bluetooth):

  • On desktop Chrome, the chooser only shows the XM6 while it is in pairing mode (the SDP record likely exposes the Sony custom service only then, outside pairing mode you only get A2DP, which Chromium correctly blocks per the Web Serial blocklist).
  • Connect succeeds and MDR commands work end-to-end: I changed ANC mode to Ambient from the web UI and the headphones reacted. So the protocol layer is fine.
  • The session still drops within seconds, with the headphones still in pairing mode the whole time. The README already calls out that Web Serial over Bluetooth is officially supported on Chrome Android 138+ only, so this is likely a Chromium desktop limitation rather than anything in our code.

Suggested next steps

  1. Land the acceptAllDevices change so the BLE chooser is at least usable. Without it the device is invisible by design on this firmware.
  2. Document in the README that on desktop the BLE path currently requires the headphones to be in pairing mode at connect time, and that the session is short-lived. Long-term the stable web target is probably Chrome Android.
  3. Open question for the BLE 10s drop: is there an MDR-level keepalive (or an explicit INIT we are missing) that would prevent the device from dropping the GATT link? Could be worth a SongPal capture as comparison.

Will keep poking at the keepalive angle, but wanted to dump the diagnosis before more time goes by. Happy to push the one-liner change as a separate PR or fold it into yours, whichever you prefer.

@mos9527
Copy link
Copy Markdown
Owner Author

mos9527 commented May 6, 2026

@kdpkke I suppose it's really a no-go with the current Web Bluetooth implementation then?

As for the Classic path, once connected (outside of pairing mode) it should function. The Android situation would be more or less the same with the Desktop impl, if not moreso limited than the Desktop one.

Web BLE might never land for us, but do feel free to push that change into this PR. Thanks again for your response!

@kdpkke
Copy link
Copy Markdown

kdpkke commented May 6, 2026

I'm not able to push anything in this PR. BTW I tell you the changes and instructions.

Made the change locally in libmdr/src/Platform/Emscripten/PlatformEmscriptenBLE.cpp:

// was:  filters: [{ services: [uuid] }]
const device = await navigator.bluetooth.requestDevice({
    acceptAllDevices: true,
    optionalServices: [uuid]
});

How to test

  1. Hold the WH-1000XM6 power button for 7s to enter pairing mode.
  2. In the web client, click "Classic" and then connect.
  3. Pick WH-1000XM6 from the chooser.

Heads up: the session still drops at around 10s while the device is still in pairing mode. Likely missing bonding or a keepalive on our side, still investigating.

@mos9527
Copy link
Copy Markdown
Owner Author

mos9527 commented May 6, 2026

@kdpkke Pushed b15a2fa

I'm not able to push anything in this PR...

Making PRs to the web-ble branch can work. Though I assume that's not really necessary given the current inoperable state of things regarding GATT discovery.

...Hold the WH-1000XM6 power button for 7s to enter pairing mode

Again, curious how this is necessary (?) for you - RFCOMM SDP service is in fact operable once connected in normal operation mode. Are you perhaps connecting to it while using LE Audio?

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.

3 participants