Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify/optimize characteristic and descriptor lookups #56

Merged
merged 6 commits into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 34 additions & 69 deletions core/src/androidMain/kotlin/Peripheral.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.juul.kable

import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
import android.bluetooth.BluetoothGattDescriptor
Expand Down Expand Up @@ -70,9 +69,12 @@ public class AndroidPeripheral internal constructor(
private val observers = Observers(this)

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

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

@Volatile
private var _connection: Connection? = null
Expand Down Expand Up @@ -144,7 +146,7 @@ public class AndroidPeripheral internal constructor(
connection.execute<OnServicesDiscovered> {
discoverServices()
}
platformServices = connection.bluetoothGatt
_platformServices = connection.bluetoothGatt
.services
.map { it.toPlatformService() }
}
Expand Down Expand Up @@ -206,37 +208,51 @@ public class AndroidPeripheral internal constructor(
): Flow<ByteArray> = observers.acquire(characteristic)

internal suspend fun startNotifications(characteristic: Characteristic) {
val bluetoothGattCharacteristic = bluetoothGattCharacteristicFrom(characteristic)
connection.bluetoothGatt.setCharacteristicNotification(bluetoothGattCharacteristic, true)
writeConfigDescriptor(characteristic, ENABLE_NOTIFICATION_VALUE)
val platformCharacteristic = platformServices.findCharacteristic(characteristic)
connection
.bluetoothGatt
.setCharacteristicNotification(platformCharacteristic.bluetoothGattCharacteristic, true)

writeConfigDescriptor(platformCharacteristic, ENABLE_NOTIFICATION_VALUE)
}

internal suspend fun stopNotifications(characteristic: Characteristic) {
writeConfigDescriptor(characteristic, DISABLE_NOTIFICATION_VALUE)
val bluetoothGattCharacteristic = bluetoothGattCharacteristicFrom(characteristic)
connection.bluetoothGatt.setCharacteristicNotification(bluetoothGattCharacteristic, false)
val platformCharacteristic = platformServices.findCharacteristic(characteristic)
writeConfigDescriptor(platformCharacteristic, DISABLE_NOTIFICATION_VALUE)

val bluetoothGattCharacteristic = platformCharacteristic.bluetoothGattCharacteristic
connection
.bluetoothGatt
.setCharacteristicNotification(bluetoothGattCharacteristic, false)
}

private suspend fun writeConfigDescriptor(
characteristic: Characteristic,
characteristic: PlatformCharacteristic,
value: ByteArray
) {
if (writeObserveDescriptor == Never) return

val descriptor = LazyDescriptor(
serviceUuid = characteristic.serviceUuid,
characteristicUuid = characteristic.characteristicUuid,
descriptorUuid = clientCharacteristicConfigUuid
)
val bluetoothGattDescriptor = bluetoothGattDescriptorOrNullFrom(descriptor)
val bluetoothGattDescriptor = characteristic
.descriptors
.firstOrNull(clientCharacteristicConfigUuid)
?.bluetoothGattDescriptor

if (bluetoothGattDescriptor != null) {
write(bluetoothGattDescriptor, value)
} else if (writeObserveDescriptor == Always) {
error("Unable to start observation for $characteristic, config descriptor not found.")
val uuid = characteristic.characteristicUuid
error("Unable to start observation for characteristic $uuid, config descriptor not found.")
}
}

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

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

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

Expand All @@ -250,57 +266,6 @@ private suspend fun Peripheral.suspendUntilDisconnected() {
state.first { it is State.Disconnected }
}

private fun AndroidPeripheral.bluetoothGattCharacteristicFrom(
characteristic: Characteristic,
): BluetoothGattCharacteristic {
val services = checkNotNull(platformServices) {
"Services have not been discovered for $this"
}

val characteristics = services
.first { characteristic.serviceUuid == it.serviceUuid }
.characteristics
return characteristics
.first { characteristic.characteristicUuid == it.characteristicUuid }
.bluetoothGattCharacteristic
}

private fun AndroidPeripheral.bluetoothGattDescriptorFrom(
descriptor: Descriptor,
): BluetoothGattDescriptor {
val services = checkNotNull(platformServices) {
"Services have not been discovered for $this"
}

val characteristics = services
.first { descriptor.serviceUuid == it.serviceUuid }
.characteristics
val descriptors = characteristics
.first { descriptor.characteristicUuid == it.characteristicUuid }
.descriptors
return descriptors
.first { descriptor.descriptorUuid == it.descriptorUuid }
.bluetoothGattDescriptor
}

private fun AndroidPeripheral.bluetoothGattDescriptorOrNullFrom(
descriptor: Descriptor,
): BluetoothGattDescriptor? {
val services = checkNotNull(platformServices) {
"Services have not been discovered for $this"
}

val characteristics = services
.firstOrNull { descriptor.serviceUuid == it.serviceUuid }
?.characteristics
val descriptors = characteristics
?.firstOrNull { descriptor.characteristicUuid == it.characteristicUuid }
?.descriptors
return descriptors
?.firstOrNull { descriptor.descriptorUuid == it.descriptorUuid }
?.bluetoothGattDescriptor
}

private val WriteType.intValue: Int
get() = when (this) {
WithResponse -> WRITE_TYPE_DEFAULT
Expand Down
39 changes: 39 additions & 0 deletions core/src/androidMain/kotlin/PlatformService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,42 @@ internal fun BluetoothGattService.toPlatformService(): PlatformService {
bluetoothGattService = this,
)
}

/** @throws NoSuchElementException if service or characteristic is not found. */
internal fun List<PlatformService>.findCharacteristic(
characteristic: Characteristic
): PlatformCharacteristic =
findCharacteristic(
serviceUuid = characteristic.serviceUuid,
characteristicUuid = characteristic.characteristicUuid
)

/** @throws NoSuchElementException if service or characteristic is not found. */
private fun List<PlatformService>.findCharacteristic(
serviceUuid: Uuid,
characteristicUuid: Uuid
): PlatformCharacteristic =
first(serviceUuid)
.characteristics
.first(characteristicUuid)

/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */
internal fun List<PlatformService>.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<PlatformService>.findDescriptor(
serviceUuid: Uuid,
characteristicUuid: Uuid,
descriptorUuid: Uuid
): PlatformDescriptor =
findCharacteristic(
serviceUuid = serviceUuid,
characteristicUuid = characteristicUuid
).descriptors.first(descriptorUuid)
5 changes: 5 additions & 0 deletions core/src/commonMain/kotlin/Characteristic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ public data class DiscoveredCharacteristic internal constructor(
override val characteristicUuid: Uuid,
public val descriptors: List<Descriptor>,
) : Characteristic

internal fun <T : Characteristic> List<T>.first(
characteristicUuid: Uuid
): T = firstOrNull { it.characteristicUuid == characteristicUuid }
?: throw NoSuchElementException("Characteristic $characteristicUuid not found")
9 changes: 9 additions & 0 deletions core/src/commonMain/kotlin/Descriptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@ public data class LazyDescriptor(
public override val characteristicUuid: Uuid,
public override val descriptorUuid: Uuid,
) : Descriptor

internal fun <T : Descriptor> List<T>.first(
descriptorUuid: Uuid
): T = firstOrNull(descriptorUuid)
?: throw NoSuchElementException("Descriptor $descriptorUuid not found")

internal fun <T : Descriptor> List<T>.firstOrNull(
descriptorUuid: Uuid
): T? = firstOrNull { it.descriptorUuid == descriptorUuid }
6 changes: 6 additions & 0 deletions core/src/commonMain/kotlin/Service.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ public data class DiscoveredService internal constructor(
override val serviceUuid: Uuid,
public val characteristics: List<DiscoveredCharacteristic>,
) : Service

/** @throws IOException if service is not found. */
internal fun <T : Service> List<T>.first(
serviceUuid: Uuid
): T = firstOrNull { it.serviceUuid == serviceUuid }
?: throw NoSuchElementException("Service $serviceUuid not found")
46 changes: 14 additions & 32 deletions core/src/jsMain/kotlin/Peripheral.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import com.juul.kable.WriteType.WithResponse
import com.juul.kable.WriteType.WithoutResponse
import com.juul.kable.external.BluetoothAdvertisingEvent
import com.juul.kable.external.BluetoothDevice
import com.juul.kable.external.BluetoothRemoteGATTCharacteristic
import com.juul.kable.external.BluetoothRemoteGATTDescriptor
import com.juul.kable.external.BluetoothRemoteGATTServer
import com.juul.kable.external.string
import kotlinx.coroutines.CancellationException
Expand Down Expand Up @@ -52,9 +50,12 @@ public class JsPeripheral internal constructor(
private val _state = MutableStateFlow<State?>(null)
public override val state: Flow<State> = _state.filterNotNull()

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

public override val services: List<DiscoveredService>?
get() = platformServices?.map { it.toDiscoveredService() }
get() = _platformServices?.map { it.toDiscoveredService() }
davidtaylor-juul marked this conversation as resolved.
Show resolved Hide resolved

private val supportsAdvertisements = js("BluetoothDevice.prototype.watchAdvertisements") != null

Expand Down Expand Up @@ -138,7 +139,7 @@ public class JsPeripheral internal constructor(
val services = gatt.getPrimaryServices()
.await()
.map { it.toPlatformService() }
platformServices = services
_platformServices = services
return services
}

Expand Down Expand Up @@ -199,33 +200,6 @@ public class JsPeripheral internal constructor(
): Flow<ByteArray> = observeDataView(characteristic)
.map { it.buffer.toByteArray() }

internal fun bluetoothRemoteGATTCharacteristicFrom(
characteristic: Characteristic
): BluetoothRemoteGATTCharacteristic {
val services = checkNotNull(platformServices) { "Services have not been discovered for $this" }
val characteristics = services
.first { it.serviceUuid == characteristic.serviceUuid }
.characteristics
return characteristics
.first { it.characteristicUuid == characteristic.characteristicUuid }
.bluetoothRemoteGATTCharacteristic
}

private fun bluetoothRemoteGATTDescriptorFrom(
descriptor: Descriptor
): BluetoothRemoteGATTDescriptor {
val services = checkNotNull(platformServices) { "Services have not been discovered for $this" }
val characteristics = services
.first { service -> service.serviceUuid == descriptor.serviceUuid }
.characteristics
val descriptors = characteristics
.first { it.characteristicUuid == descriptor.characteristicUuid }
.descriptors
return descriptors
.first { it.descriptorUuid == descriptor.descriptorUuid }
.bluetoothRemoteGATTDescriptor
}

private var isDisconnectedListenerRegistered = false
private val disconnectedListener: (JsEvent) -> Unit = { event ->
console.dir(event)
Expand All @@ -246,5 +220,13 @@ public class JsPeripheral internal constructor(
bluetoothDevice.removeEventListener(GATT_SERVER_DISCONNECTED, disconnectedListener)
}

internal 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()})"
}
39 changes: 39 additions & 0 deletions core/src/jsMain/kotlin/PlatformService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,42 @@ internal suspend fun BluetoothRemoteGATTService.toPlatformService(): PlatformSer
bluetoothRemoteGATTService = this,
)
}

/** @throws NoSuchElementException if service or characteristic is not found. */
internal fun List<PlatformService>.findCharacteristic(
characteristic: Characteristic
): PlatformCharacteristic =
findCharacteristic(
serviceUuid = characteristic.serviceUuid,
characteristicUuid = characteristic.characteristicUuid
)

/** @throws NoSuchElementException if service or characteristic is not found. */
private fun List<PlatformService>.findCharacteristic(
serviceUuid: Uuid,
characteristicUuid: Uuid
): PlatformCharacteristic =
first(serviceUuid)
.characteristics
.first(characteristicUuid)

/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */
internal fun List<PlatformService>.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<PlatformService>.findDescriptor(
serviceUuid: Uuid,
characteristicUuid: Uuid,
descriptorUuid: Uuid
): PlatformDescriptor =
findCharacteristic(
serviceUuid = serviceUuid,
characteristicUuid = characteristicUuid
).descriptors.first(descriptorUuid)
Loading