diff --git a/.gitignore b/.gitignore index 9e83621d7..919463c1b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build/ .DS_Store local.properties +kotlin-js-store/ diff --git a/README.md b/README.md index a27a91903..277b8c073 100644 --- a/README.md +++ b/README.md @@ -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( @@ -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._ @@ -338,10 +366,14 @@ observation.collect { data -> } ``` -The [`observe`] function can be called (and its returned [`Flow`] can be collected) prior to a connection being -established. Once a connection is established then characteristic changes will stream from the [`Flow`]. If the -connection drops, the [`Flow`] will remain active, and upon reconnecting it will resume streaming characteristic -changes. +When used with [`characteristicOf`], the [`observe`] function can be called (and its returned [`Flow`] can be collected) +prior to a connection being established. Once a connection is established then characteristic changes will stream from +the [`Flow`]. If the connection drops, the [`Flow`] will remain active, and upon reconnecting it will resume streaming +characteristic changes. + +A [`Characteristic`] may also be obtained via the [`Peripheral.services`] property and used with the [`observe`] +function. As before, if the connection drops, the [`Flow`] will remain active, upon reconnecting the same underlying +platform characteristic will be used to to resume streaming characteristic changes. Failures related to notifications/indications are propagated via the [`observe`] [`Flow`], for example, if the associated characteristic is invalid or cannot be found, then a `NoSuchElementException` is propagated via the @@ -515,30 +547,35 @@ limitations under the License. [`ScanSettings`]: https://developer.android.com/reference/kotlin/android/bluetooth/le/ScanSettings [`Advertisement`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-advertisement/index.html -[`advertisements`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-scanner/index.html#%5Bcom.juul.kable%2FScanner%2Fadvertisements%2F%23%2FPointingToDeclaration%2F%5D%2FProperties%2F-328684452 -[`charactisticOf`]: https://juullabs.github.io/kable/core/core/com.juul.kable/characteristic-of.html -[`connect`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-peripheral/index.html#%5Bcom.juul.kable%2FPeripheral%2Fconnect%2F%23%2FPointingToDeclaration%2F%5D%2FFunctions%2F-328684452 +[`Characteristic`]: https://juullabs.github.io/kable/core/com.juul.kable/-characteristic/index.html [`Connected`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-state/index.html#%5Bcom.juul.kable%2FState.Connected%2F%2F%2FPointingToDeclaration%2F%5D%2FClasslikes%2F-328684452 -[`CoroutineScope`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/ [`CoroutineScope.peripheral`]: https://juullabs.github.io/kable/core/core/com.juul.kable/peripheral.html [`CoroutineScope.requestPeripheral`]: https://juullabs.github.io/kable/core/core/com.juul.kable/request-peripheral.html -[`descriptorOf`]: https://juullabs.github.io/kable/core/core/com.juul.kable/descriptor-of.html -[`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 +[`CoroutineScope`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/ [`Disconnected`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-state/index.html#%5Bcom.juul.kable%2FState.Disconnected%2F%2F%2FPointingToDeclaration%2F%5D%2FClasslikes%2F-328684452 [`Disconnecting`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-state/index.html#%5Bcom.juul.kable%2FState.Disconnecting%2F%2F%2FPointingToDeclaration%2F%5D%2FClasslikes%2F-328684452 -[`first`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/first.html [`Flow`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/ [`NotReadyException`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-not-ready-exception/index.html -[`observe`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-peripheral/index.html#%5Bcom.juul.kable%2FPeripheral%2Fobserve%2F%23com.juul.kable.Characteristic%2FPointingToDeclaration%2F%5D%2FFunctions%2F-328684452 [`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 +[`Peripheral`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-peripheral/index.html +[`Scanner`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-scanner/index.html +[`WithResponse`]: https://juullabs.github.io/kable/core/com.juul.kable/-write-type/index.html#-1405019860%2FClasslikes%2F-2011752812 +[`WriteType`]: https://juullabs.github.io/kable/core/com.juul.kable/-write-type/index.html +[`advertisements`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-scanner/index.html#%5Bcom.juul.kable%2FScanner%2Fadvertisements%2F%23%2FPointingToDeclaration%2F%5D%2FProperties%2F-328684452 +[`charactisticOf`]: https://juullabs.github.io/kable/core/core/com.juul.kable/characteristic-of.html +[`connect`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-peripheral/index.html#%5Bcom.juul.kable%2FPeripheral%2Fconnect%2F%23%2FPointingToDeclaration%2F%5D%2FFunctions%2F-328684452 +[`descriptorOf`]: https://juullabs.github.io/kable/core/core/com.juul.kable/descriptor-of.html +[`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 +[`first`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/first.html +[`observe`]: https://juullabs.github.io/kable/core/core/com.juul.kable/-peripheral/index.html#%5Bcom.juul.kable%2FPeripheral%2Fobserve%2F%23com.juul.kable.Characteristic%2FPointingToDeclaration%2F%5D%2FFunctions%2F-328684452 [`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 +[`writeWithResponse`]: https://juullabs.github.io/kable/core/com.juul.kable/-characteristic/-properties/index.html#491699083%2FExtensions%2F-2011752812 [`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 +[connection-state]: https://juullabs.github.io/kable/core/core/com.juul.kable/-state/index.html [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 diff --git a/core/api/core.api b/core/api/core.api index 871b7bcaf..2756516fe 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -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 : com/juul/kable/NotConnectedException { @@ -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; @@ -239,6 +253,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/ProfileKt { + 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 } diff --git a/core/src/androidMain/kotlin/Peripheral.kt b/core/src/androidMain/kotlin/Peripheral.kt index 73a78d5e5..4012e1fb5 100644 --- a/core/src/androidMain/kotlin/Peripheral.kt +++ b/core/src/androidMain/kotlin/Peripheral.kt @@ -11,7 +11,6 @@ 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 @@ -171,12 +170,13 @@ public class AndroidPeripheral internal constructor( private val observers = Observers(this, logging) @Volatile - private var _platformServices: List? = null - private val platformServices: List - get() = checkNotNull(_platformServices) { "Services have not been discovered for $this" } + private var _discoveredServices: List? = null + private val discoveredServices: List + get() = _discoveredServices + ?: throw IllegalStateException("Services have not been discovered for $this") public override val services: List? - get() = _platformServices?.map { it.toDiscoveredService() } + get() = _discoveredServices?.toList() @Volatile private var _connection: Connection? = null @@ -267,9 +267,9 @@ public class AndroidPeripheral internal constructor( connection.execute { discoverServices() } - _platformServices = withContext(connection.dispatcher) { - connection.bluetoothGatt.services - }.map { it.toPlatformService() } + _discoveredServices = withContext(connection.dispatcher) { + connection.bluetoothGatt.services.map(::DiscoveredService) + } } /** @@ -301,11 +301,11 @@ public class AndroidPeripheral internal constructor( detail(data) } - val bluetoothGattCharacteristic = bluetoothGattCharacteristicFrom(characteristic) + val platformCharacteristic = discoveredServices.obtain(characteristic, writeType.properties) connection.execute { - bluetoothGattCharacteristic.value = data - bluetoothGattCharacteristic.writeType = writeType.intValue - writeCharacteristic(bluetoothGattCharacteristic) + platformCharacteristic.value = data + platformCharacteristic.writeType = writeType.intValue + writeCharacteristic(platformCharacteristic) } } @@ -317,9 +317,9 @@ public class AndroidPeripheral internal constructor( detail(characteristic) } - val bluetoothGattCharacteristic = bluetoothGattCharacteristicFrom(characteristic) + val platformCharacteristic = discoveredServices.obtain(characteristic, Read) return connection.execute { - readCharacteristic(bluetoothGattCharacteristic) + readCharacteristic(platformCharacteristic) }.value!! } @@ -327,27 +327,22 @@ public class AndroidPeripheral internal constructor( 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 { - bluetoothGattDescriptor.value = data - writeDescriptor(bluetoothGattDescriptor) + platformDescriptor.value = data + writeDescriptor(platformDescriptor) } } @@ -358,9 +353,10 @@ public class AndroidPeripheral internal constructor( message = "read" detail(descriptor) } - val bluetoothGattDescriptor = bluetoothGattDescriptorFrom(descriptor) + + val platformDescriptor = discoveredServices.obtain(descriptor) return connection.execute { - readDescriptor(bluetoothGattDescriptor) + readDescriptor(platformDescriptor) }.value!! } @@ -370,12 +366,13 @@ public class AndroidPeripheral internal constructor( ): Flow = 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) @@ -383,7 +380,7 @@ public class AndroidPeripheral internal constructor( } internal suspend fun stopObservation(characteristic: Characteristic) { - val platformCharacteristic = platformServices.findCharacteristic(characteristic) + val platformCharacteristic = discoveredServices.obtain(characteristic, Notify or Indicate) setConfigDescriptor(platformCharacteristic, enable = false) logger.debug { @@ -402,23 +399,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" @@ -429,9 +424,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 { @@ -442,14 +437,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)" } @@ -467,22 +454,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. diff --git a/core/src/androidMain/kotlin/PlatformCharacteristic.kt b/core/src/androidMain/kotlin/PlatformCharacteristic.kt deleted file mode 100644 index 192d75f8d..000000000 --- a/core/src/androidMain/kotlin/PlatformCharacteristic.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.juul.kable - -import android.bluetooth.BluetoothGattCharacteristic -import com.benasher44.uuid.Uuid - -@Suppress("PROPERTY_TYPE_MISMATCH_ON_OVERRIDE") // https://youtrack.jetbrains.com/issue/KTIJ-405 -internal data class PlatformCharacteristic( - override val serviceUuid: Uuid, - override val characteristicUuid: Uuid, - val bluetoothGattCharacteristic: BluetoothGattCharacteristic, - val descriptors: List, -) : Characteristic - -internal fun PlatformCharacteristic.toDiscoveredCharacteristic() = DiscoveredCharacteristic( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid, - descriptors = descriptors.map { it.toLazyDescriptor() }, -) - -internal fun BluetoothGattCharacteristic.toPlatformCharacteristic(): PlatformCharacteristic { - val platformDescriptors = descriptors.map { descriptor -> - descriptor.toPlatformDescriptor(service.uuid, uuid) - } - - return PlatformCharacteristic( - serviceUuid = service.uuid, - characteristicUuid = uuid, - descriptors = platformDescriptors, - bluetoothGattCharacteristic = this, - ) -} - -internal fun BluetoothGattCharacteristic.toLazyCharacteristic() = LazyCharacteristic( - serviceUuid = service.uuid, - characteristicUuid = uuid, -) diff --git a/core/src/androidMain/kotlin/PlatformDescriptor.kt b/core/src/androidMain/kotlin/PlatformDescriptor.kt deleted file mode 100644 index c54c3963d..000000000 --- a/core/src/androidMain/kotlin/PlatformDescriptor.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.juul.kable - -import android.bluetooth.BluetoothGattDescriptor -import com.benasher44.uuid.Uuid - -@Suppress("PROPERTY_TYPE_MISMATCH_ON_OVERRIDE") // https://youtrack.jetbrains.com/issue/KTIJ-405 -internal data class PlatformDescriptor( - override val serviceUuid: Uuid, - override val characteristicUuid: Uuid, - override val descriptorUuid: Uuid, - val bluetoothGattDescriptor: BluetoothGattDescriptor, -) : Descriptor - -internal fun PlatformDescriptor.toLazyDescriptor() = LazyDescriptor( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid, - descriptorUuid = descriptorUuid, -) - -internal fun BluetoothGattDescriptor.toPlatformDescriptor( - serviceUuid: Uuid, - characteristicUuid: Uuid, -) = PlatformDescriptor( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid, - descriptorUuid = uuid, - bluetoothGattDescriptor = this, -) diff --git a/core/src/androidMain/kotlin/PlatformService.kt b/core/src/androidMain/kotlin/PlatformService.kt deleted file mode 100644 index 0c8c23f9f..000000000 --- a/core/src/androidMain/kotlin/PlatformService.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.juul.kable - -import android.bluetooth.BluetoothGattService -import com.benasher44.uuid.Uuid - -@Suppress("PROPERTY_TYPE_MISMATCH_ON_OVERRIDE") // https://youtrack.jetbrains.com/issue/KTIJ-405 -internal data class PlatformService( - override val serviceUuid: Uuid, - val bluetoothGattService: BluetoothGattService, - val characteristics: List, -) : Service - -internal fun PlatformService.toDiscoveredService() = DiscoveredService( - serviceUuid = serviceUuid, - characteristics = characteristics.map { it.toDiscoveredCharacteristic() }, -) - -internal fun BluetoothGattService.toPlatformService(): PlatformService { - val serviceUuid = uuid - val characteristics = characteristics - .map { characteristic -> characteristic.toPlatformCharacteristic() } - - return PlatformService( - serviceUuid = serviceUuid, - characteristics = characteristics, - bluetoothGattService = this, - ) -} - -/** @throws NoSuchElementException if service or characteristic is not found. */ -internal fun List.findCharacteristic( - characteristic: Characteristic -): PlatformCharacteristic = - findCharacteristic( - serviceUuid = characteristic.serviceUuid, - characteristicUuid = characteristic.characteristicUuid - ) - -/** @throws NoSuchElementException if service or characteristic is not found. */ -private fun List.findCharacteristic( - serviceUuid: Uuid, - characteristicUuid: Uuid -): PlatformCharacteristic = - first(serviceUuid) - .characteristics - .first(characteristicUuid) - -/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */ -internal fun List.findDescriptor( - descriptor: Descriptor -): PlatformDescriptor = - findDescriptor( - serviceUuid = descriptor.serviceUuid, - characteristicUuid = descriptor.characteristicUuid, - descriptorUuid = descriptor.descriptorUuid - ) - -/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */ -private fun List.findDescriptor( - serviceUuid: Uuid, - characteristicUuid: Uuid, - descriptorUuid: Uuid -): PlatformDescriptor = - findCharacteristic( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid - ).descriptors.first(descriptorUuid) diff --git a/core/src/androidMain/kotlin/Profile.kt b/core/src/androidMain/kotlin/Profile.kt new file mode 100644 index 000000000..44adcbb18 --- /dev/null +++ b/core/src/androidMain/kotlin/Profile.kt @@ -0,0 +1,54 @@ +@file:JvmName("ProfileAndroid") + +package com.juul.kable + +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothGattService +import com.benasher44.uuid.Uuid +import com.juul.kable.Characteristic.Properties + +@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316 +internal actual typealias PlatformService = BluetoothGattService +@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316 +internal actual typealias PlatformCharacteristic = BluetoothGattCharacteristic +@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316 +internal actual typealias PlatformDescriptor = BluetoothGattDescriptor + +public actual data class DiscoveredService internal constructor( + internal actual val service: PlatformService, +) : Service { + + public actual val characteristics: List = + service.characteristics.map(::DiscoveredCharacteristic) + + override val serviceUuid: Uuid get() = service.uuid + val instanceId: Int get() = service.instanceId +} + +public actual data class DiscoveredCharacteristic internal constructor( + internal actual val characteristic: PlatformCharacteristic, +) : Characteristic { + + public actual val descriptors: List = + characteristic.descriptors.map(::DiscoveredDescriptor) + + override val serviceUuid: Uuid get() = characteristic.service.uuid + override val characteristicUuid: Uuid get() = characteristic.uuid + val instanceId: Int get() = characteristic.instanceId + actual val properties: Properties get() = Properties(characteristic.properties) +} + +public actual data class DiscoveredDescriptor internal constructor( + internal actual val descriptor: PlatformDescriptor, +) : Descriptor { + + override val serviceUuid: Uuid get() = descriptor.characteristic.service.uuid + override val characteristicUuid: Uuid get() = descriptor.characteristic.uuid + override val descriptorUuid: Uuid get() = descriptor.uuid +} + +internal fun PlatformCharacteristic.toLazyCharacteristic() = LazyCharacteristic( + serviceUuid = service.uuid, + characteristicUuid = uuid, +) diff --git a/core/src/appleMain/kotlin/Peripheral.kt b/core/src/appleMain/kotlin/Peripheral.kt index d650f72b1..afeed07aa 100644 --- a/core/src/appleMain/kotlin/Peripheral.kt +++ b/core/src/appleMain/kotlin/Peripheral.kt @@ -124,14 +124,13 @@ public class ApplePeripheral internal constructor( .launchIn(scope) } - private val _platformServices = atomic?>(null) - private val platformServices: List - get() = checkNotNull(_platformServices.value) { - "Services have not been discovered for $this" - } + private val _discoveredServices = atomic?>(null) + private val discoveredServices: List + get() = _discoveredServices.value + ?: throw IllegalStateException("Services have not been discovered for $this") public override val services: List? - get() = _platformServices.value?.map { it.toDiscoveredService() } + get() = _discoveredServices.value?.toList() private val _connection = atomic(null) private val connection: Connection @@ -238,9 +237,10 @@ public class ApplePeripheral internal constructor( } } - _platformServices.value = cbPeripheral.services?.map { service -> - (service as CBService).toPlatformService() - } + _discoveredServices.value = cbPeripheral.services + .orEmpty() + .map { it as PlatformService } + .map(::DiscoveredService) } @Throws(CancellationException::class, IOException::class, NotReadyException::class) @@ -263,9 +263,9 @@ public class ApplePeripheral internal constructor( detail(data) } - val cbCharacteristic = cbCharacteristicFrom(characteristic) + val platformCharacteristic = discoveredServices.obtain(characteristic, writeType.properties) connection.execute { - centralManager.write(cbPeripheral, data, cbCharacteristic, writeType.cbWriteType) + centralManager.write(cbPeripheral, data, platformCharacteristic, writeType.cbWriteType) } } @@ -284,14 +284,14 @@ public class ApplePeripheral internal constructor( } val connection = this.connection - val cbCharacteristic = cbCharacteristicFrom(characteristic) + val platformCharacteristic = discoveredServices.obtain(characteristic, Read) return connection.semaphore.withPermit { connection .delegate .characteristicChanges - .onSubscription { centralManager.read(cbPeripheral, cbCharacteristic) } - .firstOrThrow { it.cbCharacteristic.UUID == cbCharacteristic.UUID } + .onSubscription { centralManager.read(cbPeripheral, platformCharacteristic) } + .firstOrThrow { it.cbCharacteristic.UUID == platformCharacteristic.UUID } } } @@ -312,9 +312,9 @@ public class ApplePeripheral internal constructor( detail(data) } - val cbDescriptor = cbDescriptorFrom(descriptor) + val platformDescriptor = discoveredServices.obtain(descriptor) connection.execute { - centralManager.write(cbPeripheral, data, cbDescriptor) + centralManager.write(cbPeripheral, data, platformDescriptor) } } @@ -332,16 +332,16 @@ public class ApplePeripheral internal constructor( detail(descriptor) } - val cbDescriptor = cbDescriptorFrom(descriptor) + val platformDescriptor = discoveredServices.obtain(descriptor) return connection.execute { - centralManager.read(cbPeripheral, cbDescriptor) + centralManager.read(cbPeripheral, platformDescriptor) }.descriptor.value as NSData } public override fun observe( characteristic: Characteristic, onSubscription: OnSubscriptionAction, - ): Flow = observeAsNSData(characteristic, onSubscription).map { it.toByteArray() } + ): Flow = observeAsNSData(characteristic, onSubscription).map(NSData::toByteArray) public fun observeAsNSData( characteristic: Characteristic, @@ -354,9 +354,9 @@ public class ApplePeripheral internal constructor( detail(characteristic) } - val cbCharacteristic = cbCharacteristicFrom(characteristic) + val platformCharacteristic = discoveredServices.obtain(characteristic, Notify or Indicate) connection.execute { - centralManager.notify(cbPeripheral, cbCharacteristic) + centralManager.notify(cbPeripheral, platformCharacteristic) } } @@ -366,20 +366,12 @@ public class ApplePeripheral internal constructor( detail(characteristic) } - val cbCharacteristic = cbCharacteristicFrom(characteristic) + val platformCharacteristic = discoveredServices.obtain(characteristic, Notify or Indicate) connection.execute { - centralManager.cancelNotify(cbPeripheral, cbCharacteristic) + centralManager.cancelNotify(cbPeripheral, platformCharacteristic) } } - private fun cbCharacteristicFrom( - characteristic: Characteristic, - ) = platformServices.findCharacteristic(characteristic).cbCharacteristic - - private fun cbDescriptorFrom( - descriptor: Descriptor, - ) = platformServices.findDescriptor(descriptor).cbDescriptor - override fun toString(): String = "Peripheral(cbPeripheral=$cbPeripheral)" } diff --git a/core/src/appleMain/kotlin/PlatformCharacteristic.kt b/core/src/appleMain/kotlin/PlatformCharacteristic.kt deleted file mode 100644 index 8a14923b6..000000000 --- a/core/src/appleMain/kotlin/PlatformCharacteristic.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.juul.kable - -import com.benasher44.uuid.Uuid -import platform.CoreBluetooth.CBCharacteristic -import platform.CoreBluetooth.CBDescriptor - -internal data class PlatformCharacteristic( - override val serviceUuid: Uuid, - override val characteristicUuid: Uuid, - val cbCharacteristic: CBCharacteristic, - val descriptors: List, -) : Characteristic - -internal fun PlatformCharacteristic.toDiscoveredCharacteristic() = DiscoveredCharacteristic( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid, - descriptors = descriptors.map { it.toLazyDescriptor() }, -) - -internal fun CBCharacteristic.toPlatformCharacteristic( - serviceUuid: Uuid, -): PlatformCharacteristic { - val characteristicUuid = UUID.toUuid() - val platformDescriptors = descriptors?.map { descriptor -> - descriptor as CBDescriptor - descriptor.toPlatformDescriptor(serviceUuid, characteristicUuid) - } ?: emptyList() - - return PlatformCharacteristic( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid, - descriptors = platformDescriptors, - cbCharacteristic = this, - ) -} - -internal fun CBCharacteristic.toLazyCharacteristic() = LazyCharacteristic( - serviceUuid = service.UUID.toUuid(), - characteristicUuid = UUID.toUuid(), -) diff --git a/core/src/appleMain/kotlin/PlatformDescriptor.kt b/core/src/appleMain/kotlin/PlatformDescriptor.kt deleted file mode 100644 index 6974964c8..000000000 --- a/core/src/appleMain/kotlin/PlatformDescriptor.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.juul.kable - -import com.benasher44.uuid.Uuid -import platform.CoreBluetooth.CBDescriptor - -internal data class PlatformDescriptor( - override val serviceUuid: Uuid, - override val characteristicUuid: Uuid, - override val descriptorUuid: Uuid, - val cbDescriptor: CBDescriptor, -) : Descriptor - -internal fun PlatformDescriptor.toLazyDescriptor() = LazyDescriptor( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid, - descriptorUuid = descriptorUuid, -) - -internal fun CBDescriptor.toPlatformDescriptor( - serviceUuid: Uuid, - characteristicUuid: Uuid, -) = PlatformDescriptor( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid, - descriptorUuid = UUID.toUuid(), - cbDescriptor = this, -) diff --git a/core/src/appleMain/kotlin/PlatformService.kt b/core/src/appleMain/kotlin/PlatformService.kt deleted file mode 100644 index 3c4a1ce6b..000000000 --- a/core/src/appleMain/kotlin/PlatformService.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.juul.kable - -import com.benasher44.uuid.Uuid -import platform.CoreBluetooth.CBCharacteristic -import platform.CoreBluetooth.CBService - -internal data class PlatformService( - override val serviceUuid: Uuid, - val cbService: CBService, - val characteristics: List, -) : Service - -internal fun PlatformService.toDiscoveredService() = DiscoveredService( - serviceUuid = serviceUuid, - characteristics = characteristics.map { it.toDiscoveredCharacteristic() }, -) - -internal fun CBService.toPlatformService(): PlatformService { - val serviceUuid = UUID.toUuid() - val platformCharacteristics = characteristics?.map { characteristic -> - characteristic as CBCharacteristic - characteristic.toPlatformCharacteristic(serviceUuid) - } ?: emptyList() - - return PlatformService( - serviceUuid = serviceUuid, - characteristics = platformCharacteristics, - cbService = this, - ) -} - -/** @throws NoSuchElementException if service or characteristic is not found. */ -internal fun List.findCharacteristic( - characteristic: Characteristic -): PlatformCharacteristic = - findCharacteristic( - serviceUuid = characteristic.serviceUuid, - characteristicUuid = characteristic.characteristicUuid - ) - -/** @throws NoSuchElementException if service or characteristic is not found. */ -private fun List.findCharacteristic( - serviceUuid: Uuid, - characteristicUuid: Uuid -): PlatformCharacteristic = - first(serviceUuid) - .characteristics - .first(characteristicUuid) - -/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */ -internal fun List.findDescriptor( - descriptor: Descriptor -): PlatformDescriptor = - findDescriptor( - serviceUuid = descriptor.serviceUuid, - characteristicUuid = descriptor.characteristicUuid, - descriptorUuid = descriptor.descriptorUuid - ) - -/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */ -private fun List.findDescriptor( - serviceUuid: Uuid, - characteristicUuid: Uuid, - descriptorUuid: Uuid -): PlatformDescriptor = - findCharacteristic( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid - ).descriptors.first(descriptorUuid) diff --git a/core/src/appleMain/kotlin/Profile.kt b/core/src/appleMain/kotlin/Profile.kt new file mode 100644 index 000000000..265e6179b --- /dev/null +++ b/core/src/appleMain/kotlin/Profile.kt @@ -0,0 +1,57 @@ +package com.juul.kable + +import com.benasher44.uuid.Uuid +import com.juul.kable.Characteristic.Properties +import platform.CoreBluetooth.CBCharacteristic +import platform.CoreBluetooth.CBDescriptor +import platform.CoreBluetooth.CBService + +@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316 +internal actual typealias PlatformService = CBService +@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316 +internal actual typealias PlatformCharacteristic = CBCharacteristic +@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316 +internal actual typealias PlatformDescriptor = CBDescriptor + +public actual data class DiscoveredService internal constructor( + internal actual val service: PlatformService, +) : Service { + + public actual val characteristics: List = + service.characteristics + .orEmpty() + .map { it as PlatformCharacteristic } + .map(::DiscoveredCharacteristic) + + override val serviceUuid: Uuid = service.UUID.toUuid() +} + +public actual data class DiscoveredCharacteristic internal constructor( + internal actual val characteristic: PlatformCharacteristic, +) : Characteristic { + + public actual val descriptors: List = + characteristic.descriptors + .orEmpty() + .map { it as PlatformDescriptor } + .map(::DiscoveredDescriptor) + + override val serviceUuid: Uuid = characteristic.service.UUID.toUuid() + override val characteristicUuid: Uuid = characteristic.UUID.toUuid() + + public actual val properties: Properties = Properties(characteristic.properties.toInt()) +} + +public actual data class DiscoveredDescriptor internal constructor( + internal actual val descriptor: PlatformDescriptor, +) : Descriptor { + + override val serviceUuid: Uuid = descriptor.characteristic.service.UUID.toUuid() + override val characteristicUuid: Uuid = descriptor.characteristic.UUID.toUuid() + override val descriptorUuid: Uuid = descriptor.UUID.toUuid() +} + +internal fun PlatformCharacteristic.toLazyCharacteristic() = LazyCharacteristic( + serviceUuid = service.UUID.toUuid(), + characteristicUuid = UUID.toUuid(), +) diff --git a/core/src/commonMain/kotlin/Characteristic.kt b/core/src/commonMain/kotlin/Characteristic.kt deleted file mode 100644 index 2379289ce..000000000 --- a/core/src/commonMain/kotlin/Characteristic.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.juul.kable - -import com.benasher44.uuid.Uuid -import com.benasher44.uuid.uuidFrom - -public fun characteristicOf( - service: String, - characteristic: String, -): Characteristic = LazyCharacteristic( - serviceUuid = uuidFrom(service), - characteristicUuid = uuidFrom(characteristic) -) - -public interface Characteristic { - public val serviceUuid: Uuid - public val characteristicUuid: Uuid -} - -public data class LazyCharacteristic internal constructor( - override val serviceUuid: Uuid, - override val characteristicUuid: Uuid, -) : Characteristic - -public data class DiscoveredCharacteristic internal constructor( - override val serviceUuid: Uuid, - override val characteristicUuid: Uuid, - public val descriptors: List, -) : Characteristic - -internal fun List.first( - characteristicUuid: Uuid -): T = firstOrNull { it.characteristicUuid == characteristicUuid } - ?: throw NoSuchElementException("Characteristic $characteristicUuid not found") diff --git a/core/src/commonMain/kotlin/Descriptor.kt b/core/src/commonMain/kotlin/Descriptor.kt index 5a3fa79de..478a56ba3 100644 --- a/core/src/commonMain/kotlin/Descriptor.kt +++ b/core/src/commonMain/kotlin/Descriptor.kt @@ -1,29 +1,6 @@ package com.juul.kable import com.benasher44.uuid.Uuid -import com.benasher44.uuid.uuidFrom - -public fun descriptorOf( - service: String, - characteristic: String, - descriptor: String, -): Descriptor = LazyDescriptor( - serviceUuid = uuidFrom(service), - characteristicUuid = uuidFrom(characteristic), - descriptorUuid = uuidFrom(descriptor) -) - -public interface Descriptor { - public val serviceUuid: Uuid - public val characteristicUuid: Uuid - public val descriptorUuid: Uuid -} - -public data class LazyDescriptor( - public override val serviceUuid: Uuid, - public override val characteristicUuid: Uuid, - public override val descriptorUuid: Uuid, -) : Descriptor internal fun List.first( descriptorUuid: Uuid diff --git a/core/src/commonMain/kotlin/Peripheral.kt b/core/src/commonMain/kotlin/Peripheral.kt index 19048a8c8..9e87488e9 100644 --- a/core/src/commonMain/kotlin/Peripheral.kt +++ b/core/src/commonMain/kotlin/Peripheral.kt @@ -78,20 +78,48 @@ public interface Peripheral { */ public suspend fun disconnect(): Unit - /** @return discovered [services][Service], or `null` until a [connection][connect] has been established. */ + /** + * The list of services (GATT profile) which have been discovered on the remote peripheral. + * + * The list contains a tree of [DiscoveredService]s, [DiscoveredCharacteristic]s and [DiscoveredDescriptor]s. These + * types all hold strong references to the underlying platform type, so no guarantees are provided on the validity + * of the objects beyond a connection. If a reconnect occurs, it is recommended to retrieve the desired object from + * [services] again. Any references to objects obtained from this tree should be cleared upon disconnect or disposal + * (when parent [CoroutineScope] is cancelled) of this [Peripheral]. + * + * @return [discovered services][DiscoveredService], or `null` until a [connection][connect] has been established. + */ public val services: List? /** @throws NotReadyException if invoked without an established [connection][connect]. */ @Throws(CancellationException::class, IOException::class, NotReadyException::class) public suspend fun rssi(): Int - /** @throws NotReadyException if invoked without an established [connection][connect]. */ + /** + * Reads data from [characteristic]. + * + * If [characteristic] was created via [characteristicOf] then the first found characteristic with [Read] property + * matching the service UUID and characteristic UUID in the GATT profile will be used. If multiple characteristics + * with the same UUID and [Read] characteristic property exist in the GATT profile, then a + * [discovered characteristic][DiscoveredCharacteristic] from [services] should be used instead. + * + * @throws NotReadyException if invoked without an established [connection][connect]. + */ @Throws(CancellationException::class, IOException::class, NotReadyException::class) public suspend fun read( characteristic: Characteristic, ): ByteArray - /** @throws NotReadyException if invoked without an established [connection][connect]. */ + /** + * Writes [data] to [characteristic]. + * + * If [characteristic] was created via [characteristicOf] then the first found characteristic with a property + * matching the specified [writeType] and matching the service UUID and characteristic UUID in the GATT profile will + * be used. If multiple characteristics with the same UUID and property exist in the GATT profile, then a + * [discovered characteristic][DiscoveredCharacteristic] from [services] should be used instead. + * + * @throws NotReadyException if invoked without an established [connection][connect]. + */ @Throws(CancellationException::class, IOException::class, NotReadyException::class) public suspend fun write( characteristic: Characteristic, @@ -99,13 +127,31 @@ public interface Peripheral { writeType: WriteType = WithoutResponse, ): Unit - /** @throws NotReadyException if invoked without an established [connection][connect]. */ + /** + * Reads data from [descriptor]. + * + * If [descriptor] was created via [descriptorOf] then the first found descriptor (matching the service UUID, + * characteristic UUID and descriptor UUID) in the GATT profile will be used. If multiple descriptors with the same + * UUID exist in the GATT profile, then a [discovered descriptor][DiscoveredDescriptor] from [services] should be + * used instead. + * + * @throws NotReadyException if invoked without an established [connection][connect]. + */ @Throws(CancellationException::class, IOException::class, NotReadyException::class) public suspend fun read( descriptor: Descriptor, ): ByteArray - /** @throws NotReadyException if invoked without an established [connection][connect]. */ + /** + * Writes [data] to [descriptor]. + * + * If [descriptor] was created via [descriptorOf] then the first found descriptor (matching the service UUID, + * characteristic UUID and descriptor UUID) in the GATT profile will be used. If multiple descriptors with the same + * UUID exist in the GATT profile, then a [discovered descriptor][DiscoveredDescriptor] from [services] should be + * used instead. + * + * @throws NotReadyException if invoked without an established [connection][connect]. + */ @Throws(CancellationException::class, IOException::class, NotReadyException::class) public suspend fun write( descriptor: Descriptor, @@ -115,9 +161,15 @@ public interface Peripheral { /** * Observes changes to the specified [Characteristic]. * - * Observations can be setup ([observe] can be called) prior to a [connection][connect] being established. Once - * connected, the observation will automatically start emitting changes. If connection is lost, [Flow] will remain - * active, once reconnected characteristic changes will begin emitting again. + * If [characteristic] was created via [characteristicOf] then the first found characteristic with a property of + * **notify** or **indicate** and matching service UUID and characteristic UUID will be used. If multiple + * characteristics with the same UUID and either **notify** or **indicate** property exist in the GATT profile, then + * a [discovered characteristic][DiscoveredCharacteristic] from [services] should be used instead. + * + * When using [characteristicOf], observations can be setup ([observe] can be called) prior to a + * [connection][connect] being established. Once connected, the observation will automatically start emitting + * changes. If connection is lost, [Flow] will remain active, once reconnected characteristic changes will begin + * emitting again. * * If characteristic has a Client Characteristic Configuration descriptor (CCCD), then based on bits in the * [characteristic] properties, observe will be configured (CCCD will be written to) as **notification** or @@ -125,8 +177,7 @@ public interface Peripheral { * used). * * Failures related to notifications are propagated via the returned [observe] [Flow], for example, if the specified - * [characteristic] is invalid or cannot be found then a [NoSuchElementException] is propagated via the returned - * [Flow]. + * [characteristic] is invalid or cannot be found then returned [Flow] terminates with a [NoSuchElementException]. * * The optional [onSubscription] parameter is functionally identical to using the * [onSubscription][kotlinx.coroutines.flow.onSubscription] operator on the returned [Flow] except it has the diff --git a/core/src/commonMain/kotlin/Profile.kt b/core/src/commonMain/kotlin/Profile.kt new file mode 100644 index 000000000..45b500d5b --- /dev/null +++ b/core/src/commonMain/kotlin/Profile.kt @@ -0,0 +1,176 @@ +package com.juul.kable + +import com.benasher44.uuid.Uuid +import com.benasher44.uuid.uuidFrom +import com.juul.kable.Characteristic.Properties +import com.juul.kable.WriteType.WithResponse +import com.juul.kable.WriteType.WithoutResponse +import kotlin.jvm.JvmInline + +public interface Service { + public val serviceUuid: Uuid +} + +/* ktlint-disable no-multi-spaces */ +internal val Broadcast = Properties(1 shl 0) // 0x01 +internal val Read = Properties(1 shl 1) // 0x02 +internal val WriteWithoutResponse = Properties(1 shl 2) // 0x04 +internal val Write = Properties(1 shl 3) // 0x08 +internal val Notify = Properties(1 shl 4) // 0x10 +internal val Indicate = Properties(1 shl 5) // 0x20 +internal val SignedWrite = Properties(1 shl 6) // 0x40 +internal val ExtendedProperties = Properties(1 shl 7) // 0x80 +/* ktlint-enable no-multi-spaces */ + +public val Properties.broadcast: Boolean + get() = value and Broadcast.value != 0 + +public val Properties.read: Boolean + get() = value and Read.value != 0 + +public val Properties.writeWithoutResponse: Boolean + get() = value and WriteWithoutResponse.value != 0 + +public val Properties.write: Boolean + get() = value and Write.value != 0 + +public val Properties.notify: Boolean + get() = value and Notify.value != 0 + +public val Properties.indicate: Boolean + get() = value and Indicate.value != 0 + +public val Properties.signedWrite: Boolean + get() = value and SignedWrite.value != 0 + +public val Properties.extendedProperties: Boolean + get() = value and ExtendedProperties.value != 0 + +internal val WriteType.properties: Properties + get() = when (this) { + WithResponse -> Write + WithoutResponse -> WriteWithoutResponse + } + +public interface Characteristic { + public val serviceUuid: Uuid + public val characteristicUuid: Uuid + + @JvmInline + public value class Properties internal constructor(public val value: Int) { + internal infix fun or(other: Properties): Properties = Properties(value or other.value) + internal infix fun and(other: Properties): Properties = Properties(value and other.value) + override fun toString(): String = + mutableListOf().apply { + if (broadcast) add("broadcast") + if (read) add("read") + if (writeWithoutResponse) add("writeWithoutResponse") + if (write) add("write") + if (notify) add("notify") + if (indicate) add("indicate") + if (signedWrite) add("signedWrite") + }.joinToString() + } +} + +public interface Descriptor { + public val serviceUuid: Uuid + public val characteristicUuid: Uuid + public val descriptorUuid: Uuid +} + +internal expect class PlatformService +internal expect class PlatformCharacteristic +internal expect class PlatformDescriptor + +/** Wrapper around platform specific Bluetooth LE service. Holds a strong reference to underlying service. */ +public expect class DiscoveredService : Service { + internal val service: PlatformService + public val characteristics: List +} + +/** Wrapper around platform specific Bluetooth LE characteristic. Holds a strong reference to underlying characteristic. */ +public expect class DiscoveredCharacteristic : Characteristic { + internal val characteristic: PlatformCharacteristic + public val descriptors: List + public val properties: Properties +} + +/** Wrapper around platform specific Bluetooth LE descriptor. Holds a strong reference to underlying descriptor. */ +public expect class DiscoveredDescriptor : Descriptor { + internal val descriptor: PlatformDescriptor +} + +public data class LazyCharacteristic internal constructor( + override val serviceUuid: Uuid, + override val characteristicUuid: Uuid, +) : Characteristic + +public data class LazyDescriptor( + public override val serviceUuid: Uuid, + public override val characteristicUuid: Uuid, + public override val descriptorUuid: Uuid, +) : Descriptor + +public fun characteristicOf( + service: String, + characteristic: String, +): Characteristic = LazyCharacteristic( + serviceUuid = uuidFrom(service), + characteristicUuid = uuidFrom(characteristic), +) + +public fun descriptorOf( + service: String, + characteristic: String, + descriptor: String, +): Descriptor = LazyDescriptor( + serviceUuid = uuidFrom(service), + characteristicUuid = uuidFrom(characteristic), + descriptorUuid = uuidFrom(descriptor), +) + +internal fun List.obtain( + characteristic: Characteristic, + properties: Properties?, +): PlatformCharacteristic { + if (characteristic is DiscoveredCharacteristic) return characteristic.characteristic + + val discoveredService = firstOrNull { + it.serviceUuid == characteristic.serviceUuid + } ?: throw NoSuchElementException("Service ${characteristic.serviceUuid} not found") + + val discoveredCharacteristic = discoveredService.characteristics.firstOrNull { + it.characteristicUuid == characteristic.characteristicUuid && + (properties == null || (it.properties and properties).value != 0) + } ?: throw NoSuchElementException("Characteristic ${characteristic.characteristicUuid}${properties.text}not found") + + return discoveredCharacteristic.characteristic +} + +internal fun List.obtain( + descriptor: Descriptor, +): PlatformDescriptor { + if (descriptor is DiscoveredDescriptor) return descriptor.descriptor + + val discoveredService = firstOrNull { + it.serviceUuid == descriptor.serviceUuid + } ?: throw NoSuchElementException("Service ${descriptor.serviceUuid} not found") + + val discoveredCharacteristic = discoveredService.characteristics.firstOrNull { + it.characteristicUuid == descriptor.characteristicUuid + } ?: throw NoSuchElementException("Characteristic ${descriptor.characteristicUuid} not found") + + val discoveredDescriptor = discoveredCharacteristic.descriptors.firstOrNull { + it.descriptorUuid == descriptor.descriptorUuid + } ?: throw NoSuchElementException("Descriptor ${descriptor.descriptorUuid} not found") + + return discoveredDescriptor.descriptor +} + +private val Properties?.text: String + get() = when (this?.value?.countOneBits()) { + null, 0 -> " " + 1 -> " with $this property " + else -> " with $this properties " + } diff --git a/core/src/commonMain/kotlin/Service.kt b/core/src/commonMain/kotlin/Service.kt deleted file mode 100644 index 5eb8c6288..000000000 --- a/core/src/commonMain/kotlin/Service.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.juul.kable - -import com.benasher44.uuid.Uuid - -public interface Service { - public val serviceUuid: Uuid -} - -public data class DiscoveredService internal constructor( - override val serviceUuid: Uuid, - public val characteristics: List, -) : Service - -/** @throws NoSuchElementException if service is not found. */ -internal fun List.first( - serviceUuid: Uuid -): T = firstOrNull { it.serviceUuid == serviceUuid } - ?: throw NoSuchElementException("Service $serviceUuid not found") diff --git a/core/src/jsMain/kotlin/Peripheral.kt b/core/src/jsMain/kotlin/Peripheral.kt index 12a1280bd..0eb555c9d 100644 --- a/core/src/jsMain/kotlin/Peripheral.kt +++ b/core/src/jsMain/kotlin/Peripheral.kt @@ -81,12 +81,13 @@ public class JsPeripheral internal constructor( private val _state = MutableStateFlow(State.Disconnected()) public override val state: StateFlow = _state.asStateFlow() - private var _platformServices: List? = null - private val platformServices: List - get() = checkNotNull(_platformServices) { "Services have not been discovered for $this" } + private var _discoveredServices: List? = null + private val discoveredServices: List + get() = _discoveredServices + ?: throw IllegalStateException("Services have not been discovered for $this") public override val services: List? - get() = _platformServices?.map { it.toDiscoveredService() } + get() = _discoveredServices?.toList() private val observationListeners = mutableMapOf() @@ -181,9 +182,9 @@ public class JsPeripheral internal constructor( logger.verbose { message = "discover services" } val services = ioLock.withLock { gatt.getPrimaryServices().await() - .map { it.toPlatformService(logger) } + .map { it.toDiscoveredService(logger) } } - _platformServices = services + _discoveredServices = services } public override suspend fun write( @@ -197,11 +198,11 @@ public class JsPeripheral internal constructor( detail(data) } - val jsCharacteristic = bluetoothRemoteGATTCharacteristicFrom(characteristic) + val platformCharacteristic = discoveredServices.obtain(characteristic, writeType.properties) ioLock.withLock { when (writeType) { - WithResponse -> jsCharacteristic.writeValueWithResponse(data) - WithoutResponse -> jsCharacteristic.writeValueWithoutResponse(data) + WithResponse -> platformCharacteristic.writeValueWithResponse(data) + WithoutResponse -> platformCharacteristic.writeValueWithoutResponse(data) }.await() } } @@ -209,9 +210,9 @@ public class JsPeripheral internal constructor( public suspend fun readAsDataView( characteristic: Characteristic ): DataView { - val jsCharacteristic = bluetoothRemoteGATTCharacteristicFrom(characteristic) + val platformCharacteristic = discoveredServices.obtain(characteristic, Read) val value = ioLock.withLock { - jsCharacteristic.readValue().await() + platformCharacteristic.readValue().await() } logger.debug { message = "read" @@ -237,18 +238,18 @@ public class JsPeripheral internal constructor( detail(data) } - val jsDescriptor = bluetoothRemoteGATTDescriptorFrom(descriptor) + val platformDescriptor = discoveredServices.obtain(descriptor) ioLock.withLock { - jsDescriptor.writeValue(data).await() + platformDescriptor.writeValue(data).await() } } public suspend fun readAsDataView( descriptor: Descriptor ): DataView { - val jsDescriptor = bluetoothRemoteGATTDescriptorFrom(descriptor) + val platformDescriptor = discoveredServices.obtain(descriptor) val value = ioLock.withLock { - jsDescriptor.readValue().await() + platformDescriptor.readValue().await() } logger.debug { message = "read" @@ -296,7 +297,7 @@ public class JsPeripheral internal constructor( val listener = characteristic.createListener() observationListeners[characteristic] = listener - bluetoothRemoteGATTCharacteristicFrom(characteristic).apply { + discoveredServices.obtain(characteristic, Notify or Indicate).apply { addEventListener(CHARACTERISTIC_VALUE_CHANGED, listener) ioLock.withLock { withContext(NonCancellable) { @@ -313,7 +314,7 @@ public class JsPeripheral internal constructor( detail(characteristic) } - bluetoothRemoteGATTCharacteristicFrom(characteristic).apply { + discoveredServices.obtain(characteristic, Notify or Indicate).apply { /* Throws `DOMException` if connection is closed: * * DOMException: Failed to execute 'stopNotifications' on 'BluetoothRemoteGATTCharacteristic': @@ -365,14 +366,6 @@ public class JsPeripheral internal constructor( bluetoothDevice.removeEventListener(GATT_SERVER_DISCONNECTED, disconnectedListener) } - private fun bluetoothRemoteGATTCharacteristicFrom( - characteristic: Characteristic - ) = platformServices.findCharacteristic(characteristic).bluetoothRemoteGATTCharacteristic - - private fun bluetoothRemoteGATTDescriptorFrom( - descriptor: Descriptor - ) = platformServices.findDescriptor(descriptor).bluetoothRemoteGATTDescriptor - override fun toString(): String = "Peripheral(bluetoothDevice=${bluetoothDevice.string()})" } diff --git a/core/src/jsMain/kotlin/PlatformCharacteristic.kt b/core/src/jsMain/kotlin/PlatformCharacteristic.kt deleted file mode 100644 index e5ac1ea9d..000000000 --- a/core/src/jsMain/kotlin/PlatformCharacteristic.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.juul.kable - -import com.benasher44.uuid.Uuid -import com.juul.kable.external.BluetoothRemoteGATTCharacteristic -import com.juul.kable.logs.Logger -import com.juul.kable.logs.detail -import kotlinx.coroutines.await - -internal data class PlatformCharacteristic( - override val serviceUuid: Uuid, - override val characteristicUuid: Uuid, - val bluetoothRemoteGATTCharacteristic: BluetoothRemoteGATTCharacteristic, - val descriptors: List, -) : Characteristic - -internal fun PlatformCharacteristic.toDiscoveredCharacteristic() = DiscoveredCharacteristic( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid, - descriptors = descriptors.map { it.toLazyDescriptor() }, -) - -internal suspend fun BluetoothRemoteGATTCharacteristic.toPlatformCharacteristic( - serviceUuid: Uuid, - logger: Logger, -): PlatformCharacteristic { - val characteristicUuid = uuid.toUuid() - val descriptors = runCatching { getDescriptors().await() } - .onFailure { cause -> - logger.error(cause) { - message = "Unable to retrieve descriptor." - detail(this@toPlatformCharacteristic) - } - } - .getOrDefault(emptyArray()) - val platformDescriptors = descriptors.map { descriptor -> - descriptor.toPlatformDescriptor(serviceUuid, characteristicUuid) - } - - return PlatformCharacteristic( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid, - descriptors = platformDescriptors, - bluetoothRemoteGATTCharacteristic = this, - ) -} diff --git a/core/src/jsMain/kotlin/PlatformDescriptor.kt b/core/src/jsMain/kotlin/PlatformDescriptor.kt deleted file mode 100644 index 80d29c15f..000000000 --- a/core/src/jsMain/kotlin/PlatformDescriptor.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.juul.kable - -import com.benasher44.uuid.Uuid -import com.juul.kable.external.BluetoothRemoteGATTDescriptor - -internal data class PlatformDescriptor( - override val serviceUuid: Uuid, - override val characteristicUuid: Uuid, - override val descriptorUuid: Uuid, - val bluetoothRemoteGATTDescriptor: BluetoothRemoteGATTDescriptor, -) : Descriptor - -internal fun PlatformDescriptor.toLazyDescriptor() = LazyDescriptor( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid, - descriptorUuid = descriptorUuid, -) - -internal fun BluetoothRemoteGATTDescriptor.toPlatformDescriptor( - serviceUuid: Uuid, - characteristicUuid: Uuid, -) = PlatformDescriptor( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid, - descriptorUuid = uuid.toUuid(), - bluetoothRemoteGATTDescriptor = this, -) diff --git a/core/src/jsMain/kotlin/PlatformService.kt b/core/src/jsMain/kotlin/PlatformService.kt deleted file mode 100644 index c7fb6a6be..000000000 --- a/core/src/jsMain/kotlin/PlatformService.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.juul.kable - -import com.benasher44.uuid.Uuid -import com.juul.kable.external.BluetoothRemoteGATTService -import com.juul.kable.logs.Logger -import kotlinx.coroutines.await - -internal data class PlatformService( - override val serviceUuid: Uuid, - val bluetoothRemoteGATTService: BluetoothRemoteGATTService, - val characteristics: List, -) : Service - -internal fun PlatformService.toDiscoveredService() = DiscoveredService( - serviceUuid = serviceUuid, - characteristics = characteristics.map { it.toDiscoveredCharacteristic() }, -) - -internal suspend fun BluetoothRemoteGATTService.toPlatformService(logger: Logger): PlatformService { - val serviceUuid = uuid.toUuid() - val characteristics = getCharacteristics() - .await() - .map { characteristic -> - characteristic.toPlatformCharacteristic(serviceUuid, logger) - } - - return PlatformService( - serviceUuid = serviceUuid, - characteristics = characteristics, - bluetoothRemoteGATTService = this, - ) -} - -/** @throws NoSuchElementException if service or characteristic is not found. */ -internal fun List.findCharacteristic( - characteristic: Characteristic -): PlatformCharacteristic = - findCharacteristic( - serviceUuid = characteristic.serviceUuid, - characteristicUuid = characteristic.characteristicUuid - ) - -/** @throws NoSuchElementException if service or characteristic is not found. */ -private fun List.findCharacteristic( - serviceUuid: Uuid, - characteristicUuid: Uuid -): PlatformCharacteristic = - first(serviceUuid) - .characteristics - .first(characteristicUuid) - -/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */ -internal fun List.findDescriptor( - descriptor: Descriptor -): PlatformDescriptor = - findDescriptor( - serviceUuid = descriptor.serviceUuid, - characteristicUuid = descriptor.characteristicUuid, - descriptorUuid = descriptor.descriptorUuid - ) - -/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */ -private fun List.findDescriptor( - serviceUuid: Uuid, - characteristicUuid: Uuid, - descriptorUuid: Uuid -): PlatformDescriptor = - findCharacteristic( - serviceUuid = serviceUuid, - characteristicUuid = characteristicUuid - ).descriptors.first(descriptorUuid) diff --git a/core/src/jsMain/kotlin/Profile.kt b/core/src/jsMain/kotlin/Profile.kt new file mode 100644 index 000000000..9f67b7a7b --- /dev/null +++ b/core/src/jsMain/kotlin/Profile.kt @@ -0,0 +1,93 @@ +package com.juul.kable + +import com.benasher44.uuid.Uuid +import com.juul.kable.Characteristic.Properties +import com.juul.kable.external.BluetoothCharacteristicProperties +import com.juul.kable.external.BluetoothRemoteGATTCharacteristic +import com.juul.kable.external.BluetoothRemoteGATTDescriptor +import com.juul.kable.external.BluetoothRemoteGATTService +import com.juul.kable.logs.Logger +import com.juul.kable.logs.detail +import kotlinx.coroutines.await + +@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316 +internal actual typealias PlatformService = BluetoothRemoteGATTService +@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316 +internal actual typealias PlatformCharacteristic = BluetoothRemoteGATTCharacteristic +@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316 +internal actual typealias PlatformDescriptor = BluetoothRemoteGATTDescriptor +private typealias PlatformProperties = BluetoothCharacteristicProperties + +public actual data class DiscoveredService internal constructor( + internal actual val service: PlatformService, + public actual val characteristics: List, +) : Service { + + override val serviceUuid: Uuid = service.uuid.toUuid() +} + +public actual data class DiscoveredCharacteristic internal constructor( + internal actual val characteristic: PlatformCharacteristic, + public actual val descriptors: List, +) : Characteristic { + + override val serviceUuid: Uuid = characteristic.service.uuid.toUuid() + override val characteristicUuid: Uuid = characteristic.uuid.toUuid() + + public actual val properties: Properties = Properties(characteristic.properties) +} + +public actual data class DiscoveredDescriptor internal constructor( + internal actual val descriptor: PlatformDescriptor, +) : Descriptor { + + override val serviceUuid: Uuid = descriptor.characteristic.service.uuid.toUuid() + override val characteristicUuid: Uuid = descriptor.characteristic.uuid.toUuid() + override val descriptorUuid: Uuid = descriptor.uuid.toUuid() +} + +internal suspend fun PlatformService.toDiscoveredService(logger: Logger): DiscoveredService { + val characteristics = getCharacteristics() + .await() + .map { characteristic -> + characteristic.toDiscoveredCharacteristic(logger) + } + + return DiscoveredService( + service = this, + characteristics = characteristics, + ) +} + +private suspend fun BluetoothRemoteGATTCharacteristic.toDiscoveredCharacteristic( + logger: Logger, +): DiscoveredCharacteristic { + val descriptors = runCatching { getDescriptors().await() } + .onFailure { + logger.warn { + message = "Unable to retrieve descriptor." + detail(this@toDiscoveredCharacteristic) + } + } + .getOrDefault(emptyArray()) + val platformDescriptors = descriptors.map(::DiscoveredDescriptor) + + return DiscoveredCharacteristic( + characteristic = this, + descriptors = platformDescriptors, + ) +} + +private fun Properties(platformProperties: PlatformProperties): Properties { + var properties = Properties(0) + with(platformProperties) { + if (broadcast) properties = properties or Broadcast + if (read) properties = properties or Read + if (writeWithoutResponse) properties = properties or WriteWithoutResponse + if (write) properties = properties or Write + if (notify) properties = properties or Notify + if (indicate) properties = properties or Indicate + if (authenticatedSignedWrites) properties = properties or SignedWrite + } + return properties +} diff --git a/core/src/jsMain/kotlin/external/BluetoothCharacteristicProperties.kt b/core/src/jsMain/kotlin/external/BluetoothCharacteristicProperties.kt new file mode 100644 index 000000000..a967b6155 --- /dev/null +++ b/core/src/jsMain/kotlin/external/BluetoothCharacteristicProperties.kt @@ -0,0 +1,35 @@ +package com.juul.kable.external + +/** + * https://developer.mozilla.org/en-US/docs/Web/API/BluetoothCharacteristicProperties + * https://webbluetoothcg.github.io/web-bluetooth/#characteristicproperties-interface + */ +internal external interface BluetoothCharacteristicProperties { + + /** Returns a boolean that is `true` if signed writing to the characteristic value is permitted. */ + val authenticatedSignedWrites: Boolean + + /** Returns a boolean that is `true` if the broadcast of the characteristic value is permitted using the Server Characteristic Configuration Descriptor. */ + val broadcast: Boolean + + /** Returns a boolean that is `true` if indications of the characteristic value with acknowledgement is permitted. */ + val indicate: Boolean + + /** Returns a boolean that is `true` if notifications of the characteristic value without acknowledgement is permitted. */ + val notify: Boolean + + /** Returns a boolean that is `true` if the reading of the characteristic value is permitted. */ + val read: Boolean + + /** Returns a boolean that is `true` if reliable writes to the characteristic is permitted. */ + val reliableWrite: Boolean + + /** Returns a boolean that is `true` if reliable writes to the characteristic descriptor is permitted. */ + val writableAuxiliaries: Boolean + + /** Returns a boolean that is `true` if the writing to the characteristic with response is permitted. */ + val write: Boolean + + /** Returns a boolean that is `true` if the writing to the characteristic without response is permitted. */ + val writeWithoutResponse: Boolean +} diff --git a/core/src/jsMain/kotlin/external/BluetoothRemoteGATTCharacteristic.kt b/core/src/jsMain/kotlin/external/BluetoothRemoteGATTCharacteristic.kt index 286e4e161..8e3c0bf86 100644 --- a/core/src/jsMain/kotlin/external/BluetoothRemoteGATTCharacteristic.kt +++ b/core/src/jsMain/kotlin/external/BluetoothRemoteGATTCharacteristic.kt @@ -13,6 +13,7 @@ internal abstract external class BluetoothRemoteGATTCharacteristic : EventTarget val service: BluetoothRemoteGATTService val uuid: UUID + val properties: BluetoothCharacteristicProperties val value: DataView? fun getDescriptor(descriptor: BluetoothDescriptorUUID): Promise diff --git a/core/src/jsMain/kotlin/external/BluetoothRemoteGATTDescriptor.kt b/core/src/jsMain/kotlin/external/BluetoothRemoteGATTDescriptor.kt index 56399784e..f468010fb 100644 --- a/core/src/jsMain/kotlin/external/BluetoothRemoteGATTDescriptor.kt +++ b/core/src/jsMain/kotlin/external/BluetoothRemoteGATTDescriptor.kt @@ -11,6 +11,7 @@ import kotlin.js.Promise */ internal abstract external class BluetoothRemoteGATTDescriptor : EventTarget { val uuid: UUID + val characteristic: BluetoothRemoteGATTCharacteristic fun readValue(): Promise fun writeValue(value: BufferSource): Promise }