Migrate CGM profile to new BLE library

This commit is contained in:
Sylwester Zielinski
2023-03-17 10:28:47 +01:00
parent e15ad2967e
commit 60d41868fb
16 changed files with 326 additions and 600 deletions

View File

@@ -55,6 +55,7 @@ dependencies {
implementation(libs.nordic.theme)
implementation(libs.nordic.uiscanner)
implementation(libs.nordic.navigation)
implementation(libs.nordic.core)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.compose.material.iconsExtended)

View File

@@ -1,38 +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.cgms.data
internal data class CGMData(
val records: List<CGMRecord> = emptyList(),
val batteryLevel: Int? = null,
val requestStatus: RequestStatus = RequestStatus.IDLE
)

View File

@@ -1,323 +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.cgms.data
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.util.Log
import android.util.SparseArray
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.RecordAccessControlPointResponse
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
import no.nordicsemi.android.ble.common.callback.cgm.CGMFeatureResponse
import no.nordicsemi.android.ble.common.callback.cgm.CGMSpecificOpsControlPointResponse
import no.nordicsemi.android.ble.common.callback.cgm.CGMStatusResponse
import no.nordicsemi.android.ble.common.callback.cgm.ContinuousGlucoseMeasurementResponse
import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData
import no.nordicsemi.android.ble.common.data.cgm.CGMSpecificOpsControlPointData
import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback
import no.nordicsemi.android.ble.common.profile.cgm.CGMSpecificOpsControlPointCallback
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.ble.ktx.suspendForValidResponse
import no.nordicsemi.android.common.logger.NordicLogger
import no.nordicsemi.android.service.ConnectionObserverAdapter
import no.nordicsemi.android.utils.launchWithCatch
import java.util.*
val CGMS_SERVICE_UUID: UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb")
private val CGM_STATUS_UUID = UUID.fromString("00002AA9-0000-1000-8000-00805f9b34fb")
private val CGM_FEATURE_UUID = UUID.fromString("00002AA8-0000-1000-8000-00805f9b34fb")
private val CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb")
private val CGM_OPS_CONTROL_POINT_UUID = UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb")
private val RACP_UUID = 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 CGMManager(
context: Context,
private val scope: CoroutineScope,
private val logger: NordicLogger
) : BleManager(context) {
private var cgmStatusCharacteristic: BluetoothGattCharacteristic? = null
private var cgmFeatureCharacteristic: BluetoothGattCharacteristic? = null
private var cgmMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private var cgmSpecificOpsControlPointCharacteristic: BluetoothGattCharacteristic? = null
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
private val records: SparseArray<CGMRecord> = SparseArray<CGMRecord>()
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var secured = false
private var recordAccessRequestInProgress = false
private var sessionStartTime: Long = 0
private val data = MutableStateFlow(CGMData())
val dataHolder = ConnectionObserverAdapter<CGMData>()
init {
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)
}.launchIn(scope)
}
override fun getGattCallback(): BleManagerGattCallback {
return CGMManagerGattCallback()
}
override fun log(priority: Int, message: String) {
logger.log(priority, message)
}
override fun getMinLogPriority(): Int {
return Log.VERBOSE
}
private inner class CGMManagerGattCallback : BleManagerGattCallback() {
override fun initialize() {
super.initialize()
setNotificationCallback(cgmMeasurementCharacteristic).asValidResponseFlow<ContinuousGlucoseMeasurementResponse>()
.onEach {
if (sessionStartTime == 0L && !recordAccessRequestInProgress) {
val timeOffset = it.items.minOf { it.timeOffset }
sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L
}
it.items.map {
val timestamp = sessionStartTime + it.timeOffset * 60000L
val item = CGMRecord(it.timeOffset, it.glucoseConcentration, timestamp)
records.put(item.sequenceNumber, item)
}
data.value = data.value.copy(records = records.toList())
}.launchIn(scope)
setIndicationCallback(cgmSpecificOpsControlPointCharacteristic).asValidResponseFlow<CGMSpecificOpsControlPointResponse>()
.onEach {
if (it.isOperationCompleted) {
when (it.requestCode) {
CGMSpecificOpsControlPointCallback.CGM_OP_CODE_START_SESSION -> sessionStartTime =
System.currentTimeMillis()
CGMSpecificOpsControlPointCallback.CGM_OP_CODE_STOP_SESSION -> sessionStartTime =
0
}
} else {
when (it.requestCode) {
CGMSpecificOpsControlPointCallback.CGM_OP_CODE_START_SESSION ->
if (it.errorCode == CGMSpecificOpsControlPointCallback.CGM_ERROR_PROCEDURE_NOT_COMPLETED) {
sessionStartTime = 0
}
CGMSpecificOpsControlPointCallback.CGM_OP_CODE_STOP_SESSION -> sessionStartTime =
0
}
}
}.launchIn(scope)
setIndicationCallback(recordAccessControlPointCharacteristic).asValidResponseFlow<RecordAccessControlPointResponse>()
.onEach {
if (it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords > 0) {
onRecordsReceived(it)
} else if (it.isOperationCompleted && !it.wereRecordsFound()) {
onNoRecordsFound()
} else if (it.isOperationCompleted && it.wereRecordsFound()) {
onOperationCompleted(it)
} else if (it.errorCode > 0) {
onError(it)
}
}.launchIn(scope)
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>()
.onEach {
data.value = data.value.copy(batteryLevel = it.batteryLevel)
}.launchIn(scope)
enableNotifications(cgmMeasurementCharacteristic).enqueue()
enableIndications(cgmSpecificOpsControlPointCharacteristic).enqueue()
enableIndications(recordAccessControlPointCharacteristic).enqueue()
enableNotifications(batteryLevelCharacteristic).enqueue()
scope.launchWithCatch {
val cgmResponse = readCharacteristic(cgmFeatureCharacteristic).suspendForValidResponse<CGMFeatureResponse>()
this@CGMManager.secured = cgmResponse.features.e2eCrcSupported
}
scope.launchWithCatch {
val response = readCharacteristic(cgmStatusCharacteristic).suspendForValidResponse<CGMStatusResponse>()
if (response.status?.sessionStopped == false) {
sessionStartTime = System.currentTimeMillis() - response.timeOffset * 60000L
}
}
scope.launchWithCatch {
if (sessionStartTime == 0L) {
writeCharacteristic(
cgmSpecificOpsControlPointCharacteristic,
CGMSpecificOpsControlPointData.startSession(secured),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
}
}
}
private suspend fun onRecordsReceived(response: RecordAccessControlPointResponse) {
if (response.numberOfRecords > 0) {
if (records.size() > 0) {
val sequenceNumber = records.keyAt(records.size() - 1) + 1
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
sequenceNumber
),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
} else {
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportAllStoredRecords(),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
}
} else {
recordAccessRequestInProgress = false
data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS)
}
}
private fun onNoRecordsFound() {
recordAccessRequestInProgress = false
data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS)
}
private fun onOperationCompleted(response: RecordAccessControlPointResponse) {
when (response.requestCode) {
RecordAccessControlPointCallback.RACP_OP_CODE_ABORT_OPERATION ->
data.value = data.value.copy(requestStatus = RequestStatus.ABORTED)
else -> {
recordAccessRequestInProgress = false
data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS)
}
}
}
private fun onError(response: RecordAccessControlPointResponse) {
if (response.errorCode == RecordAccessControlPointCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
data.value = data.value.copy(requestStatus = RequestStatus.NOT_SUPPORTED)
} else {
data.value = data.value.copy(requestStatus = RequestStatus.FAILED)
}
}
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
gatt.getService(CGMS_SERVICE_UUID)?.run {
cgmStatusCharacteristic = getCharacteristic(CGM_STATUS_UUID)
cgmFeatureCharacteristic = getCharacteristic(CGM_FEATURE_UUID)
cgmMeasurementCharacteristic = getCharacteristic(CGM_MEASUREMENT_UUID)
cgmSpecificOpsControlPointCharacteristic = getCharacteristic(CGM_OPS_CONTROL_POINT_UUID)
recordAccessControlPointCharacteristic = getCharacteristic(RACP_UUID)
}
gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return cgmMeasurementCharacteristic != null
&& cgmSpecificOpsControlPointCharacteristic != null
&& recordAccessControlPointCharacteristic != null
&& cgmStatusCharacteristic != null
&& cgmFeatureCharacteristic != null
}
override fun onServicesInvalidated() {
cgmStatusCharacteristic = null
cgmFeatureCharacteristic = null
cgmMeasurementCharacteristic = null
cgmSpecificOpsControlPointCharacteristic = null
recordAccessControlPointCharacteristic = null
batteryLevelCharacteristic = null
}
}
private fun clear() {
records.clear()
}
fun requestLastRecord() {
if (recordAccessControlPointCharacteristic == null) return
clear()
data.value = data.value.copy(requestStatus = RequestStatus.PENDING)
recordAccessRequestInProgress = true
scope.launchWithCatch {
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportLastStoredRecord(),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
}
}
fun requestFirstRecord() {
if (recordAccessControlPointCharacteristic == null) return
clear()
data.value = data.value.copy(requestStatus = RequestStatus.PENDING)
recordAccessRequestInProgress = true
scope.launchWithCatch {
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportFirstStoredRecord(),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
}
}
fun requestAllRecords() {
if (recordAccessControlPointCharacteristic == null) return
clear()
data.value = data.value.copy(requestStatus = RequestStatus.PENDING)
recordAccessRequestInProgress = true
scope.launchWithCatch {
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportNumberOfAllStoredRecords(),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
}
}
}

