Skip to content

Commit 34e34c5

Browse files
authored
Simplify/optimize characteristic and descriptor lookups (#56)
When a service, characteristic or descriptor cannot be found, `NoSuchElementException ` is now thrown with a `message` stating the UUID that could not be found. On Apple platform, services are now cached on service discovery (which matches behavior of the other platform targets). Closes #14 Closes #38
1 parent b134c24 commit 34e34c5

9 files changed

Lines changed: 201 additions & 138 deletions

File tree

core/src/androidMain/kotlin/Peripheral.kt

Lines changed: 34 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.juul.kable
22

33
import android.bluetooth.BluetoothDevice
4-
import android.bluetooth.BluetoothGattCharacteristic
54
import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
65
import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
76
import android.bluetooth.BluetoothGattDescriptor
@@ -70,9 +69,12 @@ public class AndroidPeripheral internal constructor(
7069
private val observers = Observers(this)
7170

7271
@Volatile
73-
internal var platformServices: List<PlatformService>? = null
72+
private var _platformServices: List<PlatformService>? = null
73+
private val platformServices: List<PlatformService>
74+
get() = checkNotNull(_platformServices) { "Services have not been discovered for $this" }
75+
7476
public override val services: List<DiscoveredService>?
75-
get() = platformServices?.map { it.toDiscoveredService() }
77+
get() = _platformServices?.map { it.toDiscoveredService() }
7678

7779
@Volatile
7880
private var _connection: Connection? = null
@@ -144,7 +146,7 @@ public class AndroidPeripheral internal constructor(
144146
connection.execute<OnServicesDiscovered> {
145147
discoverServices()
146148
}
147-
platformServices = connection.bluetoothGatt
149+
_platformServices = connection.bluetoothGatt
148150
.services
149151
.map { it.toPlatformService() }
150152
}
@@ -206,37 +208,51 @@ public class AndroidPeripheral internal constructor(
206208
): Flow<ByteArray> = observers.acquire(characteristic)
207209

208210
internal suspend fun startNotifications(characteristic: Characteristic) {
209-
val bluetoothGattCharacteristic = bluetoothGattCharacteristicFrom(characteristic)
210-
connection.bluetoothGatt.setCharacteristicNotification(bluetoothGattCharacteristic, true)
211-
writeConfigDescriptor(characteristic, ENABLE_NOTIFICATION_VALUE)
211+
val platformCharacteristic = platformServices.findCharacteristic(characteristic)
212+
connection
213+
.bluetoothGatt
214+
.setCharacteristicNotification(platformCharacteristic.bluetoothGattCharacteristic, true)
215+
216+
writeConfigDescriptor(platformCharacteristic, ENABLE_NOTIFICATION_VALUE)
212217
}
213218

214219
internal suspend fun stopNotifications(characteristic: Characteristic) {
215-
writeConfigDescriptor(characteristic, DISABLE_NOTIFICATION_VALUE)
216-
val bluetoothGattCharacteristic = bluetoothGattCharacteristicFrom(characteristic)
217-
connection.bluetoothGatt.setCharacteristicNotification(bluetoothGattCharacteristic, false)
220+
val platformCharacteristic = platformServices.findCharacteristic(characteristic)
221+
writeConfigDescriptor(platformCharacteristic, DISABLE_NOTIFICATION_VALUE)
222+
223+
val bluetoothGattCharacteristic = platformCharacteristic.bluetoothGattCharacteristic
224+
connection
225+
.bluetoothGatt
226+
.setCharacteristicNotification(bluetoothGattCharacteristic, false)
218227
}
219228

220229
private suspend fun writeConfigDescriptor(
221-
characteristic: Characteristic,
230+
characteristic: PlatformCharacteristic,
222231
value: ByteArray
223232
) {
224233
if (writeObserveDescriptor == Never) return
225234

226-
val descriptor = LazyDescriptor(
227-
serviceUuid = characteristic.serviceUuid,
228-
characteristicUuid = characteristic.characteristicUuid,
229-
descriptorUuid = clientCharacteristicConfigUuid
230-
)
231-
val bluetoothGattDescriptor = bluetoothGattDescriptorOrNullFrom(descriptor)
235+
val bluetoothGattDescriptor = characteristic
236+
.descriptors
237+
.firstOrNull(clientCharacteristicConfigUuid)
238+
?.bluetoothGattDescriptor
232239

233240
if (bluetoothGattDescriptor != null) {
234241
write(bluetoothGattDescriptor, value)
235242
} else if (writeObserveDescriptor == Always) {
236-
error("Unable to start observation for $characteristic, config descriptor not found.")
243+
val uuid = characteristic.characteristicUuid
244+
error("Unable to start observation for characteristic $uuid, config descriptor not found.")
237245
}
238246
}
239247

248+
private fun bluetoothGattCharacteristicFrom(
249+
characteristic: Characteristic
250+
) = platformServices.findCharacteristic(characteristic).bluetoothGattCharacteristic
251+
252+
private fun bluetoothGattDescriptorFrom(
253+
descriptor: Descriptor
254+
) = platformServices.findDescriptor(descriptor).bluetoothGattDescriptor
255+
240256
override fun toString(): String = "Peripheral(bluetoothDevice=$bluetoothDevice)"
241257
}
242258

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

253-
private fun AndroidPeripheral.bluetoothGattCharacteristicFrom(
254-
characteristic: Characteristic,
255-
): BluetoothGattCharacteristic {
256-
val services = checkNotNull(platformServices) {
257-
"Services have not been discovered for $this"
258-
}
259-
260-
val characteristics = services
261-
.first { characteristic.serviceUuid == it.serviceUuid }
262-
.characteristics
263-
return characteristics
264-
.first { characteristic.characteristicUuid == it.characteristicUuid }
265-
.bluetoothGattCharacteristic
266-
}
267-
268-
private fun AndroidPeripheral.bluetoothGattDescriptorFrom(
269-
descriptor: Descriptor,
270-
): BluetoothGattDescriptor {
271-
val services = checkNotNull(platformServices) {
272-
"Services have not been discovered for $this"
273-
}
274-
275-
val characteristics = services
276-
.first { descriptor.serviceUuid == it.serviceUuid }
277-
.characteristics
278-
val descriptors = characteristics
279-
.first { descriptor.characteristicUuid == it.characteristicUuid }
280-
.descriptors
281-
return descriptors
282-
.first { descriptor.descriptorUuid == it.descriptorUuid }
283-
.bluetoothGattDescriptor
284-
}
285-
286-
private fun AndroidPeripheral.bluetoothGattDescriptorOrNullFrom(
287-
descriptor: Descriptor,
288-
): BluetoothGattDescriptor? {
289-
val services = checkNotNull(platformServices) {
290-
"Services have not been discovered for $this"
291-
}
292-
293-
val characteristics = services
294-
.firstOrNull { descriptor.serviceUuid == it.serviceUuid }
295-
?.characteristics
296-
val descriptors = characteristics
297-
?.firstOrNull { descriptor.characteristicUuid == it.characteristicUuid }
298-
?.descriptors
299-
return descriptors
300-
?.firstOrNull { descriptor.descriptorUuid == it.descriptorUuid }
301-
?.bluetoothGattDescriptor
302-
}
303-
304269
private val WriteType.intValue: Int
305270
get() = when (this) {
306271
WithResponse -> WRITE_TYPE_DEFAULT

core/src/androidMain/kotlin/PlatformService.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,42 @@ internal fun BluetoothGattService.toPlatformService(): PlatformService {
2626
bluetoothGattService = this,
2727
)
2828
}
29+
30+
/** @throws NoSuchElementException if service or characteristic is not found. */
31+
internal fun List<PlatformService>.findCharacteristic(
32+
characteristic: Characteristic
33+
): PlatformCharacteristic =
34+
findCharacteristic(
35+
serviceUuid = characteristic.serviceUuid,
36+
characteristicUuid = characteristic.characteristicUuid
37+
)
38+
39+
/** @throws NoSuchElementException if service or characteristic is not found. */
40+
private fun List<PlatformService>.findCharacteristic(
41+
serviceUuid: Uuid,
42+
characteristicUuid: Uuid
43+
): PlatformCharacteristic =
44+
first(serviceUuid)
45+
.characteristics
46+
.first(characteristicUuid)
47+
48+
/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */
49+
internal fun List<PlatformService>.findDescriptor(
50+
descriptor: Descriptor
51+
): PlatformDescriptor =
52+
findDescriptor(
53+
serviceUuid = descriptor.serviceUuid,
54+
characteristicUuid = descriptor.characteristicUuid,
55+
descriptorUuid = descriptor.descriptorUuid
56+
)
57+
58+
/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */
59+
private fun List<PlatformService>.findDescriptor(
60+
serviceUuid: Uuid,
61+
characteristicUuid: Uuid,
62+
descriptorUuid: Uuid
63+
): PlatformDescriptor =
64+
findCharacteristic(
65+
serviceUuid = serviceUuid,
66+
characteristicUuid = characteristicUuid
67+
).descriptors.first(descriptorUuid)

core/src/commonMain/kotlin/Characteristic.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@ public data class DiscoveredCharacteristic internal constructor(
2626
override val characteristicUuid: Uuid,
2727
public val descriptors: List<Descriptor>,
2828
) : Characteristic
29+
30+
internal fun <T : Characteristic> List<T>.first(
31+
characteristicUuid: Uuid
32+
): T = firstOrNull { it.characteristicUuid == characteristicUuid }
33+
?: throw NoSuchElementException("Characteristic $characteristicUuid not found")

core/src/commonMain/kotlin/Descriptor.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@ public data class LazyDescriptor(
2424
public override val characteristicUuid: Uuid,
2525
public override val descriptorUuid: Uuid,
2626
) : Descriptor
27+
28+
internal fun <T : Descriptor> List<T>.first(
29+
descriptorUuid: Uuid
30+
): T = firstOrNull(descriptorUuid)
31+
?: throw NoSuchElementException("Descriptor $descriptorUuid not found")
32+
33+
internal fun <T : Descriptor> List<T>.firstOrNull(
34+
descriptorUuid: Uuid
35+
): T? = firstOrNull { it.descriptorUuid == descriptorUuid }

core/src/commonMain/kotlin/Service.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ public data class DiscoveredService internal constructor(
1010
override val serviceUuid: Uuid,
1111
public val characteristics: List<DiscoveredCharacteristic>,
1212
) : Service
13+
14+
/** @throws IOException if service is not found. */
15+
internal fun <T : Service> List<T>.first(
16+
serviceUuid: Uuid
17+
): T = firstOrNull { it.serviceUuid == serviceUuid }
18+
?: throw NoSuchElementException("Service $serviceUuid not found")

core/src/jsMain/kotlin/Peripheral.kt

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import com.juul.kable.WriteType.WithResponse
66
import com.juul.kable.WriteType.WithoutResponse
77
import com.juul.kable.external.BluetoothAdvertisingEvent
88
import com.juul.kable.external.BluetoothDevice
9-
import com.juul.kable.external.BluetoothRemoteGATTCharacteristic
10-
import com.juul.kable.external.BluetoothRemoteGATTDescriptor
119
import com.juul.kable.external.BluetoothRemoteGATTServer
1210
import com.juul.kable.external.string
1311
import kotlinx.coroutines.CancellationException
@@ -52,9 +50,12 @@ public class JsPeripheral internal constructor(
5250
private val _state = MutableStateFlow<State?>(null)
5351
public override val state: Flow<State> = _state.filterNotNull()
5452

55-
private var platformServices: List<PlatformService>? = null
53+
private var _platformServices: List<PlatformService>? = null
54+
private val platformServices: List<PlatformService>
55+
get() = checkNotNull(_platformServices) { "Services have not been discovered for $this" }
56+
5657
public override val services: List<DiscoveredService>?
57-
get() = platformServices?.map { it.toDiscoveredService() }
58+
get() = _platformServices?.map { it.toDiscoveredService() }
5859

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

@@ -138,7 +139,7 @@ public class JsPeripheral internal constructor(
138139
val services = gatt.getPrimaryServices()
139140
.await()
140141
.map { it.toPlatformService() }
141-
platformServices = services
142+
_platformServices = services
142143
return services
143144
}
144145

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

202-
internal fun bluetoothRemoteGATTCharacteristicFrom(
203-
characteristic: Characteristic
204-
): BluetoothRemoteGATTCharacteristic {
205-
val services = checkNotNull(platformServices) { "Services have not been discovered for $this" }
206-
val characteristics = services
207-
.first { it.serviceUuid == characteristic.serviceUuid }
208-
.characteristics
209-
return characteristics
210-
.first { it.characteristicUuid == characteristic.characteristicUuid }
211-
.bluetoothRemoteGATTCharacteristic
212-
}
213-
214-
private fun bluetoothRemoteGATTDescriptorFrom(
215-
descriptor: Descriptor
216-
): BluetoothRemoteGATTDescriptor {
217-
val services = checkNotNull(platformServices) { "Services have not been discovered for $this" }
218-
val characteristics = services
219-
.first { service -> service.serviceUuid == descriptor.serviceUuid }
220-
.characteristics
221-
val descriptors = characteristics
222-
.first { it.characteristicUuid == descriptor.characteristicUuid }
223-
.descriptors
224-
return descriptors
225-
.first { it.descriptorUuid == descriptor.descriptorUuid }
226-
.bluetoothRemoteGATTDescriptor
227-
}
228-
229203
private var isDisconnectedListenerRegistered = false
230204
private val disconnectedListener: (JsEvent) -> Unit = { event ->
231205
console.dir(event)
@@ -246,5 +220,13 @@ public class JsPeripheral internal constructor(
246220
bluetoothDevice.removeEventListener(GATT_SERVER_DISCONNECTED, disconnectedListener)
247221
}
248222

223+
internal fun bluetoothRemoteGATTCharacteristicFrom(
224+
characteristic: Characteristic
225+
) = platformServices.findCharacteristic(characteristic).bluetoothRemoteGATTCharacteristic
226+
227+
private fun bluetoothRemoteGATTDescriptorFrom(
228+
descriptor: Descriptor
229+
) = platformServices.findDescriptor(descriptor).bluetoothRemoteGATTDescriptor
230+
249231
override fun toString(): String = "Peripheral(bluetoothDevice=${bluetoothDevice.string()})"
250232
}

core/src/jsMain/kotlin/PlatformService.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,42 @@ internal suspend fun BluetoothRemoteGATTService.toPlatformService(): PlatformSer
3030
bluetoothRemoteGATTService = this,
3131
)
3232
}
33+
34+
/** @throws NoSuchElementException if service or characteristic is not found. */
35+
internal fun List<PlatformService>.findCharacteristic(
36+
characteristic: Characteristic
37+
): PlatformCharacteristic =
38+
findCharacteristic(
39+
serviceUuid = characteristic.serviceUuid,
40+
characteristicUuid = characteristic.characteristicUuid
41+
)
42+
43+
/** @throws NoSuchElementException if service or characteristic is not found. */
44+
private fun List<PlatformService>.findCharacteristic(
45+
serviceUuid: Uuid,
46+
characteristicUuid: Uuid
47+
): PlatformCharacteristic =
48+
first(serviceUuid)
49+
.characteristics
50+
.first(characteristicUuid)
51+
52+
/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */
53+
internal fun List<PlatformService>.findDescriptor(
54+
descriptor: Descriptor
55+
): PlatformDescriptor =
56+
findDescriptor(
57+
serviceUuid = descriptor.serviceUuid,
58+
characteristicUuid = descriptor.characteristicUuid,
59+
descriptorUuid = descriptor.descriptorUuid
60+
)
61+
62+
/** @throws NoSuchElementException if service, characteristic or descriptor is not found. */
63+
private fun List<PlatformService>.findDescriptor(
64+
serviceUuid: Uuid,
65+
characteristicUuid: Uuid,
66+
descriptorUuid: Uuid
67+
): PlatformDescriptor =
68+
findCharacteristic(
69+
serviceUuid = serviceUuid,
70+
characteristicUuid = characteristicUuid
71+
).descriptors.first(descriptorUuid)

0 commit comments

Comments
 (0)