Skip to content

Commit fc635e3

Browse files
retr0hdeadprogram
authored andcommitted
feat(adapter): add Adapter.Reset() for in-process recovery (#446)
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.
1 parent 9314216 commit fc635e3

8 files changed

Lines changed: 80 additions & 6 deletions

adapter.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package bluetooth
44
type BLEAdapter interface {
55
Connect(address Address, params ConnectionParams) (Device, error)
66
Enable() error
7+
Reset() error
78
Scan(callback func(*Adapter, ScanResult)) (err error)
89
SetConnectHandler(c func(device Device, connected bool))
910
StopScan() error

adapter_cyw43439.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ func (a *Adapter) Enable() error {
7171
return nil
7272
}
7373

74+
// Reset is a no-op on CYW43439. Provided for interface symmetry.
75+
func (a *Adapter) Reset() error {
76+
return nil
77+
}
78+
7479
type hciSPI struct {
7580
dev *cyw43439.Device
7681
}

adapter_darwin.go

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,18 @@ var DefaultAdapter = &Adapter{
4444

4545
// Enable configures the BLE stack. It must be called before any
4646
// Bluetooth-related calls (unless otherwise indicated).
47+
//
48+
// poweredChan is cleared on both success and timeout paths so
49+
// a subsequent Enable() on the same Adapter can run again.
4750
func (a *Adapter) Enable() error {
4851
if a.poweredChan != nil {
4952
return errors.New("already calling Enable function")
5053
}
5154

5255
a.poweredChan = make(chan error, 1)
5356

54-
// Set the delegate before checking State so we don't miss an
55-
// async DidUpdateState that fires between construction and now.
57+
// Set delegate before checking state — a fresh CBCentralManager
58+
// can fire DidUpdateState before SetDelegate, losing the event.
5659
a.cmd = &centralManagerDelegate{a: a}
5760
a.cm.SetDelegate(a.cmd)
5861

@@ -82,6 +85,42 @@ func (a *Adapter) Enable() error {
8285
return nil
8386
}
8487

88+
// Reset tears down CoreBluetooth managers so a subsequent Enable()
89+
// rebuilds them from scratch. Useful for recovering from stale
90+
// CBPeripheral handles, adapter switching, and test cleanup.
91+
//
92+
// Caller must ensure no Scan/Connect/DiscoverServices is in flight.
93+
// After Reset, call Enable() to create fresh managers.
94+
//
95+
// Note: process-level CoreBluetooth state (e.g. the advertisement
96+
// deduplication table) survives Reset — only process exit clears it.
97+
func (a *Adapter) Reset() error {
98+
if a.scanChan != nil {
99+
_ = a.StopScan()
100+
}
101+
102+
// Unblock goroutines parked in Connect — closing the chan
103+
// yields a zero Peripheral so Connect returns an error.
104+
a.connectMap.Range(func(key, value any) bool {
105+
a.connectMap.Delete(key)
106+
if ch, ok := value.(chan cbgo.Peripheral); ok {
107+
defer func() { _ = recover() }()
108+
close(ch)
109+
}
110+
return true
111+
})
112+
113+
a.cm = cbgo.NewCentralManager(nil)
114+
a.pm = cbgo.NewPeripheralManager(nil)
115+
a.cmd = nil
116+
a.pmd = nil
117+
a.poweredChan = nil
118+
a.scanChan = nil
119+
a.peripheralFoundHandler = nil
120+
121+
return nil
122+
}
123+
85124
// CentralManager delegate functions
86125

87126
type centralManagerDelegate struct {
@@ -103,12 +142,10 @@ func (cmd *centralManagerDelegate) CentralManagerDidUpdateState(cmgr cbgo.Centra
103142
case cbgo.ManagerStateUnauthorized:
104143
event = errors.New("bluetooth is not authorized for this app")
105144
default:
106-
// Unknown / Resetting are intermediate; wait for the next update.
107145
return
108146
}
109-
// Non-blocking; select handles a nil poweredChan correctly (the
110-
// case is never ready, default fires) so a late or repeated
111-
// update never parks the delegate goroutine.
147+
// Non-blocking send: poweredChan may be nil after Enable
148+
// completes, or already buffered.
112149
select {
113150
case cmd.a.poweredChan <- event:
114151
default:

adapter_hci_uart.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ func (a *Adapter) Enable() error {
7171
return nil
7272
}
7373

74+
// Reset is a no-op on HCI UART. Provided for interface symmetry.
75+
func (a *Adapter) Reset() error {
76+
return nil
77+
}
78+
7479
type hciUART struct {
7580
uart *machine.UART
7681

adapter_linux.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ func (a *Adapter) Enable() (err error) {
6666
return nil
6767
}
6868

69+
// Reset clears BlueZ state so a subsequent Enable() rebuilds it.
70+
// Mostly a no-op on Linux; provided for interface symmetry.
71+
func (a *Adapter) Reset() error {
72+
a.bus = nil
73+
a.bluez = nil
74+
a.adapter = nil
75+
a.address = ""
76+
a.scanCancelChan = nil
77+
return nil
78+
}
79+
6980
func (a *Adapter) Address() (MACAddress, error) {
7081
if a.address == "" {
7182
return MACAddress{}, errors.New("adapter not enabled")

adapter_ninafw.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ func (a *Adapter) Enable() error {
6868
return a.enable()
6969
}
7070

71+
// Reset is a no-op on NINA. Provided for interface symmetry.
72+
func (a *Adapter) Reset() error {
73+
return nil
74+
}
75+
7176
func resetNINA() {
7277
machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput})
7378

adapter_sd.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ func (a *Adapter) Enable() error {
114114
return makeError(errCode)
115115
}
116116

117+
// Reset is a no-op on SoftDevice. Provided for interface symmetry.
118+
func (a *Adapter) Reset() error {
119+
return nil
120+
}
121+
117122
// DisableInterrupts must be used instead of disabling interrupts directly, to
118123
// play well with the SoftDevice. Restore interrupts to the previous state with
119124
// RestoreInterrupts.

adapter_windows.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ func (a *Adapter) Enable() error {
3737
return ole.RoInitialize(1) // initialize with multithreading enabled
3838
}
3939

40+
// Reset is a no-op on Windows. Provided for interface symmetry.
41+
func (a *Adapter) Reset() error {
42+
return nil
43+
}
44+
4045
func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericParamSignature string) error {
4146
var status foundation.AsyncStatus
4247

0 commit comments

Comments
 (0)