Finish migrating GLS profile

This commit is contained in:
Sylwester Zielinski
2023-03-16 09:40:23 +01:00
parent cc9d4d225a
commit e15ad2967e
21 changed files with 206 additions and 664 deletions

View File

@@ -46,7 +46,5 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Android operating system, and which are packaged with your app"s APK # Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete": # Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official

View File

@@ -110,10 +110,10 @@ internal class BPSViewModel @Inject constructor(
} }
} }
private fun startGattClient(blinkyDevice: ServerDevice) = viewModelScope.launch { private fun startGattClient(device: ServerDevice) = viewModelScope.launch {
_state.value = _state.value.copy(deviceName = blinkyDevice.name) _state.value = _state.value.copy(deviceName = device.name)
client = blinkyDevice.connect(context) client = device.connect(context)
client.connectionState client.connectionState
.filterNotNull() .filterNotNull()

View File

@@ -80,8 +80,8 @@ internal class CSCService : NotificationService() {
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch { private fun startGattClient(device: ServerDevice) = lifecycleScope.launch {
client = blinkyDevice.connect(this@CSCService) client = device.connect(this@CSCService)
client.connectionState client.connectionState
.onEach { repository.onConnectionStateChanged(it) } .onEach { repository.onConnectionStateChanged(it) }

View File

@@ -33,9 +33,10 @@ package no.nordicsemi.android.gls
import no.nordicsemi.android.common.navigation.createDestination import no.nordicsemi.android.common.navigation.createDestination
import no.nordicsemi.android.common.navigation.defineDestination import no.nordicsemi.android.common.navigation.defineDestination
import no.nordicsemi.android.gls.data.GLSRecord
import no.nordicsemi.android.gls.details.view.GLSDetailsScreen import no.nordicsemi.android.gls.details.view.GLSDetailsScreen
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext
internal val GlsDetailsDestinationId = createDestination<GLSRecord, Unit>("gls-details-screen") internal val GlsDetailsDestinationId = createDestination<Pair<GLSRecord, GLSMeasurementContext?>, Unit>("gls-details-screen")
val GLSDestination = defineDestination(GlsDetailsDestinationId) { GLSDetailsScreen() } val GLSDestination = defineDestination(GlsDetailsDestinationId) { GLSDetailsScreen() }

View File

@@ -1,74 +0,0 @@
/*
* Copyright (c) 2022, 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.gls.data
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextResponse
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementResponse
internal fun GlucoseMeasurementResponse.toRecord(): GLSRecord {
return this.let {
GLSRecord(
sequenceNumber = it.sequenceNumber,
time = it.time,
glucoseConcentration = it.glucoseConcentration ?: 0f,
unit = it.unit?.let { ConcentrationUnit.create(it) }
?: ConcentrationUnit.UNIT_KGPL,
type = RecordType.createOrNull(it.type),
sampleLocation = SampleLocation.createOrNull(it.sampleLocation),
status = it.status
)
}
}
internal fun GlucoseMeasurementContextResponse.toMeasurementContext(): MeasurementContext {
return this.let {
MeasurementContext(
sequenceNumber = it.sequenceNumber,
carbohydrate = it.carbohydrate,
carbohydrateAmount = it.carbohydrateAmount ?: 0f,
meal = it.meal,
tester = it.tester,
health = it.health,
exerciseDuration = it.exerciseDuration ?: 0,
exerciseIntensity = it.exerciseIntensity ?: 0,
medication = it.medication,
medicationQuantity = it.medicationAmount ?: 0f,
medicationUnit = it.medicationUnit?.let { MedicationUnit.create(it) }
?: MedicationUnit.UNIT_KG,
HbA1c = it.hbA1c ?: 0f
)
}
}
internal fun GLSRecord.copyWithNewContext(response: GlucoseMeasurementContextResponse): GLSRecord {
return copy(context = context)
}

View File

@@ -1,249 +0,0 @@
/*
* Copyright (c) 2022, 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.gls.data
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointResponse
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextResponse
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementResponse
import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.common.logger.NordicLogger
import no.nordicsemi.android.service.ConnectionObserverAdapter
import no.nordicsemi.android.utils.launchWithCatch
import java.util.*
val GLS_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb")
private val GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb")
private val GM_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb")
private val GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb")
private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
internal class GLSManager(
context: Context,
private val scope: CoroutineScope,
private val logger: NordicLogger
) : BleManager(context) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
private val data = MutableStateFlow(GLSServiceData())
val dataHolder = ConnectionObserverAdapter<GLSServiceData>()
init {
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)
}.launchIn(scope)
}
override fun log(priority: Int, message: String) {
logger.log(priority, message)
}
override fun getMinLogPriority(): Int {
return Log.VERBOSE
}
override fun getGattCallback(): BleManagerGattCallback {
return GlucoseManagerGattCallback()
}
private inner class GlucoseManagerGattCallback : BleManagerGattCallback() {
override fun initialize() {
super.initialize()
setNotificationCallback(glucoseMeasurementCharacteristic).asValidResponseFlow<GlucoseMeasurementResponse>()
.onEach { data.tryEmit(data.value.copy(records = data.value.records + it.toRecord())) }
.launchIn(scope)
setNotificationCallback(glucoseMeasurementContextCharacteristic).asValidResponseFlow<GlucoseMeasurementContextResponse>()
.onEach {
val context = it.toMeasurementContext()
data.value.records.find { context.sequenceNumber == it.sequenceNumber }?.let {
it.context = context
}
data.tryEmit(data.value)
}.launchIn(scope)
setIndicationCallback(recordAccessControlPointCharacteristic).asValidResponseFlow<RecordAccessControlPointResponse>()
.onEach {
if (it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords > 0) {
onNumberOfRecordsReceived(it)
} else if (it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords == 0) {
onRecordAccessOperationCompletedWithNoRecordsFound(it)
} else if (it.isOperationCompleted && it.wereRecordsFound()) {
onRecordAccessOperationCompleted(it)
} else if (it.errorCode > 0) {
onRecordAccessOperationError(it)
}
}.launchIn(scope)
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>()
.onEach {
data.value = data.value.copy(batteryLevel = it.batteryLevel)
}.launchIn(scope)
enableNotifications(glucoseMeasurementCharacteristic).enqueue()
enableNotifications(glucoseMeasurementContextCharacteristic).enqueue()
enableIndications(recordAccessControlPointCharacteristic).enqueue()
enableNotifications(batteryLevelCharacteristic).enqueue()
}
private fun onRecordAccessOperationCompleted(response: RecordAccessControlPointResponse) {
val status = when (response.requestCode) {
RecordAccessControlPointDataCallback.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
else -> RequestStatus.SUCCESS
}
data.tryEmit(data.value.copy(requestStatus = status))
}
private fun onRecordAccessOperationCompletedWithNoRecordsFound(response: RecordAccessControlPointResponse) {
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
}
private suspend fun onNumberOfRecordsReceived(response: RecordAccessControlPointResponse) {
if (response.numberOfRecords > 0) {
if (data.value.records.isNotEmpty()) {
val sequenceNumber = data.value.records.last().sequenceNumber + 1
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
sequenceNumber
),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
} else {
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportAllStoredRecords(),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
}
}
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
}
private fun onRecordAccessOperationError(response: RecordAccessControlPointResponse) {
log(Log.WARN, "Record Access operation failed (error ${response.errorCode})")
if (response.errorCode == RecordAccessControlPointDataCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
data.tryEmit(data.value.copy(requestStatus = RequestStatus.NOT_SUPPORTED))
} else {
data.tryEmit(data.value.copy(requestStatus = RequestStatus.FAILED))
}
}
public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
gatt.getService(GLS_SERVICE_UUID)?.run {
glucoseMeasurementCharacteristic = getCharacteristic(GM_CHARACTERISTIC)
glucoseMeasurementContextCharacteristic = getCharacteristic(GM_CONTEXT_CHARACTERISTIC)
recordAccessControlPointCharacteristic = getCharacteristic(RACP_CHARACTERISTIC)
}
gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return glucoseMeasurementCharacteristic != null && recordAccessControlPointCharacteristic != null
}
override fun onServicesInvalidated() {
glucoseMeasurementCharacteristic = null
glucoseMeasurementContextCharacteristic = null
recordAccessControlPointCharacteristic = null
}
}
private fun clear() {
data.tryEmit(data.value.copy(records = mapOf()))
val target = bluetoothDevice
if (target != null) {
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
}
}
fun requestLastRecord() {
if (recordAccessControlPointCharacteristic == null) return
val target = bluetoothDevice ?: return
clear()
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
scope.launchWithCatch {
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportLastStoredRecord(),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
}
}
fun requestFirstRecord() {
if (recordAccessControlPointCharacteristic == null) return
clear()
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
scope.launchWithCatch {
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportFirstStoredRecord(),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
}
}
fun requestAllRecords() {
if (recordAccessControlPointCharacteristic == null) return
clear()
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
scope.launchWithCatch {
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportNumberOfAllStoredRecords(),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
}
}
}

View File

@@ -1,127 +0,0 @@
/*
* Copyright (c) 2022, 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.gls.data
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementCallback.GlucoseStatus
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Carbohydrate
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Health
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Meal
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Medication
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Tester
import java.util.*
internal data class GLSRecord(
val sequenceNumber: Int = 0,
val time: Calendar? = null,
val glucoseConcentration: Float = 0f,
val unit: ConcentrationUnit = ConcentrationUnit.UNIT_KGPL,
val type: RecordType? = null,
val status: GlucoseStatus? = null,
val sampleLocation: SampleLocation? = null,
var context: MeasurementContext? = null
)
internal enum class RecordType(val id: Int) {
CAPILLARY_WHOLE_BLOOD(1),
CAPILLARY_PLASMA(2),
VENOUS_WHOLE_BLOOD(3),
VENOUS_PLASMA(4),
ARTERIAL_WHOLE_BLOOD(5),
ARTERIAL_PLASMA(6),
UNDETERMINED_WHOLE_BLOOD(7),
UNDETERMINED_PLASMA(8),
INTERSTITIAL_FLUID(9),
CONTROL_SOLUTION(10);
companion object {
fun create(value: Int): RecordType {
return values().firstOrNull { it.id == value.toInt() }
?: throw IllegalArgumentException("Cannot find element for provided value.")
}
fun createOrNull(value: Int?): RecordType? {
return values().firstOrNull { it.id == value }
}
}
}
internal data class MeasurementContext(
val sequenceNumber: Int = 0,
val carbohydrate: Carbohydrate? = null,
val carbohydrateAmount: Float = 0f,
val meal: Meal? = null,
val tester: Tester? = null,
val health: Health? = null,
val exerciseDuration: Int = 0,
val exerciseIntensity: Int = 0,
val medication: Medication?,
val medicationQuantity: Float = 0f,
val medicationUnit: MedicationUnit = MedicationUnit.UNIT_KG,
val HbA1c: Float = 0f
)
internal enum class ConcentrationUnit(val id: Int) {
UNIT_KGPL(0),
UNIT_MOLPL(1);
companion object {
fun create(value: Int): ConcentrationUnit {
return values().firstOrNull { it.id == value }
?: throw IllegalArgumentException("Cannot find element for provided value.")
}
}
}
internal enum class MedicationUnit(val id: Int) {
UNIT_KG(0),
UNIT_L(1);
companion object {
fun create(value: Int): MedicationUnit {
return values().firstOrNull { it.id == value }
?: throw IllegalArgumentException("Cannot find element for provided value.")
}
}
}
internal enum class SampleLocation(val id: Int) {
FINGER(1),
AST(2),
EARLOBE(3),
CONTROL_SOLUTION(4),
NOT_AVAILABLE(15);
companion object {
fun createOrNull(value: Int?): SampleLocation? {
return values().firstOrNull { it.id == value }
}
}
}

View File

@@ -32,10 +32,12 @@
package no.nordicsemi.android.gls.data package no.nordicsemi.android.gls.data
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GlucoseMeasurementContext import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus
internal data class GLSServiceData( internal data class GLSServiceData(
val records: Map<GLSRecord, GlucoseMeasurementContext?> = mapOf(), val records: Map<GLSRecord, GLSMeasurementContext?> = mapOf(),
val batteryLevel: Int? = null, val batteryLevel: Int? = null,
val connectionState: GattConnectionState? = null, val connectionState: GattConnectionState? = null,
val requestStatus: RequestStatus = RequestStatus.IDLE val requestStatus: RequestStatus = RequestStatus.IDLE

View File

@@ -1,36 +0,0 @@
/*
* Copyright (c) 2022, 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.gls.data
internal enum class RequestStatus {
IDLE, PENDING, SUCCESS, ABORTED, FAILED, NOT_SUPPORTED
}

View File

@@ -49,12 +49,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.data.GLSRecord
import no.nordicsemi.android.gls.main.view.toDisplayString import no.nordicsemi.android.gls.main.view.toDisplayString
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext
import no.nordicsemi.android.ui.view.ScreenSection import no.nordicsemi.android.ui.view.ScreenSection
@Composable @Composable
internal fun GLSDetailsContentView(record: GLSRecord) { internal fun GLSDetailsContentView(record: GLSRecord, context: GLSMeasurementContext?) {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
ScreenSection { ScreenSection {
@@ -86,24 +87,28 @@ internal fun GLSDetailsContentView(record: GLSRecord) {
Spacer(modifier = Modifier.size(4.dp)) Spacer(modifier = Modifier.size(4.dp))
} }
Row( record.glucoseConcentration?.let { glucoseConcentration ->
modifier = Modifier.fillMaxWidth(), record.unit?.let { unit ->
horizontalArrangement = Arrangement.SpaceBetween, Row(
verticalAlignment = Alignment.Bottom modifier = Modifier.fillMaxWidth(),
) { horizontalArrangement = Arrangement.SpaceBetween,
Text( verticalAlignment = Alignment.Bottom
text = stringResource(id = R.string.gls_details_glucose_condensation_title), ) {
style = MaterialTheme.typography.bodyMedium, Text(
color = MaterialTheme.colorScheme.outline text = stringResource(id = R.string.gls_details_glucose_condensation_title),
) style = MaterialTheme.typography.bodyMedium,
Text( color = MaterialTheme.colorScheme.outline
text = stringResource( )
id = R.string.gls_details_glucose_condensation_field, Text(
record.glucoseConcentration, text = stringResource(
record.unit.toDisplayString() id = R.string.gls_details_glucose_condensation_field,
), glucoseConcentration,
style = MaterialTheme.typography.titleLarge unit.toDisplayString()
) ),
style = MaterialTheme.typography.titleLarge
)
}
}
} }
record.status?.let { record.status?.let {
@@ -172,7 +177,7 @@ internal fun GLSDetailsContentView(record: GLSRecord) {
Spacer(modifier = Modifier.size(4.dp)) Spacer(modifier = Modifier.size(4.dp))
} }
record.context?.let { context?.let {
Divider( Divider(
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
thickness = 1.dp, thickness = 1.dp,
@@ -209,33 +214,42 @@ internal fun GLSDetailsContentView(record: GLSRecord) {
) )
Spacer(modifier = Modifier.size(4.dp)) Spacer(modifier = Modifier.size(4.dp))
} }
Field( it.exerciseDuration?.let { exerciseDuration ->
stringResource(id = R.string.gls_context_exercise_title), it.exerciseIntensity?.let { exerciseIntensity ->
stringResource( Field(
id = R.string.gls_context_exercise_field, stringResource(id = R.string.gls_context_exercise_title),
it.exerciseDuration, stringResource(
it.exerciseIntensity id = R.string.gls_context_exercise_field,
exerciseDuration,
exerciseIntensity
)
)
}
}
it.medicationUnit?.let { medicationUnit ->
Spacer(modifier = Modifier.size(4.dp))
val medicationField = String.format(
stringResource(id = R.string.gls_context_medication_field),
it.medicationQuantity,
medicationUnit.toDisplayString(),
it.medication?.toDisplayString()
) )
) Field(
Spacer(modifier = Modifier.size(4.dp)) stringResource(id = R.string.gls_context_medication_title),
medicationField
)
}
val medicationField = String.format( it.HbA1c?.let { hbA1c ->
stringResource(id = R.string.gls_context_medication_field), Spacer(modifier = Modifier.size(4.dp))
it.medicationQuantity, Field(
it.medicationUnit.toDisplayString(), stringResource(id = R.string.gls_context_hba1c_title),
it.medication?.toDisplayString() stringResource(id = R.string.gls_context_hba1c_field, hbA1c)
) )
Field( }
stringResource(id = R.string.gls_context_medication_title),
medicationField
)
Spacer(modifier = Modifier.size(4.dp)) Spacer(modifier = Modifier.size(4.dp))
Field(
stringResource(id = R.string.gls_context_hba1c_title),
stringResource(id = R.string.gls_context_hba1c_field, it.HbA1c)
)
Spacer(modifier = Modifier.size(4.dp))
} ?: Field( } ?: Field(
stringResource(id = R.string.gls_context_title), stringResource(id = R.string.gls_context_title),
stringResource(id = R.string.gls_unavailable) stringResource(id = R.string.gls_unavailable)

View File

@@ -33,15 +33,15 @@ package no.nordicsemi.android.gls.details.view
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Carbohydrate
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Health
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Meal
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Medication
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Tester
import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.data.ConcentrationUnit import no.nordicsemi.android.kotlin.ble.profile.gls.data.Carbohydrate
import no.nordicsemi.android.gls.data.MedicationUnit import no.nordicsemi.android.kotlin.ble.profile.gls.data.ConcentrationUnit
import no.nordicsemi.android.gls.data.SampleLocation import no.nordicsemi.android.kotlin.ble.profile.gls.data.Health
import no.nordicsemi.android.kotlin.ble.profile.gls.data.Meal
import no.nordicsemi.android.kotlin.ble.profile.gls.data.Medication
import no.nordicsemi.android.kotlin.ble.profile.gls.data.MedicationUnit
import no.nordicsemi.android.kotlin.ble.profile.gls.data.SampleLocation
import no.nordicsemi.android.kotlin.ble.profile.gls.data.Tester
@Composable @Composable
internal fun SampleLocation.toDisplayString(): String { internal fun SampleLocation.toDisplayString(): String {
@@ -65,8 +65,8 @@ internal fun ConcentrationUnit.toDisplayString(): String {
@Composable @Composable
internal fun MedicationUnit.toDisplayString(): String { internal fun MedicationUnit.toDisplayString(): String {
return when (this) { return when (this) {
MedicationUnit.UNIT_KG -> stringResource(id = R.string.gls_sample_location_kg) MedicationUnit.UNIT_MG -> stringResource(id = R.string.gls_sample_location_kg)
MedicationUnit.UNIT_L -> stringResource(id = R.string.gls_sample_location_l) MedicationUnit.UNIT_ML -> stringResource(id = R.string.gls_sample_location_l)
} }
} }

View File

@@ -50,6 +50,6 @@ internal fun GLSDetailsScreen() {
viewModel.navigateBack() viewModel.navigateBack()
} }
GLSDetailsContentView(record) GLSDetailsContentView(record.first, record.second)
} }
} }

View File

@@ -58,10 +58,10 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.data.GLSServiceData import no.nordicsemi.android.gls.data.GLSServiceData
import no.nordicsemi.android.gls.data.GLSRecord
import no.nordicsemi.android.gls.data.RequestStatus
import no.nordicsemi.android.gls.data.WorkingMode import no.nordicsemi.android.gls.data.WorkingMode
import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus
import no.nordicsemi.android.ui.view.BatteryLevelView import no.nordicsemi.android.ui.view.BatteryLevelView
import no.nordicsemi.android.ui.view.ScreenSection import no.nordicsemi.android.ui.view.ScreenSection
import no.nordicsemi.android.ui.view.SectionTitle import no.nordicsemi.android.ui.view.SectionTitle
@@ -139,7 +139,7 @@ private fun RecordsViewWithData(state: GLSServiceData) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
state.records.forEachIndexed { i, it -> state.records.keys.forEachIndexed { i, it ->
RecordItem(it) RecordItem(it)
if (i < state.records.size - 1) { if (i < state.records.size - 1) {
@@ -184,13 +184,12 @@ private fun RecordItem(record: GLSRecord) {
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
Text( record.glucoseConcentration?.let { glucoseConcentration -> record.unit?.let { unit ->
text = glucoseConcentrationDisplayValue( Text(
record.glucoseConcentration, text = glucoseConcentrationDisplayValue(glucoseConcentration, unit),
record.unit style = MaterialTheme.typography.labelLarge,
), )
style = MaterialTheme.typography.labelLarge, } }
)
} }
} }
} }

View File

@@ -34,9 +34,9 @@ package no.nordicsemi.android.gls.main.view
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.data.ConcentrationUnit
import no.nordicsemi.android.gls.data.RecordType
import no.nordicsemi.android.gls.data.WorkingMode import no.nordicsemi.android.gls.data.WorkingMode
import no.nordicsemi.android.kotlin.ble.profile.gls.data.ConcentrationUnit
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RecordType
@Composable @Composable
internal fun RecordType?.toDisplayString(): String { internal fun RecordType?.toDisplayString(): String {

View File

@@ -48,15 +48,7 @@ import no.nordicsemi.android.common.ui.scanner.view.DeviceDisconnectedView
import no.nordicsemi.android.common.ui.scanner.view.Reason import no.nordicsemi.android.common.ui.scanner.view.Reason
import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel
import no.nordicsemi.android.service.ConnectedResult import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.DeviceHolder
import no.nordicsemi.android.service.DisconnectedResult
import no.nordicsemi.android.service.IdleResult
import no.nordicsemi.android.service.LinkLossResult
import no.nordicsemi.android.service.MissingServiceResult
import no.nordicsemi.android.service.SuccessResult
import no.nordicsemi.android.service.UnknownErrorResult
import no.nordicsemi.android.ui.view.BackIconAppBar import no.nordicsemi.android.ui.view.BackIconAppBar
import no.nordicsemi.android.ui.view.LoggerIconAppBar import no.nordicsemi.android.ui.view.LoggerIconAppBar
import no.nordicsemi.android.ui.view.NavigateUpButton import no.nordicsemi.android.ui.view.NavigateUpButton
@@ -78,19 +70,15 @@ fun GLSScreen() {
.padding(16.dp) .padding(16.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
when (state) { if (state.deviceName == null) {
NoDeviceState -> DeviceConnectingView() DeviceConnectingView()
is WorkingState -> when (state.result) { } else {
is IdleResult, when (state.glsServiceData.connectionState) {
is ConnectingResult -> DeviceConnectingView { NavigateUpButton(navigateUp) } null,
is ConnectedResult -> DeviceConnectingView { NavigateUpButton(navigateUp) } GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER) { NavigateUpButton(navigateUp) } GattConnectionState.STATE_DISCONNECTED,
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS) { NavigateUpButton(navigateUp) } GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE) { GattConnectionState.STATE_CONNECTED -> GLSContentView(state.glsServiceData) { viewModel.onEvent(it) }
NavigateUpButton(navigateUp)
}
is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
is SuccessResult -> GLSContentView(state.result.data) { viewModel.onEvent(it) }
} }
} }
} }
@@ -99,14 +87,10 @@ fun GLSScreen() {
@Composable @Composable
private fun AppBar(state: GLSViewState, navigateUp: () -> Unit, viewModel: GLSViewModel) { private fun AppBar(state: GLSViewState, navigateUp: () -> Unit, viewModel: GLSViewModel) {
val toolbarName = (state as? WorkingState)?.let { if (state.deviceName == null) {
(it.result as? DeviceHolder)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.gls_title), navigateUp) BackIconAppBar(stringResource(id = R.string.gls_title), navigateUp)
} else { } else {
LoggerIconAppBar(toolbarName, { LoggerIconAppBar(state.deviceName, {
viewModel.onEvent(DisconnectEvent) viewModel.onEvent(DisconnectEvent)
}, { viewModel.onEvent(DisconnectEvent) }) { }, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent) viewModel.onEvent(OpenLoggerEvent)

View File

@@ -31,8 +31,8 @@
package no.nordicsemi.android.gls.main.view package no.nordicsemi.android.gls.main.view
import no.nordicsemi.android.gls.data.GLSRecord
import no.nordicsemi.android.gls.data.WorkingMode import no.nordicsemi.android.gls.data.WorkingMode
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord
internal sealed class GLSScreenViewEvent internal sealed class GLSScreenViewEvent

View File

@@ -32,13 +32,20 @@
package no.nordicsemi.android.gls.main.view package no.nordicsemi.android.gls.main.view
import no.nordicsemi.android.gls.data.GLSServiceData import no.nordicsemi.android.gls.data.GLSServiceData
import no.nordicsemi.android.gls.data.RequestStatus import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus
internal data class GLSViewState( internal data class GLSViewState(
val glsServiceData: GLSServiceData = GLSServiceData(), val glsServiceData: GLSServiceData = GLSServiceData(),
val deviceName: String? = null val deviceName: String? = null
) { ) {
fun copyWithNewConnectionState(connectionState: GattConnectionState): GLSViewState {
return copy(glsServiceData = glsServiceData.copy(connectionState = connectionState))
}
fun copyAndClear(): GLSViewState { fun copyAndClear(): GLSViewState {
return copy(glsServiceData = glsServiceData.copy(records = mapOf(), requestStatus = RequestStatus.IDLE)) return copy(glsServiceData = glsServiceData.copy(records = mapOf(), requestStatus = RequestStatus.IDLE))
} }
@@ -46,4 +53,24 @@ internal data class GLSViewState(
fun copyWithNewRequestStatus(requestStatus: RequestStatus): GLSViewState { fun copyWithNewRequestStatus(requestStatus: RequestStatus): GLSViewState {
return copy(glsServiceData = glsServiceData.copy(requestStatus = requestStatus)) return copy(glsServiceData = glsServiceData.copy(requestStatus = requestStatus))
} }
fun copyWithNewBatteryLevel(batteryLevel: Int): GLSViewState {
return copy(glsServiceData = glsServiceData.copy(batteryLevel = batteryLevel))
}
//todo optimise
fun copyWithNewRecord(record: GLSRecord): GLSViewState {
val records = glsServiceData.records.toMutableMap()
records[record] = null
return copy(glsServiceData = glsServiceData.copy(records = records.toMap()))
}
//todo optimise
fun copyWithNewContext(context: GLSMeasurementContext): GLSViewState {
val records = glsServiceData.records.toMutableMap()
return records.keys.firstOrNull { it.sequenceNumber == context.sequenceNumber }?.let {
records[it] = context
copy(glsServiceData = glsServiceData.copy(records = records.toMap()))
} ?: this
}
} }

View File

@@ -48,13 +48,9 @@ import kotlinx.coroutines.launch
import no.nordicsemi.android.analytics.AppAnalytics import no.nordicsemi.android.analytics.AppAnalytics
import no.nordicsemi.android.analytics.Profile import no.nordicsemi.android.analytics.Profile
import no.nordicsemi.android.analytics.ProfileConnectedEvent import no.nordicsemi.android.analytics.ProfileConnectedEvent
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointResponse
import no.nordicsemi.android.common.navigation.NavigationResult import no.nordicsemi.android.common.navigation.NavigationResult
import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.common.navigation.Navigator
import no.nordicsemi.android.gls.GlsDetailsDestinationId import no.nordicsemi.android.gls.GlsDetailsDestinationId
import no.nordicsemi.android.gls.data.GLS_SERVICE_UUID
import no.nordicsemi.android.gls.data.RequestStatus
import no.nordicsemi.android.gls.data.WorkingMode import no.nordicsemi.android.gls.data.WorkingMode
import no.nordicsemi.android.gls.main.view.DisconnectEvent import no.nordicsemi.android.gls.main.view.DisconnectEvent
import no.nordicsemi.android.gls.main.view.GLSScreenViewEvent import no.nordicsemi.android.gls.main.view.GLSScreenViewEvent
@@ -72,10 +68,13 @@ import no.nordicsemi.android.kotlin.ble.profile.gls.GlucoseMeasurementContextPar
import no.nordicsemi.android.kotlin.ble.profile.gls.GlucoseMeasurementParser import no.nordicsemi.android.kotlin.ble.profile.gls.GlucoseMeasurementParser
import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointInputParser import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointInputParser
import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointParser import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointParser
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord
import no.nordicsemi.android.kotlin.ble.profile.gls.data.NumberOfRecordsData import no.nordicsemi.android.kotlin.ble.profile.gls.data.NumberOfRecordsData
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RecordAccessControlPointData import no.nordicsemi.android.kotlin.ble.profile.gls.data.RecordAccessControlPointData
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus
import no.nordicsemi.android.kotlin.ble.profile.gls.data.ResponseData import no.nordicsemi.android.kotlin.ble.profile.gls.data.ResponseData
import no.nordicsemi.android.service.ConnectedResult import no.nordicsemi.android.kotlin.ble.profile.racp.RACPOpCode
import no.nordicsemi.android.kotlin.ble.profile.racp.RACPResponseCode
import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@@ -102,12 +101,14 @@ internal class GLSViewModel @Inject constructor(
private lateinit var client: BleGattClient private lateinit var client: BleGattClient
private lateinit var glucoseMeasurementCharacteristic: BleGattCharacteristic private lateinit var glucoseMeasurementCharacteristic: BleGattCharacteristic
private lateinit var glucoseMeasurementContextCharacteristic: BleGattCharacteristic
private lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic private lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic
private val _state = MutableStateFlow(GLSViewState()) private val _state = MutableStateFlow(GLSViewState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
private val highestSequenceNumber
get() = state.value.glsServiceData.records.keys.maxByOrNull { it.sequenceNumber }?.sequenceNumber ?: -1
init { init {
navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(GLS_SERVICE_UUID)) navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(GLS_SERVICE_UUID))
@@ -125,16 +126,20 @@ internal class GLSViewModel @Inject constructor(
fun onEvent(event: GLSScreenViewEvent) { fun onEvent(event: GLSScreenViewEvent) {
when (event) { when (event) {
OpenLoggerEvent -> repository.openLogger() OpenLoggerEvent -> TODO()
DisconnectEvent -> navigationManager.navigateUp() DisconnectEvent -> navigationManager.navigateUp()
is OnWorkingModeSelected -> repository.requestMode(event.workingMode) is OnWorkingModeSelected -> onEvent(event)
is OnGLSRecordClick -> navigationManager.navigateTo(GlsDetailsDestinationId, event.record) is OnGLSRecordClick -> navigateToDetails(event.record)
DisconnectEvent -> navigationManager.navigateUp() DisconnectEvent -> navigationManager.navigateUp()
} }
} }
private fun navigateToDetails(record: GLSRecord) {
val context = state.value.glsServiceData.records[record]
navigationManager.navigateTo(GlsDetailsDestinationId, record to context)
}
private fun onDeviceSelected(device: ServerDevice) { private fun onDeviceSelected(device: ServerDevice) {
_state.value = _state.value.copy(deviceName = device.name)
startGattClient(device) startGattClient(device)
} }
@@ -146,100 +151,98 @@ internal class GLSViewModel @Inject constructor(
} }
} }
private fun connectDevice(device: ServerDevice) { private fun startGattClient(device: ServerDevice) = viewModelScope.launch {
repository.downloadData(viewModelScope, device).onEach { client = device.connect(context)
_state.value = WorkingState(it)
(it as? ConnectedResult)?.let {
analytics.logEvent(ProfileConnectedEvent(Profile.GLS))
}
}.launchIn(viewModelScope)
}
private fun startGattClient(blinkyDevice: ServerDevice) = viewModelScope.launch {
client = blinkyDevice.connect(context)
client.connectionState client.connectionState
.onEach { _state.value = _state.value.copy() }
.filterNotNull() .filterNotNull()
.onEach { _state.value = _state.value.copyWithNewConnectionState(it) }
.onEach { stopIfDisconnected(it) } .onEach { stopIfDisconnected(it) }
.onEach { logAnalytics(it) }
.launchIn(viewModelScope) .launchIn(viewModelScope)
client.services client.services
.filterNotNull() .filterNotNull()
.onEach { configureGatt(it) } .onEach { configureGatt(it, device) }
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
private suspend fun configureGatt(services: BleGattServices) { private fun logAnalytics(connectionState: GattConnectionState) {
if (connectionState == GattConnectionState.STATE_CONNECTED) {
analytics.logEvent(ProfileConnectedEvent(Profile.GLS))
}
}
private suspend fun configureGatt(services: BleGattServices, device: ServerDevice) {
val glsService = services.findService(GLS_SERVICE_UUID)!! val glsService = services.findService(GLS_SERVICE_UUID)!!
glucoseMeasurementCharacteristic = glsService.findCharacteristic(GM_CHARACTERISTIC)!! glucoseMeasurementCharacteristic = glsService.findCharacteristic(GM_CHARACTERISTIC)!!
glucoseMeasurementContextCharacteristic = glsService.findCharacteristic(GM_CONTEXT_CHARACTERISTIC)!!
recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!! recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!!
val batteryService = services.findService(BATTERY_SERVICE_UUID)!! val batteryService = services.findService(BATTERY_SERVICE_UUID)!!
val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!! val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!!
batteryLevelCharacteristic.getNotifications() batteryLevelCharacteristic.getNotifications()
.mapNotNull { BatteryLevelParser.parse(it) } .mapNotNull { BatteryLevelParser.parse(it) }
.onEach { repository.onBatteryLevelChanged(it) } .onEach { _state.value = _state.value.copyWithNewBatteryLevel(it) }
.launchIn(viewModelScope) .launchIn(viewModelScope)
glucoseMeasurementCharacteristic.getNotifications() glucoseMeasurementCharacteristic.getNotifications()
.mapNotNull { GlucoseMeasurementParser.parse(it) } .mapNotNull { GlucoseMeasurementParser.parse(it) }
.onEach { } .onEach { _state.value = _state.value.copyWithNewRecord(it) }
.launchIn(viewModelScope) .launchIn(viewModelScope)
glucoseMeasurementContextCharacteristic.getNotifications() glsService.findCharacteristic(GM_CONTEXT_CHARACTERISTIC)?.getNotifications()
.mapNotNull { GlucoseMeasurementContextParser.parse(it) } ?.mapNotNull { GlucoseMeasurementContextParser.parse(it) }
.onEach { } ?.onEach { _state.value = _state.value.copyWithNewContext(it) }
.launchIn(viewModelScope) ?.launchIn(viewModelScope)
recordAccessControlPointCharacteristic.getNotifications() recordAccessControlPointCharacteristic.getNotifications()
.mapNotNull { RecordAccessControlPointParser.parse(it) } .mapNotNull { RecordAccessControlPointParser.parse(it) }
.onEach { onAccessControlPointDataReceived(it) } .onEach { onAccessControlPointDataReceived(it) }
.launchIn(viewModelScope) .launchIn(viewModelScope)
_state.value = _state.value.copy(deviceName = device.name)
} }
private fun stopIfDisconnected(connectionState: GattConnectionState) { private fun stopIfDisconnected(connectionState: GattConnectionState) {
if (connectionState == GattConnectionState.STATE_DISCONNECTED) { if (connectionState == GattConnectionState.STATE_DISCONNECTED) {
stopSelf() navigationManager.navigateUp()
} }
} }
private fun onAccessControlPointDataReceived(data: RecordAccessControlPointData) { private fun onAccessControlPointDataReceived(data: RecordAccessControlPointData) = viewModelScope.launch {
when (data) { when (data) {
is NumberOfRecordsData -> if () is NumberOfRecordsData -> onNumberOfRecordsReceived(data.numberOfRecords)
is ResponseData -> TODO() is ResponseData -> when (data.responseCode) {
} RACPResponseCode.RACP_RESPONSE_SUCCESS -> onRecordAccessOperationCompleted(data.requestCode)
if (it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords > 0) { RACPResponseCode.RACP_ERROR_NO_RECORDS_FOUND -> onRecordAccessOperationCompletedWithNoRecordsFound()
onNumberOfRecordsReceived(it) RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED,
} else if (it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords == 0) { RACPResponseCode.RACP_ERROR_INVALID_OPERATOR,
onRecordAccessOperationCompletedWithNoRecordsFound(it) RACPResponseCode.RACP_ERROR_OPERATOR_NOT_SUPPORTED,
} else if (it.isOperationCompleted && it.wereRecordsFound()) { RACPResponseCode.RACP_ERROR_INVALID_OPERAND,
onRecordAccessOperationCompleted(it) RACPResponseCode.RACP_ERROR_ABORT_UNSUCCESSFUL,
} else if (it.errorCode > 0) { RACPResponseCode.RACP_ERROR_PROCEDURE_NOT_COMPLETED,
onRecordAccessOperationError(it) RACPResponseCode.RACP_ERROR_OPERAND_NOT_SUPPORTED -> onRecordAccessOperationError(data.responseCode)
}
} }
} }
private fun onRecordAccessOperationCompleted(response: RecordAccessControlPointResponse) { private fun onRecordAccessOperationCompleted(requestCode: RACPOpCode) {
val status = when (response.requestCode) { val status = when (requestCode) {
RecordAccessControlPointDataCallback.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
else -> RequestStatus.SUCCESS else -> RequestStatus.SUCCESS
} }
_state.value = _state.value.copyWithNewRequestStatus(status) _state.value = _state.value.copyWithNewRequestStatus(status)
} }
private fun onRecordAccessOperationCompletedWithNoRecordsFound(response: RecordAccessControlPointResponse) { private fun onRecordAccessOperationCompletedWithNoRecordsFound() {
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.SUCCESS) _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.SUCCESS)
} }
private suspend fun onNumberOfRecordsReceived(response: RecordAccessControlPointResponse) { private suspend fun onNumberOfRecordsReceived(numberOfRecords: Int) {
if (response.numberOfRecords > 0) { if (numberOfRecords > 0) {
if (data.value.records.isNotEmpty()) { if (state.value.glsServiceData.records.isNotEmpty()) {
val sequenceNumber = data.value.records.last().sequenceNumber + 1
recordAccessControlPointCharacteristic.write( recordAccessControlPointCharacteristic.write(
RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber).value RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(highestSequenceNumber).value
) )
} else { } else {
recordAccessControlPointCharacteristic.write( recordAccessControlPointCharacteristic.write(
@@ -250,8 +253,8 @@ internal class GLSViewModel @Inject constructor(
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.SUCCESS) _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.SUCCESS)
} }
private fun onRecordAccessOperationError(response: RecordAccessControlPointResponse) { private fun onRecordAccessOperationError(response: RACPResponseCode) {
if (response.errorCode == RecordAccessControlPointDataCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) { if (response == RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.NOT_SUPPORTED) _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.NOT_SUPPORTED)
} else { } else {
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.FAILED) _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.FAILED)
@@ -262,21 +265,21 @@ internal class GLSViewModel @Inject constructor(
_state.value = _state.value.copyAndClear() _state.value = _state.value.copyAndClear()
} }
suspend fun requestLastRecord() { private suspend fun requestLastRecord() {
clear()
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING)
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportLastStoredRecord().value) recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportLastStoredRecord().value)
clear()
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING)
} }
suspend fun requestFirstRecord() { private suspend fun requestFirstRecord() {
clear()
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING)
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportFirstStoredRecord().value) recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportFirstStoredRecord().value)
clear()
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING)
} }
suspend fun requestAllRecords() { private suspend fun requestAllRecords() {
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value)
clear() clear()
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING) _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING)
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value)
} }
} }

View File

@@ -82,8 +82,8 @@ internal class HRSService : NotificationService() {
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch { private fun startGattClient(device: ServerDevice) = lifecycleScope.launch {
client = blinkyDevice.connect(this@HRSService) client = device.connect(this@HRSService)
client.connectionState client.connectionState
.onEach { repository.onConnectionStateChanged(it) } .onEach { repository.onConnectionStateChanged(it) }

View File

@@ -80,8 +80,8 @@ internal class HTSService : NotificationService() {
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch { private fun startGattClient(device: ServerDevice) = lifecycleScope.launch {
client = blinkyDevice.connect(this@HTSService) client = device.connect(this@HTSService)
client.connectionState client.connectionState
.onEach { repository.onConnectionStateChanged(it) } .onEach { repository.onConnectionStateChanged(it) }

View File

@@ -80,8 +80,8 @@ internal class RSCSService : NotificationService() {
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch { private fun startGattClient(device: ServerDevice) = lifecycleScope.launch {
client = blinkyDevice.connect(this@RSCSService) client = device.connect(this@RSCSService)
client.connectionState client.connectionState
.onEach { repository.onConnectionStateChanged(it) } .onEach { repository.onConnectionStateChanged(it) }