mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-20 16:04:22 +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
|
# 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
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
|||||||
@@ -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
|
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
|
||||||
|
|||||||
@@ -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.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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,6 @@ internal fun GLSDetailsScreen() {
|
|||||||
viewModel.navigateBack()
|
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 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,
|
} }
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
Reference in New Issue
Block a user