Skip to content

darwin: return an error when Connect fails - #519

Open
acouvreur wants to merge 2 commits into
devfrom
darwin-connect-error
Open

acouvreur wants to merge 2 commits into
devfrom
darwin-connect-error

Conversation

@acouvreur

@acouvreur acouvreur commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Fixes #278.

On macOS, Connect could return a zero Device with a nil error, and the first GATT call on it panicked. The central manager delegate dropped the error from DidFailToConnectPeripheral and DidDisconnectPeripheral, and Connect only returned its own timeout error.

This happens when you connect right after Disconnect. The disconnect of the old connection reaches the new pending Connect, as jbanyer described in the issue.

The delegate now sends the error with the peripheral. A disconnect always counts as a failure, even with no CoreBluetooth error. Connect returns its own timeout error first, then the CoreBluetooth error, then a generic error.

When Connect fails for a reason other than its own timeout, it cancels the pending CoreBluetooth connection. Without this, that attempt can succeed later and leave a link that no Device owns.

A Connect unblocked by Adapter.Reset now returns an error instead of a zero Device.

Behavior change

A stale disconnect still makes the new Connect fail. It now returns an error instead of a zero Device, so the caller must retry. On the hardware below, one retry always connected.

Testing

MacBook Pro 15,2 (Intel, BCM4364, macOS 15.8.1) with a Valve Lighthouse base station. Compared the base commit d679711 with this branch.

Case Before After
Reconnect right after Disconnect (steps from #278) Nil error and zero Device on the first try, then a nil pointer panic 10/10 return Connect failed: peripheral disconnected in about 0.5 ms. 10/10 retries connect and find 4 services
Link state 3 s after that failed reconnect Panics before 0/5 still connected. Without CancelConnect, 5/5 stay connected with no Device
1 ms ConnectionTimeout 3/3 timeout on Connect
Reset 5 ms into a pending Connect Nil error, zero Device Connect canceled by adapter Reset, zero Device
Normal connect 3/3 connect in 150 to 230 ms

The central manager delegate dropped the error from
DidFailToConnectPeripheral and DidDisconnectPeripheral. Connect then
returned a zero Device with a nil error, and the first GATT call on it
panicked.

Send the error along with the peripheral and return it from Connect.
Fall back to a generic error when CoreBluetooth gives none, which happens
when a disconnect from an earlier connection reaches a pending Connect.
@acouvreur
acouvreur marked this pull request as ready for review October 17, 2026 20:44
@deadprogram deadprogram added this to the 0.17.0 milestone Oct 27, 2026

@deadprogram deadprogram left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. The main fix is correct. Before this change Connect returned connectionError, which is nil unless the timeout fired.

Some points:

  1. When a stale disconnect gets to Connect and it returns an error, the a.cm.Connect(prphs[0], nil) call is still pending in CoreBluetooth. If that attempt then succeeds, DidConnectPeripheral finds no channel and ignores it. The OS link stays up and no Device owns it. Please call a.cm.CancelConnect(prphs[0]) on this error path when connectionError == nil.
  2. After this change, a stale disconnect still makes the new Connect fail, so the caller must retry. This is better than a panic, but please say it in the PR description.
  3. The Reset comment still has an em dash (Connect — closing). You change the next line, so please fix this one too.

Also please test on a Mac with the steps in the description before we merge.

@acouvreur

Copy link
Copy Markdown
Member Author
  1. Done in 1f08ebf: Connect calls a.cm.CancelConnect(prphs[0]) on the error path when connectionError == nil. On a Mac, without the call the abandoned attempt connected in 5 of 5 runs and the link stayed up with no Device. With it, 0 of 5. The same commit also makes DidDisconnectPeripheral always send an error. A disconnect after CancelPeripheralConnection has no CoreBluetooth error, so if the peripheral was already in the connecting state, the State() == Disconnected check missed it and Connect could report success.
  2. Added to the description under "Behavior change for callers".
  3. Fixed.

I tested on a Mac with the steps from the description. The results for the base commit and this branch are in the table in the description.

@deadprogram

Copy link
Copy Markdown
Member

@acouvreur please update your last commit to remove the coding tool attritbution. Thanks.

@deadprogram deadprogram modified the milestones: 0.17.0, 0.18.0 Oct 28, 2026
When the disconnect of a previous connection reaches a pending Connect,
the CoreBluetooth connection attempt is still running. If it then
succeeds, nothing owns the link. Cancel it on this error path.

The disconnect delegate now always sends an error, since a disconnect
with no CoreBluetooth error (after CancelPeripheralConnection) can reach
Connect while the peripheral is already in the connecting state.
@acouvreur
acouvreur force-pushed the darwin-connect-error branch from 1f08ebf to 6f6b23b Compare October 31, 2026 00:51
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.

Immediately call adapter.Connect(addr) after device.Disconnect() will cause DiscoverServices() panic

2 participants