View File

@@ -1,42 +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.cgms.data
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
internal data class CGMRecord(
var sequenceNumber: Int,
var glucoseConcentration: Float,
var timestamp: Long
) : Parcelable

View File

@@ -0,0 +1,18 @@
package no.nordicsemi.android.cgms.data
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMRecord
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus
internal data class CGMServiceData(
val records: List<CGMRecordWithSequenceNumber> = emptyList(),
val batteryLevel: Int? = null,
val connectionState: GattConnectionState? = null,
val requestStatus: RequestStatus = RequestStatus.IDLE
)
data class CGMRecordWithSequenceNumber(
val sequenceNumber: Int,
val record: CGMRecord,
val timestamp: Long
)

View File

@@ -1,43 +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.cgms.data
import android.util.SparseArray
import androidx.core.util.keyIterator
internal fun SparseArray<CGMRecord>.toList(): List<CGMRecord> {
val list = mutableListOf<CGMRecord>()
this.keyIterator().forEach {
list.add(get(it))
}
return list.sortedBy { it.sequenceNumber }.toList()
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright (c) 2022, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.android.cgms.data
internal enum class RequestStatus {
IDLE, PENDING, SUCCESS, ABORTED, FAILED, NOT_SUPPORTED
}

View File

@@ -33,23 +33,19 @@ package no.nordicsemi.android.cgms.repository
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.cgms.data.CGMData
import no.nordicsemi.android.cgms.data.CGMManager
import no.nordicsemi.android.common.logger.NordicLogger
import no.nordicsemi.android.common.logger.NordicLoggerFactory
import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber
import no.nordicsemi.android.cgms.data.CGMServiceCommand
import no.nordicsemi.android.cgms.data.CGMServiceData
import no.nordicsemi.android.common.core.simpleSharedFlow
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.IdleResult
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus
import no.nordicsemi.android.service.DisconnectAndStopEvent
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.android.ui.view.StringConst
import javax.inject.Inject
import javax.inject.Singleton
@@ -58,68 +54,53 @@ class CGMRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val serviceManager: ServiceManager,
private val loggerFactory: NordicLoggerFactory,
private val stringConst: StringConst
) {
private var manager: CGMManager? = null
private var logger: NordicLogger? = null
private val _data = MutableStateFlow<BleManagerResult<CGMData>>(IdleResult())
private val _data = MutableStateFlow(CGMServiceData())
internal val data = _data.asStateFlow()
val isRunning = data.map { it.isRunning() }
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
private val _stopEvent = simpleSharedFlow<DisconnectAndStopEvent>()
internal val stopEvent = _stopEvent.asSharedFlow()
private val _command = simpleSharedFlow<CGMServiceCommand>()
internal val command = _command.asSharedFlow()
val isRunning = data.map { it.connectionState == GattConnectionState.STATE_CONNECTED }
val hasRecords = data.value.records.isNotEmpty()
val highestSequenceNumber = data.value.records.maxOfOrNull { it.sequenceNumber } ?: -1
fun launch(device: ServerDevice) {
serviceManager.startService(CGMService::class.java, device)
}
fun start(device: ServerDevice, scope: CoroutineScope) {
val createdLogger = loggerFactory.create(stringConst.APP_NAME, "CGMS", device.address).also {
logger = it
}
val manager = CGMManager(context, scope, createdLogger)
this.manager = manager
manager.dataHolder.status.onEach {
_data.value = it
}.launchIn(scope)
scope.launch {
manager.start(device)
}
fun onDataReceived(data: List<CGMRecordWithSequenceNumber>) {
_data.value = _data.value.copy(records = _data.value.records + data)
}
private suspend fun CGMManager.start(device: ServerDevice) {
// try {
// connect(device.device)
// .useAutoConnect(false)
// .retry(3, 100)
// .suspend()
// } catch (e: Exception) {
// e.printStackTrace()
// }
internal fun onCommand(command: CGMServiceCommand) {
_command.tryEmit(command)
}
fun requestAllRecords() {
manager?.requestAllRecords()
fun onConnectionStateChanged(connectionState: GattConnectionState?) {
_data.value = _data.value.copy(connectionState = connectionState)
}
fun requestLastRecord() {
manager?.requestLastRecord()
fun onBatteryLevelChanged(batteryLevel: Int) {
_data.value = _data.value.copy(batteryLevel = batteryLevel)
}
fun requestFirstRecord() {
manager?.requestFirstRecord()
fun onNewRequestStatus(requestStatus: RequestStatus) {
_data.value = _data.value.copy(requestStatus = requestStatus)
}
fun openLogger() {
NordicLogger.launch(context, logger)
TODO()
}
fun clear() {
_data.value = _data.value.copy(records = emptyList())
}
fun release() {
manager?.disconnect()?.enqueue()
logger = null
manager = null
_stopEvent.tryEmit(DisconnectAndStopEvent())
}
}

View File

@@ -31,33 +31,263 @@
package no.nordicsemi.android.cgms.repository
import android.annotation.SuppressLint
import android.content.Intent
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.common.data.cgm.CGMSpecificOpsControlPointData
import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber
import no.nordicsemi.android.cgms.data.CGMServiceCommand
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.kotlin.ble.core.client.callback.BleGattClient
import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattCharacteristic
import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattServices
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser
import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMFeatureParser
import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMMeasurementParser
import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMSpecificOpsControlPointParser
import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMStatusParser
import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMErrorCode
import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMOpCode
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.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.kotlin.ble.profile.racp.RACPOpCode
import no.nordicsemi.android.kotlin.ble.profile.racp.RACPResponseCode
import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService
import java.util.*
import javax.inject.Inject
val CGMS_SERVICE_UUID: UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb")
private val CGM_STATUS_UUID = UUID.fromString("00002AA9-0000-1000-8000-00805f9b34fb")
private val CGM_FEATURE_UUID = UUID.fromString("00002AA8-0000-1000-8000-00805f9b34fb")
private val CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb")
private val CGM_OPS_CONTROL_POINT_UUID = UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb")
private val RACP_UUID = 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")
@SuppressLint("MissingPermission")
@AndroidEntryPoint
internal class CGMService : NotificationService() {
@Inject
lateinit var repository: CGMRepository
private lateinit var client: BleGattClient
private var secured = false
private var recordAccessRequestInProgress = false
private var sessionStartTime: Long = 0
private lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<ServerDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope)
startGattClient(device)
repository.hasBeenDisconnected.onEach {
if (it) stopSelf()
}.launchIn(lifecycleScope)
repository.stopEvent
.onEach { disconnect() }
.launchIn(lifecycleScope)
repository.command
.onEach { onCommand(it) }
.launchIn(lifecycleScope)
return START_REDELIVER_INTENT
}
private fun onCommand(command: CGMServiceCommand) = lifecycleScope.launch{
when (command) {
CGMServiceCommand.REQUEST_ALL_RECORDS -> requestAllRecords()
CGMServiceCommand.REQUEST_LAST_RECORD -> requestLastRecord()
CGMServiceCommand.REQUEST_FIRST_RECORD -> requestFirstRecord()
CGMServiceCommand.DISCONNECT -> client.disconnect()
}
}
private fun startGattClient(device: ServerDevice) = lifecycleScope.launch {
client = device.connect(this@CGMService)
client.connectionState
.onEach { repository.onConnectionStateChanged(it) }
.filterNotNull()
.onEach { stopIfDisconnected(it) }
.launchIn(lifecycleScope)
client.services
.filterNotNull()
.onEach { configureGatt(it) }
.launchIn(lifecycleScope)
}
private suspend fun configureGatt(services: BleGattServices) {
val glsService = services.findService(CGMS_SERVICE_UUID)!!
val statusCharacteristic = glsService.findCharacteristic(CGM_STATUS_UUID)!!
val featureCharacteristic = glsService.findCharacteristic(CGM_FEATURE_UUID)!!
val measurementCharacteristic = glsService.findCharacteristic(CGM_MEASUREMENT_UUID)!!
val opsControlPointCharacteristic = glsService.findCharacteristic(CGM_OPS_CONTROL_POINT_UUID)!!
recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_UUID)!!
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) }
.launchIn(lifecycleScope)
measurementCharacteristic.getNotifications()
.mapNotNull { CGMMeasurementParser.parse(it) }
.onEach {
if (sessionStartTime == 0L && !recordAccessRequestInProgress) {
val timeOffset = it.minOf { it.timeOffset }
sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L
}
val result = it.map {
val timestamp = sessionStartTime + it.timeOffset * 60000L
CGMRecordWithSequenceNumber(it.timeOffset, it, timestamp)
}
repository.onDataReceived(result)
}
.launchIn(lifecycleScope)
opsControlPointCharacteristic.getNotifications()
.mapNotNull { CGMSpecificOpsControlPointParser.parse(it) }
.onEach {
if (it.isOperationCompleted) {
if (it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION) {
sessionStartTime = System.currentTimeMillis()
} else {
sessionStartTime = 0
}
} else {
if (it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION && it.errorCode == CGMErrorCode.CGM_ERROR_PROCEDURE_NOT_COMPLETED) {
sessionStartTime = 0
} else if (it.requestCode == CGMOpCode.CGM_OP_CODE_STOP_SESSION) {
sessionStartTime = 0
}
}
}
.launchIn(lifecycleScope)
recordAccessControlPointCharacteristic.getNotifications()
.mapNotNull { RecordAccessControlPointParser.parse(it) }
.onEach { onAccessControlPointDataReceived(it) }
.launchIn(lifecycleScope)
val featuresEnvelope = featureCharacteristic.read().let { CGMFeatureParser.parse(it) }!!
secured = featuresEnvelope.features.e2eCrcSupported
val statusEnvelope = statusCharacteristic.read().let { CGMStatusParser.parse(it) }!!
if (!statusEnvelope.status.sessionStopped) {
sessionStartTime = System.currentTimeMillis() - statusEnvelope.timeOffset * 60000L
}
if (sessionStartTime == 0L) {
opsControlPointCharacteristic.write(CGMSpecificOpsControlPointData.startSession(secured).value!!)
}
}
private fun onAccessControlPointDataReceived(data: RecordAccessControlPointData) = lifecycleScope.launch {
when (data) {
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(requestCode: RACPOpCode) {
val status = when (requestCode) {
RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
else -> RequestStatus.SUCCESS
}
repository.onNewRequestStatus(status)
}
private fun onRecordAccessOperationCompletedWithNoRecordsFound() {
repository.onNewRequestStatus(RequestStatus.SUCCESS)
}
private suspend fun onNumberOfRecordsReceived(numberOfRecords: Int) {
if (numberOfRecords > 0) {
if (repository.hasRecords) {
recordAccessControlPointCharacteristic.write(
RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(repository.highestSequenceNumber).value
)
} else {
recordAccessControlPointCharacteristic.write(
RecordAccessControlPointInputParser.reportAllStoredRecords().value
)
}
}
repository.onNewRequestStatus(RequestStatus.SUCCESS)
}
private fun onRecordAccessOperationError(response: RACPResponseCode) {
if (response == RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
repository.onNewRequestStatus(RequestStatus.NOT_SUPPORTED)
} else {
repository.onNewRequestStatus(RequestStatus.FAILED)
}
}
private fun clear() {
repository.clear()
}
private suspend fun requestLastRecord() {
clear()
repository.onNewRequestStatus(RequestStatus.PENDING)
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportLastStoredRecord().value)
}
private suspend fun requestFirstRecord() {
clear()
repository.onNewRequestStatus(RequestStatus.PENDING)
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportFirstStoredRecord().value)
}
private suspend fun requestAllRecords() {
clear()
repository.onNewRequestStatus(RequestStatus.PENDING)
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value)
}
private fun stopIfDisconnected(connectionState: GattConnectionState) {
if (connectionState == GattConnectionState.STATE_DISCONNECTED) {
stopSelf()
}
}
private fun disconnect() {
client.disconnect()
}
}

