@@ -139,10 +139,17 @@ class KableBleConnection(private val scope: CoroutineScope) : BleConnection {
139139 meshtasticDevice.advertisement?.let { adv -> Peripheral (adv) { commonConfig() } }
140140 ? : createPeripheral(device.address) { commonConfig() }
141141
142- cleanUpPeripheral(device.address)
143- peripheral = p
144-
145- ActiveBleConnection .active = ActiveConnection (p, device.address)
142+ // Install ownership of the new peripheral atomically. Cancellation between
143+ // peripheral construction and field assignment would strand `p` (Kable allocates
144+ // a per-peripheral scope + Bluetooth-state observer eagerly), so the cleanup,
145+ // assignment, and ActiveBleConnection update must complete as a single unit.
146+ // _deviceFlow.emit() is intentionally outside this block — making it
147+ // non-cancellable could hang teardown on a slow collector.
148+ withContext(NonCancellable ) {
149+ cleanUpPeripheral(device.address)
150+ peripheral = p
151+ ActiveBleConnection .active = ActiveConnection (p, device.address)
152+ }
146153
147154 _deviceFlow .emit(device)
148155
@@ -212,11 +219,18 @@ class KableBleConnection(private val scope: CoroutineScope) : BleConnection {
212219 stateJob?.cancel()
213220 stateJob = null
214221
222+ // Capture the peripheral we own before clearing it so we can identity-check
223+ // ActiveBleConnection below. A stale disconnect from an earlier connection
224+ // attempt's exception handler must not clobber a newer connection that has
225+ // already installed itself as active.
226+ val owned = peripheral
215227 safeClosePeripheral(" disconnect" )
216228 peripheral = null
217229 connectionScope = null
218230
219- ActiveBleConnection .active = null
231+ if (owned != null && ActiveBleConnection .active?.peripheral == = owned) {
232+ ActiveBleConnection .active = null
233+ }
220234
221235 _deviceFlow .emit(null )
222236 }
0 commit comments