Add CGMS module

This commit is contained in:
Sylwester Zieliński
2021-10-18 09:25:44 +02:00
parent d6ccce9187
commit 670b711ec7
41 changed files with 1653 additions and 48 deletions

View File

@@ -52,6 +52,7 @@ dependencies {
//https://github.com/google/dagger/issues/2123 //https://github.com/google/dagger/issues/2123
implementation project(':profile_bps') implementation project(':profile_bps')
implementation project(':profile_csc') implementation project(':profile_csc')
implementation project(':profile_cgms')
implementation project(':profile_gls') implementation project(':profile_gls')
implementation project(':profile_hrs') implementation project(':profile_hrs')
implementation project(':profile_hts') implementation project(':profile_hts')
@@ -62,6 +63,7 @@ dependencies {
implementation project(':lib_permission') implementation project(':lib_permission')
implementation project(":lib_theme") implementation project(":lib_theme")
implementation project(":lib_utils") implementation project(":lib_utils")
implementation project(":lib_service")
implementation libs.nordic.ble.common implementation libs.nordic.ble.common

View File

@@ -6,6 +6,8 @@ import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height 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.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -26,10 +28,12 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import no.nordicsemi.android.bps.view.BPSScreen 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.csc.view.CSCScreen
import no.nordicsemi.android.gls.view.GLSScreen import no.nordicsemi.android.gls.view.GLSScreen
import no.nordicsemi.android.hrs.view.HRSScreen import no.nordicsemi.android.hrs.view.HRSScreen
import no.nordicsemi.android.hts.view.HTSScreen 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.BluetoothNotAvailableScreen
import no.nordicsemi.android.permission.view.BluetoothNotEnabledScreen import no.nordicsemi.android.permission.view.BluetoothNotEnabledScreen
import no.nordicsemi.android.permission.view.RequestPermissionScreen import no.nordicsemi.android.permission.view.RequestPermissionScreen
@@ -59,6 +63,7 @@ internal fun HomeScreen() {
composable(NavDestination.BPS.id) { BPSScreen { viewModel.navigateUp() } } composable(NavDestination.BPS.id) { BPSScreen { viewModel.navigateUp() } }
composable(NavDestination.PRX.id) { PRXScreen { viewModel.navigateUp() } } composable(NavDestination.PRX.id) { PRXScreen { viewModel.navigateUp() } }
composable(NavDestination.RSCS.id) { RSCSScreen { 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.REQUEST_PERMISSION.id) { RequestPermissionScreen(continueAction) }
composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen{ viewModel.finish() } } composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen{ viewModel.finish() } }
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) { composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
@@ -75,6 +80,7 @@ internal fun HomeScreen() {
}.exhaustive }.exhaustive
} }
} }
composable(NavDestination.BONDING.id) { BondingScreen(continueAction) }
} }
LaunchedEffect(state) { LaunchedEffect(state) {
@@ -90,19 +96,23 @@ fun HomeView(callback: (NavDestination) -> Unit) {
(context as? Activity)?.finish() (context as? Activity)?.finish()
} }
FeatureButton(R.drawable.ic_csc, R.string.csc_module) { callback(NavDestination.CSC) } Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Spacer(modifier = Modifier.height(1.dp)) FeatureButton(R.drawable.ic_csc, R.string.csc_module) { callback(NavDestination.CSC) }
FeatureButton(R.drawable.ic_hrs, R.string.hrs_module) { callback(NavDestination.HRS) } Spacer(modifier = Modifier.height(1.dp))
Spacer(modifier = Modifier.height(1.dp)) FeatureButton(R.drawable.ic_hrs, R.string.hrs_module) { callback(NavDestination.HRS) }
FeatureButton(R.drawable.ic_gls, R.string.gls_module) { callback(NavDestination.GLS) } Spacer(modifier = Modifier.height(1.dp))
Spacer(modifier = Modifier.height(1.dp)) FeatureButton(R.drawable.ic_gls, R.string.gls_module) { callback(NavDestination.GLS) }
FeatureButton(R.drawable.ic_hts, R.string.hts_module) { callback(NavDestination.HTS) } Spacer(modifier = Modifier.height(1.dp))
Spacer(modifier = Modifier.height(1.dp)) FeatureButton(R.drawable.ic_hts, R.string.hts_module) { callback(NavDestination.HTS) }
FeatureButton(R.drawable.ic_bps, R.string.bps_module) { callback(NavDestination.BPS) } Spacer(modifier = Modifier.height(1.dp))
Spacer(modifier = Modifier.height(1.dp)) FeatureButton(R.drawable.ic_bps, R.string.bps_module) { callback(NavDestination.BPS) }
FeatureButton(R.drawable.ic_rscs, R.string.rscs_module) { callback(NavDestination.RSCS) } Spacer(modifier = Modifier.height(1.dp))
Spacer(modifier = Modifier.height(1.dp)) FeatureButton(R.drawable.ic_rscs, R.string.rscs_module) { callback(NavDestination.RSCS) }
FeatureButton(R.drawable.ic_proximity, R.string.prx_module) { callback(NavDestination.PRX) } 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) }
}
} }
} }

