mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-22 00:44:26 +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.uiscanner)
|
||||
implementation(libs.nordic.navigation)
|
||||
implementation(libs.nordic.core)
|
||||
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
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 dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import no.nordicsemi.android.ble.ktx.suspend
|
||||
import no.nordicsemi.android.cgms.data.CGMData
|
||||
import no.nordicsemi.android.cgms.data.CGMManager
|
||||
import no.nordicsemi.android.common.logger.NordicLogger
|
||||
import no.nordicsemi.android.common.logger.NordicLoggerFactory
|
||||
import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber
|
||||
import no.nordicsemi.android.cgms.data.CGMServiceCommand
|
||||
import no.nordicsemi.android.cgms.data.CGMServiceData
|
||||
import no.nordicsemi.android.common.core.simpleSharedFlow
|
||||
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
|
||||
import no.nordicsemi.android.service.BleManagerResult
|
||||
import no.nordicsemi.android.service.IdleResult
|
||||
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus
|
||||
import no.nordicsemi.android.service.DisconnectAndStopEvent
|
||||
import no.nordicsemi.android.service.ServiceManager
|
||||
import no.nordicsemi.android.ui.view.StringConst
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -58,68 +54,53 @@ class CGMRepository @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val loggerFactory: NordicLoggerFactory,
|
||||
private val stringConst: StringConst
|
||||
) {
|
||||
private var manager: CGMManager? = null
|
||||
private var logger: NordicLogger? = null
|
||||
|
||||
private val _data = MutableStateFlow<BleManagerResult<CGMData>>(IdleResult())
|
||||
private val _data = MutableStateFlow(CGMServiceData())
|
||||
internal val data = _data.asStateFlow()
|
||||
|
||||
val isRunning = data.map { it.isRunning() }
|
||||
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
|
||||
private val _stopEvent = simpleSharedFlow<DisconnectAndStopEvent>()
|
||||
internal val stopEvent = _stopEvent.asSharedFlow()
|
||||
|
||||
private val _command = simpleSharedFlow<CGMServiceCommand>()
|
||||
internal val command = _command.asSharedFlow()
|
||||
|
||||
val isRunning = data.map { it.connectionState == GattConnectionState.STATE_CONNECTED }
|
||||
val hasRecords = data.value.records.isNotEmpty()
|
||||
val highestSequenceNumber = data.value.records.maxOfOrNull { it.sequenceNumber } ?: -1
|
||||
|
||||
fun launch(device: ServerDevice) {
|
||||
serviceManager.startService(CGMService::class.java, device)
|
||||
}
|
||||
|
||||
fun start(device: ServerDevice, scope: CoroutineScope) {
|
||||
val createdLogger = loggerFactory.create(stringConst.APP_NAME, "CGMS", device.address).also {
|
||||
logger = it
|
||||
}
|
||||
val manager = CGMManager(context, scope, createdLogger)
|
||||
this.manager = manager
|
||||
|
||||
manager.dataHolder.status.onEach {
|
||||
_data.value = it
|
||||
}.launchIn(scope)
|
||||
|
||||
scope.launch {
|
||||
manager.start(device)
|
||||
}
|
||||
fun onDataReceived(data: List<CGMRecordWithSequenceNumber>) {
|
||||
_data.value = _data.value.copy(records = _data.value.records + data)
|
||||
}
|
||||
|
||||
private suspend fun CGMManager.start(device: ServerDevice) {
|
||||
// try {
|
||||
// connect(device.device)
|
||||
// .useAutoConnect(false)
|
||||
// .retry(3, 100)
|
||||
// .suspend()
|
||||
// } catch (e: Exception) {
|
||||
// e.printStackTrace()
|
||||
// }
|
||||
internal fun onCommand(command: CGMServiceCommand) {
|
||||
_command.tryEmit(command)
|
||||
}
|
||||
|
||||
fun requestAllRecords() {
|
||||
manager?.requestAllRecords()
|
||||
fun onConnectionStateChanged(connectionState: GattConnectionState?) {
|
||||
_data.value = _data.value.copy(connectionState = connectionState)
|
||||
}
|
||||
|
||||
fun requestLastRecord() {
|
||||
manager?.requestLastRecord()
|
||||
fun onBatteryLevelChanged(batteryLevel: Int) {
|
||||
_data.value = _data.value.copy(batteryLevel = batteryLevel)
|
||||
}
|
||||
|
||||
fun requestFirstRecord() {
|
||||
manager?.requestFirstRecord()
|
||||
fun onNewRequestStatus(requestStatus: RequestStatus) {
|
||||
_data.value = _data.value.copy(requestStatus = requestStatus)
|
||||
}
|
||||
|
||||
fun openLogger() {
|
||||
NordicLogger.launch(context, logger)
|
||||
TODO()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_data.value = _data.value.copy(records = emptyList())
|
||||
}
|
||||
|
||||
fun release() {
|
||||
manager?.disconnect()?.enqueue()
|
||||
logger = null
|
||||
manager = null
|
||||
_stopEvent.tryEmit(DisconnectAndStopEvent())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,33 +31,263 @@
|
||||
|
||||
package no.nordicsemi.android.cgms.repository
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import no.nordicsemi.android.ble.common.data.cgm.CGMSpecificOpsControlPointData
|
||||
import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber
|
||||
import no.nordicsemi.android.cgms.data.CGMServiceCommand
|
||||
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
|
||||
import no.nordicsemi.android.kotlin.ble.core.client.callback.BleGattClient
|
||||
import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattCharacteristic
|
||||
import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattServices
|
||||
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
|
||||
import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser
|
||||
import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMFeatureParser
|
||||
import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMMeasurementParser
|
||||
import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMSpecificOpsControlPointParser
|
||||
import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMStatusParser
|
||||
import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMErrorCode
|
||||
import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMOpCode
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointInputParser
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointParser
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.data.NumberOfRecordsData
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RecordAccessControlPointData
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.data.ResponseData
|
||||
import no.nordicsemi.android.kotlin.ble.profile.racp.RACPOpCode
|
||||
import no.nordicsemi.android.kotlin.ble.profile.racp.RACPResponseCode
|
||||
import no.nordicsemi.android.service.DEVICE_DATA
|
||||
import no.nordicsemi.android.service.NotificationService
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
val CGMS_SERVICE_UUID: UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb")
|
||||
private val CGM_STATUS_UUID = UUID.fromString("00002AA9-0000-1000-8000-00805f9b34fb")
|
||||
private val CGM_FEATURE_UUID = UUID.fromString("00002AA8-0000-1000-8000-00805f9b34fb")
|
||||
private val CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb")
|
||||
private val CGM_OPS_CONTROL_POINT_UUID = UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
private val RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
|
||||
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@AndroidEntryPoint
|
||||
internal class CGMService : NotificationService() {
|
||||
|
||||
@Inject
|
||||
lateinit var repository: CGMRepository
|
||||
|
||||
private lateinit var client: BleGattClient
|
||||
|
||||
private var secured = false
|
||||
|
||||
private var recordAccessRequestInProgress = false
|
||||
|
||||
private var sessionStartTime: Long = 0
|
||||
|
||||
private lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
val device = intent!!.getParcelableExtra<ServerDevice>(DEVICE_DATA)!!
|
||||
|
||||
repository.start(device, lifecycleScope)
|
||||
startGattClient(device)
|
||||
|
||||
repository.hasBeenDisconnected.onEach {
|
||||
if (it) stopSelf()
|
||||
}.launchIn(lifecycleScope)
|
||||
repository.stopEvent
|
||||
.onEach { disconnect() }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
repository.command
|
||||
.onEach { onCommand(it) }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
private fun onCommand(command: CGMServiceCommand) = lifecycleScope.launch{
|
||||
when (command) {
|
||||
CGMServiceCommand.REQUEST_ALL_RECORDS -> requestAllRecords()
|
||||
CGMServiceCommand.REQUEST_LAST_RECORD -> requestLastRecord()
|
||||
CGMServiceCommand.REQUEST_FIRST_RECORD -> requestFirstRecord()
|
||||
CGMServiceCommand.DISCONNECT -> client.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startGattClient(device: ServerDevice) = lifecycleScope.launch {
|
||||
client = device.connect(this@CGMService)
|
||||
|
||||
client.connectionState
|
||||
.onEach { repository.onConnectionStateChanged(it) }
|
||||
.filterNotNull()
|
||||
.onEach { stopIfDisconnected(it) }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
client.services
|
||||
.filterNotNull()
|
||||
.onEach { configureGatt(it) }
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private suspend fun configureGatt(services: BleGattServices) {
|
||||
val glsService = services.findService(CGMS_SERVICE_UUID)!!
|
||||
val statusCharacteristic = glsService.findCharacteristic(CGM_STATUS_UUID)!!
|
||||
val featureCharacteristic = glsService.findCharacteristic(CGM_FEATURE_UUID)!!
|
||||
val measurementCharacteristic = glsService.findCharacteristic(CGM_MEASUREMENT_UUID)!!
|
||||
val opsControlPointCharacteristic = glsService.findCharacteristic(CGM_OPS_CONTROL_POINT_UUID)!!
|
||||
recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_UUID)!!
|
||||
val batteryService = services.findService(BATTERY_SERVICE_UUID)!!
|
||||
val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!!
|
||||
|
||||
batteryLevelCharacteristic.getNotifications()
|
||||
.mapNotNull { BatteryLevelParser.parse(it) }
|
||||
.onEach { repository.onBatteryLevelChanged(it) }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
measurementCharacteristic.getNotifications()
|
||||
.mapNotNull { CGMMeasurementParser.parse(it) }
|
||||
.onEach {
|
||||
if (sessionStartTime == 0L && !recordAccessRequestInProgress) {
|
||||
val timeOffset = it.minOf { it.timeOffset }
|
||||
sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L
|
||||
}
|
||||
|
||||
val result = it.map {
|
||||
val timestamp = sessionStartTime + it.timeOffset * 60000L
|
||||
CGMRecordWithSequenceNumber(it.timeOffset, it, timestamp)
|
||||
}
|
||||
|
||||
repository.onDataReceived(result)
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
opsControlPointCharacteristic.getNotifications()
|
||||
.mapNotNull { CGMSpecificOpsControlPointParser.parse(it) }
|
||||
.onEach {
|
||||
if (it.isOperationCompleted) {
|
||||
if (it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION) {
|
||||
sessionStartTime = System.currentTimeMillis()
|
||||
} else {
|
||||
sessionStartTime = 0
|
||||
}
|
||||
} else {
|
||||
if (it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION && it.errorCode == CGMErrorCode.CGM_ERROR_PROCEDURE_NOT_COMPLETED) {
|
||||
sessionStartTime = 0
|
||||
} else if (it.requestCode == CGMOpCode.CGM_OP_CODE_STOP_SESSION) {
|
||||
sessionStartTime = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
recordAccessControlPointCharacteristic.getNotifications()
|
||||
.mapNotNull { RecordAccessControlPointParser.parse(it) }
|
||||
.onEach { onAccessControlPointDataReceived(it) }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
val featuresEnvelope = featureCharacteristic.read().let { CGMFeatureParser.parse(it) }!!
|
||||
secured = featuresEnvelope.features.e2eCrcSupported
|
||||
|
||||
val statusEnvelope = statusCharacteristic.read().let { CGMStatusParser.parse(it) }!!
|
||||
if (!statusEnvelope.status.sessionStopped) {
|
||||
sessionStartTime = System.currentTimeMillis() - statusEnvelope.timeOffset * 60000L
|
||||
}
|
||||
|
||||
if (sessionStartTime == 0L) {
|
||||
opsControlPointCharacteristic.write(CGMSpecificOpsControlPointData.startSession(secured).value!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAccessControlPointDataReceived(data: RecordAccessControlPointData) = lifecycleScope.launch {
|
||||
when (data) {
|
||||
is NumberOfRecordsData -> onNumberOfRecordsReceived(data.numberOfRecords)
|
||||
is ResponseData -> when (data.responseCode) {
|
||||
RACPResponseCode.RACP_RESPONSE_SUCCESS -> onRecordAccessOperationCompleted(data.requestCode)
|
||||
RACPResponseCode.RACP_ERROR_NO_RECORDS_FOUND -> onRecordAccessOperationCompletedWithNoRecordsFound()
|
||||
RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED,
|
||||
RACPResponseCode.RACP_ERROR_INVALID_OPERATOR,
|
||||
RACPResponseCode.RACP_ERROR_OPERATOR_NOT_SUPPORTED,
|
||||
RACPResponseCode.RACP_ERROR_INVALID_OPERAND,
|
||||
RACPResponseCode.RACP_ERROR_ABORT_UNSUCCESSFUL,
|
||||
RACPResponseCode.RACP_ERROR_PROCEDURE_NOT_COMPLETED,
|
||||
RACPResponseCode.RACP_ERROR_OPERAND_NOT_SUPPORTED -> onRecordAccessOperationError(data.responseCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationCompleted(requestCode: RACPOpCode) {
|
||||
val status = when (requestCode) {
|
||||
RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
|
||||
else -> RequestStatus.SUCCESS
|
||||
}
|
||||
repository.onNewRequestStatus(status)
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationCompletedWithNoRecordsFound() {
|
||||
repository.onNewRequestStatus(RequestStatus.SUCCESS)
|
||||
}
|
||||
|
||||
private suspend fun onNumberOfRecordsReceived(numberOfRecords: Int) {
|
||||
if (numberOfRecords > 0) {
|
||||
if (repository.hasRecords) {
|
||||
recordAccessControlPointCharacteristic.write(
|
||||
RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(repository.highestSequenceNumber).value
|
||||
)
|
||||
} else {
|
||||
recordAccessControlPointCharacteristic.write(
|
||||
RecordAccessControlPointInputParser.reportAllStoredRecords().value
|
||||
)
|
||||
}
|
||||
}
|
||||
repository.onNewRequestStatus(RequestStatus.SUCCESS)
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationError(response: RACPResponseCode) {
|
||||
if (response == RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
|
||||
repository.onNewRequestStatus(RequestStatus.NOT_SUPPORTED)
|
||||
} else {
|
||||
repository.onNewRequestStatus(RequestStatus.FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
repository.clear()
|
||||
}
|
||||
|
||||
private suspend fun requestLastRecord() {
|
||||
clear()
|
||||
repository.onNewRequestStatus(RequestStatus.PENDING)
|
||||
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportLastStoredRecord().value)
|
||||
}
|
||||
|
||||
private suspend fun requestFirstRecord() {
|
||||
clear()
|
||||
repository.onNewRequestStatus(RequestStatus.PENDING)
|
||||
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportFirstStoredRecord().value)
|
||||
}
|
||||
|
||||
private suspend fun requestAllRecords() {
|
||||
clear()
|
||||
repository.onNewRequestStatus(RequestStatus.PENDING)
|
||||
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value)
|
||||
}
|
||||
|
||||
private fun stopIfDisconnected(connectionState: GattConnectionState) {
|
||||
if (connectionState == GattConnectionState.STATE_DISCONNECTED) {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnect() {
|
||||
client.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,16 +52,16 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.cgms.R
|
||||
import no.nordicsemi.android.cgms.data.CGMData
|
||||
import no.nordicsemi.android.cgms.data.CGMRecord
|
||||
import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber
|
||||
import no.nordicsemi.android.cgms.data.CGMServiceCommand
|
||||
import no.nordicsemi.android.cgms.data.RequestStatus
|
||||
import no.nordicsemi.android.cgms.data.CGMServiceData
|
||||
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus
|
||||
import no.nordicsemi.android.ui.view.BatteryLevelView
|
||||
import no.nordicsemi.android.ui.view.ScreenSection
|
||||
import no.nordicsemi.android.ui.view.SectionTitle
|
||||
|
||||
@Composable
|
||||
internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
|
||||
internal fun CGMContentView(state: CGMServiceData, onEvent: (CGMViewEvent) -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
@@ -91,7 +91,7 @@ internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
|
||||
private fun SettingsView(state: CGMServiceData, onEvent: (CGMViewEvent) -> Unit) {
|
||||
ScreenSection {
|
||||
SectionTitle(icon = Icons.Default.Settings, title = "Request items")
|
||||
|
||||
@@ -119,7 +119,7 @@ private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecordsView(state: CGMData) {
|
||||
private fun RecordsView(state: CGMServiceData) {
|
||||
ScreenSection {
|
||||
if (state.records.isEmpty()) {
|
||||
RecordsViewWithoutData()
|
||||
@@ -131,7 +131,7 @@ private fun RecordsView(state: CGMData) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecordsViewWithData(state: CGMData) {
|
||||
private fun RecordsViewWithData(state: CGMServiceData) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
SectionTitle(resId = R.drawable.ic_records, title = "Records")
|
||||
|
||||
@@ -148,7 +148,7 @@ private fun RecordsViewWithData(state: CGMData) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecordItem(record: CGMRecord) {
|
||||
private fun RecordItem(record: CGMRecordWithSequenceNumber) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -34,16 +34,17 @@ package no.nordicsemi.android.cgms.view
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import no.nordicsemi.android.cgms.R
|
||||
import no.nordicsemi.android.cgms.data.CGMRecord
|
||||
import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber
|
||||
import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMRecord
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
internal fun CGMRecord.formattedTime(): String {
|
||||
internal fun CGMRecordWithSequenceNumber.formattedTime(): String {
|
||||
val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US)
|
||||
return timeFormat.format(Date(timestamp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun CGMRecord.glucoseConcentration(): String {
|
||||
return stringResource(id = R.string.cgms_value_unit, glucoseConcentration)
|
||||
internal fun CGMRecordWithSequenceNumber.glucoseConcentration(): String {
|
||||
return stringResource(id = R.string.cgms_value_unit, record.glucoseConcentration)
|
||||
}
|
||||
|
||||
@@ -48,15 +48,7 @@ import no.nordicsemi.android.cgms.viewmodel.CGMViewModel
|
||||
import no.nordicsemi.android.common.ui.scanner.view.DeviceConnectingView
|
||||
import no.nordicsemi.android.common.ui.scanner.view.DeviceDisconnectedView
|
||||
import no.nordicsemi.android.common.ui.scanner.view.Reason
|
||||
import no.nordicsemi.android.service.ConnectedResult
|
||||
import no.nordicsemi.android.service.ConnectingResult
|
||||
import no.nordicsemi.android.service.DeviceHolder
|
||||
import no.nordicsemi.android.service.DisconnectedResult
|
||||
import no.nordicsemi.android.service.IdleResult
|
||||
import no.nordicsemi.android.service.LinkLossResult
|
||||
import no.nordicsemi.android.service.MissingServiceResult
|
||||
import no.nordicsemi.android.service.SuccessResult
|
||||
import no.nordicsemi.android.service.UnknownErrorResult
|
||||
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
|
||||
import no.nordicsemi.android.ui.view.BackIconAppBar
|
||||
import no.nordicsemi.android.ui.view.LoggerIconAppBar
|
||||
import no.nordicsemi.android.ui.view.NavigateUpButton
|
||||
@@ -78,17 +70,15 @@ fun CGMScreen() {
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when (state) {
|
||||
NoDeviceState -> DeviceConnectingView()
|
||||
is WorkingState -> when (state.result) {
|
||||
is IdleResult,
|
||||
is ConnectingResult -> DeviceConnectingView { NavigateUpButton(navigateUp) }
|
||||
is ConnectedResult -> DeviceConnectingView { NavigateUpButton(navigateUp) }
|
||||
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER) { NavigateUpButton(navigateUp) }
|
||||
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS) { NavigateUpButton(navigateUp) }
|
||||
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE) { NavigateUpButton(navigateUp) }
|
||||
is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
|
||||
is SuccessResult -> CGMContentView(state.result.data) { viewModel.onEvent(it) }
|
||||
if (state.deviceName == null) {
|
||||
DeviceConnectingView()
|
||||
} else {
|
||||
when (state.result?.connectionState) {
|
||||
null,
|
||||
GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) }
|
||||
GattConnectionState.STATE_DISCONNECTED,
|
||||
GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
|
||||
GattConnectionState.STATE_CONNECTED -> CGMContentView(state.result) { viewModel.onEvent(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,15 +87,11 @@ fun CGMScreen() {
|
||||
|
||||
@Composable
|
||||
private fun AppBar(state: CGMViewState, navigateUp: () -> Unit, viewModel: CGMViewModel) {
|
||||
val toolbarName = (state as? WorkingState)?.let {
|
||||
(it.result as? DeviceHolder)?.deviceName()
|
||||
}
|
||||
|
||||
if (toolbarName == null) {
|
||||
BackIconAppBar(stringResource(id = R.string.cgms_title), navigateUp)
|
||||
} else {
|
||||
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
|
||||
if (state.deviceName?.isNotBlank() == true) {
|
||||
LoggerIconAppBar(state.deviceName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
|
||||
viewModel.onEvent(OpenLoggerEvent)
|
||||
}
|
||||
} else {
|
||||
BackIconAppBar(stringResource(id = R.string.cgms_title), navigateUp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,9 @@
|
||||
|
||||
package no.nordicsemi.android.cgms.view
|
||||
|
||||
import no.nordicsemi.android.cgms.data.CGMData
|
||||
import no.nordicsemi.android.service.BleManagerResult
|
||||
import no.nordicsemi.android.cgms.data.CGMServiceData
|
||||
|
||||
internal sealed class CGMViewState
|
||||
|
||||
internal data class WorkingState(val result: BleManagerResult<CGMData>) : CGMViewState()
|
||||
internal object NoDeviceState : CGMViewState()
|
||||
internal data class CGMViewState(
|
||||
val result: CGMServiceData? = null,
|
||||
val deviceName: String? = null
|
||||
)
|
||||
|
||||
@@ -44,21 +44,19 @@ import kotlinx.coroutines.launch
|
||||
import no.nordicsemi.android.analytics.AppAnalytics
|
||||
import no.nordicsemi.android.analytics.Profile
|
||||
import no.nordicsemi.android.analytics.ProfileConnectedEvent
|
||||
import no.nordicsemi.android.cgms.data.CGMS_SERVICE_UUID
|
||||
import no.nordicsemi.android.cgms.data.CGMServiceCommand
|
||||
import no.nordicsemi.android.cgms.repository.CGMRepository
|
||||
import no.nordicsemi.android.cgms.repository.CGMS_SERVICE_UUID
|
||||
import no.nordicsemi.android.cgms.view.CGMViewEvent
|
||||
import no.nordicsemi.android.cgms.view.CGMViewState
|
||||
import no.nordicsemi.android.cgms.view.DisconnectEvent
|
||||
import no.nordicsemi.android.cgms.view.NavigateUp
|
||||
import no.nordicsemi.android.cgms.view.NoDeviceState
|
||||
import no.nordicsemi.android.cgms.view.OnWorkingModeSelected
|
||||
import no.nordicsemi.android.cgms.view.OpenLoggerEvent
|
||||
import no.nordicsemi.android.cgms.view.WorkingState
|
||||
import no.nordicsemi.android.common.navigation.NavigationResult
|
||||
import no.nordicsemi.android.common.navigation.Navigator
|
||||
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
|
||||
import no.nordicsemi.android.service.ConnectedResult
|
||||
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
|
||||
import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -69,7 +67,7 @@ internal class CGMViewModel @Inject constructor(
|
||||
private val analytics: AppAnalytics
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow<CGMViewState>(NoDeviceState)
|
||||
private val _state = MutableStateFlow(CGMViewState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
@@ -80,9 +78,9 @@ internal class CGMViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
repository.data.onEach {
|
||||
_state.value = WorkingState(it)
|
||||
_state.value = _state.value.copy(result = it)
|
||||
|
||||
(it as? ConnectedResult)?.let {
|
||||
if (it.connectionState == GattConnectionState.STATE_CONNECTED) {
|
||||
analytics.logEvent(ProfileConnectedEvent(Profile.CGMS))
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
@@ -113,16 +111,10 @@ internal class CGMViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun onCommandReceived(workingMode: CGMServiceCommand) {
|
||||
when (workingMode) {
|
||||
CGMServiceCommand.REQUEST_ALL_RECORDS -> repository.requestAllRecords()
|
||||
CGMServiceCommand.REQUEST_LAST_RECORD -> repository.requestLastRecord()
|
||||
CGMServiceCommand.REQUEST_FIRST_RECORD -> repository.requestFirstRecord()
|
||||
CGMServiceCommand.DISCONNECT -> disconnect()
|
||||
}
|
||||
repository.onCommand(workingMode)
|
||||
}
|
||||
|
||||
private fun disconnect() {
|
||||
repository.release()
|
||||
navigationManager.navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ internal class GLSViewModel @Inject constructor(
|
||||
.onEach { onAccessControlPointDataReceived(it) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
_state.value = _state.value.copy(deviceName = device.name)
|
||||
_state.value = _state.value.copy(deviceName = device.name) //prevents UI from appearing before BLE connection is set up
|
||||
}
|
||||
|
||||
private fun stopIfDisconnected(connectionState: GattConnectionState) {
|
||||
|
||||
Reference in New Issue
Block a user