Add tests to GLS profile

This commit is contained in:
Sylwester Zielinski
2023-05-16 10:16:36 +02:00
parent fffdc0f1e0
commit 3f42fae284
8 changed files with 298 additions and 13 deletions

View File

@@ -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())
}

View File

@@ -35,6 +35,7 @@ import android.app.Application
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import no.nordicsemi.android.analytics.AppAnalytics import no.nordicsemi.android.analytics.AppAnalytics
import no.nordicsemi.android.analytics.AppOpenEvent import no.nordicsemi.android.analytics.AppOpenEvent
import no.nordicsemi.android.gls.GlsServer
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
@@ -43,9 +44,14 @@ class NrfToolboxApplication : Application() {
@Inject @Inject
lateinit var analytics: AppAnalytics lateinit var analytics: AppAnalytics
@Inject
lateinit var glsServer: GlsServer
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
analytics.logEvent(AppOpenEvent) analytics.logEvent(AppOpenEvent)
glsServer.start(this)
} }
} }

View File

@@ -21,7 +21,7 @@ val ScannerDestination = defineDestination(ScannerDestinationId) {
uuid = arg, uuid = arg,
onResult = { onResult = {
when (it) { when (it) {
is DeviceSelected -> navigationViewModel.navigateUpWithResult(ScannerDestinationId, it.device) is DeviceSelected -> navigationViewModel.navigateUpWithResult(ScannerDestinationId, it.scanResults.device)
ScanningCancelled -> navigationViewModel.navigateUp() ScanningCancelled -> navigationViewModel.navigateUp()
} }
} }

View File

@@ -47,6 +47,7 @@ dependencies {
implementation(libs.nordic.blek.client) implementation(libs.nordic.blek.client)
implementation(libs.nordic.blek.profile) implementation(libs.nordic.blek.profile)
implementation(libs.nordic.blek.server)
implementation(libs.chart) implementation(libs.chart)
@@ -63,4 +64,6 @@ dependencies {
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.service) implementation(libs.androidx.lifecycle.service)
testImplementation(libs.junit4)
} }

View File

@@ -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))
}
}
}
}

View File

@@ -87,13 +87,13 @@ import javax.inject.Inject
val GLS_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb") val GLS_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb")
private val GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb") val GLUCOSE_MEASUREMENT_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb")
private val GM_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb") val GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb")
private val GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb") val GLUCOSE_FEATURE_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb")
private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-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") 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_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@HiltViewModel @HiltViewModel
@@ -202,7 +202,7 @@ internal class GLSViewModel @Inject constructor(
private suspend fun configureGatt(services: BleGattServices) { private suspend fun configureGatt(services: BleGattServices) {
val glsService = services.findService(GLS_SERVICE_UUID)!! val glsService = services.findService(GLS_SERVICE_UUID)!!
glucoseMeasurementCharacteristic = glsService.findCharacteristic(GM_CHARACTERISTIC)!! glucoseMeasurementCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CHARACTERISTIC)!!
recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!! recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!!
val batteryService = services.findService(BATTERY_SERVICE_UUID)!! val batteryService = services.findService(BATTERY_SERVICE_UUID)!!
val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!! val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!!
@@ -219,7 +219,7 @@ internal class GLSViewModel @Inject constructor(
.catch { it.printStackTrace() } .catch { it.printStackTrace() }
.launchIn(viewModelScope) .launchIn(viewModelScope)
glsService.findCharacteristic(GM_CONTEXT_CHARACTERISTIC)?.getNotifications() glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC)?.getNotifications()
?.mapNotNull { GlucoseMeasurementContextParser.parse(it) } ?.mapNotNull { GlucoseMeasurementContextParser.parse(it) }
?.onEach { _state.value = _state.value.copyWithNewContext(it) } ?.onEach { _state.value = _state.value.copyWithNewContext(it) }
?.catch { it.printStackTrace() } ?.catch { it.printStackTrace() }

View File

@@ -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)
}
}

View File

@@ -50,7 +50,7 @@ dependencyResolutionManagement {
} }
versionCatalogs { versionCatalogs {
create("libs") { 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_ui")
include(":lib_utils") include(":lib_utils")
//if (file("../Android-Common-Libraries").exists()) { if (file("../Android-Common-Libraries").exists()) {
// includeBuild("../Android-Common-Libraries") includeBuild("../Android-Common-Libraries")
//} }
if (file("../Kotlin-BLE-Library").exists()) { if (file("../Kotlin-BLE-Library").exists()) {
includeBuild("../Kotlin-BLE-Library") includeBuild("../Kotlin-BLE-Library")