mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 07:24:22 +01:00
Add CGMS module
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
9
app/src/main/res/drawable/ic_cgm.xml
Normal file
9
app/src/main/res/drawable/ic_cgm.xml
Normal 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>
|
||||
@@ -6,4 +6,5 @@
|
||||
<string name="bps_module">BPS</string>
|
||||
<string name="rscs_module">RSCS</string>
|
||||
<string name="prx_module">PRX</string>
|
||||
<string name="cgm_module">CGMS</string>
|
||||
</resources>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,6 @@ enum class BluetoothPermissionState {
|
||||
BLUETOOTH_NOT_AVAILABLE,
|
||||
BLUETOOTH_NOT_ENABLED,
|
||||
DEVICE_NOT_CONNECTED,
|
||||
BONDING_REQUIRED,
|
||||
READY
|
||||
}
|
||||
|
||||
@@ -23,4 +23,8 @@
|
||||
<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">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>
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
profile_cgms/build.gradle
Normal file
26
profile_cgms/build.gradle
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
5
profile_cgms/src/main/AndroidManifest.xml
Normal file
5
profile_cgms/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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(")")
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package no.nordicsemi.android.cgms.view
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun CGMScreen(finishAction: () -> Unit) {
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ internal data class GLSData(
|
||||
val records: List<GLSRecord> = emptyList(),
|
||||
val batteryLevel: Int = 0,
|
||||
val requestStatus: RequestStatus = RequestStatus.IDLE,
|
||||
val isDeviceBonded: Boolean = false,
|
||||
val selectedMode: WorkingMode = WorkingMode.ALL
|
||||
) {
|
||||
fun modeItems(): List<RadioGroupItem<WorkingMode>> {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="no.nordicsemi.android.prx">
|
||||
|
||||
<application>
|
||||
<service android:name=".service.Hilt_PRXService"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()) { }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user