mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-22 08:54:21 +01:00
Migrate CGM profile to new BLE library
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
)
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user