Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide Peripheral CoroutineScope via scope property #846

Merged
merged 2 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,29 +169,29 @@ val peripheral = Peripheral(advertisement) {
```

[`Peripheral`] objects represent actions that can be performed against a remote peripheral, such as connection
handling and I/O operations. [`Peripheral`] objects are themselves [`CoroutineScope`]s, and coroutines can be
`launch`ed from them:
handling and I/O operations. [`Peripheral`] objects provide a [`CoroutineScope`] `scope` property, and coroutines
can be `launch`ed from it:

```kotlin
peripheral.launch {
// Long running task that will be cancelled when peripheral
// is disposed (i.e. `Peripheral.cancel()` is called).
peripheral.scope.launch {
// Long running task that will be cancelled when peripheral is disposed
// (i.e. `peripheral.close()` is called).
}
```

> [!IMPORTANT]
> When a [`Peripheral`] is no longer needed, it should be disposed via `cancel`:
> When a [`Peripheral`] is no longer needed, it should be disposed via `close`:
>
> ```kotlin
> peripheral.cancel()
> peripheral.close()
> ```
>
> Once a [`Peripheral`] is cancelled (via `cancel`) it can no longer be used (e.g. calling `connect` will throw
> Once a [`Peripheral`] is disposed (via `close`) it can no longer be used (e.g. calling `connect` will throw
> `IllegalStateException`).

> [!TIP]
> `launch`ed coroutines from a `Peripheral` object are permitted to run until `Peripheral.cancel()` is called
> (i.e. can span across reconnects); for tasks that should only run for the duration of a single connection
> `launch`ed coroutines from a `Peripheral` object's `scope` are permitted to run until `Peripheral.dispose()`
> is called (i.e. can span across reconnects); for tasks that should only run for the duration of a single connection
> (i.e. shutdown on disconnect), `launch` via the `CoroutineScope` returned from `Peripheral.connect` instead.

### Configuration
Expand Down
3 changes: 2 additions & 1 deletion kable-core/api/android/kable-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,12 @@ public final class com/juul/kable/OnDemandThreadingStrategy : com/juul/kable/Thr
public fun release (Lcom/juul/kable/Threading;)V
}

public abstract interface class com/juul/kable/Peripheral : kotlinx/coroutines/CoroutineScope {
public abstract interface class com/juul/kable/Peripheral : java/lang/AutoCloseable {
public abstract fun connect (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun disconnect (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getIdentifier ()Ljava/lang/String;
public abstract fun getName ()Ljava/lang/String;
public abstract fun getScope ()Lkotlinx/coroutines/CoroutineScope;
public abstract fun getServices ()Lkotlinx/coroutines/flow/StateFlow;
public abstract fun getState ()Lkotlinx/coroutines/flow/StateFlow;
public abstract fun maximumWriteValueLengthForType (Lcom/juul/kable/WriteType;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand Down
3 changes: 2 additions & 1 deletion kable-core/api/jvm/kable-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,12 @@ public final class com/juul/kable/ObservationExceptionPeripheral {
public abstract interface annotation class com/juul/kable/ObsoleteKableApi : java/lang/annotation/Annotation {
}

public abstract interface class com/juul/kable/Peripheral : kotlinx/coroutines/CoroutineScope {
public abstract interface class com/juul/kable/Peripheral : java/lang/AutoCloseable {
public abstract fun connect (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun disconnect (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getIdentifier ()Ljava/lang/String;
public abstract fun getName ()Ljava/lang/String;
public abstract fun getScope ()Lkotlinx/coroutines/CoroutineScope;
public abstract fun getServices ()Lkotlinx/coroutines/flow/StateFlow;
public abstract fun getState ()Lkotlinx/coroutines/flow/StateFlow;
public abstract fun maximumWriteValueLengthForType (Lcom/juul/kable/WriteType;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.juul.kable.logs.Logging
import com.juul.kable.logs.Logging.DataProcessor.Operation
import com.juul.kable.logs.detail
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -72,7 +73,7 @@ internal class BluetoothDeviceAndroidPeripheral(
}
}

private val connectAction = sharedRepeatableAction(::establishConnection)
private val connectAction = scope.sharedRepeatableAction(::establishConnection)

override val identifier: String = bluetoothDevice.address
private val logger = Logger(logging, "Kable/Peripheral", bluetoothDevice.toString())
Expand Down Expand Up @@ -349,7 +350,11 @@ internal class BluetoothDeviceAndroidPeripheral(
bluetoothState
.filter { state -> state == STATE_TURNING_OFF || state == STATE_OFF }
.onEach(action)
.launchIn(this)
.launchIn(scope)
}

override fun close() {
scope.cancel("$this closed")
}

override fun toString(): String = "Peripheral(bluetoothDevice=$bluetoothDevice)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public fun CoroutineScope.peripheral(
builder.disconnectTimeout,
)
coroutineContext.job.invokeOnCompletion {
peripheral.cancel()
peripheral.scope.cancel()
}
return peripheral
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.juul.kable.logs.Logging.DataProcessor.Operation.Write
import com.juul.kable.logs.detail
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -82,7 +83,7 @@ internal class CBPeripheralCoreBluetoothPeripheral(
}
}

private val connectAction = sharedRepeatableAction(::establishConnection)
private val connectAction = scope.sharedRepeatableAction(::establishConnection)

private val observers = Observers<NSData>(this, logging, exceptionHandler = observationExceptionHandler)
private val canSendWriteWithoutResponse = MutableStateFlow(cbPeripheral.canSendWriteWithoutResponse)
Expand Down Expand Up @@ -345,14 +346,14 @@ internal class CBPeripheralCoreBluetoothPeripheral(
.filter { event -> event.identifier == cbPeripheral.identifier }
.map(ConnectionEvent::toState)
.onEach(action)
.launchIn(this)
.launchIn(scope)
}

private fun onBluetoothPoweredOff(action: suspend (CBManagerState) -> Unit) {
central.delegate
.state.filter { state -> state != CBManagerStatePoweredOn }
.onEach(action)
.launchIn(this)
.launchIn(scope)
}

private fun createPeripheralDelegate() = PeripheralDelegate(
Expand All @@ -362,6 +363,10 @@ internal class CBPeripheralCoreBluetoothPeripheral(
cbPeripheral.identifier.UUIDString,
)

override fun close() {
scope.cancel("$this closed")
}

override fun toString(): String = "Peripheral(cbPeripheral=$cbPeripheral)"
}

Expand Down
6 changes: 4 additions & 2 deletions kable-core/src/commonMain/kotlin/BasePeripheral.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.juul.kable

import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope

internal abstract class BasePeripheral(identifier: Identifier) : Peripheral {

override val coroutineContext =
SilentSupervisor() + CoroutineName("Kable/Peripheral/$identifier")
override val scope = CoroutineScope(
SilentSupervisor() + CoroutineName("Kable/Peripheral/$identifier"),
)
}
12 changes: 11 additions & 1 deletion kable-core/src/commonMain/kotlin/Peripheral.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,17 @@ public enum class WriteType {
WithoutResponse,
}

public interface Peripheral : CoroutineScope {
/**
* Represents a remote Bluetooth Low Energy peripheral. Should be disposed (via [close]) when no
* longer needed.
*/
public interface Peripheral : AutoCloseable {

/**
* [CoroutineScope] tied to the lifecycle of this [Peripheral] (is cancelled upon [Peripheral]
* being disposed via [close]).
*/
public val scope: CoroutineScope

/**
* Provides a conflated [Flow] of the [Peripheral]'s [State].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart.UNDISPATCHED
import kotlinx.coroutines.async
import kotlinx.coroutines.await
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -43,7 +44,7 @@ internal class BluetoothDeviceWebBluetoothPeripheral(
private val logging: Logging,
) : BasePeripheral(bluetoothDevice.id), WebBluetoothPeripheral {

private val connectAction = sharedRepeatableAction(::establishConnection)
private val connectAction = scope.sharedRepeatableAction(::establishConnection)

private val logger = Logger(logging, identifier = bluetoothDevice.id)

Expand Down Expand Up @@ -283,5 +284,9 @@ internal class BluetoothDeviceWebBluetoothPeripheral(
connectionOrThrow().stopObservation(characteristic)
}

override fun close() {
scope.cancel("$this closed")
}

override fun toString(): String = "Peripheral(bluetoothDevice=${bluetoothDevice.string()})"
}