mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-20 07:54:20 +01:00
Finish migrating GLS profile
This commit is contained in:
@@ -46,7 +46,5 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
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=official
|
||||
@@ -110,10 +110,10 @@ internal class BPSViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun startGattClient(blinkyDevice: ServerDevice) = viewModelScope.launch {
|
||||
_state.value = _state.value.copy(deviceName = blinkyDevice.name)
|
||||
private fun startGattClient(device: ServerDevice) = viewModelScope.launch {
|
||||
_state.value = _state.value.copy(deviceName = device.name)
|
||||
|
||||
client = blinkyDevice.connect(context)
|
||||
client = device.connect(context)
|
||||
|
||||
client.connectionState
|
||||
.filterNotNull()
|
||||
|
||||
@@ -80,8 +80,8 @@ internal class CSCService : NotificationService() {
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch {
|
||||
client = blinkyDevice.connect(this@CSCService)
|
||||
private fun startGattClient(device: ServerDevice) = lifecycleScope.launch {
|
||||
client = device.connect(this@CSCService)
|
||||
|
||||
client.connectionState
|
||||
.onEach { repository.onConnectionStateChanged(it) }
|
||||
|
||||
@@ -33,9 +33,10 @@ package no.nordicsemi.android.gls
|
||||
|
||||
import no.nordicsemi.android.common.navigation.createDestination
|
||||
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.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() }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,10 +32,12 @@
|
||||
package no.nordicsemi.android.gls.data
|
||||
|
||||
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(
|
||||
val records: Map<GLSRecord, GlucoseMeasurementContext?> = mapOf(),
|
||||
val records: Map<GLSRecord, GLSMeasurementContext?> = mapOf(),
|
||||
val batteryLevel: Int? = null,
|
||||
val connectionState: GattConnectionState? = null,
|
||||
val requestStatus: RequestStatus = RequestStatus.IDLE
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -49,12 +49,13 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.kotlin.ble.profile.gls.data.GLSRecord
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext
|
||||
import no.nordicsemi.android.ui.view.ScreenSection
|
||||
|
||||
@Composable
|
||||
internal fun GLSDetailsContentView(record: GLSRecord) {
|
||||
internal fun GLSDetailsContentView(record: GLSRecord, context: GLSMeasurementContext?) {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
ScreenSection {
|
||||
@@ -86,24 +87,28 @@ internal fun GLSDetailsContentView(record: GLSRecord) {
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.gls_details_glucose_condensation_title),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.gls_details_glucose_condensation_field,
|
||||
record.glucoseConcentration,
|
||||
record.unit.toDisplayString()
|
||||
),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
record.glucoseConcentration?.let { glucoseConcentration ->
|
||||
record.unit?.let { unit ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.gls_details_glucose_condensation_title),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.gls_details_glucose_condensation_field,
|
||||
glucoseConcentration,
|
||||
unit.toDisplayString()
|
||||
),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record.status?.let {
|
||||
@@ -172,7 +177,7 @@ internal fun GLSDetailsContentView(record: GLSRecord) {
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
}
|
||||
|
||||
record.context?.let {
|
||||
context?.let {
|
||||
Divider(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
thickness = 1.dp,
|
||||
@@ -209,33 +214,42 @@ internal fun GLSDetailsContentView(record: GLSRecord) {
|
||||
)
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
}
|
||||
Field(
|
||||
stringResource(id = R.string.gls_context_exercise_title),
|
||||
stringResource(
|
||||
id = R.string.gls_context_exercise_field,
|
||||
it.exerciseDuration,
|
||||
it.exerciseIntensity
|
||||
it.exerciseDuration?.let { exerciseDuration ->
|
||||
it.exerciseIntensity?.let { exerciseIntensity ->
|
||||
Field(
|
||||
stringResource(id = R.string.gls_context_exercise_title),
|
||||
stringResource(
|
||||
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()
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Field(
|
||||
stringResource(id = R.string.gls_context_medication_title),
|
||||
medicationField
|
||||
)
|
||||
}
|
||||
|
||||
val medicationField = String.format(
|
||||
stringResource(id = R.string.gls_context_medication_field),
|
||||
it.medicationQuantity,
|
||||
it.medicationUnit.toDisplayString(),
|
||||
it.medication?.toDisplayString()
|
||||
)
|
||||
Field(
|
||||
stringResource(id = R.string.gls_context_medication_title),
|
||||
medicationField
|
||||
)
|
||||
it.HbA1c?.let { hbA1c ->
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Field(
|
||||
stringResource(id = R.string.gls_context_hba1c_title),
|
||||
stringResource(id = R.string.gls_context_hba1c_field, hbA1c)
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
stringResource(id = R.string.gls_context_title),
|
||||
stringResource(id = R.string.gls_unavailable)
|
||||
|
||||
@@ -33,15 +33,15 @@ package no.nordicsemi.android.gls.details.view
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
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.data.ConcentrationUnit
|
||||
import no.nordicsemi.android.gls.data.MedicationUnit
|
||||
import no.nordicsemi.android.gls.data.SampleLocation
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.data.Carbohydrate
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.data.ConcentrationUnit
|
||||
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
|
||||
internal fun SampleLocation.toDisplayString(): String {
|
||||
@@ -65,8 +65,8 @@ internal fun ConcentrationUnit.toDisplayString(): String {
|
||||
@Composable
|
||||
internal fun MedicationUnit.toDisplayString(): String {
|
||||
return when (this) {
|
||||
MedicationUnit.UNIT_KG -> stringResource(id = R.string.gls_sample_location_kg)
|
||||
MedicationUnit.UNIT_L -> stringResource(id = R.string.gls_sample_location_l)
|
||||
MedicationUnit.UNIT_MG -> stringResource(id = R.string.gls_sample_location_kg)
|
||||
MedicationUnit.UNIT_ML -> stringResource(id = R.string.gls_sample_location_l)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,6 @@ internal fun GLSDetailsScreen() {
|
||||
viewModel.navigateBack()
|
||||
}
|
||||
|
||||
GLSDetailsContentView(record)
|
||||
GLSDetailsContentView(record.first, record.second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,10 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.gls.R
|
||||
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.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.ScreenSection
|
||||
import no.nordicsemi.android.ui.view.SectionTitle
|
||||
@@ -139,7 +139,7 @@ private fun RecordsViewWithData(state: GLSServiceData) {
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
state.records.forEachIndexed { i, it ->
|
||||
state.records.keys.forEachIndexed { i, it ->
|
||||
RecordItem(it)
|
||||
|
||||
if (i < state.records.size - 1) {
|
||||
@@ -184,13 +184,12 @@ private fun RecordItem(record: GLSRecord) {
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
|
||||
Text(
|
||||
text = glucoseConcentrationDisplayValue(
|
||||
record.glucoseConcentration,
|
||||
record.unit
|
||||
),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
record.glucoseConcentration?.let { glucoseConcentration -> record.unit?.let { unit ->
|
||||
Text(
|
||||
text = glucoseConcentrationDisplayValue(glucoseConcentration, unit),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
} }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ package no.nordicsemi.android.gls.main.view
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.kotlin.ble.profile.gls.data.ConcentrationUnit
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RecordType
|
||||
|
||||
@Composable
|
||||
internal fun RecordType?.toDisplayString(): String {
|
||||
|
||||
@@ -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.gls.R
|
||||
import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel
|
||||
import no.nordicsemi.android.service.ConnectedResult
|
||||
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.kotlin.ble.core.data.GattConnectionState
|
||||
import no.nordicsemi.android.ui.view.BackIconAppBar
|
||||
import no.nordicsemi.android.ui.view.LoggerIconAppBar
|
||||
import no.nordicsemi.android.ui.view.NavigateUpButton
|
||||
@@ -78,19 +70,15 @@ fun GLSScreen() {
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when (state) {
|
||||
NoDeviceState -> DeviceConnectingView()
|
||||
is WorkingState -> when (state.result) {
|
||||
is IdleResult,
|
||||
is ConnectingResult -> DeviceConnectingView { NavigateUpButton(navigateUp) }
|
||||
is ConnectedResult -> DeviceConnectingView { NavigateUpButton(navigateUp) }
|
||||
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER) { NavigateUpButton(navigateUp) }
|
||||
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS) { NavigateUpButton(navigateUp) }
|
||||
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE) {
|
||||
NavigateUpButton(navigateUp)
|
||||
}
|
||||
is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
|
||||
is SuccessResult -> GLSContentView(state.result.data) { viewModel.onEvent(it) }
|
||||
if (state.deviceName == null) {
|
||||
DeviceConnectingView()
|
||||
} else {
|
||||
when (state.glsServiceData.connectionState) {
|
||||
null,
|
||||
GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) }
|
||||
GattConnectionState.STATE_DISCONNECTED,
|
||||
GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
|
||||
GattConnectionState.STATE_CONNECTED -> GLSContentView(state.glsServiceData) { viewModel.onEvent(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,14 +87,10 @@ fun GLSScreen() {
|
||||
|
||||
@Composable
|
||||
private fun AppBar(state: GLSViewState, navigateUp: () -> Unit, viewModel: GLSViewModel) {
|
||||
val toolbarName = (state as? WorkingState)?.let {
|
||||
(it.result as? DeviceHolder)?.deviceName()
|
||||
}
|
||||
|
||||
if (toolbarName == null) {
|
||||
if (state.deviceName == null) {
|
||||
BackIconAppBar(stringResource(id = R.string.gls_title), navigateUp)
|
||||
} else {
|
||||
LoggerIconAppBar(toolbarName, {
|
||||
LoggerIconAppBar(state.deviceName, {
|
||||
viewModel.onEvent(DisconnectEvent)
|
||||
}, { viewModel.onEvent(DisconnectEvent) }) {
|
||||
viewModel.onEvent(OpenLoggerEvent)
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
|
||||
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.kotlin.ble.profile.gls.data.GLSRecord
|
||||
|
||||
internal sealed class GLSScreenViewEvent
|
||||
|
||||
|
||||
@@ -32,13 +32,20 @@
|
||||
package no.nordicsemi.android.gls.main.view
|
||||
|
||||
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(
|
||||
val glsServiceData: GLSServiceData = GLSServiceData(),
|
||||
val deviceName: String? = null
|
||||
) {
|
||||
|
||||
fun copyWithNewConnectionState(connectionState: GattConnectionState): GLSViewState {
|
||||
return copy(glsServiceData = glsServiceData.copy(connectionState = connectionState))
|
||||
}
|
||||
|
||||
fun copyAndClear(): GLSViewState {
|
||||
return copy(glsServiceData = glsServiceData.copy(records = mapOf(), requestStatus = RequestStatus.IDLE))
|
||||
}
|
||||
@@ -46,4 +53,24 @@ internal data class GLSViewState(
|
||||
fun copyWithNewRequestStatus(requestStatus: RequestStatus): GLSViewState {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,13 +48,9 @@ import kotlinx.coroutines.launch
|
||||
import no.nordicsemi.android.analytics.AppAnalytics
|
||||
import no.nordicsemi.android.analytics.Profile
|
||||
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.Navigator
|
||||
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.main.view.DisconnectEvent
|
||||
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.RecordAccessControlPointInputParser
|
||||
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.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.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 java.util.*
|
||||
import javax.inject.Inject
|
||||
@@ -102,12 +101,14 @@ internal class GLSViewModel @Inject constructor(
|
||||
private lateinit var client: BleGattClient
|
||||
|
||||
private lateinit var glucoseMeasurementCharacteristic: BleGattCharacteristic
|
||||
private lateinit var glucoseMeasurementContextCharacteristic: BleGattCharacteristic
|
||||
private lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic
|
||||
|
||||
private val _state = MutableStateFlow(GLSViewState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private val highestSequenceNumber
|
||||
get() = state.value.glsServiceData.records.keys.maxByOrNull { it.sequenceNumber }?.sequenceNumber ?: -1
|
||||
|
||||
init {
|
||||
navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(GLS_SERVICE_UUID))
|
||||
|
||||
@@ -125,16 +126,20 @@ internal class GLSViewModel @Inject constructor(
|
||||
|
||||
fun onEvent(event: GLSScreenViewEvent) {
|
||||
when (event) {
|
||||
OpenLoggerEvent -> repository.openLogger()
|
||||
OpenLoggerEvent -> TODO()
|
||||
DisconnectEvent -> navigationManager.navigateUp()
|
||||
is OnWorkingModeSelected -> repository.requestMode(event.workingMode)
|
||||
is OnGLSRecordClick -> navigationManager.navigateTo(GlsDetailsDestinationId, event.record)
|
||||
is OnWorkingModeSelected -> onEvent(event)
|
||||
is OnGLSRecordClick -> navigateToDetails(event.record)
|
||||
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) {
|
||||
_state.value = _state.value.copy(deviceName = device.name)
|
||||
startGattClient(device)
|
||||
}
|
||||
|
||||
@@ -146,100 +151,98 @@ internal class GLSViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectDevice(device: ServerDevice) {
|
||||
repository.downloadData(viewModelScope, device).onEach {
|
||||
_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)
|
||||
private fun startGattClient(device: ServerDevice) = viewModelScope.launch {
|
||||
client = device.connect(context)
|
||||
|
||||
client.connectionState
|
||||
.onEach { _state.value = _state.value.copy() }
|
||||
.filterNotNull()
|
||||
.onEach { _state.value = _state.value.copyWithNewConnectionState(it) }
|
||||
.onEach { stopIfDisconnected(it) }
|
||||
.onEach { logAnalytics(it) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
client.services
|
||||
.filterNotNull()
|
||||
.onEach { configureGatt(it) }
|
||||
.onEach { configureGatt(it, device) }
|
||||
.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)!!
|
||||
glucoseMeasurementCharacteristic = glsService.findCharacteristic(GM_CHARACTERISTIC)!!
|
||||
glucoseMeasurementContextCharacteristic = glsService.findCharacteristic(GM_CONTEXT_CHARACTERISTIC)!!
|
||||
recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!!
|
||||
val batteryService = services.findService(BATTERY_SERVICE_UUID)!!
|
||||
val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!!
|
||||
|
||||
batteryLevelCharacteristic.getNotifications()
|
||||
.mapNotNull { BatteryLevelParser.parse(it) }
|
||||
.onEach { repository.onBatteryLevelChanged(it) }
|
||||
.onEach { _state.value = _state.value.copyWithNewBatteryLevel(it) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
glucoseMeasurementCharacteristic.getNotifications()
|
||||
.mapNotNull { GlucoseMeasurementParser.parse(it) }
|
||||
.onEach { }
|
||||
.onEach { _state.value = _state.value.copyWithNewRecord(it) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
glucoseMeasurementContextCharacteristic.getNotifications()
|
||||
.mapNotNull { GlucoseMeasurementContextParser.parse(it) }
|
||||
.onEach { }
|
||||
.launchIn(viewModelScope)
|
||||
glsService.findCharacteristic(GM_CONTEXT_CHARACTERISTIC)?.getNotifications()
|
||||
?.mapNotNull { GlucoseMeasurementContextParser.parse(it) }
|
||||
?.onEach { _state.value = _state.value.copyWithNewContext(it) }
|
||||
?.launchIn(viewModelScope)
|
||||
|
||||
recordAccessControlPointCharacteristic.getNotifications()
|
||||
.mapNotNull { RecordAccessControlPointParser.parse(it) }
|
||||
.onEach { onAccessControlPointDataReceived(it) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
_state.value = _state.value.copy(deviceName = device.name)
|
||||
}
|
||||
|
||||
private fun stopIfDisconnected(connectionState: GattConnectionState) {
|
||||
if (connectionState == GattConnectionState.STATE_DISCONNECTED) {
|
||||
stopSelf()
|
||||
navigationManager.navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAccessControlPointDataReceived(data: RecordAccessControlPointData) {
|
||||
private fun onAccessControlPointDataReceived(data: RecordAccessControlPointData) = viewModelScope.launch {
|
||||
when (data) {
|
||||
is NumberOfRecordsData -> if ()
|
||||
is ResponseData -> TODO()
|
||||
}
|
||||
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)
|
||||
is NumberOfRecordsData -> onNumberOfRecordsReceived(data.numberOfRecords)
|
||||
is ResponseData -> when (data.responseCode) {
|
||||
RACPResponseCode.RACP_RESPONSE_SUCCESS -> onRecordAccessOperationCompleted(data.requestCode)
|
||||
RACPResponseCode.RACP_ERROR_NO_RECORDS_FOUND -> onRecordAccessOperationCompletedWithNoRecordsFound()
|
||||
RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED,
|
||||
RACPResponseCode.RACP_ERROR_INVALID_OPERATOR,
|
||||
RACPResponseCode.RACP_ERROR_OPERATOR_NOT_SUPPORTED,
|
||||
RACPResponseCode.RACP_ERROR_INVALID_OPERAND,
|
||||
RACPResponseCode.RACP_ERROR_ABORT_UNSUCCESSFUL,
|
||||
RACPResponseCode.RACP_ERROR_PROCEDURE_NOT_COMPLETED,
|
||||
RACPResponseCode.RACP_ERROR_OPERAND_NOT_SUPPORTED -> onRecordAccessOperationError(data.responseCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationCompleted(response: RecordAccessControlPointResponse) {
|
||||
val status = when (response.requestCode) {
|
||||
RecordAccessControlPointDataCallback.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
|
||||
private fun onRecordAccessOperationCompleted(requestCode: RACPOpCode) {
|
||||
val status = when (requestCode) {
|
||||
RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
|
||||
else -> RequestStatus.SUCCESS
|
||||
}
|
||||
_state.value = _state.value.copyWithNewRequestStatus(status)
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationCompletedWithNoRecordsFound(response: RecordAccessControlPointResponse) {
|
||||
private fun onRecordAccessOperationCompletedWithNoRecordsFound() {
|
||||
_state.value = _state.value.copyWithNewRequestStatus(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
|
||||
private suspend fun onNumberOfRecordsReceived(numberOfRecords: Int) {
|
||||
if (numberOfRecords > 0) {
|
||||
if (state.value.glsServiceData.records.isNotEmpty()) {
|
||||
recordAccessControlPointCharacteristic.write(
|
||||
RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber).value
|
||||
RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(highestSequenceNumber).value
|
||||
)
|
||||
} else {
|
||||
recordAccessControlPointCharacteristic.write(
|
||||
@@ -250,8 +253,8 @@ internal class GLSViewModel @Inject constructor(
|
||||
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.SUCCESS)
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationError(response: RecordAccessControlPointResponse) {
|
||||
if (response.errorCode == RecordAccessControlPointDataCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
|
||||
private fun onRecordAccessOperationError(response: RACPResponseCode) {
|
||||
if (response == RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
|
||||
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.NOT_SUPPORTED)
|
||||
} else {
|
||||
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.FAILED)
|
||||
@@ -262,21 +265,21 @@ internal class GLSViewModel @Inject constructor(
|
||||
_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)
|
||||
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)
|
||||
clear()
|
||||
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING)
|
||||
}
|
||||
|
||||
suspend fun requestAllRecords() {
|
||||
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value)
|
||||
private suspend fun requestAllRecords() {
|
||||
clear()
|
||||
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING)
|
||||
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ internal class HRSService : NotificationService() {
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch {
|
||||
client = blinkyDevice.connect(this@HRSService)
|
||||
private fun startGattClient(device: ServerDevice) = lifecycleScope.launch {
|
||||
client = device.connect(this@HRSService)
|
||||
|
||||
client.connectionState
|
||||
.onEach { repository.onConnectionStateChanged(it) }
|
||||
|
||||
@@ -80,8 +80,8 @@ internal class HTSService : NotificationService() {
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch {
|
||||
client = blinkyDevice.connect(this@HTSService)
|
||||
private fun startGattClient(device: ServerDevice) = lifecycleScope.launch {
|
||||
client = device.connect(this@HTSService)
|
||||
|
||||
client.connectionState
|
||||
.onEach { repository.onConnectionStateChanged(it) }
|
||||
|
||||
@@ -80,8 +80,8 @@ internal class RSCSService : NotificationService() {
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch {
|
||||
client = blinkyDevice.connect(this@RSCSService)
|
||||
private fun startGattClient(device: ServerDevice) = lifecycleScope.launch {
|
||||
client = device.connect(this@RSCSService)
|
||||
|
||||
client.connectionState
|
||||
.onEach { repository.onConnectionStateChanged(it) }
|
||||
|
||||
Reference in New Issue
Block a user