diff --git a/bluetooth/src/commonMain/kotlin/Bluetooth.kt b/bluetooth/src/commonMain/kotlin/Bluetooth.kt index df6c56cf9..0310b3178 100644 --- a/bluetooth/src/commonMain/kotlin/Bluetooth.kt +++ b/bluetooth/src/commonMain/kotlin/Bluetooth.kt @@ -628,6 +628,15 @@ fun Flow.value(): Flow = distinctUntilChanged( characteristic?.value() ?: emptyFlow() } +/** + * Gets a ([Flow] of) the [ByteArray] value from a [Flow] of an [RemoteCharacteristic] or an empty [ByteArray] if data is unavailable + * This method will automatically subscribe/unsubscribe to the [RemoteCharacteristic] when the [Flow] is collected + * @return the [Flow] of the [ByteArray] value of the [RemoteCharacteristic] in the given [Flow], or an empty [ByteArray] if data is unavailable + */ +fun Flow.valueOrEmpty(): Flow = distinctUntilChanged().flatMapLatest { characteristic -> + characteristic?.value() ?: flowOf(byteArrayOf()) +} + /** * Gets a ([Flow] of) [T] value from a [Flow] of an [RemoteCharacteristic] * This method will automatically subscribe/unsubscribe to the [RemoteCharacteristic] when the [Flow] is collected @@ -641,6 +650,19 @@ fun Flow.value(deserializationStrategy: Deserializati characteristic?.value(deserializationStrategy, bluetoothFormat) ?: emptyFlow() } +/** + * Gets a ([Flow] of) [T] value from a [Flow] of an [RemoteCharacteristic] or `null` if data is unavailable + * This method will automatically subscribe/unsubscribe to the [RemoteCharacteristic] when the [Flow] is collected + * @param T the type of the data to receive + * @param deserializationStrategy the [DeserializationStrategy] to use to deserialize the [ByteArray] to [T] + * @param bluetoothFormat the [BluetoothFormat] to use to deserialize the [ByteArray] to [T] + * @return the [Flow] of the [T] value of the [RemoteCharacteristic] in the given [Flow] or `null` if data is unavailable + */ +fun Flow.valueOrNull(deserializationStrategy: DeserializationStrategy, bluetoothFormat: BluetoothFormat = BluetoothFormat): Flow = + distinctUntilChanged().flatMapLatest { characteristic -> + characteristic?.value(deserializationStrategy, bluetoothFormat) ?: flowOf(null) + } + /** * Gets a ([Flow] of) [T] value from a [Flow] of an [RemoteCharacteristic] * This method will automatically subscribe/unsubscribe to the [RemoteCharacteristic] when the [Flow] is collected @@ -651,6 +673,16 @@ fun Flow.value(deserializationStrategy: Deserializati inline fun Flow.value(bluetoothFormat: BluetoothFormat = BluetoothFormat): Flow = value(bluetoothFormat.serializersModule.serializer(), bluetoothFormat) +/** + * Gets a ([Flow] of) [T] value from a [Flow] of an [RemoteCharacteristic] + * This method will automatically subscribe/unsubscribe to the [RemoteCharacteristic] when the [Flow] is collected + * @param T the type of the data to receive + * @param bluetoothFormat the [BluetoothFormat] to use to deserialize the [ByteArray] to [T] + * @return the [Flow] of the [T] value of the [RemoteCharacteristic] in the given [Flow] + */ +inline fun Flow.valueOrNull(bluetoothFormat: BluetoothFormat = BluetoothFormat): Flow = + valueOrNull(bluetoothFormat.serializersModule.serializer(), bluetoothFormat) + /** * Gets a ([Flow] of) the [ByteArray] value from a [RemoteCharacteristic] * This method will automatically subscribe/unsubscribe to the [RemoteCharacteristic] when the [Flow] is collected diff --git a/bluetooth/src/commonMain/kotlin/extensions/Accessors.kt b/bluetooth/src/commonMain/kotlin/extensions/Accessors.kt index e35850e00..2ee484475 100644 --- a/bluetooth/src/commonMain/kotlin/extensions/Accessors.kt +++ b/bluetooth/src/commonMain/kotlin/extensions/Accessors.kt @@ -1,58 +1,127 @@ package com.splendo.kaluga.bluetooth.extensions -import com.splendo.kaluga.bluetooth.Characteristic +import com.splendo.kaluga.bluetooth.RemoteCharacteristic import com.splendo.kaluga.bluetooth.RemoteDescriptor +import com.splendo.kaluga.bluetooth.RemoteService import com.splendo.kaluga.bluetooth.UUIDException import com.splendo.kaluga.bluetooth.characteristics import com.splendo.kaluga.bluetooth.descriptors import com.splendo.kaluga.bluetooth.device.ConnectableDevice +import com.splendo.kaluga.bluetooth.discoveredServices +import com.splendo.kaluga.bluetooth.serialization.BluetoothFormat import com.splendo.kaluga.bluetooth.services import com.splendo.kaluga.bluetooth.value +import com.splendo.kaluga.bluetooth.valueOrNull import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.serialization.DeserializationStrategy /** - * Provides access to device data flow by service, characteristic and descriptor string uuids. + * Provides access to a [RemoteService] [Flow] by service string uuids. + * Only emits after services have been discovered. + * @param serviceUUID string service uuid representation + * @throws UUIDException.InvalidFormat if [serviceUUID] is not valid + * @return the [Flow] of the [com.splendo.kaluga.bluetooth.RemoteService] associated with [serviceUUID]. Flow throws [NoSuchElementException] if the service cannot be found after discovery. + */ +fun Flow.serviceFlow(serviceUUID: String) = discoveredServices()[serviceUUID] + +/** + * Provides access to a [RemoteService] [Flow] by service string uuids. + * Emits `null` if the service cannot be found. + * @param serviceUUID string service uuid representation + * @throws UUIDException.InvalidFormat if [serviceUUID] is not valid + * @return the [Flow] of the [RemoteService] associated with [serviceUUID] or `null` if the service is not available. + */ +fun Flow.serviceOrNullFlow(serviceUUID: String) = services().getOrNull(serviceUUID) + +/** + * Provides access to [RemoteService] by service string uuids. + * The method will suspend until services have been discovered. + * @param serviceUUID string service uuid representation + * @throws UUIDException.InvalidFormat if [serviceUUID] is not valid + * @throws NoSuchElementException if the service cannot be found after discovery. + * @return the [RemoteService] associated with [serviceUUID] + */ +suspend fun Flow.service(serviceUUID: String) = serviceFlow(serviceUUID) + .first() + +/** + * Provides access to [RemoteService] by service and characteristic string uuids or `null` if not available. * @param serviceUUID string service uuid representation - * @param characteristicUUID string characteristic uuid representation * @throws UUIDException.InvalidFormat + * @return the [RemoteService] associated with [serviceUUID] or `null` if not available */ -fun Flow.dataFlow(serviceUUID: String, characteristicUUID: String) = characteristicsFlow(serviceUUID, characteristicUUID).value() +suspend fun Flow.serviceOrNull(serviceUUID: String) = serviceOrNullFlow(serviceUUID) + .first() /** - * Provides access to characteristic's flow by service and characteristic string uuids. + * Provides access to [RemoteCharacteristic]'s flow by service and characteristic string uuids. + * Only emits after services have been discovered. * @param serviceUUID string service uuid representation * @param characteristicUUID string characteristic uuid representation - * @throws UUIDException.InvalidFormat + * @throws UUIDException.InvalidFormat if [serviceUUID] or [characteristicUUID] is not valid + * @return the [Flow] of the [RemoteCharacteristic] associated with [serviceUUID] and [characteristicUUID]. Flow throws [NoSuchElementException] if the characteristic cannot be found after discovery. */ -fun Flow.characteristicsFlow(serviceUUID: String, characteristicUUID: String) = services().getOrNull(serviceUUID) +fun Flow.characteristicFlow(serviceUUID: String, characteristicUUID: String) = serviceFlow(serviceUUID) .characteristics()[characteristicUUID] - .filterNotNull() /** - * Provides access to [Characteristic] by service and characteristic string uuids. + * Provides access to [RemoteCharacteristic]'s flow by service and characteristic string uuids. + * Emits `null` if the characteristic cannot be found. + * @param serviceUUID string service uuid representation + * @param characteristicUUID string characteristic uuid representation + * @throws UUIDException.InvalidFormat if [serviceUUID] or [characteristicUUID] is not valid + * @return the [Flow] of the [RemoteCharacteristic] associated with [serviceUUID] and [characteristicUUID] or `null` if the characteristic is not available. + */ +fun Flow.characteristicOrNullFlow(serviceUUID: String, characteristicUUID: String) = serviceOrNullFlow(serviceUUID) + .characteristics().getOrNull(characteristicUUID) + +/** + * Provides access to [RemoteCharacteristic] by service and characteristic string uuids. * The method will suspend if characteristic is not available. * @param serviceUUID string service uuid representation * @param characteristicUUID string characteristic uuid representation - * @throws UUIDException.InvalidFormat + * @throws UUIDException.InvalidFormat if [serviceUUID] or [characteristicUUID] is not valid + * @throws NoSuchElementException if the characteristic cannot be found after discovery. + * @return the [RemoteCharacteristic] associated with [serviceUUID] and [characteristicUUID] */ -suspend fun Flow.characteristic(serviceUUID: String, characteristicUUID: String) = services().getOrNull(serviceUUID) - .characteristics()[characteristicUUID] - .filterNotNull() +suspend fun Flow.characteristic(serviceUUID: String, characteristicUUID: String) = characteristicFlow(serviceUUID, characteristicUUID) .first() /** - * Provides access to descriptors's flow by service, characteristic and descriptor string uuids. + * Provides access to [RemoteCharacteristic] by service and characteristic string uuids or `null` if not available. + * @param serviceUUID string service uuid representation + * @param characteristicUUID string characteristic uuid representation + * @throws UUIDException.InvalidFormat if [serviceUUID] or [characteristicUUID] is not valid + * @return the [RemoteCharacteristic] associated with [serviceUUID] and [characteristicUUID] or `null` if not available + */ +suspend fun Flow.characteristicOrNull(serviceUUID: String, characteristicUUID: String) = characteristicOrNullFlow(serviceUUID, characteristicUUID) + .first() + +/** + * Provides access to [RemoteDescriptor]'s flow by service, characteristic, and descriptor string uuids. + * Only emits after services have been discovered. * @param serviceUUID string service uuid representation * @param characteristicUUID string characteristic uuid representation * @param descriptorUUID string descriptor uuid representation - * @throws UUIDException.InvalidFormat + * @throws UUIDException.InvalidFormat if [serviceUUID], [characteristicUUID], or [descriptorUUID] is not valid + * @return the [Flow] of the [RemoteDescriptor] associated with [serviceUUID], [characteristicUUID], and [descriptorUUID]. Flow throws [NoSuchElementException] if the descriptor cannot be found after discovery. */ -fun Flow.descriptorsFlow(serviceUUID: String, characteristicUUID: String, descriptorUUID: String) = services().getOrNull(serviceUUID) - .characteristics()[characteristicUUID] +fun Flow.descriptorFlow(serviceUUID: String, characteristicUUID: String, descriptorUUID: String) = characteristicFlow(serviceUUID, characteristicUUID) .descriptors()[descriptorUUID] - .filterNotNull() + +/** + * Provides access to [RemoteDescriptor]'s flow by service, characteristic, and descriptor string uuids. + * Emits `null` if the descriptor cannot be found. + * @param serviceUUID string service uuid representation + * @param characteristicUUID string characteristic uuid representation + * @param descriptorUUID string descriptor uuid representation + * @throws UUIDException.InvalidFormat if [serviceUUID], [characteristicUUID], or [descriptorUUID] is not valid + * @return the [Flow] of the [RemoteDescriptor] associated with [serviceUUID], [characteristicUUID], and [descriptorUUID] or `null` if the descriptor is not available. + */ +fun Flow.descriptorOrNullFlow(serviceUUID: String, characteristicUUID: String, descriptorUUID: String) = + characteristicOrNullFlow(serviceUUID, characteristicUUID) + .descriptors().getOrNull(descriptorUUID) /** * Provides access to [RemoteDescriptor] by service, characteristic and descriptor string uuids. @@ -60,10 +129,66 @@ fun Flow.descriptorsFlow(serviceUUID: String, characteristic * @param serviceUUID string service uuid representation * @param characteristicUUID string characteristic uuid representation * @param descriptorUUID string descriptor uuid representation - * @throws UUIDException.InvalidFormat + * @throws UUIDException.InvalidFormat if [serviceUUID], [characteristicUUID], or [descriptorUUID] is not valid + * @throws NoSuchElementException if the descriptor cannot be found after discovery. + * @return the [RemoteDescriptor] associated with [serviceUUID], [characteristicUUID], and [descriptorUUID] */ -suspend fun Flow.descriptor(serviceUUID: String, characteristicUUID: String, descriptorUUID: String) = services().getOrNull(serviceUUID) - .characteristics()[characteristicUUID] - .descriptors()[descriptorUUID] - .filterNotNull() - .first() +suspend fun Flow.descriptor(serviceUUID: String, characteristicUUID: String, descriptorUUID: String) = + descriptorFlow(serviceUUID, characteristicUUID, descriptorUUID) + .first() + +/** + * Provides access to device data flow by service and characteristic string uuids. + * Only emits after services have been discovered. + * @param serviceUUID string service uuid representation + * @param characteristicUUID string characteristic uuid representation + * @throws UUIDException.InvalidFormat if [serviceUUID] or [characteristicUUID] is not valid + * @return the [Flow] of the [ByteArray] value of the [RemoteCharacteristic] associated with [serviceUUID] and [characteristicUUID]. Flow throws [NoSuchElementException] if the characteristic cannot be found after discovery. + */ +fun Flow.dataFlow(serviceUUID: String, characteristicUUID: String) = characteristicFlow(serviceUUID, characteristicUUID).value() + +/** + * Provides access to device data flow [T] by service and characteristic string uuids. + * Only emits after services have been discovered. + * @param T the type of the data to receive + * @param serviceUUID string service uuid representation + * @param characteristicUUID string characteristic uuid representation + * @param deserializationStrategy the [DeserializationStrategy] to use to deserialize the [ByteArray] to [T] + * @param bluetoothFormat the [BluetoothFormat] to use to deserialize the [ByteArray] to [T] + * @throws UUIDException.InvalidFormat if [serviceUUID] or [characteristicUUID] is not valid + * @return the [Flow] of the [T] value of the [RemoteCharacteristic] associated with [serviceUUID] and [characteristicUUID]. Flow throws [NoSuchElementException] if the characteristic cannot be found after discovery. + */ +inline fun Flow.dataFlow( + serviceUUID: String, + characteristicUUID: String, + deserializationStrategy: DeserializationStrategy, + bluetoothFormat: BluetoothFormat = BluetoothFormat, +) = characteristicFlow(serviceUUID, characteristicUUID).value(deserializationStrategy, bluetoothFormat) + +/** + * Provides access to device data flow by service and characteristic string uuids. + * Emits and empty [ByteArray] if the service cannot be found. + * @param serviceUUID string service uuid representation + * @param characteristicUUID string characteristic uuid representation + * @throws UUIDException.InvalidFormat if [serviceUUID] or [characteristicUUID] is not valid + * @return the [Flow] of the [ByteArray] value of the [RemoteCharacteristic] associated with [serviceUUID] and [characteristicUUID]. Emits an empty [ByteArray] if the characteristic is not available. + */ +fun Flow.dataOrEmptyFlow(serviceUUID: String, characteristicUUID: String) = characteristicOrNullFlow(serviceUUID, characteristicUUID).value() + +/** + * Provides access to device data flow [T] by service and characteristic string uuids. + * Emits `null` if the descriptor cannot be found. + * @param T the type of the data to receive + * @param serviceUUID string service uuid representation + * @param characteristicUUID string characteristic uuid representation + * @param deserializationStrategy the [DeserializationStrategy] to use to deserialize the [ByteArray] to [T] + * @param bluetoothFormat the [BluetoothFormat] to use to deserialize the [ByteArray] to [T] + * @throws UUIDException.InvalidFormat if [serviceUUID] or [characteristicUUID] is not valid + * @return the [Flow] of the [T] value of the [RemoteCharacteristic] associated with [serviceUUID] and [characteristicUUID]. Emits `null` if the descriptor cannot be found. + */ +inline fun Flow.dataOrNullFlow( + serviceUUID: String, + characteristicUUID: String, + deserializationStrategy: DeserializationStrategy, + bluetoothFormat: BluetoothFormat = BluetoothFormat, +) = characteristicOrNullFlow(serviceUUID, characteristicUUID).valueOrNull(deserializationStrategy, bluetoothFormat)