View File

@@ -2,17 +2,19 @@ package no.nordicsemi.android.nrftoolbox
const val ARGS_KEY = "args" const val ARGS_KEY = "args"
enum class NavDestination(val id: String) { enum class NavDestination(val id: String, val pairingRequired: Boolean) {
HOME("home-screen"), HOME("home-screen", false),
CSC("csc-screen"), CSC("csc-screen", false),
HRS("hrs-screen"), HRS("hrs-screen", false),
HTS("hts-screen"), HTS("hts-screen", false),
GLS("gls-screen"), GLS("gls-screen", true),
BPS("bps-screen"), BPS("bps-screen", false),
PRX("prx-screen"), PRX("prx-screen", true),
RSCS("rscs-screen"), RSCS("rscs-screen", false),
REQUEST_PERMISSION("request-permission"), CGMS("cgms-screen", false),
BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"), REQUEST_PERMISSION("request-permission", false),
BLUETOOTH_NOT_ENABLED("bluetooth-not-enabled"), BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available", false),
DEVICE_NOT_CONNECTED("device-not-connected/{$ARGS_KEY}"); BLUETOOTH_NOT_ENABLED("bluetooth-not-enabled", false),
DEVICE_NOT_CONNECTED("device-not-connected/{$ARGS_KEY}", false),
BONDING("bonding", false);
} }

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import no.nordicsemi.android.bps.repository.BPS_SERVICE_UUID 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.csc.service.CYCLING_SPEED_AND_CADENCE_SERVICE_UUID
import no.nordicsemi.android.gls.repository.GLS_SERVICE_UUID import no.nordicsemi.android.gls.repository.GLS_SERVICE_UUID
import no.nordicsemi.android.hrs.service.HR_SERVICE_UUID import no.nordicsemi.android.hrs.service.HR_SERVICE_UUID
@@ -49,7 +50,13 @@ class NavigationViewModel @Inject constructor(
} else when (bleScanner.getBluetoothStatus()) { } else when (bleScanner.getBluetoothStatus()) {
ScannerStatus.NOT_AVAILABLE -> BluetoothPermissionState.BLUETOOTH_NOT_AVAILABLE ScannerStatus.NOT_AVAILABLE -> BluetoothPermissionState.BLUETOOTH_NOT_AVAILABLE
ScannerStatus.DISABLED -> BluetoothPermissionState.BLUETOOTH_NOT_ENABLED 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_AVAILABLE -> NavDestination.BLUETOOTH_NOT_AVAILABLE
BluetoothPermissionState.BLUETOOTH_NOT_ENABLED -> NavDestination.BLUETOOTH_NOT_ENABLED BluetoothPermissionState.BLUETOOTH_NOT_ENABLED -> NavDestination.BLUETOOTH_NOT_ENABLED
BluetoothPermissionState.DEVICE_NOT_CONNECTED -> NavDestination.DEVICE_NOT_CONNECTED BluetoothPermissionState.DEVICE_NOT_CONNECTED -> NavDestination.DEVICE_NOT_CONNECTED
BluetoothPermissionState.BONDING_REQUIRED -> NavDestination.BONDING
BluetoothPermissionState.READY -> targetDestination BluetoothPermissionState.READY -> targetDestination
} }
@@ -79,10 +87,12 @@ class NavigationViewModel @Inject constructor(
NavDestination.BPS -> BPS_SERVICE_UUID.toString() NavDestination.BPS -> BPS_SERVICE_UUID.toString()
NavDestination.RSCS -> RSCS_SERVICE_UUID.toString() NavDestination.RSCS -> RSCS_SERVICE_UUID.toString()
NavDestination.PRX -> IMMEDIATE_ALERT_SERVICE_UUID.toString() NavDestination.PRX -> IMMEDIATE_ALERT_SERVICE_UUID.toString()
NavDestination.CGMS -> CGMS_UUID.toString()
NavDestination.HOME, NavDestination.HOME,
NavDestination.REQUEST_PERMISSION, NavDestination.REQUEST_PERMISSION,
NavDestination.BLUETOOTH_NOT_AVAILABLE, NavDestination.BLUETOOTH_NOT_AVAILABLE,
NavDestination.BLUETOOTH_NOT_ENABLED, NavDestination.BLUETOOTH_NOT_ENABLED,
NavDestination.BONDING,
NavDestination.DEVICE_NOT_CONNECTED -> throw IllegalArgumentException("There is no serivce related to the destination: $destination") NavDestination.DEVICE_NOT_CONNECTED -> throw IllegalArgumentException("There is no serivce related to the destination: $destination")
} }
} }

