Skip to content

Fix playback device on multipoint - #57

Merged
mos9527 merged 2 commits into
mos9527:v1-compatfrom
jkolo:multipoint-management
Aug 21, 2026
Merged

Fix playback device on multipoint#57
mos9527 merged 2 commits into
mos9527:v1-compatfrom
jkolo:multipoint-management

Conversation

@jkolo

@jkolo jkolo commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Adds Sound Connect's "Fixing playback device" to the multipoint device list: it pins audio to the current playback device so the headphones stop handing it over to the other connected device. Switching the playback device already worked on this branch; fixing it did not.

Protocol

SOURCE_SWITCH_CONTROL's param byte carries the state, and it is inverted against the user-facing feature: the wire value is 1 while switching is free and 0 once the device is fixed. I would have guessed the opposite from the naming, so it is worth flagging for review.

Established on a WF-1000XM5 (fw 6.1.0):

  1. read the byte with nothing fixed → 1
  2. "Fixing playback device" in Sound Connect → 0
  3. "Unfixing" → 1
  4. then drove it from this client and confirmed the padlock appears next to the device in the official app

No Protocol*.hpp or tooling/ida/ changes — every struct needed already existed.

Failure reporting

SourceSwitchControlResult is surfaced through a new mdrHeadphonesGetPlaybackDeviceFixResult. The headphones refuse to fix while a call is in progress or while the voice assistant holds priority (MULTIPOINT_FIXATION_FAILED_CALL / _VA in the app), and without this the button silently does nothing. Caught live: the refusal in the added capture is a real FAIL_CALLING from a phone call.

Drive-by fix

mdrHeadphonesGetFeature pinned its upper bound to MDR_FEATURE_SAFE_LISTENING, so any newly added MDR_FEATURE_* read back as MDR_RESULT_ERROR_INVALID_ARGUMENT. This silently hid the new button until I tested the built client.

ABI

Additive only — new feature id, new enum, two new functions, no struct layout change — so MDR_ABI_VERSION is left at 1. Happy to bump it if you would rather every version change be visible.

Tests

Two rx packets added to tests/WF-1000XM5-6.1.0/ (same model and firmware as the existing capture): the not-fixed param and the FAIL_CALLING refusal. Neither carries an address or device name. Note there is no captured frame for the fixed state — every fix attempt during the capture session was refused because a call was active.

Not covered

  • MUSIC_HAND_OVER_SETTING is left alone; WF-1000XM5 does not advertise it, so I had no way to test it.
  • Separately, roughly one connect in three aborts with Malformed payload right after the Sound Connect app releases the link. It reproduces on unmodified receive-path code, so it is not from this change — I can open an issue with logs if useful.

Disclosure

Written with Claude Code (Claude Fable 5); see the Co-Authored-By trailer on the commit. I have reviewed the change and am responsible for it. Draft because the three-button row layout still needs a look on a smaller window.

@mos9527
mos9527 requested review from Amrsatrio and mos9527 August 11, 2026 13:46
@mos9527

mos9527 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Thanks for the PR. I did not expect one to arrive before the refactor to completely land but oh well, new features are always welcome :D

What's also surprising is this feature itself - took me a while to find in the offical app here:

image

...So by 'fixing' it's a literal translation, likely from Japanese. This is quite confusing without context, maybe we don't have to follow their naming for the export here as this API. IMO SourceSwitchControl fits better - this is of course up to you though.

And sorry about the CI. f64c810 renamed a few names which you should change to reflect. See also f64c810#diff-5df8f943c57ef06245f1073e0a7d9d78665ded5d1bc134309a2847075b957d77L138

Linking to related protocol structs:

// THMSGV2T2PeripheralSetParam
struct PeripheralSetParam
{
// CODEGEN EnumRange Command::PERI_SET_PARAM
Command command{Command::PERI_SET_PARAM}; // 0x0
// CODEGEN Ignore OUT_OF_RANGE is expected
PeripheralInquiredType inquiredType{PeripheralInquiredType::PAIRING_DEVICE_MANAGEMENT_CLASSIC_BT}; // 0x1
MDR_DEFINE_TRIVIAL_SERIALIZATION(PeripheralSetParam);
};
// THMSGV2T2PeripheralSetParamMusicHandOverSetting
struct PeripheralSetParamMusicHandOverSetting
{
// CODEGEN EnumRange Command::PERI_SET_PARAM
Command command{Command::PERI_SET_PARAM}; // 0x0
// CODEGEN EnumRange PeripheralInquiredType::MUSIC_HAND_OVER_SETTING
PeripheralInquiredType inquiredType{PeripheralInquiredType::MUSIC_HAND_OVER_SETTING}; // 0x1
UInt8 on{}; // 0x2
MDR_DEFINE_TRIVIAL_SERIALIZATION(PeripheralSetParamMusicHandOverSetting);
};
// THMSGV2T2PeripheralSetParamSourceSwitchControl
struct PeripheralSetParamSourceSwitchControl
{
// CODEGEN EnumRange Command::PERI_SET_PARAM
Command command{Command::PERI_SET_PARAM}; // 0x0
// CODEGEN EnumRange PeripheralInquiredType::SOURCE_SWITCH_CONTROL
PeripheralInquiredType inquiredType{PeripheralInquiredType::SOURCE_SWITCH_CONTROL}; // 0x1
UInt8 value{}; // 0x2
MDR_DEFINE_TRIVIAL_SERIALIZATION(PeripheralSetParamSourceSwitchControl);
};

