Skip to content

feat(adapter): add Adapter.Reset() for in-process recovery#446

Merged
deadprogram merged 1 commit into
tinygo-org:devfrom
retr0h:feat/adapter-reset
May 17, 2026
Merged

feat(adapter): add Adapter.Reset() for in-process recovery#446
deadprogram merged 1 commit into
tinygo-org:devfrom
retr0h:feat/adapter-reset

Conversation

@retr0h
Copy link
Copy Markdown
Contributor

@retr0h retr0h commented May 1, 2026

Summary

Adds Reset() to *Adapter on darwin and linux. Reset tears down the underlying transport-specific state (CoreBluetooth managers on darwin, D-Bus handles on linux) so a subsequent Enable() rebuilds them from scratch.

Depends on #445 (Enable callable after first invocation) — once that lands I'll rebase and remove this note.

Motivation

There's currently no API to throw away adapter state and start fresh on the same *Adapter. Useful for:

  • Recovery flows that hit a stale CBPeripheral handle on an otherwise-healthy central (Connect blocks forever waiting for a delegate callback that will never arrive — Reset breaks the deadlock).
  • Switching between adapters in long-running processes.
  • Cleanup in tests.

What it does

  • cancels in-flight scan via existing StopScan
  • drains pending Connect waiters via close(ch) so callers unblock and return an error rather than parking on a callback that a fresh central will never deliver
  • replaces cm / pm with fresh cbgo.NewCentralManager(nil) / cbgo.NewPeripheralManager(nil); ARC reclaims the old ones
  • zeros per-Adapter channels and handlers (poweredChan, scanChan, peripheralFoundHandler)

After Reset, calling Enable() again is required to set up fresh delegates and wait for powered-on. This relies on Enable's poweredChan-cleanup behavior in #445, which is why the two PRs are stacked.

Honest about the limits

The doc comment is explicit about what Reset does not do:

Reset is NOT a complete CoreBluetooth state reset. Some framework-level state (notably the advertisement-deduplication table that suppresses repeated DidDiscoverPeripheral callbacks for already-known UUIDs) is held by CoreBluetooth at the process level and survives recreating the CBCentralManager. The only reliable way to clear that state is process exit. If you're trying to recover from "a peripheral I previously scanned won't reappear in subsequent Scans," Reset is unlikely to help on its own.

I empirically validated this in a real recovery flow: after a long macOS sleep, ~20 Reset cycles over 10 hours never restored the ability to re-discover a known peripheral. The dedup table outlives any in-process API we have access to. Recovery from that specific failure mode requires self-exec or a subprocess BLE helper — out of scope for this library.

That said, Reset still has a useful niche:

  • Adapter switching (multiple adapters on the same machine).
  • Stale CBPeripheral handles on a central that's otherwise healthy.
  • Test isolation between scenarios.

Linux

Mostly a no-op — BlueZ is naturally per-call and doesn't carry the kind of cached peripheral state that wedges CoreBluetooth — but keeps the API symmetric so callers can write platform-agnostic recovery code.

Caller contract

Caller MUST ensure no Scan / Connect / DiscoverServices is in flight when Reset is called — there is no internal locking. Documented in the comment. The intended usage pattern is:

session goroutine errors out
caller calls adapter.Reset()
caller calls adapter.Enable()
caller starts a fresh session

Verification

  • go build . clean on darwin/arm64
  • Behavior validated against a Meshtastic peripheral: pre-Reset, an in-process retry after a stale CBPeripheral handle hangs forever in Connect. Post-Reset, Connect returns an error from the closed connectMap channel, the caller logs it and retries the full scan-and-connect path on the new central.

Backwards compatibility

Purely additive. Existing callers see no change.

🤖 Generated with Claude Code

@deadprogram
Copy link
Copy Markdown
Member

Thank you for working on this @retr0h a couple quick things.

Any new functions should be added to the interface definitions. Also at least no-op versions added to all platforms. We've been trying to get the consistency improved.

Also, perhaps slightly briefer on the comments, if you please. 😸

Thanks!

@retr0h retr0h force-pushed the feat/adapter-reset branch 2 times, most recently from 2207fe9 to d80d0cd Compare May 16, 2026 23:51
Adds Reset() to *Adapter on all platforms. On darwin it tears down
CoreBluetooth managers so a subsequent Enable() rebuilds from scratch.
On linux it clears BlueZ handles. Other platforms are no-ops for
interface symmetry.

Also adds Reset() to the BLEAdapter interface definition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@retr0h retr0h force-pushed the feat/adapter-reset branch from d80d0cd to 8dd1d1c Compare May 16, 2026 23:55
@retr0h
Copy link
Copy Markdown
Contributor Author

retr0h commented May 16, 2026

Thank you for working on this @retr0h a couple quick things.

Any new functions should be added to the interface definitions. Also at least no-op versions added to all platforms. We've been trying to get the consistency improved.

Also, perhaps slightly briefer on the comments, if you please. 😸

Thanks!

@deadprogram I've gone ahead and implemented your corrections. Let me know if you need anything else.

@deadprogram
Copy link
Copy Markdown
Member

Thank you very much for the useful addition @retr0h and the quick update. Now squash/merging!

@deadprogram deadprogram merged commit 51c791c into tinygo-org:dev May 17, 2026
4 checks passed
deadprogram pushed a commit that referenced this pull request May 17, 2026
Adds Reset() to *Adapter on all platforms. On darwin it tears down
CoreBluetooth managers so a subsequent Enable() rebuilds from scratch.
On linux it clears BlueZ handles. Other platforms are no-ops for
interface symmetry.

Also adds Reset() to the BLEAdapter interface definition.
retr0h added a commit to retr0h/meshx that referenced this pull request May 18, 2026
Bump tinygo.org/x/bluetooth to include tinygo-org/bluetooth#446
which adds Adapter.Reset() for in-process recovery.

When Connect times out on a stale CBPeripheral handle (post-sleep/wake),
call Reset() to tear down the central manager so the next Enable()
rebuilds from scratch. Previously this required a process restart;
now the reconnect loop recovers automatically.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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