View File

@@ -0,0 +1,9 @@
<vector android:height="80dp" android:viewportHeight="1024"
android:viewportWidth="1024" android:width="80dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00B3DC" android:pathData="M236.3,441.9c-77.1,0 -139.8,-61 -139.8,-135.9c0,-20.6 4.6,-40.4 13.8,-58.8c0,0 0,-0.1 0.1,-0.1c3.6,-7.2 7.8,-14.1 12.6,-20.6L212.1,83c5.8,-9.4 16.5,-14.6 27.5,-13.3c9.6,1.1 17.8,7 22.1,15.4c4.3,7.4 39.3,63.6 90,144.2c2.8,4 5.4,8.2 7.8,12.5c0.2,0.4 0.4,0.8 0.6,1.1c10.5,19.4 16,41.2 16,63.1C376.1,380.9 313.4,441.9 236.3,441.9zM161.3,272.6c-5.2,10.5 -7.8,21.7 -7.8,33.4c0,43.5 37.1,78.9 82.8,78.9s82.8,-35.4 82.8,-78.9c0,-12.8 -3.1,-25 -9.3,-36.3c0,0 0,0 0,0c0,0 0,0 0,-0.1c-1.5,-2.7 -3.2,-5.4 -5,-7.9c-0.3,-0.5 -0.6,-0.9 -0.9,-1.4c-11.6,-18.4 -34,-54.1 -53.9,-85.9c-5.3,-8.4 -9.8,-15.7 -13.8,-22.1l-65.4,105.3c-0.5,0.8 -1,1.5 -1.5,2.2c-3,4 -5.6,8.2 -7.8,12.5C161.4,272.4 161.4,272.5 161.3,272.6zM263.3,89C263.3,89 263.3,89 263.3,89C263.3,89 263.3,89 263.3,89zM263.3,88.9C263.3,88.9 263.3,89 263.3,88.9C263.3,89 263.3,88.9 263.3,88.9z"/>
<path android:fillColor="#00B3DC" android:pathData="M403.9,1011.3c-22.3,0 -43.8,-7.6 -61.4,-21.9c0,0 0,0 0,0L167.7,847c-41.7,-33.9 -47.9,-95.5 -14,-137.1L679.2,64.7C695.7,44.5 719,32 744.9,29.3c25.9,-2.6 51.3,5 71.5,21.4l174.8,142.4c20.2,16.4 32.8,39.8 35.4,65.7s-5,51.3 -21.4,71.5L479.6,975.4c-16.4,20.2 -39.8,32.8 -65.7,35.4C410.6,1011.2 407.2,1011.3 403.9,1011.3zM378.5,945.2c8.4,6.8 18.9,10 29.7,8.9c10.7,-1.1 20.4,-6.3 27.3,-14.7L961,294.2c6.8,-8.4 10,-18.9 8.9,-29.7c-1.1,-10.7 -6.3,-20.4 -14.7,-27.3L780.3,94.9c-8.4,-6.8 -18.9,-10 -29.7,-8.9c-10.8,1.1 -20.4,6.3 -27.3,14.7L197.9,745.9c-14.1,17.3 -11.5,42.8 5.8,56.9L378.5,945.2L378.5,945.2z"/>
<path android:fillColor="#00B3DC" android:pathData="M572,414.3m-41.9,0a41.9,41.9 0,1 1,83.8 0a41.9,41.9 0,1 1,-83.8 0"/>
<path android:fillColor="#00B3DC" android:pathData="M684.5,505.9m-41.9,0a41.9,41.9 0,1 1,83.8 0a41.9,41.9 0,1 1,-83.8 0"/>
<path android:fillColor="#00B3DC" android:pathData="M474.3,534.2m-41.9,0a41.9,41.9 0,1 1,83.8 0a41.9,41.9 0,1 1,-83.8 0"/>
<path android:fillColor="#00B3DC" android:pathData="M586.8,625.8m-41.9,0a41.9,41.9 0,1 1,83.8 0a41.9,41.9 0,1 1,-83.8 0"/>
</vector>

