From 69f085a83c5a1576c96bceddae986590aafe1d2f Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Sun, 4 Dec 2022 17:18:01 +0100 Subject: [PATCH] combov2: Use extra internal SP as the main pump state store Signed-off-by: Carlos Rafael Giani --- .../pump/combov2/AAPSPumpStateStore.kt | 233 ++++++++++++++++++ .../nightscout/pump/combov2/ComboV2Plugin.kt | 45 +++- .../nightscout/pump/combov2/InternalSP.kt | 199 +++++++++++++++ .../pump/combov2/SPPumpStateStore.kt | 165 ------------- 4 files changed, 476 insertions(+), 166 deletions(-) create mode 100644 pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/AAPSPumpStateStore.kt create mode 100644 pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/InternalSP.kt delete mode 100644 pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/SPPumpStateStore.kt diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/AAPSPumpStateStore.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/AAPSPumpStateStore.kt new file mode 100644 index 0000000000..163b7518c3 --- /dev/null +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/AAPSPumpStateStore.kt @@ -0,0 +1,233 @@ +package info.nightscout.pump.combov2 + +import info.nightscout.comboctl.base.BluetoothAddress +import info.nightscout.comboctl.base.CurrentTbrState +import info.nightscout.comboctl.base.InvariantPumpData +import info.nightscout.comboctl.base.Nonce +import info.nightscout.comboctl.base.PumpStateStore +import info.nightscout.comboctl.base.Tbr +import info.nightscout.comboctl.base.toBluetoothAddress +import info.nightscout.comboctl.base.toCipher +import info.nightscout.comboctl.base.toNonce +import info.nightscout.shared.sharedPreferences.SP +import info.nightscout.shared.sharedPreferences.SPDelegateInt +import info.nightscout.shared.sharedPreferences.SPDelegateLong +import info.nightscout.shared.sharedPreferences.SPDelegateString +import kotlinx.datetime.Instant +import kotlinx.datetime.UtcOffset +import kotlin.reflect.KClassifier + +/** + * Special pump state store that mainly uses the internalSP, but also is able to sync up the AAPS main SP with that internal SP. + * + * This pump state store solves a problem: What if the user already paired AAPS with a Combo, and then imports and old AAPS + * settings file, intending to just restore other settings like the basal profile, but does _not_ intend to import an old + * pump state? After all, if we write a pump state to the AAPS main SP, the state values will also be written into a settings + * file when exporting said settings. If we just relied on the main AAPS SP, the current pump state - including pairing info + * like the PC and CP keys - would be overwritten with the old ones from the imported settings, and the user would have to + * unnecessarily re-pair the Combo every time settings get imported. + * + * The solution is for this driver to _not_ primarily use the AAPS main SP. Instead, it uses its own internal SP. That SP + * is exclusively used by this driver. But, a copy of the pump state values are stored in the AAPS main SP, and kept in + * sync with the contents of the internal SP. + * + * When a new pump state is created, the invariant values (CP/PC keys etc.) are written in both the AAPS main SP and the + * internal SP, along with the initial nonce. During operation, the driver will update the nonce value of the internal SP. + * If TBR states are changed, then these changes are stored in the internal SP. Callers can use [copyVariantValuesToAAPSMainSP] + * to update the corresponding values in the AAPS main SP to keep the two SPs in sync. + * + * There are a couple of special situations: + * + * 1. There is no pump state in the internal SP, and there is no pump state in the AAPS main SP. This is the unpaired state. + * 2. There is no pump state in the internal SP, but there is one in the AAPS main SP. This typically happens when the + * user (re)installed AAPS, and immediately after installing, imported AAPS settings. Callers are then supposed to call + * [copyAllValuesFromAAPSMainSP] to import the pump state from the AAPS main SP over to the internal SP and continue to + * work with that state. + * 3. There is a pump state, and there is also one in the AAPS main SP. The latter one is then ignored. A pump state in the + * AAPS main SP solely and only exists to be able to export/import pump states. It is not used for actual pump operations. + * In particular, if - as mentioned above - a pump is already paired, and the user imports settings, this logic prevents + * the current pump state to be overwritten. + */ +class AAPSPumpStateStore( + private val aapsMainSP: SP, + private val internalSP: InternalSP +) : PumpStateStore { + private var btAddress: String + by SPDelegateString(internalSP, PreferenceKeys.BT_ADDRESS_KEY.str, "") + + // The nonce is updated with commit instead of apply to make sure + // is atomically written to storage synchronously, minimizing + // the likelihood that it could be lost due to app crashes etc. + // It is very important to not lose the nonce, hence that choice. + private var nonceString: String + by SPDelegateString(internalSP, PreferenceKeys.NONCE_KEY.str, Nonce.nullNonce().toString(), commit = true) + + private var cpCipherString: String + by SPDelegateString(internalSP, PreferenceKeys.CP_CIPHER_KEY.str, "") + private var pcCipherString: String + by SPDelegateString(internalSP, PreferenceKeys.PC_CIPHER_KEY.str, "") + private var keyResponseAddressInt: Int + by SPDelegateInt(internalSP, PreferenceKeys.KEY_RESPONSE_ADDRESS_KEY.str, 0) + private var pumpID: String + by SPDelegateString(internalSP, PreferenceKeys.PUMP_ID_KEY.str, "") + private var tbrTimestamp: Long + by SPDelegateLong(internalSP, PreferenceKeys.TBR_TIMESTAMP_KEY.str, 0) + private var tbrPercentage: Int + by SPDelegateInt(internalSP, PreferenceKeys.TBR_PERCENTAGE_KEY.str, 0) + private var tbrDuration: Int + by SPDelegateInt(internalSP, PreferenceKeys.TBR_DURATION_KEY.str, 0) + private var tbrType: String + by SPDelegateString(internalSP, PreferenceKeys.TBR_TYPE_KEY.str, "") + private var utcOffsetSeconds: Int + by SPDelegateInt(internalSP, PreferenceKeys.UTC_OFFSET_KEY.str, 0) + + override fun createPumpState( + pumpAddress: BluetoothAddress, + invariantPumpData: InvariantPumpData, + utcOffset: UtcOffset, + tbrState: CurrentTbrState + ) { + internalSP.edit(commit = true) { + putString(PreferenceKeys.BT_ADDRESS_KEY.str, pumpAddress.toString().uppercase()) + putString(PreferenceKeys.CP_CIPHER_KEY.str, invariantPumpData.clientPumpCipher.toString()) + putString(PreferenceKeys.PC_CIPHER_KEY.str, invariantPumpData.pumpClientCipher.toString()) + putInt(PreferenceKeys.KEY_RESPONSE_ADDRESS_KEY.str, invariantPumpData.keyResponseAddress.toInt() and 0xFF) + putString(PreferenceKeys.PUMP_ID_KEY.str, invariantPumpData.pumpID) + putLong(PreferenceKeys.TBR_TIMESTAMP_KEY.str, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.timestamp.epochSeconds else -1) + putInt(PreferenceKeys.TBR_PERCENTAGE_KEY.str, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.percentage else -1) + putInt(PreferenceKeys.TBR_DURATION_KEY.str, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.durationInMinutes else -1) + putString(PreferenceKeys.TBR_TYPE_KEY.str, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.type.stringId else "") + putInt(PreferenceKeys.UTC_OFFSET_KEY.str, utcOffset.totalSeconds) + } + + copyAllValuesToAAPSMainSP(commit = true) + } + + override fun deletePumpState(pumpAddress: BluetoothAddress): Boolean { + val hasState = internalSP.contains(PreferenceKeys.NONCE_KEY.str) + + internalSP.edit(commit = true) { + for (keys in PreferenceKeys.values()) + remove(keys.str) + } + + aapsMainSP.edit(commit = true) { + for (keys in PreferenceKeys.values()) + remove(keys.str) + } + + return hasState + } + + override fun hasPumpState(pumpAddress: BluetoothAddress): Boolean = + internalSP.contains(PreferenceKeys.NONCE_KEY.str) + + override fun getAvailablePumpStateAddresses(): Set = + if (btAddress.isBlank()) setOf() else setOf(btAddress.toBluetoothAddress()) + + override fun getInvariantPumpData(pumpAddress: BluetoothAddress) = InvariantPumpData( + clientPumpCipher = cpCipherString.toCipher(), + pumpClientCipher = pcCipherString.toCipher(), + keyResponseAddress = keyResponseAddressInt.toByte(), + pumpID = pumpID + ) + + override fun getCurrentTxNonce(pumpAddress: BluetoothAddress) = nonceString.toNonce() + + override fun setCurrentTxNonce(pumpAddress: BluetoothAddress, currentTxNonce: Nonce) { + nonceString = currentTxNonce.toString() + } + + override fun getCurrentUtcOffset(pumpAddress: BluetoothAddress) = + UtcOffset(seconds = utcOffsetSeconds) + + override fun setCurrentUtcOffset(pumpAddress: BluetoothAddress, utcOffset: UtcOffset) { + utcOffsetSeconds = utcOffset.totalSeconds + } + + override fun getCurrentTbrState(pumpAddress: BluetoothAddress) = + if (tbrTimestamp >= 0) + CurrentTbrState.TbrStarted(Tbr( + timestamp = Instant.fromEpochSeconds(tbrTimestamp), + percentage = tbrPercentage, + durationInMinutes = tbrDuration, + type = Tbr.Type.fromStringId(tbrType)!! + )) + else + CurrentTbrState.NoTbrOngoing + + + override fun setCurrentTbrState(pumpAddress: BluetoothAddress, currentTbrState: CurrentTbrState) { + when (currentTbrState) { + is CurrentTbrState.TbrStarted -> { + tbrTimestamp = currentTbrState.tbr.timestamp.epochSeconds + tbrPercentage = currentTbrState.tbr.percentage + tbrDuration = currentTbrState.tbr.durationInMinutes + tbrType = currentTbrState.tbr.type.stringId + } + else -> { + tbrTimestamp = -1 + tbrPercentage = -1 + tbrDuration = -1 + tbrType = "" + } + } + } + + // Copies only those pump state values from the internal SP to the AAPS main SP which can vary during + // pump operations. These are the TBR values, the UTC offset, and the nonce. Users are recommended to + // call this after AAPS disconnects the pump. + fun copyVariantValuesToAAPSMainSP(commit: Boolean) = + copyValuesBetweenSPs(commit, from = internalSP, to = aapsMainSP, arrayOf( + PreferenceKeys.NONCE_KEY, + PreferenceKeys.TBR_TIMESTAMP_KEY, + PreferenceKeys.TBR_PERCENTAGE_KEY, + PreferenceKeys.TBR_DURATION_KEY, + PreferenceKeys.TBR_TYPE_KEY, + PreferenceKeys.UTC_OFFSET_KEY + )) + + // Copies all pump state values from the AAPS main SP to the internal SP. This is supposed to be + // called if the internal SP is empty. That way, a pump state can be imported from AAPS settings files. + fun copyAllValuesFromAAPSMainSP(commit: Boolean) = + copyValuesBetweenSPs(commit, from = aapsMainSP, to = internalSP, keys = PreferenceKeys.values()) + + // Copies all pump state values from the internal SP to the AAPS main SP to. The createPumpState() + // function calls this after creating the pump state to ensure both SPs are in sync. Also, this + // should be called when the driver starts in case AAPS settings are imported and there is already + // a pump state present in the internal SP. Calling this then ensures that the pump state in the + // main SP is fully synced up with the one from the internal SP, and does not contain some old + // state that is not in use anymore. + fun copyAllValuesToAAPSMainSP(commit: Boolean) = + copyValuesBetweenSPs(commit, from = internalSP, to = aapsMainSP, keys = PreferenceKeys.values()) + + private fun copyValuesBetweenSPs(commit: Boolean, from: SP, to: SP, keys: Array) { + to.edit(commit) { + for (key in keys) { + if (!from.contains(key.str)) + continue + when (key.type) { + Int::class -> putInt(key.str, from.getInt(key.str, 0)) + Long::class -> putLong(key.str, from.getLong(key.str, 0L)) + String::class -> putString(key.str, from.getString(key.str, "")) + } + } + } + } + + private enum class PreferenceKeys(val str: String, val type: KClassifier) { + BT_ADDRESS_KEY("combov2-bt-address-key", String::class), + NONCE_KEY("combov2-nonce-key", String::class), + CP_CIPHER_KEY("combov2-cp-cipher-key", String::class), + PC_CIPHER_KEY("combov2-pc-cipher-key", String::class), + KEY_RESPONSE_ADDRESS_KEY("combov2-key-response-address-key", Int::class), + PUMP_ID_KEY("combov2-pump-id-key", String::class), + TBR_TIMESTAMP_KEY("combov2-tbr-timestamp", Long::class), + TBR_PERCENTAGE_KEY("combov2-tbr-percentage", Int::class), + TBR_DURATION_KEY("combov2-tbr-duration", Int::class), + TBR_TYPE_KEY("combov2-tbr-type", String::class), + UTC_OFFSET_KEY("combov2-utc-offset", Int::class); + + override fun toString(): String = str + } +} \ No newline at end of file diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt index a886cc5830..b2e19253db 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt @@ -133,7 +133,17 @@ class ComboV2Plugin @Inject constructor ( private val _pumpDescription = PumpDescription() - private val pumpStateStore = SPPumpStateStore(sp) + // The internal SP is the one that will be mainly used by the driver. + // The AAPS main SP is updated when the pump state store is created + // and when the driver disconnects (to update the nonce value). + private val internalSP = InternalSP( + context.getSharedPreferences( + context.packageName + ".COMBO_PUMP_STATE_STORE", + Context.MODE_PRIVATE + ), + context + ) + private val pumpStateStore = AAPSPumpStateStore(aapsMainSP = sp, internalSP = internalSP) // These are initialized in onStart() and torn down in onStop(). private var bluetoothInterface: AndroidBluetoothInterface? = null @@ -236,6 +246,34 @@ class ComboV2Plugin @Inject constructor ( override fun onStart() { super.onStart() + // Check if there is a pump state in the internal SP. If not, try to + // copy a pump state from the AAPS main SP. It is possible for example + // that AAPS was reinstalled, and the previous settings were imported. + // In that case, the internal SP is empty, but there is a pump state + // that comes from the settings. We want to restore that pump state + // then. If however, there _is_ a pump state in the internal SP, then + // we just ignore any state in the main SP. For example, if the user + // imports an older AAPS settings file with an old pump state, and a + // Combo is already paired with AAPS, then it makes no sense to overwrite + // the current pump state with the old one from the imported settings. + if (pumpStateStore.getAvailablePumpStateAddresses().isEmpty()) { + aapsLogger.info(LTag.PUMP, "There is no pump state in the internal SP; trying to copy a pump state from the main AAPS SP") + pumpStateStore.copyAllValuesFromAAPSMainSP(commit = true) + val btAddress = pumpStateStore.getAvailablePumpStateAddresses().firstOrNull() + if (btAddress == null) + aapsLogger.info(LTag.PUMP, "No pump state found in the main AAPS SP; continuing without a pump state (implying that no pump is paired)") + else + aapsLogger.info(LTag.PUMP, "Pump state found in the main AAPS SP (bluetooth address: $btAddress); continuing with that state") + } else { + // Copy over the internal SP pump state to the main AAPS SP. If the user + // just imported AAPS settings, and said settings contained an old pump + // state, then that old pump state is ignored if there is already a + // current pump state in the internal SP - but we still need to make sure + // the old pump state in the main AAPS SP is replaced by the current one. + aapsLogger.debug(LTag.PUMP, "Copying internal SP pump state to main AAPS SP") + pumpStateStore.copyAllValuesToAAPSMainSP(commit = false) + } + aapsLogger.debug(LTag.PUMP, "Creating bluetooth interface") bluetoothInterface = AndroidBluetoothInterface(context) @@ -624,6 +662,11 @@ class ComboV2Plugin @Inject constructor ( override fun disconnect(reason: String) { aapsLogger.debug(LTag.PUMP, "Disconnecting from Combo; reason: $reason") disconnectInternal(forceDisconnect = false) + + // Sync up the TBR and nonce states in the main AAPS SP. We don't do this all the + // time since this is unnecessary waste of resources. It is sufficient to update + // those once AAPS is done with the connection. + pumpStateStore.copyVariantValuesToAAPSMainSP(commit = false) } // This is called when (a) the AAPS watchdog is about to toggle diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/InternalSP.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/InternalSP.kt new file mode 100644 index 0000000000..7c37a362ea --- /dev/null +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/InternalSP.kt @@ -0,0 +1,199 @@ +package info.nightscout.pump.combov2 + +import android.annotation.SuppressLint +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.StringRes +import info.nightscout.shared.SafeParse +import info.nightscout.shared.sharedPreferences.SP + +// This is a copy of the AAPS SPImplementation. We keep this to be able +// to set up a custom internal SP store for the Combo pump state. +class InternalSP( + private val sharedPreferences: SharedPreferences, + private val context: Context +) : SP { + @SuppressLint("ApplySharedPref") + override fun edit(commit: Boolean, block: SP.Editor.() -> Unit) { + val spEdit = sharedPreferences.edit() + + val edit = object : SP.Editor { + override fun clear() { + spEdit.clear() + } + + override fun remove(@StringRes resourceID: Int) { + spEdit.remove(context.getString(resourceID)) + } + override fun remove(key: String) { + spEdit.remove(key) + } + + override fun putBoolean(key: String, value: Boolean) { + spEdit.putBoolean(key, value) + } + override fun putBoolean(@StringRes resourceID: Int, value: Boolean) { + spEdit.putBoolean(context.getString(resourceID), value) + } + override fun putDouble(key: String, value: Double) { + spEdit.putString(key, value.toString()) + } + override fun putDouble(@StringRes resourceID: Int, value: Double) { + spEdit.putString(context.getString(resourceID), value.toString()) + } + override fun putLong(key: String, value: Long) { + spEdit.putLong(key, value) + } + override fun putLong(@StringRes resourceID: Int, value: Long) { + spEdit.putLong(context.getString(resourceID), value) + } + override fun putInt(key: String, value: Int) { + spEdit.putInt(key, value) + } + override fun putInt(@StringRes resourceID: Int, value: Int) { + spEdit.putInt(context.getString(resourceID), value) + } + override fun putString(key: String, value: String) { + spEdit.putString(key, value) + } + override fun putString(@StringRes resourceID: Int, value: String) { + spEdit.putString(context.getString(resourceID), value) + } + } + + block(edit) + + if (commit) + spEdit.commit() + else + spEdit.apply() + } + + override fun getAll(): Map = sharedPreferences.all + + override fun clear() = sharedPreferences.edit().clear().apply() + + override fun contains(key: String): Boolean = sharedPreferences.contains(key) + + override fun contains(resourceId: Int): Boolean = sharedPreferences.contains(context.getString(resourceId)) + + override fun remove(resourceID: Int) = + sharedPreferences.edit().remove(context.getString(resourceID)).apply() + + override fun remove(key: String) = + sharedPreferences.edit().remove(key).apply() + + override fun getString(resourceID: Int, defaultValue: String): String = + sharedPreferences.getString(context.getString(resourceID), defaultValue) ?: defaultValue + + override fun getStringOrNull(resourceID: Int, defaultValue: String?): String? = + sharedPreferences.getString(context.getString(resourceID), defaultValue) ?: defaultValue + + override fun getStringOrNull(key: String, defaultValue: String?): String? = + sharedPreferences.getString(key, defaultValue) + + override fun getString(key: String, defaultValue: String): String = + sharedPreferences.getString(key, defaultValue) ?: defaultValue + + override fun getBoolean(resourceID: Int, defaultValue: Boolean): Boolean { + return try { + sharedPreferences.getBoolean(context.getString(resourceID), defaultValue) + } catch (e: Exception) { + defaultValue + } + } + + override fun getBoolean(key: String, defaultValue: Boolean): Boolean { + return try { + sharedPreferences.getBoolean(key, defaultValue) + } catch (e: Exception) { + defaultValue + } + } + + override fun getDouble(resourceID: Int, defaultValue: Double): Double = + SafeParse.stringToDouble(sharedPreferences.getString(context.getString(resourceID), defaultValue.toString())) + + override fun getDouble(key: String, defaultValue: Double): Double = + SafeParse.stringToDouble(sharedPreferences.getString(key, defaultValue.toString())) + + override fun getInt(resourceID: Int, defaultValue: Int): Int { + return try { + sharedPreferences.getInt(context.getString(resourceID), defaultValue) + } catch (e: Exception) { + SafeParse.stringToInt(sharedPreferences.getString(context.getString(resourceID), defaultValue.toString())) + } + } + + override fun getInt(key: String, defaultValue: Int): Int { + return try { + sharedPreferences.getInt(key, defaultValue) + } catch (e: Exception) { + SafeParse.stringToInt(sharedPreferences.getString(key, defaultValue.toString())) + } + } + + override fun getLong(resourceID: Int, defaultValue: Long): Long { + return try { + sharedPreferences.getLong(context.getString(resourceID), defaultValue) + } catch (e: Exception) { + try { + SafeParse.stringToLong(sharedPreferences.getString(context.getString(resourceID), defaultValue.toString())) + } catch (e: Exception) { + defaultValue + } + } + } + + override fun getLong(key: String, defaultValue: Long): Long { + return try { + sharedPreferences.getLong(key, defaultValue) + } catch (e: Exception) { + try { + SafeParse.stringToLong(sharedPreferences.getString(key, defaultValue.toString())) + } catch (e: Exception) { + defaultValue + } + } + } + + override fun incLong(resourceID: Int) { + val value = getLong(resourceID, 0) + 1L + sharedPreferences.edit().putLong(context.getString(resourceID), value).apply() + } + + override fun putBoolean(key: String, value: Boolean) = sharedPreferences.edit().putBoolean(key, value).apply() + + override fun putBoolean(resourceID: Int, value: Boolean) = + sharedPreferences.edit().putBoolean(context.getString(resourceID), value).apply() + + override fun putDouble(key: String, value: Double) = + sharedPreferences.edit().putString(key, value.toString()).apply() + + override fun putDouble(resourceID: Int, value: Double) { + sharedPreferences.edit().putString(context.getString(resourceID), value.toString()).apply() + } + + override fun putLong(key: String, value: Long) = + sharedPreferences.edit().putLong(key, value).apply() + + override fun putLong(resourceID: Int, value: Long) = + sharedPreferences.edit().putLong(context.getString(resourceID), value).apply() + + override fun putInt(key: String, value: Int) = + sharedPreferences.edit().putInt(key, value).apply() + + override fun putInt(resourceID: Int, value: Int) = + sharedPreferences.edit().putInt(context.getString(resourceID), value).apply() + + override fun incInt(resourceID: Int) { + val value = getInt(resourceID, 0) + 1 + sharedPreferences.edit().putInt(context.getString(resourceID), value).apply() + } + + override fun putString(resourceID: Int, value: String) = + sharedPreferences.edit().putString(context.getString(resourceID), value).apply() + + override fun putString(key: String, value: String) = + sharedPreferences.edit().putString(key, value).apply() +} \ No newline at end of file diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/SPPumpStateStore.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/SPPumpStateStore.kt deleted file mode 100644 index 0bc026e5af..0000000000 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/SPPumpStateStore.kt +++ /dev/null @@ -1,165 +0,0 @@ -package info.nightscout.pump.combov2 - -import info.nightscout.comboctl.base.BluetoothAddress -import info.nightscout.comboctl.base.CurrentTbrState -import info.nightscout.comboctl.base.InvariantPumpData -import info.nightscout.comboctl.base.Nonce -import info.nightscout.comboctl.base.PumpStateStore -import info.nightscout.comboctl.base.Tbr -import info.nightscout.comboctl.base.toBluetoothAddress -import info.nightscout.comboctl.base.toCipher -import info.nightscout.comboctl.base.toNonce -import info.nightscout.shared.sharedPreferences.SP -import info.nightscout.shared.sharedPreferences.SPDelegateInt -import info.nightscout.shared.sharedPreferences.SPDelegateLong -import info.nightscout.shared.sharedPreferences.SPDelegateString -import kotlinx.datetime.Instant -import kotlinx.datetime.UtcOffset - -/** - * AndroidAPS [SP] based pump state store. - * - * This store is set up to contain a single paired pump. AndroidAPS is not - * designed to handle multiple pumps, so this simplification makes sense. - * This affects all accessors, which - */ -class SPPumpStateStore(private val sp: SP) : PumpStateStore { - private var btAddress: String - by SPDelegateString(sp, BT_ADDRESS_KEY, "") - - // The nonce is updated with commit instead of apply to make sure - // is atomically written to storage synchronously, minimizing - // the likelihood that it could be lost due to app crashes etc. - // It is very important to not lose the nonce, hence that choice. - private var nonceString: String - by SPDelegateString(sp, NONCE_KEY, Nonce.nullNonce().toString(), commit = true) - - private var cpCipherString: String - by SPDelegateString(sp, CP_CIPHER_KEY, "") - private var pcCipherString: String - by SPDelegateString(sp, PC_CIPHER_KEY, "") - private var keyResponseAddressInt: Int - by SPDelegateInt(sp, KEY_RESPONSE_ADDRESS_KEY, 0) - private var pumpID: String - by SPDelegateString(sp, PUMP_ID_KEY, "") - private var tbrTimestamp: Long - by SPDelegateLong(sp, TBR_TIMESTAMP_KEY, 0) - private var tbrPercentage: Int - by SPDelegateInt(sp, TBR_PERCENTAGE_KEY, 0) - private var tbrDuration: Int - by SPDelegateInt(sp, TBR_DURATION_KEY, 0) - private var tbrType: String - by SPDelegateString(sp, TBR_TYPE_KEY, "") - private var utcOffsetSeconds: Int - by SPDelegateInt(sp, UTC_OFFSET_KEY, 0) - - override fun createPumpState( - pumpAddress: BluetoothAddress, - invariantPumpData: InvariantPumpData, - utcOffset: UtcOffset, - tbrState: CurrentTbrState - ) { - // Write these values via edit() instead of using the delegates - // above to be able to write all of them with a single commit. - sp.edit(commit = true) { - putString(BT_ADDRESS_KEY, pumpAddress.toString().uppercase()) - putString(CP_CIPHER_KEY, invariantPumpData.clientPumpCipher.toString()) - putString(PC_CIPHER_KEY, invariantPumpData.pumpClientCipher.toString()) - putInt(KEY_RESPONSE_ADDRESS_KEY, invariantPumpData.keyResponseAddress.toInt() and 0xFF) - putString(PUMP_ID_KEY, invariantPumpData.pumpID) - putLong(TBR_TIMESTAMP_KEY, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.timestamp.epochSeconds else -1) - putInt(TBR_PERCENTAGE_KEY, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.percentage else -1) - putInt(TBR_DURATION_KEY, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.durationInMinutes else -1) - putString(TBR_TYPE_KEY, if (tbrState is CurrentTbrState.TbrStarted) tbrState.tbr.type.stringId else "") - putInt(UTC_OFFSET_KEY, utcOffset.totalSeconds) - } - } - - override fun deletePumpState(pumpAddress: BluetoothAddress): Boolean { - val hasState = sp.contains(NONCE_KEY) - - sp.edit(commit = true) { - remove(BT_ADDRESS_KEY) - remove(NONCE_KEY) - remove(CP_CIPHER_KEY) - remove(PC_CIPHER_KEY) - remove(KEY_RESPONSE_ADDRESS_KEY) - remove(TBR_TIMESTAMP_KEY) - remove(TBR_PERCENTAGE_KEY) - remove(TBR_DURATION_KEY) - remove(TBR_TYPE_KEY) - remove(UTC_OFFSET_KEY) - } - - return hasState - } - - override fun hasPumpState(pumpAddress: BluetoothAddress) = - sp.contains(NONCE_KEY) - - override fun getAvailablePumpStateAddresses() = - if (btAddress.isBlank()) setOf() else setOf(btAddress.toBluetoothAddress()) - - override fun getInvariantPumpData(pumpAddress: BluetoothAddress) = InvariantPumpData( - clientPumpCipher = cpCipherString.toCipher(), - pumpClientCipher = pcCipherString.toCipher(), - keyResponseAddress = keyResponseAddressInt.toByte(), - pumpID = pumpID - ) - - override fun getCurrentTxNonce(pumpAddress: BluetoothAddress) = nonceString.toNonce() - - override fun setCurrentTxNonce(pumpAddress: BluetoothAddress, currentTxNonce: Nonce) { - nonceString = currentTxNonce.toString() - } - - override fun getCurrentUtcOffset(pumpAddress: BluetoothAddress) = - UtcOffset(seconds = utcOffsetSeconds) - - override fun setCurrentUtcOffset(pumpAddress: BluetoothAddress, utcOffset: UtcOffset) { - utcOffsetSeconds = utcOffset.totalSeconds - } - - override fun getCurrentTbrState(pumpAddress: BluetoothAddress) = - if (tbrTimestamp >= 0) - CurrentTbrState.TbrStarted(Tbr( - timestamp = Instant.fromEpochSeconds(tbrTimestamp), - percentage = tbrPercentage, - durationInMinutes = tbrDuration, - type = Tbr.Type.fromStringId(tbrType)!! - )) - else - CurrentTbrState.NoTbrOngoing - - - override fun setCurrentTbrState(pumpAddress: BluetoothAddress, currentTbrState: CurrentTbrState) { - when (currentTbrState) { - is CurrentTbrState.TbrStarted -> { - tbrTimestamp = currentTbrState.tbr.timestamp.epochSeconds - tbrPercentage = currentTbrState.tbr.percentage - tbrDuration = currentTbrState.tbr.durationInMinutes - tbrType = currentTbrState.tbr.type.stringId - } - else -> { - tbrTimestamp = -1 - tbrPercentage = -1 - tbrDuration = -1 - tbrType = "" - } - } - } - - companion object { - const val BT_ADDRESS_KEY = "combov2-bt-address-key" - const val NONCE_KEY = "combov2-nonce-key" - const val CP_CIPHER_KEY = "combov2-cp-cipher-key" - const val PC_CIPHER_KEY = "combov2-pc-cipher-key" - const val KEY_RESPONSE_ADDRESS_KEY = "combov2-key-response-address-key" - const val PUMP_ID_KEY = "combov2-pump-id-key" - const val TBR_TIMESTAMP_KEY = "combov2-tbr-timestamp" - const val TBR_PERCENTAGE_KEY = "combov2-tbr-percentage" - const val TBR_DURATION_KEY = "combov2-tbr-duration" - const val TBR_TYPE_KEY = "combov2-tbr-type" - const val UTC_OFFSET_KEY = "combov2-utc-offset" - } -}