Skip to content

Commit

Permalink
Support constructing Peripherals using builder lambda (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
twyatt authored Jun 9, 2021
1 parent 7eee085 commit f171302
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 81 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,41 @@ connection handling and I/O operations.
val peripheral = scope.peripheral(advertisement)
```

To configure a `peripheral`, options may be set in the builder lambda:

```kotlin
val peripheral = scope.peripheral(advertisement) {
// Set peripheral configuration.
}
```

All platforms support an `onServicesDiscovered` action (that is executed after service discovery but before observations
are wired up):

```kotlin
val peripheral = scope.peripheral(advertisement) {
onServicesDiscovered {
// Perform any desired I/O operations.
}
}
```

_Exceptions thrown in `onServicesDiscovered` are propagated to the `Peripheral`'s [`connect`] call._

### Android

On Android targets, additional configuration options are available (all configuration directives are optional):

```kotlin
val peripheral = scope.peripheral(advertisement) {
onServicesDiscovered {
requestMtu(...)
}
transport = Transport.Le // default
phy = Phy.Le1M // default
}
```

### JavaScript

On JavaScript, rather than processing a stream of advertisements, a specific peripheral can be requested using the
Expand Down
22 changes: 21 additions & 1 deletion core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,25 @@ public final class com/juul/kable/Peripheral$DefaultImpls {
public static synthetic fun write$default (Lcom/juul/kable/Peripheral;Lcom/juul/kable/Characteristic;[BLcom/juul/kable/WriteType;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}

public final class com/juul/kable/PeripheralBuilder {
public final fun getPhy ()Lcom/juul/kable/Phy;
public final fun getTransport ()Lcom/juul/kable/Transport;
public final fun onServicesDiscovered (Lkotlin/jvm/functions/Function2;)V
public final fun setPhy (Lcom/juul/kable/Phy;)V
public final fun setTransport (Lcom/juul/kable/Transport;)V
}

public final class com/juul/kable/PeripheralKt {
public static final fun peripheral (Lkotlinx/coroutines/CoroutineScope;Landroid/bluetooth/BluetoothDevice;Lcom/juul/kable/Transport;Lcom/juul/kable/Phy;)Lcom/juul/kable/Peripheral;
public static final fun peripheral (Lkotlinx/coroutines/CoroutineScope;Landroid/bluetooth/BluetoothDevice;Lcom/juul/kable/WriteNotificationDescriptor;)Lcom/juul/kable/Peripheral;
public static final fun peripheral (Lkotlinx/coroutines/CoroutineScope;Lcom/juul/kable/Advertisement;)Lcom/juul/kable/Peripheral;
public static final fun peripheral (Lkotlinx/coroutines/CoroutineScope;Landroid/bluetooth/BluetoothDevice;Lkotlin/jvm/functions/Function1;)Lcom/juul/kable/Peripheral;
public static final fun peripheral (Lkotlinx/coroutines/CoroutineScope;Lcom/juul/kable/Advertisement;Lcom/juul/kable/Transport;Lcom/juul/kable/Phy;)Lcom/juul/kable/Peripheral;
public static final fun peripheral (Lkotlinx/coroutines/CoroutineScope;Lcom/juul/kable/Advertisement;Lcom/juul/kable/WriteNotificationDescriptor;)Lcom/juul/kable/Peripheral;
public static final fun peripheral (Lkotlinx/coroutines/CoroutineScope;Lcom/juul/kable/Advertisement;Lkotlin/jvm/functions/Function1;)Lcom/juul/kable/Peripheral;
public static synthetic fun peripheral$default (Lkotlinx/coroutines/CoroutineScope;Landroid/bluetooth/BluetoothDevice;Lcom/juul/kable/Transport;Lcom/juul/kable/Phy;ILjava/lang/Object;)Lcom/juul/kable/Peripheral;
public static synthetic fun peripheral$default (Lkotlinx/coroutines/CoroutineScope;Landroid/bluetooth/BluetoothDevice;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/juul/kable/Peripheral;
public static synthetic fun peripheral$default (Lkotlinx/coroutines/CoroutineScope;Lcom/juul/kable/Advertisement;Lcom/juul/kable/Transport;Lcom/juul/kable/Phy;ILjava/lang/Object;)Lcom/juul/kable/Peripheral;
public static synthetic fun peripheral$default (Lkotlinx/coroutines/CoroutineScope;Lcom/juul/kable/Advertisement;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/juul/kable/Peripheral;
}

public final class com/juul/kable/Phy : java/lang/Enum {
Expand All @@ -206,6 +217,15 @@ public abstract interface class com/juul/kable/Service {
public abstract fun getServiceUuid ()Ljava/util/UUID;
}

public final class com/juul/kable/ServicesDiscoveredPeripheral {
public final fun read (Lcom/juul/kable/Characteristic;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun read (Lcom/juul/kable/Descriptor;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun requestMtu (ILkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun write (Lcom/juul/kable/Characteristic;[BLcom/juul/kable/WriteType;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun write (Lcom/juul/kable/Descriptor;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun write$default (Lcom/juul/kable/ServicesDiscoveredPeripheral;Lcom/juul/kable/Characteristic;[BLcom/juul/kable/WriteType;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}

public abstract class com/juul/kable/State {
}

Expand Down
100 changes: 41 additions & 59 deletions core/src/androidMain/kotlin/Peripheral.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,98 +37,78 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlin.DeprecationLevel.WARNING
import kotlin.coroutines.CoroutineContext

private val clientCharacteristicConfigUuid = uuidFrom(CLIENT_CHARACTERISTIC_CONFIG_UUID)

/** Preferred transport for GATT connections to remote dual-mode devices. */
public enum class Transport {

/** No preference of physical transport for GATT connections to remote dual-mode devices. */
Auto,

/** Prefer BR/EDR transport for GATT connections to remote dual-mode devices. */
BrEdr,

/** Prefer LE transport for GATT connections to remote dual-mode devices. */
Le,
}

/** Preferred Physical Layer (PHY) for connections to remote LE devices. */
public enum class Phy {

/** Bluetooth LE 1M PHY. */
Le1M,

/**
* Bluetooth LE 2M PHY.
*
* Per [Exploring Bluetooth 5 – Going the Distance](https://www.bluetooth.com/blog/exploring-bluetooth-5-going-the-distance/#mcetoc_1d7vdh6b25):
* "The new LE 2M PHY allows the physical layer to operate at 2 Ms/s and thus enables higher data rates than LE 1M
* and Bluetooth 4."
*/
Le2M,

/**
* Bluetooth LE Coded PHY.
*
* Per [Exploring Bluetooth 5 – Going the Distance](https://www.bluetooth.com/blog/exploring-bluetooth-5-going-the-distance/#mcetoc_1d7vdh6b26):
* "The LE Coded PHY allows range to be quadrupled (approximately), compared to Bluetooth® 4 and this has been
* accomplished without increasing the transmission power required."
*/
LeCoded,
}
@Deprecated(
message = "'writeObserveDescriptor' parameter is no longer used and is handled automatically by 'observe' function. 'writeObserveDescriptor' argument will be removed in a future release.",
replaceWith = ReplaceWith("peripheral(advertisement)"),
level = DeprecationLevel.ERROR,
)
public fun CoroutineScope.peripheral(
bluetoothDevice: BluetoothDevice,
writeObserveDescriptor: WriteNotificationDescriptor,
): Peripheral = throw UnsupportedOperationException()

public actual fun CoroutineScope.peripheral(
@Deprecated(
message = "'writeObserveDescriptor' parameter is no longer used and is handled automatically by 'observe' function. 'writeObserveDescriptor' argument will be removed in a future release.",
replaceWith = ReplaceWith("peripheral(advertisement)"),
level = DeprecationLevel.ERROR,
)
public fun CoroutineScope.peripheral(
advertisement: Advertisement,
): Peripheral = peripheral(advertisement.bluetoothDevice)
writeObserveDescriptor: WriteNotificationDescriptor,
): Peripheral = throw UnsupportedOperationException()

/**
* @param transport preferred transport for GATT connections to remote dual-mode devices.
* @param phy preferred PHY for connections to remote LE device.
*/
@Deprecated(message = "Use builder lambda.")
public fun CoroutineScope.peripheral(
advertisement: Advertisement,
transport: Transport = Transport.Le,
transport: Transport,
phy: Phy = Phy.Le1M,
): Peripheral = peripheral(advertisement.bluetoothDevice, transport, phy)
): Peripheral = peripheral(advertisement) {
this.transport = transport
this.phy = phy
}

/**
* @param transport preferred transport for GATT connections to remote dual-mode devices.
* @param phy preferred PHY for connections to remote LE device.
*/
@Deprecated(message = "Use builder lambda.")
public fun CoroutineScope.peripheral(
bluetoothDevice: BluetoothDevice,
transport: Transport = Transport.Le,
transport: Transport,
phy: Phy = Phy.Le1M,
): Peripheral = AndroidPeripheral(coroutineContext, bluetoothDevice, transport, phy)
): Peripheral = peripheral(bluetoothDevice) {
this.transport = transport
this.phy = phy
}

@Deprecated(
message = "'writeObserveDescriptor' parameter is no longer used and is handled automatically by 'observe' function. 'writeObserveDescriptor' argument will be removed in a future release.",
replaceWith = ReplaceWith("peripheral(advertisement)"),
level = WARNING,
)
public fun CoroutineScope.peripheral(
public actual fun CoroutineScope.peripheral(
advertisement: Advertisement,
writeObserveDescriptor: WriteNotificationDescriptor,
): Peripheral = peripheral(advertisement.bluetoothDevice, writeObserveDescriptor)
builderAction: PeripheralBuilderAction,
): Peripheral = peripheral(advertisement.bluetoothDevice, builderAction)

@Deprecated(
message = "'writeObserveDescriptor' parameter is no longer used and is handled automatically by 'observe' function. 'writeObserveDescriptor' argument will be removed in a future release.",
replaceWith = ReplaceWith("peripheral(advertisement)"),
level = WARNING,
)
public fun CoroutineScope.peripheral(
bluetoothDevice: BluetoothDevice,
writeObserveDescriptor: WriteNotificationDescriptor,
): Peripheral = AndroidPeripheral(coroutineContext, bluetoothDevice, Transport.Le, Phy.Le1M)
builderAction: PeripheralBuilderAction = {},
): Peripheral {
val builder = PeripheralBuilder()
builder.builderAction()
return AndroidPeripheral(coroutineContext, bluetoothDevice, builder.transport, builder.phy, builder.onServicesDiscovered)
}

public class AndroidPeripheral internal constructor(
parentCoroutineContext: CoroutineContext,
private val bluetoothDevice: BluetoothDevice,
private val transport: Transport,
private val phy: Phy,
private val onServicesDiscovered: ServicesDiscoveredAction,
) : Peripheral {

private val job = SupervisorJob(parentCoroutineContext[Job]).apply {
Expand Down Expand Up @@ -183,6 +163,7 @@ public class AndroidPeripheral internal constructor(
try {
suspendUntilConnected()
discoverServices()
onServicesDiscovered(ServicesDiscoveredPeripheral(this@AndroidPeripheral))
observers.rewire()
} catch (t: Throwable) {
dispose()
Expand Down Expand Up @@ -226,6 +207,7 @@ public class AndroidPeripheral internal constructor(
.map { it.toPlatformService() }
}

/** @throws NotReadyException if invoked without an established [connection][Peripheral.connect]. */
public suspend fun requestMtu(mtu: Int) {
connection.execute<OnMtuChanged> {
this@execute.requestMtu(mtu)
Expand Down
85 changes: 85 additions & 0 deletions core/src/androidMain/kotlin/PeripheralBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.juul.kable

/** Preferred transport for GATT connections to remote dual-mode devices. */
public enum class Transport {

/** No preference of physical transport for GATT connections to remote dual-mode devices. */
Auto,

/** Prefer BR/EDR transport for GATT connections to remote dual-mode devices. */
BrEdr,

/** Prefer LE transport for GATT connections to remote dual-mode devices. */
Le,
}

/** Preferred Physical Layer (PHY) for connections to remote LE devices. */
public enum class Phy {

/** Bluetooth LE 1M PHY. */
Le1M,

/**
* Bluetooth LE 2M PHY.
*
* Per [Exploring Bluetooth 5 – Going the Distance](https://www.bluetooth.com/blog/exploring-bluetooth-5-going-the-distance/#mcetoc_1d7vdh6b25):
* "The new LE 2M PHY allows the physical layer to operate at 2 Ms/s and thus enables higher data rates than LE 1M
* and Bluetooth 4."
*/
Le2M,

/**
* Bluetooth LE Coded PHY.
*
* Per [Exploring Bluetooth 5 – Going the Distance](https://www.bluetooth.com/blog/exploring-bluetooth-5-going-the-distance/#mcetoc_1d7vdh6b26):
* "The LE Coded PHY allows range to be quadrupled (approximately), compared to Bluetooth® 4 and this has been
* accomplished without increasing the transmission power required."
*/
LeCoded,
}

public actual class ServicesDiscoveredPeripheral internal constructor(
private val peripheral: AndroidPeripheral
) {

public actual suspend fun read(
characteristic: Characteristic,
): ByteArray = peripheral.read(characteristic)

public actual suspend fun read(
descriptor: Descriptor,
): ByteArray = peripheral.read(descriptor)

public actual suspend fun write(
characteristic: Characteristic,
data: ByteArray,
writeType: WriteType,
) {
peripheral.write(characteristic, data, writeType)
}

public actual suspend fun write(
descriptor: Descriptor,
data: ByteArray,
) {
peripheral.write(descriptor, data)
}

public suspend fun requestMtu(
mtu: Int
): Unit = peripheral.requestMtu(mtu)
}

public actual class PeripheralBuilder internal actual constructor() {

internal var onServicesDiscovered: ServicesDiscoveredAction = {}
public actual fun onServicesDiscovered(action: ServicesDiscoveredAction) {
onServicesDiscovered = action
}

/** Preferred transport for GATT connections to remote dual-mode devices. */
public var transport: Transport = Transport.Le

/** Preferred PHY for connections to remote LE device. */
public var phy: Phy = Phy.Le1M
}
13 changes: 12 additions & 1 deletion core/src/appleMain/kotlin/Peripheral.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,22 @@ import kotlin.native.concurrent.freeze

public actual fun CoroutineScope.peripheral(
advertisement: Advertisement,
): Peripheral = ApplePeripheral(coroutineContext, advertisement.cbPeripheral)
builderAction: PeripheralBuilderAction,
): Peripheral {
val builder = PeripheralBuilder()
builder.builderAction()
return ApplePeripheral(
coroutineContext,
advertisement.cbPeripheral,
builder.onServicesDiscovered
)
}

@OptIn(ExperimentalStdlibApi::class) // for CancellationException in @Throws
public class ApplePeripheral internal constructor(
parentCoroutineContext: CoroutineContext,
private val cbPeripheral: CBPeripheral,
private val onServicesDiscovered: ServicesDiscoveredAction,
) : Peripheral {

private val job = SupervisorJob(parentCoroutineContext.job) // todo: Disconnect/dispose CBPeripheral on completion?
Expand Down Expand Up @@ -142,6 +152,7 @@ public class ApplePeripheral internal constructor(
suspendUntilConnected()

discoverServices()
onServicesDiscovered(ServicesDiscoveredPeripheral(this@ApplePeripheral))
observers.rewire()
} catch (t: Throwable) {
withContext(NonCancellable) {
Expand Down
Loading

0 comments on commit f171302

Please sign in to comment.