Repository navigation
macOS: multipoint device list, switch playback device and fix it (closes #25) - #28
Merged
AmitRajput-Dev merged 2 commits intoOct 28, 2026
Conversation
Adds Sound Connect's "Switch playback device and fix" for v2 headsets, so playback can be pinned to the Mac (e.g. for calls) and the phone can't take it over. Transport: BluetoothWrapper can now send and match on command table 2 (DATA_MDR_NO2, 0x0e) and ACKs table 2 frames. The tables are separate command spaces - in table 1, 0x30-0x3f are the firmware-update commands - so the multipoint opcodes only ever go out on 0x0e. Discovery (GETs only, v2 only, gated on the init reply advertising table 2): support functions (06 00), device list (36 02 / 36 00), fixed state (36 01), and the MULTIPOINT_SETTING general-setting slot found by name (d0 d1..d4), then its value (d6). Raw replies are kept for "Copy diagnostics". Writes (each refused locally unless discovery saw function 0x31): - switch playback: 3c 01 <address> -> 3d 01 <result> <address> - fix / unfix: 38 01 00 / 01 -> 39 01 <value> <result> "Switch and fix" = unfix if fixed, switch, fix; if the switch is refused it re-fixes the old device. A plain switch unfixes afterwards, since the headset carries "fixed" across a switch. State is re-read after every write. UI: Devices card (connected/paired, playback and lock icons, This Mac tag, Fix/Unfix, per-device switch menu) and a menu-bar item with "Pin Playback to This Mac" whose icon shows the playback device (phone, computer or slot number). The device list is re-read every 10 s so the icon follows changes made from the phone. Opcodes and layouts from mos9527/SonyHeadphonesClient (ProtocolV2T2.hpp) and nullpoint. Verified on a WH-1000XM5 fw 2.5.1: list, switch, fix, unfix, and the FAIL_CALLING refusal (39 01 00 02) during a call.
- Read the multipoint state (device list, fixed state) right after battery/EQ/DSEE instead of after the capability probes, so the playback device appears with the battery rather than after any probe timeouts. - Devices card shows a "Reading devices from the headset..." placeholder until the first read lands, and a spinner in its header during refreshes, switches and fixes. - Move the Devices card up, right after Ambient Sound Control.
Owner
|
Merged via #30. Thank you, multipoint was the most requested feature (#25). A review before merging turned up a few issues, all fixed in the merge:
The menu-bar item now opens a quick-controls panel, and your menu is still there on right-click. Ships in v0.4.0. |
Closed
fredybp
pushed a commit
to fredybp/SonyBridge
that referenced
this pull request
Nov 4, 2026
… fuzzing/hardening Conflicts with AmitRajput-Dev#28 (multipoint) resolved: - Multipoint "Connect to 2 devices": keep AmitRajput-Dev#28's name-based general-settings slot lookup (MULTIPOINT_SETTING in D1..D4) and drop AmitRajput-Dev#7's hard-coded 0xd1 touch-sensor probe; AmitRajput-Dev#7's GadgetBridge write (d8 <slot> 00 <v> + 98 00 06 01) now targets the discovered slot. One set of bridge/model properties. - Bridge: keep AmitRajput-Dev#7's shared_ptr + single serial I/O queue; multipoint runs on it too (its own queue bought nothing behind the connector mutex). Slot search moved after the capability probes so battery/EQ show immediately. - initDevice: v1/v2 init from AmitRajput-Dev#7; table-2 flag only on v2. - Reset _initialized on every new session and release the dead session when the headset drops the link, so a reconnect re-runs the handshake. - Switch/fix decisions read playback/pin state inside the queued write. - Devices card shown only when the headset returned a list or pin state. - BluetoothWrapper: one zero-byte recv guard (AmitRajput-Dev#21 and AmitRajput-Dev#7 both added one). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fredybp
pushed a commit
to fredybp/SonyBridge
that referenced
this pull request
Nov 4, 2026
requestBattery() probed the single-battery sub-type first and returned on any reply, so TWS earbuds never reached the dual probe and L/R stayed unset. Probe order now follows the model name (WF-*/LinkBuds try dual first), with a fallback to the other layout either way. - add DUAL2 (0x01) for the WF-C500/C510/C700N line - keep the per-earbud charging flags instead of discarding them - treat a level of 0 as "not reporting" (docked), not a flat battery - read the charging case (0x0a) - re-read battery on the dynamic poll so it stops going stale (v2 only: 0x22 is POWER_OFF on v1) Sub-types and byte offsets verified against a WF-1000XM5 by docking one earbud and watching byte [4] track it; captured frames are in Constants.h. (cherry picked from commit ff1605d) Conflict resolution on top of AmitRajput-Dev#7/AmitRajput-Dev#28 (by maintainer): - keep AmitRajput-Dev#7's v1 battery branch (10 00); per-earbud probing is v2-only - a 0/0 dual reply is a real answer (both earbuds docked), not "unsupported": rejecting it fell through to DUAL2/single probes that time out, stalling the command queue ~5s on every battery poll - battery re-read runs on the serial command queue (no _dynamicTick race) and model writes are change-gated Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fredybp
pushed a commit
to fredybp/SonyBridge
that referenced
this pull request
Nov 4, 2026
A short audio dropout was enough to desync the response stream for the rest of the session: changing NC/Ambient or an EQ preset reverted a second or two later, and DSEE wouldn't toggle at all. _readMessage() left the parser stranded whenever a chunk arrived garbled or truncated - bytes before the first START_MARKER were concatenated into the message, and a second start marker threw. 60/61/62 are escaped inside a frame, so a start marker unambiguously begins one; resync on it in both cases, and cap the buffer so a stream that never delivers an END_MARKER can't grow it forever. _waitForAck() read exactly one frame and assumed it was the ACK. The device pushes unsolicited notifications on the same stream (a state change, a press of the headset's own button), so it could take _seqNumber from the wrong frame - and the device dedupes on that sequence number, silently discarding the next command as a duplicate. Wait for an actual ACK frame instead; skipped notifications are still acked back by _readMessage(). Also: - requestAmbientState() matches sub-type 0x17 rather than any 0x67, so a notification with a different layout is no longer parsed as the reply - the read-back no longer overwrites a change that hasn't been sent yet. It cleared the pending change too (isChanged() went false), so the command was never sent at all. discardAmbientChanges() releases that state when a write genuinely fails, so polling can't stay suppressed - the 2s poll stopped republishing dsee/eqPreset/eqBands, which it never re-read - they came straight back from the C++ cache, which only updates once a write succeeds. Failed writes roll back explicitly with the error instead, and the EQ sliders no longer fight the timer mid-drag (cherry picked from commit 40e65d4) Conflict resolution on top of AmitRajput-Dev#7/AmitRajput-Dev#28 (by maintainer): _waitForAck kept AmitRajput-Dev#7's equivalent ACK loop (now on MAX_FRAMES_PER_TRANSACTION); parser keeps this commit's resync-on-START_MARKER plus AmitRajput-Dev#7's noise skipping; the poll's change-gated writes and AmitRajput-Dev#7's slider coalescing are kept, with this commit's "sync either way" on a failed ambient write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #25
Adds Sound Connect's multipoint controls to the macOS app: see which devices are connected to the headset, which one has playback, switch playback to another device, and "fix" (pin) it — e.g. pin playback to the Mac before a call so the phone can't take over the headphones.
What's added
Devices card (v2 headsets that advertise it), right after Ambient Sound Control:
Menu-bar item
Protocol
The multipoint commands live in command table 2 (frame type
0x0e,DATA_MDR_NO2).BluetoothWrappercan now send on it, match replies on it and ACK its frames; existing table 1 calls are unchanged (new default parameter).06 0007 00 n (function, priority)…— needs0x31and0x32/0x3036 02(36 00fallback)37 02 n {addr[17] slot cod[3] len name}… playbackSlot36 0137 01 v(0 = fixed, 1 = free)3c 01 addr[17]3d 01 result addr38 01 00/38 01 0139 01 value resultd0 d1..d4 01to findMULTIPOINT_SETTING, thend6 slotd7 slot 00 v(0 = on)Safety:
0x0e(in table 1,0x30–0x3fare the firmware-update commands), only to v2 devices, and only if the init reply advertises table 2. v1 devices get nothing new.0x31, and switching only targets devices the headset lists as connected.Opcodes and layouts come from mos9527/SonyHeadphonesClient (
ProtocolV2T2.hpp, generated from Sound Connect) and mehrshaad/nullpoint.Tested
WH-1000XM5, firmware 2.5.1, on macOS:
39 01 00 02, FAIL_CALLING), so playback stays fixed to the MacI also replayed the WF-1000XM5 captures from mos9527's repo through the parser.
Not covered: the Windows/Linux UI (the core changes are shared and keep the existing call sites, but I only built the macOS app), disconnect/unpair, and writing the multipoint on/off setting.
macOS 12 needed to build with Xcode 27.2
Xcode 27.2 no longer builds the project as-is: it rejects
MACOSX_DEPLOYMENT_TARGET = 11.0("the range of supported deployment target versions is 12.0 to 27.2.x"). To build and test this I raised it to 12.0 in all four build configurations ofSonyHeadphonesClient.xcodeproj. I left that change out of this PR so the app keeps targeting macOS 11 — it's a one-line-per-config change in this commit on my fork if you want to take it too.Disclosure
This was mainly vibe coded (written with an AI assistant; I drove it and tested it on my headphones). If that's a problem for the project, feel free to reject it.