From cb9e6efa3a457b50928b1931123fb732e837f887 Mon Sep 17 00:00:00 2001 From: Travis Wyatt Date: Tue, 28 Jan 2025 14:09:03 -0800 Subject: [PATCH] Provide `Peripheral` `CoroutineScope` via `scope` property --- README.md | 20 +++++++++---------- kable-core/api/android/kable-core.api | 3 ++- kable-core/api/jvm/kable-core.api | 3 ++- .../BluetoothDeviceAndroidPeripheral.kt | 9 +++++++-- .../kotlin/Peripheral.deprecated.kt | 2 +- .../CBPeripheralCoreBluetoothPeripheral.kt | 11 +++++++--- .../src/commonMain/kotlin/BasePeripheral.kt | 6 ++++-- .../src/commonMain/kotlin/Peripheral.kt | 12 ++++++++++- .../BluetoothDeviceWebBluetoothPeripheral.kt | 7 ++++++- 9 files changed, 51 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index a1f4fe560..c255ab3c1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/kable-core/api/android/kable-core.api b/kable-core/api/android/kable-core.api index 046ed650b..b2e2909a3 100644 --- a/kable-core/api/android/kable-core.api +++ b/kable-core/api/android/kable-core.api @@ -312,11 +312,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; diff --git a/kable-core/api/jvm/kable-core.api b/kable-core/api/jvm/kable-core.api index 3ee1dca16..14c390393 100644 --- a/kable-core/api/jvm/kable-core.api +++ b/kable-core/api/jvm/kable-core.api @@ -232,11 +232,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; diff --git a/kable-core/src/androidMain/kotlin/BluetoothDeviceAndroidPeripheral.kt b/kable-core/src/androidMain/kotlin/BluetoothDeviceAndroidPeripheral.kt index a0fcf8eec..deba3082f 100644 --- a/kable-core/src/androidMain/kotlin/BluetoothDeviceAndroidPeripheral.kt +++ b/kable-core/src/androidMain/kotlin/BluetoothDeviceAndroidPeripheral.kt @@ -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 @@ -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()) @@ -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)" diff --git a/kable-core/src/androidMain/kotlin/Peripheral.deprecated.kt b/kable-core/src/androidMain/kotlin/Peripheral.deprecated.kt index 5bf7296ea..7d1933fdc 100644 --- a/kable-core/src/androidMain/kotlin/Peripheral.deprecated.kt +++ b/kable-core/src/androidMain/kotlin/Peripheral.deprecated.kt @@ -38,7 +38,7 @@ public fun CoroutineScope.peripheral( builder.disconnectTimeout, ) coroutineContext.job.invokeOnCompletion { - peripheral.cancel() + peripheral.scope.cancel() } return peripheral } diff --git a/kable-core/src/appleMain/kotlin/CBPeripheralCoreBluetoothPeripheral.kt b/kable-core/src/appleMain/kotlin/CBPeripheralCoreBluetoothPeripheral.kt index 32ade24b1..26614cbae 100644 --- a/kable-core/src/appleMain/kotlin/CBPeripheralCoreBluetoothPeripheral.kt +++ b/kable-core/src/appleMain/kotlin/CBPeripheralCoreBluetoothPeripheral.kt @@ -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 @@ -82,7 +83,7 @@ internal class CBPeripheralCoreBluetoothPeripheral( } } - private val connectAction = sharedRepeatableAction(::establishConnection) + private val connectAction = scope.sharedRepeatableAction(::establishConnection) private val observers = Observers(this, logging, exceptionHandler = observationExceptionHandler) private val canSendWriteWithoutResponse = MutableStateFlow(cbPeripheral.canSendWriteWithoutResponse) @@ -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( @@ -362,6 +363,10 @@ internal class CBPeripheralCoreBluetoothPeripheral( cbPeripheral.identifier.UUIDString, ) + override fun close() { + scope.cancel("$this closed") + } + override fun toString(): String = "Peripheral(cbPeripheral=$cbPeripheral)" } diff --git a/kable-core/src/commonMain/kotlin/BasePeripheral.kt b/kable-core/src/commonMain/kotlin/BasePeripheral.kt index 10b5860a9..7fe135d7d 100644 --- a/kable-core/src/commonMain/kotlin/BasePeripheral.kt +++ b/kable-core/src/commonMain/kotlin/BasePeripheral.kt @@ -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"), + ) } diff --git a/kable-core/src/commonMain/kotlin/Peripheral.kt b/kable-core/src/commonMain/kotlin/Peripheral.kt index 1ce86048c..30d702e30 100644 --- a/kable-core/src/commonMain/kotlin/Peripheral.kt +++ b/kable-core/src/commonMain/kotlin/Peripheral.kt @@ -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]. diff --git a/kable-core/src/jsMain/kotlin/BluetoothDeviceWebBluetoothPeripheral.kt b/kable-core/src/jsMain/kotlin/BluetoothDeviceWebBluetoothPeripheral.kt index f56064cdf..67a9946dc 100644 --- a/kable-core/src/jsMain/kotlin/BluetoothDeviceWebBluetoothPeripheral.kt +++ b/kable-core/src/jsMain/kotlin/BluetoothDeviceWebBluetoothPeripheral.kt @@ -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 @@ -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) @@ -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()})" }