From 3f42fae2844239adab946a89cc1b60e80b4b7cf6 Mon Sep 17 00:00:00 2001 From: Sylwester Zielinski Date: Tue, 16 May 2023 10:16:36 +0200 Subject: [PATCH] Add tests to GLS profile --- .../nrftoolbox/ApplicationScopeModule.kt | 16 ++ .../nrftoolbox/NrfToolboxApplication.kt | 6 + .../toolbox/scanner/ScannerDestination.kt | 2 +- profile_gls/build.gradle.kts | 3 + .../no/nordicsemi/android/gls/GlsServer.kt | 240 ++++++++++++++++++ .../gls/main/viewmodel/GLSViewModel.kt | 16 +- .../nordicsemi/android/gls/ExampleUnitTest.kt | 20 ++ settings.gradle.kts | 8 +- 8 files changed, 298 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ApplicationScopeModule.kt create mode 100644 profile_gls/src/debug/java/no/nordicsemi/android/gls/GlsServer.kt create mode 100644 profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ApplicationScopeModule.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ApplicationScopeModule.kt new file mode 100644 index 00000000..0e1e58c8 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ApplicationScopeModule.kt @@ -0,0 +1,16 @@ +package no.nordicsemi.android.nrftoolbox + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob + +@Module +@InstallIn(SingletonComponent::class) +class ApplicationScopeModule { + + @Provides + fun applicationScope() = CoroutineScope(SupervisorJob()) +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt index 05cb85b9..fce2b821 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt @@ -35,6 +35,7 @@ import android.app.Application import dagger.hilt.android.HiltAndroidApp import no.nordicsemi.android.analytics.AppAnalytics import no.nordicsemi.android.analytics.AppOpenEvent +import no.nordicsemi.android.gls.GlsServer import javax.inject.Inject @HiltAndroidApp @@ -43,9 +44,14 @@ class NrfToolboxApplication : Application() { @Inject lateinit var analytics: AppAnalytics + @Inject + lateinit var glsServer: GlsServer + override fun onCreate() { super.onCreate() analytics.logEvent(AppOpenEvent) + + glsServer.start(this) } } diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/ScannerDestination.kt b/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/ScannerDestination.kt index 8e86d360..2373fc08 100644 --- a/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/ScannerDestination.kt +++ b/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/ScannerDestination.kt @@ -21,7 +21,7 @@ val ScannerDestination = defineDestination(ScannerDestinationId) { uuid = arg, onResult = { when (it) { - is DeviceSelected -> navigationViewModel.navigateUpWithResult(ScannerDestinationId, it.device) + is DeviceSelected -> navigationViewModel.navigateUpWithResult(ScannerDestinationId, it.scanResults.device) ScanningCancelled -> navigationViewModel.navigateUp() } } diff --git a/profile_gls/build.gradle.kts b/profile_gls/build.gradle.kts index 254fee01..d9f2fae5 100644 --- a/profile_gls/build.gradle.kts +++ b/profile_gls/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { implementation(libs.nordic.blek.client) implementation(libs.nordic.blek.profile) + implementation(libs.nordic.blek.server) implementation(libs.chart) @@ -63,4 +64,6 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.activity.compose) implementation(libs.androidx.lifecycle.service) + + testImplementation(libs.junit4) } diff --git a/profile_gls/src/debug/java/no/nordicsemi/android/gls/GlsServer.kt b/profile_gls/src/debug/java/no/nordicsemi/android/gls/GlsServer.kt new file mode 100644 index 00000000..8054a9ae --- /dev/null +++ b/profile_gls/src/debug/java/no/nordicsemi/android/gls/GlsServer.kt @@ -0,0 +1,240 @@ +package no.nordicsemi.android.gls + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.gls.main.viewmodel.BATTERY_LEVEL_CHARACTERISTIC_UUID +import no.nordicsemi.android.gls.main.viewmodel.BATTERY_SERVICE_UUID +import no.nordicsemi.android.gls.main.viewmodel.GLS_SERVICE_UUID +import no.nordicsemi.android.gls.main.viewmodel.GLUCOSE_MEASUREMENT_CHARACTERISTIC +import no.nordicsemi.android.gls.main.viewmodel.GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC +import no.nordicsemi.android.gls.main.viewmodel.RACP_CHARACTERISTIC +import no.nordicsemi.android.kotlin.ble.core.data.BleGattPermission +import no.nordicsemi.android.kotlin.ble.core.data.BleGattProperty +import no.nordicsemi.android.kotlin.ble.core.ext.toDisplayString +import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointInputParser +import no.nordicsemi.android.kotlin.ble.server.main.BleGattServer +import no.nordicsemi.android.kotlin.ble.server.main.service.BleGattServerServiceType +import no.nordicsemi.android.kotlin.ble.server.main.service.BleServerGattCharacteristic +import no.nordicsemi.android.kotlin.ble.server.main.service.BleServerGattCharacteristicConfig +import no.nordicsemi.android.kotlin.ble.server.main.service.BleServerGattServiceConfig +import no.nordicsemi.android.kotlin.ble.server.main.service.BluetoothGattServerConnection +import javax.inject.Inject +import javax.inject.Singleton + +private const val STANDARD_DELAY = 1000L + +@SuppressLint("MissingPermission") +@Singleton +class GlsServer @Inject constructor( + private val scope: CoroutineScope +) { + + private val records = listOf( + byteArrayOf( + 0x07, + 0x00, + 0x00, + 0xDC.toByte(), + 0x07, + 0x01, + 0x01, + 0x0C, + 0x1E, + 0x05, + 0x00, + 0x00, + 0x26, + 0xD2.toByte(), + 0x11 + ), + byteArrayOf( + 0x07, + 0x01, + 0x00, + 0xDC.toByte(), + 0x07, + 0x01, + 0x01, + 0x0C, + 0x1E, + 0x08, + 0x00, + 0x00, + 0x3D, + 0xD2.toByte(), + 0x11 + ), + byteArrayOf( + 0x07, + 0x02, + 0x00, + 0xDC.toByte(), + 0x07, + 0x01, + 0x01, + 0x0C, + 0x1E, + 0x0B, + 0x00, + 0x00, + 0x54, + 0xD2.toByte(), + 0x11 + ), + byteArrayOf( + 0x07, + 0x03, + 0x00, + 0xDC.toByte(), + 0x07, + 0x01, + 0x01, + 0x0C, + 0x1E, + 0x0E, + 0x00, + 0x00, + 0x6B, + 0xD2.toByte(), + 0x11 + ), + byteArrayOf( + 0x07, + 0x04, + 0x00, + 0xDC.toByte(), + 0x07, + 0x01, + 0x01, + 0x0C, + 0x1E, + 0x11, + 0x00, + 0x00, + 0x82.toByte(), + 0xD2.toByte(), + 0x11 + ) + ) + + private val racp = byteArrayOf(0x06, 0x00, 0x01, 0x01) + + fun start(context: Context) = scope.launch { + val gmCharacteristic = BleServerGattCharacteristicConfig( + GLUCOSE_MEASUREMENT_CHARACTERISTIC, + listOf(BleGattProperty.PROPERTY_NOTIFY), + listOf() + ) + + val gmContextCharacteristic = BleServerGattCharacteristicConfig( + GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC, + listOf(BleGattProperty.PROPERTY_NOTIFY), + listOf() + ) + + val racpCharacteristic = BleServerGattCharacteristicConfig( + RACP_CHARACTERISTIC, + listOf(BleGattProperty.PROPERTY_INDICATE, BleGattProperty.PROPERTY_WRITE), + listOf(BleGattPermission.PERMISSION_WRITE) + ) + + val serviceConfig = BleServerGattServiceConfig( + GLS_SERVICE_UUID, + BleGattServerServiceType.SERVICE_TYPE_PRIMARY, + listOf(gmCharacteristic, gmContextCharacteristic, racpCharacteristic) + ) + + val batteryLevelCharacteristic = BleServerGattCharacteristicConfig( + BATTERY_LEVEL_CHARACTERISTIC_UUID, + listOf(BleGattProperty.PROPERTY_READ, BleGattProperty.PROPERTY_NOTIFY), + listOf(BleGattPermission.PERMISSION_READ) + ) + + val batteryService = BleServerGattServiceConfig( + BATTERY_SERVICE_UUID, + BleGattServerServiceType.SERVICE_TYPE_PRIMARY, + listOf(batteryLevelCharacteristic) + ) + + val server = BleGattServer.create( + context = context, + config = arrayOf(serviceConfig, batteryService), + mock = true + ) + + launch { + server.connections + .mapNotNull { it.values.firstOrNull() } + .collect { setUpConnection(it) } + } + } + + private fun setUpConnection(connection: BluetoothGattServerConnection) { + startGlsService(connection) + startBatteryService(connection) + } + + private fun startGlsService(connection: BluetoothGattServerConnection) { + val glsService = connection.services.findService(GLS_SERVICE_UUID)!! + val glsCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CHARACTERISTIC)!! + val glsContextCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC)!! + val racpCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!! + + racpCharacteristic.value + .filter { it.isNotEmpty() } + .onEach { + if (it.contentEquals(RecordAccessControlPointInputParser.reportAllStoredRecords().value)) { + sendAll(glsCharacteristic) + racpCharacteristic.setValue(racp) + } else if (it.contentEquals(RecordAccessControlPointInputParser.reportLastStoredRecord().value)) { + sendLast(glsCharacteristic) + racpCharacteristic.setValue(racp) + } else if (it.contentEquals(RecordAccessControlPointInputParser.reportFirstStoredRecord().value)) { + sendFirst(glsCharacteristic) + racpCharacteristic.setValue(racp) + } else { + throw IllegalArgumentException("Unknown value") + } + } + .launchIn(scope) + } + + private fun sendFirst(characteristics: BleServerGattCharacteristic) { + characteristics.setValue(records.first()) + } + + private fun sendLast(characteristics: BleServerGattCharacteristic) { + characteristics.setValue(records.last()) + } + + private fun sendAll(characteristics: BleServerGattCharacteristic) = scope.launch { + records.forEach { + characteristics.setValue(it) + delay(STANDARD_DELAY) + } + } + + private fun startBatteryService(connection: BluetoothGattServerConnection) { + val batteryService = connection.services.findService(BATTERY_SERVICE_UUID)!! + val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!! + + scope.launch { + repeat(100) { + batteryLevelCharacteristic.setValue(byteArrayOf(0x61)) + delay(STANDARD_DELAY) + batteryLevelCharacteristic.setValue(byteArrayOf(0x60)) + delay(STANDARD_DELAY) + batteryLevelCharacteristic.setValue(byteArrayOf(0x5F)) + } + } + } +} \ No newline at end of file diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt index 8a34c0bd..703c4cef 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt @@ -87,13 +87,13 @@ import javax.inject.Inject val GLS_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb") -private val GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb") -private val GM_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb") -private val GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb") -private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb") +val GLUCOSE_MEASUREMENT_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb") +val GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb") +val GLUCOSE_FEATURE_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb") +val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb") -private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") +val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") +val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") @SuppressLint("MissingPermission") @HiltViewModel @@ -202,7 +202,7 @@ internal class GLSViewModel @Inject constructor( private suspend fun configureGatt(services: BleGattServices) { val glsService = services.findService(GLS_SERVICE_UUID)!! - glucoseMeasurementCharacteristic = glsService.findCharacteristic(GM_CHARACTERISTIC)!! + glucoseMeasurementCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CHARACTERISTIC)!! recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!! val batteryService = services.findService(BATTERY_SERVICE_UUID)!! val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!! @@ -219,7 +219,7 @@ internal class GLSViewModel @Inject constructor( .catch { it.printStackTrace() } .launchIn(viewModelScope) - glsService.findCharacteristic(GM_CONTEXT_CHARACTERISTIC)?.getNotifications() + glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC)?.getNotifications() ?.mapNotNull { GlucoseMeasurementContextParser.parse(it) } ?.onEach { _state.value = _state.value.copyWithNewContext(it) } ?.catch { it.printStackTrace() } diff --git a/profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt b/profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt new file mode 100644 index 00000000..885a5d38 --- /dev/null +++ b/profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt @@ -0,0 +1,20 @@ +package no.nordicsemi.android.gls + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + + + + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index fd80b33e..b43ae10f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -50,7 +50,7 @@ dependencyResolutionManagement { } versionCatalogs { create("libs") { - from("no.nordicsemi.android.gradle:version-catalog:1.5.1") + from("no.nordicsemi.android.gradle:version-catalog:1.5.2") } } } @@ -75,9 +75,9 @@ include(":lib_service") include(":lib_ui") include(":lib_utils") -//if (file("../Android-Common-Libraries").exists()) { -// includeBuild("../Android-Common-Libraries") -//} +if (file("../Android-Common-Libraries").exists()) { + includeBuild("../Android-Common-Libraries") +} if (file("../Kotlin-BLE-Library").exists()) { includeBuild("../Kotlin-BLE-Library")