View File

@@ -52,16 +52,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.cgms.R
import no.nordicsemi.android.cgms.data.CGMData
import no.nordicsemi.android.cgms.data.CGMRecord
import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber
import no.nordicsemi.android.cgms.data.CGMServiceCommand
import no.nordicsemi.android.cgms.data.RequestStatus
import no.nordicsemi.android.cgms.data.CGMServiceData
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
@Composable
internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
internal fun CGMContentView(state: CGMServiceData, onEvent: (CGMViewEvent) -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
@@ -91,7 +91,7 @@ internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
}
@Composable
private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
private fun SettingsView(state: CGMServiceData, onEvent: (CGMViewEvent) -> Unit) {
ScreenSection {
SectionTitle(icon = Icons.Default.Settings, title = "Request items")
@@ -119,7 +119,7 @@ private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
}
@Composable
private fun RecordsView(state: CGMData) {
private fun RecordsView(state: CGMServiceData) {
ScreenSection {
if (state.records.isEmpty()) {
RecordsViewWithoutData()
@@ -131,7 +131,7 @@ private fun RecordsView(state: CGMData) {
}
@Composable
private fun RecordsViewWithData(state: CGMData) {
private fun RecordsViewWithData(state: CGMServiceData) {
Column(modifier = Modifier.fillMaxWidth()) {
SectionTitle(resId = R.drawable.ic_records, title = "Records")
@@ -148,7 +148,7 @@ private fun RecordsViewWithData(state: CGMData) {
}
@Composable
private fun RecordItem(record: CGMRecord) {
private fun RecordItem(record: CGMRecordWithSequenceNumber) {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(
modifier = Modifier

View File

@@ -34,16 +34,17 @@ package no.nordicsemi.android.cgms.view
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import no.nordicsemi.android.cgms.R
import no.nordicsemi.android.cgms.data.CGMRecord
import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber
import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMRecord
import java.text.SimpleDateFormat
import java.util.*
internal fun CGMRecord.formattedTime(): String {
internal fun CGMRecordWithSequenceNumber.formattedTime(): String {
val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US)
return timeFormat.format(Date(timestamp))
}
@Composable
internal fun CGMRecord.glucoseConcentration(): String {
return stringResource(id = R.string.cgms_value_unit, glucoseConcentration)
internal fun CGMRecordWithSequenceNumber.glucoseConcentration(): String {
return stringResource(id = R.string.cgms_value_unit, record.glucoseConcentration)
}

View File

@@ -48,15 +48,7 @@ import no.nordicsemi.android.cgms.viewmodel.CGMViewModel
import no.nordicsemi.android.common.ui.scanner.view.DeviceConnectingView
import no.nordicsemi.android.common.ui.scanner.view.DeviceDisconnectedView
import no.nordicsemi.android.common.ui.scanner.view.Reason
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,17 +70,15 @@ fun CGMScreen() {
.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 -> CGMContentView(state.result.data) { viewModel.onEvent(it) }
if (state.deviceName == null) {
DeviceConnectingView()
} else {
when (state.result?.connectionState) {
null,
GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) }
GattConnectionState.STATE_DISCONNECTED,
GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
GattConnectionState.STATE_CONNECTED -> CGMContentView(state.result) { viewModel.onEvent(it) }
}
}
}
@@ -97,15 +87,11 @@ fun CGMScreen() {
@Composable
private fun AppBar(state: CGMViewState, navigateUp: () -> Unit, viewModel: CGMViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? DeviceHolder)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.cgms_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
if (state.deviceName?.isNotBlank() == true) {
LoggerIconAppBar(state.deviceName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
} else {
BackIconAppBar(stringResource(id = R.string.cgms_title), navigateUp)
}
}

View File

@@ -31,10 +31,9 @@
package no.nordicsemi.android.cgms.view
import no.nordicsemi.android.cgms.data.CGMData
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.cgms.data.CGMServiceData
internal sealed class CGMViewState
internal data class WorkingState(val result: BleManagerResult<CGMData>) : CGMViewState()
internal object NoDeviceState : CGMViewState()
internal data class CGMViewState(
val result: CGMServiceData? = null,
val deviceName: String? = null
)

View File

@@ -44,21 +44,19 @@ 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.cgms.data.CGMS_SERVICE_UUID
import no.nordicsemi.android.cgms.data.CGMServiceCommand
import no.nordicsemi.android.cgms.repository.CGMRepository
import no.nordicsemi.android.cgms.repository.CGMS_SERVICE_UUID
import no.nordicsemi.android.cgms.view.CGMViewEvent
import no.nordicsemi.android.cgms.view.CGMViewState
import no.nordicsemi.android.cgms.view.DisconnectEvent
import no.nordicsemi.android.cgms.view.NavigateUp
import no.nordicsemi.android.cgms.view.NoDeviceState
import no.nordicsemi.android.cgms.view.OnWorkingModeSelected
import no.nordicsemi.android.cgms.view.OpenLoggerEvent
import no.nordicsemi.android.cgms.view.WorkingState
import no.nordicsemi.android.common.navigation.NavigationResult
import no.nordicsemi.android.common.navigation.Navigator
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.service.ConnectedResult
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId
import javax.inject.Inject
@@ -69,7 +67,7 @@ internal class CGMViewModel @Inject constructor(
private val analytics: AppAnalytics
) : ViewModel() {
private val _state = MutableStateFlow<CGMViewState>(NoDeviceState)
private val _state = MutableStateFlow(CGMViewState())
val state = _state.asStateFlow()
init {
@@ -80,9 +78,9 @@ internal class CGMViewModel @Inject constructor(
}
repository.data.onEach {
_state.value = WorkingState(it)
_state.value = _state.value.copy(result = it)
(it as? ConnectedResult)?.let {
if (it.connectionState == GattConnectionState.STATE_CONNECTED) {
analytics.logEvent(ProfileConnectedEvent(Profile.CGMS))
}
}.launchIn(viewModelScope)
@@ -113,16 +111,10 @@ internal class CGMViewModel @Inject constructor(
}
private fun onCommandReceived(workingMode: CGMServiceCommand) {
when (workingMode) {
CGMServiceCommand.REQUEST_ALL_RECORDS -> repository.requestAllRecords()
CGMServiceCommand.REQUEST_LAST_RECORD -> repository.requestLastRecord()
CGMServiceCommand.REQUEST_FIRST_RECORD -> repository.requestFirstRecord()
CGMServiceCommand.DISCONNECT -> disconnect()
}
repository.onCommand(workingMode)
}
private fun disconnect() {
repository.release()
navigationManager.navigateUp()
}
}

View File

@@ -200,7 +200,7 @@ internal class GLSViewModel @Inject constructor(
.onEach { onAccessControlPointDataReceived(it) }
.launchIn(viewModelScope)
_state.value = _state.value.copy(deviceName = device.name)
_state.value = _state.value.copy(deviceName = device.name) //prevents UI from appearing before BLE connection is set up
}
private fun stopIfDisconnected(connectionState: GattConnectionState) {