diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 22f5bb92b..8511eb515 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -246,7 +246,6 @@ dependencies { implementation(libs.kermit) implementation(libs.nordic.client.android) - implementation(libs.nordic.environment.android) debugImplementation(libs.androidx.compose.ui.test.manifest) @@ -265,9 +264,9 @@ dependencies { testImplementation(libs.mockk) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.nordic.client.android.mock) - testImplementation(libs.nordic.client.core.mock) + testImplementation(libs.nordic.client.mock) testImplementation(libs.nordic.core.mock) - testImplementation(libs.nordic.environment.android.mock) + testImplementation(libs.nordic.core.android.mock) testImplementation(libs.robolectric) testImplementation(libs.androidx.test.core) } diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt index 65d9f058f..f41869beb 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * Copyright (c) 2025 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package com.geeksville.mesh.repository.bluetooth import android.annotation.SuppressLint @@ -46,9 +47,9 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.seconds import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid /** Repository responsible for maintaining and updating the state of Bluetooth availability. */ -@OptIn(ExperimentalUuidApi::class) @Singleton class BluetoothRepository @Inject @@ -94,6 +95,7 @@ constructor( fun isValid(bleAddress: String): Boolean = BluetoothAdapter.checkBluetoothAddress(bleAddress) /** Starts a BLE scan for Meshtastic devices. The results are published to the [scannedDevices] flow. */ + @OptIn(ExperimentalUuidApi::class) @SuppressLint("MissingPermission") fun startScan() { if (isScanning.value) return @@ -104,7 +106,7 @@ constructor( scanJob = processLifecycle.coroutineScope.launch(dispatchers.default) { centralManager - .scan(5.seconds) { ServiceUuid(BTM_SERVICE_UUID) } + .scan(5.seconds) { ServiceUuid(BTM_SERVICE_UUID.toKotlinUuid()) } .distinctByPeripheral() .map { it.peripheral } .onStart { _isScanning.value = true } @@ -144,6 +146,7 @@ constructor( refreshState() } + @OptIn(ExperimentalUuidApi::class) internal suspend fun updateBluetoothState() { val hasPerms = application.hasBluetoothPermission() val enabled = centralManager.state.value == Manager.State.POWERED_ON @@ -167,9 +170,11 @@ constructor( } /** Checks if a peripheral is one of ours, either by its advertised name or by the services it provides. */ + @OptIn(ExperimentalUuidApi::class) private fun isMatchingPeripheral(peripheral: Peripheral): Boolean { val nameMatches = peripheral.name?.matches(Regex(BLE_NAME_PATTERN)) ?: false - val hasRequiredService = peripheral.services(listOf(BTM_SERVICE_UUID)).value?.isNotEmpty() ?: false + val hasRequiredService = + peripheral.services(listOf(BTM_SERVICE_UUID.toKotlinUuid())).value?.isNotEmpty() ?: false return nameMatches || hasRequiredService } diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt index 16a42ea73..2c1e5e569 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * Copyright (c) 2025 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package com.geeksville.mesh.repository.bluetooth import android.content.Context @@ -27,7 +28,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import no.nordicsemi.kotlin.ble.client.android.CentralManager import no.nordicsemi.kotlin.ble.client.android.native -import no.nordicsemi.kotlin.ble.environment.android.NativeAndroidEnvironment import javax.inject.Singleton @Module @@ -35,13 +35,8 @@ import javax.inject.Singleton object BluetoothRepositoryModule { @Provides @Singleton - fun provideAndroidEnvironment(@ApplicationContext context: Context): NativeAndroidEnvironment = - NativeAndroidEnvironment.getInstance(context, isNeverForLocationFlagSet = true) - - @Provides - @Singleton - fun provideCentralManager(environment: NativeAndroidEnvironment, coroutineScope: CoroutineScope): CentralManager = - CentralManager.native(environment, coroutineScope) + fun provideCentralManager(@ApplicationContext context: Context, coroutineScope: CoroutineScope): CentralManager = + CentralManager.native(context, coroutineScope) @Provides @Singleton diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BleError.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BleError.kt index 2e7c25f1f..af35efae7 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/BleError.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BleError.kt @@ -17,6 +17,7 @@ package com.geeksville.mesh.repository.radio import com.geeksville.mesh.service.RadioNotConnectedException +import no.nordicsemi.kotlin.ble.client.exception.BluetoothUnavailableException import no.nordicsemi.kotlin.ble.client.exception.ConnectionFailedException import no.nordicsemi.kotlin.ble.client.exception.InvalidAttributeException import no.nordicsemi.kotlin.ble.client.exception.OperationFailedException @@ -25,7 +26,6 @@ import no.nordicsemi.kotlin.ble.client.exception.ScanningException import no.nordicsemi.kotlin.ble.client.exception.ValueDoesNotMatchException import no.nordicsemi.kotlin.ble.core.ConnectionState import no.nordicsemi.kotlin.ble.core.exception.BluetoothException -import no.nordicsemi.kotlin.ble.core.exception.BluetoothUnavailableException import no.nordicsemi.kotlin.ble.core.exception.GattException import no.nordicsemi.kotlin.ble.core.exception.ManagerClosedException diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt index eb191c2ae..d7b9a71e2 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt @@ -52,8 +52,9 @@ import no.nordicsemi.kotlin.ble.client.exception.InvalidAttributeException import no.nordicsemi.kotlin.ble.core.CharacteristicProperty import no.nordicsemi.kotlin.ble.core.ConnectionState import no.nordicsemi.kotlin.ble.core.WriteType +import java.util.UUID import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid +import kotlin.uuid.toKotlinUuid /** * A [IRadioInterface] implementation for BLE devices using Nordic Kotlin BLE Library. @@ -66,7 +67,6 @@ import kotlin.uuid.Uuid * @param service The [RadioInterfaceService] to use for handling radio events. * @param address The BLE address of the device to connect to. */ -@OptIn(ExperimentalUuidApi::class) @SuppressLint("MissingPermission") class NordicBleInterface @AssistedInject @@ -251,22 +251,23 @@ constructor( } @Suppress("TooGenericExceptionCaught") + @OptIn(ExperimentalUuidApi::class) private fun discoverServicesAndSetupCharacteristics(peripheral: Peripheral) { connectionScope.launch { peripheral - .services(listOf(BTM_SERVICE_UUID)) + .services(listOf(BTM_SERVICE_UUID.toKotlinUuid())) .onEach { services -> - val meshtasticService = services?.find { it.uuid == BTM_SERVICE_UUID } + val meshtasticService = services?.find { it.uuid == BTM_SERVICE_UUID.toKotlinUuid() } if (meshtasticService != null) { toRadioCharacteristic = - meshtasticService.characteristics.find { it.uuid == BTM_TORADIO_CHARACTER } + meshtasticService.characteristics.find { it.uuid == BTM_TORADIO_CHARACTER.toKotlinUuid() } fromNumCharacteristic = - meshtasticService.characteristics.find { it.uuid == BTM_FROMNUM_CHARACTER } + meshtasticService.characteristics.find { it.uuid == BTM_FROMNUM_CHARACTER.toKotlinUuid() } fromRadioCharacteristic = - meshtasticService.characteristics.find { it.uuid == BTM_FROMRADIO_CHARACTER } + meshtasticService.characteristics.find { it.uuid == BTM_FROMRADIO_CHARACTER.toKotlinUuid() } logRadioCharacteristic = - meshtasticService.characteristics.find { it.uuid == BTM_LOGRADIO_CHARACTER } + meshtasticService.characteristics.find { it.uuid == BTM_LOGRADIO_CHARACTER.toKotlinUuid() } if ( listOf(toRadioCharacteristic, fromNumCharacteristic, fromRadioCharacteristic).all { @@ -311,6 +312,7 @@ constructor( // --- Notification Setup --- + @OptIn(ExperimentalUuidApi::class) private suspend fun setupNotifications() { retryCall { fromNumCharacteristic?.subscribe() } ?.onStart { Logger.d { "[$address] Subscribing to fromNumCharacteristic" } } @@ -449,12 +451,11 @@ constructor( } } -@OptIn(ExperimentalUuidApi::class) object BleConstants { const val BLE_NAME_PATTERN = "^.*_([0-9a-fA-F]{4})$" - val BTM_SERVICE_UUID: Uuid = Uuid.parse("6ba1b218-15a8-461f-9fa8-5dcae273eafd") - val BTM_TORADIO_CHARACTER: Uuid = Uuid.parse("f75c76d2-129e-4dad-a1dd-7866124401e7") - val BTM_FROMNUM_CHARACTER: Uuid = Uuid.parse("ed9da18c-a800-4f66-a670-aa7547e34453") - val BTM_FROMRADIO_CHARACTER: Uuid = Uuid.parse("2c55e69e-4993-11ed-b878-0242ac120002") - val BTM_LOGRADIO_CHARACTER: Uuid = Uuid.parse("5a3d6e49-06e6-4423-9944-e9de8cdf9547") + val BTM_SERVICE_UUID: UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd") + val BTM_TORADIO_CHARACTER: UUID = UUID.fromString("f75c76d2-129e-4dad-a1dd-7866124401e7") + val BTM_FROMNUM_CHARACTER: UUID = UUID.fromString("ed9da18c-a800-4f66-a670-aa7547e34453") + val BTM_FROMRADIO_CHARACTER: UUID = UUID.fromString("2c55e69e-4993-11ed-b878-0242ac120002") + val BTM_LOGRADIO_CHARACTER: UUID = UUID.fromString("5a3d6e49-06e6-4423-9944-e9de8cdf9547") } diff --git a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceDrainTest.kt b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceDrainTest.kt index a575b757d..1d2778ed7 100644 --- a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceDrainTest.kt +++ b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceDrainTest.kt @@ -37,8 +37,10 @@ import no.nordicsemi.kotlin.ble.core.LegacyAdvertisingSetParameters import no.nordicsemi.kotlin.ble.core.Permission import org.junit.Ignore import org.junit.Test +import java.util.UUID import kotlin.time.Duration.Companion.milliseconds import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid @OptIn(ExperimentalCoroutinesApi::class, ExperimentalUuidApi::class) class NordicBleInterfaceDrainTest { @@ -46,6 +48,8 @@ class NordicBleInterfaceDrainTest { private val testDispatcher = StandardTestDispatcher() private val address = "00:11:22:33:44:55" + private fun UUID.toKotlinUuid(): Uuid = Uuid.parse(this.toString()) + @Ignore("Flaky: relies on timing in the Nordic BLE mock library which causes intermittent CI failures") @Test fun `drainPacketQueueAndDispatch reads multiple packets until empty`() = runTest(testDispatcher) { @@ -91,9 +95,9 @@ class NordicBleInterfaceDrainTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID) { + Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER, + uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), properties = setOf( CharacteristicProperty.WRITE, @@ -103,18 +107,18 @@ class NordicBleInterfaceDrainTest { ) fromNumHandle = Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER, + uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) fromRadioHandle = Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER, + uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER, + uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) diff --git a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt index 02ea3914c..9d23cb2db 100644 --- a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt +++ b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt @@ -38,8 +38,10 @@ import no.nordicsemi.kotlin.ble.core.LegacyAdvertisingSetParameters import no.nordicsemi.kotlin.ble.core.Permission import org.junit.Before import org.junit.Test +import java.util.UUID import kotlin.time.Duration.Companion.milliseconds import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid @OptIn(ExperimentalCoroutinesApi::class, ExperimentalUuidApi::class) class NordicBleInterfaceRetryTest { @@ -47,6 +49,8 @@ class NordicBleInterfaceRetryTest { private val testDispatcher = UnconfinedTestDispatcher() private val address = "00:11:22:33:44:55" + private fun UUID.toKotlinUuid(): Uuid = Uuid.parse(this.toString()) + @Before fun setup() { Logger.setLogWriters( @@ -102,10 +106,10 @@ class NordicBleInterfaceRetryTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID) { + Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { toRadioHandle = Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER, + uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), properties = setOf( CharacteristicProperty.WRITE, @@ -114,17 +118,17 @@ class NordicBleInterfaceRetryTest { permission = Permission.WRITE, ) Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER, + uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER, + uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER, + uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) @@ -207,10 +211,10 @@ class NordicBleInterfaceRetryTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID) { + Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { toRadioHandle = Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER, + uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), properties = setOf( CharacteristicProperty.WRITE, @@ -219,17 +223,17 @@ class NordicBleInterfaceRetryTest { permission = Permission.WRITE, ) Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER, + uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER, + uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER, + uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) diff --git a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt index 0f011e97b..d5c5344b7 100644 --- a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt +++ b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt @@ -39,8 +39,10 @@ import no.nordicsemi.kotlin.ble.core.Permission import no.nordicsemi.kotlin.ble.core.and import org.junit.Before import org.junit.Test +import java.util.UUID import kotlin.time.Duration.Companion.milliseconds import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid @OptIn(ExperimentalCoroutinesApi::class, ExperimentalUuidApi::class) class NordicBleInterfaceTest { @@ -48,6 +50,8 @@ class NordicBleInterfaceTest { private val testDispatcher = UnconfinedTestDispatcher() private val address = "00:11:22:33:44:55" + private fun UUID.toKotlinUuid(): Uuid = Uuid.parse(this.toString()) + @Before fun setup() { Logger.setLogWriters( @@ -103,28 +107,28 @@ class NordicBleInterfaceTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID) { + Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER, + uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), properties = CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, permission = Permission.WRITE, ) fromNumHandle = Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER, + uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) fromRadioHandle = Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER, + uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) logRadioHandle = Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER, + uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) @@ -236,10 +240,10 @@ class NordicBleInterfaceTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID) { + Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { toRadioHandle = Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER, + uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), properties = setOf( CharacteristicProperty.WRITE, @@ -253,17 +257,17 @@ class NordicBleInterfaceTest { } // Add other required chars to avoid discovery failure Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER, + uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER, + uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER, + uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) @@ -334,9 +338,9 @@ class NordicBleInterfaceTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID) { + Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER, + uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), properties = setOf( CharacteristicProperty.WRITE, @@ -345,17 +349,17 @@ class NordicBleInterfaceTest { permission = Permission.WRITE, ) Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER, + uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER, + uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER, + uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) @@ -425,27 +429,27 @@ class NordicBleInterfaceTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID) { + Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { // OMIT toRadio characteristic to force failure /* Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER, + uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.WRITE, CharacteristicProperty.WRITE_WITHOUT_RESPONSE), permission = Permission.WRITE ) */ Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER, + uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER, + uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER, + uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) @@ -509,9 +513,9 @@ class NordicBleInterfaceTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID) { + Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER, + uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), properties = setOf( CharacteristicProperty.WRITE, @@ -520,17 +524,17 @@ class NordicBleInterfaceTest { permission = Permission.WRITE, ) Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER, + uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER, + uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER, + uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt index 7d4dbfe92..e0cf40cf0 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt @@ -106,8 +106,7 @@ private inline fun Project.configureKotlin() { // Enable experimental coroutines APIs, including Flow "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-Xcontext-parameters", - "-Xannotation-default-target=param-property", - "-Xskip-prerelease-check" + "-Xannotation-default-target=param-property" ) } } diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts index 51c668d31..2650c8375 100644 --- a/feature/firmware/build.gradle.kts +++ b/feature/firmware/build.gradle.kts @@ -78,8 +78,8 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.nordic.client.android.mock) - testImplementation(libs.nordic.client.core.mock) + testImplementation(libs.nordic.client.mock) + testImplementation(libs.nordic.core.android.mock) testImplementation(libs.nordic.core.mock) - testImplementation(libs.nordic.environment.android.mock) testImplementation(libs.mockk) } diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt index 1891858fb..db48737b6 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt @@ -36,9 +36,10 @@ import no.nordicsemi.kotlin.ble.client.android.Peripheral import no.nordicsemi.kotlin.ble.client.distinctByPeripheral import no.nordicsemi.kotlin.ble.core.ConnectionState import no.nordicsemi.kotlin.ble.core.WriteType +import java.util.UUID import kotlin.time.Duration.Companion.seconds import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid +import kotlin.uuid.toKotlinUuid /** * BLE transport implementation for ESP32 Unified OTA protocol. Uses Nordic Kotlin-BLE-Library for modern coroutine @@ -48,7 +49,6 @@ import kotlin.uuid.Uuid * - OTA Characteristic (Write): 62ec0272-3ec5-11eb-b378-0242ac130005 * - TX Characteristic (Notify): 62ec0272-3ec5-11eb-b378-0242ac130003 */ -@OptIn(ExperimentalUuidApi::class) class BleOtaTransport( private val centralManager: CentralManager, private val address: String, @@ -74,33 +74,38 @@ class BleOtaTransport( * ESP32 bootloaders may use the original MAC address OR increment the last byte by 1 for OTA mode, so we check both * addresses. */ + @OptIn(ExperimentalUuidApi::class) private suspend fun scanForOtaDevice(): Peripheral? { // ESP32 OTA bootloader may use MAC address with last byte incremented by 1 val otaAddress = calculateOtaAddress(address) - Logger.i { "BLE OTA: Will match addresses: $address, $otaAddress" } + val targetAddresses = setOf(address, otaAddress) + Logger.i { "BLE OTA: Will match addresses: $targetAddresses" } repeat(SCAN_RETRY_COUNT) { attempt -> Logger.i { "BLE OTA: Scanning for device (attempt ${attempt + 1}/$SCAN_RETRY_COUNT)..." } - // Use the new scan DSL for better efficiency + // Scan without service UUID filter - ESP32 OTA bootloader may not advertise the UUID + // Log all devices found during scan for debugging + val foundDevices = mutableSetOf() val peripheral = centralManager - .scan(SCAN_TIMEOUT) { - Any { - Address(address) - Address(otaAddress) - } - } + .scan(SCAN_TIMEOUT) .distinctByPeripheral() .map { it.peripheral } - .onEach { p -> Logger.d { "BLE OTA: Scan found matching device: ${p.address} (name=${p.name})" } } - .firstOrNull() + .onEach { p -> + if (foundDevices.add(p.address)) { + Logger.d { "BLE OTA: Scan found device: ${p.address} (name=${p.name})" } + } + } + .firstOrNull { it.address in targetAddresses } if (peripheral != null) { Logger.i { "BLE OTA: Found target device at ${peripheral.address}" } return peripheral } + Logger.w { "BLE OTA: Target addresses $targetAddresses not in ${foundDevices.size} devices found" } + if (attempt < SCAN_RETRY_COUNT - 1) { Logger.i { "BLE OTA: Device not found, waiting ${SCAN_RETRY_DELAY_MS}ms before retry..." } kotlinx.coroutines.delay(SCAN_RETRY_DELAY_MS) @@ -124,18 +129,20 @@ class BleOtaTransport( } /** Connect to the device and discover OTA service. */ + @OptIn(ExperimentalUuidApi::class) @Suppress("LongMethod") override suspend fun connect(): Result = runCatching { Logger.i { "BLE OTA: Waiting ${REBOOT_DELAY_MS}ms for device to reboot into OTA mode..." } kotlinx.coroutines.delay(REBOOT_DELAY_MS) - Logger.i { "BLE OTA: Connecting to $address using Nordic BLE Library v2..." } + Logger.i { "BLE OTA: Connecting to $address using Nordic BLE Library..." } // Scan for device by address - device must have rebooted into OTA mode val p = scanForOtaDevice() ?: throw OtaProtocolException.ConnectionFailed( - "Device not found. Ensure the device has rebooted into OTA mode and is advertising.", + "Device not found at address $address. " + + "Ensure the device has rebooted into OTA mode and is advertising.", ) peripheral = p @@ -157,30 +164,33 @@ class BleOtaTransport( .launchIn(transportScope) // Wait for connection or failure with timeout - try { - withTimeout(CONNECTION_TIMEOUT_MS) { - p.state.first { it is ConnectionState.Connected || it is ConnectionState.Disconnected } + // Don't use drop(1) - we might already be connected by the time we start collecting + val connectionState = + try { + withTimeout(CONNECTION_TIMEOUT_MS) { + p.state.first { it is ConnectionState.Connected || it is ConnectionState.Disconnected } + } + } catch (@Suppress("SwallowedException") e: kotlinx.coroutines.TimeoutCancellationException) { + Logger.w { "BLE OTA: Timed out waiting to connect to ${p.address}. Error: ${e.message}" } + throw OtaProtocolException.Timeout("Timed out connecting to device at address ${p.address}") } - } catch (@Suppress("SwallowedException") e: kotlinx.coroutines.TimeoutCancellationException) { - Logger.w { "BLE OTA: Timed out waiting to connect to ${p.address}" } - throw OtaProtocolException.Timeout("Timed out connecting to device at address ${p.address}") - } - if (p.isConnected != true) { - Logger.w { "BLE OTA: Failed to connect to ${p.address} (state=${p.state.value})" } + if (connectionState is ConnectionState.Disconnected) { + Logger.w { "BLE OTA: Failed to connect to ${p.address} (state=$connectionState)" } throw OtaProtocolException.ConnectionFailed("Failed to connect to device at address ${p.address}") } Logger.i { "BLE OTA: Connected to ${p.address}, discovering services..." } - // Discover services using kotlin.uuid.Uuid - val services = p.services(listOf(SERVICE_UUID)).filterNotNull().first() + // Discover services + val services = p.services(listOf(SERVICE_UUID.toKotlinUuid())).filterNotNull().first() val meshtasticOtaService = - services.find { it.uuid == SERVICE_UUID } + services.find { it.uuid == SERVICE_UUID.toKotlinUuid() } ?: throw OtaProtocolException.ConnectionFailed("ESP32 OTA service not found") - otaCharacteristic = meshtasticOtaService.characteristics.find { it.uuid == OTA_CHARACTERISTIC_UUID } - val txChar = meshtasticOtaService.characteristics.find { it.uuid == TX_CHARACTERISTIC_UUID } + otaCharacteristic = + meshtasticOtaService.characteristics.find { it.uuid == OTA_CHARACTERISTIC_UUID.toKotlinUuid() } + val txChar = meshtasticOtaService.characteristics.find { it.uuid == TX_CHARACTERISTIC_UUID.toKotlinUuid() } if (otaCharacteristic == null || txChar == null) { throw OtaProtocolException.ConnectionFailed("Required characteristics not found") @@ -335,9 +345,9 @@ class BleOtaTransport( companion object { // Service and Characteristic UUIDs from ESP32 Unified OTA spec - private val SERVICE_UUID = Uuid.parse("4FAFC201-1FB5-459E-8FCC-C5C9C331914B") - private val OTA_CHARACTERISTIC_UUID = Uuid.parse("62ec0272-3ec5-11eb-b378-0242ac130005") - private val TX_CHARACTERISTIC_UUID = Uuid.parse("62ec0272-3ec5-11eb-b378-0242ac130003") + private val SERVICE_UUID = UUID.fromString("4FAFC201-1FB5-459E-8FCC-C5C9C331914B") + private val OTA_CHARACTERISTIC_UUID = UUID.fromString("62ec0272-3ec5-11eb-b378-0242ac130005") + private val TX_CHARACTERISTIC_UUID = UUID.fromString("62ec0272-3ec5-11eb-b378-0242ac130003") // Timeouts and retries private val SCAN_TIMEOUT = 10.seconds diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt index 2f1b6aab1..9ad49d93e 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt @@ -35,6 +35,7 @@ import no.nordicsemi.kotlin.ble.core.Permission import no.nordicsemi.kotlin.ble.core.and import org.junit.Assert.assertTrue import org.junit.Test +import java.util.UUID import kotlin.time.Duration.Companion.milliseconds import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -45,16 +46,17 @@ class BleOtaTransportErrorTest { private val testDispatcher = StandardTestDispatcher() private val address = "00:11:22:33:44:55" - private val serviceUuid = Uuid.parse("4FAFC201-1FB5-459E-8FCC-C5C9C331914B") - private val otaCharacteristicUuid = Uuid.parse("62ec0272-3ec5-11eb-b378-0242ac130005") - private val txCharacteristicUuid = Uuid.parse("62ec0272-3ec5-11eb-b378-0242ac130003") + private val serviceUuid = UUID.fromString("4FAFC201-1FB5-459E-8FCC-C5C9C331914B") + private val otaCharacteristicUuid = UUID.fromString("62ec0272-3ec5-11eb-b378-0242ac130005") + private val txCharacteristicUuid = UUID.fromString("62ec0272-3ec5-11eb-b378-0242ac130003") + + private fun UUID.toKotlinUuid(): Uuid = Uuid.parse(this.toString()) @Test fun `startOta fails when device rejects hash`() = runTest(testDispatcher) { val centralManager = CentralManager.Factory.mock(scope = backgroundScope) lateinit var otaPeripheral: PeripheralSpec var txCharHandle: Int = -1 - var otaCharHandle: Int = -1 val eventHandler = object : PeripheralSpecEventHandler { @@ -84,17 +86,16 @@ class BleOtaTransportErrorTest { CompleteLocalName("ESP32-OTA") } connectable(name = "ESP32-OTA", eventHandler = eventHandler, isBonded = true) { - Service(uuid = serviceUuid) { - otaCharHandle = - Characteristic( - uuid = otaCharacteristicUuid, - properties = - CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, - permission = Permission.WRITE, - ) + Service(uuid = serviceUuid.toKotlinUuid()) { + Characteristic( + uuid = otaCharacteristicUuid.toKotlinUuid(), + properties = + CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, + permission = Permission.WRITE, + ) txCharHandle = Characteristic( - uuid = txCharacteristicUuid, + uuid = txCharacteristicUuid.toKotlinUuid(), property = CharacteristicProperty.NOTIFY, permission = Permission.READ, ) @@ -119,7 +120,6 @@ class BleOtaTransportErrorTest { val centralManager = CentralManager.Factory.mock(scope = backgroundScope) lateinit var otaPeripheral: PeripheralSpec var txCharHandle: Int = -1 - var otaCharHandle: Int = -1 val eventHandler = object : PeripheralSpecEventHandler { @@ -146,17 +146,16 @@ class BleOtaTransportErrorTest { CompleteLocalName("ESP32-OTA") } connectable(name = "ESP32-OTA", eventHandler = eventHandler, isBonded = true) { - Service(uuid = serviceUuid) { - otaCharHandle = - Characteristic( - uuid = otaCharacteristicUuid, - properties = - CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, - permission = Permission.WRITE, - ) + Service(uuid = serviceUuid.toKotlinUuid()) { + Characteristic( + uuid = otaCharacteristicUuid.toKotlinUuid(), + properties = + CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, + permission = Permission.WRITE, + ) txCharHandle = Characteristic( - uuid = txCharacteristicUuid, + uuid = txCharacteristicUuid.toKotlinUuid(), property = CharacteristicProperty.NOTIFY, permission = Permission.READ, ) @@ -193,7 +192,6 @@ class BleOtaTransportErrorTest { val centralManager = CentralManager.Factory.mock(scope = backgroundScope) lateinit var otaPeripheral: PeripheralSpec var txCharHandle: Int = -1 - var otaCharHandle: Int = -1 val eventHandler = object : PeripheralSpecEventHandler { @@ -227,17 +225,16 @@ class BleOtaTransportErrorTest { CompleteLocalName("ESP32-OTA") } connectable(name = "ESP32-OTA", eventHandler = eventHandler, isBonded = true) { - Service(uuid = serviceUuid) { - otaCharHandle = - Characteristic( - uuid = otaCharacteristicUuid, - properties = - CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, - permission = Permission.WRITE, - ) + Service(uuid = serviceUuid.toKotlinUuid()) { + Characteristic( + uuid = otaCharacteristicUuid.toKotlinUuid(), + properties = + CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, + permission = Permission.WRITE, + ) txCharHandle = Characteristic( - uuid = txCharacteristicUuid, + uuid = txCharacteristicUuid.toKotlinUuid(), property = CharacteristicProperty.NOTIFY, permission = Permission.READ, ) diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt index 3fcf567d7..5a6b508ae 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt @@ -33,8 +33,13 @@ import no.nordicsemi.kotlin.ble.client.android.Peripheral import no.nordicsemi.kotlin.ble.client.android.ScanResult import no.nordicsemi.kotlin.ble.core.ConnectionState import org.junit.Test +import java.util.UUID import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid +import kotlin.uuid.toKotlinUuid + +private val SERVICE_UUID = UUID.fromString("4FAFC201-1FB5-459E-8FCC-C5C9C331914B") +private val OTA_CHARACTERISTIC_UUID = UUID.fromString("62ec0272-3ec5-11eb-b378-0242ac130005") +private val TX_CHARACTERISTIC_UUID = UUID.fromString("62ec0272-3ec5-11eb-b378-0242ac130003") @OptIn(ExperimentalCoroutinesApi::class, ExperimentalUuidApi::class) class BleOtaTransportMtuTest { @@ -52,20 +57,15 @@ class BleOtaTransportMtuTest { val service: RemoteService = mockk(relaxed = true) val scanResult: ScanResult = mockk(relaxed = true) - val serviceUuid = Uuid.parse("4FAFC201-1FB5-459E-8FCC-C5C9C331914B") - val otaCharUuid = Uuid.parse("62ec0272-3ec5-11eb-b378-0242ac130005") - val txCharUuid = Uuid.parse("62ec0272-3ec5-11eb-b378-0242ac130003") - every { scanResult.peripheral } returns peripheral every { centralManager.scan(any(), any()) } returns flowOf(scanResult) every { peripheral.address } returns address every { peripheral.state } returns MutableStateFlow(ConnectionState.Connected) - every { peripheral.isConnected } returns true coEvery { peripheral.services(any()) } returns MutableStateFlow(listOf(service)) - every { service.uuid } returns serviceUuid + every { service.uuid } returns SERVICE_UUID.toKotlinUuid() every { service.characteristics } returns listOf(otaChar, txChar) - every { otaChar.uuid } returns otaCharUuid - every { txChar.uuid } returns txCharUuid + every { otaChar.uuid } returns OTA_CHARACTERISTIC_UUID.toKotlinUuid() + every { txChar.uuid } returns TX_CHARACTERISTIC_UUID.toKotlinUuid() coEvery { centralManager.connect(any(), any()) } returns Unit every { txChar.subscribe() } returns MutableSharedFlow() diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt index 42ee344b2..657cd18c4 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt @@ -37,16 +37,23 @@ import no.nordicsemi.kotlin.ble.core.and import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test +import java.util.UUID import kotlin.time.Duration.Companion.milliseconds import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +private val SERVICE_UUID = UUID.fromString("4FAFC201-1FB5-459E-8FCC-C5C9C331914B") +private val OTA_CHARACTERISTIC_UUID = UUID.fromString("62ec0272-3ec5-11eb-b378-0242ac130005") +private val TX_CHARACTERISTIC_UUID = UUID.fromString("62ec0272-3ec5-11eb-b378-0242ac130003") + @OptIn(ExperimentalCoroutinesApi::class, ExperimentalUuidApi::class) class BleOtaTransportNordicMockTest { private val testDispatcher = kotlinx.coroutines.test.StandardTestDispatcher() private val address = "00:11:22:33:44:55" + private fun java.util.UUID.toKotlinUuid(): Uuid = Uuid.parse(this.toString()) + @Before fun setup() { Logger.setLogWriters( @@ -68,10 +75,6 @@ class BleOtaTransportNordicMockTest { var otaCharHandle: Int = -1 var txCharHandle: Int = -1 - val serviceUuid = Uuid.parse("4FAFC201-1FB5-459E-8FCC-C5C9C331914B") - val otaCharUuid = Uuid.parse("62ec0272-3ec5-11eb-b378-0242ac130005") - val txCharUuid = Uuid.parse("62ec0272-3ec5-11eb-b378-0242ac130003") - // Use a property to hold the peripheral since we need it in the event handler lateinit var otaPeripheral: PeripheralSpec @@ -114,17 +117,17 @@ class BleOtaTransportNordicMockTest { CompleteLocalName("ESP32-OTA") } connectable(name = "ESP32-OTA", eventHandler = eventHandler) { - Service(uuid = serviceUuid) { + Service(uuid = SERVICE_UUID.toKotlinUuid()) { otaCharHandle = Characteristic( - uuid = otaCharUuid, + uuid = OTA_CHARACTERISTIC_UUID.toKotlinUuid(), properties = CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, permission = Permission.WRITE, ) txCharHandle = Characteristic( - uuid = txCharUuid, + uuid = TX_CHARACTERISTIC_UUID.toKotlinUuid(), property = CharacteristicProperty.NOTIFY, permission = Permission.READ, ) diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt index b106e08dd..c5edf8b39 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt @@ -32,8 +32,13 @@ import no.nordicsemi.kotlin.ble.client.android.Peripheral import no.nordicsemi.kotlin.ble.client.android.ScanResult import no.nordicsemi.kotlin.ble.core.ConnectionState import org.junit.Test +import java.util.UUID import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid +import kotlin.uuid.toKotlinUuid + +private val SERVICE_UUID = UUID.fromString("4FAFC201-1FB5-459E-8FCC-C5C9C331914B") +private val OTA_CHARACTERISTIC_UUID = UUID.fromString("62ec0272-3ec5-11eb-b378-0242ac130005") +private val TX_CHARACTERISTIC_UUID = UUID.fromString("62ec0272-3ec5-11eb-b378-0242ac130003") @OptIn(ExperimentalCoroutinesApi::class, ExperimentalUuidApi::class) class BleOtaTransportTest { @@ -51,10 +56,6 @@ class BleOtaTransportTest { val service: RemoteService = mockk(relaxed = true) val scanResult: ScanResult = mockk() - val serviceUuid = Uuid.parse("4FAFC201-1FB5-459E-8FCC-C5C9C331914B") - val otaCharUuid = Uuid.parse("62ec0272-3ec5-11eb-b378-0242ac130005") - val txCharUuid = Uuid.parse("62ec0272-3ec5-11eb-b378-0242ac130003") - every { scanResult.peripheral } returns peripheral // Mock the scan call. It takes a Duration and a lambda. @@ -62,13 +63,12 @@ class BleOtaTransportTest { every { peripheral.address } returns address every { peripheral.state } returns MutableStateFlow(ConnectionState.Connected) - every { peripheral.isConnected } returns true coEvery { peripheral.services(any()) } returns MutableStateFlow(listOf(service)) - every { service.uuid } returns serviceUuid + every { service.uuid } returns SERVICE_UUID.toKotlinUuid() every { service.characteristics } returns listOf(otaChar, txChar) - every { otaChar.uuid } returns otaCharUuid - every { txChar.uuid } returns txCharUuid + every { otaChar.uuid } returns OTA_CHARACTERISTIC_UUID.toKotlinUuid() + every { txChar.uuid } returns TX_CHARACTERISTIC_UUID.toKotlinUuid() coEvery { centralManager.connect(any(), any()) } returns Unit diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 994fb293f..442626d6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,7 +54,7 @@ wire = "6.0.0-alpha02" vico = "3.0.0-beta.3" # Removed gradle-doctor dependency-guard = "0.5.0" -nordic-ble = "2.0.0-alpha13" +nordic-ble = "2.0.0-alpha12" [libraries] @@ -78,7 +78,7 @@ androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = androidx-hilt-lifecycle-viewmodel-compose = { module = "androidx.hilt:hilt-lifecycle-viewmodel-compose", version.ref = "androidxHilt" } androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "androidxHilt" } androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } -androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version = "lifecycle" } +androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } @@ -189,10 +189,9 @@ markdown-renderer-android = { module = "com.mikepenz:multiplatform-markdown-rend material = { module = "com.google.android.material:material", version = "1.13.0" } nordic-client-android = { module = "no.nordicsemi.kotlin.ble:client-android", version.ref = "nordic-ble" } nordic-client-android-mock = { module = "no.nordicsemi.kotlin.ble:client-android-mock", version.ref = "nordic-ble" } -nordic-client-core-mock = { module = "no.nordicsemi.kotlin.ble:client-core-mock", version.ref = "nordic-ble" } +nordic-client-mock = { module = "no.nordicsemi.kotlin.ble:client-mock", version.ref = "nordic-ble" } +nordic-core-android-mock = { module = "no.nordicsemi.kotlin.ble:core-android-mock", version.ref = "nordic-ble" } nordic-core-mock = { module = "no.nordicsemi.kotlin.ble:core-mock", version.ref = "nordic-ble" } -nordic-environment-android = { module = "no.nordicsemi.kotlin.ble:environment-android", version.ref = "nordic-ble" } -nordic-environment-android-mock = { module = "no.nordicsemi.kotlin.ble:environment-android-mock", version.ref = "nordic-ble" } nordic-dfu = { module = "no.nordicsemi.android:dfu", version = "2.11.0" } org-eclipse-paho-client-mqttv3 = { module = "org.eclipse.paho:org.eclipse.paho.client.mqttv3", version = "1.2.5" } osmbonuspack = { module = "com.github.MKergall:osmbonuspack", version = "6.9.0" }