Skip to content

Commit

Permalink
Convert discovered services, characteristics, descriptors to interfac…
Browse files Browse the repository at this point in the history
…es (#839)
  • Loading branch information
twyatt authored Jan 29, 2025
1 parent cbbb30e commit 66b6a6f
Show file tree
Hide file tree
Showing 21 changed files with 356 additions and 164 deletions.
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ androidx-core = { module = "androidx.core:core-ktx", version = "1.15.0" }
androidx-startup = { module = "androidx.startup:startup-runtime", version = "1.2.0" }
atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" }
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.1" }
equalsverifier = { module = "nl.jqno.equalsverifier:equalsverifier", version = "3.18.1" }
khronicle = { module = "com.juul.khronicle:khronicle-core", version = "0.5.1" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version = "0.6.0" }
mockk = { module = "io.mockk:mockk", version = "1.13.16" }
tuulbox-collections = { module = "com.juul.tuulbox:collections", version.ref = "tuulbox" }
tuulbox-coroutines = { module = "com.juul.tuulbox:coroutines", version.ref = "tuulbox" }
wrappers-bom = { module = "org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom", version = "2025.1.6" }
Expand Down
35 changes: 6 additions & 29 deletions kable-core/api/android/kable-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -114,39 +114,16 @@ public abstract interface class com/juul/kable/Descriptor {
public abstract fun getServiceUuid ()Lkotlin/uuid/Uuid;
}

public final class com/juul/kable/DiscoveredCharacteristic : com/juul/kable/Characteristic {
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 ()Lkotlin/uuid/Uuid;
public final fun getDescriptors ()Ljava/util/List;
public final fun getInstanceId ()I
public final fun getProperties-bty6q6U ()I
public fun getServiceUuid ()Lkotlin/uuid/Uuid;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public abstract interface class com/juul/kable/DiscoveredCharacteristic : com/juul/kable/Characteristic {
public abstract fun getDescriptors ()Ljava/util/List;
public abstract fun getProperties-bty6q6U ()I
}

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 ()Lkotlin/uuid/Uuid;
public fun getDescriptorUuid ()Lkotlin/uuid/Uuid;
public fun getServiceUuid ()Lkotlin/uuid/Uuid;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public abstract interface class com/juul/kable/DiscoveredDescriptor : com/juul/kable/Descriptor {
}

public final class com/juul/kable/DiscoveredService : com/juul/kable/Service {
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 ()Lkotlin/uuid/Uuid;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public abstract interface class com/juul/kable/DiscoveredService : com/juul/kable/Service {
public abstract fun getCharacteristics ()Ljava/util/List;
}

public abstract interface annotation class com/juul/kable/ExperimentalApi : java/lang/annotation/Annotation {
Expand Down
21 changes: 6 additions & 15 deletions kable-core/api/jvm/kable-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,16 @@ public abstract interface class com/juul/kable/Descriptor {
public abstract fun getServiceUuid ()Lkotlin/uuid/Uuid;
}

public final class com/juul/kable/DiscoveredCharacteristic : com/juul/kable/Characteristic {
public fun <init> ()V
public fun getCharacteristicUuid ()Lkotlin/uuid/Uuid;
public final fun getDescriptors ()Ljava/util/List;
public final fun getProperties-bty6q6U ()I
public fun getServiceUuid ()Lkotlin/uuid/Uuid;
public abstract interface class com/juul/kable/DiscoveredCharacteristic : com/juul/kable/Characteristic {
public abstract fun getDescriptors ()Ljava/util/List;
public abstract fun getProperties-bty6q6U ()I
}

public final class com/juul/kable/DiscoveredDescriptor : com/juul/kable/Descriptor {
public fun <init> ()V
public fun getCharacteristicUuid ()Lkotlin/uuid/Uuid;
public fun getDescriptorUuid ()Lkotlin/uuid/Uuid;
public fun getServiceUuid ()Lkotlin/uuid/Uuid;
public abstract interface class com/juul/kable/DiscoveredDescriptor : com/juul/kable/Descriptor {
}

public final class com/juul/kable/DiscoveredService : com/juul/kable/Service {
public fun <init> ()V
public final fun getCharacteristics ()Ljava/util/List;
public fun getServiceUuid ()Lkotlin/uuid/Uuid;
public abstract interface class com/juul/kable/DiscoveredService : com/juul/kable/Service {
public abstract fun getCharacteristics ()Ljava/util/List;
}

public abstract interface annotation class com/juul/kable/ExperimentalApi : java/lang/annotation/Annotation {
Expand Down
5 changes: 5 additions & 0 deletions kable-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ kotlin {
implementation(libs.tuulbox.coroutines)
}

androidUnitTest.dependencies {
implementation(libs.equalsverifier)
implementation(libs.mockk)
}

jsMain.dependencies {
api(libs.wrappers.web)
api(project.dependencies.platform(libs.wrappers.bom))
Expand Down
2 changes: 1 addition & 1 deletion kable-core/src/androidMain/kotlin/BluetoothDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ internal fun BluetoothDevice.connect(
transport: Transport,
phy: Phy,
state: MutableStateFlow<State>,
services: MutableStateFlow<List<DiscoveredService>?>,
services: MutableStateFlow<List<PlatformDiscoveredService>?>,
mtu: MutableStateFlow<Int?>,
onCharacteristicChanged: MutableSharedFlow<ObservationEvent<ByteArray>>,
logging: Logging,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ internal class BluetoothDeviceAndroidPeripheral(
private val _state = MutableStateFlow<State>(Disconnected())
override val state = _state.asStateFlow()

private val _services = MutableStateFlow<List<DiscoveredService>?>(null)
private val _services = MutableStateFlow<List<PlatformDiscoveredService>?>(null)
override val services = _services.asStateFlow()
private fun servicesOrThrow() = services.value ?: error("Services have not been discovered")

Expand Down
4 changes: 2 additions & 2 deletions kable-core/src/androidMain/kotlin/Connection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ internal class Connection(
internal val gatt: BluetoothGatt,
private val threading: Threading,
private val callback: Callback,
private val services: MutableStateFlow<List<DiscoveredService>?>,
private val services: MutableStateFlow<List<PlatformDiscoveredService>?>,
private val disconnectTimeout: Duration,
logging: Logging,
) {
Expand Down Expand Up @@ -107,7 +107,7 @@ internal class Connection(
repeat(retries) { attempt ->
val discoveredServices = execute<OnServicesDiscovered> {
discoverServicesOrThrow()
}.services.map(::DiscoveredService)
}.services.map(::PlatformDiscoveredService)

if (discoveredServices.isEmpty()) {
logger.warn {
Expand Down
112 changes: 84 additions & 28 deletions kable-core/src/androidMain/kotlin/Profile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothGattService
import com.juul.kable.Characteristic.Properties
import kotlin.uuid.Uuid
import kotlin.uuid.toKotlinUuid

@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316
Expand All @@ -18,40 +17,97 @@ 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 {
internal actual class PlatformDiscoveredService(
actual val service: PlatformService,
) : DiscoveredService {

public actual val characteristics: List<DiscoveredCharacteristic> =
service.characteristics.map(::DiscoveredCharacteristic)
actual override val characteristics = service.characteristics.map(::PlatformDiscoveredCharacteristic)
actual override val serviceUuid get() = service.uuid.toKotlinUuid()
val instanceId get() = service.instanceId

actual override val serviceUuid: Uuid get() = service.uuid.toKotlinUuid()
val instanceId: Int get() = service.instanceId
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PlatformDiscoveredService
if (serviceUuid != other.serviceUuid) return false
if (instanceId != other.instanceId) return false
return true
}

override fun hashCode(): Int {
var result = serviceUuid.hashCode()
result = 31 * result + instanceId
return result
}

override fun toString() = "DiscoveredService(serviceUuid=$serviceUuid, instanceId=$instanceId)"
}

public actual data class DiscoveredCharacteristic internal constructor(
internal actual val characteristic: PlatformCharacteristic,
) : Characteristic {
internal actual class PlatformDiscoveredCharacteristic internal constructor(
actual val characteristic: PlatformCharacteristic,
) : DiscoveredCharacteristic {

public actual val descriptors: List<DiscoveredDescriptor> =
characteristic.descriptors.map(::DiscoveredDescriptor)
actual override val descriptors = characteristic.descriptors.map(::PlatformDiscoveredDescriptor)
actual override val serviceUuid get() = characteristic.service.uuid.toKotlinUuid()
val serviceInstanceId get() = characteristic.service.instanceId
actual override val characteristicUuid get() = characteristic.uuid.toKotlinUuid()
val instanceId get() = characteristic.instanceId
actual override val properties get() = Properties(characteristic.properties)

actual override val serviceUuid: Uuid get() = characteristic.service.uuid.toKotlinUuid()
actual override val characteristicUuid: Uuid get() = characteristic.uuid.toKotlinUuid()
val instanceId: Int get() = characteristic.instanceId
public actual val properties: Properties get() = Properties(characteristic.properties)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PlatformDiscoveredCharacteristic
if (serviceUuid != other.serviceUuid) return false
if (serviceInstanceId != other.serviceInstanceId) return false
if (characteristicUuid != other.characteristicUuid) return false
if (instanceId != other.instanceId) return false
return true
}

public actual data class DiscoveredDescriptor internal constructor(
internal actual val descriptor: PlatformDescriptor,
) : Descriptor {
override fun hashCode(): Int {
var result = serviceUuid.hashCode()
result = 31 * result + serviceInstanceId
result = 31 * result + characteristicUuid.hashCode()
result = 31 * result + instanceId
return result
}

actual override val serviceUuid: Uuid get() = descriptor.characteristic.service.uuid.toKotlinUuid()
actual override val characteristicUuid: Uuid get() = descriptor.characteristic.uuid.toKotlinUuid()
actual override val descriptorUuid: Uuid get() = descriptor.uuid.toKotlinUuid()
override fun toString() =
"DiscoveredCharacteristic(serviceUuid=$serviceUuid, serviceInstanceId=$serviceInstanceId, characteristicUuid=$characteristicUuid, instanceId=$instanceId)"
}

internal fun PlatformCharacteristic.toLazyCharacteristic() = LazyCharacteristic(
serviceUuid = service.uuid.toKotlinUuid(),
characteristicUuid = uuid.toKotlinUuid(),
)
internal actual class PlatformDiscoveredDescriptor internal constructor(
actual val descriptor: PlatformDescriptor,
) : DiscoveredDescriptor {

actual override val serviceUuid get() = descriptor.characteristic.service.uuid.toKotlinUuid()
val serviceInstanceId get() = descriptor.characteristic.service.instanceId
actual override val characteristicUuid get() = descriptor.characteristic.uuid.toKotlinUuid()
val characteristicInstanceId get() = descriptor.characteristic.instanceId
actual override val descriptorUuid get() = descriptor.uuid.toKotlinUuid()

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PlatformDiscoveredDescriptor
if (serviceUuid != other.serviceUuid) return false
if (serviceInstanceId != other.serviceInstanceId) return false
if (characteristicUuid != other.characteristicUuid) return false
if (characteristicInstanceId != other.characteristicInstanceId) return false
if (descriptorUuid != other.descriptorUuid) return false
return true
}

override fun hashCode(): Int {
var result = serviceUuid.hashCode()
result = 31 * result + serviceInstanceId
result = 31 * result + characteristicUuid.hashCode()
result = 31 * result + characteristicInstanceId
result = 31 * result + descriptorUuid.hashCode()
return result
}

override fun toString() =
"DiscoveredDescriptor(serviceUuid=$serviceUuid, serviceInstanceId=$serviceInstanceId, characteristicUuid=$characteristicUuid, characteristicInstanceId=$characteristicInstanceId, descriptorUuid=$descriptorUuid)"
}
4 changes: 2 additions & 2 deletions kable-core/src/androidMain/kotlin/gatt/Callback.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import android.bluetooth.BluetoothProfile.STATE_DISCONNECTING
import com.juul.kable.NotConnectedException
import com.juul.kable.ObservationEvent
import com.juul.kable.ObservationEvent.CharacteristicChange
import com.juul.kable.PlatformDiscoveredCharacteristic
import com.juul.kable.State
import com.juul.kable.State.Disconnected.Status.Cancelled
import com.juul.kable.State.Disconnected.Status.CentralDisconnected
Expand Down Expand Up @@ -42,7 +43,6 @@ import com.juul.kable.logs.Logging
import com.juul.kable.logs.Logging.DataProcessor.Operation.Change
import com.juul.kable.logs.Logging.DataProcessor.Operation.Read
import com.juul.kable.logs.detail
import com.juul.kable.toLazyCharacteristic
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
import kotlinx.coroutines.channels.SendChannel
Expand Down Expand Up @@ -189,7 +189,7 @@ internal class Callback(
detail(characteristic)
detail(value, Change)
}
val event = CharacteristicChange(characteristic.toLazyCharacteristic(), value)
val event = CharacteristicChange(PlatformDiscoveredCharacteristic(characteristic), value)
onCharacteristicChanged.tryEmitOrLog(event)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.juul.kable

import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothGattService
import io.mockk.every
import io.mockk.mockk
import nl.jqno.equalsverifier.EqualsVerifier
import nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi
import kotlin.test.Test
import kotlin.uuid.toJavaUuid

class ProfileTests {

@Test
fun PlatformDiscoveredService_equals_verified() {
EqualsVerifier
.forClass(PlatformDiscoveredService::class.java)
.withIgnoredFields("characteristics")
.withMocks()
.verify()
}

@Test
fun PlatformDiscoveredCharacteristic_equals_verified() {
EqualsVerifier
.forClass(PlatformDiscoveredCharacteristic::class.java)
.withIgnoredFields("descriptors")
.withMocks()
.verify()
}

@Test
fun PlatformDiscoveredDescriptor_equals_verified() {
EqualsVerifier
.forClass(PlatformDiscoveredDescriptor::class.java)
.withMocks()
.verify()
}
}

private val redService = mockk<BluetoothGattService> {
every { instanceId } returns 1
every { uuid } returns (Bluetooth.BaseUuid + 0x1).toJavaUuid()
}
private val blueService = mockk<BluetoothGattService> {
every { instanceId } returns 2
every { uuid } returns (Bluetooth.BaseUuid + 0x2).toJavaUuid()
}

private val redCharacteristic = mockk<BluetoothGattCharacteristic> {
every { instanceId } returns 3
every { service } returns redService
every { uuid } returns (Bluetooth.BaseUuid + 0x3).toJavaUuid()
}
private val blueCharacteristic = mockk<BluetoothGattCharacteristic> {
every { instanceId } returns 4
every { service } returns redService
every { uuid } returns (Bluetooth.BaseUuid + 0x4).toJavaUuid()
}

private val redDescriptor = mockk<BluetoothGattDescriptor> {
every { characteristic } returns redCharacteristic
every { uuid } returns (Bluetooth.BaseUuid + 0x5).toJavaUuid()
}
private val blueDescriptor = mockk<BluetoothGattDescriptor> {
every { characteristic } returns blueCharacteristic
every { uuid } returns (Bluetooth.BaseUuid + 0x6).toJavaUuid()
}

private fun <T> SingleTypeEqualsVerifierApi<T>.withMocks(): SingleTypeEqualsVerifierApi<T> =
withPrefabValues(BluetoothGattService::class.java, redService, blueService)
.withPrefabValues(BluetoothGattCharacteristic::class.java, redCharacteristic, blueCharacteristic)
.withPrefabValues(BluetoothGattDescriptor::class.java, redDescriptor, blueDescriptor)
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ internal class CBPeripheralCoreBluetoothPeripheral(
private val observers = Observers<NSData>(this, logging, exceptionHandler = observationExceptionHandler)
private val canSendWriteWithoutResponse = MutableStateFlow(cbPeripheral.canSendWriteWithoutResponse)

private val _services = MutableStateFlow<List<DiscoveredService>?>(null)
private val _services = MutableStateFlow<List<PlatformDiscoveredService>?>(null)
override val services = _services.asStateFlow()
private fun servicesOrThrow() = services.value ?: error("Services have not been discovered")

Expand Down
2 changes: 1 addition & 1 deletion kable-core/src/appleMain/kotlin/CentralManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public class CentralManager internal constructor(
peripheral: CBPeripheral,
delegate: PeripheralDelegate,
state: MutableStateFlow<State>,
services: MutableStateFlow<List<DiscoveredService>?>,
services: MutableStateFlow<List<PlatformDiscoveredService>?>,
disconnectTimeout: Duration,
logging: Logging,
options: Map<Any?, *>? = null,
Expand Down
Loading

0 comments on commit 66b6a6f

Please sign in to comment.