Skip to content

Commit

Permalink
Allow for configuration of transport and PHY on Android (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
twyatt authored Apr 28, 2021
1 parent 27c6469 commit 151e54d
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 11 deletions.
21 changes: 20 additions & 1 deletion core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,21 @@ public final class com/juul/kable/Peripheral$DefaultImpls {
}

public final class com/juul/kable/PeripheralKt {
public static final fun peripheral (Lkotlinx/coroutines/CoroutineScope;Landroid/bluetooth/BluetoothDevice;)Lcom/juul/kable/Peripheral;
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;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 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;Lcom/juul/kable/Advertisement;Lcom/juul/kable/Transport;Lcom/juul/kable/Phy;ILjava/lang/Object;)Lcom/juul/kable/Peripheral;
}

public final class com/juul/kable/Phy : java/lang/Enum {
public static final field Le1M Lcom/juul/kable/Phy;
public static final field Le2M Lcom/juul/kable/Phy;
public static final field LeCoded Lcom/juul/kable/Phy;
public static fun valueOf (Ljava/lang/String;)Lcom/juul/kable/Phy;
public static fun values ()[Lcom/juul/kable/Phy;
}

public final class com/juul/kable/ScanFailedException : java/lang/IllegalStateException {
Expand Down Expand Up @@ -265,6 +276,14 @@ public final class com/juul/kable/State$Disconnecting : com/juul/kable/State {
public static final field INSTANCE Lcom/juul/kable/State$Disconnecting;
}

public final class com/juul/kable/Transport : java/lang/Enum {
public static final field Auto Lcom/juul/kable/Transport;
public static final field BrEdr Lcom/juul/kable/Transport;
public static final field Le Lcom/juul/kable/Transport;
public static fun valueOf (Ljava/lang/String;)Lcom/juul/kable/Transport;
public static fun values ()[Lcom/juul/kable/Transport;
}

public final class com/juul/kable/WriteNotificationDescriptor : java/lang/Enum {
public static final field Always Lcom/juul/kable/WriteNotificationDescriptor;
public static final field Auto Lcom/juul/kable/WriteNotificationDescriptor;
Expand Down
49 changes: 41 additions & 8 deletions core/src/androidMain/kotlin/BluetoothDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package com.juul.kable
import android.annotation.TargetApi
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothDevice.PHY_LE_1M_MASK
import android.bluetooth.BluetoothDevice.PHY_LE_2M_MASK
import android.bluetooth.BluetoothDevice.PHY_LE_CODED_MASK
import android.bluetooth.BluetoothDevice.TRANSPORT_AUTO
import android.bluetooth.BluetoothDevice.TRANSPORT_BREDR
import android.bluetooth.BluetoothDevice.TRANSPORT_LE
import android.content.Context
import android.os.Build
import android.os.Handler
Expand All @@ -13,25 +17,39 @@ import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.newSingleThreadContext

/**
* @param transport is only used on API level >= 23.
* @param phy is only used on API level >= 26.
*/
internal fun BluetoothDevice.connect(
context: Context,
transport: Transport,
phy: Phy,
state: MutableStateFlow<State>,
invokeOnClose: () -> Unit,
): Connection? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
connectApi26(context, state, invokeOnClose)
connectApi26(context, transport, phy, state, invokeOnClose)
} else {
connectApi21(context, state, invokeOnClose)
connectApi21(context, transport, state, invokeOnClose)
}

/**
* @param transport is only used on API level >= 23.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun BluetoothDevice.connectApi21(
context: Context,
transport: Transport,
state: MutableStateFlow<State>,
invokeOnClose: () -> Unit,
): Connection? {
val callback = Callback(state)
val bluetoothGatt = connectGatt(context, false, callback) ?: return null
val bluetoothGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
connectGatt(context, false, callback, transport.intValue)
} else {
connectGatt(context, false, callback)
} ?: return null

// Explicitly set Connecting state so when Peripheral is suspending until Connected, it doesn't incorrectly see
// Disconnected before the connection request has kicked off the Connecting state (via Callback).
Expand All @@ -52,6 +70,8 @@ private fun BluetoothDevice.connectApi21(
@TargetApi(Build.VERSION_CODES.O)
private fun BluetoothDevice.connectApi26(
context: Context,
transport: Transport,
phy: Phy,
state: MutableStateFlow<State>,
invokeOnClose: () -> Unit,
): Connection? {
Expand All @@ -61,12 +81,9 @@ private fun BluetoothDevice.connectApi26(
val dispatcher = handler.asCoroutineDispatcher()
val callback = Callback(state)

// todo: Have `transport` and `phy` be configurable.
val transport = TRANSPORT_AUTO
val phy = PHY_LE_1M_MASK

val bluetoothGatt =
connectGatt(context, false, callback, transport, phy, handler) ?: return null
connectGatt(context, false, callback, transport.intValue, phy.intValue, handler)
?: return null

// Explicitly set Connecting state so when Peripheral is suspending until Connected, it doesn't incorrectly see
// Disconnected before the connection request has kicked off the Connecting state (via Callback).
Expand All @@ -87,5 +104,21 @@ private fun BluetoothDevice.connectApi26(
}
}

private val Transport.intValue: Int
@TargetApi(Build.VERSION_CODES.M)
get() = when (this) {
Transport.Auto -> TRANSPORT_AUTO
Transport.BrEdr -> TRANSPORT_BREDR
Transport.Le -> TRANSPORT_LE
}

private val Phy.intValue: Int
@TargetApi(Build.VERSION_CODES.O)
get() = when (this) {
Phy.Le1M -> PHY_LE_1M_MASK
Phy.Le2M -> PHY_LE_2M_MASK
Phy.LeCoded -> PHY_LE_CODED_MASK
}

private val BluetoothDevice.threadName: String
get() = "Gatt@$this"
62 changes: 60 additions & 2 deletions core/src/androidMain/kotlin/Peripheral.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,67 @@ 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,
}

public actual fun CoroutineScope.peripheral(
advertisement: Advertisement,
): Peripheral = peripheral(advertisement.bluetoothDevice)

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

/**
* @param transport preferred transport for GATT connections to remote dual-mode devices.
* @param phy preferred PHY for connections to remote LE device.
*/
public fun CoroutineScope.peripheral(
bluetoothDevice: BluetoothDevice,
): Peripheral = AndroidPeripheral(coroutineContext, bluetoothDevice)
transport: Transport = Transport.Le,
phy: Phy = Phy.Le1M,
): Peripheral = AndroidPeripheral(coroutineContext, bluetoothDevice, transport, 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.",
Expand All @@ -68,11 +122,13 @@ public fun CoroutineScope.peripheral(
public fun CoroutineScope.peripheral(
bluetoothDevice: BluetoothDevice,
writeObserveDescriptor: WriteNotificationDescriptor,
): Peripheral = AndroidPeripheral(coroutineContext, bluetoothDevice)
): Peripheral = AndroidPeripheral(coroutineContext, bluetoothDevice, Transport.Le, Phy.Le1M)

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

private val job = SupervisorJob(parentCoroutineContext[Job]).apply {
Expand Down Expand Up @@ -108,6 +164,8 @@ public class AndroidPeripheral internal constructor(
private fun establishConnection(): Connection =
bluetoothDevice.connect(
applicationContext,
transport,
phy,
_state,
invokeOnClose = { connectJob.value = null }
) ?: throw ConnectionRejectedException()
Expand Down

0 comments on commit 151e54d

Please sign in to comment.