From 8dd1d1cd70f3a004c5a9510ba877057952e7a36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D7=A0=CF=85=CE=B1=CE=B7=20=D7=A0=CF=85=CE=B1=CE=B7=D1=95?= =?UTF-8?q?=CF=83=CE=B7?= Date: Fri, 1 May 2026 09:55:39 -0700 Subject: [PATCH] feat(adapter): add Adapter.Reset() for in-process recovery 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) --- adapter.go | 1 + adapter_cyw43439.go | 5 +++++ adapter_darwin.go | 49 +++++++++++++++++++++++++++++++++++++++------ adapter_hci_uart.go | 5 +++++ adapter_linux.go | 11 ++++++++++ adapter_ninafw.go | 5 +++++ adapter_sd.go | 5 +++++ adapter_windows.go | 5 +++++ 8 files changed, 80 insertions(+), 6 deletions(-) diff --git a/adapter.go b/adapter.go index 03d25dac..20d36c36 100644 --- a/adapter.go +++ b/adapter.go @@ -4,6 +4,7 @@ package bluetooth type BLEAdapter interface { Connect(address Address, params ConnectionParams) (Device, error) Enable() error + Reset() error Scan(callback func(*Adapter, ScanResult)) (err error) SetConnectHandler(c func(device Device, connected bool)) StopScan() error diff --git a/adapter_cyw43439.go b/adapter_cyw43439.go index e3a19eeb..b40c885d 100644 --- a/adapter_cyw43439.go +++ b/adapter_cyw43439.go @@ -71,6 +71,11 @@ func (a *Adapter) Enable() error { return nil } +// Reset is a no-op on CYW43439. Provided for interface symmetry. +func (a *Adapter) Reset() error { + return nil +} + type hciSPI struct { dev *cyw43439.Device } diff --git a/adapter_darwin.go b/adapter_darwin.go index 3220b305..1b52b876 100644 --- a/adapter_darwin.go +++ b/adapter_darwin.go @@ -44,6 +44,9 @@ var DefaultAdapter = &Adapter{ // Enable configures the BLE stack. It must be called before any // Bluetooth-related calls (unless otherwise indicated). +// +// poweredChan is cleared on both success and timeout paths so +// a subsequent Enable() on the same Adapter can run again. func (a *Adapter) Enable() error { if a.poweredChan != nil { return errors.New("already calling Enable function") @@ -51,8 +54,8 @@ func (a *Adapter) Enable() error { a.poweredChan = make(chan error, 1) - // Set the delegate before checking State so we don't miss an - // async DidUpdateState that fires between construction and now. + // Set delegate before checking state — a fresh CBCentralManager + // can fire DidUpdateState before SetDelegate, losing the event. a.cmd = ¢ralManagerDelegate{a: a} a.cm.SetDelegate(a.cmd) @@ -82,6 +85,42 @@ func (a *Adapter) Enable() error { return nil } +// Reset tears down CoreBluetooth managers so a subsequent Enable() +// rebuilds them from scratch. Useful for recovering from stale +// CBPeripheral handles, adapter switching, and test cleanup. +// +// Caller must ensure no Scan/Connect/DiscoverServices is in flight. +// After Reset, call Enable() to create fresh managers. +// +// Note: process-level CoreBluetooth state (e.g. the advertisement +// deduplication table) survives Reset — only process exit clears it. +func (a *Adapter) Reset() error { + if a.scanChan != nil { + _ = a.StopScan() + } + + // Unblock goroutines parked in Connect — closing the chan + // yields a zero Peripheral so Connect returns an error. + a.connectMap.Range(func(key, value any) bool { + a.connectMap.Delete(key) + if ch, ok := value.(chan cbgo.Peripheral); ok { + defer func() { _ = recover() }() + close(ch) + } + return true + }) + + a.cm = cbgo.NewCentralManager(nil) + a.pm = cbgo.NewPeripheralManager(nil) + a.cmd = nil + a.pmd = nil + a.poweredChan = nil + a.scanChan = nil + a.peripheralFoundHandler = nil + + return nil +} + // CentralManager delegate functions type centralManagerDelegate struct { @@ -103,12 +142,10 @@ func (cmd *centralManagerDelegate) CentralManagerDidUpdateState(cmgr cbgo.Centra case cbgo.ManagerStateUnauthorized: event = errors.New("bluetooth is not authorized for this app") default: - // Unknown / Resetting are intermediate; wait for the next update. return } - // Non-blocking; select handles a nil poweredChan correctly (the - // case is never ready, default fires) so a late or repeated - // update never parks the delegate goroutine. + // Non-blocking send: poweredChan may be nil after Enable + // completes, or already buffered. select { case cmd.a.poweredChan <- event: default: diff --git a/adapter_hci_uart.go b/adapter_hci_uart.go index 4f94ff37..0e53ff39 100644 --- a/adapter_hci_uart.go +++ b/adapter_hci_uart.go @@ -71,6 +71,11 @@ func (a *Adapter) Enable() error { return nil } +// Reset is a no-op on HCI UART. Provided for interface symmetry. +func (a *Adapter) Reset() error { + return nil +} + type hciUART struct { uart *machine.UART diff --git a/adapter_linux.go b/adapter_linux.go index eb836e19..a62d6d44 100644 --- a/adapter_linux.go +++ b/adapter_linux.go @@ -66,6 +66,17 @@ func (a *Adapter) Enable() (err error) { return nil } +// Reset clears BlueZ state so a subsequent Enable() rebuilds it. +// Mostly a no-op on Linux; provided for interface symmetry. +func (a *Adapter) Reset() error { + a.bus = nil + a.bluez = nil + a.adapter = nil + a.address = "" + a.scanCancelChan = nil + return nil +} + func (a *Adapter) Address() (MACAddress, error) { if a.address == "" { return MACAddress{}, errors.New("adapter not enabled") diff --git a/adapter_ninafw.go b/adapter_ninafw.go index 295df13c..6136a310 100644 --- a/adapter_ninafw.go +++ b/adapter_ninafw.go @@ -68,6 +68,11 @@ func (a *Adapter) Enable() error { return a.enable() } +// Reset is a no-op on NINA. Provided for interface symmetry. +func (a *Adapter) Reset() error { + return nil +} + func resetNINA() { machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput}) diff --git a/adapter_sd.go b/adapter_sd.go index 4226497a..2d42c862 100644 --- a/adapter_sd.go +++ b/adapter_sd.go @@ -114,6 +114,11 @@ func (a *Adapter) Enable() error { return makeError(errCode) } +// Reset is a no-op on SoftDevice. Provided for interface symmetry. +func (a *Adapter) Reset() error { + return nil +} + // DisableInterrupts must be used instead of disabling interrupts directly, to // play well with the SoftDevice. Restore interrupts to the previous state with // RestoreInterrupts. diff --git a/adapter_windows.go b/adapter_windows.go index 1370fbd0..ef7d86c8 100644 --- a/adapter_windows.go +++ b/adapter_windows.go @@ -37,6 +37,11 @@ func (a *Adapter) Enable() error { return ole.RoInitialize(1) // initialize with multithreading enabled } +// Reset is a no-op on Windows. Provided for interface symmetry. +func (a *Adapter) Reset() error { + return nil +} + func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericParamSignature string) error { var status foundation.AsyncStatus