mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-24 01:44:23 +01:00
Add first working example of CGM service in new approach
This commit is contained in:
@@ -88,6 +88,7 @@ dependencies {
|
||||
implementation libs.bundles.hilt
|
||||
kapt libs.bundles.hiltkapt
|
||||
|
||||
implementation libs.bundles.icons
|
||||
implementation libs.bundles.compose
|
||||
implementation libs.androidx.core
|
||||
implementation libs.material
|
||||
|
||||
@@ -8,6 +8,7 @@ import no.nordicsemi.android.hrs.view.HRSScreen
|
||||
import no.nordicsemi.android.hts.view.HTSScreen
|
||||
import no.nordicsemi.android.navigation.ComposeDestination
|
||||
import no.nordicsemi.android.navigation.ComposeDestinations
|
||||
import no.nordicsemi.android.nrftoolbox.view.HomeScreen
|
||||
import no.nordicsemi.android.prx.view.PRXScreen
|
||||
import no.nordicsemi.android.rscs.view.RSCSScreen
|
||||
import no.nordicsemi.android.uart.view.UARTScreen
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package no.nordicsemi.android.nrftoolbox
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import no.nordicsemi.android.navigation.NavigationManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val navigationManager: NavigationManager
|
||||
) : ViewModel() {
|
||||
|
||||
fun openProfile(destination: ProfileDestination) {
|
||||
navigationManager.navigateTo(destination.destination.id)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package no.nordicsemi.android.nrftoolbox
|
||||
package no.nordicsemi.android.nrftoolbox.view
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
@@ -6,19 +6,25 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.nrftoolbox.R
|
||||
import no.nordicsemi.android.theme.view.ScreenSection
|
||||
|
||||
@Composable
|
||||
@@ -26,6 +32,7 @@ fun FeatureButton(
|
||||
@DrawableRes iconId: Int,
|
||||
@StringRes nameCode: Int,
|
||||
@StringRes name: Int,
|
||||
isRunning: Boolean? = null,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
ScreenSection(onClick = onClick) {
|
||||
@@ -57,15 +64,32 @@ fun FeatureButton(
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = nameCode),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
// Text(
|
||||
// text = stringResource(id = nameCode),
|
||||
// style = MaterialTheme.typography.headlineSmall,
|
||||
// textAlign = TextAlign.Center
|
||||
// )
|
||||
|
||||
isRunning?.let {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_running_indicator),
|
||||
contentDescription = stringResource(id = R.string.running_profile_icon),
|
||||
tint = getRunningIndicatorColor(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getRunningIndicatorColor(isRunning: Boolean): Color {
|
||||
return if (isRunning) {
|
||||
colorResource(id = R.color.nordicGrass)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outline
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun FeatureButtonPreview() {
|
||||
@@ -1,4 +1,4 @@
|
||||
package no.nordicsemi.android.nrftoolbox
|
||||
package no.nordicsemi.android.nrftoolbox.view
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -6,17 +6,23 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.nrftoolbox.BuildConfig
|
||||
import no.nordicsemi.android.nrftoolbox.ProfileDestination
|
||||
import no.nordicsemi.android.nrftoolbox.R
|
||||
import no.nordicsemi.android.nrftoolbox.viewmodel.HomeViewModel
|
||||
import no.nordicsemi.android.theme.view.TitleAppBar
|
||||
|
||||
@Composable
|
||||
fun HomeScreen() {
|
||||
val viewModel: HomeViewModel = hiltViewModel()
|
||||
val state = viewModel.state.collectAsState().value
|
||||
|
||||
Column {
|
||||
TitleAppBar(stringResource(id = R.string.app_name))
|
||||
@@ -34,21 +40,15 @@ fun HomeScreen() {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.bluetooth_services),
|
||||
text = stringResource(id = R.string.viewmodel_profiles),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(R.drawable.ic_csc, R.string.csc_module, R.string.csc_module_full) {
|
||||
viewModel.openProfile(ProfileDestination.CSC)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(R.drawable.ic_hrs, R.string.hrs_module, R.string.hrs_module_full) {
|
||||
viewModel.openProfile(ProfileDestination.HRS)
|
||||
FeatureButton(R.drawable.ic_bps, R.string.bps_module, R.string.bps_module_full) {
|
||||
viewModel.openProfile(ProfileDestination.BPS)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -59,31 +59,45 @@ fun HomeScreen() {
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(R.drawable.ic_hts, R.string.hts_module, R.string.hts_module_full) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.service_profiles),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(R.drawable.ic_csc, R.string.csc_module, R.string.csc_module_full, state.isCSCModuleRunning) {
|
||||
viewModel.openProfile(ProfileDestination.CSC)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(R.drawable.ic_hrs, R.string.hrs_module, R.string.hrs_module_full, state.isHRSModuleRunning) {
|
||||
viewModel.openProfile(ProfileDestination.HRS)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(R.drawable.ic_hts, R.string.hts_module, R.string.hts_module_full, state.isHTSModuleRunning) {
|
||||
viewModel.openProfile(ProfileDestination.HTS)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(R.drawable.ic_bps, R.string.bps_module, R.string.bps_module_full) {
|
||||
viewModel.openProfile(ProfileDestination.BPS)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(R.drawable.ic_rscs, R.string.rscs_module, R.string.rscs_module_full) {
|
||||
FeatureButton(R.drawable.ic_rscs, R.string.rscs_module, R.string.rscs_module_full, state.isRSCSModuleRunning) {
|
||||
viewModel.openProfile(ProfileDestination.RSCS)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(R.drawable.ic_prx, R.string.prx_module, R.string.prx_module_full) {
|
||||
FeatureButton(R.drawable.ic_prx, R.string.prx_module, R.string.prx_module_full, state.isPRXModuleRunning) {
|
||||
viewModel.openProfile(ProfileDestination.PRX)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(R.drawable.ic_cgm, R.string.cgm_module, R.string.cgm_module_full) {
|
||||
FeatureButton(R.drawable.ic_cgm, R.string.cgm_module, R.string.cgm_module_full, state.isCGMModuleRunning) {
|
||||
viewModel.openProfile(ProfileDestination.CGMS)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package no.nordicsemi.android.nrftoolbox.view
|
||||
|
||||
data class HomeViewState(
|
||||
val isCSCModuleRunning: Boolean = false,
|
||||
val isHRSModuleRunning: Boolean = false,
|
||||
val isHTSModuleRunning: Boolean = false,
|
||||
val isRSCSModuleRunning: Boolean = false,
|
||||
val isPRXModuleRunning: Boolean = false,
|
||||
val isCGMModuleRunning: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
package no.nordicsemi.android.nrftoolbox.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.cgms.data.CGMRepository
|
||||
import no.nordicsemi.android.navigation.NavigationManager
|
||||
import no.nordicsemi.android.nrftoolbox.ProfileDestination
|
||||
import no.nordicsemi.android.nrftoolbox.view.HomeViewState
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val navigationManager: NavigationManager,
|
||||
private val cgmRepository: CGMRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(HomeViewState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
cgmRepository.isRunning.onEach {
|
||||
_state.value = _state.value.copy(isCGMModuleRunning = it)
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun openProfile(destination: ProfileDestination) {
|
||||
navigationManager.navigateTo(destination.destination.id)
|
||||
}
|
||||
}
|
||||
9
app/src/main/res/drawable/ic_running_indicator.xml
Normal file
9
app/src/main/res/drawable/ic_running_indicator.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,12m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
@@ -20,6 +20,9 @@
|
||||
<string name="dfu_module">DFU</string>
|
||||
<string name="dfu_module_full">Available in separate application.</string>
|
||||
|
||||
<string name="bluetooth_services">Bluetooth services</string>
|
||||
<string name="viewmodel_profiles">ViewModel profiles</string>
|
||||
<string name="service_profiles">Service profiles</string>
|
||||
<string name="utils_services">Utils services</string>
|
||||
|
||||
<string name="running_profile_icon">Icon indicating if the profile is running</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,10 +9,12 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HourglassTop
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -22,11 +24,12 @@ import no.nordicsemi.android.theme.R
|
||||
import no.nordicsemi.android.theme.view.ScreenSection
|
||||
|
||||
@Composable
|
||||
fun DeviceConnectingView() {
|
||||
fun DeviceConnectingView(navigateUp: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ScreenSection {
|
||||
Icon(
|
||||
@@ -64,11 +67,17 @@ fun DeviceConnectingView() {
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Button(onClick = { navigateUp() }) {
|
||||
Text(text = stringResource(id = R.string.disconnect))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun DeviceConnectingView_Preview() {
|
||||
DeviceConnectingView()
|
||||
DeviceConnectingView { }
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HighlightOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -22,11 +24,12 @@ enum class Reason {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeviceDisconnectedView(reason: Reason) {
|
||||
fun DeviceDisconnectedView(reason: Reason, navigateUp: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ScreenSection {
|
||||
Icon(
|
||||
@@ -62,11 +65,17 @@ fun DeviceDisconnectedView(reason: Reason) {
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Button(onClick = { navigateUp() }) {
|
||||
Text(text = stringResource(id = R.string.go_up))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun DeviceDisconnectedView_Preview() {
|
||||
DeviceConnectingView()
|
||||
DeviceConnectingView { }
|
||||
}
|
||||
|
||||
@@ -70,5 +70,5 @@ fun NoDeviceView() {
|
||||
@Preview
|
||||
@Composable
|
||||
fun NoDeviceView_Preview() {
|
||||
DeviceConnectingView()
|
||||
DeviceConnectingView { }
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
<string name="dialog">Dialog</string>
|
||||
<string name="cancel">CANCEL</string>
|
||||
|
||||
<string name="go_up">Go up</string>
|
||||
|
||||
<string name="close_app">Close the application.</string>
|
||||
<string name="back_screen">Close the current screen.</string>
|
||||
|
||||
<string name="disconnect">DISCONNECT</string>
|
||||
<string name="disconnect">Disconnect</string>
|
||||
<string name="field_battery">Battery</string>
|
||||
|
||||
<string name="device_disconnected">Disconnected</string>
|
||||
|
||||
@@ -24,6 +24,8 @@ fun BPSScreen() {
|
||||
val state = viewModel.state.collectAsState().value
|
||||
|
||||
Column {
|
||||
val navigateUp = { viewModel.onEvent(DisconnectEvent) }
|
||||
|
||||
BackIconAppBar(stringResource(id = R.string.bps_title)) {
|
||||
viewModel.onEvent(DisconnectEvent)
|
||||
}
|
||||
@@ -32,11 +34,11 @@ fun BPSScreen() {
|
||||
when (state) {
|
||||
NoDeviceState -> NoDeviceView()
|
||||
is WorkingState -> when (state.result) {
|
||||
is ConnectingResult -> DeviceConnectingView()
|
||||
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER)
|
||||
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS)
|
||||
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE)
|
||||
is ReadyResult -> DeviceConnectingView()
|
||||
is ConnectingResult,
|
||||
is ReadyResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
|
||||
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
|
||||
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
|
||||
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp)
|
||||
is SuccessResult -> BPSContentView(state.result.data) { viewModel.onEvent(it) }
|
||||
}
|
||||
}.exhaustive
|
||||
|
||||
@@ -2,24 +2,25 @@ package no.nordicsemi.android.cgms.data
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import no.nordicsemi.android.ble.ktx.suspend
|
||||
import no.nordicsemi.android.cgms.repository.CGMManager
|
||||
import no.nordicsemi.android.cgms.repository.CGMService
|
||||
import no.nordicsemi.android.service.BleManagerResult
|
||||
import no.nordicsemi.android.service.ConnectingResult
|
||||
import no.nordicsemi.android.service.ServiceManager
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
import java.lang.Exception
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class CGMRepository @Inject constructor(
|
||||
class CGMRepository @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
private val serviceManager: ServiceManager,
|
||||
@@ -27,38 +28,55 @@ internal class CGMRepository @Inject constructor(
|
||||
private var manager: CGMManager? = null
|
||||
|
||||
private val _data = MutableStateFlow<BleManagerResult<CGMData>>(ConnectingResult())
|
||||
val data = _data.asStateFlow()
|
||||
internal val data = _data.asStateFlow()
|
||||
|
||||
private val _isRunning = MutableStateFlow(false)
|
||||
val isRunning = _isRunning.asStateFlow()
|
||||
|
||||
fun launch(device: BluetoothDevice) {
|
||||
serviceManager.startService(CGMService::class.java, device)
|
||||
}
|
||||
|
||||
fun startManager(device: BluetoothDevice, scope: CoroutineScope) {
|
||||
fun start(device: BluetoothDevice, scope: CoroutineScope) {
|
||||
val manager = CGMManager(context, scope)
|
||||
|
||||
manager.dataHolder.status.onEach {
|
||||
_data.value = it
|
||||
Log.d("AAATESTAAA", "data: $it")
|
||||
}.launchIn(scope)
|
||||
|
||||
manager.connect(device)
|
||||
.useAutoConnect(false)
|
||||
.retry(3, 100)
|
||||
.enqueue()
|
||||
scope.launch {
|
||||
manager.start(device)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendNewServiceCommand(workingMode: CGMServiceCommand) {
|
||||
when (workingMode) {
|
||||
CGMServiceCommand.REQUEST_ALL_RECORDS -> manager?.requestAllRecords()
|
||||
CGMServiceCommand.REQUEST_LAST_RECORD -> manager?.requestLastRecord()
|
||||
CGMServiceCommand.REQUEST_FIRST_RECORD -> manager?.requestFirstRecord()
|
||||
CGMServiceCommand.DISCONNECT -> release()
|
||||
}.exhaustive
|
||||
private suspend fun CGMManager.start(device: BluetoothDevice) {
|
||||
try {
|
||||
connect(device)
|
||||
.useAutoConnect(false)
|
||||
.retry(3, 100)
|
||||
.suspend()
|
||||
_isRunning.value = true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun release() {
|
||||
fun requestAllRecords() {
|
||||
manager?.requestAllRecords()
|
||||
}
|
||||
|
||||
fun requestLastRecord() {
|
||||
manager?.requestLastRecord()
|
||||
}
|
||||
|
||||
fun requestFirstRecord() {
|
||||
manager?.requestFirstRecord()
|
||||
}
|
||||
|
||||
fun release() {
|
||||
serviceManager.stopService(CGMService::class.java)
|
||||
manager?.disconnect()?.enqueue()
|
||||
manager = null
|
||||
_isRunning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.ble.BleManager
|
||||
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointResponse
|
||||
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
|
||||
import no.nordicsemi.android.ble.common.callback.cgm.CGMFeatureResponse
|
||||
import no.nordicsemi.android.ble.common.callback.cgm.CGMSpecificOpsControlPointResponse
|
||||
import no.nordicsemi.android.ble.common.callback.cgm.CGMStatusResponse
|
||||
@@ -98,6 +99,11 @@ internal class CGMManager(
|
||||
return CGMManagerGattCallback()
|
||||
}
|
||||
|
||||
override fun log(priority: Int, message: String) {
|
||||
super.log(priority, message)
|
||||
Log.d("COROUTINE-EXCEPTION", message)
|
||||
}
|
||||
|
||||
private inner class CGMManagerGattCallback : BleManagerGattCallback() {
|
||||
override fun initialize() {
|
||||
super.initialize()
|
||||
@@ -169,6 +175,11 @@ internal class CGMManager(
|
||||
}
|
||||
}.launchIn(scope)
|
||||
|
||||
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>()
|
||||
.onEach {
|
||||
data.value = data.value.copy(batteryLevel = it.batteryLevel)
|
||||
}.launchIn(scope)
|
||||
|
||||
enableNotifications(cgmMeasurementCharacteristic).enqueue()
|
||||
enableIndications(cgmSpecificOpsControlPointCharacteristic).enqueue()
|
||||
enableIndications(recordAccessControlPointCharacteristic).enqueue()
|
||||
@@ -191,7 +202,9 @@ internal class CGMManager(
|
||||
val sequenceNumber = records.keyAt(records.size() - 1) + 1
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber),
|
||||
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
|
||||
sequenceNumber
|
||||
),
|
||||
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
||||
).suspend()
|
||||
} else {
|
||||
@@ -232,28 +245,31 @@ internal class CGMManager(
|
||||
}
|
||||
|
||||
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
|
||||
val service = gatt.getService(CGMS_SERVICE_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)
|
||||
gatt.getService(CGMS_SERVICE_UUID)?.run {
|
||||
cgmStatusCharacteristic = getCharacteristic(CGM_STATUS_UUID)
|
||||
cgmFeatureCharacteristic = getCharacteristic(CGM_FEATURE_UUID)
|
||||
cgmMeasurementCharacteristic = getCharacteristic(CGM_MEASUREMENT_UUID)
|
||||
cgmSpecificOpsControlPointCharacteristic = getCharacteristic(CGM_OPS_CONTROL_POINT_UUID)
|
||||
recordAccessControlPointCharacteristic = getCharacteristic(RACP_UUID)
|
||||
}
|
||||
return cgmMeasurementCharacteristic != null && cgmSpecificOpsControlPointCharacteristic != null && recordAccessControlPointCharacteristic != null && cgmStatusCharacteristic != null && cgmFeatureCharacteristic != null
|
||||
gatt.getService(BATTERY_SERVICE_UUID)?.run {
|
||||
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
|
||||
}
|
||||
return batteryLevelCharacteristic != null
|
||||
&& cgmMeasurementCharacteristic != null
|
||||
&& cgmSpecificOpsControlPointCharacteristic != null
|
||||
&& recordAccessControlPointCharacteristic != null
|
||||
&& cgmStatusCharacteristic != null
|
||||
&& cgmFeatureCharacteristic != null
|
||||
}
|
||||
|
||||
override fun onServicesInvalidated() {}
|
||||
|
||||
override fun onDeviceDisconnected() {
|
||||
super.onDeviceDisconnected()
|
||||
override fun onServicesInvalidated() {
|
||||
cgmStatusCharacteristic = null
|
||||
cgmFeatureCharacteristic = null
|
||||
cgmMeasurementCharacteristic = null
|
||||
cgmSpecificOpsControlPointCharacteristic = null
|
||||
recordAccessControlPointCharacteristic = null
|
||||
batteryLevelCharacteristic = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ internal class CGMService : NotificationService() {
|
||||
|
||||
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
|
||||
|
||||
repository.startManager(device, lifecycleScope)
|
||||
repository.start(device, lifecycleScope)
|
||||
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
@@ -24,19 +24,19 @@ fun CGMScreen() {
|
||||
val state = viewModel.state.collectAsState().value
|
||||
|
||||
Column {
|
||||
BackIconAppBar(stringResource(id = R.string.cgms_title)) {
|
||||
viewModel.onEvent(DisconnectEvent)
|
||||
}
|
||||
val navigateUp = { viewModel.onEvent(NavigateUp) }
|
||||
|
||||
BackIconAppBar(stringResource(id = R.string.cgms_title), navigateUp)
|
||||
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
when (state) {
|
||||
NoDeviceState -> NoDeviceView()
|
||||
is WorkingState -> when (state.result) {
|
||||
is ConnectingResult -> DeviceConnectingView()
|
||||
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER)
|
||||
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS)
|
||||
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE)
|
||||
is ReadyResult -> DeviceConnectingView()
|
||||
is ConnectingResult,
|
||||
is ReadyResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
|
||||
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
|
||||
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
|
||||
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp)
|
||||
is SuccessResult -> CGMContentView(state.result.data) { viewModel.onEvent(it) }
|
||||
}
|
||||
}.exhaustive
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package no.nordicsemi.android.cgms.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -29,6 +28,24 @@ internal class CGMScreenViewModel @Inject constructor(
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
if (!repository.isRunning.value) {
|
||||
requestBluetoothDevice()
|
||||
}
|
||||
|
||||
repository.data.onEach {
|
||||
_state.value = WorkingState(it)
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun onEvent(event: CGMViewEvent) {
|
||||
when (event) {
|
||||
DisconnectEvent -> disconnect()
|
||||
is OnWorkingModeSelected -> onCommandReceived(event.workingMode)
|
||||
NavigateUp -> navigationManager.navigateUp()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun requestBluetoothDevice() {
|
||||
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(CGMS_SERVICE_UUID))
|
||||
|
||||
navigationManager.recentResult.onEach {
|
||||
@@ -36,11 +53,6 @@ internal class CGMScreenViewModel @Inject constructor(
|
||||
handleArgs(it)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
repository.data.onEach {
|
||||
_state.value = WorkingState(it)
|
||||
Log.d("AAATESTAAA", "vm data: $it")
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun handleArgs(args: DestinationResult) {
|
||||
@@ -50,11 +62,12 @@ internal class CGMScreenViewModel @Inject constructor(
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
fun onEvent(event: CGMViewEvent) {
|
||||
when (event) {
|
||||
DisconnectEvent -> disconnect()
|
||||
is OnWorkingModeSelected -> repository.sendNewServiceCommand(event.workingMode)
|
||||
NavigateUp -> navigationManager.navigateUp()
|
||||
private fun onCommandReceived(workingMode: CGMServiceCommand) {
|
||||
when (workingMode) {
|
||||
CGMServiceCommand.REQUEST_ALL_RECORDS -> repository.requestAllRecords()
|
||||
CGMServiceCommand.REQUEST_LAST_RECORD -> repository.requestLastRecord()
|
||||
CGMServiceCommand.REQUEST_FIRST_RECORD -> repository.requestFirstRecord()
|
||||
CGMServiceCommand.DISCONNECT -> disconnect()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
@@ -63,6 +76,7 @@ internal class CGMScreenViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun disconnect() {
|
||||
repository.sendNewServiceCommand(CGMServiceCommand.DISCONNECT)
|
||||
repository.release()
|
||||
navigationManager.navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package no.nordicsemi.android.gls.main.view
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -25,21 +24,21 @@ fun GLSScreen() {
|
||||
val state = viewModel.state.collectAsState().value
|
||||
|
||||
Column {
|
||||
val navigateUp = { viewModel.onEvent(DisconnectEvent) }
|
||||
|
||||
BackIconAppBar(stringResource(id = R.string.gls_title)) {
|
||||
viewModel.onEvent(DisconnectEvent)
|
||||
}
|
||||
|
||||
Log.d("AAATESTAAA", "state: $state")
|
||||
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
when (state) {
|
||||
NoDeviceState -> NoDeviceView()
|
||||
is WorkingState -> when (state.result) {
|
||||
is ConnectingResult -> DeviceConnectingView()
|
||||
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER)
|
||||
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS)
|
||||
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE)
|
||||
is ReadyResult -> DeviceConnectingView()
|
||||
is ConnectingResult,
|
||||
is ReadyResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
|
||||
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
|
||||
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
|
||||
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp)
|
||||
is SuccessResult -> GLSContentView(state.result.data) { viewModel.onEvent(it) }
|
||||
}
|
||||
}.exhaustive
|
||||
|
||||
Reference in New Issue
Block a user