View File

@@ -6,4 +6,5 @@
<string name="bps_module">BPS</string> <string name="bps_module">BPS</string>
<string name="rscs_module">RSCS</string> <string name="rscs_module">RSCS</string>
<string name="prx_module">PRX</string> <string name="prx_module">PRX</string>
<string name="cgm_module">CGMS</string>
</resources> </resources>

View File

@@ -7,6 +7,7 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import no.nordicsemi.android.permission.bonding.repository.BondingStateObserver
import no.nordicsemi.android.permission.tools.PermissionHelper import no.nordicsemi.android.permission.tools.PermissionHelper
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
import javax.inject.Singleton import javax.inject.Singleton
@@ -31,4 +32,10 @@ internal object HiltModule {
fun createPermissionHelper(@ApplicationContext context: Context): PermissionHelper { fun createPermissionHelper(@ApplicationContext context: Context): PermissionHelper {
return PermissionHelper(context) return PermissionHelper(context)
} }
@Singleton
@Provides
fun createBondingStateObserver(@ApplicationContext context: Context): BondingStateObserver {
return BondingStateObserver(context)
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,5 +5,6 @@ enum class BluetoothPermissionState {
BLUETOOTH_NOT_AVAILABLE, BLUETOOTH_NOT_AVAILABLE,
BLUETOOTH_NOT_ENABLED, BLUETOOTH_NOT_ENABLED,
DEVICE_NOT_CONNECTED, DEVICE_NOT_CONNECTED,
BONDING_REQUIRED,
READY READY
} }

View File

@@ -23,4 +23,8 @@
<string name="scanner__bluetooth_not_enabled">Bluetooth not enabled.</string> <string name="scanner__bluetooth_not_enabled">Bluetooth not enabled.</string>
<string name="scanner__bluetooth_open_settings_info">To enable Bluetooth please open settings.</string> <string name="scanner__bluetooth_open_settings_info">To enable Bluetooth please open settings.</string>
<string name="scanner__bluetooth_open_settings">Open settings</string> <string name="scanner__bluetooth_open_settings">Open settings</string>
<string name="bonding_in_progress">Bonding in progress. Please follow instruction on the screen.</string>
<string name="bonding_success">Bonding success. Please wait for the redirect to chosen profile screen.</string>
<string name="bonding_error">We cannot get data from the peripheral without bonding. Please bond the device.</string>
</resources> </resources>

View File

@@ -10,6 +10,15 @@ class SelectedBluetoothDeviceHolder {
fun isBondingRequired(): Boolean { fun isBondingRequired(): Boolean {
return device?.bondState == BluetoothDevice.BOND_NONE 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() { fun bondDevice() {
device?.createBond() device?.createBond()
} }
@@ -22,3 +31,18 @@ class SelectedBluetoothDeviceHolder {
device = null 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")
}
}
}
}

26
profile_cgms/build.gradle Normal file
View File

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

View File

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

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="no.nordicsemi.android.cgms">
</manifest>

View File

@@ -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<CGMEvent>(Idle)
val data: StateFlow<CGMEvent> = _data
fun emitNewEvent(event: CGMEvent) {
_data.tryEmit(event)
}
fun clear() {
_data.tryEmit(Idle)
}
}

View File

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

View File

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

View File

