Skip to content

Commit 3cedf88

Browse files
authored
Merge pull request #213 from NordicSemiconductor/connection-states
`Closed` connection state removed in favor of `Disconnected`
2 parents 2dec397 + 2dd804a commit 3cedf88

File tree

8 files changed

+78
-44
lines changed

8 files changed

+78
-44
lines changed

client-android-mock/src/main/java/no/nordicsemi/kotlin/ble/client/android/mock/internal/MockExecutor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ open class MockExecutor(
5959
name: String?,
6060
): Peripheral.Executor {
6161
override val type: PeripheralType = peripheralSpec.type
62-
override val initialState: ConnectionState = ConnectionState.Closed
62+
override val initialState: ConnectionState = ConnectionState.Disconnected()
6363
override val initialServices: List<RemoteService> = emptyList()
6464

6565
override val identifier: String = peripheralSpec.identifier

client-android/src/main/java/no/nordicsemi/kotlin/ble/client/android/internal/NativeExecutor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ internal class NativeExecutor(
7676
} catch (_: SecurityException) {
7777
name
7878
}
79-
override val initialState: ConnectionState = ConnectionState.Closed
79+
override val initialState: ConnectionState = ConnectionState.Disconnected()
8080
override val initialServices: List<RemoteService> = emptyList()
8181

8282
/**

client-core-android/src/main/java/no/nordicsemi/kotlin/ble/client/android/Peripheral.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,15 @@ open class Peripheral(
260260
// direct connection.
261261
//
262262
// See: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/system/stack/gatt/gatt_api.cc;l=1450
263-
val reason = state.reason
263+
val reason = state.reason!!
264264
if (reason is Reason.Unknown && (reason.status == 133 || reason.status == 135)) {
265265
logger.warn("Connection attempt failed (reason: {})", Reason.UnsupportedAddress)
266266
_state.update { ConnectionState.Disconnected(Reason.UnsupportedAddress) }
267267
throw ConnectionFailedException(Reason.UnsupportedAddress)
268268
}
269-
logger.warn("Connection attempt failed (reason: {})", state.reason)
269+
logger.warn("Connection attempt failed (reason: {})", reason)
270270
_state.update { state }
271-
throw ConnectionFailedException(state.reason)
271+
throw ConnectionFailedException(reason)
272272
}
273273
else -> {}
274274
}
@@ -309,9 +309,10 @@ open class Peripheral(
309309
}
310310
is ConnectionState.Disconnected -> {
311311
check(options.retry > 0) {
312-
logger.warn("Connection attempt failed (reason: {})", state.reason)
312+
val reason = state.reason!!
313+
logger.warn("Connection attempt failed (reason: {})", reason)
313314
_state.update { state }
314-
throw ConnectionFailedException(state.reason)
315+
throw ConnectionFailedException(reason)
315316
}
316317
logger.warn("Connection attempt failed (reason: {}), retrying in {}...",
317318
state.reason, options.retryDelay)
@@ -581,7 +582,6 @@ open class Peripheral(
581582
* by [services] will emit an empty list of services following by updated list of services
582583
* when the new service discovery is complete.
583584
*
584-
* The peripheral must be in any state other then [ConnectionState.Closed].
585585
* It is safe to call this method when the peripheral is connected, connecting, or disconnecting.
586586
* It may be called when the device is disconnected but only when the connection was made using
587587
* [AutoConnect][CentralManager.ConnectionOptions.AutoConnect] option in which case the system
@@ -590,9 +590,9 @@ open class Peripheral(
590590
* A connection made using [Direct][CentralManager.ConnectionOptions.Direct] option closes
591591
* automatically immediately after disconnection.
592592
*
593-
* When invoked when closed the method throws [PeripheralClosedException].
593+
* When invoked on a closed connection the method throws [PeripheralClosedException].
594594
*
595-
* @throws PeripheralClosedException If the peripheral is in [Closed][ConnectionState.Closed] state.
595+
* @throws PeripheralClosedException If the peripheral is closed.
596596
* @throws OperationFailedException If cache could not be refreshed.
597597
* @throws SecurityException If BLUETOOTH_CONNECT permission is denied.
598598
*/

client-core-android/src/main/java/no/nordicsemi/kotlin/ble/client/android/preview/PreviewPeripheral.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ open class PreviewPeripheral(
386386
type: PeripheralType = PeripheralType.LE,
387387
rssi: Int = -40, // dBm
388388
phy: PhyInUse = PhyInUse.PHY_LE_1M,
389-
state: ConnectionState = ConnectionState.Closed,
389+
state: ConnectionState = ConnectionState.Disconnected(),
390390
services: ServerScope.() -> Unit = {
391391
Service(Service.GENERIC_ACCESS_UUID) {
392392
Characteristic(

client-core/src/main/java/no/nordicsemi/kotlin/ble/client/GattEvent.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ sealed class GattEvent {
5757
data class ConnectionStateChanged(val newState: ConnectionState) : GattEvent() {
5858

5959
/**
60-
* Returns whether the new state is [ConnectionState.Disconnected] or [ConnectionState.Closed].
60+
* Returns whether the new state is [ConnectionState.Disconnected].
6161
*/
6262
val disconnected: Boolean
63-
get() = newState is ConnectionState.Disconnected || newState is ConnectionState.Closed
63+
get() = newState is ConnectionState.Disconnected
6464
}
6565

6666
/**

client-core/src/main/java/no/nordicsemi/kotlin/ble/client/Peripheral.kt

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import kotlinx.coroutines.flow.SharedFlow
4141
import kotlinx.coroutines.flow.SharingStarted
4242
import kotlinx.coroutines.flow.StateFlow
4343
import kotlinx.coroutines.flow.asStateFlow
44-
import kotlinx.coroutines.flow.filter
4544
import kotlinx.coroutines.flow.filterIsInstance
4645
import kotlinx.coroutines.flow.first
4746
import kotlinx.coroutines.flow.firstOrNull
@@ -239,14 +238,28 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
239238

240239
gattEventCollector = impl.events
241240
// Handle each GATT event.
242-
.onEach { handle(it) }
243-
// In case of a Connection State Changed event...
244-
.filterIsInstance(ConnectionStateChanged::class)
245-
// ...when the device got disconnected and the closeWhenDisconnected flag was set...
246-
.filter { closeWhenDisconnected && it.newState is ConnectionState.Disconnected }
247-
// ...cancel the collector. This will call...
248-
.onEach { gattEventCollector?.cancel() }
249-
// ...the onCompletion method.
241+
.onEach { event ->
242+
when {
243+
// In case of a disconnection event...
244+
event is ConnectionStateChanged && event.newState is ConnectionState.Disconnected -> {
245+
// ...when the connection was terminated using disconnect() or cancelled,
246+
// or the closeWhenDisconnected flag was set (no automatic reconnection)
247+
// process the event and cancel the collector.
248+
if (event.newState.isUserInitiated || closeWhenDisconnected) {
249+
handle(event)
250+
// This will call the onCompletion method below.
251+
gattEventCollector?.cancel()
252+
} else {
253+
// If the connection will be retried, switch immediately to Connecting state.
254+
// Note: This changes state Connected -> Connecting, without going through
255+
// Disconnected state.
256+
handle(ConnectionStateChanged(ConnectionState.Connecting))
257+
}
258+
}
259+
// Handle other events.
260+
else -> handle(event)
261+
}
262+
}
250263
.onCompletion {
251264
gattEventCollector = null
252265
close()
@@ -266,7 +279,6 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
266279
handleClose()
267280
serviceDiscoveryRequested = false
268281
impl.close()
269-
_state.update { ConnectionState.Closed }
270282
}
271283
gattEventCollector = null
272284
}
@@ -315,6 +327,9 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
315327
is ConnectionState.Connected -> {
316328
initiateConnection()
317329
}
330+
// When the link is lost, a peripheral state may transition from Connected
331+
// to Connecting or Disconnected.
332+
is ConnectionState.Connecting,
318333
is ConnectionState.Disconnected -> {
319334
handleDisconnection()
320335
}
@@ -486,10 +501,6 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
486501
suspend fun disconnect() {
487502
// Depending on the state...
488503
when (state.value) {
489-
is ConnectionState.Closed -> {
490-
// Do nothing when already closed.
491-
return
492-
}
493504
is ConnectionState.Disconnected -> {
494505
// Make sure auto-connection is closed.
495506
close()

core/src/main/java/no/nordicsemi/kotlin/ble/core/ConnectionState.kt

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,30 +33,54 @@
3333

3434
package no.nordicsemi.kotlin.ble.core
3535

36+
import no.nordicsemi.kotlin.ble.core.ConnectionState.Disconnected.Reason.Cancelled
37+
import no.nordicsemi.kotlin.ble.core.ConnectionState.Disconnected.Reason.Success
3638
import kotlin.time.Duration
3739

3840
/**
3941
* Connection state of a Bluetooth LE peripheral.
4042
*/
4143
sealed class ConnectionState {
4244

43-
/** Connection has been initiated. */
45+
/**
46+
* The client is trying to connect to the peripheral.
47+
*
48+
* When connected, the state will change to [Connected].
49+
*
50+
* On timeout or cancellation, the state will change to [Disconnected] with the correct reason.
51+
*/
4452
data object Connecting: ConnectionState()
4553

46-
/** The peripheral is connected. */
54+
/**
55+
* The peripheral is connected.
56+
*
57+
* When the connection terminates, the state will change to [Disconnected]
58+
* or [Connecting] if the client will try to reconnect.
59+
*/
4760
data object Connected: ConnectionState()
4861

49-
/** Disconnection has been initiated. */
62+
/**
63+
* Disconnection has been initiated.
64+
*
65+
* This state is set when the `disconnect()` method is called on the peripheral.
66+
*
67+
* This is followed by [Disconnected] state with reason [Success][Disconnected.Reason.Success].
68+
*/
5069
data object Disconnecting: ConnectionState()
5170

5271
/**
5372
* Device has disconnected.
5473
*
55-
* This state may be immediately followed by [Closed] state.
74+
* If the [reason] is null, it means that no connection attempt was made. This is
75+
* the initial state of the peripheral.
76+
*
77+
* Note, that it doesn't mean that the peripheral is not connected, it may be connected
78+
* using another [Manager].
5679
*
57-
* @param reason Reason of disconnection.
80+
* @param reason Reason of disconnection or `null` if the connection was never initiated.
81+
* This is the initial state of the peripheral.
5882
*/
59-
data class Disconnected(val reason: Reason): ConnectionState() {
83+
data class Disconnected(val reason: Reason? = null): ConnectionState() {
6084

6185
/** Reason of disconnection. */
6286
sealed class Reason {
@@ -115,21 +139,22 @@ sealed class ConnectionState {
115139
* @property duration The duration of the timeout.
116140
*/
117141
data class Timeout(val duration: Duration): Reason()
118-
119-
/** A quick check whether the disconnection was initiated by the user. */
120-
val isUserInitiated: Boolean
121-
get() = this is Success || this is Cancelled
122142
}
123-
}
124143

125-
/** The connection is closed. */
126-
data object Closed: ConnectionState()
144+
/**
145+
* A quick check whether the disconnection was initiated by the user.
146+
*
147+
* This returns `false` in the initial state, that is before the connection attempt was started.
148+
*/
149+
val isUserInitiated: Boolean
150+
get() = reason is Success || reason is Cancelled
151+
}
127152

128153
/** Whether the connection is open. */
129154
val isConnected: Boolean
130155
get() = this is Connected
131156

132157
/** Whether the connection closed or getting closed. */
133158
val isDisconnected: Boolean
134-
get() = this is Disconnected || this is Closed
159+
get() = this is Disconnected
135160
}

sample/src/main/java/no/nordicsemi/kotlin/ble/android/sample/scanner/ScannerViewModel.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import kotlinx.coroutines.delay
4141
import kotlinx.coroutines.flow.MutableStateFlow
4242
import kotlinx.coroutines.flow.StateFlow
4343
import kotlinx.coroutines.flow.asStateFlow
44-
import kotlinx.coroutines.flow.buffer
4544
import kotlinx.coroutines.flow.catch
4645
import kotlinx.coroutines.flow.filterNot
4746
import kotlinx.coroutines.flow.filterNotNull
@@ -355,7 +354,6 @@ class ScannerViewModel @Inject constructor(
355354

356355
private fun observePeripheralState(peripheral: Peripheral, scope: CoroutineScope) {
357356
peripheral.state
358-
.buffer()
359357
.onEach {
360358
Timber.i("State of $peripheral: $it")
361359

@@ -367,7 +365,7 @@ class ScannerViewModel @Inject constructor(
367365
}
368366
}
369367

370-
is ConnectionState.Closed -> {
368+
is ConnectionState.Disconnected -> {
371369
// Just for testing, wait with cancelling the scope to get all the logs.
372370
delay(500)
373371
// Cancel connection scope, so that previously launched jobs are cancelled.

0 commit comments

Comments
 (0)