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

View File

@@ -31,33 +31,263 @@
package no.nordicsemi.android.cgms.repository package no.nordicsemi.android.cgms.repository
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach 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.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.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService import no.nordicsemi.android.service.NotificationService
import java.util.*
import javax.inject.Inject 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 @AndroidEntryPoint
internal class CGMService : NotificationService() { internal class CGMService : NotificationService() {
@Inject @Inject
lateinit var repository: CGMRepository 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 { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<ServerDevice>(DEVICE_DATA)!! val device = intent!!.getParcelableExtra<ServerDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope) startGattClient(device)
repository.hasBeenDisconnected.onEach { repository.stopEvent
if (it) stopSelf() .onEach { disconnect() }
}.launchIn(lifecycleScope) .launchIn(lifecycleScope)
repository.command
.onEach { onCommand(it) }
.launchIn(lifecycleScope)
return START_REDELIVER_INTENT 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import no.nordicsemi.android.cgms.R import no.nordicsemi.android.cgms.R
import no.nordicsemi.android.cgms.data.CGMData import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber
import no.nordicsemi.android.cgms.data.CGMRecord
import no.nordicsemi.android.cgms.data.CGMServiceCommand 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.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
@Composable @Composable
internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) { internal fun CGMContentView(state: CGMServiceData, onEvent: (CGMViewEvent) -> Unit) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
@@ -91,7 +91,7 @@ internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
} }
@Composable @Composable
private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) { private fun SettingsView(state: CGMServiceData, onEvent: (CGMViewEvent) -> Unit) {
ScreenSection { ScreenSection {
SectionTitle(icon = Icons.Default.Settings, title = "Request items") SectionTitle(icon = Icons.Default.Settings, title = "Request items")
@@ -119,7 +119,7 @@ private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
} }
@Composable @Composable
private fun RecordsView(state: CGMData) { private fun RecordsView(state: CGMServiceData) {
ScreenSection { ScreenSection {
if (state.records.isEmpty()) { if (state.records.isEmpty()) {
RecordsViewWithoutData() RecordsViewWithoutData()
@@ -131,7 +131,7 @@ private fun RecordsView(state: CGMData) {
} }
@Composable @Composable
private fun RecordsViewWithData(state: CGMData) { private fun RecordsViewWithData(state: CGMServiceData) {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
SectionTitle(resId = R.drawable.ic_records, title = "Records") SectionTitle(resId = R.drawable.ic_records, title = "Records")
@@ -148,7 +148,7 @@ private fun RecordsViewWithData(state: CGMData) {
} }
@Composable @Composable
private fun RecordItem(record: CGMRecord) { private fun RecordItem(record: CGMRecordWithSequenceNumber) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Column( Column(
modifier = Modifier modifier = Modifier

View File

@@ -34,16 +34,17 @@ package no.nordicsemi.android.cgms.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.cgms.R 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.text.SimpleDateFormat
import java.util.* import java.util.*
internal fun CGMRecord.formattedTime(): String { internal fun CGMRecordWithSequenceNumber.formattedTime(): String {
val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US) val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US)
return timeFormat.format(Date(timestamp)) return timeFormat.format(Date(timestamp))
} }
@Composable @Composable
internal fun CGMRecord.glucoseConcentration(): String { internal fun CGMRecordWithSequenceNumber.glucoseConcentration(): String {
return stringResource(id = R.string.cgms_value_unit, glucoseConcentration) 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.DeviceConnectingView
import no.nordicsemi.android.common.ui.scanner.view.DeviceDisconnectedView 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.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,17 +70,15 @@ fun CGMScreen() {
.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.result?.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) { NavigateUpButton(navigateUp) } GattConnectionState.STATE_CONNECTED -> CGMContentView(state.result) { viewModel.onEvent(it) }
is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
is SuccessResult -> CGMContentView(state.result.data) { viewModel.onEvent(it) }
} }
} }
} }
@@ -97,15 +87,11 @@ fun CGMScreen() {
@Composable @Composable
private fun AppBar(state: CGMViewState, navigateUp: () -> Unit, viewModel: CGMViewModel) { private fun AppBar(state: CGMViewState, navigateUp: () -> Unit, viewModel: CGMViewModel) {
val toolbarName = (state as? WorkingState)?.let { if (state.deviceName?.isNotBlank() == true) {
(it.result as? DeviceHolder)?.deviceName() LoggerIconAppBar(state.deviceName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.cgms_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent) viewModel.onEvent(OpenLoggerEvent)
} }
} else {
BackIconAppBar(stringResource(id = R.string.cgms_title), navigateUp)
} }
} }

View File

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

View File

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

View File

@@ -200,7 +200,7 @@ internal class GLSViewModel @Inject constructor(
.onEach { onAccessControlPointDataReceived(it) } .onEach { onAccessControlPointDataReceived(it) }
.launchIn(viewModelScope) .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) { private fun stopIfDisconnected(connectionState: GattConnectionState) {