@@ -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<CGMRecord> = SparseArray<CGMRecord>()
/** 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<CGMRecord> {
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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
package no.nordicsemi.android.cgms.view
import androidx.compose.runtime.Composable
@Composable
fun CGMScreen(finishAction: () -> Unit) {
}

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ internal data class GLSData(
val records: List<GLSRecord> = emptyList(), val records: List<GLSRecord> = emptyList(),
val batteryLevel: Int = 0, val batteryLevel: Int = 0,
val requestStatus: RequestStatus = RequestStatus.IDLE, val requestStatus: RequestStatus = RequestStatus.IDLE,
val isDeviceBonded: Boolean = false,
val selectedMode: WorkingMode = WorkingMode.ALL val selectedMode: WorkingMode = WorkingMode.ALL
) { ) {
fun modeItems(): List<RadioGroupItem<WorkingMode>> { fun modeItems(): List<RadioGroupItem<WorkingMode>> {

View File

@@ -19,8 +19,8 @@ fun GLSScreen(finishAction: () -> Unit) {
val state = viewModel.state.collectAsState().value val state = viewModel.state.collectAsState().value
val isScreenActive = viewModel.isActive.collectAsState().value val isScreenActive = viewModel.isActive.collectAsState().value
LaunchedEffect(state.isDeviceBonded) { LaunchedEffect("connect") {
viewModel.bondDevice() viewModel.connectDevice()
} }
LaunchedEffect(isScreenActive) { LaunchedEffect(isScreenActive) {

View File

@@ -43,15 +43,7 @@ internal class GLSViewModel @Inject constructor(
}.exhaustive }.exhaustive
} }
fun bondDevice() { fun connectDevice() {
if (deviceHolder.isBondingRequired()) {
deviceHolder.bondDevice()
} else {
connectDevice()
}
}
private fun connectDevice() {
deviceHolder.device?.let { deviceHolder.device?.let {
glsManager.connect(it) glsManager.connect(it)
.useAutoConnect(false) .useAutoConnect(false)

View File

@@ -2,4 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="no.nordicsemi.android.prx"> package="no.nordicsemi.android.prx">
<application>
<service android:name=".service.Hilt_PRXService"/>
</application>
</manifest> </manifest>

View File

@@ -1,10 +1,19 @@
package no.nordicsemi.android.prx.data package no.nordicsemi.android.prx.data
internal data class PRXData( internal data class PRXData(
private val batteryLevel: Int = 0, val batteryLevel: Int = 0,
private val localAlarmLevel: AlarmLevel = AlarmLevel.NONE, val localAlarmLevel: AlarmLevel = AlarmLevel.NONE,
private val remoteAlarmLevel: Boolean = false 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) { internal enum class AlarmLevel(val value: Int) {
NONE(0x00), NONE(0x00),

View File

@@ -21,7 +21,7 @@ internal class PRXDataHolder @Inject constructor() {
} }
fun setRemoteAlarmLevel(isOn: Boolean) { fun setRemoteAlarmLevel(isOn: Boolean) {
_data.tryEmit(_data.value.copy(remoteAlarmLevel = isOn)) _data.tryEmit(_data.value.copy(isRemoteAlarm = isOn))
} }
fun clear(){ fun clear(){

View File

@@ -1,11 +1,48 @@
package no.nordicsemi.android.prx.view 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.material.Text
import androidx.compose.runtime.Composable 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.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 @Composable
internal fun ContentView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) { 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))
}
} }

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -45,7 +46,7 @@ fun PRXScreen(finishAction: () -> Unit) {
@Composable @Composable
private fun PRXView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) { private fun PRXView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) {
Column { Column(horizontalAlignment = Alignment.CenterHorizontally) {
BackIconAppBar(stringResource(id = R.string.prx_title)) { BackIconAppBar(stringResource(id = R.string.prx_title)) {
onEvent(DisconnectEvent) onEvent(DisconnectEvent)
} }
@@ -56,6 +57,6 @@ private fun PRXView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) {
@Preview @Preview
@Composable @Composable
private fun PRXViewPreview(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) { private fun PRXViewPreview() {
PRXView(state) { } PRXView(PRXData()) { }
} }

View File

@@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="prx_title">Proximity</string> <string name="prx_title">Proximity</string>
<string name="prx_is_remote_alarm">Remote alarm</string>
<string name="prx_local_alarm_level">Local alarm level</string>
</resources> </resources>

View File

@@ -63,6 +63,7 @@ rootProject.name = "Android-nRF-Toolbox"
include ':app' include ':app'
include ':profile_bps' include ':profile_bps'
include ':profile_cgms'
include ':profile_csc' include ':profile_csc'
include ':profile_gls' include ':profile_gls'
include ':profile_hrs' include ':profile_hrs'