I'll be getting back to you once I got the time to fully review this PR.

Comment thread libmdr/include/mdr-c/Headphones.h
Comment thread libmdr/src/Details.hpp Outdated
Comment thread libmdr/src/Headphones.cpp
Comment thread libmdr/src/HeadphonesV2.cpp Outdated
Comment thread libmdr/src/HeadphonesV2.cpp Outdated
Comment thread libmdr/src/HeadphonesV2T2.cpp Outdated
@mos9527
mos9527 marked this pull request as ready for review August 13, 2026 11:00
@mos9527

mos9527 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

That's the review on my part. The changes are small enough, too - if you don't have too much time to make the changes let me know and I'll handle it for you.

Cheers and thanks again for the output.

jkolo and others added 2 commits August 21, 2026 01:47
Sound Connect's "Fixing playback device" pins audio to the current playback
device so the headphones stop handing it over to the other multipoint device.

SOURCE_SWITCH_CONTROL's param byte carries this, inverted against the feature:
the wire value is 1 while switching is free and 0 once the device is fixed.
Verified on WF-1000XM5 (fw 6.1.0) by toggling the setting in Sound Connect and
reading the byte back, then driving it the other way and confirming the padlock
appears in the official app.

SourceSwitchControlResult is surfaced too - the headphones refuse to fix while a
call is in progress or the voice assistant holds priority, which Sound Connect
reports as a dialog and would otherwise look like a dead button. The added rx
packets are a real capture of the not-fixed state and of a FAIL_CALLING refusal.

mdrHeadphonesGetFeature's upper bound was pinned to the last feature id, so any
newly added MDR_FEATURE_* read back as invalid.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKt3Lk68pt5yr92YV1iTqn
…rsion

Rebased onto the current MDRProperty API (pending()/override() replace
submittedDirty()/commitOneShot(), renamed underneath this branch), and
renamed the public surface as requested in review: MDR_FEATURE_PLAYBACK_DEVICE_FIX
becomes MDR_FEATURE_SOURCE_SWITCH_CONTROL, mdrHeadphonesGet/SetPlaybackDeviceFixed
becomes mdrHeadphonesGet/SetSourceSwitchControl, MDRPlaybackFixResult becomes
MDRSourceSwitchControlResult - matching SOURCE_SWITCH_CONTROL and the packet
debugger instead of translating Sound Connect's Japanese-derived label.

libmdr now tracks the wire value directly (1 = switching free) instead of
mangling it into a "fixed" flag; the one inversion left lives in Client.cpp,
where "Fixing playback device" is a UI concept. Also dropped two MDR_LOG_DEBUG
calls in the SOURCE_SWITCH_CONTROL notify/param handlers, matching the other
protocol handlers, which don't log there either.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy2qqZHg5iaikSzDSNMT9o
@jkolo
jkolo force-pushed the multipoint-management branch from 93c9941 to 57c5f9b Compare August 21, 2026 00:01
@jkolo

jkolo commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Sorry for the silence - took the offer and pushed the changes myself instead.

Rebased onto v1-compat (ae71d32), fixed the pending()/override() build break, and renamed the public surface: MDR_FEATURE_PLAYBACK_DEVICE_FIX -> MDR_FEATURE_SOURCE_SWITCH_CONTROL, mdrHeadphonesGet/SetPlaybackDeviceFixed -> mdrHeadphonesGet/SetSourceSwitchControl, MDRPlaybackFixResult -> MDRSourceSwitchControlResult. libmdr now tracks the wire value as-is (1 = switching free); the one inversion left lives in Client.cpp where "Fixing playback device" is the UI concept. Replies inline on each thread.

This is a public ABI rename - MDR_ABI_VERSION is still 1u and this hasn't shipped, so I didn't bump it, but flagging in case you want to.

CI on the push is stuck on action_required - looks like it needs your approval to run on this fork PR, I don't have admin rights to trigger it myself.

Capture for the new test packets is still outstanding - recording a full session on the WF-1000XM5 and will push it separately once I have the hardware in front of me, replacing the two loose files currently in tests/WF-1000XM5-6.1.0/.

@mos9527

mos9527 commented Aug 21, 2026

Copy link
Copy Markdown
Owner

LGTM. Thanks a lot!

@mos9527
mos9527 merged commit 67888dd into mos9527:v1-compat Aug 21, 2026
5 checks passed
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.

2 participants