Skip to content

fix(macos): macOS 15 fixes for feature/apple-macos-support (sqlite, RFCOMM open handshake) - #1

Merged
SeanathanVT merged 2 commits into
SeanathanVT:feature/apple-macos-supportfrom
afrijaldz:fix/macos-rfcomm-open-handshake
Sep 17, 2026
Merged

SeanathanVT merged 2 commits into
SeanathanVT:feature/apple-macos-supportfrom
afrijaldz:fix/macos-rfcomm-open-handshake

Conversation

@afrijaldz

Copy link
Copy Markdown

Companion to Oppzippy#372 — results of testing 404318de on macOS 15.4 / Apple Silicon / Soundcore R50i, with a fix on top. This is meant as a reference for the macOS 15 case, not a request to merge as-is: take whatever is useful, or just use it as evidence of what fails on 15. Details and numbers are in the Oppzippy#372 comment.

What fails on macOS 15 with the branch as-is

  1. CLI/GUI don't start: malformed database schema (quick_preset) - wrong number of arguments to function json_valid(). macOS 15 ships sqlite 3.43; the 2-arg json_valid() needs 3.45+.
  2. Every other connect fails with kIOReturnNotOpen after 49 write retries. closeConnection is async and isConnected() stays true for ~0.5 s; ensure_connected reopens inside that window and the channel never finishes opening. The failure leaves the device disconnected, so the next attempt takes the plain openConnection path and works — hence the alternation.

What this changes

  • lib/Cargo.toml: bundled rusqlite for macOS (same as Windows/Android).
  • rfcomm.rs:
    • Reuse an existing baseband connection instead of cycling it on every connect (no audio chime, ~0.5 s instead of ~3.5 s).
    • Wait for rfcommChannelOpenComplete (5 s timeout) before reporting the channel open. The delegate signals through a channel, so it doesn't matter which thread the callback lands on. Only the callback is trusted: isOpen() flips to true ~200 ms earlier and writes in that window fail with 0xE00002E7.
    • Keep the baseband cycle as a fallback when the open times out on a pre-existing connection, and wait for isConnected() to go false before reopening so the cycle actually works.

Tested (macOS 15.4, M-series, R50i)

  • 15/15 back-to-back connect + EQ preset write (4 recovered via the fallback)
  • 10/10 with a 1 s gap, connect ~0.5 s
  • EQ presets and button config verified to persist from a fresh process
  • cargo check -p openscq30-gui passes

Not verified: macOS 26/27

I can't test there. The one assumption that matters: your code comment says the open callback isn't dispatched until the first successful write on 26/27. On 15 it arrives ~0.5 s after openRFCOMMChannelAsync with no write at all. If your observation holds on 26/27, the wait here would always time out and connects would fail there. If that's the case, a more conservative variant is easy: on timeout, log and proceed anyway (leaving the existing write-retry loop as the safety net) instead of failing — happy to push that instead if you'd prefer it.

The system sqlite on macOS 15 is 3.43, which lacks the two-argument
json_valid() used by the quick_preset migration, so the storage layer
fails to open with "malformed database schema". Windows and Android
already build with the bundled sqlite for the same reason.
…eband connection as a fallback

On macOS 15 (Apple Silicon, Soundcore R50i) every other connect failed
with kIOReturnNotOpen after exhausting the write retries. closeConnection
is asynchronous, so calling openConnection/openRFCOMMChannelAsync right
after it (which ensure_connected did whenever the device was already
connected) raced the teardown and the channel never finished opening.
When that failed the device was left disconnected, so the next attempt
took the plain openConnection path and succeeded, hence the alternation.

- Reuse an existing baseband connection instead of cycling it on every
  connect. This also removes the audible disconnect/reconnect and cuts
  connect time from ~3.5s to ~0.5s when the device is already connected.
- Wait for rfcommChannelOpenComplete (with a timeout) before reporting
  the channel as open, rather than finding out on the first write. Only
  the callback is trusted: isOpen() flips to true a few hundred ms
  earlier and writes in that window fail.
- Keep the baseband cycle as a fallback for when the open times out on a
  pre-existing connection (the stuck-channel case), and wait for
  isConnected() to go false before reopening so the cycle actually works.

Tested on macOS 15.4 / M-series / Soundcore R50i: 15/15 back-to-back
connects + EQ writes (4 of them recovered via the fallback), 10/10 with a
1s gap, EQ presets and button config verified to persist from a fresh
process.
@SeanathanVT

Copy link
Copy Markdown
Owner

Thanks for this! good points all around. Sorry for missing this.. I have no macOS 15 host to test with. Verified that everything is still working on macOS 27 though.

FWIW, I couldn't reproduce the "connect fails on every other attempt" behavior that you referenced, but these changes (unmodified) did not cause a regression on my macOS 27 host. I'm cool with keeping them as-is. Thanks again!

@SeanathanVT
SeanathanVT merged commit 6ddee0c into SeanathanVT:feature/apple-macos-support Sep 17, 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