diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8511eb515..22f5bb92b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -246,6 +246,7 @@ dependencies { implementation(libs.kermit) implementation(libs.nordic.client.android) + implementation(libs.nordic.environment.android) debugImplementation(libs.androidx.compose.ui.test.manifest) @@ -264,9 +265,9 @@ dependencies { testImplementation(libs.mockk) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.nordic.client.android.mock) - testImplementation(libs.nordic.client.mock) + testImplementation(libs.nordic.client.core.mock) testImplementation(libs.nordic.core.mock) - testImplementation(libs.nordic.core.android.mock) + testImplementation(libs.nordic.environment.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 f41869beb..65d9f058f 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 Meshtastic LLC + * Copyright (c) 2025-2026 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,7 +14,6 @@ * 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 @@ -47,9 +46,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 @@ -95,7 +94,6 @@ 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 @@ -106,7 +104,7 @@ constructor( scanJob = processLifecycle.coroutineScope.launch(dispatchers.default) { centralManager - .scan(5.seconds) { ServiceUuid(BTM_SERVICE_UUID.toKotlinUuid()) } + .scan(5.seconds) { ServiceUuid(BTM_SERVICE_UUID) } .distinctByPeripheral() .map { it.peripheral } .onStart { _isScanning.value = true } @@ -146,7 +144,6 @@ constructor( refreshState() } - @OptIn(ExperimentalUuidApi::class) internal suspend fun updateBluetoothState() { val hasPerms = application.hasBluetoothPermission() val enabled = centralManager.state.value == Manager.State.POWERED_ON @@ -170,11 +167,9 @@ 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.toKotlinUuid())).value?.isNotEmpty() ?: false + val hasRequiredService = peripheral.services(listOf(BTM_SERVICE_UUID)).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 2c1e5e569..16a42ea73 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 Meshtastic LLC + * Copyright (c) 2025-2026 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,7 +14,6 @@ * 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 @@ -28,6 +27,7 @@ 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,8 +35,13 @@ import javax.inject.Singleton object BluetoothRepositoryModule { @Provides @Singleton - fun provideCentralManager(@ApplicationContext context: Context, coroutineScope: CoroutineScope): CentralManager = - CentralManager.native(context, coroutineScope) + fun provideAndroidEnvironment(@ApplicationContext context: Context): NativeAndroidEnvironment = + NativeAndroidEnvironment.getInstance(context, isNeverForLocationFlagSet = true) + + @Provides + @Singleton + fun provideCentralManager(environment: NativeAndroidEnvironment, coroutineScope: CoroutineScope): CentralManager = + CentralManager.native(environment, 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 af35efae7..2e7c25f1f 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,7 +17,6 @@ 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 @@ -26,6 +25,7 @@ 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 d7b9a71e2..eb191c2ae 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,9 +52,8 @@ 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.toKotlinUuid +import kotlin.uuid.Uuid /** * A [IRadioInterface] implementation for BLE devices using Nordic Kotlin BLE Library. @@ -67,6 +66,7 @@ import kotlin.uuid.toKotlinUuid * @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,23 +251,22 @@ constructor( } @Suppress("TooGenericExceptionCaught") - @OptIn(ExperimentalUuidApi::class) private fun discoverServicesAndSetupCharacteristics(peripheral: Peripheral) { connectionScope.launch { peripheral - .services(listOf(BTM_SERVICE_UUID.toKotlinUuid())) + .services(listOf(BTM_SERVICE_UUID)) .onEach { services -> - val meshtasticService = services?.find { it.uuid == BTM_SERVICE_UUID.toKotlinUuid() } + val meshtasticService = services?.find { it.uuid == BTM_SERVICE_UUID } if (meshtasticService != null) { toRadioCharacteristic = - meshtasticService.characteristics.find { it.uuid == BTM_TORADIO_CHARACTER.toKotlinUuid() } + meshtasticService.characteristics.find { it.uuid == BTM_TORADIO_CHARACTER } fromNumCharacteristic = - meshtasticService.characteristics.find { it.uuid == BTM_FROMNUM_CHARACTER.toKotlinUuid() } + meshtasticService.characteristics.find { it.uuid == BTM_FROMNUM_CHARACTER } fromRadioCharacteristic = - meshtasticService.characteristics.find { it.uuid == BTM_FROMRADIO_CHARACTER.toKotlinUuid() } + meshtasticService.characteristics.find { it.uuid == BTM_FROMRADIO_CHARACTER } logRadioCharacteristic = - meshtasticService.characteristics.find { it.uuid == BTM_LOGRADIO_CHARACTER.toKotlinUuid() } + meshtasticService.characteristics.find { it.uuid == BTM_LOGRADIO_CHARACTER } if ( listOf(toRadioCharacteristic, fromNumCharacteristic, fromRadioCharacteristic).all { @@ -312,7 +311,6 @@ constructor( // --- Notification Setup --- - @OptIn(ExperimentalUuidApi::class) private suspend fun setupNotifications() { retryCall { fromNumCharacteristic?.subscribe() } ?.onStart { Logger.d { "[$address] Subscribing to fromNumCharacteristic" } } @@ -451,11 +449,12 @@ constructor( } } +@OptIn(ExperimentalUuidApi::class) object BleConstants { const val BLE_NAME_PATTERN = "^.*_([0-9a-fA-F]{4})$" - 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") + 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") } 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 1d2778ed7..a575b757d 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,10 +37,8 @@ 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 { @@ -48,8 +46,6 @@ 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) { @@ -95,9 +91,9 @@ class NordicBleInterfaceDrainTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { + Service(uuid = BleConstants.BTM_SERVICE_UUID) { Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_TORADIO_CHARACTER, properties = setOf( CharacteristicProperty.WRITE, @@ -107,18 +103,18 @@ class NordicBleInterfaceDrainTest { ) fromNumHandle = Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMNUM_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) fromRadioHandle = Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMRADIO_CHARACTER, properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_LOGRADIO_CHARACTER, 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 9d23cb2db..02ea3914c 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,10 +38,8 @@ 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 { @@ -49,8 +47,6 @@ 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( @@ -106,10 +102,10 @@ class NordicBleInterfaceRetryTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { + Service(uuid = BleConstants.BTM_SERVICE_UUID) { toRadioHandle = Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_TORADIO_CHARACTER, properties = setOf( CharacteristicProperty.WRITE, @@ -118,17 +114,17 @@ class NordicBleInterfaceRetryTest { permission = Permission.WRITE, ) Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMNUM_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMRADIO_CHARACTER, properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_LOGRADIO_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) @@ -211,10 +207,10 @@ class NordicBleInterfaceRetryTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { + Service(uuid = BleConstants.BTM_SERVICE_UUID) { toRadioHandle = Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_TORADIO_CHARACTER, properties = setOf( CharacteristicProperty.WRITE, @@ -223,17 +219,17 @@ class NordicBleInterfaceRetryTest { permission = Permission.WRITE, ) Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMNUM_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMRADIO_CHARACTER, properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_LOGRADIO_CHARACTER, 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 d5c5344b7..0f011e97b 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,10 +39,8 @@ 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 { @@ -50,8 +48,6 @@ 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( @@ -107,28 +103,28 @@ class NordicBleInterfaceTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { + Service(uuid = BleConstants.BTM_SERVICE_UUID) { Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_TORADIO_CHARACTER, properties = CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, permission = Permission.WRITE, ) fromNumHandle = Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMNUM_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) fromRadioHandle = Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMRADIO_CHARACTER, properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) logRadioHandle = Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_LOGRADIO_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) @@ -240,10 +236,10 @@ class NordicBleInterfaceTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { + Service(uuid = BleConstants.BTM_SERVICE_UUID) { toRadioHandle = Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_TORADIO_CHARACTER, properties = setOf( CharacteristicProperty.WRITE, @@ -257,17 +253,17 @@ class NordicBleInterfaceTest { } // Add other required chars to avoid discovery failure Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMNUM_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMRADIO_CHARACTER, properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_LOGRADIO_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) @@ -338,9 +334,9 @@ class NordicBleInterfaceTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { + Service(uuid = BleConstants.BTM_SERVICE_UUID) { Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_TORADIO_CHARACTER, properties = setOf( CharacteristicProperty.WRITE, @@ -349,17 +345,17 @@ class NordicBleInterfaceTest { permission = Permission.WRITE, ) Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMNUM_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMRADIO_CHARACTER, properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_LOGRADIO_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) @@ -429,27 +425,27 @@ class NordicBleInterfaceTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { + Service(uuid = BleConstants.BTM_SERVICE_UUID) { // OMIT toRadio characteristic to force failure /* Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_TORADIO_CHARACTER, properties = setOf(CharacteristicProperty.WRITE, CharacteristicProperty.WRITE_WITHOUT_RESPONSE), permission = Permission.WRITE ) */ Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMNUM_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMRADIO_CHARACTER, properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_LOGRADIO_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) @@ -513,9 +509,9 @@ class NordicBleInterfaceTest { isBonded = true, eventHandler = eventHandler, cachedServices = { - Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) { + Service(uuid = BleConstants.BTM_SERVICE_UUID) { Characteristic( - uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_TORADIO_CHARACTER, properties = setOf( CharacteristicProperty.WRITE, @@ -524,17 +520,17 @@ class NordicBleInterfaceTest { permission = Permission.WRITE, ) Characteristic( - uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMNUM_CHARACTER, properties = setOf(CharacteristicProperty.NOTIFY), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_FROMRADIO_CHARACTER, properties = setOf(CharacteristicProperty.READ), permission = Permission.READ, ) Characteristic( - uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(), + uuid = BleConstants.BTM_LOGRADIO_CHARACTER, 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 e0cf40cf0..7d4dbfe92 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,7 +106,8 @@ private inline fun Project.configureKotlin() { // Enable experimental coroutines APIs, including Flow "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-Xcontext-parameters", - "-Xannotation-default-target=param-property" + "-Xannotation-default-target=param-property", + "-Xskip-prerelease-check" ) } } diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts index 2650c8375..51c668d31 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.mock) - testImplementation(libs.nordic.core.android.mock) + testImplementation(libs.nordic.client.core.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 db48737b6..1891858fb 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,10 +36,9 @@ 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.toKotlinUuid +import kotlin.uuid.Uuid /** * BLE transport implementation for ESP32 Unified OTA protocol. Uses Nordic Kotlin-BLE-Library for modern coroutine @@ -49,6 +48,7 @@ import kotlin.uuid.toKotlinUuid * - 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,38 +74,33 @@ 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) - val targetAddresses = setOf(address, otaAddress) - Logger.i { "BLE OTA: Will match addresses: $targetAddresses" } + Logger.i { "BLE OTA: Will match addresses: $address, $otaAddress" } repeat(SCAN_RETRY_COUNT) { attempt -> Logger.i { "BLE OTA: Scanning for device (attempt ${attempt + 1}/$SCAN_RETRY_COUNT)..." } - // Scan without service UUID filter - ESP32 OTA bootloader may not advertise the UUID - // Log all devices found during scan for debugging - val foundDevices = mutableSetOf() + // Use the new scan DSL for better efficiency val peripheral = centralManager - .scan(SCAN_TIMEOUT) - .distinctByPeripheral() - .map { it.peripheral } - .onEach { p -> - if (foundDevices.add(p.address)) { - Logger.d { "BLE OTA: Scan found device: ${p.address} (name=${p.name})" } + .scan(SCAN_TIMEOUT) { + Any { + Address(address) + Address(otaAddress) } } - .firstOrNull { it.address in targetAddresses } + .distinctByPeripheral() + .map { it.peripheral } + .onEach { p -> Logger.d { "BLE OTA: Scan found matching device: ${p.address} (name=${p.name})" } } + .firstOrNull() 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) @@ -129,20 +124,18 @@ 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..." } + Logger.i { "BLE OTA: Connecting to $address using Nordic BLE Library v2..." } // Scan for device by address - device must have rebooted into OTA mode val p = scanForOtaDevice() ?: throw OtaProtocolException.ConnectionFailed( - "Device not found at address $address. " + - "Ensure the device has rebooted into OTA mode and is advertising.", + "Device not found. Ensure the device has rebooted into OTA mode and is advertising.", ) peripheral = p @@ -164,33 +157,30 @@ class BleOtaTransport( .launchIn(transportScope) // Wait for connection or failure with timeout - // 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}") + 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}" } + throw OtaProtocolException.Timeout("Timed out connecting to device at address ${p.address}") + } - if (connectionState is ConnectionState.Disconnected) { - Logger.w { "BLE OTA: Failed to connect to ${p.address} (state=$connectionState)" } + if (p.isConnected != true) { + Logger.w { "BLE OTA: Failed to connect to ${p.address} (state=${p.state.value})" } throw OtaProtocolException.ConnectionFailed("Failed to connect to device at address ${p.address}") } Logger.i { "BLE OTA: Connected to ${p.address}, discovering services..." } - // Discover services - val services = p.services(listOf(SERVICE_UUID.toKotlinUuid())).filterNotNull().first() + // Discover services using kotlin.uuid.Uuid + val services = p.services(listOf(SERVICE_UUID)).filterNotNull().first() val meshtasticOtaService = - services.find { it.uuid == SERVICE_UUID.toKotlinUuid() } + services.find { it.uuid == SERVICE_UUID } ?: throw OtaProtocolException.ConnectionFailed("ESP32 OTA service not found") - otaCharacteristic = - meshtasticOtaService.characteristics.find { it.uuid == OTA_CHARACTERISTIC_UUID.toKotlinUuid() } - val txChar = meshtasticOtaService.characteristics.find { it.uuid == TX_CHARACTERISTIC_UUID.toKotlinUuid() } + otaCharacteristic = meshtasticOtaService.characteristics.find { it.uuid == OTA_CHARACTERISTIC_UUID } + val txChar = meshtasticOtaService.characteristics.find { it.uuid == TX_CHARACTERISTIC_UUID } if (otaCharacteristic == null || txChar == null) { throw OtaProtocolException.ConnectionFailed("Required characteristics not found") @@ -345,9 +335,9 @@ class BleOtaTransport( companion object { // Service and Characteristic UUIDs from ESP32 Unified OTA spec - 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") + 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") // 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 9ad49d93e..2f1b6aab1 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,7 +35,6 @@ 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 @@ -46,17 +45,16 @@ class BleOtaTransportErrorTest { private val testDispatcher = StandardTestDispatcher() private val address = "00:11:22:33:44:55" - 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()) + 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") @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 { @@ -86,16 +84,17 @@ class BleOtaTransportErrorTest { CompleteLocalName("ESP32-OTA") } connectable(name = "ESP32-OTA", eventHandler = eventHandler, isBonded = true) { - Service(uuid = serviceUuid.toKotlinUuid()) { - Characteristic( - uuid = otaCharacteristicUuid.toKotlinUuid(), - properties = - CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, - permission = Permission.WRITE, - ) + Service(uuid = serviceUuid) { + otaCharHandle = + Characteristic( + uuid = otaCharacteristicUuid, + properties = + CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, + permission = Permission.WRITE, + ) txCharHandle = Characteristic( - uuid = txCharacteristicUuid.toKotlinUuid(), + uuid = txCharacteristicUuid, property = CharacteristicProperty.NOTIFY, permission = Permission.READ, ) @@ -120,6 +119,7 @@ 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,16 +146,17 @@ class BleOtaTransportErrorTest { CompleteLocalName("ESP32-OTA") } connectable(name = "ESP32-OTA", eventHandler = eventHandler, isBonded = true) { - Service(uuid = serviceUuid.toKotlinUuid()) { - Characteristic( - uuid = otaCharacteristicUuid.toKotlinUuid(), - properties = - CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, - permission = Permission.WRITE, - ) + Service(uuid = serviceUuid) { + otaCharHandle = + Characteristic( + uuid = otaCharacteristicUuid, + properties = + CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, + permission = Permission.WRITE, + ) txCharHandle = Characteristic( - uuid = txCharacteristicUuid.toKotlinUuid(), + uuid = txCharacteristicUuid, property = CharacteristicProperty.NOTIFY, permission = Permission.READ, ) @@ -192,6 +193,7 @@ 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 { @@ -225,16 +227,17 @@ class BleOtaTransportErrorTest { CompleteLocalName("ESP32-OTA") } connectable(name = "ESP32-OTA", eventHandler = eventHandler, isBonded = true) { - Service(uuid = serviceUuid.toKotlinUuid()) { - Characteristic( - uuid = otaCharacteristicUuid.toKotlinUuid(), - properties = - CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, - permission = Permission.WRITE, - ) + Service(uuid = serviceUuid) { + otaCharHandle = + Characteristic( + uuid = otaCharacteristicUuid, + properties = + CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, + permission = Permission.WRITE, + ) txCharHandle = Characteristic( - uuid = txCharacteristicUuid.toKotlinUuid(), + uuid = txCharacteristicUuid, 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 5a6b508ae..3fcf567d7 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,13 +33,8 @@ 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.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") +import kotlin.uuid.Uuid @OptIn(ExperimentalCoroutinesApi::class, ExperimentalUuidApi::class) class BleOtaTransportMtuTest { @@ -57,15 +52,20 @@ 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 SERVICE_UUID.toKotlinUuid() + every { service.uuid } returns serviceUuid every { service.characteristics } returns listOf(otaChar, txChar) - every { otaChar.uuid } returns OTA_CHARACTERISTIC_UUID.toKotlinUuid() - every { txChar.uuid } returns TX_CHARACTERISTIC_UUID.toKotlinUuid() + every { otaChar.uuid } returns otaCharUuid + every { txChar.uuid } returns txCharUuid 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 657cd18c4..42ee344b2 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,23 +37,16 @@ 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( @@ -75,6 +68,10 @@ 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 @@ -117,17 +114,17 @@ class BleOtaTransportNordicMockTest { CompleteLocalName("ESP32-OTA") } connectable(name = "ESP32-OTA", eventHandler = eventHandler) { - Service(uuid = SERVICE_UUID.toKotlinUuid()) { + Service(uuid = serviceUuid) { otaCharHandle = Characteristic( - uuid = OTA_CHARACTERISTIC_UUID.toKotlinUuid(), + uuid = otaCharUuid, properties = CharacteristicProperty.WRITE and CharacteristicProperty.WRITE_WITHOUT_RESPONSE, permission = Permission.WRITE, ) txCharHandle = Characteristic( - uuid = TX_CHARACTERISTIC_UUID.toKotlinUuid(), + uuid = txCharUuid, 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 c5edf8b39..b106e08dd 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,13 +32,8 @@ 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.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") +import kotlin.uuid.Uuid @OptIn(ExperimentalCoroutinesApi::class, ExperimentalUuidApi::class) class BleOtaTransportTest { @@ -56,6 +51,10 @@ 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. @@ -63,12 +62,13 @@ 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 SERVICE_UUID.toKotlinUuid() + every { service.uuid } returns serviceUuid every { service.characteristics } returns listOf(otaChar, txChar) - every { otaChar.uuid } returns OTA_CHARACTERISTIC_UUID.toKotlinUuid() - every { txChar.uuid } returns TX_CHARACTERISTIC_UUID.toKotlinUuid() + every { otaChar.uuid } returns otaCharUuid + every { txChar.uuid } returns txCharUuid coEvery { centralManager.connect(any(), any()) } returns Unit diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 442626d6b..994fb293f 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-alpha12" +nordic-ble = "2.0.0-alpha13" [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.ref = "lifecycle" } +androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version = "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,9 +189,10 @@ 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-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-client-core-mock = { module = "no.nordicsemi.kotlin.ble:client-core-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" }