diff --git a/app/build.gradle b/app/build.gradle
index 1bfdfe72..07ea47bb 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -52,6 +52,7 @@ dependencies {
//https://github.com/google/dagger/issues/2123
implementation project(':profile_bps')
implementation project(':profile_csc')
+ implementation project(':profile_cgms')
implementation project(':profile_gls')
implementation project(':profile_hrs')
implementation project(':profile_hts')
@@ -62,6 +63,7 @@ dependencies {
implementation project(':lib_permission')
implementation project(":lib_theme")
implementation project(":lib_utils")
+ implementation project(":lib_service")
implementation libs.nordic.ble.common
diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt
index a67c99ca..d9b16ad7 100644
--- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt
+++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt
@@ -6,6 +6,8 @@ import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -26,10 +28,12 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import no.nordicsemi.android.bps.view.BPSScreen
+import no.nordicsemi.android.cgms.view.CGMScreen
import no.nordicsemi.android.csc.view.CSCScreen
import no.nordicsemi.android.gls.view.GLSScreen
import no.nordicsemi.android.hrs.view.HRSScreen
import no.nordicsemi.android.hts.view.HTSScreen
+import no.nordicsemi.android.permission.bonding.view.BondingScreen
import no.nordicsemi.android.permission.view.BluetoothNotAvailableScreen
import no.nordicsemi.android.permission.view.BluetoothNotEnabledScreen
import no.nordicsemi.android.permission.view.RequestPermissionScreen
@@ -59,6 +63,7 @@ internal fun HomeScreen() {
composable(NavDestination.BPS.id) { BPSScreen { viewModel.navigateUp() } }
composable(NavDestination.PRX.id) { PRXScreen { viewModel.navigateUp() } }
composable(NavDestination.RSCS.id) { RSCSScreen { viewModel.navigateUp() } }
+ composable(NavDestination.CGMS.id) { CGMScreen { viewModel.navigateUp() } }
composable(NavDestination.REQUEST_PERMISSION.id) { RequestPermissionScreen(continueAction) }
composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen{ viewModel.finish() } }
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
@@ -75,6 +80,7 @@ internal fun HomeScreen() {
}.exhaustive
}
}
+ composable(NavDestination.BONDING.id) { BondingScreen(continueAction) }
}
LaunchedEffect(state) {
@@ -90,19 +96,23 @@ fun HomeView(callback: (NavDestination) -> Unit) {
(context as? Activity)?.finish()
}
- FeatureButton(R.drawable.ic_csc, R.string.csc_module) { callback(NavDestination.CSC) }
- Spacer(modifier = Modifier.height(1.dp))
- FeatureButton(R.drawable.ic_hrs, R.string.hrs_module) { callback(NavDestination.HRS) }
- Spacer(modifier = Modifier.height(1.dp))
- FeatureButton(R.drawable.ic_gls, R.string.gls_module) { callback(NavDestination.GLS) }
- Spacer(modifier = Modifier.height(1.dp))
- FeatureButton(R.drawable.ic_hts, R.string.hts_module) { callback(NavDestination.HTS) }
- Spacer(modifier = Modifier.height(1.dp))
- FeatureButton(R.drawable.ic_bps, R.string.bps_module) { callback(NavDestination.BPS) }
- Spacer(modifier = Modifier.height(1.dp))
- FeatureButton(R.drawable.ic_rscs, R.string.rscs_module) { callback(NavDestination.RSCS) }
- Spacer(modifier = Modifier.height(1.dp))
- FeatureButton(R.drawable.ic_proximity, R.string.prx_module) { callback(NavDestination.PRX) }
+ Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
+ FeatureButton(R.drawable.ic_csc, R.string.csc_module) { callback(NavDestination.CSC) }
+ Spacer(modifier = Modifier.height(1.dp))
+ FeatureButton(R.drawable.ic_hrs, R.string.hrs_module) { callback(NavDestination.HRS) }
+ Spacer(modifier = Modifier.height(1.dp))
+ FeatureButton(R.drawable.ic_gls, R.string.gls_module) { callback(NavDestination.GLS) }
+ Spacer(modifier = Modifier.height(1.dp))
+ FeatureButton(R.drawable.ic_hts, R.string.hts_module) { callback(NavDestination.HTS) }
+ Spacer(modifier = Modifier.height(1.dp))
+ FeatureButton(R.drawable.ic_bps, R.string.bps_module) { callback(NavDestination.BPS) }
+ Spacer(modifier = Modifier.height(1.dp))
+ FeatureButton(R.drawable.ic_rscs, R.string.rscs_module) { callback(NavDestination.RSCS) }
+ Spacer(modifier = Modifier.height(1.dp))
+ FeatureButton(R.drawable.ic_prx, R.string.prx_module) { callback(NavDestination.PRX) }
+ Spacer(modifier = Modifier.height(1.dp))
+ FeatureButton(R.drawable.ic_cgm, R.string.cgm_module) { callback(NavDestination.CGMS) }
+ }
}
}
diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt
index fdcfecd1..dd510234 100644
--- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt
+++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt
@@ -2,17 +2,19 @@ package no.nordicsemi.android.nrftoolbox
const val ARGS_KEY = "args"
-enum class NavDestination(val id: String) {
- HOME("home-screen"),
- CSC("csc-screen"),
- HRS("hrs-screen"),
- HTS("hts-screen"),
- GLS("gls-screen"),
- BPS("bps-screen"),
- PRX("prx-screen"),
- RSCS("rscs-screen"),
- REQUEST_PERMISSION("request-permission"),
- BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"),
- BLUETOOTH_NOT_ENABLED("bluetooth-not-enabled"),
- DEVICE_NOT_CONNECTED("device-not-connected/{$ARGS_KEY}");
+enum class NavDestination(val id: String, val pairingRequired: Boolean) {
+ HOME("home-screen", false),
+ CSC("csc-screen", false),
+ HRS("hrs-screen", false),
+ HTS("hts-screen", false),
+ GLS("gls-screen", true),
+ BPS("bps-screen", false),
+ PRX("prx-screen", true),
+ RSCS("rscs-screen", false),
+ CGMS("cgms-screen", false),
+ REQUEST_PERMISSION("request-permission", false),
+ BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available", false),
+ BLUETOOTH_NOT_ENABLED("bluetooth-not-enabled", false),
+ DEVICE_NOT_CONNECTED("device-not-connected/{$ARGS_KEY}", false),
+ BONDING("bonding", false);
}
diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt
index a929352d..80f81f4e 100644
--- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt
+++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import no.nordicsemi.android.bps.repository.BPS_SERVICE_UUID
+import no.nordicsemi.android.cgms.repository.CGMS_UUID
import no.nordicsemi.android.csc.service.CYCLING_SPEED_AND_CADENCE_SERVICE_UUID
import no.nordicsemi.android.gls.repository.GLS_SERVICE_UUID
import no.nordicsemi.android.hrs.service.HR_SERVICE_UUID
@@ -49,7 +50,13 @@ class NavigationViewModel @Inject constructor(
} else when (bleScanner.getBluetoothStatus()) {
ScannerStatus.NOT_AVAILABLE -> BluetoothPermissionState.BLUETOOTH_NOT_AVAILABLE
ScannerStatus.DISABLED -> BluetoothPermissionState.BLUETOOTH_NOT_ENABLED
- ScannerStatus.ENABLED -> selectedDevice.device?.let { BluetoothPermissionState.READY } ?: BluetoothPermissionState.DEVICE_NOT_CONNECTED
+ ScannerStatus.ENABLED -> selectedDevice.device?.let {
+ if (targetDestination.pairingRequired && selectedDevice.isBondingRequired()) {
+ BluetoothPermissionState.BONDING_REQUIRED
+ } else {
+ BluetoothPermissionState.READY
+ }
+ } ?: BluetoothPermissionState.DEVICE_NOT_CONNECTED
}
}
@@ -59,6 +66,7 @@ class NavigationViewModel @Inject constructor(
BluetoothPermissionState.BLUETOOTH_NOT_AVAILABLE -> NavDestination.BLUETOOTH_NOT_AVAILABLE
BluetoothPermissionState.BLUETOOTH_NOT_ENABLED -> NavDestination.BLUETOOTH_NOT_ENABLED
BluetoothPermissionState.DEVICE_NOT_CONNECTED -> NavDestination.DEVICE_NOT_CONNECTED
+ BluetoothPermissionState.BONDING_REQUIRED -> NavDestination.BONDING
BluetoothPermissionState.READY -> targetDestination
}
@@ -79,10 +87,12 @@ class NavigationViewModel @Inject constructor(
NavDestination.BPS -> BPS_SERVICE_UUID.toString()
NavDestination.RSCS -> RSCS_SERVICE_UUID.toString()
NavDestination.PRX -> IMMEDIATE_ALERT_SERVICE_UUID.toString()
+ NavDestination.CGMS -> CGMS_UUID.toString()
NavDestination.HOME,
NavDestination.REQUEST_PERMISSION,
NavDestination.BLUETOOTH_NOT_AVAILABLE,
NavDestination.BLUETOOTH_NOT_ENABLED,
+ NavDestination.BONDING,
NavDestination.DEVICE_NOT_CONNECTED -> throw IllegalArgumentException("There is no serivce related to the destination: $destination")
}
}
diff --git a/app/src/main/res/drawable/ic_cgm.xml b/app/src/main/res/drawable/ic_cgm.xml
new file mode 100644
index 00000000..028f8b82
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cgm.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_proximity.xml b/app/src/main/res/drawable/ic_prx.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_proximity.xml
rename to app/src/main/res/drawable/ic_prx.xml
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fa104f0b..f959b1c7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -6,4 +6,5 @@
BPS
RSCS
PRX
+ CGMS
\ No newline at end of file
diff --git a/lib_permission/src/main/java/no/nordicsemi/android/permission/HiltModule.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/HiltModule.kt
index f7c54249..d850d0cb 100644
--- a/lib_permission/src/main/java/no/nordicsemi/android/permission/HiltModule.kt
+++ b/lib_permission/src/main/java/no/nordicsemi/android/permission/HiltModule.kt
@@ -7,6 +7,7 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
+import no.nordicsemi.android.permission.bonding.repository.BondingStateObserver
import no.nordicsemi.android.permission.tools.PermissionHelper
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
import javax.inject.Singleton
@@ -31,4 +32,10 @@ internal object HiltModule {
fun createPermissionHelper(@ApplicationContext context: Context): PermissionHelper {
return PermissionHelper(context)
}
+
+ @Singleton
+ @Provides
+ fun createBondingStateObserver(@ApplicationContext context: Context): BondingStateObserver {
+ return BondingStateObserver(context)
+ }
}
diff --git a/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/repository/BondingStateObserver.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/repository/BondingStateObserver.kt
new file mode 100644
index 00000000..22a00dd9
--- /dev/null
+++ b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/repository/BondingStateObserver.kt
@@ -0,0 +1,57 @@
+package no.nordicsemi.android.permission.bonding.repository
+
+import android.bluetooth.BluetoothDevice
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.util.Log
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import no.nordicsemi.android.service.BondingState
+
+class BondingStateObserver(private val context: Context) {
+
+ val events: MutableSharedFlow = MutableSharedFlow(
+ extraBufferCapacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+
+ fun startObserving() {
+ context.applicationContext.registerReceiver(
+ broadcastReceiver,
+ IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
+ )
+ }
+
+ fun stopObserving() {
+ context.applicationContext.unregisterReceiver(broadcastReceiver)
+ }
+
+ private val broadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ with(intent) {
+ if (action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
+ val device = getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ val previousBondState = getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1)
+ val bondState = getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1)
+ val bondTransition = "${previousBondState.toBondStateDescription()} to " + bondState.toBondStateDescription()
+ Log.w("Bond state change", "${device?.address} bond state changed | $bondTransition")
+ events.tryEmit(BondingStateChangeEvent(device, bondState))
+ }
+ }
+ }
+
+ private fun Int.toBondStateDescription() = when(this) {
+ BluetoothDevice.BOND_BONDED -> "BONDED"
+ BluetoothDevice.BOND_BONDING -> "BONDING"
+ BluetoothDevice.BOND_NONE -> "NOT BONDED"
+ else -> "ERROR: $this"
+ }
+ }
+}
+
+data class BondingStateChangeEvent(val device: BluetoothDevice?, private val bondStateValue: Int) {
+
+ val bondState = BondingState.create(bondStateValue)
+}
diff --git a/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingErrorView.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingErrorView.kt
new file mode 100644
index 00000000..a3f72000
--- /dev/null
+++ b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingErrorView.kt
@@ -0,0 +1,32 @@
+package no.nordicsemi.android.permission.bonding.view
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import no.nordicsemi.android.permission.R
+import no.nordicsemi.android.theme.view.ScreenSection
+
+@Composable
+internal fun BondingErrorView() {
+ ScreenSection {
+ Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
+ Text(
+ text = stringResource(id = R.string.bonding_error),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun BondingErrorViewPreview() {
+ BondingErrorView()
+}
diff --git a/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingInProgressView.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingInProgressView.kt
new file mode 100644
index 00000000..e4c96e2f
--- /dev/null
+++ b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingInProgressView.kt
@@ -0,0 +1,32 @@
+package no.nordicsemi.android.permission.bonding.view
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import no.nordicsemi.android.permission.R
+import no.nordicsemi.android.theme.view.ScreenSection
+
+@Composable
+internal fun BondingInProgressView() {
+ ScreenSection {
+ Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
+ Text(
+ text = stringResource(id = R.string.bonding_in_progress),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun BondingInProgressViewPreview() {
+ BondingInProgressView()
+}
diff --git a/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingScreen.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingScreen.kt
new file mode 100644
index 00000000..949e1a39
--- /dev/null
+++ b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingScreen.kt
@@ -0,0 +1,25 @@
+package no.nordicsemi.android.permission.bonding.view
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.hilt.navigation.compose.hiltViewModel
+import no.nordicsemi.android.permission.bonding.viewmodel.BondingViewModel
+import no.nordicsemi.android.service.BondingState
+import no.nordicsemi.android.utils.exhaustive
+
+@Composable
+fun BondingScreen(finishAction: () -> Unit) {
+ val viewModel: BondingViewModel = hiltViewModel()
+ val state = viewModel.state.collectAsState().value
+
+ LaunchedEffect("start") {
+ viewModel.bondDevice()
+ }
+
+ when (state) {
+ BondingState.BONDING -> BondingInProgressView()
+ BondingState.BONDED -> finishAction()
+ BondingState.NONE -> BondingErrorView()
+ }.exhaustive
+}
diff --git a/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingSuccessView.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingSuccessView.kt
new file mode 100644
index 00000000..fe5d3f7b
--- /dev/null
+++ b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/view/BondingSuccessView.kt
@@ -0,0 +1,32 @@
+package no.nordicsemi.android.permission.bonding
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import no.nordicsemi.android.permission.R
+import no.nordicsemi.android.theme.view.ScreenSection
+
+@Composable
+internal fun BondingSuccessView() {
+ ScreenSection {
+ Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
+ Text(
+ text = stringResource(id = R.string.bonding_success),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun BondingSuccessViewPreview() {
+ BondingSuccessView()
+}
diff --git a/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/viewmodel/BondingViewModel.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/viewmodel/BondingViewModel.kt
new file mode 100644
index 00000000..cf022372
--- /dev/null
+++ b/lib_permission/src/main/java/no/nordicsemi/android/permission/bonding/viewmodel/BondingViewModel.kt
@@ -0,0 +1,43 @@
+package no.nordicsemi.android.permission.bonding.viewmodel
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import no.nordicsemi.android.permission.bonding.repository.BondingStateObserver
+import no.nordicsemi.android.service.BondingState
+import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
+import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class BondingViewModel @Inject constructor(
+ private val deviceHolder: SelectedBluetoothDeviceHolder,
+ private val bondingStateObserver: BondingStateObserver
+) : CloseableViewModel() {
+
+ val state = MutableStateFlow(deviceHolder.getBondingState())
+
+ init {
+ bondingStateObserver.events.onEach { event ->
+ event.device?.let {
+ if (it == deviceHolder.device) {
+ state.tryEmit(event.bondState)
+ } else {
+ state.tryEmit(BondingState.NONE)
+ }
+ } ?: state.tryEmit(event.bondState)
+ }.launchIn(viewModelScope)
+ bondingStateObserver.startObserving()
+ }
+
+ fun bondDevice() {
+ deviceHolder.bondDevice()
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ bondingStateObserver.stopObserving()
+ }
+}
diff --git a/lib_permission/src/main/java/no/nordicsemi/android/permission/viewmodel/BluetoothPermissionState.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/viewmodel/BluetoothPermissionState.kt
index 9444f576..a0f6b205 100644
--- a/lib_permission/src/main/java/no/nordicsemi/android/permission/viewmodel/BluetoothPermissionState.kt
+++ b/lib_permission/src/main/java/no/nordicsemi/android/permission/viewmodel/BluetoothPermissionState.kt
@@ -5,5 +5,6 @@ enum class BluetoothPermissionState {
BLUETOOTH_NOT_AVAILABLE,
BLUETOOTH_NOT_ENABLED,
DEVICE_NOT_CONNECTED,
+ BONDING_REQUIRED,
READY
}
diff --git a/lib_permission/src/main/res/values/strings.xml b/lib_permission/src/main/res/values/strings.xml
index a12e9a62..8f942818 100644
--- a/lib_permission/src/main/res/values/strings.xml
+++ b/lib_permission/src/main/res/values/strings.xml
@@ -23,4 +23,8 @@
Bluetooth not enabled.
To enable Bluetooth please open settings.
Open settings
+
+ Bonding in progress. Please follow instruction on the screen.
+ Bonding success. Please wait for the redirect to chosen profile screen.
+ We cannot get data from the peripheral without bonding. Please bond the device.
diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt b/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt
index fb97f2b1..d05078fe 100644
--- a/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt
+++ b/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt
@@ -10,6 +10,15 @@ class SelectedBluetoothDeviceHolder {
fun isBondingRequired(): Boolean {
return device?.bondState == BluetoothDevice.BOND_NONE
}
+
+ fun getBondingState(): BondingState {
+ return when (device?.bondState) {
+ BluetoothDevice.BOND_BONDED -> BondingState.BONDED
+ BluetoothDevice.BOND_BONDING -> BondingState.BONDING
+ else -> BondingState.NONE
+ }
+ }
+
fun bondDevice() {
device?.createBond()
}
@@ -22,3 +31,18 @@ class SelectedBluetoothDeviceHolder {
device = null
}
}
+
+enum class BondingState {
+ NONE, BONDING, BONDED;
+
+ companion object {
+ fun create(value: Int): BondingState {
+ return when (value) {
+ BluetoothDevice.BOND_BONDED -> BONDED
+ BluetoothDevice.BOND_BONDING -> BONDING
+ BluetoothDevice.BOND_NONE -> NONE
+ else -> throw IllegalArgumentException("Cannot create BondingState for the value: $value")
+ }
+ }
+ }
+}
diff --git a/profile_cgms/build.gradle b/profile_cgms/build.gradle
new file mode 100644
index 00000000..d397c91b
--- /dev/null
+++ b/profile_cgms/build.gradle
@@ -0,0 +1,26 @@
+apply from: rootProject.file("library.gradle")
+apply plugin: 'kotlin-parcelize'
+
+dependencies {
+ implementation project(":lib_service")
+ implementation project(":lib_theme")
+ implementation project(":lib_utils")
+
+ implementation libs.nordic.ble.common
+
+ implementation libs.nordic.log
+
+ implementation libs.bundles.compose
+ implementation libs.androidx.core
+ implementation libs.material
+ implementation libs.lifecycle.activity
+ implementation libs.lifecycle.service
+ implementation libs.compose.lifecycle
+ implementation libs.compose.activity
+
+ testImplementation libs.test.junit
+ androidTestImplementation libs.android.test.junit
+ androidTestImplementation libs.android.test.espresso
+ androidTestImplementation libs.android.test.compose.ui
+ debugImplementation libs.android.test.compose.tooling
+}
diff --git a/profile_cgms/src/androidTest/java/no/nordicsemi/android/cgms/ExampleInstrumentedTest.kt b/profile_cgms/src/androidTest/java/no/nordicsemi/android/cgms/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..5febd881
--- /dev/null
+++ b/profile_cgms/src/androidTest/java/no/nordicsemi/android/cgms/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package no.nordicsemi.android.cgms
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("no.nordicsemi.android.cgms.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/profile_cgms/src/main/AndroidManifest.xml b/profile_cgms/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..6a415b28
--- /dev/null
+++ b/profile_cgms/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMDataHolder.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMDataHolder.kt
new file mode 100644
index 00000000..cfcf4e3c
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMDataHolder.kt
@@ -0,0 +1,21 @@
+package no.nordicsemi.android.cgms.data
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+internal class CGMDataHolder @Inject constructor() {
+
+ private val _data = MutableStateFlow(Idle)
+ val data: StateFlow = _data
+
+ fun emitNewEvent(event: CGMEvent) {
+ _data.tryEmit(event)
+ }
+
+ fun clear() {
+ _data.tryEmit(Idle)
+ }
+}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMEvent.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMEvent.kt
new file mode 100644
index 00000000..80c199ac
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMEvent.kt
@@ -0,0 +1,21 @@
+package no.nordicsemi.android.cgms.data
+
+internal sealed class CGMEvent
+
+internal object Idle : CGMEvent()
+
+internal data class OnCGMValueReceived(val record: CGMRecord) : CGMEvent()
+
+internal object OnOperationStarted : CGMEvent()
+
+internal object OnOperationCompleted : CGMEvent()
+
+internal object OnOperationFailed : CGMEvent()
+
+internal object OnOperationAborted : CGMEvent()
+
+internal object OnOperationNotSupported : CGMEvent()
+
+internal object OnDataSetCleared : CGMEvent()
+
+internal data class OnNumberOfRecordsRequested(val value: Int) : CGMEvent()
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRecord.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRecord.kt
new file mode 100644
index 00000000..ecf97197
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRecord.kt
@@ -0,0 +1,11 @@
+package no.nordicsemi.android.cgms.data
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+internal data class CGMRecord(
+ var sequenceNumber: Int,
+ var glucoseConcentration: Float,
+ var timestamp: Long
+) : Parcelable
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMManager.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMManager.kt
new file mode 100644
index 00000000..a915ab82
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMManager.kt
@@ -0,0 +1,566 @@
+/*
+ * Copyright (c) 2016, Nordic Semiconductor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package no.nordicsemi.android.cgms.repository
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGattCharacteristic
+import android.content.Context
+import android.util.Log
+import android.util.SparseArray
+import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback
+import no.nordicsemi.android.ble.common.callback.cgm.CGMFeatureDataCallback
+import no.nordicsemi.android.ble.common.callback.cgm.CGMSpecificOpsControlPointDataCallback
+import no.nordicsemi.android.ble.common.callback.cgm.CGMStatusDataCallback
+import no.nordicsemi.android.ble.common.callback.cgm.ContinuousGlucoseMeasurementDataCallback
+import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData
+import no.nordicsemi.android.ble.common.data.cgm.CGMSpecificOpsControlPointData
+import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback
+import no.nordicsemi.android.ble.common.profile.cgm.CGMSpecificOpsControlPointCallback
+import no.nordicsemi.android.ble.common.profile.cgm.CGMTypes
+import no.nordicsemi.android.ble.data.Data
+import no.nordicsemi.android.cgms.data.CGMDataHolder
+import no.nordicsemi.android.cgms.data.CGMRecord
+import no.nordicsemi.android.cgms.data.OnCGMValueReceived
+import no.nordicsemi.android.cgms.data.OnDataSetCleared
+import no.nordicsemi.android.cgms.data.OnNumberOfRecordsRequested
+import no.nordicsemi.android.cgms.data.OnOperationAborted
+import no.nordicsemi.android.cgms.data.OnOperationCompleted
+import no.nordicsemi.android.cgms.data.OnOperationFailed
+import no.nordicsemi.android.cgms.data.OnOperationNotSupported
+import no.nordicsemi.android.cgms.data.OnOperationStarted
+import no.nordicsemi.android.log.LogContract
+import no.nordicsemi.android.service.BatteryManager
+import java.util.*
+
+/** Cycling Speed and Cadence service UUID. */
+val CGMS_UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb")
+private val CGM_STATUS_UUID = UUID.fromString("00002AA9-0000-1000-8000-00805f9b34fb")
+private val CGM_FEATURE_UUID = UUID.fromString("00002AA8-0000-1000-8000-00805f9b34fb")
+private val CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb")
+private val CGM_OPS_CONTROL_POINT_UUID =
+ UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb")
+
+/** Record Access Control Point characteristic UUID. */
+private val RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
+
+internal class CGMManager(
+ context: Context,
+ private val dataHolder: CGMDataHolder
+) : BatteryManager(context) {
+
+ private var cgmStatusCharacteristic: BluetoothGattCharacteristic? = null
+ private var cgmFeatureCharacteristic: BluetoothGattCharacteristic? = null
+ private var cgmMeasurementCharacteristic: BluetoothGattCharacteristic? = null
+ private var cgmSpecificOpsControlPointCharacteristic: BluetoothGattCharacteristic? = null
+ private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
+ private val records: SparseArray = SparseArray()
+
+ /** A flag set to true if the remote device supports E2E CRC. */
+ private var secured = false
+
+ /**
+ * A flag set when records has been requested using RACP. This is to distinguish CGM packets
+ * received as continuous measurements or requested.
+ */
+ private var recordAccessRequestInProgress = false
+
+ /**
+ * The timestamp when the session has started. This is needed to display the user facing
+ * times of samples.
+ */
+ private var sessionStartTime: Long = 0
+ override fun onBatteryLevelChanged(batteryLevel: Int) {
+ TODO("Not yet implemented")
+ }
+
+ override fun getGattCallback(): BatteryManagerGattCallback {
+ return CGMManagerGattCallback()
+ }
+
+ /**
+ * BluetoothGatt mCallbacks for connection/disconnection, service discovery,
+ * receiving notification, etc.
+ */
+ private inner class CGMManagerGattCallback : BatteryManagerGattCallback() {
+ override fun initialize() {
+ // Enable Battery service
+ super.initialize()
+
+ // Read CGM Feature characteristic, mainly to see if the device supports E2E CRC.
+ // This is not supported in the experimental CGMS from the SDK.
+ readCharacteristic(cgmFeatureCharacteristic)
+ .with(object : CGMFeatureDataCallback() {
+ override fun onContinuousGlucoseMonitorFeaturesReceived(
+ device: BluetoothDevice, features: CGMTypes.CGMFeatures,
+ type: Int, sampleLocation: Int, secured: Boolean
+ ) {
+ this@CGMManager.secured = features.e2eCrcSupported
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "E2E CRC feature " + if (this@CGMManager.secured) "supported" else "not supported"
+ )
+ }
+ })
+ .fail { _: BluetoothDevice?, _: Int ->
+ log(
+ Log.WARN,
+ "Could not read CGM Feature characteristic"
+ )
+ }
+ .enqueue()
+
+ // Check if the session is already started. This is not supported in the experimental CGMS from the SDK.
+ readCharacteristic(cgmStatusCharacteristic)
+ .with(object : CGMStatusDataCallback() {
+ override fun onContinuousGlucoseMonitorStatusChanged(
+ device: BluetoothDevice,
+ status: CGMTypes.CGMStatus,
+ timeOffset: Int,
+ secured: Boolean
+ ) {
+ if (!status.sessionStopped) {
+ sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L
+ log(LogContract.Log.Level.APPLICATION, "Session already started")
+ }
+ }
+ })
+ .fail { _: BluetoothDevice?, _: Int ->
+ log(
+ Log.WARN,
+ "Could not read CGM Status characteristic"
+ )
+ }
+ .enqueue()
+
+ // Set notification and indication mCallbacks
+ setNotificationCallback(cgmMeasurementCharacteristic)
+ .with(object : ContinuousGlucoseMeasurementDataCallback() {
+ override fun onDataReceived(device: BluetoothDevice, data: Data) {
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + CGMMeasurementParser.parse(data).toString() + "\" received"
+ )
+ super.onDataReceived(device, data)
+ }
+
+ override fun onContinuousGlucoseMeasurementReceived(
+ device: BluetoothDevice,
+ glucoseConcentration: Float,
+ cgmTrend: Float?,
+ cgmQuality: Float?,
+ status: CGMTypes.CGMStatus?,
+ timeOffset: Int,
+ secured: Boolean
+ ) {
+ // If the CGM Status characteristic has not been read and the session was already started before,
+ // estimate the Session Start Time by subtracting timeOffset minutes from the current timestamp.
+ if (sessionStartTime == 0L && !recordAccessRequestInProgress) {
+ sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L
+ }
+
+ // Calculate the sample timestamp based on the Session Start Time
+ val timestamp =
+ sessionStartTime + timeOffset * 60000L // Sequence number is in minutes since Start Session
+ val record = CGMRecord(timeOffset, glucoseConcentration, timestamp)
+ records.put(record.sequenceNumber, record)
+ dataHolder.emitNewEvent(OnCGMValueReceived(record))
+ }
+
+ override fun onContinuousGlucoseMeasurementReceivedWithCrcError(
+ device: BluetoothDevice,
+ data: Data
+ ) {
+ log(
+ Log.WARN,
+ "Continuous Glucose Measurement record received with CRC error"
+ )
+ }
+ })
+ setIndicationCallback(cgmSpecificOpsControlPointCharacteristic)
+ .with(object : CGMSpecificOpsControlPointDataCallback() {
+ override fun onDataReceived(device: BluetoothDevice, data: Data) {
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + CGMSpecificOpsControlPointParser.parse(data)
+ .toString() + "\" received"
+ )
+ super.onDataReceived(device, data)
+ }
+
+ @SuppressLint("SwitchIntDef")
+ override fun onCGMSpecificOpsOperationCompleted(
+ device: BluetoothDevice,
+ @CGMSpecificOpsControlPointCallback.CGMOpCode requestCode: Int,
+ secured: Boolean
+ ) {
+ when (requestCode) {
+ CGMSpecificOpsControlPointCallback.CGM_OP_CODE_START_SESSION -> sessionStartTime =
+ System.currentTimeMillis()
+ CGMSpecificOpsControlPointCallback.CGM_OP_CODE_STOP_SESSION -> sessionStartTime =
+ 0
+ }
+ }
+
+ @SuppressLint("SwitchIntDef")
+ override fun onCGMSpecificOpsOperationError(
+ device: BluetoothDevice,
+ @CGMSpecificOpsControlPointCallback.CGMOpCode requestCode: Int,
+ @CGMSpecificOpsControlPointCallback.CGMErrorCode errorCode: Int,
+ secured: Boolean
+ ) {
+ when (requestCode) {
+ CGMSpecificOpsControlPointCallback.CGM_OP_CODE_START_SESSION -> {
+ if (errorCode == CGMSpecificOpsControlPointCallback.CGM_ERROR_PROCEDURE_NOT_COMPLETED) {
+ // Session was already started before.
+ // Looks like the CGM Status characteristic has not been read,
+ // otherwise we would have got the Session Start Time before.
+ // The Session Start Time will be calculated when a next CGM
+ // packet is received based on it's Time Offset.
+ }
+ sessionStartTime = 0
+ }
+ CGMSpecificOpsControlPointCallback.CGM_OP_CODE_STOP_SESSION -> sessionStartTime =
+ 0
+ }
+ }
+
+ override fun onCGMSpecificOpsResponseReceivedWithCrcError(
+ device: BluetoothDevice,
+ data: Data
+ ) {
+ log(Log.ERROR, "Request failed: CRC error")
+ }
+ })
+ setIndicationCallback(recordAccessControlPointCharacteristic)
+ .with(object : RecordAccessControlPointDataCallback() {
+ override fun onDataReceived(device: BluetoothDevice, data: Data) {
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + RecordAccessControlPointParser.parse(data)
+ .toString() + "\" received"
+ )
+ super.onDataReceived(device, data)
+ }
+
+ @SuppressLint("SwitchIntDef")
+ override fun onRecordAccessOperationCompleted(
+ device: BluetoothDevice,
+ @RecordAccessControlPointCallback.RACPOpCode requestCode: Int
+ ) {
+ when (requestCode) {
+ RecordAccessControlPointCallback.RACP_OP_CODE_ABORT_OPERATION -> dataHolder.emitNewEvent(
+ OnOperationAborted
+ )
+ else -> {
+ recordAccessRequestInProgress = false
+ dataHolder.emitNewEvent(OnOperationCompleted)
+ }
+ }
+ }
+
+ override fun onRecordAccessOperationCompletedWithNoRecordsFound(
+ device: BluetoothDevice,
+ @RecordAccessControlPointCallback.RACPOpCode requestCode: Int
+ ) {
+ recordAccessRequestInProgress = false
+ dataHolder.emitNewEvent(OnOperationCompleted)
+ }
+
+ override fun onNumberOfRecordsReceived(
+ device: BluetoothDevice,
+ numberOfRecords: Int
+ ) {
+ dataHolder.emitNewEvent(OnNumberOfRecordsRequested(numberOfRecords))
+ if (numberOfRecords > 0) {
+ if (records.size() > 0) {
+ val sequenceNumber = records.keyAt(records.size() - 1) + 1
+ writeCharacteristic(
+ recordAccessControlPointCharacteristic,
+ RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
+ sequenceNumber
+ )
+ )
+ .enqueue()
+ } else {
+ writeCharacteristic(
+ recordAccessControlPointCharacteristic,
+ RecordAccessControlPointData.reportAllStoredRecords()
+ )
+ .enqueue()
+ }
+ } else {
+ recordAccessRequestInProgress = false
+ dataHolder.emitNewEvent(OnOperationCompleted)
+ }
+ }
+
+ override fun onRecordAccessOperationError(
+ device: BluetoothDevice,
+ @RecordAccessControlPointCallback.RACPOpCode requestCode: Int,
+ @RecordAccessControlPointCallback.RACPErrorCode errorCode: Int
+ ) {
+ log(Log.WARN, "Record Access operation failed (error $errorCode)")
+ if (errorCode == RecordAccessControlPointCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
+ dataHolder.emitNewEvent(OnOperationNotSupported)
+ } else {
+ dataHolder.emitNewEvent(OnOperationFailed)
+ }
+ }
+ })
+
+ // Enable notifications and indications
+ enableNotifications(cgmMeasurementCharacteristic)
+ .fail { _: BluetoothDevice?, status: Int ->
+ log(
+ Log.WARN,
+ "Failed to enable Continuous Glucose Measurement notifications ($status)"
+ )
+ }
+ .enqueue()
+ enableIndications(cgmSpecificOpsControlPointCharacteristic)
+ .fail { _: BluetoothDevice?, status: Int ->
+ log(
+ Log.WARN,
+ "Failed to enable CGM Specific Ops Control Point indications notifications ($status)"
+ )
+ }
+ .enqueue()
+ enableIndications(recordAccessControlPointCharacteristic)
+ .fail { _: BluetoothDevice?, status: Int ->
+ log(
+ Log.WARN,
+ "Failed to enabled Record Access Control Point indications (error $status)"
+ )
+ }
+ .enqueue()
+
+ // Start Continuous Glucose session if hasn't been started before
+ if (sessionStartTime == 0L) {
+ writeCharacteristic(
+ cgmSpecificOpsControlPointCharacteristic,
+ CGMSpecificOpsControlPointData.startSession(secured)
+ )
+ .with { _: BluetoothDevice, data: Data ->
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + CGMSpecificOpsControlPointParser.parse(data) + "\" sent"
+ )
+ }
+ .fail { _: BluetoothDevice?, status: Int ->
+ log(
+ LogContract.Log.Level.ERROR,
+ "Failed to start session (error $status)"
+ )
+ }
+ .enqueue()
+ }
+ }
+
+ override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
+ val service = gatt.getService(CGMS_UUID)
+ if (service != null) {
+ cgmStatusCharacteristic = service.getCharacteristic(CGM_STATUS_UUID)
+ cgmFeatureCharacteristic = service.getCharacteristic(CGM_FEATURE_UUID)
+ cgmMeasurementCharacteristic = service.getCharacteristic(CGM_MEASUREMENT_UUID)
+ cgmSpecificOpsControlPointCharacteristic = service.getCharacteristic(
+ CGM_OPS_CONTROL_POINT_UUID
+ )
+ recordAccessControlPointCharacteristic = service.getCharacteristic(RACP_UUID)
+ }
+ return cgmMeasurementCharacteristic != null && cgmSpecificOpsControlPointCharacteristic != null && recordAccessControlPointCharacteristic != null
+ }
+
+ override fun onServicesInvalidated() { }
+
+ override fun onDeviceDisconnected() {
+ super.onDeviceDisconnected()
+ cgmStatusCharacteristic = null
+ cgmFeatureCharacteristic = null
+ cgmMeasurementCharacteristic = null
+ cgmSpecificOpsControlPointCharacteristic = null
+ recordAccessControlPointCharacteristic = null
+ }
+ }
+
+ /**
+ * Returns a list of CGM records obtained from this device. The key in the array is the
+ */
+ fun getRecords(): SparseArray {
+ return records
+ }
+
+ /**
+ * Clears the records list locally
+ */
+ fun clear() {
+ records.clear()
+ dataHolder.emitNewEvent(OnDataSetCleared)
+ }
+
+ /**
+ * Sends the request to obtain the last (most recent) record from glucose device.
+ * The data will be returned to Glucose Measurement characteristic as a notification followed by
+ * Record Access Control Point indication with status code Success or other in case of error.
+ */
+ val lastRecord: Unit
+ get() {
+ if (recordAccessControlPointCharacteristic == null) return
+ clear()
+ dataHolder.emitNewEvent(OnOperationStarted)
+ recordAccessRequestInProgress = true
+ writeCharacteristic(
+ recordAccessControlPointCharacteristic,
+ RecordAccessControlPointData.reportLastStoredRecord()
+ )
+ .with { device: BluetoothDevice, data: Data ->
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"
+ )
+ }
+ .enqueue()
+ }
+
+ /**
+ * Sends the request to obtain the first (oldest) record from glucose device.
+ * The data will be returned to Glucose Measurement characteristic as a notification followed by
+ * Record Access Control Point indication with status code Success or other in case of error.
+ */
+ val firstRecord: Unit
+ get() {
+ if (recordAccessControlPointCharacteristic == null) return
+ clear()
+ dataHolder.emitNewEvent(OnOperationStarted)
+ recordAccessRequestInProgress = true
+ writeCharacteristic(
+ recordAccessControlPointCharacteristic,
+ RecordAccessControlPointData.reportFirstStoredRecord()
+ )
+ .with { _: BluetoothDevice, data: Data ->
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"
+ )
+ }
+ .enqueue()
+ }
+
+ /**
+ * Sends abort operation signal to the device.
+ */
+ fun abort() {
+ if (recordAccessControlPointCharacteristic == null) return
+ writeCharacteristic(
+ recordAccessControlPointCharacteristic,
+ RecordAccessControlPointData.abortOperation()
+ )
+ .with { _: BluetoothDevice, data: Data ->
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"
+ )
+ }
+ .enqueue()
+ }
+
+ /**
+ * Sends the request to obtain all records from glucose device. Initially we want to notify the
+ * user about the number of the records so the Report Number of Stored Records request is send.
+ * The data will be returned to Glucose Measurement characteristic as a notification followed by
+ * Record Access Control Point indication with status code Success or other in case of error.
+ */
+ val allRecords: Unit
+ get() {
+ if (recordAccessControlPointCharacteristic == null) return
+ clear()
+ dataHolder.emitNewEvent(OnOperationStarted)
+ recordAccessRequestInProgress = true
+ writeCharacteristic(
+ recordAccessControlPointCharacteristic,
+ RecordAccessControlPointData.reportNumberOfAllStoredRecords()
+ )
+ .with { _: BluetoothDevice, data: Data ->
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"
+ )
+ }
+ .enqueue()
+ }
+
+ /**
+ * Sends the request to obtain all records from glucose device. Initially we want to notify the
+ * user about the number of the records so the Report Number of Stored Records request is send.
+ * The data will be returned to Glucose Measurement characteristic as a notification followed by
+ * Record Access Control Point indication with status code Success or other in case of error.
+ */
+ fun refreshRecords() {
+ if (recordAccessControlPointCharacteristic == null) return
+ if (records.size() == 0) {
+ allRecords
+ } else {
+ dataHolder.emitNewEvent(OnOperationStarted)
+
+ // Obtain the last sequence number
+ val sequenceNumber = records.keyAt(records.size() - 1) + 1
+ recordAccessRequestInProgress = true
+ writeCharacteristic(
+ recordAccessControlPointCharacteristic,
+ RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)
+ )
+ .with { _: BluetoothDevice, data: Data ->
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"
+ )
+ }
+ .enqueue()
+ // Info:
+ // Operators OPERATOR_GREATER_THEN_OR_EQUAL, OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by the CGMS sample from SDK
+ // The "Operation not supported" response will be received
+ }
+ }
+
+ /**
+ * Sends the request to remove all stored records from the Continuous Glucose Monitor device.
+ * This feature is not supported by the CGMS sample from the SDK, so monitor will answer with
+ * the Op Code Not Supported error.
+ */
+ fun deleteAllRecords() {
+ if (recordAccessControlPointCharacteristic == null) return
+ clear()
+ dataHolder.emitNewEvent(OnOperationStarted)
+ writeCharacteristic(
+ recordAccessControlPointCharacteristic,
+ RecordAccessControlPointData.deleteAllStoredRecords()
+ )
+ .with { _: BluetoothDevice, data: Data ->
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + RecordAccessControlPointParser.parse(data) + "\" sent"
+ )
+ }
+ .enqueue()
+ }
+}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMMeasurementParser.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMMeasurementParser.kt
new file mode 100644
index 00000000..10c22ed5
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMMeasurementParser.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) 2015, Nordic Semiconductor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package no.nordicsemi.android.cgms.repository
+
+import no.nordicsemi.android.ble.data.Data
+import java.util.*
+
+object CGMMeasurementParser {
+
+ private const val FLAGS_CGM_TREND_INFO_PRESENT = 1
+ private const val FLAGS_CGM_QUALITY_PRESENT = 1 shl 1
+ private const val FLAGS_SENSOR_STATUS_ANNUNCIATION_WARNING_OCTET_PRESENT = 1 shl 2
+ private const val FLAGS_SENSOR_STATUS_ANNUNCIATION_CAL_TEMP_OCTET_PRESENT = 1 shl 3
+ private const val FLAGS_SENSOR_STATUS_ANNUNCIATION_STATUS_OCTET_PRESENT = 1 shl 4
+ private const val SSA_SESSION_STOPPED = 1
+ private const val SSA_DEVICE_BATTERY_LOW = 1 shl 1
+ private const val SSA_SENSOR_TYPE_INCORRECT = 1 shl 2
+ private const val SSA_SENSOR_MALFUNCTION = 1 shl 3
+ private const val SSA_DEVICE_SPEC_ALERT = 1 shl 4
+ private const val SSA_GENERAL_DEVICE_FAULT = 1 shl 5
+ private const val SSA_TIME_SYNC_REQUIRED = 1 shl 8
+ private const val SSA_CALIBRATION_NOT_ALLOWED = 1 shl 9
+ private const val SSA_CALIBRATION_RECOMMENDED = 1 shl 10
+ private const val SSA_CALIBRATION_REQUIRED = 1 shl 11
+ private const val SSA_SENSOR_TEMP_TOO_HIGH = 1 shl 12
+ private const val SSA_SENSOR_TEMP_TOO_LOW = 1 shl 13
+ private const val SSA_RESULT_LOWER_THAN_PATIENT_LOW_LEVEL = 1 shl 16
+ private const val SSA_RESULT_HIGHER_THAN_PATIENT_HIGH_LEVEL = 1 shl 17
+ private const val SSA_RESULT_LOWER_THAN_HYPO_LEVEL = 1 shl 18
+ private const val SSA_RESULT_HIGHER_THAN_HYPER_LEVEL = 1 shl 19
+ private const val SSA_SENSOR_RATE_OF_DECREASE_EXCEEDED = 1 shl 20
+ private const val SSA_SENSOR_RATE_OF_INCREASE_EXCEEDED = 1 shl 21
+ private const val SSA_RESULT_LOWER_THAN_DEVICE_CAN_PROCESS = 1 shl 22
+ private const val SSA_RESULT_HIGHER_THAN_DEVICE_CAN_PROCESS = 1 shl 23
+
+ fun parse(data: Data): String {
+ // The CGM Measurement characteristic is a variable length structure containing one or more CGM Measurement records
+ val totalSize = data.value!!.size
+ val builder = StringBuilder()
+ var offset = 0
+ while (offset < totalSize) {
+ offset += parseRecord(builder, data, offset)
+ if (offset < totalSize) builder.append("\n\n")
+ }
+ return builder.toString()
+ }
+
+ private fun parseRecord(builder: StringBuilder, data: Data, offset: Int): Int {
+ // Read size and flags bytes
+ var offset = offset
+ val size = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+ val flags = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+
+ /*
+ * false CGM Trend Information is not preset
+ * true CGM Trend Information is preset
+ */
+ val cgmTrendInformationPresent = flags and FLAGS_CGM_TREND_INFO_PRESENT > 0
+
+ /*
+ * false CGM Quality is not preset
+ * true CGM Quality is preset
+ */
+ val cgmQualityPresent = flags and FLAGS_CGM_QUALITY_PRESENT > 0
+
+ /*
+ * false Sensor Status Annunciation - Warning-Octet is not preset
+ * true Sensor Status Annunciation - Warning-Octet is preset
+ */
+ val ssaWarningOctetPresent =
+ flags and FLAGS_SENSOR_STATUS_ANNUNCIATION_WARNING_OCTET_PRESENT > 0
+
+ /*
+ * false Sensor Status Annunciation - Calibration/Temp-Octet is not preset
+ * true Sensor Status Annunciation - Calibration/Temp-Octet is preset
+ */
+ val ssaCalTempOctetPresent =
+ flags and FLAGS_SENSOR_STATUS_ANNUNCIATION_CAL_TEMP_OCTET_PRESENT > 0
+
+ /*
+ * false Sensor Status Annunciation - Status-Octet is not preset
+ * true Sensor Status Annunciation - Status-Octet is preset
+ */
+ val ssaStatusOctetPresent =
+ flags and FLAGS_SENSOR_STATUS_ANNUNCIATION_STATUS_OCTET_PRESENT > 0
+
+ // Read CGM Glucose Concentration
+ val glucoseConcentration = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ offset += 2
+
+ // Read time offset
+ val timeOffset = data.getIntValue(Data.FORMAT_UINT16, offset)!!
+ offset += 2
+ builder.append("Glucose concentration: ").append(glucoseConcentration).append(" mg/dL\n")
+ builder.append("Sequence number: ").append(timeOffset).append(" (Time Offset in min)\n")
+ if (ssaWarningOctetPresent) {
+ val ssaWarningOctet = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+ builder.append("Warnings:\n")
+ if (ssaWarningOctet and SSA_SESSION_STOPPED > 0) builder.append("- Session Stopped\n")
+ if (ssaWarningOctet and SSA_DEVICE_BATTERY_LOW > 0) builder.append("- Device Battery Low\n")
+ if (ssaWarningOctet and SSA_SENSOR_TYPE_INCORRECT > 0) builder.append("- Sensor Type Incorrect\n")
+ if (ssaWarningOctet and SSA_SENSOR_MALFUNCTION > 0) builder.append("- Sensor Malfunction\n")
+ if (ssaWarningOctet and SSA_DEVICE_SPEC_ALERT > 0) builder.append("- Device Specific Alert\n")
+ if (ssaWarningOctet and SSA_GENERAL_DEVICE_FAULT > 0) builder.append("- General Device Fault\n")
+ }
+ if (ssaCalTempOctetPresent) {
+ val ssaCalTempOctet = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+ builder.append("Cal/Temp Info:\n")
+ if (ssaCalTempOctet and SSA_TIME_SYNC_REQUIRED > 0) builder.append("- Time Synchronization Required\n")
+ if (ssaCalTempOctet and SSA_CALIBRATION_NOT_ALLOWED > 0) builder.append("- Calibration Not Allowed\n")
+ if (ssaCalTempOctet and SSA_CALIBRATION_RECOMMENDED > 0) builder.append("- Calibration Recommended\n")
+ if (ssaCalTempOctet and SSA_CALIBRATION_REQUIRED > 0) builder.append("- Calibration Required\n")
+ if (ssaCalTempOctet and SSA_SENSOR_TEMP_TOO_HIGH > 0) builder.append("- Sensor Temp Too High\n")
+ if (ssaCalTempOctet and SSA_SENSOR_TEMP_TOO_LOW > 0) builder.append("- Sensor Temp Too Low\n")
+ }
+ if (ssaStatusOctetPresent) {
+ val ssaStatusOctet = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+ builder.append("Status:\n")
+ if (ssaStatusOctet and SSA_RESULT_LOWER_THAN_PATIENT_LOW_LEVEL > 0) builder.append("- Result Lower then Patient Low Level\n")
+ if (ssaStatusOctet and SSA_RESULT_HIGHER_THAN_PATIENT_HIGH_LEVEL > 0) builder.append("- Result Higher then Patient High Level\n")
+ if (ssaStatusOctet and SSA_RESULT_LOWER_THAN_HYPO_LEVEL > 0) builder.append("- Result Lower then Hypo Level\n")
+ if (ssaStatusOctet and SSA_RESULT_HIGHER_THAN_HYPER_LEVEL > 0) builder.append("- Result Higher then Hyper Level\n")
+ if (ssaStatusOctet and SSA_SENSOR_RATE_OF_DECREASE_EXCEEDED > 0) builder.append("- Sensor Rate of Decrease Exceeded\n")
+ if (ssaStatusOctet and SSA_SENSOR_RATE_OF_INCREASE_EXCEEDED > 0) builder.append("- Sensor Rate of Increase Exceeded\n")
+ if (ssaStatusOctet and SSA_RESULT_LOWER_THAN_DEVICE_CAN_PROCESS > 0) builder.append("- Result Lower then Device Can Process\n")
+ if (ssaStatusOctet and SSA_RESULT_HIGHER_THAN_DEVICE_CAN_PROCESS > 0) builder.append("- Result Higher then Device Can Process\n")
+ }
+ if (cgmTrendInformationPresent) {
+ val trend = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ offset += 2
+ builder.append("Trend: ").append(trend).append(" mg/dL/min\n")
+ }
+ if (cgmQualityPresent) {
+ val quality = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ offset += 2
+ builder.append("Quality: ").append(quality).append("%\n")
+ }
+ if (size > offset + 1) {
+ val crc = data.getIntValue(Data.FORMAT_UINT16, offset)!!
+ // offset += 2;
+ builder.append(String.format(Locale.US, "E2E-CRC: 0x%04X\n", crc))
+ }
+ builder.setLength(builder.length - 1) // Remove last \n
+ return size
+ }
+}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt
new file mode 100644
index 00000000..fefc381d
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt
@@ -0,0 +1,15 @@
+package no.nordicsemi.android.cgms.repository
+
+import dagger.hilt.android.AndroidEntryPoint
+import no.nordicsemi.android.cgms.data.CGMDataHolder
+import no.nordicsemi.android.service.ForegroundBleService
+import javax.inject.Inject
+
+@AndroidEntryPoint
+internal class CGMService : ForegroundBleService() {
+
+ @Inject
+ lateinit var dataHolder: CGMDataHolder
+
+ override val manager: CGMManager by lazy { CGMManager(this, dataHolder) }
+}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMSpecificOpsControlPointParser.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMSpecificOpsControlPointParser.kt
new file mode 100644
index 00000000..9f579328
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMSpecificOpsControlPointParser.kt
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2015, Nordic Semiconductor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package no.nordicsemi.android.cgms.repository
+
+import no.nordicsemi.android.ble.data.Data
+
+object CGMSpecificOpsControlPointParser {
+
+ private const val OP_SET_CGM_COMMUNICATION_INTERVAL = 1
+ private const val OP_GET_CGM_COMMUNICATION_INTERVAL = 2
+ private const val OP_CGM_COMMUNICATION_INTERVAL_RESPONSE = 3
+ private const val OP_SET_GLUCOSE_CALIBRATION_VALUE = 4
+ private const val OP_GET_GLUCOSE_CALIBRATION_VALUE = 5
+ private const val OP_GLUCOSE_CALIBRATION_VALUE_RESPONSE = 6
+ private const val OP_SET_PATIENT_HIGH_ALERT_LEVEL = 7
+ private const val OP_GET_PATIENT_HIGH_ALERT_LEVEL = 8
+ private const val OP_PATIENT_HIGH_ALERT_LEVEL_RESPONSE = 9
+ private const val OP_SET_PATIENT_LOW_ALERT_LEVEL = 10
+ private const val OP_GET_PATIENT_LOW_ALERT_LEVEL = 11
+ private const val OP_PATIENT_LOW_ALERT_LEVEL_RESPONSE = 12
+ private const val OP_SET_HYPO_ALERT_LEVEL = 13
+ private const val OP_GET_HYPO_ALERT_LEVEL = 14
+ private const val OP_HYPO_ALERT_LEVEL_RESPONSE = 15
+ private const val OP_SET_HYPER_ALERT_LEVEL = 16
+ private const val OP_GET_HYPER_ALERT_LEVEL = 17
+ private const val OP_HYPER_ALERT_LEVEL_RESPONSE = 18
+ private const val OP_SET_RATE_OF_DECREASE_ALERT_LEVEL = 19
+ private const val OP_GET_RATE_OF_DECREASE_ALERT_LEVEL = 20
+ private const val OP_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE = 21
+ private const val OP_SET_RATE_OF_INCREASE_ALERT_LEVEL = 22
+ private const val OP_GET_RATE_OF_INCREASE_ALERT_LEVEL = 23
+ private const val OP_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE = 24
+ private const val OP_RESET_DEVICE_SPECIFIC_ALERT = 25
+ private const val OP_CODE_START_SESSION = 26
+ private const val OP_CODE_STOP_SESSION = 27
+ private const val OP_CODE_RESPONSE_CODE = 28
+
+ // TODO this parser does not support E2E-CRC!
+ fun parse(data: Data): String {
+ var offset = 0
+ val opCode = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+ val builder = StringBuilder()
+ builder.append(parseOpCode(opCode))
+ when (opCode) {
+ OP_SET_CGM_COMMUNICATION_INTERVAL, OP_CGM_COMMUNICATION_INTERVAL_RESPONSE -> {
+ val interval = data.getIntValue(Data.FORMAT_UINT8, offset)!!
+ builder.append(" to ").append(interval).append(" min")
+ }
+ OP_SET_GLUCOSE_CALIBRATION_VALUE -> {
+ val calConcentration = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ offset += 2
+ val calTime = data.getIntValue(Data.FORMAT_UINT16, offset)!!
+ offset += 2
+ val calTypeSampleLocation = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+ val calType = calTypeSampleLocation and 0x0F
+ val calSampleLocation = calTypeSampleLocation and 0xF0 shr 4
+ val calNextCalibrationTime = data.getIntValue(Data.FORMAT_UINT16, offset)!!
+ // offset += 2;
+ // final int calCalibrationDataRecordNumber = data.getIntValue(Data.FORMAT_UINT16, offset);
+ // offset += 2;
+ // final int calStatus = data.getIntValue(Data.FORMAT_UINT8, offset++);
+ builder.append(" to:\n")
+ builder.append("Glucose Concentration of Calibration: ").append(calConcentration)
+ .append(" mg/dL\n")
+ builder.append("Time: ").append(calTime).append(" min\n")
+ builder.append("Type: ").append(parseType(calType)).append("\n")
+ builder.append("Sample Location: ").append(parseSampleLocation(calSampleLocation))
+ .append("\n")
+ builder.append("Next Calibration Time: ")
+ .append(parseNextCalibrationTime(calNextCalibrationTime))
+ .append(" min\n") // field ignored on Set
+ }
+ OP_GET_GLUCOSE_CALIBRATION_VALUE -> {
+ val calibrationRecordNumber = data.getIntValue(Data.FORMAT_UINT16, offset)!!
+ builder.append(": ").append(parseRecordNumber(calibrationRecordNumber))
+ }
+ OP_GLUCOSE_CALIBRATION_VALUE_RESPONSE -> {
+ val calConcentration = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ offset += 2
+ val calTime = data.getIntValue(Data.FORMAT_UINT16, offset)!!
+ offset += 2
+ val calTypeSampleLocation = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+ val calType = calTypeSampleLocation and 0x0F
+ val calSampleLocation = calTypeSampleLocation and 0xF0 shr 4
+ val calNextCalibrationTime = data.getIntValue(Data.FORMAT_UINT16, offset)!!
+ offset += 2
+ val calCalibrationDataRecordNumber = data.getIntValue(Data.FORMAT_UINT16, offset)!!
+ offset += 2
+ val calStatus = data.getIntValue(Data.FORMAT_UINT8, offset)!!
+ builder.append(":\n")
+ if (calCalibrationDataRecordNumber > 0) {
+ builder.append("Glucose Concentration of Calibration: ")
+ .append(calConcentration).append(" mg/dL\n")
+ builder.append("Time: ").append(calTime).append(" min\n")
+ builder.append("Type: ").append(parseType(calType)).append("\n")
+ builder.append("Sample Location: ")
+ .append(parseSampleLocation(calSampleLocation)).append("\n")
+ builder.append("Next Calibration Time: ")
+ .append(parseNextCalibrationTime(calNextCalibrationTime)).append("\n")
+ builder.append("Data Record Number: ").append(calCalibrationDataRecordNumber)
+ parseStatus(builder, calStatus)
+ } else {
+ builder.append("No Calibration Data Stored")
+ }
+ }
+ OP_SET_PATIENT_HIGH_ALERT_LEVEL, OP_SET_PATIENT_LOW_ALERT_LEVEL, OP_SET_HYPO_ALERT_LEVEL, OP_SET_HYPER_ALERT_LEVEL -> {
+ val level = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ builder.append(" to: ").append(level).append(" mg/dL")
+ }
+ OP_PATIENT_HIGH_ALERT_LEVEL_RESPONSE, OP_PATIENT_LOW_ALERT_LEVEL_RESPONSE, OP_HYPO_ALERT_LEVEL_RESPONSE, OP_HYPER_ALERT_LEVEL_RESPONSE -> {
+ val level = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ builder.append(": ").append(level).append(" mg/dL")
+ }
+ OP_SET_RATE_OF_DECREASE_ALERT_LEVEL, OP_SET_RATE_OF_INCREASE_ALERT_LEVEL -> {
+ val level = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ builder.append(" to: ").append(level).append(" mg/dL/min")
+ }
+ OP_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE, OP_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE -> {
+ val level = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ builder.append(": ").append(level).append(" mg/dL/min")
+ }
+ OP_CODE_RESPONSE_CODE -> {
+ val requestOpCode = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+ val responseCode = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+ builder.append(" to ").append(parseOpCode(requestOpCode)).append(": ").append(
+ parseResponseCode(responseCode)
+ )
+ }
+ }
+ return builder.toString()
+ }
+
+ private fun parseOpCode(code: Int): String {
+ return when (code) {
+ OP_SET_CGM_COMMUNICATION_INTERVAL -> "Set CGM Communication Interval"
+ OP_GET_CGM_COMMUNICATION_INTERVAL -> "Get CGM Communication Interval"
+ OP_CGM_COMMUNICATION_INTERVAL_RESPONSE -> "CGM Communication Interval"
+ OP_SET_GLUCOSE_CALIBRATION_VALUE -> "Set CGM Calibration Value"
+ OP_GET_GLUCOSE_CALIBRATION_VALUE -> "Get CGM Calibration Value"
+ OP_GLUCOSE_CALIBRATION_VALUE_RESPONSE -> "CGM Calibration Value"
+ OP_SET_PATIENT_HIGH_ALERT_LEVEL -> "Set Patient High Alert Level"
+ OP_GET_PATIENT_HIGH_ALERT_LEVEL -> "Get Patient High Alert Level"
+ OP_PATIENT_HIGH_ALERT_LEVEL_RESPONSE -> "Patient High Alert Level"
+ OP_SET_PATIENT_LOW_ALERT_LEVEL -> "Set Patient Low Alert Level"
+ OP_GET_PATIENT_LOW_ALERT_LEVEL -> "Get Patient Low Alert Level"
+ OP_PATIENT_LOW_ALERT_LEVEL_RESPONSE -> "Patient Low Alert Level"
+ OP_SET_HYPO_ALERT_LEVEL -> "Set Hypo Alert Level"
+ OP_GET_HYPO_ALERT_LEVEL -> "Get Hypo Alert Level"
+ OP_HYPO_ALERT_LEVEL_RESPONSE -> "Hypo Alert Level"
+ OP_SET_HYPER_ALERT_LEVEL -> "Set Hyper Alert Level"
+ OP_GET_HYPER_ALERT_LEVEL -> "Get Hyper Alert Level"
+ OP_HYPER_ALERT_LEVEL_RESPONSE -> "Hyper Alert Level"
+ OP_SET_RATE_OF_DECREASE_ALERT_LEVEL -> "Set Rate of Decrease Alert Level"
+ OP_GET_RATE_OF_DECREASE_ALERT_LEVEL -> "Get Rate of Decrease Alert Level"
+ OP_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE -> "Rate of Decrease Alert Level"
+ OP_SET_RATE_OF_INCREASE_ALERT_LEVEL -> "Set Rate of Increase Alert Level"
+ OP_GET_RATE_OF_INCREASE_ALERT_LEVEL -> "Get Rate of Increase Alert Level"
+ OP_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE -> "Rate of Increase Alert Level"
+ OP_RESET_DEVICE_SPECIFIC_ALERT -> "Reset Device Specific Alert"
+ OP_CODE_START_SESSION -> "Start Session"
+ OP_CODE_STOP_SESSION -> "Stop Session"
+ OP_CODE_RESPONSE_CODE -> "Response"
+ else -> "Reserved for future use ($code)"
+ }
+ }
+
+ private fun parseResponseCode(code: Int): String {
+ return when (code) {
+ 1 -> "Success"
+ 2 -> "Op Code not supported"
+ 3 -> "Invalid Operand"
+ 4 -> "Procedure not completed"
+ 5 -> "Parameter out of range"
+ else -> "Reserved for future use ($code)"
+ }
+ }
+
+ private fun parseType(type: Int): String {
+ return when (type) {
+ 1 -> "Capillary Whole blood"
+ 2 -> "Capillary Plasma"
+ 3 -> "Capillary Whole blood"
+ 4 -> "Venous Plasma"
+ 5 -> "Arterial Whole blood"
+ 6 -> "Arterial Plasma"
+ 7 -> "Undetermined Whole blood"
+ 8 -> "Undetermined Plasma"
+ 9 -> "Interstitial Fluid (ISF)"
+ 10 -> "Control Solution"
+ else -> "Reserved for future use ($type)"
+ }
+ }
+
+ private fun parseSampleLocation(location: Int): String {
+ return when (location) {
+ 1 -> "Finger"
+ 2 -> "Alternate Site Test (AST)"
+ 3 -> "Earlobe"
+ 4 -> "Control solution"
+ 5 -> "Subcutaneous tissue"
+ 15 -> "Sample Location value not available"
+ else -> "Reserved for future use ($location)"
+ }
+ }
+
+ private fun parseNextCalibrationTime(time: Int): String {
+ return if (time == 0) "Calibration Required Instantly" else "$time min"
+ }
+
+ private fun parseRecordNumber(time: Int): String {
+ return if (time == 0xFFFF) "Last Calibration Data" else time.toString()
+ }
+
+ private fun parseStatus(builder: StringBuilder, status: Int) {
+ if (status == 0) return
+ builder.append("\nStatus:\n")
+ if (status and 1 > 0) builder.append("- Calibration Data rejected")
+ if (status and 2 > 0) builder.append("- Calibration Data out of range")
+ if (status and 4 > 0) builder.append("- Calibration Process pending")
+ if (status and 0xF8 > 0) builder.append("- Reserved for future use (").append(status)
+ .append(")")
+ }
+}
\ No newline at end of file
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/RecordAccessControlPointParser.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/RecordAccessControlPointParser.kt
new file mode 100644
index 00000000..8237115f
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/RecordAccessControlPointParser.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2015, Nordic Semiconductor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package no.nordicsemi.android.cgms.repository
+
+import no.nordicsemi.android.ble.data.Data
+
+object RecordAccessControlPointParser {
+
+ private const val OP_CODE_REPORT_STORED_RECORDS = 1
+ private const val OP_CODE_DELETE_STORED_RECORDS = 2
+ private const val OP_CODE_ABORT_OPERATION = 3
+ private const val OP_CODE_REPORT_NUMBER_OF_RECORDS = 4
+ private const val OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE = 5
+ private const val OP_CODE_RESPONSE_CODE = 6
+ private const val OPERATOR_NULL = 0
+ private const val OPERATOR_ALL_RECORDS = 1
+ private const val OPERATOR_LESS_THEN_OR_EQUAL = 2
+ private const val OPERATOR_GREATER_THEN_OR_EQUAL = 3
+ private const val OPERATOR_WITHING_RANGE = 4
+ private const val OPERATOR_FIRST_RECORD = 5
+ private const val OPERATOR_LAST_RECORD = 6
+ private const val RESPONSE_SUCCESS = 1
+ private const val RESPONSE_OP_CODE_NOT_SUPPORTED = 2
+ private const val RESPONSE_INVALID_OPERATOR = 3
+ private const val RESPONSE_OPERATOR_NOT_SUPPORTED = 4
+ private const val RESPONSE_INVALID_OPERAND = 5
+ private const val RESPONSE_NO_RECORDS_FOUND = 6
+ private const val RESPONSE_ABORT_UNSUCCESSFUL = 7
+ private const val RESPONSE_PROCEDURE_NOT_COMPLETED = 8
+ private const val RESPONSE_OPERAND_NOT_SUPPORTED = 9
+
+ fun parse(data: Data): String {
+ val builder = StringBuilder()
+ val opCode = data.getIntValue(Data.FORMAT_UINT8, 0)!!
+ val operator = data.getIntValue(Data.FORMAT_UINT8, 1)!!
+ when (opCode) {
+ OP_CODE_REPORT_STORED_RECORDS, OP_CODE_DELETE_STORED_RECORDS, OP_CODE_ABORT_OPERATION, OP_CODE_REPORT_NUMBER_OF_RECORDS -> builder.append(
+ getOpCode(opCode)
+ ).append("\n")
+ OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE -> {
+ builder.append(getOpCode(opCode)).append(": ")
+ val value = data.getIntValue(Data.FORMAT_UINT16, 2)!!
+ builder.append(value).append("\n")
+ }
+ OP_CODE_RESPONSE_CODE -> {
+ builder.append(getOpCode(opCode)).append(" for ")
+ val targetOpCode = data.getIntValue(Data.FORMAT_UINT8, 2)!!
+ builder.append(getOpCode(targetOpCode)).append(": ")
+ val status = data.getIntValue(Data.FORMAT_UINT8, 3)!!
+ builder.append(getStatus(status)).append("\n")
+ }
+ }
+ when (operator) {
+ OPERATOR_ALL_RECORDS, OPERATOR_FIRST_RECORD, OPERATOR_LAST_RECORD -> builder.append("Operator: ")
+ .append(
+ getOperator(operator)
+ ).append("\n")
+ OPERATOR_GREATER_THEN_OR_EQUAL, OPERATOR_LESS_THEN_OR_EQUAL -> {
+ val filter = data.getIntValue(Data.FORMAT_UINT8, 2)!!
+ val value = data.getIntValue(Data.FORMAT_UINT16, 3)!!
+ builder.append("Operator: ").append(getOperator(operator)).append(" ").append(value)
+ .append(" (filter: ").append(filter).append(")\n")
+ }
+ OPERATOR_WITHING_RANGE -> {
+ val filter = data.getIntValue(Data.FORMAT_UINT8, 2)!!
+ val value1 = data.getIntValue(Data.FORMAT_UINT16, 3)!!
+ val value2 = data.getIntValue(Data.FORMAT_UINT16, 5)!!
+ builder.append("Operator: ").append(getOperator(operator)).append(" ")
+ .append(value1).append("-").append(value2).append(" (filter: ").append(filter)
+ .append(")\n")
+ }
+ }
+ if (builder.length > 0) builder.setLength(builder.length - 1)
+ return builder.toString()
+ }
+
+ private fun getOpCode(opCode: Int): String {
+ return when (opCode) {
+ OP_CODE_REPORT_STORED_RECORDS -> "Report stored records"
+ OP_CODE_DELETE_STORED_RECORDS -> "Delete stored records"
+ OP_CODE_ABORT_OPERATION -> "Abort operation"
+ OP_CODE_REPORT_NUMBER_OF_RECORDS -> "Report number of stored records"
+ OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE -> "Number of stored records response"
+ OP_CODE_RESPONSE_CODE -> "Response Code"
+ else -> "Reserved for future use"
+ }
+ }
+
+ private fun getOperator(operator: Int): String {
+ return when (operator) {
+ OPERATOR_NULL -> "Null"
+ OPERATOR_ALL_RECORDS -> "All records"
+ OPERATOR_LESS_THEN_OR_EQUAL -> "Less than or equal to"
+ OPERATOR_GREATER_THEN_OR_EQUAL -> "Greater than or equal to"
+ OPERATOR_WITHING_RANGE -> "Within range of"
+ OPERATOR_FIRST_RECORD -> "First record(i.e. oldest record)"
+ OPERATOR_LAST_RECORD -> "Last record (i.e. most recent record)"
+ else -> "Reserved for future use"
+ }
+ }
+
+ private fun getStatus(status: Int): String {
+ return when (status) {
+ RESPONSE_SUCCESS -> "Success"
+ RESPONSE_OP_CODE_NOT_SUPPORTED -> "Operation not supported"
+ RESPONSE_INVALID_OPERATOR -> "Invalid operator"
+ RESPONSE_OPERATOR_NOT_SUPPORTED -> "Operator not supported"
+ RESPONSE_INVALID_OPERAND -> "Invalid operand"
+ RESPONSE_NO_RECORDS_FOUND -> "No records found"
+ RESPONSE_ABORT_UNSUCCESSFUL -> "Abort unsuccessful"
+ RESPONSE_PROCEDURE_NOT_COMPLETED -> "Procedure not completed"
+ RESPONSE_OPERAND_NOT_SUPPORTED -> "Operand not supported"
+ else -> "Reserved for future use"
+ }
+ }
+}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt
new file mode 100644
index 00000000..3e5763b6
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt
@@ -0,0 +1,7 @@
+package no.nordicsemi.android.cgms.view
+
+import androidx.compose.runtime.Composable
+
+@Composable
+fun CGMScreen(finishAction: () -> Unit) {
+}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMScreenViewModel.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMScreenViewModel.kt
new file mode 100644
index 00000000..040d3ee1
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMScreenViewModel.kt
@@ -0,0 +1,14 @@
+package no.nordicsemi.android.cgms.viewmodel
+
+import dagger.hilt.android.lifecycle.HiltViewModel
+import no.nordicsemi.android.cgms.data.CGMDataHolder
+import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+internal class CGMScreenViewModel @Inject constructor(
+ private val dataHolder: CGMDataHolder
+) : CloseableViewModel() {
+
+ val state = dataHolder.data
+}
diff --git a/profile_cgms/src/test/java/no/nordicsemi/android/cgms/ExampleUnitTest.kt b/profile_cgms/src/test/java/no/nordicsemi/android/cgms/ExampleUnitTest.kt
new file mode 100644
index 00000000..307c6590
--- /dev/null
+++ b/profile_cgms/src/test/java/no/nordicsemi/android/cgms/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package no.nordicsemi.android.cgms
+
+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)
+ }
+}
\ No newline at end of file
diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt
index 14dbbbc6..34702b68 100644
--- a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt
+++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt
@@ -6,7 +6,6 @@ internal data class GLSData(
val records: List = emptyList(),
val batteryLevel: Int = 0,
val requestStatus: RequestStatus = RequestStatus.IDLE,
- val isDeviceBonded: Boolean = false,
val selectedMode: WorkingMode = WorkingMode.ALL
) {
fun modeItems(): List> {
diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt
index c047ddb3..d633aeb5 100644
--- a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt
+++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt
@@ -19,8 +19,8 @@ fun GLSScreen(finishAction: () -> Unit) {
val state = viewModel.state.collectAsState().value
val isScreenActive = viewModel.isActive.collectAsState().value
- LaunchedEffect(state.isDeviceBonded) {
- viewModel.bondDevice()
+ LaunchedEffect("connect") {
+ viewModel.connectDevice()
}
LaunchedEffect(isScreenActive) {
diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt
index e075f08c..f8a5364c 100644
--- a/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt
+++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt
@@ -43,15 +43,7 @@ internal class GLSViewModel @Inject constructor(
}.exhaustive
}
- fun bondDevice() {
- if (deviceHolder.isBondingRequired()) {
- deviceHolder.bondDevice()
- } else {
- connectDevice()
- }
- }
-
- private fun connectDevice() {
+ fun connectDevice() {
deviceHolder.device?.let {
glsManager.connect(it)
.useAutoConnect(false)
diff --git a/profile_prx/src/main/AndroidManifest.xml b/profile_prx/src/main/AndroidManifest.xml
index f322bb43..f7d522ad 100644
--- a/profile_prx/src/main/AndroidManifest.xml
+++ b/profile_prx/src/main/AndroidManifest.xml
@@ -2,4 +2,7 @@
+
+
+
diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXData.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXData.kt
index 926e6608..e3697e9b 100644
--- a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXData.kt
+++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXData.kt
@@ -1,10 +1,19 @@
package no.nordicsemi.android.prx.data
internal data class PRXData(
- private val batteryLevel: Int = 0,
- private val localAlarmLevel: AlarmLevel = AlarmLevel.NONE,
- private val remoteAlarmLevel: Boolean = false
-)
+ val batteryLevel: Int = 0,
+ val localAlarmLevel: AlarmLevel = AlarmLevel.NONE,
+ val isRemoteAlarm: Boolean = false
+) {
+
+ fun displayLocalAlarm(): String {
+ return when (localAlarmLevel) {
+ AlarmLevel.NONE -> "none"
+ AlarmLevel.MEDIUM -> "medium"
+ AlarmLevel.HIGH -> "height"
+ }
+ }
+}
internal enum class AlarmLevel(val value: Int) {
NONE(0x00),
diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXDataHolder.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXDataHolder.kt
index 4f2fab54..f9f6fb3d 100644
--- a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXDataHolder.kt
+++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXDataHolder.kt
@@ -21,7 +21,7 @@ internal class PRXDataHolder @Inject constructor() {
}
fun setRemoteAlarmLevel(isOn: Boolean) {
- _data.tryEmit(_data.value.copy(remoteAlarmLevel = isOn))
+ _data.tryEmit(_data.value.copy(isRemoteAlarm = isOn))
}
fun clear(){
diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt
index 0cc33481..120d9655 100644
--- a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt
+++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt
@@ -1,11 +1,48 @@
package no.nordicsemi.android.prx.view
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import no.nordicsemi.android.prx.R
import no.nordicsemi.android.prx.data.PRXData
+import no.nordicsemi.android.theme.view.BatteryLevelView
+import no.nordicsemi.android.theme.view.KeyValueField
+import no.nordicsemi.android.theme.view.ScreenSection
@Composable
internal fun ContentView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) {
+ ScreenSection {
+ Column {
+ KeyValueField(
+ stringResource(id = R.string.prx_is_remote_alarm),
+ state.isRemoteAlarm.toString()
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ KeyValueField(
+ stringResource(id = R.string.prx_local_alarm_level),
+ state.displayLocalAlarm()
+ )
+ }
+ }
- Text(text = "aa")
+ Spacer(modifier = Modifier.height(16.dp))
+
+ BatteryLevelView(state.batteryLevel)
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(
+ colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary),
+ onClick = { onEvent(DisconnectEvent) }
+ ) {
+ Text(text = stringResource(id = R.string.disconnect))
+ }
}
diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt
index dbf2981f..b9081b61 100644
--- a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt
+++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -45,7 +46,7 @@ fun PRXScreen(finishAction: () -> Unit) {
@Composable
private fun PRXView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) {
- Column {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
BackIconAppBar(stringResource(id = R.string.prx_title)) {
onEvent(DisconnectEvent)
}
@@ -56,6 +57,6 @@ private fun PRXView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) {
@Preview
@Composable
-private fun PRXViewPreview(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) {
- PRXView(state) { }
+private fun PRXViewPreview() {
+ PRXView(PRXData()) { }
}
diff --git a/profile_prx/src/main/res/values/strings.xml b/profile_prx/src/main/res/values/strings.xml
index 6d3258a2..290b4856 100644
--- a/profile_prx/src/main/res/values/strings.xml
+++ b/profile_prx/src/main/res/values/strings.xml
@@ -1,4 +1,7 @@
Proximity
+
+ Remote alarm
+ Local alarm level
diff --git a/settings.gradle b/settings.gradle
index a75b3116..ddf6ba2d 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -63,6 +63,7 @@ rootProject.name = "Android-nRF-Toolbox"
include ':app'
include ':profile_bps'
+include ':profile_cgms'
include ':profile_csc'
include ':profile_gls'
include ':profile_hrs'