Skip to content

Build1 publisher3 min readPublished

A weekend of protocol work gave macOS the Soundcore controls Anker ships only on phones

The Space 2's SDP records advertise a vendor control channel on 30 and firmware OTA on 12 and 13. Encoding the protocol took six test packets; opening the channel took finding that IOBluetooth's first open always fails.

The Engineer · Build desk

Illustration accompanying A weekend of protocol work gave macOS the Soundcore controls Anker ships only on phones

What happened

  • Anker ships the Soundcore app for Android and iOS only, so switching off noise cancelling on a Space 2 that is already paired to a MacBook means reaching for a phone.
  • A developer reverse-engineered the protocol over one weekend, pair-programming with Claude, and shipped SoundcoreBridge, a native macOS menu bar app and CLI that speaks it.
  • Dumping the Space 2's SDP records exposes a vendor control service on channel 30, firmware OTA on channel 12, chipset OTA on channel 13 and Apple iAP2 on channel 16.
  • When no channel would open, he concluded macOS itself was the limitation, wrote it up as such, and started designing a Raspberry Pi bridge to sit between the Mac and the headphones.
  • Claude pushed back on that conclusion and surfaced SonyBridge, an open-source macOS project doing the same thing; a spike reproducing its approach opened the channel.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • capability A Mac can now set noise cancelling and EQ on a device Anker supports only through its mobile app, with no phone and no bridging hardware in the path.
  • exposure The record that hands you the control channel also hands you the two OTA channels, so a client with a wrong channel constant writes firmware instead of a setting.
  • constraint The frame format carries across Soundcore models but the control UUID's suffix does not, so anyone porting this starts with their own SDP dump rather than a constant copied from the post.
  • decision A failure that reproduces on a second device is not independent evidence when both runs go through the same client; the first-open bug showed up identically in the test and in the control.

The protocol is small. Four magic bytes, a sequence byte, a two-byte command, a two-byte little-endian total length, the payload, and a one-byte checksum that is the sum of every preceding byte truncated to 8 bits [6][7]. Device replies swap the magic to `09 FF 00 00` [8]. The worked example in the post is `08 EE 00 00 | 00 | 06 81 | 10 00 | 00 5F 02 00 00 01 | EF` [6]. Its length field reads 0x0010, or 16, and the frame as printed is 16 bytes, so the length counts header, payload and checksum together [1]. The 15 bytes before the checksum sum to 495, and 495 modulo 256 is 239, which is 0xEF [2].

He tested the codec before he tested the hardware. Six packets documented for other Soundcore models went through his encoder and came back byte for byte, which he calls a five-minute investment that let him rule out the encoder later, when nothing worked [9].

Nothing worked for a while. `openRFCOMMChannelAsync` returned success and the delegate callback never fired, and the synchronous variant returned `kIOReturnError` immediately [10]. He tried channel 30, then 17, then 1, which is handsfree and definitely exists, then 2, which advertises nothing [11]. He opened RFCOMM to an Android phone on OBEX and phonebook access and got the identical failure, across two devices and seven channels, from a bare binary and from a signed `.app` [12]. `bluetoothd` logged no connect attempt reaching the air, TCC was granted, the app was not sandboxed, and the baseband link was up [13].

The first `openRFCOMMChannelAsync` in a process always fails, apparently because it lazily initialises IOBluetooth's run-loop source, and a retry succeeds [16]. The spike that eventually worked had made two throwaway attempts against the phone before the one that connected [17]. "It was measuring my own bug twice and I read it as corroboration," the developer wrote of his control experiment [18]. The Raspberry Pi bridge he had started designing would have been hardware bought to work around a missing retry [3].

The second problem was delivery. IOBluetooth hands delegate callbacks to the run loop of the thread that opened the channel, not to a dispatch queue, so a Swift continuation awaited on that thread never sees them and the code has to pump the run loop itself [19]. CoreBluetooth uses a dispatch queue, and it had worked for him throughout [20]. Both faults present the same way from the caller's side: a success return with no callback [4].

Channels 12 and 13 in the same SDP record flash device firmware and chipset firmware [3]. He hard-blocks both by service identity, and says to do that before writing a single byte [5].

What to watch

  • An Anker firmware update that changes the Space 2's SDP records or the per-model UUID suffix would break the client.
  • Whether forks of SoundcoreBridge keep the service-identity block on channels 12 and 13.
  • Whether Apple documents the first-open failure in IOBluetooth or changes the behaviour in a macOS release.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories