Skip to content

Commit

Permalink
Make services direct references and match properties for I/O
Browse files Browse the repository at this point in the history
Previously, all I/O operations would lazily search for the first match
when looking up characteristics or descriptors. This was problematic if
duplicate UUIDs existed. Discovered GATT profile items (services,
characteristics and descriptors) as provided via `Peripheral.services`
are now direct references to underlying platform types. This means they
no longer trigger lookups for I/O operations and specific GATT profile
items can be sought after and used.

When using `characteristicOf`, the properties of the characteristic are
now taken into account when searching for the underlying platform
object. For example, when performing a read, only a characteristic with
a matching UUID **and** `read` property will be considered a match.
  • Loading branch information
twyatt committed Jan 6, 2022
1 parent 8854ba2 commit e2f435a
Show file tree
Hide file tree
Showing 26 changed files with 624 additions and 612 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
build/
.DS_Store
local.properties
kotlin-js-store/
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,15 @@ For example, a peripheral might have the following structure:
- Service S2
- Characteristic C3

To access a characteristic or descriptor, use the [`charactisticOf`] or [`descriptorOf`] functions, respectively.
To access a characteristic or descriptor, use the [`charactisticOf`] or [`descriptorOf`] functions, respectively. These
functions lazily search for the first match (based on UUID) in the GATT profile when performing I/O.

In the above example, to access "Descriptor D3":
_When performing I/O operations on a characteristic ([`read`], [`write`], [`observe`]), the properties of the
characteristic are taken into account when finding the first match. For example, when performing a [`write`] with a
[`WriteType`] of [`WithResponse`], the first characteristic matching the expected UUID **and** having the
[`writeWithResponse`] property will be used._

In the above example, to lazily access "Descriptor D3":

```kotlin
val descriptor = descriptorOf(
Expand All @@ -311,7 +317,29 @@ val descriptor = descriptorOf(
)
```

Once connected, data can be read from, or written to, characteristics and/or descriptors via [`read`] and [`write`]
Alternatively, a characteristic or descriptor may be obtained by traversing the [`Peripheral.services`]. This is useful
when multiple characteristics or descriptors have the same UUID. Objects obtained from the [`Peripheral.services`] hold
strong references to the underlying platform types, so special care must be taken to properly remove references to
objects retrieved from [`Peripheral.services`] when no longer needed.

To access "Descriptor D3" using a discovered descriptor:

```kotlin
val services = peripheral.services ?: error("Services have not been discovered")
val descriptor = services
.first { it.serviceUuid == uuidFrom("00001815-0000-1000-8000-00805f9b34fb") }
.characteristics
.first { it.characteristicUuid == uuidFrom("00002a56-0000-1000-8000-00805f9b34fb") }
.descriptors
.first { it.descriptorUuid == uuidFrom("00002902-0000-1000-8000-00805f9b34fb") }
```

_This example uses a similar search algorithm as `descriptorOf`, but other search methods may be utilized. For example,
properties of the characteristic could be queried to find a specific characteristic that is expected to be the parent of
the sought after descriptor. When searching for a specific characteristic, descriptors can be read that may identity the
sought after characteristic._

When connected, data can be read from, or written to, characteristics and/or descriptors via [`read`] and [`write`]
functions.

_The [`read`] and [`write`] functions throw [`NotReadyException`] until a connection is established._
Expand Down Expand Up @@ -533,12 +561,16 @@ limitations under the License.
[`Options`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-options/index.html
[`Peripheral`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-peripheral/index.html
[`Peripheral.disconnect`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-peripheral/index.html#%5Bcom.juul.kable%2FPeripheral%2Fdisconnect%2F%23%2FPointingToDeclaration%2F%5D%2FFunctions%2F-328684452
[`Peripheral.services`]: https://juullabs.github.io/kable/core/com.juul.kable/-peripheral/index.html#-1607712299%2FProperties%2F-2011752812
[`read`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-peripheral/index.html#%5Bcom.juul.kable%2FPeripheral%2Fread%2F%23com.juul.kable.Characteristic%2FPointingToDeclaration%2F%2C+com.juul.kable%2FPeripheral%2Fread%2F%23com.juul.kable.Descriptor%2FPointingToDeclaration%2F%5D%2FFunctions%2F-328684452
[`requestPeripheral`]: https://juullabs.github.io/kable/core/core/com.juul.kable/request-peripheral.html
[`Scanner`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-scanner/index.html
[`state`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-peripheral/index.html#%5Bcom.juul.kable%2FPeripheral%2Fstate%2F%23%2FPointingToDeclaration%2F%5D%2FProperties%2F-328684452
[connection-state]: https://juullabs.github.io/kable/core/core/com.juul.kable/-state/index.html
[`write`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-peripheral/index.html#%5Bcom.juul.kable%2FPeripheral%2Fwrite%2F%23com.juul.kable.Descriptor%23kotlin.ByteArray%2FPointingToDeclaration%2F%2C+com.juul.kable%2FPeripheral%2Fwrite%2F%23com.juul.kable.Characteristic%23kotlin.ByteArray%23com.juul.kable.WriteType%2FPointingToDeclaration%2F%5D%2FFunctions%2F-328684452
[`WriteType`]: https://juullabs.github.io/kable/core/com.juul.kable/-write-type/index.html
[`WithResponse`]: https://juullabs.github.io/kable/core/com.juul.kable/-write-type/index.html#-1405019860%2FClasslikes%2F-2011752812
[`writeWithResponse`]: https://juullabs.github.io/kable/core/com.juul.kable/-characteristic/-properties/index.html#491699083%2FExtensions%2F-2011752812

[badge-android]: http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat
[badge-ios]: http://img.shields.io/badge/platform-ios-CDCDCD.svg?style=flat
Expand Down
57 changes: 42 additions & 15 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,17 @@ public abstract interface class com/juul/kable/Characteristic {
public abstract fun getServiceUuid ()Ljava/util/UUID;
}

public final class com/juul/kable/CharacteristicKt {
public static final fun characteristicOf (Ljava/lang/String;Ljava/lang/String;)Lcom/juul/kable/Characteristic;
public final class com/juul/kable/Characteristic$Properties {
public static final synthetic fun box-impl (I)Lcom/juul/kable/Characteristic$Properties;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (ILjava/lang/Object;)Z
public static final fun equals-impl0 (II)Z
public final fun getValue ()I
public fun hashCode ()I
public static fun hashCode-impl (I)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (I)Ljava/lang/String;
public final synthetic fun unbox-impl ()I
}

public final class com/juul/kable/ConnectionLostException : java/io/IOException {
Expand All @@ -72,31 +81,36 @@ public abstract interface class com/juul/kable/Descriptor {
public abstract fun getServiceUuid ()Ljava/util/UUID;
}

public final class com/juul/kable/DescriptorKt {
public static final fun descriptorOf (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/juul/kable/Descriptor;
}

public final class com/juul/kable/DiscoveredCharacteristic : com/juul/kable/Characteristic {
public final fun component1 ()Ljava/util/UUID;
public final fun component2 ()Ljava/util/UUID;
public final fun component3 ()Ljava/util/List;
public final fun copy (Ljava/util/UUID;Ljava/util/UUID;Ljava/util/List;)Lcom/juul/kable/DiscoveredCharacteristic;
public static synthetic fun copy$default (Lcom/juul/kable/DiscoveredCharacteristic;Ljava/util/UUID;Ljava/util/UUID;Ljava/util/List;ILjava/lang/Object;)Lcom/juul/kable/DiscoveredCharacteristic;
public final fun copy (Landroid/bluetooth/BluetoothGattCharacteristic;)Lcom/juul/kable/DiscoveredCharacteristic;
public static synthetic fun copy$default (Lcom/juul/kable/DiscoveredCharacteristic;Landroid/bluetooth/BluetoothGattCharacteristic;ILjava/lang/Object;)Lcom/juul/kable/DiscoveredCharacteristic;
public fun equals (Ljava/lang/Object;)Z
public fun getCharacteristicUuid ()Ljava/util/UUID;
public final fun getDescriptors ()Ljava/util/List;
public final fun getInstanceId ()I
public final fun getProperties-bty6q6U ()I
public fun getServiceUuid ()Ljava/util/UUID;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/juul/kable/DiscoveredDescriptor : com/juul/kable/Descriptor {
public final fun copy (Landroid/bluetooth/BluetoothGattDescriptor;)Lcom/juul/kable/DiscoveredDescriptor;
public static synthetic fun copy$default (Lcom/juul/kable/DiscoveredDescriptor;Landroid/bluetooth/BluetoothGattDescriptor;ILjava/lang/Object;)Lcom/juul/kable/DiscoveredDescriptor;
public fun equals (Ljava/lang/Object;)Z
public fun getCharacteristicUuid ()Ljava/util/UUID;
public fun getDescriptorUuid ()Ljava/util/UUID;
public fun getServiceUuid ()Ljava/util/UUID;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/juul/kable/DiscoveredService : com/juul/kable/Service {
public final fun component1 ()Ljava/util/UUID;
public final fun component2 ()Ljava/util/List;
public final fun copy (Ljava/util/UUID;Ljava/util/List;)Lcom/juul/kable/DiscoveredService;
public static synthetic fun copy$default (Lcom/juul/kable/DiscoveredService;Ljava/util/UUID;Ljava/util/List;ILjava/lang/Object;)Lcom/juul/kable/DiscoveredService;
public final fun copy (Landroid/bluetooth/BluetoothGattService;)Lcom/juul/kable/DiscoveredService;
public static synthetic fun copy$default (Lcom/juul/kable/DiscoveredService;Landroid/bluetooth/BluetoothGattService;ILjava/lang/Object;)Lcom/juul/kable/DiscoveredService;
public fun equals (Ljava/lang/Object;)Z
public final fun getCharacteristics ()Ljava/util/List;
public final fun getInstanceId ()I
public fun getServiceUuid ()Ljava/util/UUID;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
Expand Down Expand Up @@ -235,6 +249,19 @@ public final class com/juul/kable/Priority : java/lang/Enum {
public static fun values ()[Lcom/juul/kable/Priority;
}

public final class com/juul/kable/ProfileCommon {
public static final fun characteristicOf (Ljava/lang/String;Ljava/lang/String;)Lcom/juul/kable/Characteristic;
public static final fun descriptorOf (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/juul/kable/Descriptor;
public static final fun getBroadcast-G25LNqA (I)Z
public static final fun getExtendedProperties-G25LNqA (I)Z
public static final fun getIndicate-G25LNqA (I)Z
public static final fun getNotify-G25LNqA (I)Z
public static final fun getRead-G25LNqA (I)Z
public static final fun getSignedWrite-G25LNqA (I)Z
public static final fun getWrite-G25LNqA (I)Z
public static final fun getWriteWithoutResponse-G25LNqA (I)Z
}

public final class com/juul/kable/ScanFailedException : java/lang/IllegalStateException {
public final fun getErrorCode ()I
}
Expand Down
86 changes: 37 additions & 49 deletions core/src/androidMain/kotlin/Peripheral.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import android.bluetooth.BluetoothGattCharacteristic.PROPERTY_INDICATE
import android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY
import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
import android.bluetooth.BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
import android.bluetooth.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
import com.benasher44.uuid.uuidFrom
import com.juul.kable.Characteristic.Properties
import com.juul.kable.WriteType.WithResponse
import com.juul.kable.WriteType.WithoutResponse
import com.juul.kable.external.CLIENT_CHARACTERISTIC_CONFIG_UUID
Expand Down Expand Up @@ -169,12 +169,13 @@ public class AndroidPeripheral internal constructor(
private val observers = Observers(this, logging)

@Volatile
private var _platformServices: List<PlatformService>? = null
private val platformServices: List<PlatformService>
get() = checkNotNull(_platformServices) { "Services have not been discovered for $this" }
private var _discoveredServices: List<DiscoveredService>? = null
private val discoveredServices: List<DiscoveredService>
get() = _discoveredServices
?: throw IllegalStateException("Services have not been discovered for $this")

public override val services: List<DiscoveredService>?
get() = _platformServices?.map { it.toDiscoveredService() }
get() = _discoveredServices?.toList()

@Volatile
private var _connection: Connection? = null
Expand Down Expand Up @@ -264,9 +265,9 @@ public class AndroidPeripheral internal constructor(
connection.execute<OnServicesDiscovered> {
discoverServices()
}
_platformServices = withContext(connection.dispatcher) {
connection.bluetoothGatt.services
}.map { it.toPlatformService() }
_discoveredServices = withContext(connection.dispatcher) {
connection.bluetoothGatt.services.map(::DiscoveredService)
}
}

/**
Expand Down Expand Up @@ -298,11 +299,11 @@ public class AndroidPeripheral internal constructor(
detail(data)
}

val bluetoothGattCharacteristic = bluetoothGattCharacteristicFrom(characteristic)
val platformCharacteristic = discoveredServices.obtain(characteristic, writeType.properties)
connection.execute<OnCharacteristicWrite> {
bluetoothGattCharacteristic.value = data
bluetoothGattCharacteristic.writeType = writeType.intValue
writeCharacteristic(bluetoothGattCharacteristic)
platformCharacteristic.value = data
platformCharacteristic.writeType = writeType.intValue
writeCharacteristic(platformCharacteristic)
}
}

Expand All @@ -314,37 +315,32 @@ public class AndroidPeripheral internal constructor(
detail(characteristic)
}

val bluetoothGattCharacteristic = bluetoothGattCharacteristicFrom(characteristic)
val platformCharacteristic = discoveredServices.obtain(characteristic, Read)
return connection.execute<OnCharacteristicRead> {
readCharacteristic(bluetoothGattCharacteristic)
readCharacteristic(platformCharacteristic)
}.value!!
}

public override suspend fun write(
descriptor: Descriptor,
data: ByteArray,
) {
logger.debug {
message = "write"
detail(descriptor)
detail(data)
}
write(bluetoothGattDescriptorFrom(descriptor), data)
write(discoveredServices.obtain(descriptor), data)
}

private suspend fun write(
bluetoothGattDescriptor: BluetoothGattDescriptor,
platformDescriptor: PlatformDescriptor,
data: ByteArray,
) {
logger.debug {
message = "write"
detail(bluetoothGattDescriptor)
detail(platformDescriptor)
detail(data)
}

connection.execute<OnDescriptorWrite> {
bluetoothGattDescriptor.value = data
writeDescriptor(bluetoothGattDescriptor)
platformDescriptor.value = data
writeDescriptor(platformDescriptor)
}
}

Expand All @@ -355,9 +351,10 @@ public class AndroidPeripheral internal constructor(
message = "read"
detail(descriptor)
}
val bluetoothGattDescriptor = bluetoothGattDescriptorFrom(descriptor)

val platformDescriptor = discoveredServices.obtain(descriptor)
return connection.execute<OnDescriptorRead> {
readDescriptor(bluetoothGattDescriptor)
readDescriptor(platformDescriptor)
}.value!!
}

Expand All @@ -367,20 +364,21 @@ public class AndroidPeripheral internal constructor(
): Flow<ByteArray> = observers.acquire(characteristic, onSubscription)

internal suspend fun startObservation(characteristic: Characteristic) {
val platformCharacteristic = platformServices.findCharacteristic(characteristic)
logger.debug {
message = "setCharacteristicNotification"
detail(characteristic)
detail("value", "true")
}

val platformCharacteristic = discoveredServices.obtain(characteristic, Notify or Indicate)
connection
.bluetoothGatt
.setCharacteristicNotification(platformCharacteristic, true)
setConfigDescriptor(platformCharacteristic, enable = true)
}

internal suspend fun stopObservation(characteristic: Characteristic) {
val platformCharacteristic = platformServices.findCharacteristic(characteristic)
val platformCharacteristic = discoveredServices.obtain(characteristic, Notify or Indicate)

try {
setConfigDescriptor(platformCharacteristic, enable = false)
Expand All @@ -402,23 +400,21 @@ public class AndroidPeripheral internal constructor(
) {
val configDescriptor = characteristic.configDescriptor
if (configDescriptor != null) {
val bluetoothGattDescriptor = configDescriptor.bluetoothGattDescriptor

if (enable) {
when {
characteristic.supportsNotify -> {
logger.verbose {
message = "Writing ENABLE_NOTIFICATION_VALUE to CCCD"
detail(bluetoothGattDescriptor)
detail(configDescriptor)
}
write(bluetoothGattDescriptor, ENABLE_NOTIFICATION_VALUE)
write(configDescriptor, ENABLE_NOTIFICATION_VALUE)
}
characteristic.supportsIndicate -> {
logger.verbose {
message = "Writing ENABLE_INDICATION_VALUE to CCCD"
detail(bluetoothGattDescriptor)
detail(configDescriptor)
}
write(bluetoothGattDescriptor, ENABLE_INDICATION_VALUE)
write(configDescriptor, ENABLE_INDICATION_VALUE)
}
else -> logger.warn {
message = "Characteristic supports neither notification nor indication"
Expand All @@ -429,9 +425,9 @@ public class AndroidPeripheral internal constructor(
if (characteristic.supportsNotify || characteristic.supportsIndicate) {
logger.verbose {
message = "Writing DISABLE_NOTIFICATION_VALUE to CCCD"
detail(bluetoothGattDescriptor)
detail(configDescriptor)
}
write(bluetoothGattDescriptor, DISABLE_NOTIFICATION_VALUE)
write(configDescriptor, DISABLE_NOTIFICATION_VALUE)
}
}
} else {
Expand All @@ -442,14 +438,6 @@ public class AndroidPeripheral internal constructor(
}
}

private fun bluetoothGattCharacteristicFrom(
characteristic: Characteristic
) = platformServices.findCharacteristic(characteristic).bluetoothGattCharacteristic

private fun bluetoothGattDescriptorFrom(
descriptor: Descriptor
) = platformServices.findDescriptor(descriptor).bluetoothGattDescriptor

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

Expand All @@ -467,22 +455,22 @@ private val Priority.intValue: Int
}

/** @throws GattRequestRejectedException if [BluetoothGatt.setCharacteristicNotification] returns `false`. */
private fun BluetoothGatt.setCharacteristicNotification(
private fun BluetoothGatt.setCharacteristicNotificationOrThrow(
characteristic: PlatformCharacteristic,
enable: Boolean,
) {
setCharacteristicNotification(characteristic.bluetoothGattCharacteristic, enable) ||
setCharacteristicNotification(characteristic, enable) ||
throw GattRequestRejectedException()
}

private val PlatformCharacteristic.configDescriptor: PlatformDescriptor?
get() = descriptors.firstOrNull(clientCharacteristicConfigUuid)
get() = descriptors.firstOrNull { clientCharacteristicConfigUuid == it.uuid }

private val PlatformCharacteristic.supportsNotify: Boolean
get() = bluetoothGattCharacteristic.properties and PROPERTY_NOTIFY != 0
get() = properties and PROPERTY_NOTIFY != 0

private val PlatformCharacteristic.supportsIndicate: Boolean
get() = bluetoothGattCharacteristic.properties and PROPERTY_INDICATE != 0
get() = properties and PROPERTY_INDICATE != 0

/**
* Explicitly check the adapter state before connecting in order to respect system settings.
Expand Down
Loading

0 comments on commit e2f435a

Please sign in to comment.