From acf9f05928cbf808ed9c3ed986c02dd9973a57bb Mon Sep 17 00:00:00 2001 From: AdrianLxM Date: Wed, 29 Apr 2020 00:39:20 +0200 Subject: [PATCH 01/16] Removed internal function TypedArrayUtils is marked as `@RestrictTo(LIBRARY_GROUP_PREFIX)`. getAttr() will chose between the two parameters, so if both are the same has no functionality anyway. --- .../utils/textValidator/ValidatingEditTextPreference.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/utils/textValidator/ValidatingEditTextPreference.kt b/app/src/main/java/info/nightscout/androidaps/utils/textValidator/ValidatingEditTextPreference.kt index 190a3bea73..b4a49c42de 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/textValidator/ValidatingEditTextPreference.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/textValidator/ValidatingEditTextPreference.kt @@ -2,7 +2,6 @@ package info.nightscout.androidaps.utils.textValidator import android.content.Context import android.util.AttributeSet -import androidx.core.content.res.TypedArrayUtils import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference.OnBindEditTextListener import androidx.preference.PreferenceViewHolder @@ -23,8 +22,7 @@ class ValidatingEditTextPreference(ctx: Context, attrs: AttributeSet, defStyleAt : this(ctx, attrs, defStyle, 0) constructor(ctx: Context, attrs: AttributeSet) - : this(ctx, attrs, TypedArrayUtils.getAttr(ctx, R.attr.editTextPreferenceStyle, - R.attr.editTextPreferenceStyle)) + : this(ctx, attrs, R.attr.editTextPreferenceStyle) private lateinit var editTextValidator: EditTextValidator @@ -33,4 +31,4 @@ class ValidatingEditTextPreference(ctx: Context, attrs: AttributeSet, defStyleAt holder?.isDividerAllowedAbove = false holder?.isDividerAllowedBelow = false } -} \ No newline at end of file +} From 01e8934f7a9b9009d2155b43213d277eedb72d59 Mon Sep 17 00:00:00 2001 From: Tim Gunn <2896311+Tornado-Tim@users.noreply.github.com> Date: Sat, 2 May 2020 00:05:50 +1200 Subject: [PATCH 02/16] Add deterministic LGS function --- .../androidaps/plugins/aps/loop/LoopPlugin.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/loop/LoopPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/aps/loop/LoopPlugin.java index fefd50f132..dcf32753a1 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/aps/loop/LoopPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/loop/LoopPlugin.java @@ -288,6 +288,21 @@ public class LoopPlugin extends PluginBase { return true; } + public boolean isLGS(){ + Constraint closedLoopEnabled = constraintChecker.isClosedLoopAllowed(); + Double MaxIOBallowed = constraintChecker.getMaxIOBAllowed().value(); + String APSmode = sp.getString(R.string.key_aps_mode, "open"); + Double LGSthreshold = 0d; + PumpInterface pump = activePlugin.getActivePump(); + boolean isLGS = false; + if (!isSuspended() && !pump.isSuspended()) + if (closedLoopEnabled.value()) + if ((MaxIOBallowed.equals(LGSthreshold)) || (APSmode.equals("lgs"))) + isLGS = true; + + return isLGS; + } + public boolean isSuperBolus() { if (loopSuspendedTill == 0) return false; From b54e1a0af365bcbe3ab37530634d70ba656c2f8d Mon Sep 17 00:00:00 2001 From: Tim Gunn <2896311+Tornado-Tim@users.noreply.github.com> Date: Sat, 2 May 2020 00:07:24 +1200 Subject: [PATCH 03/16] Add LGS --- .../plugins/constraints/safety/SafetyPlugin.java | 6 +++++- .../plugins/general/overview/OverviewFragment.kt | 6 ++++-- .../java/info/nightscout/androidaps/utils/HardLimits.kt | 8 ++++++++ app/src/main/res/values/arrays.xml | 2 ++ app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ 6 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPlugin.java index a32b7233e1..dbb2b650ed 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPlugin.java @@ -98,7 +98,7 @@ public class SafetyPlugin extends PluginBase implements ConstraintsInterface { @NonNull @Override public Constraint isClosedLoopAllowed(@NonNull Constraint value) { String mode = sp.getString(R.string.key_aps_mode, "open"); - if (!mode.equals("closed")) + if ((mode.equals("open"))) value.set(getAapsLogger(), false, getResourceHelper().gs(R.string.closedmodedisabledinpreferences), this); if (!buildHelper.isEngineeringModeOrRelease()) { @@ -266,6 +266,7 @@ public class SafetyPlugin extends PluginBase implements ConstraintsInterface { @NonNull @Override public Constraint applyMaxIOBConstraints(@NonNull Constraint maxIob) { double maxIobPref; + String apsmode = sp.getString(R.string.key_aps_mode, "open"); if (openAPSSMBPlugin.isEnabled(PluginType.APS)) maxIobPref = sp.getDouble(R.string.key_openapssmb_max_iob, 3d); else @@ -276,6 +277,9 @@ public class SafetyPlugin extends PluginBase implements ConstraintsInterface { maxIob.setIfSmaller(getAapsLogger(), hardLimits.maxIobAMA(), String.format(getResourceHelper().gs(R.string.limitingiob), hardLimits.maxIobAMA(), getResourceHelper().gs(R.string.hardlimit)), this); if (openAPSSMBPlugin.isEnabled(PluginType.APS)) maxIob.setIfSmaller(getAapsLogger(), hardLimits.maxIobSMB(), String.format(getResourceHelper().gs(R.string.limitingiob), hardLimits.maxIobSMB(), getResourceHelper().gs(R.string.hardlimit)), this); + if ((apsmode.equals("lgs"))) + maxIob.setIfSmaller(getAapsLogger(), hardLimits.maxIobLGS(), String.format(getResourceHelper().gs(R.string.limitingiob), hardLimits.maxIobLGS(), getResourceHelper().gs(R.string.lowglucosesuspend)), this); + return maxIob; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt index cc3f881a13..fc1b80bd2a 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt @@ -622,8 +622,10 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList } loopPlugin.isEnabled(PluginType.LOOP) -> { - overview_apsmode?.text = if (closedLoopEnabled.value()) resourceHelper.gs(R.string.closedloop) else resourceHelper.gs(R.string.openloop) - overview_apsmode?.setBackgroundColor(resourceHelper.gc(R.color.ribbonDefault)) + val APSmode = sp.getString(R.string.key_aps_mode, "open") + val isLGS = loopPlugin.isLGS + overview_apsmode?.text = if (closedLoopEnabled.value()) if (isLGS) resourceHelper.gs(R.string.lgs) else resourceHelper.gs(R.string.closedloop) else resourceHelper.gs(R.string.openloop) + overview_apsmode?.setBackgroundColor(if (isLGS) resourceHelper.gc(R.color.ribbonUnusual) else resourceHelper.gc(R.color.ribbonDefault)) overview_apsmode?.setTextColor(resourceHelper.gc(R.color.ribbonTextDefault)) } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/HardLimits.kt b/app/src/main/java/info/nightscout/androidaps/utils/HardLimits.kt index 48eba49112..5943da70a4 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/HardLimits.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/HardLimits.kt @@ -45,6 +45,10 @@ class HardLimits @Inject constructor( val MAXIOB_SMB = doubleArrayOf(3.0, 7.0, 12.0, 25.0) val MAXBASAL = doubleArrayOf(2.0, 5.0, 10.0, 12.0) + //LGS Hard limits + //No IOB at all + val MAXIOB_LGS = 0.0 + private fun loadAge(): Int { val sp_age = sp.getString(R.string.key_age, "") val age: Int @@ -68,6 +72,10 @@ class HardLimits @Inject constructor( return MAXIOB_SMB[loadAge()] } + fun maxIobLGS(): Double { + return MAXIOB_LGS + } + fun maxBasal(): Double { return MAXBASAL[loadAge()] } diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 04d78d3406..d11a14eefd 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -4,10 +4,12 @@ @string/closedloop @string/openloop + @string/lowglucosesuspend closed open + lgs diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 8d1b977fea..d30a19a76e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -86,6 +86,7 @@ #ff0400 #FFFFFF #303030 + #01017A #FFFFFF #2E2E2E diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b0f6821b69..45ed657b64 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -187,10 +187,12 @@ Closed Loop Open Loop + Low Glucose Suspend Loop Disabled Disable loop Enable loop + LGS New suggestion available Unsupported version of Nightscout LOOP DISABLED BY CONSTRAINTS From 560a081c7c3b5c0006ba078de7b2cc7437ba0bea Mon Sep 17 00:00:00 2001 From: Tim Gunn <2896311+Tornado-Tim@users.noreply.github.com> Date: Sat, 2 May 2020 01:50:35 +1200 Subject: [PATCH 04/16] Inherit from HardLimits class --- .../androidaps/plugins/aps/loop/LoopPlugin.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/loop/LoopPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/aps/loop/LoopPlugin.java index dcf32753a1..93fcef0f0b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/aps/loop/LoopPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/loop/LoopPlugin.java @@ -68,6 +68,7 @@ import info.nightscout.androidaps.utils.FabricPrivacy; import info.nightscout.androidaps.utils.T; import info.nightscout.androidaps.utils.resources.ResourceHelper; import info.nightscout.androidaps.utils.sharedPreferences.SP; +import info.nightscout.androidaps.utils.HardLimits; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; @@ -88,6 +89,7 @@ public class LoopPlugin extends PluginBase { private final IobCobCalculatorPlugin iobCobCalculatorPlugin; private final ReceiverStatusStore receiverStatusStore; private final FabricPrivacy fabricPrivacy; + private final HardLimits hardLimits; private CompositeDisposable disposable = new CompositeDisposable(); @@ -132,7 +134,8 @@ public class LoopPlugin extends PluginBase { Lazy actionStringHandler, // TODO Adrian use RxBus instead of Lazy IobCobCalculatorPlugin iobCobCalculatorPlugin, ReceiverStatusStore receiverStatusStore, - FabricPrivacy fabricPrivacy + FabricPrivacy fabricPrivacy, + HardLimits hardLimits ) { super(new PluginDescription() .mainType(PluginType.LOOP) @@ -158,6 +161,7 @@ public class LoopPlugin extends PluginBase { this.iobCobCalculatorPlugin = iobCobCalculatorPlugin; this.receiverStatusStore = receiverStatusStore; this.fabricPrivacy = fabricPrivacy; + this.hardLimits = hardLimits; loopSuspendedTill = sp.getLong("loopSuspendedTill", 0L); isSuperBolus = sp.getBoolean("isSuperBolus", false); @@ -292,12 +296,12 @@ public class LoopPlugin extends PluginBase { Constraint closedLoopEnabled = constraintChecker.isClosedLoopAllowed(); Double MaxIOBallowed = constraintChecker.getMaxIOBAllowed().value(); String APSmode = sp.getString(R.string.key_aps_mode, "open"); - Double LGSthreshold = 0d; PumpInterface pump = activePlugin.getActivePump(); boolean isLGS = false; + if (!isSuspended() && !pump.isSuspended()) if (closedLoopEnabled.value()) - if ((MaxIOBallowed.equals(LGSthreshold)) || (APSmode.equals("lgs"))) + if ((MaxIOBallowed.equals(hardLimits.getMAXIOB_LGS())) || (APSmode.equals("lgs"))) isLGS = true; return isLGS; From 96757145bf039a2a9d8a147f18c78a296358918d Mon Sep 17 00:00:00 2001 From: Tim Gunn <2896311+Tornado-Tim@users.noreply.github.com> Date: Sat, 2 May 2020 01:54:08 +1200 Subject: [PATCH 05/16] Use auto getter method --- .../androidaps/plugins/constraints/safety/SafetyPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPlugin.java index dbb2b650ed..ab325f1f0c 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPlugin.java @@ -278,7 +278,7 @@ public class SafetyPlugin extends PluginBase implements ConstraintsInterface { if (openAPSSMBPlugin.isEnabled(PluginType.APS)) maxIob.setIfSmaller(getAapsLogger(), hardLimits.maxIobSMB(), String.format(getResourceHelper().gs(R.string.limitingiob), hardLimits.maxIobSMB(), getResourceHelper().gs(R.string.hardlimit)), this); if ((apsmode.equals("lgs"))) - maxIob.setIfSmaller(getAapsLogger(), hardLimits.maxIobLGS(), String.format(getResourceHelper().gs(R.string.limitingiob), hardLimits.maxIobLGS(), getResourceHelper().gs(R.string.lowglucosesuspend)), this); + maxIob.setIfSmaller(getAapsLogger(), hardLimits.getMAXIOB_LGS(), String.format(getResourceHelper().gs(R.string.limitingiob), hardLimits.getMAXIOB_LGS(), getResourceHelper().gs(R.string.lowglucosesuspend)), this); return maxIob; } From 9e2c3255e2568b3198d53542d46841191f494574 Mon Sep 17 00:00:00 2001 From: Tim Gunn <2896311+Tornado-Tim@users.noreply.github.com> Date: Sat, 2 May 2020 01:54:46 +1200 Subject: [PATCH 06/16] Remove not needed getter function --- .../main/java/info/nightscout/androidaps/utils/HardLimits.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/utils/HardLimits.kt b/app/src/main/java/info/nightscout/androidaps/utils/HardLimits.kt index 5943da70a4..c9e2835ceb 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/HardLimits.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/HardLimits.kt @@ -72,10 +72,6 @@ class HardLimits @Inject constructor( return MAXIOB_SMB[loadAge()] } - fun maxIobLGS(): Double { - return MAXIOB_LGS - } - fun maxBasal(): Double { return MAXBASAL[loadAge()] } From 5c3876217813d76f4651ef74cf42227043274a34 Mon Sep 17 00:00:00 2001 From: Tim Gunn <2896311+Tornado-Tim@users.noreply.github.com> Date: Sat, 2 May 2020 01:55:11 +1200 Subject: [PATCH 07/16] Format code so its easier to read --- .../plugins/general/overview/OverviewFragment.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt index fc1b80bd2a..962e443c66 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt @@ -622,9 +622,16 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList } loopPlugin.isEnabled(PluginType.LOOP) -> { - val APSmode = sp.getString(R.string.key_aps_mode, "open") val isLGS = loopPlugin.isLGS - overview_apsmode?.text = if (closedLoopEnabled.value()) if (isLGS) resourceHelper.gs(R.string.lgs) else resourceHelper.gs(R.string.closedloop) else resourceHelper.gs(R.string.openloop) + overview_apsmode?.text = + if (closedLoopEnabled.value()) + if (isLGS) + resourceHelper.gs(R.string.lgs) + else + resourceHelper.gs(R.string.closedloop) + else + resourceHelper.gs(R.string.openloop) + overview_apsmode?.setBackgroundColor(if (isLGS) resourceHelper.gc(R.color.ribbonUnusual) else resourceHelper.gc(R.color.ribbonDefault)) overview_apsmode?.setTextColor(resourceHelper.gc(R.color.ribbonTextDefault)) } From 0bbdca3b4adf67a59145a98b9959c53abf6ed2c8 Mon Sep 17 00:00:00 2001 From: Tim Gunn <2896311+Tornado-Tim@users.noreply.github.com> Date: Sat, 2 May 2020 14:50:53 +1200 Subject: [PATCH 08/16] Add test --- .../plugins/constraints/safety/SafetyPluginTest.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPluginTest.kt b/app/src/test/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPluginTest.kt index 9da162a301..28261260a9 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPluginTest.kt +++ b/app/src/test/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPluginTest.kt @@ -242,4 +242,14 @@ class SafetyPluginTest : TestBaseWithProfile() { """.trimIndent(), d.getReasons(aapsLogger)) Assert.assertEquals("Safety: Limiting IOB to 1.5 U because of max value in preferences", d.getMostLimitedReasons(aapsLogger)) } + + @Test fun iobShouldBeZero() { + `when`(sp.getString(R.string.key_aps_mode, "open")).thenReturn("lgs") + + // Apply IOB limits + var d = Constraint(hardLimits.MAXIOB_LGS) + d = safetyPlugin.applyMaxIOBConstraints(d) + Assert.assertEquals(0.0, d.value()!!) + Assert.assertEquals("Safety: Limiting IOB to 0.0 U because of Low Glucose Suspend", d.getMostLimitedReasons(aapsLogger)) + } } \ No newline at end of file From 4c2fdef71abb5c10315322ed44f699f1a34e6070 Mon Sep 17 00:00:00 2001 From: Tim Gunn <2896311+Tornado-Tim@users.noreply.github.com> Date: Sat, 2 May 2020 19:13:16 +1200 Subject: [PATCH 09/16] Fix tests --- .../androidaps/plugins/aps/loop/LoopPluginTest.kt | 8 +++++++- .../plugins/constraints/safety/SafetyPluginTest.kt | 11 +---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/aps/loop/LoopPluginTest.kt b/app/src/test/java/info/nightscout/androidaps/plugins/aps/loop/LoopPluginTest.kt index dbfa209d11..40495880a3 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/aps/loop/LoopPluginTest.kt +++ b/app/src/test/java/info/nightscout/androidaps/plugins/aps/loop/LoopPluginTest.kt @@ -19,6 +19,7 @@ import info.nightscout.androidaps.plugins.pump.virtual.VirtualPumpPlugin import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin import info.nightscout.androidaps.receivers.ReceiverStatusStore import info.nightscout.androidaps.utils.FabricPrivacy +import info.nightscout.androidaps.utils.HardLimits import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.sharedPreferences.SP import org.junit.Assert @@ -49,11 +50,15 @@ class LoopPluginTest : TestBase() { @Mock lateinit var fabricPrivacy: FabricPrivacy @Mock lateinit var receiverStatusStore: ReceiverStatusStore + private lateinit var hardLimits: HardLimits + lateinit var loopPlugin: LoopPlugin val injector = HasAndroidInjector { AndroidInjector { } } @Before fun prepareMock() { - loopPlugin = LoopPlugin(injector, aapsLogger, rxBus, sp, constraintChecker, resourceHelper, profileFunction, context, commandQueue, activePlugin, treatmentsPlugin, virtualPumpPlugin, actionStringHandler, iobCobCalculatorPlugin, receiverStatusStore, fabricPrivacy) + hardLimits = HardLimits(aapsLogger, rxBus, sp, resourceHelper, context) + + loopPlugin = LoopPlugin(injector, aapsLogger, rxBus, sp, constraintChecker, resourceHelper, profileFunction, context, commandQueue, activePlugin, treatmentsPlugin, virtualPumpPlugin, actionStringHandler, iobCobCalculatorPlugin, receiverStatusStore, fabricPrivacy, hardLimits) `when`(activePlugin.getActivePump()).thenReturn(virtualPumpPlugin) } @@ -61,6 +66,7 @@ class LoopPluginTest : TestBase() { fun testPluginInterface() { `when`(resourceHelper.gs(R.string.loop)).thenReturn("Loop") `when`(resourceHelper.gs(R.string.loop_shortname)).thenReturn("LOOP") + `when`(sp.getString(R.string.key_aps_mode, "open")).thenReturn("closed") val pumpDescription = PumpDescription() `when`(virtualPumpPlugin.pumpDescription).thenReturn(pumpDescription) Assert.assertEquals(LoopFragment::class.java.name, loopPlugin.pluginDescription.fragmentClass) diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPluginTest.kt b/app/src/test/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPluginTest.kt index 28261260a9..3e36b8bca0 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPluginTest.kt +++ b/app/src/test/java/info/nightscout/androidaps/plugins/constraints/safety/SafetyPluginTest.kt @@ -230,6 +230,7 @@ class SafetyPluginTest : TestBaseWithProfile() { } @Test fun iobShouldBeLimited() { + `when`(sp.getString(R.string.key_aps_mode, "open")).thenReturn("closed") `when`(sp.getDouble(R.string.key_openapsma_max_iob, 1.5)).thenReturn(1.5) `when`(sp.getString(R.string.key_age, "")).thenReturn("teenage") @@ -242,14 +243,4 @@ class SafetyPluginTest : TestBaseWithProfile() { """.trimIndent(), d.getReasons(aapsLogger)) Assert.assertEquals("Safety: Limiting IOB to 1.5 U because of max value in preferences", d.getMostLimitedReasons(aapsLogger)) } - - @Test fun iobShouldBeZero() { - `when`(sp.getString(R.string.key_aps_mode, "open")).thenReturn("lgs") - - // Apply IOB limits - var d = Constraint(hardLimits.MAXIOB_LGS) - d = safetyPlugin.applyMaxIOBConstraints(d) - Assert.assertEquals(0.0, d.value()!!) - Assert.assertEquals("Safety: Limiting IOB to 0.0 U because of Low Glucose Suspend", d.getMostLimitedReasons(aapsLogger)) - } } \ No newline at end of file From 9efbbd3c1715fa1984d8fe7e71c4b105c29b30c4 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 2 May 2020 10:49:55 +0200 Subject: [PATCH 10/16] clanup --- .../general/overview/StatusLightHandler.kt | 18 +++--- .../maintenance/ClassicPrefsFormatTest.kt | 24 ++++---- .../maintenance/EncryptedPrefsFormatTest.kt | 35 ++++++------ .../testing/mockers/AAPSMocker.java | 56 ------------------- .../testing/utils/SingleStringStorage.kt | 12 +--- 5 files changed, 42 insertions(+), 103 deletions(-) delete mode 100644 app/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/StatusLightHandler.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/StatusLightHandler.kt index 54bb6f3177..b05e1cb015 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/StatusLightHandler.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/StatusLightHandler.kt @@ -28,17 +28,17 @@ class StatusLightHandler @Inject constructor( */ fun updateStatusLights(careportal_canulaage: TextView?, careportal_insulinage: TextView?, careportal_reservoirlevel: TextView?, careportal_sensorage: TextView?, careportal_pbage: TextView?, careportal_batterylevel: TextView?) { val pump = activePlugin.activePump - handleAge(careportal_canulaage, "cage", CareportalEvent.SITECHANGE, R.string.key_statuslights_cage_warning, 48.0, R.string.key_statuslights_cage_critical, 72.0) - handleAge(careportal_insulinage, "iage", CareportalEvent.INSULINCHANGE, R.string.key_statuslights_iage_warning, 72.0, R.string.key_statuslights_iage_critical, 144.0) - handleAge(careportal_sensorage, "sage", CareportalEvent.SENSORCHANGE, R.string.key_statuslights_sage_warning, 216.0, R.string.key_statuslights_sage_critical, 240.0) - handleAge(careportal_pbage, "bage", CareportalEvent.PUMPBATTERYCHANGE, R.string.key_statuslights_bage_warning, 216.0, R.string.key_statuslights_bage_critical, 240.0) + handleAge(careportal_canulaage, CareportalEvent.SITECHANGE, R.string.key_statuslights_cage_warning, 48.0, R.string.key_statuslights_cage_critical, 72.0) + handleAge(careportal_insulinage, CareportalEvent.INSULINCHANGE, R.string.key_statuslights_iage_warning, 72.0, R.string.key_statuslights_iage_critical, 144.0) + handleAge(careportal_sensorage, CareportalEvent.SENSORCHANGE, R.string.key_statuslights_sage_warning, 216.0, R.string.key_statuslights_sage_critical, 240.0) + handleAge(careportal_pbage, CareportalEvent.PUMPBATTERYCHANGE, R.string.key_statuslights_bage_warning, 216.0, R.string.key_statuslights_bage_critical, 240.0) if (!Config.NSCLIENT) - handleLevel(careportal_reservoirlevel, R.string.key_statuslights_res_critical, 10.0, R.string.key_statuslights_res_warning, 80.0, pump.reservoirLevel) + handleLevel(careportal_reservoirlevel, R.string.key_statuslights_res_critical, 10.0, R.string.key_statuslights_res_warning, 80.0, pump.reservoirLevel, "U") if (!Config.NSCLIENT && pump.model() != PumpType.AccuChekCombo) - handleLevel(careportal_batterylevel, R.string.key_statuslights_bat_critical, 26.0, R.string.key_statuslights_bat_warning, 51.0, pump.batteryLevel.toDouble()) + handleLevel(careportal_batterylevel, R.string.key_statuslights_bat_critical, 26.0, R.string.key_statuslights_bat_warning, 51.0, pump.batteryLevel.toDouble(), "%") } - private fun handleAge(view: TextView?, nsSettingPlugin: String, eventName: String, @StringRes warnSettings: Int, defaultWarnThreshold: Double, @StringRes urgentSettings: Int, defaultUrgentThreshold: Double) { + private fun handleAge(view: TextView?, eventName: String, @StringRes warnSettings: Int, defaultWarnThreshold: Double, @StringRes urgentSettings: Int, defaultUrgentThreshold: Double) { val warn = sp.getDouble(warnSettings, defaultWarnThreshold) val urgent = sp.getDouble(urgentSettings, defaultUrgentThreshold) val careportalEvent = MainApp.getDbHelper().getLastCareportalEvent(eventName) @@ -50,11 +50,11 @@ class StatusLightHandler @Inject constructor( } } - private fun handleLevel(view: TextView?, criticalSetting: Int, criticalDefaultValue: Double, warnSetting: Int, warnDefaultValue: Double, level: Double) { + private fun handleLevel(view: TextView?, criticalSetting: Int, criticalDefaultValue: Double, warnSetting: Int, warnDefaultValue: Double, level: Double, units: String) { val resUrgent = sp.getDouble(criticalSetting, criticalDefaultValue) val resWarn = sp.getDouble(warnSetting, warnDefaultValue) @Suppress("SetTextI18n") - view?.text = " " + DecimalFormatter.to0Decimal(level) + view?.text = " " + DecimalFormatter.to0Decimal(level) + units warnColors.setColorInverse(view, level, resWarn, resUrgent) } } \ No newline at end of file diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/ClassicPrefsFormatTest.kt b/app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/ClassicPrefsFormatTest.kt index 0755190d2b..f0a5c07491 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/ClassicPrefsFormatTest.kt +++ b/app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/ClassicPrefsFormatTest.kt @@ -1,45 +1,36 @@ package info.nightscout.androidaps.plugins.general.maintenance -import info.nightscout.androidaps.MainApp import info.nightscout.androidaps.TestBase import info.nightscout.androidaps.plugins.general.maintenance.formats.ClassicPrefsFormat import info.nightscout.androidaps.plugins.general.maintenance.formats.Prefs -import info.nightscout.androidaps.testing.mockers.AAPSMocker import info.nightscout.androidaps.testing.utils.SingleStringStorage import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.sharedPreferences.SP import org.hamcrest.CoreMatchers import org.junit.Assert -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.Mockito import org.mockito.Mockito.`when` import org.powermock.core.classloader.annotations.PrepareForTest import org.powermock.modules.junit4.PowerMockRunner import java.io.File @RunWith(PowerMockRunner::class) -@PrepareForTest(AAPSMocker::class, MainApp::class, File::class) +@PrepareForTest(File::class) class ClassicPrefsFormatTest : TestBase() { @Mock lateinit var resourceHelper: ResourceHelper @Mock lateinit var sp: SP - @Before - fun mock() { - AAPSMocker.prepareMock() - AAPSMocker.resetMockedSharedPrefs() - } - @Test fun preferenceLoadingTest() { val test = "key1::val1\nkeyB::valB" val classicFormat = ClassicPrefsFormat(resourceHelper, SingleStringStorage(test)) - val prefs = classicFormat.loadPreferences(AAPSMocker.getMockedFile(), "") + val prefs = classicFormat.loadPreferences(getMockedFile(), "") Assert.assertThat(prefs.values.size, CoreMatchers.`is`(2)) Assert.assertThat(prefs.values["key1"], CoreMatchers.`is`("val1")) @@ -59,7 +50,14 @@ class ClassicPrefsFormatTest : TestBase() { mapOf() ) - classicFormat.savePreferences(AAPSMocker.getMockedFile(), prefs) + classicFormat.savePreferences(getMockedFile(), prefs) } + private fun getMockedFile(): File { + val file = Mockito.mock(File::class.java) + `when`(file.exists()).thenReturn(true) + `when`(file.canRead()).thenReturn(true) + `when`(file.canWrite()).thenReturn(true) + return file + } } diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/EncryptedPrefsFormatTest.kt b/app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/EncryptedPrefsFormatTest.kt index 4537783ce4..fa52651e50 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/EncryptedPrefsFormatTest.kt +++ b/app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/EncryptedPrefsFormatTest.kt @@ -1,9 +1,7 @@ package info.nightscout.androidaps.plugins.general.maintenance -import info.nightscout.androidaps.MainApp import info.nightscout.androidaps.TestBase import info.nightscout.androidaps.plugins.general.maintenance.formats.* -import info.nightscout.androidaps.testing.mockers.AAPSMocker import info.nightscout.androidaps.testing.utils.SingleStringStorage import info.nightscout.androidaps.utils.CryptoUtil import info.nightscout.androidaps.utils.assumeAES256isSupported @@ -24,19 +22,17 @@ import java.io.File @PowerMockIgnore("javax.crypto.*") @RunWith(PowerMockRunner::class) -@PrepareForTest(AAPSMocker::class, MainApp::class, File::class, ResourceHelper::class) +@PrepareForTest(File::class) class EncryptedPrefsFormatTest : TestBase() { @Mock lateinit var resourceHelper: ResourceHelper @Mock lateinit var sp: SP - var cryptoUtil: CryptoUtil = CryptoUtil(aapsLogger) + private var cryptoUtil: CryptoUtil = CryptoUtil(aapsLogger) @Before fun mock() { - AAPSMocker.prepareMock() - AAPSMocker.resetMockedSharedPrefs() Mockito.`when`(resourceHelper.gs(ArgumentMatchers.anyInt())).thenReturn("mock translation") } @@ -56,7 +52,7 @@ class EncryptedPrefsFormatTest : TestBase() { val storage = SingleStringStorage(frozenPrefs) val encryptedFormat = EncryptedPrefsFormat(resourceHelper, cryptoUtil, storage) - val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret") + val prefs = encryptedFormat.loadPreferences(getMockedFile(), "sikret") assumeAES256isSupported(cryptoUtil) @@ -82,7 +78,7 @@ class EncryptedPrefsFormatTest : TestBase() { PrefsMetadataKey.ENCRYPTION to PrefMetadata(EncryptedPrefsFormat.FORMAT_KEY_ENC, PrefsStatus.OK) ) ) - encryptedFormat.savePreferences(AAPSMocker.getMockedFile(), prefs, "sikret") + encryptedFormat.savePreferences(getMockedFile(), prefs, "sikret") aapsLogger.debug(storage.contents) } @@ -99,8 +95,8 @@ class EncryptedPrefsFormatTest : TestBase() { PrefsMetadataKey.ENCRYPTION to PrefMetadata(EncryptedPrefsFormat.FORMAT_KEY_ENC, PrefsStatus.OK) ) ) - encryptedFormat.savePreferences(AAPSMocker.getMockedFile(), prefsIn, "tajemnica") - val prefsOut = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "tajemnica") + encryptedFormat.savePreferences(getMockedFile(), prefsIn, "tajemnica") + val prefsOut = encryptedFormat.loadPreferences(getMockedFile(), "tajemnica") assumeAES256isSupported(cryptoUtil) @@ -129,7 +125,7 @@ class EncryptedPrefsFormatTest : TestBase() { val storage = SingleStringStorage(frozenPrefs) val encryptedFormat = EncryptedPrefsFormat(resourceHelper, cryptoUtil, storage) - val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "it-is-NOT-right-secret") + val prefs = encryptedFormat.loadPreferences(getMockedFile(), "it-is-NOT-right-secret") Assert.assertThat(prefs.values.size, CoreMatchers.`is`(0)) @@ -156,7 +152,7 @@ class EncryptedPrefsFormatTest : TestBase() { val storage = SingleStringStorage(frozenPrefs) val encryptedFormat = EncryptedPrefsFormat(resourceHelper, cryptoUtil, storage) - val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret") + val prefs = encryptedFormat.loadPreferences(getMockedFile(), "sikret") assumeAES256isSupported(cryptoUtil) @@ -183,7 +179,7 @@ class EncryptedPrefsFormatTest : TestBase() { val storage = SingleStringStorage(frozenPrefs) val encryptedFormat = EncryptedPrefsFormat(resourceHelper, cryptoUtil, storage) - val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret") + val prefs = encryptedFormat.loadPreferences(getMockedFile(), "sikret") Assert.assertThat(prefs.values.size, CoreMatchers.`is`(0)) Assert.assertThat(prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status, CoreMatchers.`is`(PrefsStatus.ERROR)) @@ -198,7 +194,7 @@ class EncryptedPrefsFormatTest : TestBase() { val storage = SingleStringStorage(frozenPrefs) val encryptedFormat = EncryptedPrefsFormat(resourceHelper, cryptoUtil, storage) - val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret") + val prefs = encryptedFormat.loadPreferences(getMockedFile(), "sikret") Assert.assertThat(prefs.values.size, CoreMatchers.`is`(0)) Assert.assertThat(prefs.metadata[PrefsMetadataKey.FILE_FORMAT]?.status, CoreMatchers.`is`(PrefsStatus.ERROR)) @@ -210,7 +206,7 @@ class EncryptedPrefsFormatTest : TestBase() { val storage = SingleStringStorage(frozenPrefs) val encryptedFormat = EncryptedPrefsFormat(resourceHelper, cryptoUtil, storage) - encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret") + encryptedFormat.loadPreferences(getMockedFile(), "sikret") } @Test(expected = PrefFormatError::class) @@ -229,7 +225,14 @@ class EncryptedPrefsFormatTest : TestBase() { val storage = SingleStringStorage(frozenPrefs) val encryptedFormat = EncryptedPrefsFormat(resourceHelper, cryptoUtil, storage) - encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret") + encryptedFormat.loadPreferences(getMockedFile(), "sikret") } + private fun getMockedFile(): File { + val file = Mockito.mock(File::class.java) + Mockito.`when`(file.exists()).thenReturn(true) + Mockito.`when`(file.canRead()).thenReturn(true) + Mockito.`when`(file.canWrite()).thenReturn(true) + return file + } } diff --git a/app/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java b/app/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java deleted file mode 100644 index 3b495af147..0000000000 --- a/app/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java +++ /dev/null @@ -1,56 +0,0 @@ -package info.nightscout.androidaps.testing.mockers; - -import android.content.Context; -import android.content.SharedPreferences; - -import org.mockito.ArgumentMatchers; -import org.mockito.invocation.InvocationOnMock; -import org.powermock.api.mockito.PowerMockito; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; - -import info.nightscout.androidaps.MainApp; -import info.nightscout.androidaps.testing.mocks.SharedPreferencesMock; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mockStatic; - -public class AAPSMocker { - - private static final Map mockedSharedPrefs = new HashMap<>(); - - public static void prepareMock() throws Exception { - Context mockedContext = mock(Context.class); - mockStatic(MainApp.class, InvocationOnMock::callRealMethod); - - PowerMockito.when(mockedContext, "getSharedPreferences", ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()).thenAnswer(invocation -> { - - final String key = invocation.getArgument(0); - if (mockedSharedPrefs.containsKey(key)) { - return mockedSharedPrefs.get(key); - } else { - SharedPreferencesMock newPrefs = new SharedPreferencesMock(); - mockedSharedPrefs.put(key, newPrefs); - return newPrefs; - } - }); - - resetMockedSharedPrefs(); - } - - public static void resetMockedSharedPrefs() { - mockedSharedPrefs.clear(); - } - - public static File getMockedFile() { - File file = mock(File.class); - when(file.exists()).thenReturn(true); - when(file.canRead()).thenReturn(true); - when(file.canWrite()).thenReturn(true); - return file; - } - -} diff --git a/app/src/test/java/info/nightscout/androidaps/testing/utils/SingleStringStorage.kt b/app/src/test/java/info/nightscout/androidaps/testing/utils/SingleStringStorage.kt index 8a3b06d56e..9991779066 100644 --- a/app/src/test/java/info/nightscout/androidaps/testing/utils/SingleStringStorage.kt +++ b/app/src/test/java/info/nightscout/androidaps/testing/utils/SingleStringStorage.kt @@ -3,20 +3,14 @@ package info.nightscout.androidaps.testing.utils import info.nightscout.androidaps.utils.storage.Storage import java.io.File -class SingleStringStorage : Storage { - - var contents: String = "" - - constructor(contents: String) { - this.contents = contents - } +class SingleStringStorage(var contents: String) : Storage { override fun getFileContents(file: File): String { return contents } - override fun putFileContents(file: File, putContents: String) { - contents = putContents + override fun putFileContents(file: File, contents: String) { + this.contents = contents } override fun toString(): String { From 40519b92a22dd849288dcea21cb8f0273399953a Mon Sep 17 00:00:00 2001 From: Tim Gunn <2896311+Tornado-Tim@users.noreply.github.com> Date: Sat, 2 May 2020 21:22:21 +1200 Subject: [PATCH 11/16] Fix further tests --- .../nightscout/androidaps/interfaces/ConstraintsCheckerTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/test/java/info/nightscout/androidaps/interfaces/ConstraintsCheckerTest.kt b/app/src/test/java/info/nightscout/androidaps/interfaces/ConstraintsCheckerTest.kt index cd090dbde4..7698cc6844 100644 --- a/app/src/test/java/info/nightscout/androidaps/interfaces/ConstraintsCheckerTest.kt +++ b/app/src/test/java/info/nightscout/androidaps/interfaces/ConstraintsCheckerTest.kt @@ -310,6 +310,7 @@ class ConstraintsCheckerTest : TestBaseWithProfile() { @Test fun iobAMAShouldBeLimited() { // No limit by default + `when`(sp.getString(R.string.key_aps_mode, "open")).thenReturn("closed") `when`(sp.getDouble(R.string.key_openapsma_max_iob, 1.5)).thenReturn(1.5) `when`(sp.getString(R.string.key_age, "")).thenReturn("teenage") openAPSAMAPlugin.setPluginEnabled(PluginType.APS, true) @@ -325,6 +326,7 @@ class ConstraintsCheckerTest : TestBaseWithProfile() { @Test fun iobSMBShouldBeLimited() { // No limit by default + `when`(sp.getString(R.string.key_aps_mode, "open")).thenReturn("closed") `when`(sp.getDouble(R.string.key_openapssmb_max_iob, 3.0)).thenReturn(3.0) `when`(sp.getString(R.string.key_age, "")).thenReturn("teenage") openAPSSMBPlugin.setPluginEnabled(PluginType.APS, true) From 9995e7448b596e79e9f71f4139152b96d3b499f8 Mon Sep 17 00:00:00 2001 From: AdrianLxM Date: Tue, 28 Apr 2020 21:31:25 +0200 Subject: [PATCH 12/16] RandomBGPlugin - stay between min and max The sinus is in interval [-1,1]. If we shift it by one interval it would map it to 0 to 2 times the interval -> divide by two. --- .../info/nightscout/androidaps/plugins/source/RandomBgPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/source/RandomBgPlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/source/RandomBgPlugin.kt index 27f3fd4b4c..bb008b383b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/source/RandomBgPlugin.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/source/RandomBgPlugin.kt @@ -79,7 +79,7 @@ class RandomBgPlugin @Inject constructor( val cal = GregorianCalendar() val currentMinute = cal.get(Calendar.MINUTE) + (cal.get(Calendar.HOUR_OF_DAY) % 2) * 60 - val bgMgdl = min + (max - min) + (max - min) * sin(currentMinute / 120.0 * 2 * PI) + val bgMgdl = min + ((max - min) + (max - min) * sin(currentMinute / 120.0 * 2 * PI))/2 val bgReading = BgReading() bgReading.value = bgMgdl From 634042b40d6e983262c9ac2be44b0f36efe3156f Mon Sep 17 00:00:00 2001 From: Andy Rozman Date: Sat, 2 May 2020 21:49:15 +0100 Subject: [PATCH 13/16] - omnipod eros driver and REQUIRED changes --- app/build.gradle | 1 + ...ware.android.library.wizardpager-1.1.1.aar | Bin 0 -> 96989 bytes .../androidaps/db/DatabaseHelper.java | 41 +- .../overview/notifications/Notification.java | 3 + .../pump/common/PumpPluginAbstract.java | 12 +- .../plugins/pump/common/data/PumpStatus.java | 6 +- .../plugins/pump/common/defs/PumpType.java | 4 +- .../dialog/RileyLinkBLEScanActivity.java | 28 +- .../RileyLinkCommunicationManager.java | 19 +- .../common/hw/rileylink/RileyLinkUtil.java | 8 + .../pump/common/hw/rileylink/ble/RFSpy.java | 7 + .../hw/rileylink/data/RLHistoryItem.java | 13 + .../rileylink/defs/RileyLinkPumpDevice.java | 19 + .../RileyLinkStatusGeneralFragment.java | 65 +- .../rileylink/service/RileyLinkService.java | 17 +- .../tasks/InitializePumpManagerTask.java | 56 +- .../ResetRileyLinkConfigurationTask.java | 18 +- .../service/tasks/WakeAndTuneTask.java | 9 +- .../pump/medtronic/MedtronicPumpPlugin.java | 13 +- .../comm/MedtronicCommunicationManager.java | 5 + .../medtronic/data/MedtronicHistoryData.java | 3 +- .../medtronic/driver/MedtronicPumpStatus.java | 7 +- .../service/RileyLinkMedtronicService.java | 10 +- .../plugins/pump/omnipod/OmnipodFragment.kt | 458 ++++++++ .../pump/omnipod/OmnipodPumpPlugin.java | 1034 +++++++++++++++++ .../comm/BolusProgressIndicationConsumer.java | 7 + .../comm/OmnipodCommunicationManager.java | 345 ++++++ .../pump/omnipod/comm/OmnipodManager.java | 664 +++++++++++ .../pump/omnipod/comm/SetupActionResult.java | 61 + .../comm/action/AcknowledgeAlertsAction.java | 39 + .../comm/action/AssignAddressAction.java | 50 + .../pump/omnipod/comm/action/BolusAction.java | 53 + .../comm/action/CancelDeliveryAction.java | 56 + .../comm/action/ConfigureAlertsAction.java | 36 + .../comm/action/ConfigurePodAction.java | 59 + .../comm/action/DeactivatePodAction.java | 38 + .../omnipod/comm/action/GetPodInfoAction.java | 29 + .../omnipod/comm/action/GetStatusAction.java | 24 + .../comm/action/InsertCannulaAction.java | 71 ++ .../omnipod/comm/action/OmnipodAction.java | 7 + .../pump/omnipod/comm/action/PrimeAction.java | 61 + .../comm/action/SetBasalScheduleAction.java | 53 + .../comm/action/SetTempBasalAction.java | 48 + .../action/service/InsertCannulaService.java | 59 + .../comm/action/service/PrimeService.java | 37 + .../ActionInitializationException.java | 9 + .../CommandInitializationException.java | 13 + .../exception/CommunicationException.java | 36 + .../comm/exception/CrcMismatchException.java | 24 + .../IllegalDeliveryStatusException.java | 26 + .../exception/IllegalPacketTypeException.java | 27 + .../IllegalPodProgressException.java | 25 + .../exception/IllegalResponseException.java | 26 + .../IllegalSetupProgressException.java | 26 + .../exception/MessageDecodingException.java | 13 + .../exception/NonceOutOfSyncException.java | 9 + .../comm/exception/NonceResyncException.java | 9 + .../exception/NotEnoughDataException.java | 17 + .../comm/exception/PodFaultException.java | 17 + .../PodReturnedErrorResponseException.java | 17 + .../comm/message/IRawRepresentable.java | 5 + .../omnipod/comm/message/MessageBlock.java | 31 + .../message/NonceResyncableMessageBlock.java | 7 + .../omnipod/comm/message/OmnipodMessage.java | 148 +++ .../omnipod/comm/message/OmnipodPacket.java | 82 ++ .../command/AcknowledgeAlertsCommand.java | 54 + .../message/command/AssignAddressCommand.java | 31 + .../command/BasalScheduleExtraCommand.java | 127 ++ .../message/command/BeepConfigCommand.java | 61 + .../message/command/BolusExtraCommand.java | 76 ++ .../command/CancelDeliveryCommand.java | 71 ++ .../command/ConfigureAlertsCommand.java | 50 + .../message/command/ConfigurePodCommand.java | 56 + .../message/command/DeactivatePodCommand.java | 41 + .../message/command/FaultConfigCommand.java | 50 + .../message/command/GetStatusCommand.java | 30 + .../command/SetInsulinScheduleCommand.java | 98 ++ .../command/TempBasalExtraCommand.java | 108 ++ .../comm/message/response/ErrorResponse.java | 50 + .../comm/message/response/StatusResponse.java | 119 ++ .../message/response/VersionResponse.java | 91 ++ .../message/response/podinfo/PodInfo.java | 17 + .../response/podinfo/PodInfoActiveAlerts.java | 92 ++ .../response/podinfo/PodInfoDataLog.java | 83 ++ .../PodInfoFaultAndInitializationTime.java | 54 + .../response/podinfo/PodInfoFaultEvent.java | 172 +++ .../podinfo/PodInfoOlderPulseLog.java | 56 + .../podinfo/PodInfoRecentPulseLog.java | 64 + .../response/podinfo/PodInfoResponse.java | 40 + .../pump/omnipod/defs/AlertConfiguration.java | 71 ++ .../defs/AlertConfigurationFactory.java | 32 + .../plugins/pump/omnipod/defs/AlertSet.java | 44 + .../plugins/pump/omnipod/defs/AlertSlot.java | 35 + .../pump/omnipod/defs/AlertTrigger.java | 14 + .../plugins/pump/omnipod/defs/AlertType.java | 11 + .../pump/omnipod/defs/BeepConfigType.java | 41 + .../plugins/pump/omnipod/defs/BeepRepeat.java | 23 + .../plugins/pump/omnipod/defs/BeepType.java | 34 + .../pump/omnipod/defs/DeliveryStatus.java | 33 + .../pump/omnipod/defs/DeliveryType.java | 27 + .../pump/omnipod/defs/ErrorResponseType.java | 24 + .../pump/omnipod/defs/FaultEventType.java | 148 +++ .../pump/omnipod/defs/FirmwareVersion.java | 32 + .../pump/omnipod/defs/LogEventErrorCode.java | 24 + .../pump/omnipod/defs/MessageBlockType.java | 63 + .../plugins/pump/omnipod/defs/NonceState.java | 53 + .../pump/omnipod/defs/OmnipodCommandType.java | 24 + .../OmnipodCommunicationManagerInterface.java | 79 ++ .../omnipod/defs/OmnipodCustomActionType.java | 24 + .../pump/omnipod/defs/OmnipodPodType.java | 6 + .../defs/OmnipodPumpPluginInterface.java | 15 + .../omnipod/defs/OmnipodStatusRequest.java | 20 + .../omnipod/defs/OmnipodUIResponseType.java | 13 + .../plugins/pump/omnipod/defs/PacketType.java | 42 + .../pump/omnipod/defs/PodDeviceState.java | 38 + .../pump/omnipod/defs/PodInfoType.java | 61 + .../pump/omnipod/defs/PodInitActionType.java | 88 ++ .../pump/omnipod/defs/PodInitReceiver.java | 7 + .../pump/omnipod/defs/PodProgressStatus.java | 43 + .../pump/omnipod/defs/PodResponseType.java | 9 + .../pump/omnipod/defs/SetupProgress.java | 21 + .../pump/omnipod/defs/TimerAlertTrigger.java | 9 + .../defs/UnitsRemainingAlertTrigger.java | 7 + .../defs/schedule/BasalDeliverySchedule.java | 61 + .../defs/schedule/BasalDeliveryTable.java | 112 ++ .../omnipod/defs/schedule/BasalSchedule.java | 147 +++ .../defs/schedule/BasalScheduleEntry.java | 40 + .../defs/schedule/BasalTableEntry.java | 53 + .../defs/schedule/BolusDeliverySchedule.java | 60 + .../defs/schedule/DeliverySchedule.java | 10 + .../defs/schedule/InsulinScheduleType.java | 26 + .../pump/omnipod/defs/schedule/RateEntry.java | 77 ++ .../schedule/TempBasalDeliverySchedule.java | 69 ++ .../omnipod/defs/state/PodSessionState.java | 299 +++++ .../omnipod/defs/state/PodSetupState.java | 43 + .../pump/omnipod/defs/state/PodState.java | 68 ++ .../defs/state/PodStateChangedHandler.java | 6 + .../omnipod/dialogs/PodHistoryActivity.java | 339 ++++++ .../omnipod/dialogs/PodManagementActivity.kt | 170 +++ .../dialogs/wizard/defs/PodActionType.java | 7 + .../wizard/initpod/InitActionFragment.java | 241 ++++ .../wizard/initpod/InitActionPage.java | 73 ++ .../dialogs/wizard/initpod/InitPodTask.java | 62 + .../wizard/model/FullInitPodWizardModel.java | 48 + .../wizard/model/InitPodWizardModel.java | 21 + .../wizard/model/RemovePodWizardModel.java | 66 ++ .../wizard/model/ShortInitPodWizardModel.java | 39 + .../wizard/pages/InitPodRefreshAction.java | 100 ++ .../dialogs/wizard/pages/PodInfoFragment.java | 189 +++ .../dialogs/wizard/pages/PodInfoPage.java | 34 + .../removepod/RemoveActionFragment.java | 72 ++ .../wizard/removepod/RemovePodActionPage.java | 31 + .../omnipod/driver/OmnipodDriverState.java | 10 + .../omnipod/driver/OmnipodPumpStatus.java | 193 +++ .../driver/comm/AapsOmnipodManager.java | 757 ++++++++++++ .../pump/omnipod/driver/db/PodHistory.java | 136 +++ .../driver/db/PodHistoryEntryType.java | 93 ++ .../pump/omnipod/driver/ui/OmnipodUIComm.java | 83 ++ .../driver/ui/OmnipodUIPostprocessor.java | 111 ++ .../pump/omnipod/driver/ui/OmnipodUITask.java | 248 ++++ .../EventOmnipodAcknowledgeAlertsChanged.kt | 8 + .../events/EventOmnipodDeviceStatusChange.kt | 46 + .../events/EventOmnipodPumpValuesChanged.kt | 8 + .../events/EventOmnipodRefreshButtonState.kt | 5 + .../omnipod/exception/OmnipodException.java | 23 + .../service/RileyLinkOmnipodService.java | 240 ++++ .../plugins/pump/omnipod/util/OmniCRC.java | 74 ++ .../pump/omnipod/util/OmnipodConst.java | 56 + .../pump/omnipod/util/OmnipodUtil.java | 195 ++++ app/src/main/res/layout/omnipod_fragment.xml | 633 ++++++++++ app/src/main/res/layout/omnipod_initpod.xml | 106 ++ .../layout/omnipod_initpod_init_action.xml | 80 ++ .../res/layout/omnipod_initpod_pod_info.xml | 56 + .../layout/omnipod_initpod_pod_info_item.xml | 45 + .../layout/omnipod_pod_history_activity.xml | 58 + .../res/layout/omnipod_pod_history_item.xml | 33 + app/src/main/res/layout/omnipod_pod_mgmt.xml | 115 ++ app/src/main/res/values/strings.xml | 143 +++ app/src/main/res/values/styles.xml | 32 + app/src/main/res/xml/pref_omnipod.xml | 45 + .../command/AcknowledgeAlertsCommandTest.java | 42 + .../command/AssignAddressCommandTest.java | 21 + .../BasalScheduleExtraCommandTest.java | 173 +++ .../command/BeepConfigCommandTest.java | 27 + .../command/BolusExtraCommandTest.java | 62 + .../command/CancelDeliveryCommandTest.java | 45 + .../command/ConfigureAlertsCommandTest.java | 96 ++ .../command/ConfigurePodCommandTest.java | 25 + .../command/DeactivatePodCommandTest.java | 23 + .../command/FaultConfigCommandTest.java | 5 + .../message/command/GetStatusCommandTest.java | 31 + .../SetInsulinScheduleCommandTest.java | 248 ++++ .../command/TempBasalExtraCommandTest.java | 75 ++ .../defs/schedule/BasalTableEntryTest.java | 21 + .../message/response/ErrorResponseTest.java | 46 + .../message/response/StatusResponseTest.java | 104 ++ .../message/response/VersionResponseTest.java | 69 ++ .../podinfo/PodInfoActiveAlertsTest.java | 35 + .../response/podinfo/PodInfoDataLogTest.java | 23 + ...PodInfoFaultAndInitializationTimeTest.java | 28 + .../podinfo/PodInfoFaultEventTest.java | 118 ++ .../podinfo/PodInfoOlderPulseLogTest.java | 5 + .../podinfo/PodInfoRecentPulseLogTest.java | 16 + .../response/podinfo/PodInfoResponseTest.java | 58 + .../defs/state/PodSessionStateTest.java | 107 ++ .../driver/comm/AapsOmnipodManagerTest.java | 163 +++ 206 files changed, 14916 insertions(+), 71 deletions(-) create mode 100644 app/libs/com.atech-software.android.library.wizardpager-1.1.1.aar create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/defs/RileyLinkPumpDevice.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodFragment.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodPumpPlugin.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/BolusProgressIndicationConsumer.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodCommunicationManager.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodManager.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/SetupActionResult.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AcknowledgeAlertsAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AssignAddressAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/BolusAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/CancelDeliveryAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigureAlertsAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigurePodAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/DeactivatePodAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetPodInfoAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetStatusAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/InsertCannulaAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/OmnipodAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PrimeAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetBasalScheduleAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetTempBasalAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/InsertCannulaService.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PrimeService.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/ActionInitializationException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommandInitializationException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommunicationException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CrcMismatchException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalDeliveryStatusException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPacketTypeException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPodProgressException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalResponseException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalSetupProgressException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/MessageDecodingException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceOutOfSyncException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceResyncException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NotEnoughDataException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodFaultException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodReturnedErrorResponseException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/IRawRepresentable.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/MessageBlock.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/NonceResyncableMessageBlock.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodMessage.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodPacket.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommand.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponse.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponse.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponse.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfo.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlerts.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLog.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTime.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEvent.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderPulseLog.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentPulseLog.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponse.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfiguration.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfigurationFactory.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSet.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSlot.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertTrigger.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepConfigType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepRepeat.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryStatus.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/ErrorResponseType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FaultEventType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FirmwareVersion.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/LogEventErrorCode.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/MessageBlockType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/NonceState.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommandType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommunicationManagerInterface.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCustomActionType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPodType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPumpPluginInterface.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodStatusRequest.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodUIResponseType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PacketType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodDeviceState.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInfoType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitActionType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitReceiver.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodProgressStatus.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodResponseType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/SetupProgress.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/TimerAlertTrigger.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/UnitsRemainingAlertTrigger.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliverySchedule.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliveryTable.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalSchedule.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleEntry.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalTableEntry.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BolusDeliverySchedule.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/DeliverySchedule.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/InsulinScheduleType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/RateEntry.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/TempBasalDeliverySchedule.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSessionState.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSetupState.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodState.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodStateChangedHandler.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodHistoryActivity.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodManagementActivity.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/defs/PodActionType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionFragment.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionPage.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitPodTask.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/FullInitPodWizardModel.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/InitPodWizardModel.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/RemovePodWizardModel.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/ShortInitPodWizardModel.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/InitPodRefreshAction.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoFragment.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoPage.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemoveActionFragment.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemovePodActionPage.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodDriverState.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodPumpStatus.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/comm/AapsOmnipodManager.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistory.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistoryEntryType.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIComm.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIPostprocessor.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUITask.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodAcknowledgeAlertsChanged.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodDeviceStatusChange.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodPumpValuesChanged.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodRefreshButtonState.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/OmnipodException.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/service/RileyLinkOmnipodService.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmniCRC.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodConst.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodUtil.java create mode 100644 app/src/main/res/layout/omnipod_fragment.xml create mode 100644 app/src/main/res/layout/omnipod_initpod.xml create mode 100644 app/src/main/res/layout/omnipod_initpod_init_action.xml create mode 100644 app/src/main/res/layout/omnipod_initpod_pod_info.xml create mode 100644 app/src/main/res/layout/omnipod_initpod_pod_info_item.xml create mode 100644 app/src/main/res/layout/omnipod_pod_history_activity.xml create mode 100644 app/src/main/res/layout/omnipod_pod_history_item.xml create mode 100644 app/src/main/res/layout/omnipod_pod_mgmt.xml create mode 100644 app/src/main/res/xml/pref_omnipod.xml create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommandTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/defs/schedule/BasalTableEntryTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponseTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponseTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponseTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlertsTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLogTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTimeTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEventTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderPulseLogTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentPulseLogTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponseTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSessionStateTest.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/comm/AapsOmnipodManagerTest.java diff --git a/app/build.gradle b/app/build.gradle index 0867f61e70..fe6ce65442 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -278,6 +278,7 @@ dependencies { implementation "com.joanzapata.iconify:android-iconify-fontawesome:2.2.2" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.madgag.spongycastle:core:1.58.0.0' + implementation(name: "com.atech-software.android.library.wizardpager-1.1.1", ext: "aar") implementation("com.google.android:flexbox:0.3.0") { exclude group: "com.android.support" diff --git a/app/libs/com.atech-software.android.library.wizardpager-1.1.1.aar b/app/libs/com.atech-software.android.library.wizardpager-1.1.1.aar new file mode 100644 index 0000000000000000000000000000000000000000..57e2f806bb18165fbd3ffe0bc1e15112e2648102 GIT binary patch literal 96989 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 literal 0 HcmV?d00001 diff --git a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java index 93807543f5..d1c63d284c 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java @@ -57,6 +57,7 @@ import info.nightscout.androidaps.plugins.pump.danaR.comm.RecordTypes; import info.nightscout.androidaps.plugins.pump.insight.database.InsightBolusID; import info.nightscout.androidaps.plugins.pump.insight.database.InsightHistoryOffset; import info.nightscout.androidaps.plugins.pump.insight.database.InsightPumpID; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.db.PodHistory; import info.nightscout.androidaps.plugins.pump.virtual.VirtualPumpPlugin; import info.nightscout.androidaps.utils.JsonHelper; import info.nightscout.androidaps.utils.PercentageSplitter; @@ -86,8 +87,9 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public static final String DATABASE_INSIGHT_HISTORY_OFFSETS = "InsightHistoryOffsets"; public static final String DATABASE_INSIGHT_BOLUS_IDS = "InsightBolusIDs"; public static final String DATABASE_INSIGHT_PUMP_IDS = "InsightPumpIDs"; + public static final String DATABASE_POD_HISTORY = "PodHistory"; - private static final int DATABASE_VERSION = 11; + private static final int DATABASE_VERSION = 12; public static Long earliestDataChange = null; @@ -135,6 +137,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { TableUtils.createTableIfNotExists(connectionSource, InsightHistoryOffset.class); TableUtils.createTableIfNotExists(connectionSource, InsightBolusID.class); TableUtils.createTableIfNotExists(connectionSource, InsightPumpID.class); + TableUtils.createTableIfNotExists(connectionSource, PodHistory.class); database.execSQL("INSERT INTO sqlite_sequence (name, seq) SELECT \"" + DATABASE_INSIGHT_BOLUS_IDS + "\", " + System.currentTimeMillis() + " " + "WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name = \"" + DATABASE_INSIGHT_BOLUS_IDS + "\")"); database.execSQL("INSERT INTO sqlite_sequence (name, seq) SELECT \"" + DATABASE_INSIGHT_PUMP_IDS + "\", " + System.currentTimeMillis() + " " + @@ -211,6 +214,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { TableUtils.dropTable(connectionSource, CareportalEvent.class, true); TableUtils.dropTable(connectionSource, ProfileSwitch.class, true); TableUtils.dropTable(connectionSource, TDD.class, true); + TableUtils.dropTable(connectionSource, PodHistory.class, true); TableUtils.createTableIfNotExists(connectionSource, TempTarget.class); TableUtils.createTableIfNotExists(connectionSource, BgReading.class); TableUtils.createTableIfNotExists(connectionSource, DanaRHistoryRecord.class); @@ -220,6 +224,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { TableUtils.createTableIfNotExists(connectionSource, CareportalEvent.class); TableUtils.createTableIfNotExists(connectionSource, ProfileSwitch.class); TableUtils.createTableIfNotExists(connectionSource, TDD.class); + TableUtils.createTableIfNotExists(connectionSource, PodHistory.class); updateEarliestDataChange(0); } catch (SQLException e) { log.error("Unhandled exception", e); @@ -354,6 +359,10 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return getDao(InsightHistoryOffset.class); } + private Dao getDaoPodHistory() throws SQLException { + return getDao(PodHistory.class); + } + public static long roundDateToSec(long date) { long rounded = date - date % 1000; if (rounded != date) @@ -1847,4 +1856,34 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } // ---------------- Food handling --------------- + + // ---------------- PodHistory handling --------------- + + public void createOrUpdate(PodHistory podHistory) { + try { + getDaoPodHistory().createOrUpdate(podHistory); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + } + + + public List getPodHistoryFromTime(long from, boolean ascending) { + try { + Dao daoPodHistory = getDaoPodHistory(); + List podHistories; + QueryBuilder queryBuilder = daoPodHistory.queryBuilder(); + queryBuilder.orderBy("date", ascending); + //queryBuilder.limit(100L); + Where where = queryBuilder.where(); + where.ge("date", from); + PreparedQuery preparedQuery = queryBuilder.prepare(); + podHistories = daoPodHistory.query(preparedQuery); + return podHistories; + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return new ArrayList<>(); + } + } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java index 3089ca1a7c..b13cd74796 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java @@ -67,6 +67,9 @@ public class Notification { public static final int OVER_24H_TIME_CHANGE_REQUESTED = 54; public static final int INVALID_VERSION = 55; public static final int PERMISSION_SYSTEM_WINDOW = 56; + public static final int OMNIPOD_PUMP_ALARM = 57; + public static final int TIME_OR_TIMEZONE_CHANGE = 58; + public static final int OMNIPOD_POD_NOT_ATTACHED = 59; public static final int USERMESSAGE = 1000; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/PumpPluginAbstract.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/PumpPluginAbstract.java index efca466951..126b017b5e 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/PumpPluginAbstract.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/PumpPluginAbstract.java @@ -57,11 +57,14 @@ import io.reactivex.schedulers.Schedulers; public abstract class PumpPluginAbstract extends PumpPluginBase implements PumpInterface, ConstraintsInterface { private CompositeDisposable disposable = new CompositeDisposable(); + protected HasAndroidInjector injector; protected AAPSLogger aapsLogger; protected RxBusWrapper rxBus; protected ActivePluginProvider activePlugin; protected Context context; protected FabricPrivacy fabricPrivacy; + protected ResourceHelper resourceHelper; + protected CommandQueueProvider commandQueue; protected SP sp; /* @@ -99,7 +102,9 @@ public abstract class PumpPluginAbstract extends PumpPluginBase implements PumpI this.activePlugin = activePlugin; this.context = context; this.fabricPrivacy = fabricPrivacy; + this.resourceHelper = resourceHelper; this.sp = sp; + this.commandQueue = commandQueue; pumpDescription.setPumpDescription(pumpType); this.pumpType = pumpType; @@ -109,12 +114,6 @@ public abstract class PumpPluginAbstract extends PumpPluginBase implements PumpI public abstract void initPumpStatusData(); - public abstract void resetRileyLinkConfiguration(); - - public abstract void doTuneUpDevice(); - - public abstract RileyLinkService getRileyLinkService(); - @Override protected void onStart() { super.onStart(); @@ -464,6 +463,7 @@ public abstract class PumpPluginAbstract extends PumpPluginBase implements PumpI public void setPumpType(PumpType pumpType) { this.pumpType = pumpType; + this.pumpDescription.setPumpDescription(pumpType); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/data/PumpStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/data/PumpStatus.java index b704de584f..34a3418902 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/data/PumpStatus.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/data/PumpStatus.java @@ -49,16 +49,20 @@ public abstract class PumpStatus { public int tempBasalRatio = 0; public int tempBasalRemainMin = 0; public Date tempBasalStart; + private PumpType pumpType; //protected PumpDescription pumpDescription; - public PumpStatus() { + public PumpStatus(PumpType pumpType) { // public PumpStatus(PumpDescription pumpDescription) { // this.pumpDescription = pumpDescription; // this.initSettings(); + this.pumpType = pumpType; } + public abstract void initSettings(); + public void setLastCommunicationToNow() { this.lastDataTime = DateUtil.now(); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java index 4cd32df3f7..3fff66fdf7 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java @@ -57,13 +57,13 @@ public enum PumpType { new DoseSettings(0.01d, 15, 24 * 60, 0.05d), // PumpTempBasalType.Percent, new DoseSettings(10, 15, 24 * 60, 0d, 250d), PumpCapability.BasalRate_Duration15and30minAllowed, // - 0.02d, null, 0.01d, DoseStepSize.InsightBolus, PumpCapability.InsightCapabilities, false, false), // + 0.02d, null, 0.01d, DoseStepSize.InsightBolus, PumpCapability.InsightCapabilities), // AccuChekSolo("Accu-Chek Solo", ManufacturerType.Roche, "Solo", 0.01d, null, // new DoseSettings(0.01d, 15, 24 * 60, 0.05d), // PumpTempBasalType.Percent, new DoseSettings(10, 15, 24 * 60, 0d, 250d), PumpCapability.BasalRate_Duration15and30minAllowed, // - 0.02d, null, 0.01d, DoseStepSize.InsightBolus, PumpCapability.InsightCapabilities, false, false), // + 0.02d, null, 0.01d, DoseStepSize.InsightBolus, PumpCapability.InsightCapabilities), // // Animas diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/dialog/RileyLinkBLEScanActivity.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/dialog/RileyLinkBLEScanActivity.java index 50a617a94b..ccb47a7d4a 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/dialog/RileyLinkBLEScanActivity.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/dialog/RileyLinkBLEScanActivity.java @@ -41,14 +41,20 @@ import javax.inject.Inject; import info.nightscout.androidaps.R; import info.nightscout.androidaps.activities.NoSplashAppCompatActivity; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; +import info.nightscout.androidaps.interfaces.PumpInterface; import info.nightscout.androidaps.logging.AAPSLogger; import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.common.ManufacturerType; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.GattAttributes; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; import info.nightscout.androidaps.plugins.pump.common.utils.LocationHelper; import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin; import info.nightscout.androidaps.plugins.pump.medtronic.events.EventMedtronicPumpConfigurationChanged; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged; import info.nightscout.androidaps.utils.resources.ResourceHelper; import info.nightscout.androidaps.utils.sharedPreferences.SP; @@ -60,8 +66,7 @@ public class RileyLinkBLEScanActivity extends NoSplashAppCompatActivity { @Inject RxBusWrapper rxBus; @Inject ResourceHelper resourceHelper; @Inject RileyLinkUtil rileyLinkUtil; - // TODO change this. Currently verifyConfiguration uses MDT data not only RL - @Inject MedtronicPumpPlugin medtronicPumpPlugin; + @Inject ActivePluginProvider activePlugin; private static final int PERMISSION_REQUEST_COARSE_LOCATION = 30241; // arbitrary. private static final int REQUEST_ENABLE_BT = 30242; // arbitrary @@ -110,9 +115,24 @@ public class RileyLinkBLEScanActivity extends NoSplashAppCompatActivity { sp.putString(RileyLinkConst.Prefs.RileyLinkAddress, bleAddress); - medtronicPumpPlugin.getRileyLinkService().verifyConfiguration(); // force reloading of address + PumpInterface activePump = activePlugin.getActivePump(); - rxBus.send(new EventMedtronicPumpConfigurationChanged()); + if (activePump.manufacturer()== ManufacturerType.Medtronic) { + RileyLinkPumpDevice rileyLinkPump = (RileyLinkPumpDevice)activePump; + rileyLinkPump.getRileyLinkService().verifyConfiguration(); // force reloading of address + + rxBus.send(new EventMedtronicPumpConfigurationChanged()); + + } else if (activePlugin.getActivePump().manufacturer()== ManufacturerType.Insulet) { + if (activePump.model()== PumpType.Insulet_Omnipod_Dash) { + aapsLogger.error("Omnipod Dash not yet implemented."); + } else { + RileyLinkPumpDevice rileyLinkPump = (RileyLinkPumpDevice)activePump; + rileyLinkPump.getRileyLinkService().verifyConfiguration(); // force reloading of address + + rxBus.send(new EventOmnipodPumpValuesChanged()); + } + } finish(); }); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java index 4887d3d4a3..d352250071 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java @@ -21,7 +21,7 @@ import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks.WakeAndTuneTask; import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; -import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus; +import info.nightscout.androidaps.utils.Round; import info.nightscout.androidaps.utils.sharedPreferences.SP; /** @@ -33,16 +33,14 @@ public abstract class RileyLinkCommunicationManager { @Inject protected AAPSLogger aapsLogger; @Inject protected SP sp; - - @Inject MedtronicPumpStatus medtronicPumpStatus; - @Inject RileyLinkServiceData rileyLinkServiceData; - @Inject ServiceTaskExecutor serviceTaskExecutor; + @Inject protected RileyLinkServiceData rileyLinkServiceData; + @Inject protected ServiceTaskExecutor serviceTaskExecutor; private final int SCAN_TIMEOUT = 1500; private final int ALLOWED_PUMP_UNREACHABLE = 10 * 60 * 1000; // 10 minutes - protected final HasAndroidInjector injector; + public final HasAndroidInjector injector; protected final RFSpy rfspy; protected int receiverDeviceAwakeForMinutes = 1; // override this in constructor of specific implementation protected String receiverDeviceID; // String representation of receiver device (ex. Pump (xxxxxx) or Pod (yyyyyy)) @@ -77,7 +75,7 @@ public abstract class RileyLinkCommunicationManager { return sendAndListen(msg, timeout_ms, repeatCount, 0, extendPreamble_ms, clazz); } - private E sendAndListen(RLMessage msg, int timeout_ms, int repeatCount, int retryCount, Integer extendPreamble_ms, Class clazz) + protected E sendAndListen(RLMessage msg, int timeout_ms, int repeatCount, int retryCount, Integer extendPreamble_ms, Class clazz) throws RileyLinkCommunicationException { // internal flag @@ -129,6 +127,9 @@ public abstract class RileyLinkCommunicationManager { public abstract E createResponseMessage(byte[] payload, Class clazz); + public abstract void setPumpDeviceState(PumpDeviceState pumpDeviceState); + + public void wakeUp(boolean force) { wakeUp(receiverDeviceAwakeForMinutes, force); } @@ -150,7 +151,7 @@ public abstract class RileyLinkCommunicationManager { // **** FIXME: this wakeup doesn't seem to work well... must revisit // receiverDeviceAwakeForMinutes = duration_minutes; - medtronicPumpStatus.setPumpDeviceState(PumpDeviceState.WakingUp); + setPumpDeviceState(PumpDeviceState.WakingUp); if (force) nextWakeUpRequired = 0L; @@ -208,7 +209,7 @@ public abstract class RileyLinkCommunicationManager { double[] scanFrequencies = rileyLinkServiceData.rileyLinkTargetFrequency.getScanFrequencies(); if (scanFrequencies.length == 1) { - return RileyLinkUtil.isSame(scanFrequencies[0], frequency); + return Round.isSame(scanFrequencies[0], frequency); } else { return (scanFrequencies[0] <= frequency && scanFrequencies[scanFrequencies.length - 1] >= frequency); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkUtil.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkUtil.java index 8fc900aa90..ff524b2ed2 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkUtil.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkUtil.java @@ -18,6 +18,7 @@ import javax.inject.Singleton; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.encoding.Encoding4b6b; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.encoding.Encoding4b6bGeoff; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkEncodingType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkTargetFrequency; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.data.BleAdvertisedData; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.data.RLHistoryItem; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.data.ServiceResult; @@ -37,6 +38,9 @@ public class RileyLinkUtil { private RileyLinkEncodingType encoding; private Encoding4b6b encoding4b6b; + // TODO maybe not needed + private RileyLinkTargetFrequency rileyLinkTargetFrequency; + @Inject public RileyLinkUtil() { } @@ -154,4 +158,8 @@ public class RileyLinkUtil { public Encoding4b6b getEncoding4b6b() { return encoding4b6b; } + + public void setRileyLinkTargetFrequency(RileyLinkTargetFrequency rileyLinkTargetFrequency_) { + this.rileyLinkTargetFrequency = rileyLinkTargetFrequency_; + } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RFSpy.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RFSpy.java index ef3641fa02..f3f6399570 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RFSpy.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RFSpy.java @@ -57,6 +57,7 @@ public class RFSpy { private UUID radioDataUUID = UUID.fromString(GattAttributes.CHARA_RADIO_DATA); private UUID radioVersionUUID = UUID.fromString(GattAttributes.CHARA_RADIO_VERSION); private UUID responseCountUUID = UUID.fromString(GattAttributes.CHARA_RADIO_RESPONSE_COUNT); + private RileyLinkFirmwareVersion firmwareVersion; private String bleVersion; // We don't use it so no need of sofisticated logic private Double currentFrequencyMHz; @@ -68,6 +69,12 @@ public class RFSpy { reader = new RFSpyReader(aapsLogger, rileyLinkBle); } + + public RileyLinkFirmwareVersion getRLVersionCached() { + return firmwareVersion; + } + + public String getBLEVersionCached() { return bleVersion; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/data/RLHistoryItem.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/data/RLHistoryItem.java index f8d1909bad..a75e59b17a 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/data/RLHistoryItem.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/data/RLHistoryItem.java @@ -8,6 +8,8 @@ import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLin import info.nightscout.androidaps.plugins.pump.medtronic.defs.MedtronicCommandType; import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; import info.nightscout.androidaps.utils.resources.ResourceHelper; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommandType; + /** * Created by andy on 5/19/18. @@ -23,6 +25,7 @@ public class RLHistoryItem { private RileyLinkTargetDevice targetDevice; private PumpDeviceState pumpDeviceState; + private OmnipodCommandType omnipodCommandType; public RLHistoryItem(RileyLinkServiceState serviceState, RileyLinkError errorCode, @@ -50,6 +53,13 @@ public class RLHistoryItem { } + public RLHistoryItem(OmnipodCommandType omnipodCommandType) { + this.dateTime = new LocalDateTime(); + this.omnipodCommandType = omnipodCommandType; + source = RLHistoryItemSource.OmnipodCommand; + } + + public LocalDateTime getDateTime() { return dateTime; } @@ -79,6 +89,9 @@ public class RLHistoryItem { case MedtronicCommand: return medtronicCommandType.name(); + case OmnipodCommand: + return omnipodCommandType.name(); + default: return "Unknown Description"; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/defs/RileyLinkPumpDevice.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/defs/RileyLinkPumpDevice.java new file mode 100644 index 0000000000..7ad31c2066 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/defs/RileyLinkPumpDevice.java @@ -0,0 +1,19 @@ +package info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs; + +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkService; + +public interface RileyLinkPumpDevice { + + void setIsBusy(boolean isBusy_); + + boolean isBusy(); + + void resetRileyLinkConfiguration(); + + boolean hasTuneUp(); + + void doTuneUpDevice(); + + RileyLinkService getRileyLinkService(); + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/dialog/RileyLinkStatusGeneralFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/dialog/RileyLinkStatusGeneralFragment.java index 34833f707e..623ecae9ef 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/dialog/RileyLinkStatusGeneralFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/dialog/RileyLinkStatusGeneralFragment.java @@ -13,7 +13,15 @@ import java.util.Locale; import javax.inject.Inject; import dagger.android.support.DaggerFragment; +import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; +import info.nightscout.androidaps.interfaces.PumpDescription; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.plugins.common.ManufacturerType; +import info.nightscout.androidaps.plugins.pump.common.PumpPluginAbstract; +import info.nightscout.androidaps.plugins.pump.common.data.PumpStatus; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; import info.nightscout.androidaps.plugins.pump.common.dialog.RefreshableInterface; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkFirmwareVersion; @@ -23,6 +31,7 @@ import info.nightscout.androidaps.plugins.pump.common.utils.StringUtil; import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin; import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus; import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; import info.nightscout.androidaps.utils.resources.ResourceHelper; /** @@ -31,11 +40,10 @@ import info.nightscout.androidaps.utils.resources.ResourceHelper; public class RileyLinkStatusGeneralFragment extends DaggerFragment implements RefreshableInterface { - @Inject RileyLinkUtil rileyLinkUtil; - @Inject MedtronicUtil medtronicUtil; - @Inject MedtronicPumpStatus medtronicPumpStatus; + @Inject ActivePluginProvider activePlugin; @Inject ResourceHelper resourceHelper; - @Inject MedtronicPumpPlugin medtronicPumpPlugin; + @Inject MedtronicUtil medtronicUtil; + @Inject AAPSLogger aapsLogger; @Inject RileyLinkServiceData rileyLinkServiceData; TextView connectionStatus; @@ -120,16 +128,16 @@ public class RileyLinkStatusGeneralFragment extends DaggerFragment implements Re } - // TODO add handling for Omnipod pump status + PumpPluginAbstract pumpPlugin = (PumpPluginAbstract)activePlugin.getActivePump(); + + if (pumpPlugin.manufacturer()== ManufacturerType.Medtronic) { + MedtronicPumpStatus medtronicPumpStatus = (MedtronicPumpStatus)pumpPlugin.getPumpStatusData(); - if (medtronicPumpStatus != null) { this.deviceType.setText(resourceHelper.gs(RileyLinkTargetDevice.MedtronicPump.getResourceId())); - this.deviceModel.setText(medtronicPumpPlugin.getPumpDescription().pumpType.getDescription()); + this.deviceModel.setText(pumpPlugin.getPumpType().getDescription()); this.serialNumber.setText(medtronicPumpStatus.serialNumber); this.pumpFrequency.setText(resourceHelper.gs(medtronicPumpStatus.pumpFrequency.equals("medtronic_pump_frequency_us_ca") ? R.string.medtronic_pump_frequency_us_ca : R.string.medtronic_pump_frequency_worldwide)); - // TODO extend when Omnipod used - if (medtronicUtil.getMedtronicPumpModel() != null) this.connectedDevice.setText("Medtronic " + medtronicUtil.getMedtronicPumpModel().getPumpModel()); else @@ -143,7 +151,44 @@ public class RileyLinkStatusGeneralFragment extends DaggerFragment implements Re this.lastDeviceContact.setText(StringUtil.toDateTimeString(new LocalDateTime( medtronicPumpStatus.lastDataTime))); else - this.lastDeviceContact.setText("Never"); + this.lastDeviceContact.setText(resourceHelper.gs(R.string.common_never)); + } else { + + OmnipodPumpStatus omnipodPumpStatus = (OmnipodPumpStatus)pumpPlugin.getPumpStatusData(); + + this.deviceType.setText(resourceHelper.gs(RileyLinkTargetDevice.Omnipod.getResourceId())); + this.deviceModel.setText(pumpPlugin.getPumpType() == PumpType.Insulet_Omnipod ? "Eros" : "Dash"); + + if (pumpPlugin.getPumpType()== PumpType.Insulet_Omnipod_Dash) { + aapsLogger.error("Omnipod Dash not yet supported !!!"); + + this.pumpFrequency.setText("-"); + } else { + + this.pumpFrequency.setText(resourceHelper.gs(R.string.omnipod_frequency)); + + if (omnipodPumpStatus != null) { + + if (omnipodPumpStatus.podAvailable) { + this.serialNumber.setText(omnipodPumpStatus.podLotNumber); + this.connectedDevice.setText(omnipodPumpStatus.pumpType == PumpType.Insulet_Omnipod ? "Eros Pod" : "Dash Pod"); + } else { + this.serialNumber.setText("??"); + this.connectedDevice.setText("-"); + } + + if (rileyLinkServiceData.lastGoodFrequency != null) + this.lastUsedFrequency.setText(String.format(Locale.ENGLISH, "%.2f MHz", + rileyLinkServiceData.lastGoodFrequency)); + + if (omnipodPumpStatus.lastConnection != 0) + this.lastDeviceContact.setText(StringUtil.toDateTimeString(new LocalDateTime( + omnipodPumpStatus.lastDataTime))); + else + this.lastDeviceContact.setText(resourceHelper.gs(R.string.common_never)); + } + } + } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/RileyLinkService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/RileyLinkService.java index 414bc6177b..09273eb719 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/RileyLinkService.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/RileyLinkService.java @@ -10,6 +10,8 @@ import org.jetbrains.annotations.NotNull; import javax.inject.Inject; import dagger.android.DaggerService; +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; import info.nightscout.androidaps.logging.AAPSLogger; import info.nightscout.androidaps.logging.LTag; import info.nightscout.androidaps.plugins.bus.RxBusWrapper; @@ -27,6 +29,7 @@ import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.data. import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus; import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil; +import info.nightscout.androidaps.utils.resources.ResourceHelper; import info.nightscout.androidaps.utils.sharedPreferences.SP; /** @@ -40,9 +43,10 @@ public abstract class RileyLinkService extends DaggerService { @Inject protected Context context; @Inject protected RxBusWrapper rxBus; @Inject protected RileyLinkUtil rileyLinkUtil; - @Inject protected MedtronicUtil medtronicUtil; // TODO should be avoided here as it's MDT + @Inject protected HasAndroidInjector injector; + @Inject protected ResourceHelper resourceHelper; @Inject protected RileyLinkServiceData rileyLinkServiceData; - @Inject protected MedtronicPumpStatus medtronicPumpStatus; + @Inject protected ActivePluginProvider activePlugin; @NotNull protected RileyLinkBLE rileyLinkBLE; // android-bluetooth management, must be set in initRileyLinkServiceData protected BluetoothAdapter bluetoothAdapter; @@ -202,11 +206,13 @@ public abstract class RileyLinkService extends DaggerService { } + + // FIXME: This needs to be run in a session so that is interruptable, has a separate thread, etc. public void doTuneUpDevice() { rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.TuneUpDevice); - medtronicPumpStatus.setPumpDeviceState(PumpDeviceState.Sleeping); + setPumpDeviceState(PumpDeviceState.Sleeping); double lastGoodFrequency = 0.0d; @@ -238,6 +244,9 @@ public abstract class RileyLinkService extends DaggerService { } + public abstract void setPumpDeviceState(PumpDeviceState pumpDeviceState); + + public void disconnectRileyLink() { if (rileyLinkBLE.isConnected()) { @@ -272,4 +281,6 @@ public abstract class RileyLinkService extends DaggerService { else return null; } + + public abstract boolean verifyConfiguration(); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/InitializePumpManagerTask.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/InitializePumpManagerTask.java index d97399e6a1..97a7adc018 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/InitializePumpManagerTask.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/InitializePumpManagerTask.java @@ -8,14 +8,18 @@ import dagger.android.HasAndroidInjector; import info.nightscout.androidaps.interfaces.ActivePluginProvider; import info.nightscout.androidaps.logging.AAPSLogger; import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.common.ManufacturerType; import info.nightscout.androidaps.plugins.pump.common.PumpPluginAbstract; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkCommunicationManager; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkTargetFrequency; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkError; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkServiceData; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.data.ServiceTransport; +import info.nightscout.androidaps.utils.Round; import info.nightscout.androidaps.utils.sharedPreferences.SP; /** @@ -62,29 +66,53 @@ public class InitializePumpManagerTask extends ServiceTask { lastGoodFrequency = rileyLinkServiceData.lastGoodFrequency; } - RileyLinkCommunicationManager rileyLinkCommunicationManager = ((PumpPluginAbstract) activePlugin.getActivePump()).getRileyLinkService().getDeviceCommunicationManager(); + // TODO Omnipod/Dagger needs refactoring + RileyLinkCommunicationManager rileyLinkCommunicationManager = ((RileyLinkPumpDevice) activePlugin.getActivePump()).getRileyLinkService().getDeviceCommunicationManager(); + + if (activePlugin.getActivePump().manufacturer() == ManufacturerType.Medtronic) { + + if ((lastGoodFrequency > 0.0d) + && rileyLinkCommunicationManager.isValidFrequency(lastGoodFrequency)) { + + rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.RileyLinkReady); + + aapsLogger.info(LTag.PUMPCOMM, "Setting radio frequency to {} MHz", lastGoodFrequency); + + rileyLinkCommunicationManager.setRadioFrequencyForPump(lastGoodFrequency); + + boolean foundThePump = rileyLinkCommunicationManager.tryToConnectToDevice(); + + if (foundThePump) { + rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.PumpConnectorReady); + } else { + rileyLinkServiceData.setServiceState(RileyLinkServiceState.PumpConnectorError, + RileyLinkError.NoContactWithDevice); + rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.IPC.MSG_PUMP_tunePump, context); + } + + } else { + rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.IPC.MSG_PUMP_tunePump, context); + } + } else { + + if (!Round.isSame(lastGoodFrequency, RileyLinkTargetFrequency.Omnipod.getScanFrequencies()[0])) { + lastGoodFrequency = RileyLinkTargetFrequency.Omnipod.getScanFrequencies()[0]; + lastGoodFrequency = Math.round(lastGoodFrequency * 1000d) / 1000d; + + rileyLinkServiceData.lastGoodFrequency = lastGoodFrequency; + } - if ((lastGoodFrequency > 0.0d) - && rileyLinkCommunicationManager.isValidFrequency(lastGoodFrequency)) { rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.RileyLinkReady); + rileyLinkUtil.setRileyLinkTargetFrequency(RileyLinkTargetFrequency.Omnipod); aapsLogger.info(LTag.PUMPCOMM, "Setting radio frequency to {} MHz", lastGoodFrequency); rileyLinkCommunicationManager.setRadioFrequencyForPump(lastGoodFrequency); - boolean foundThePump = rileyLinkCommunicationManager.tryToConnectToDevice(); + rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.PumpConnectorReady); - if (foundThePump) { - rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.PumpConnectorReady); - } else { - rileyLinkServiceData.setServiceState(RileyLinkServiceState.PumpConnectorError, - RileyLinkError.NoContactWithDevice); - rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.IPC.MSG_PUMP_tunePump, context); - } - - } else { - rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.IPC.MSG_PUMP_tunePump, context); } } + } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/ResetRileyLinkConfigurationTask.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/ResetRileyLinkConfigurationTask.java index fb738a241c..370015ab95 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/ResetRileyLinkConfigurationTask.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/ResetRileyLinkConfigurationTask.java @@ -4,10 +4,11 @@ import javax.inject.Inject; import dagger.android.HasAndroidInjector; import info.nightscout.androidaps.interfaces.ActivePluginProvider; -import info.nightscout.androidaps.interfaces.PumpInterface; import info.nightscout.androidaps.plugins.bus.RxBus; import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.common.ManufacturerType; import info.nightscout.androidaps.plugins.pump.common.PumpPluginAbstract; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.data.ServiceTransport; import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin; import info.nightscout.androidaps.plugins.pump.medtronic.events.EventRefreshButtonState; @@ -21,9 +22,6 @@ public class ResetRileyLinkConfigurationTask extends PumpTask { @Inject ActivePluginProvider activePlugin; @Inject RxBusWrapper rxBus; - private static final String TAG = "ResetRileyLinkTask"; - - public ResetRileyLinkConfigurationTask(HasAndroidInjector injector) { super(injector); } @@ -36,12 +34,16 @@ public class ResetRileyLinkConfigurationTask extends PumpTask { @Override public void run() { - PumpPluginAbstract pump = (PumpPluginAbstract) activePlugin.getActivePump(); + RileyLinkPumpDevice pumpAbstract = (RileyLinkPumpDevice)activePlugin.getActivePump(); + rxBus.send(new EventRefreshButtonState(false)); - MedtronicPumpPlugin.isBusy = true; - pump.resetRileyLinkConfiguration(); - MedtronicPumpPlugin.isBusy = false; + + pumpAbstract.setIsBusy(true); + pumpAbstract.resetRileyLinkConfiguration(); + pumpAbstract.setIsBusy(false); + rxBus.send(new EventRefreshButtonState(true)); } + } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/WakeAndTuneTask.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/WakeAndTuneTask.java index d7eed28ef4..219da8bf3f 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/WakeAndTuneTask.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/WakeAndTuneTask.java @@ -6,6 +6,7 @@ import dagger.android.HasAndroidInjector; import info.nightscout.androidaps.interfaces.ActivePluginProvider; import info.nightscout.androidaps.plugins.bus.RxBusWrapper; import info.nightscout.androidaps.plugins.pump.common.PumpPluginAbstract; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.data.ServiceTransport; import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin; import info.nightscout.androidaps.plugins.pump.medtronic.events.EventRefreshButtonState; @@ -33,11 +34,11 @@ public class WakeAndTuneTask extends PumpTask { @Override public void run() { - PumpPluginAbstract pump = (PumpPluginAbstract) activePlugin.getActivePump(); + RileyLinkPumpDevice pumpDevice = (RileyLinkPumpDevice)activePlugin.getActivePump(); rxBus.send(new EventRefreshButtonState(false)); - MedtronicPumpPlugin.isBusy = true; - pump.doTuneUpDevice(); - MedtronicPumpPlugin.isBusy = false; + pumpDevice.setIsBusy(true); + pumpDevice.doTuneUpDevice(); + pumpDevice.setIsBusy(false); rxBus.send(new EventRefreshButtonState(true)); } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicPumpPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicPumpPlugin.java index 4bb5609f33..9d4b89675e 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicPumpPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicPumpPlugin.java @@ -57,6 +57,7 @@ import info.nightscout.androidaps.plugins.pump.common.defs.PumpDriverState; import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkServiceData; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks.ResetRileyLinkConfigurationTask; @@ -95,7 +96,7 @@ import info.nightscout.androidaps.utils.sharedPreferences.SP; * @author Andy Rozman (andy.rozman@gmail.com) */ @Singleton -public class MedtronicPumpPlugin extends PumpPluginAbstract implements PumpInterface { +public class MedtronicPumpPlugin extends PumpPluginAbstract implements PumpInterface, RileyLinkPumpDevice { private final SP sp; private final RileyLinkUtil rileyLinkUtil; @@ -234,6 +235,11 @@ public class MedtronicPumpPlugin extends PumpPluginAbstract implements PumpInter rileyLinkMedtronicService.resetRileyLinkConfiguration(); } + @Override + public boolean hasTuneUp() { + return true; + } + @Override public void doTuneUpDevice() { rileyLinkMedtronicService.doTuneUpDevice(); } @@ -329,6 +335,11 @@ public class MedtronicPumpPlugin extends PumpPluginAbstract implements PumpInter } + @Override + public void setIsBusy(boolean isBusy_) { + isBusy = isBusy_; + } + @Override public boolean isBusy() { if (displayConnectionMessages) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicCommunicationManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicCommunicationManager.java index 348110c671..086dc92cd7 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicCommunicationManager.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicCommunicationManager.java @@ -92,6 +92,11 @@ public class MedtronicCommunicationManager extends RileyLinkCommunicationManager return (E) pumpMessage; } + @Override + public void setPumpDeviceState(PumpDeviceState pumpDeviceState) { + this.medtronicPumpStatus.setPumpDeviceState(pumpDeviceState); + } + public void setDoWakeUpBeforeCommand(boolean doWakeUp) { this.doWakeUpBeforeCommand = doWakeUp; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.java index 4d81eccc3a..496f2af06b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.java @@ -56,6 +56,7 @@ import info.nightscout.androidaps.plugins.treatments.Treatment; import info.nightscout.androidaps.plugins.treatments.TreatmentService; import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.Round; import info.nightscout.androidaps.utils.sharedPreferences.SP; @@ -659,7 +660,7 @@ public class MedtronicHistoryData { Treatment treatment = (Treatment) dbObjectBase; - if (RileyLinkUtil.isSame(treatment.insulin, 0d)) { + if (Round.isSame(treatment.insulin, 0d)) { removeList.add(dbObjectBase); } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/driver/MedtronicPumpStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/driver/MedtronicPumpStatus.java index 02f08e93d7..f7686fa824 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/driver/MedtronicPumpStatus.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/driver/MedtronicPumpStatus.java @@ -60,13 +60,12 @@ public class MedtronicPumpStatus extends PumpStatus { @Inject - public MedtronicPumpStatus( - ResourceHelper resourceHelper, + public MedtronicPumpStatus(ResourceHelper resourceHelper, SP sp, RxBusWrapper rxBus, RileyLinkUtil rileyLinkUtil ) { - super(); + super(PumpType.Medtronic_522_722); this.resourceHelper = resourceHelper; this.sp = sp; this.rxBus = rxBus; @@ -75,7 +74,7 @@ public class MedtronicPumpStatus extends PumpStatus { } - private void initSettings() { + public void initSettings() { this.activeProfileName = "STD"; this.reservoirRemainingUnits = 75d; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java index ac88f946aa..7c6a55d092 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java @@ -38,8 +38,6 @@ import info.nightscout.androidaps.utils.resources.ResourceHelper; */ public class RileyLinkMedtronicService extends RileyLinkService { - @Inject HasAndroidInjector injector; - @Inject ResourceHelper resourceHelper; @Inject MedtronicPumpPlugin medtronicPumpPlugin; @Inject MedtronicUtil medtronicUtil; @Inject MedtronicUIPostprocessor medtronicUIPostprocessor; @@ -120,6 +118,12 @@ public class RileyLinkMedtronicService extends RileyLinkService { } + @Override + public void setPumpDeviceState(PumpDeviceState pumpDeviceState) { + this.medtronicPumpStatus.setPumpDeviceState(pumpDeviceState); + } + + public MedtronicUIComm getMedtronicUIComm() { return medtronicUIComm; } @@ -236,7 +240,7 @@ public class RileyLinkMedtronicService extends RileyLinkService { } else { PumpType pumpType = medtronicPumpStatus.getMedtronicPumpMap().get(pumpTypePart); medtronicPumpStatus.medtronicDeviceType = medtronicPumpStatus.getMedtronicDeviceTypeMap().get(pumpTypePart); - medtronicPumpPlugin.getPumpDescription().setPumpDescription(pumpType); + medtronicPumpPlugin.setPumpType(pumpType); if (pumpTypePart.startsWith("7")) medtronicPumpStatus.reservoirFullUnits = 300; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodFragment.kt new file mode 100644 index 0000000000..fce29225d4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodFragment.kt @@ -0,0 +1,458 @@ +package info.nightscout.androidaps.plugins.pump.omnipod + +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.os.Handler +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import dagger.android.support.DaggerFragment +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.R +import info.nightscout.androidaps.events.EventPreferenceChange +import info.nightscout.androidaps.interfaces.ActivePluginProvider +import info.nightscout.androidaps.interfaces.CommandQueueProvider +import info.nightscout.androidaps.logging.AAPSLogger +import info.nightscout.androidaps.logging.LTag +import info.nightscout.androidaps.plugins.bus.RxBusWrapper +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkTargetDevice +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.dialog.RileyLinkStatusActivity +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodStatusRequest +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodDeviceState +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.PodManagementActivity +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodAcknowledgeAlertsChanged +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodDeviceStatusChange +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodRefreshButtonState +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil +import info.nightscout.androidaps.queue.Callback +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.FabricPrivacy +import info.nightscout.androidaps.utils.Round +import info.nightscout.androidaps.utils.T +import info.nightscout.androidaps.utils.WarnColors +import info.nightscout.androidaps.utils.alertDialogs.OKDialog +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.androidaps.utils.sharedPreferences.SP +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.omnipod_fragment.* +import javax.inject.Inject + +class OmnipodFragment : DaggerFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var mainApp: MainApp + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var resourceHelper: ResourceHelper + @Inject lateinit var rxBus: RxBusWrapper + @Inject lateinit var commandQueue: CommandQueueProvider + @Inject lateinit var activePlugin: ActivePluginProvider + @Inject lateinit var omnipodPumpPlugin: OmnipodPumpPlugin + @Inject lateinit var warnColors: WarnColors + @Inject lateinit var omnipodPumpStatus: OmnipodPumpStatus + @Inject lateinit var sp: SP + @Inject lateinit var omnipodUtil: OmnipodUtil + + private var disposable: CompositeDisposable = CompositeDisposable() + + private val loopHandler = Handler() + private lateinit var refreshLoop: Runnable + + operator fun CompositeDisposable.plusAssign(disposable: Disposable) { + add(disposable) + } + + init { + refreshLoop = Runnable { + activity?.runOnUiThread { updateGUI() } + loopHandler.postDelayed(refreshLoop, T.mins(1).msecs()) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.omnipod_fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + omnipod_rl_status.text = resourceHelper.gs(RileyLinkServiceState.NotStarted.getResourceId(RileyLinkTargetDevice.Omnipod)) + + omnipod_pod_status.setTextColor(Color.WHITE) + omnipod_pod_status.text = "{fa-bed}" + + omnipod_pod_mgmt.setOnClickListener { + if (omnipodPumpPlugin.rileyLinkService?.verifyConfiguration() == true) { + startActivity(Intent(context, PodManagementActivity::class.java)) + } else { + displayNotConfiguredDialog() + } + } + + omnipod_refresh.setOnClickListener { + if (omnipodPumpPlugin.rileyLinkService?.verifyConfiguration() != true) { + OmnipodUtil.displayNotConfiguredDialog(context) + } else { + omnipod_refresh.isEnabled = false + omnipodPumpPlugin.addPodStatusRequest(OmnipodStatusRequest.GetPodState); + commandQueue.readStatus("Clicked Refresh", object : Callback() { + override fun run() { + activity?.runOnUiThread { omnipod_refresh.isEnabled = true } + } + }) + } + } + + omnipod_stats.setOnClickListener { + if (omnipodPumpPlugin.rileyLinkService?.verifyConfiguration() == true) { + startActivity(Intent(context, RileyLinkStatusActivity::class.java)) + } else { + displayNotConfiguredDialog() + } + } + + omnipod_pod_active_alerts_ack.setOnClickListener { + if (omnipodPumpPlugin.rileyLinkService?.verifyConfiguration() != true) { + displayNotConfiguredDialog() + } else { + omnipod_pod_active_alerts_ack.isEnabled = false + omnipodPumpPlugin.addPodStatusRequest(OmnipodStatusRequest.AcknowledgeAlerts); + commandQueue.readStatus("Clicked Alert Ack", null) + } + } + + omnipod_pod_debug.setOnClickListener { + if (omnipodPumpPlugin.rileyLinkService?.verifyConfiguration() != true) { + displayNotConfiguredDialog() + } else { + omnipod_pod_debug.isEnabled = false + omnipodPumpPlugin.addPodStatusRequest(OmnipodStatusRequest.GetPodPulseLog); + commandQueue.readStatus("Clicked Refresh", object : Callback() { + override fun run() { + activity?.runOnUiThread { omnipod_pod_debug.isEnabled = true } + } + }) + } + } + + omnipod_lastconnection.setTextColor(Color.WHITE) + + setVisibilityOfPodDebugButton() + + updateGUI() + } + + override fun onResume() { + super.onResume() + loopHandler.postDelayed(refreshLoop, T.mins(1).msecs()) + disposable += rxBus + .toObservable(EventOmnipodRefreshButtonState::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ omnipod_refresh.isEnabled = it.newState }, { fabricPrivacy.logException(it) }) + disposable += rxBus + .toObservable(EventOmnipodDeviceStatusChange::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + aapsLogger.info(LTag.PUMP, "onStatusEvent(EventOmnipodDeviceStatusChange): {}", it) + setDeviceStatus() + }, { fabricPrivacy.logException(it) }) + disposable += rxBus + .toObservable(EventOmnipodPumpValuesChanged::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ updateGUI() }, { fabricPrivacy.logException(it) }) + disposable += rxBus + .toObservable(EventOmnipodAcknowledgeAlertsChanged::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ updateAcknowledgeAlerts() }, { fabricPrivacy.logException(it) }) + disposable += rxBus + .toObservable(EventPreferenceChange::class.java) + .observeOn(Schedulers.io()) + .subscribe({ event -> + setVisibilityOfPodDebugButton() + }, { fabricPrivacy.logException(it) }) + } + + fun setVisibilityOfPodDebugButton() { + val isEnabled = sp.getBoolean(OmnipodConst.Prefs.PodDebuggingOptionsEnabled, false) + + if (isEnabled) + omnipod_pod_debug.visibility = View.VISIBLE + else + omnipod_pod_debug.visibility = View.GONE + } + + private fun displayNotConfiguredDialog() { + context?.let { + OKDialog.show(it, resourceHelper.gs(R.string.combo_warning), + resourceHelper.gs(R.string.omnipod_error_operation_not_possible_no_configuration), null) + } + } + + override fun onPause() { + super.onPause() + disposable.clear() + loopHandler.removeCallbacks(refreshLoop) + } + + @Synchronized + private fun setDeviceStatus() { + //val omnipodPumpStatus: OmnipodPumpStatus = OmnipodUtil.getPumpStatus() + // omnipodPumpStatus.rileyLinkServiceState = checkStatusSet(omnipodPumpStatus.rileyLinkServiceState, + // RileyLinkUtil.getServiceState()) as RileyLinkServiceState? + + aapsLogger.info(LTag.PUMP, "setDeviceStatus: [pumpStatus={}]", omnipodPumpStatus) + + val resourceId = omnipodPumpStatus.rileyLinkServiceState.getResourceId(RileyLinkTargetDevice.Omnipod) + val rileyLinkError = omnipodPumpPlugin.rileyLinkService?.error + + omnipod_rl_status.text = + when { + omnipodPumpStatus.rileyLinkServiceState == RileyLinkServiceState.NotStarted -> resourceHelper.gs(resourceId) + omnipodPumpStatus.rileyLinkServiceState.isConnecting -> "{fa-bluetooth-b spin} " + resourceHelper.gs(resourceId) + omnipodPumpStatus.rileyLinkServiceState.isError && rileyLinkError == null -> "{fa-bluetooth-b} " + resourceHelper.gs(resourceId) + omnipodPumpStatus.rileyLinkServiceState.isError && rileyLinkError != null -> "{fa-bluetooth-b} " + resourceHelper.gs(rileyLinkError.getResourceId(RileyLinkTargetDevice.MedtronicPump)) + else -> "{fa-bluetooth-b} " + resourceHelper.gs(resourceId) + } + omnipod_rl_status.setTextColor(if (rileyLinkError != null) Color.RED else Color.WHITE) + + // omnipodPumpStatus.rileyLinkError = checkStatusSet(omnipodPumpStatus.rileyLinkError, + // RileyLinkUtil.getError()) as RileyLinkError? + + omnipod_errors.text = + omnipodPumpStatus.rileyLinkError?.let { + resourceHelper.gs(it.getResourceId(RileyLinkTargetDevice.Omnipod)) + } ?: "-" + + val driverState = omnipodUtil.getDriverState(); + + aapsLogger.info(LTag.PUMP, "getDriverState: [driverState={}]", driverState) + + if (driverState == OmnipodDriverState.NotInitalized) { + omnipod_pod_address.text = resourceHelper.gs(R.string.omnipod_pod_name_no_info) + omnipod_pod_expiry.text = "-" + omnipod_pod_status.text = resourceHelper.gs(R.string.omnipod_pod_not_initalized) + omnipodPumpStatus.podAvailable = false + omnipodPumpStatus.podNumber == null + } else if (driverState == OmnipodDriverState.Initalized_NoPod) { + omnipod_pod_address.text = resourceHelper.gs(R.string.omnipod_pod_name_no_info) + omnipod_pod_expiry.text = "-" + omnipod_pod_status.text = resourceHelper.gs(R.string.omnipod_pod_no_pod_connected) + omnipodPumpStatus.podAvailable = false + omnipodPumpStatus.podNumber == null + } else if (driverState == OmnipodDriverState.Initalized_PodInitializing) { + omnipod_pod_address.text = omnipodPumpStatus.podSessionState.address.toString() + omnipod_pod_expiry.text = "-" + omnipod_pod_status.text = omnipodPumpStatus.podSessionState.getSetupProgress().name + omnipodPumpStatus.podAvailable = false + omnipodPumpStatus.podNumber == omnipodPumpStatus.podSessionState.address.toString() + } else { + omnipodPumpStatus.podLotNumber = "" + omnipodPumpStatus.podSessionState.lot + omnipodPumpStatus.podAvailable = true + omnipod_pod_address.text = omnipodPumpStatus.podSessionState.address.toString() + omnipod_pod_expiry.text = omnipodPumpStatus.podSessionState.expiryDateAsString + omnipodPumpStatus.podNumber = omnipodPumpStatus.podSessionState.address.toString() + + //pumpStatus.podSessionState = checkStatusSet(pumpStatus.podSessionState, + // OmnipodUtil.getPodSessionState()) as PodSessionState? + + var podDeviceState = omnipodPumpStatus.podDeviceState + + when (podDeviceState) { + null, + PodDeviceState.Sleeping -> omnipod_pod_status.text = "{fa-bed} " // + pumpStatus.pumpDeviceState.name()); + PodDeviceState.NeverContacted, + PodDeviceState.WakingUp, + PodDeviceState.PumpUnreachable, + PodDeviceState.ErrorWhenCommunicating, + PodDeviceState.TimeoutWhenCommunicating, + PodDeviceState.InvalidConfiguration -> omnipod_pod_status.text = " " + resourceHelper.gs(podDeviceState.resourceId) + + PodDeviceState.Active -> { + + omnipod_pod_status.text = "Active"; +// val cmd = OmnipodUtil.getCurrentCommand() +// if (cmd == null) +// omnipod_pod_status.text = " " + resourceHelper.gs(pumpStatus.pumpDeviceState.resourceId) +// else { +// aapsLogger.debug(LTag.PUMP,"Command: " + cmd) +// val cmdResourceId = cmd.resourceId +// if (cmd == MedtronicCommandType.GetHistoryData) { +// omnipod_pod_status.text = OmnipodUtil.frameNumber?.let { +// resourceHelper.gs(cmdResourceId, OmnipodUtil.pageNumber, OmnipodUtil.frameNumber) +// } +// ?: resourceHelper.gs(R.string.medtronic_cmd_desc_get_history_request, OmnipodUtil.pageNumber) +// } else { +// omnipod_pod_status.text = " " + (cmdResourceId?.let { resourceHelper.gs(it) } +// ?: cmd.getCommandDescription()) +// } +// } + } + + else -> aapsLogger.warn(LTag.PUMP, "Unknown pump state: " + omnipodPumpStatus.podDeviceState) + } + + } + +// pumpStatus.pumpDeviceState = checkStatusSet(pumpStatus.pumpDeviceState, +// OmnipodUtil.getPumpDeviceState()) as PumpDeviceState? +// +// when (pumpStatus.pumpDeviceState) { +// null, +// PumpDeviceState.Sleeping -> omnipod_pod_status.text = "{fa-bed} " // + pumpStatus.pumpDeviceState.name()); +// PumpDeviceState.NeverContacted, +// PumpDeviceState.WakingUp, +// PumpDeviceState.PumpUnreachable, +// PumpDeviceState.ErrorWhenCommunicating, +// PumpDeviceState.TimeoutWhenCommunicating, +// PumpDeviceState.InvalidConfiguration -> omnipod_pod_status.text = " " + resourceHelper.gs(pumpStatus.pumpDeviceState.resourceId) +// PumpDeviceState.Active -> { +// val cmd = OmnipodUtil.getCurrentCommand() +// if (cmd == null) +// omnipod_pod_status.text = " " + resourceHelper.gs(pumpStatus.pumpDeviceState.resourceId) +// else { +// aapsLogger.debug(LTag.PUMP,"Command: " + cmd) +// val cmdResourceId = cmd.resourceId +// if (cmd == MedtronicCommandType.GetHistoryData) { +// omnipod_pod_status.text = OmnipodUtil.frameNumber?.let { +// resourceHelper.gs(cmdResourceId, OmnipodUtil.pageNumber, OmnipodUtil.frameNumber) +// } +// ?: resourceHelper.gs(R.string.medtronic_cmd_desc_get_history_request, OmnipodUtil.pageNumber) +// } else { +// omnipod_pod_status.text = " " + (cmdResourceId?.let { resourceHelper.gs(it) } +// ?: cmd.getCommandDescription()) +// } +// } +// } +// else -> aapsLogger.warn(LTag.PUMP,"Unknown pump state: " + pumpStatus.pumpDeviceState) +// } + + val status = commandQueue.spannedStatus() + if (status.toString() == "") { + omnipod_queue.visibility = View.GONE + } else { + omnipod_queue.visibility = View.VISIBLE + omnipod_queue.text = status + } + } + + private fun checkStatusSet(object1: Any?, object2: Any?): Any? { + return if (object1 == null) { + object2 + } else { + if (object1 != object2) { + object2 + } else + object1 + } + } + + // GUI functions + fun updateGUI() { + val plugin = omnipodPumpPlugin + //val omnipodPumpStatus = OmnipodUtil.getPumpStatus() + var pumpType = omnipodPumpStatus.pumpType + + if (pumpType == null) { + aapsLogger.warn(LTag.PUMP, "PumpType was not set, reseting to Omnipod.") + pumpType = PumpType.Insulet_Omnipod; + } + + setDeviceStatus() + + if (omnipodPumpStatus.podAvailable) { + // last connection + if (omnipodPumpStatus.lastConnection != 0L) { + //val minAgo = DateUtil.minAgo(pumpStatus.lastConnection) + val min = (System.currentTimeMillis() - omnipodPumpStatus.lastConnection) / 1000 / 60 + if (omnipodPumpStatus.lastConnection + 60 * 1000 > System.currentTimeMillis()) { + omnipod_lastconnection.setText(R.string.combo_pump_connected_now) + //omnipod_lastconnection.setTextColor(Color.WHITE) + } else { //if (pumpStatus.lastConnection + 30 * 60 * 1000 < System.currentTimeMillis()) { + + if (min < 60) { + omnipod_lastconnection.text = resourceHelper.gs(R.string.minago, min) + } else if (min < 1440) { + val h = (min / 60).toInt() + omnipod_lastconnection.text = (resourceHelper.gq(R.plurals.objective_hours, h, h) + " " + + resourceHelper.gs(R.string.ago)) + } else { + val h = (min / 60).toInt() + val d = h / 24 + // h = h - (d * 24); + omnipod_lastconnection.text = (resourceHelper.gq(R.plurals.objective_days, d, d) + " " + + resourceHelper.gs(R.string.ago)) + } + //omnipod_lastconnection.setTextColor(Color.RED) + } +// } else { +// omnipod_lastconnection.text = minAgo +// //omnipod_lastconnection.setTextColor(Color.WHITE) +// } + } + + // last bolus + val bolus = omnipodPumpStatus.lastBolusAmount + val bolusTime = omnipodPumpStatus.lastBolusTime + if (bolus != null && bolusTime != null && omnipodPumpStatus.podAvailable) { + val agoMsc = System.currentTimeMillis() - omnipodPumpStatus.lastBolusTime.time + val bolusMinAgo = agoMsc.toDouble() / 60.0 / 1000.0 + val unit = resourceHelper.gs(R.string.insulin_unit_shortname) + val ago: String + if (agoMsc < 60 * 1000) { + ago = resourceHelper.gs(R.string.combo_pump_connected_now) + } else if (bolusMinAgo < 60) { + ago = DateUtil.minAgo(resourceHelper, omnipodPumpStatus.lastBolusTime.time) + } else { + ago = DateUtil.hourAgo(omnipodPumpStatus.lastBolusTime.time, resourceHelper) + } + omnipod_lastbolus.text = resourceHelper.gs(R.string.omnipod_last_bolus, pumpType.determineCorrectBolusSize(bolus), unit, ago) + } else { + omnipod_lastbolus.text = "" + } + + // base basal rate + omnipod_basabasalrate.text = resourceHelper.gs(R.string.pump_basebasalrate, pumpType.determineCorrectBasalSize(plugin.baseBasalRate)) + + omnipod_tempbasal.text = activePlugin.activeTreatments + .getTempBasalFromHistory(System.currentTimeMillis())?.toStringFull() ?: "" + + // reservoir + if (Round.isSame(omnipodPumpStatus.reservoirRemainingUnits, 75.0)) { + omnipod_reservoir.text = resourceHelper.gs(R.string.omnipod_reservoir_over50) + } else { + omnipod_reservoir.text = resourceHelper.gs(R.string.omnipod_reservoir_left, omnipodPumpStatus.reservoirRemainingUnits) + } + warnColors.setColorInverse(omnipod_reservoir, omnipodPumpStatus.reservoirRemainingUnits, 50.0, 20.0) + + } else { + omnipod_basabasalrate.text = "" + omnipod_reservoir.text = "" + omnipod_tempbasal.text = "" + omnipod_lastbolus.text = "" + omnipod_lastconnection.text = "" + omnipod_lastconnection.setTextColor(Color.WHITE) + } + + omnipod_errors.text = omnipodPumpStatus.errorInfo + + updateAcknowledgeAlerts() + + omnipod_refresh.isEnabled = omnipodPumpStatus.podAvailable + + } + + private fun updateAcknowledgeAlerts() { + omnipod_pod_active_alerts_ack.isEnabled = omnipodPumpStatus.ackAlertsAvailable + omnipod_pod_active_alerts.text = omnipodPumpStatus.ackAlertsText + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodPumpPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodPumpPlugin.java new file mode 100644 index 0000000000..62adc49855 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodPumpPlugin.java @@ -0,0 +1,1034 @@ +package info.nightscout.androidaps.plugins.pump.omnipod; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.SystemClock; + +import androidx.annotation.NonNull; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.activities.ErrorHelperActivity; +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.db.Source; +import info.nightscout.androidaps.db.TemporaryBasal; +import info.nightscout.androidaps.events.EventPreferenceChange; +import info.nightscout.androidaps.events.EventRefreshOverview; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; +import info.nightscout.androidaps.interfaces.CommandQueueProvider; +import info.nightscout.androidaps.interfaces.PluginDescription; +import info.nightscout.androidaps.interfaces.PluginType; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.general.actions.defs.CustomAction; +import info.nightscout.androidaps.plugins.general.actions.defs.CustomActionType; +import info.nightscout.androidaps.plugins.general.overview.events.EventDismissNotification; +import info.nightscout.androidaps.plugins.general.overview.events.EventNewNotification; +import info.nightscout.androidaps.plugins.general.overview.notifications.Notification; +import info.nightscout.androidaps.plugins.pump.common.PumpPluginAbstract; +import info.nightscout.androidaps.plugins.pump.common.data.PumpStatus; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpDriverState; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkServiceData; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks.ResetRileyLinkConfigurationTask; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks.ServiceTaskExecutor; +import info.nightscout.androidaps.plugins.pump.common.utils.DateTimeUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoRecentPulseLog; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommandType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommunicationManagerInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCustomActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodPumpPluginInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodStatusRequest; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.ui.OmnipodUIComm; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.ui.OmnipodUITask; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodRefreshButtonState; +import info.nightscout.androidaps.plugins.pump.omnipod.service.RileyLinkOmnipodService; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; +import info.nightscout.androidaps.utils.FabricPrivacy; +import info.nightscout.androidaps.utils.Round; +import info.nightscout.androidaps.utils.TimeChangeType; +import info.nightscout.androidaps.utils.resources.ResourceHelper; +import info.nightscout.androidaps.utils.sharedPreferences.SP; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Created by andy on 23.04.18. + * + * @author Andy Rozman (andy.rozman@gmail.com) + */ +@Singleton +public class OmnipodPumpPlugin extends PumpPluginAbstract implements OmnipodPumpPluginInterface, RileyLinkPumpDevice { + + // TODO Dagger (maybe done) + private static OmnipodPumpPlugin plugin = null; + private RileyLinkServiceData rileyLinkServiceData; + private ServiceTaskExecutor serviceTaskExecutor; + private RileyLinkOmnipodService rileyLinkOmnipodService; + private OmnipodUtil omnipodUtil; + protected OmnipodPumpStatus omnipodPumpStatus = null; + protected OmnipodUIComm omnipodUIComm; + + private CompositeDisposable disposable = new CompositeDisposable(); + + + // variables for handling statuses and history + protected boolean firstRun = true; + protected boolean isRefresh = false; + private boolean isBasalProfileInvalid = false; + private boolean basalProfileChanged = false; + private boolean isInitialized = false; + protected OmnipodCommunicationManagerInterface omnipodCommunicationManager; + + public static boolean isBusy = false; + protected List busyTimestamps = new ArrayList<>(); + protected boolean sentIdToFirebase = false; + protected boolean hasTimeDateOrTimeZoneChanged = false; + private int timeChangeRetries = 0; + + private Profile currentProfile; + + boolean omnipodServiceRunning = false; + + private long nextPodCheck = 0L; + //OmnipodDriverState driverState = OmnipodDriverState.NotInitalized; + + @Inject + public OmnipodPumpPlugin( + HasAndroidInjector injector, + AAPSLogger aapsLogger, + RxBusWrapper rxBus, + Context context, + ResourceHelper resourceHelper, + ActivePluginProvider activePlugin, + SP sp, + OmnipodUtil omnipodUtil, + OmnipodPumpStatus omnipodPumpStatus, + CommandQueueProvider commandQueue, + FabricPrivacy fabricPrivacy, + RileyLinkServiceData rileyLinkServiceData, + ServiceTaskExecutor serviceTaskExecutor) { + + super(new PluginDescription() // + .mainType(PluginType.PUMP) // + .fragmentClass(OmnipodFragment.class.getName()) // + .pluginName(R.string.omnipod_name) // + .shortName(R.string.omnipod_name_short) // + .preferencesId(R.xml.pref_omnipod) // + .description(R.string.description_pump_omnipod), // + PumpType.Insulet_Omnipod, + injector, resourceHelper, aapsLogger, commandQueue, rxBus, activePlugin, sp, context, fabricPrivacy + ); + this.rileyLinkServiceData = rileyLinkServiceData; + this.serviceTaskExecutor = serviceTaskExecutor; + + displayConnectionMessages = false; + OmnipodPumpPlugin.plugin = this; + this.omnipodUtil = omnipodUtil; + this.omnipodPumpStatus = omnipodPumpStatus; + + //OmnipodUtil.setDriverState(); + +// TODO loop +// if (OmnipodUtil.isOmnipodEros()) { +// OmnipodUtil.setPlugin(this); +// OmnipodUtil.setOmnipodPodType(OmnipodPodType.Eros); +// OmnipodUtil.setPumpType(PumpType.Insulet_Omnipod); +// } + +// // TODO ccc + + + serviceConnection = new ServiceConnection() { + + @Override + public void onServiceDisconnected(ComponentName name) { + + aapsLogger.debug(LTag.PUMP, "RileyLinkOmnipodService is disconnected"); + rileyLinkOmnipodService = null; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + + aapsLogger.debug(LTag.PUMP, "RileyLinkOmnipodService is connected"); + RileyLinkOmnipodService.LocalBinder mLocalBinder = (RileyLinkOmnipodService.LocalBinder) service; + rileyLinkOmnipodService = mLocalBinder.getServiceInstance(); + + new Thread(() -> { + + for (int i = 0; i < 20; i++) { + SystemClock.sleep(5000); + + aapsLogger.debug(LTag.PUMP, "Starting Omnipod-RileyLink service"); + if (rileyLinkOmnipodService.setNotInPreInit()) { + break; + } + } + + +// if (OmnipodPumpPlugin.this.omnipodPumpStatus != null) { +// +// aapsLogger.debug(LTag.PUMP, "Starting OmniPod-RileyLink service"); +// if (omnipodService.setNotInPreInit()) { +// if (omnipodCommunicationManager == null) { +// omnipodCommunicationManager = AapsOmnipodManager.getInstance(); +// omnipodCommunicationManager.setPumpStatus(OmnipodPumpPlugin.this.omnipodPumpStatus); +// omnipodServiceRunning = true; +// } else { +// omnipodCommunicationManager.setPumpStatus(OmnipodPumpPlugin.this.omnipodPumpStatus); +// } +// +// omnipodUtil.setOmnipodPodType(OmnipodPodType.Eros); +// //omnipodUtil.setPlugin(OmnipodPumpPlugin.this); +// +// omnipodUIComm = new OmnipodUIComm(omnipodCommunicationManager, plugin, OmnipodPumpPlugin.this.omnipodPumpStatus); +// break; +// } +// } +// +// SystemClock.sleep(5000); + //} + }).start(); + } + }; + } + + protected OmnipodPumpPlugin(PluginDescription pluginDescription, PumpType pumpType, + HasAndroidInjector injector, + AAPSLogger aapsLogger, + RxBusWrapper rxBus, + Context context, + ResourceHelper resourceHelper, + ActivePluginProvider activePlugin, + info.nightscout.androidaps.utils.sharedPreferences.SP sp, + CommandQueueProvider commandQueue, + FabricPrivacy fabricPrivacy) { + super(pluginDescription, pumpType, injector, resourceHelper, aapsLogger, commandQueue, rxBus, activePlugin, sp, context, fabricPrivacy); + +// this.rileyLinkUtil = rileyLinkUtil; +// this.medtronicUtil = medtronicUtil; +// this.sp = sp; +// this.medtronicPumpStatus = medtronicPumpStatus; +// this.medtronicHistoryData = medtronicHistoryData; +// this.rileyLinkServiceData = rileyLinkServiceData; +// this.serviceTaskExecutor = serviceTaskExecutor; + + } + + @Deprecated + public static OmnipodPumpPlugin getPlugin() { + if (plugin == null) + throw new IllegalStateException("Plugin not injected jet"); + return plugin; + } + + + @Override + protected void onStart() { + super.onStart(); + disposable.add(rxBus + .toObservable(EventPreferenceChange.class) + .observeOn(Schedulers.io()) + .subscribe(event -> { + if ((event.isChanged(getResourceHelper(), R.string.key_omnipod_beep_basal_enabled)) || + (event.isChanged(getResourceHelper(), R.string.key_omnipod_beep_bolus_enabled)) || + (event.isChanged(getResourceHelper(), R.string.key_omnipod_beep_tbr_enabled)) || + (event.isChanged(getResourceHelper(), R.string.key_omnipod_pod_debugging_options_enabled)) || + (event.isChanged(getResourceHelper(), R.string.key_omnipod_beep_smb_enabled)) || + (event.isChanged(getResourceHelper(), R.string.key_omnipod_timechange_enabled))) + rileyLinkOmnipodService.verifyConfiguration(); + }, fabricPrivacy::logException) + ); + //rileyLinkOmnipodService.verifyConfiguration(); + //initPumpStatusData(); + } + +// @Override +// protected void onResume() { +// +// } + +// private void refreshConfiguration() { +// if (pumpStatusLocal != null) { +// pumpStatusLocal.refreshConfiguration(); +// } +// verifyConfiguration() +// } + + @Override + protected void onStop() { + disposable.clear(); + super.onStop(); + } + + private String getLogPrefix() { + return "OmnipodPlugin::"; + } + + + @Override + public void initPumpStatusData() { + + omnipodPumpStatus.lastConnection = sp.getLong(RileyLinkConst.Prefs.LastGoodDeviceCommunicationTime, 0L); + omnipodPumpStatus.lastDataTime = omnipodPumpStatus.lastConnection; + omnipodPumpStatus.previousConnection = omnipodPumpStatus.lastConnection; + + if (rileyLinkOmnipodService != null) rileyLinkOmnipodService.verifyConfiguration(); + + aapsLogger.debug(LTag.PUMP, "initPumpStatusData: " + this.omnipodPumpStatus); + + // set first Omnipod Pump Start + if (!sp.contains(OmnipodConst.Statistics.FirstPumpStart)) { + sp.putLong(OmnipodConst.Statistics.FirstPumpStart, System.currentTimeMillis()); + } + + } + + + @Override + public void onStartCustomActions() { + + // check status every minute (if any status needs refresh we send readStatus command) + new Thread(() -> { + + do { + SystemClock.sleep(60000); + + if (this.isInitialized) { + clearBusyQueue(); + } + + if (!this.omnipodStatusRequestList.isEmpty() || this.hasTimeDateOrTimeZoneChanged) { + if (!getCommandQueue().statusInQueue()) { + getCommandQueue().readStatus("Status Refresh Requested", null); + } + } + + doPodCheck(); + + } while (serviceRunning); + + }).start(); + } + + private void doPodCheck() { + if (System.currentTimeMillis() > this.nextPodCheck) { + if (omnipodUtil.getDriverState() == OmnipodDriverState.Initalized_NoPod) { + Notification notification = new Notification(Notification.OMNIPOD_POD_NOT_ATTACHED, resourceHelper.gs(R.string.omnipod_error_pod_not_attached), Notification.NORMAL); + rxBus.send(new EventNewNotification(notification)); + } else { + rxBus.send(new EventDismissNotification(Notification.OMNIPOD_POD_NOT_ATTACHED)); + } + + this.nextPodCheck = DateTimeUtil.getTimeInFutureFromMinutes(15); + } + } + + + @Override + public Class getServiceClass() { + return RileyLinkOmnipodService.class; + } + + @Override + public PumpStatus getPumpStatusData() { + return this.omnipodPumpStatus; + } + + + @Override + public String deviceID() { + return "Omnipod"; + } + + + // Pump Plugin + + private boolean isServiceSet() { + return rileyLinkOmnipodService != null; + } + + + @Override + public boolean isInitialized() { + if (displayConnectionMessages) + aapsLogger.debug(LTag.PUMP, getLogPrefix() + "isInitialized"); + return isServiceSet() && isInitialized; + } + + + @Override + public boolean isBusy() { + if (displayConnectionMessages) + aapsLogger.debug(LTag.PUMP, getLogPrefix() + "isBusy"); + + if (isServiceSet()) { + + if (isBusy || !omnipodPumpStatus.podAvailable) + return true; + + if (busyTimestamps.size() > 0) { + + clearBusyQueue(); + + return (busyTimestamps.size() > 0); + } + } + + return false; + } + + + @Override + public void resetRileyLinkConfiguration() { + rileyLinkOmnipodService.resetRileyLinkConfiguration(); + } + + + @Override + public boolean hasTuneUp() { + return false; + } + + + @Override + public void doTuneUpDevice() { + rileyLinkOmnipodService.doTuneUpDevice(); + } + + + @Override + public RileyLinkOmnipodService getRileyLinkService() { + return rileyLinkOmnipodService; + } + + + private synchronized void clearBusyQueue() { + + if (busyTimestamps.size() == 0) { + return; + } + + Set deleteFromQueue = new HashSet<>(); + + for (Long busyTimestamp : busyTimestamps) { + + if (System.currentTimeMillis() > busyTimestamp) { + deleteFromQueue.add(busyTimestamp); + } + } + + if (deleteFromQueue.size() == busyTimestamps.size()) { + busyTimestamps.clear(); + //setEnableCustomAction(MedtronicCustomActionType.ClearBolusBlock, false); + } + + if (deleteFromQueue.size() > 0) { + busyTimestamps.removeAll(deleteFromQueue); + } + + } + + + @Override + public boolean isConnected() { + if (displayConnectionMessages) + aapsLogger.debug(LTag.PUMP, getLogPrefix() + "isConnected"); + return isServiceSet() && rileyLinkOmnipodService.isInitialized(); + } + + + @Override + public boolean isConnecting() { + if (displayConnectionMessages) + aapsLogger.debug(LTag.PUMP, getLogPrefix() + "isConnecting"); + return !isServiceSet() || !rileyLinkOmnipodService.isInitialized(); + } + + + @Override + public boolean isSuspended() { + + return (omnipodUtil.getDriverState() == OmnipodDriverState.Initalized_NoPod) || + (omnipodUtil.getPodSessionState() != null && omnipodUtil.getPodSessionState().isSuspended()); + +// return (pumpStatusLocal != null && !pumpStatusLocal.podAvailable) || +// (OmnipodUtil.getPodSessionState() != null && OmnipodUtil.getPodSessionState().isSuspended()); +// +// TODO ddd +// return (OmnipodUtil.getDriverState() == OmnipodDriverState.Initalized_NoPod) || +// (OmnipodUtil.getPodSessionState() != null && OmnipodUtil.getPodSessionState().isSuspended()); +// +// return (pumpStatusLocal != null && !pumpStatusLocal.podAvailable) || +// (OmnipodUtil.getPodSessionState() != null && OmnipodUtil.getPodSessionState().isSuspended()); + } + + @Override + public void getPumpStatus() { + + if (firstRun) { + initializePump(!isRefresh); + triggerUIChange(); + } else if (!omnipodStatusRequestList.isEmpty()) { + + List removeList = new ArrayList<>(); + + for (OmnipodStatusRequest omnipodStatusRequest : omnipodStatusRequestList) { + if (omnipodStatusRequest == OmnipodStatusRequest.GetPodPulseLog) { + OmnipodUITask omnipodUITask = omnipodUIComm.executeCommand(omnipodStatusRequest.getCommandType()); + + PodInfoRecentPulseLog result = (PodInfoRecentPulseLog) omnipodUITask.returnDataObject; + + if (result == null) { + aapsLogger.warn(LTag.PUMP, "Result was null."); + } else { + aapsLogger.warn(LTag.PUMP, "Result was NOT null."); + + Intent i = new Intent(MainApp.instance(), ErrorHelperActivity.class); + i.putExtra("soundid", 0); + i.putExtra("status", "Pulse Log (copied to clipboard):\n" + result.toString()); + i.putExtra("title", resourceHelper.gs(R.string.combo_warning)); + i.putExtra("clipboardContent", result.toString()); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + MainApp.instance().startActivity(i); + +// OKDialog.show(MainApp.instance().getApplicationContext(), MainApp.gs(R.string.action), +// "Pulse Log:\n" + result.toString(), null); + } + + } else { + omnipodUIComm.executeCommand(omnipodStatusRequest.getCommandType()); + } + removeList.add(omnipodStatusRequest); + } + + omnipodStatusRequestList.removeAll(removeList); + + } else if (this.hasTimeDateOrTimeZoneChanged) { + OmnipodUITask omnipodUITask = omnipodUIComm.executeCommand(OmnipodCommandType.SetTime); + + if (omnipodUITask.wasCommandSuccessful()) { + this.hasTimeDateOrTimeZoneChanged = false; + timeChangeRetries = 0; + + Notification notification = new Notification( + Notification.TIME_OR_TIMEZONE_CHANGE, + resourceHelper.gs(R.string.time_or_timezone_change), + Notification.INFO, 60); + rxBus.send(new EventNewNotification(notification)); + + } else { + timeChangeRetries++; + + if (timeChangeRetries > 3) { + this.hasTimeDateOrTimeZoneChanged = false; + timeChangeRetries = 0; + } + } + } + } + + + public void setIsBusy(boolean isBusy_) { + isBusy = isBusy_; + } + + + private void getPodPumpStatus() { + // TODO read pod status + aapsLogger.error(LTag.PUMP, "getPodPumpStatus() NOT IMPLEMENTED"); + + //addPodStatusRequest(OmnipodStatusRequest.GetPodState); + + //getPodPumpStatusObject().driverState = OmnipodDriverState.Initalized_PodAvailable; + //driverState = OmnipodDriverState.Initalized_PodAvailable; + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_PodAttached); + // we would probably need to read Basal Profile here too + } + + + List omnipodStatusRequestList = new ArrayList<>(); + + public void addPodStatusRequest(OmnipodStatusRequest pumpStatusRequest) { + if (pumpStatusRequest == OmnipodStatusRequest.ResetState) { + resetStatusState(); + } else { + omnipodStatusRequestList.add(pumpStatusRequest); + } + } + + @Override + public void setDriverState(OmnipodDriverState state) { + //this.driverState = state; + } + + + public void resetStatusState() { + firstRun = true; + isRefresh = true; + } + + + private void setRefreshButtonEnabled(boolean enabled) { + rxBus.send(new EventOmnipodRefreshButtonState(enabled)); + } + + + private void initializePump(boolean realInit) { + + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "initializePump - start"); + + // TODO ccc + //OmnipodPumpStatus podPumpStatus = getPodPumpStatusObject(); + + setRefreshButtonEnabled(false); + + PodSessionState podSessionState = null; + + if (omnipodUtil.getPodSessionState() != null) { + podSessionState = omnipodUtil.getPodSessionState(); + } else { + String podState = sp.getString(OmnipodConst.Prefs.PodState, null); + + aapsLogger.info(LTag.PUMP, "PodSessionState-SP: loaded from SharedPreferences: " + podState); + + if (podState != null) { + podSessionState = omnipodUtil.getGsonInstance().fromJson(podState, PodSessionState.class); + podSessionState.injectDaggerClass(injector); + omnipodUtil.setPodSessionState(podSessionState); + } + } + + + if (podSessionState != null) { + aapsLogger.debug(LTag.PUMP, "PodSessionState (saved): " + podSessionState); + + if (!isRefresh) { + pumpState = PumpDriverState.Initialized; + } + + // TODO handle if session state too old + getPodPumpStatus(); + + } else { + aapsLogger.debug(LTag.PUMP, "No PodSessionState found. Pod probably not running."); + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_NoPod); + } + + finishAction("Omnipod Pump"); + +// if (!sentIdToFirebase) { +// Bundle params = new Bundle(); +// params.putString("version", BuildConfig.VERSION); +// MainApp.getFirebaseAnalytics().logEvent("OmnipodPumpInit", params); +// +// sentIdToFirebase = true; +// } + + isInitialized = true; + + this.firstRun = false; + } + + + @Override + public boolean isThisProfileSet(Profile profile) { + + // TODO status was not yet read from pod + // TODO maybe not possible, need to see how we will handle that + if (currentProfile == null) { + this.currentProfile = profile; + return true; + } + + return (currentProfile.areProfileBasalPatternsSame(profile)); + } + + + @Override + public long lastDataTime() { + if (omnipodPumpStatus.lastConnection != 0) { + return omnipodPumpStatus.lastConnection; + } + + return System.currentTimeMillis(); + } + + + @Override + public double getBaseBasalRate() { + + if (currentProfile != null) { + int hour = (new GregorianCalendar()).get(Calendar.HOUR_OF_DAY); + return currentProfile.getBasalTimeFromMidnight(DateTimeUtil.getTimeInS(hour * 60)); + } else { + return 0.0d; + } + } + + + @Override + public double getReservoirLevel() { + return omnipodPumpStatus.reservoirRemainingUnits; + } + + + @Override + public int getBatteryLevel() { + return 75; + } + + + @Override + protected void triggerUIChange() { + rxBus.send(new EventOmnipodPumpValuesChanged()); + } + + + @Override + public boolean isFakingTempsByExtendedBoluses() { + return false; + } + + + @Override + @NonNull + protected PumpEnactResult deliverBolus(final DetailedBolusInfo detailedBolusInfo) { + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "deliverBolus - {}", detailedBolusInfo); + + setRefreshButtonEnabled(false); + + try { + + OmnipodUITask responseTask = omnipodUIComm.executeCommand(OmnipodCommandType.SetBolus, + detailedBolusInfo); + + PumpEnactResult result = responseTask.getResult(); + + setRefreshButtonEnabled(true); + + if (result.success) { + + // we subtract insulin, exact amount will be visible with next remainingInsulin update. +// if (getPodPumpStatusObject().reservoirRemainingUnits != 0 && +// getPodPumpStatusObject().reservoirRemainingUnits != 75 ) { +// getPodPumpStatusObject().reservoirRemainingUnits -= detailedBolusInfo.insulin; +// } + + incrementStatistics(detailedBolusInfo.isSMB ? OmnipodConst.Statistics.SMBBoluses + : OmnipodConst.Statistics.StandardBoluses); + + result.carbsDelivered(detailedBolusInfo.carbs); + } + + return result; + } finally { + finishAction("Bolus"); + } + } + + @Override + public void stopBolusDelivering() { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "stopBolusDelivering"); + + setRefreshButtonEnabled(false); + + OmnipodUITask responseTask = omnipodUIComm.executeCommand(OmnipodCommandType.CancelBolus); + + PumpEnactResult result = responseTask.getResult(); + + //setRefreshButtonEnabled(true); + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "stopBolusDelivering - wasSuccess={}", result.success); + + //finishAction("Bolus"); + } + + + private void incrementStatistics(String statsKey) { + long currentCount = sp.getLong(statsKey, 0L); + currentCount++; + sp.putLong(statsKey, currentCount); + } + + + // if enforceNew===true current temp basal is canceled and new TBR set (duration is prolonged), + // if false and the same rate is requested enacted=false and success=true is returned and TBR is not changed + @Override + public PumpEnactResult setTempBasalAbsolute(Double absoluteRate, Integer durationInMinutes, Profile profile, + boolean enforceNew) { + + setRefreshButtonEnabled(false); + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "setTempBasalAbsolute: rate: {}, duration={}", absoluteRate, durationInMinutes); + + // read current TBR + TempBasalPair tbrCurrent = readTBR(); + + if (tbrCurrent != null) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "setTempBasalAbsolute: Current Basal: duration: {} min, rate={}", + tbrCurrent.getDurationMinutes(), tbrCurrent.getInsulinRate()); + } + + if (tbrCurrent != null && !enforceNew) { + if (Round.isSame(tbrCurrent.getInsulinRate(), absoluteRate)) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "setTempBasalAbsolute - No enforceNew and same rate. Exiting."); + finishAction("TBR"); + return new PumpEnactResult(getInjector()).success(true).enacted(false); + } + } + + // if TBR is running we will cancel it. +// if (tbrCurrent != null) { +// +// aapsLogger.info(LTag.PUMP,getLogPrefix() + "setTempBasalAbsolute - TBR running - so canceling it."); +// +// // CANCEL +// OmnipodUITask responseTask2 = omnipodUIComm.executeCommand(OmnipodCommandType.CancelTemporaryBasal); +// +// PumpEnactResult result = responseTask2.getResult(); +// +// if (result.success) { +// +// aapsLogger.info(LTag.PUMP,getLogPrefix() + "setTempBasalAbsolute - Current TBR cancelled."); +// } else { +// +// aapsLogger.error(LTag.PUMP,getLogPrefix() + "setTempBasalAbsolute - Cancel TBR failed."); +// +// finishAction("TBR"); +// +// return result; +// } +// } + + // now start new TBR + OmnipodUITask responseTask = omnipodUIComm.executeCommand(OmnipodCommandType.SetTemporaryBasal, + absoluteRate, durationInMinutes); + + PumpEnactResult result = responseTask.getResult(); + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "setTempBasalAbsolute - setTBR. Response: " + result.success); + + if (result.success) { + incrementStatistics(OmnipodConst.Statistics.TBRsSet); + } + + finishAction("TBR"); + return result; + } + + protected TempBasalPair readTBR() { + // TODO we can do it like this or read status from pod ?? + if (omnipodPumpStatus.tempBasalEnd < System.currentTimeMillis()) { + // TBR done + omnipodPumpStatus.clearTemporaryBasal(); + + return null; + } + + return omnipodPumpStatus.getTemporaryBasal(); + } + + + protected void finishAction(String overviewKey) { + if (overviewKey != null) + rxBus.send(new EventRefreshOverview(overviewKey, false)); + + triggerUIChange(); + + setRefreshButtonEnabled(true); + } + + + @Override + public PumpEnactResult cancelTempBasal(boolean enforceNew) { + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "cancelTempBasal - started"); + + setRefreshButtonEnabled(false); + + TempBasalPair tbrCurrent = readTBR(); + + if (tbrCurrent == null) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "cancelTempBasal - TBR already canceled."); + finishAction("TBR"); + return new PumpEnactResult(getInjector()).success(true).enacted(false); + } + + OmnipodUITask responseTask2 = omnipodUIComm.executeCommand(OmnipodCommandType.CancelTemporaryBasal); + + PumpEnactResult result = responseTask2.getResult(); + + finishAction("TBR"); + + if (result.success) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "cancelTempBasal - Cancel TBR successful."); + + TemporaryBasal tempBasal = new TemporaryBasal() // + .date(System.currentTimeMillis()) // + .duration(0) // + .source(Source.USER); + + activePlugin.getActiveTreatments().addToHistoryTempBasal(tempBasal); + } else { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "cancelTempBasal - Cancel TBR failed."); + } + + return result; + } + + @NotNull + @Override + public String serialNumber() { + return StringUtils.isNotBlank(omnipodPumpStatus.podNumber) ? + omnipodPumpStatus.podNumber : "None"; + } + + @NotNull + @Override + public PumpEnactResult setNewBasalProfile(Profile profile) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "setNewBasalProfile"); + + // this shouldn't be needed, but let's do check if profile setting we are setting is same as current one + if (this.currentProfile != null && this.currentProfile.areProfileBasalPatternsSame(profile)) { + return new PumpEnactResult(getInjector()) // + .success(true) // + .enacted(false) // + .comment(resourceHelper.gs(R.string.medtronic_cmd_basal_profile_not_set_is_same)); + } + + setRefreshButtonEnabled(false); + + OmnipodUITask responseTask = omnipodUIComm.executeCommand(OmnipodCommandType.SetBasalProfile, + profile); + + PumpEnactResult result = responseTask.getResult(); + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "Basal Profile was set: " + result.success); + + if (result.success) { + this.currentProfile = profile; + + Notification notification = new Notification(Notification.PROFILE_SET_OK, + resourceHelper.gs(R.string.profile_set_ok), + Notification.INFO, 60); + rxBus.send(new EventNewNotification(notification)); + } else { + Notification notification = new Notification(Notification.FAILED_UDPATE_PROFILE, + resourceHelper.gs(R.string.failedupdatebasalprofile), + Notification.URGENT); + rxBus.send(new EventNewNotification(notification)); + } + + return result; + } + + + // OPERATIONS not supported by Pump or Plugin + + protected List customActions = null; + + private CustomAction customActionResetRLConfig = new CustomAction( + R.string.medtronic_custom_action_reset_rileylink, OmnipodCustomActionType.ResetRileyLinkConfiguration, true); + + + @Override + public List getCustomActions() { + + if (customActions == null) { + this.customActions = Arrays.asList( + customActionResetRLConfig //, + ); + } + + return this.customActions; + } + + + @Override + public void executeCustomAction(CustomActionType customActionType) { + OmnipodCustomActionType mcat = (OmnipodCustomActionType) customActionType; + + switch (mcat) { + + case ResetRileyLinkConfiguration: { + serviceTaskExecutor.startTask(new ResetRileyLinkConfigurationTask(getInjector())); + } + break; + + default: + break; + } + } + + @Override + public void timezoneOrDSTChanged(TimeChangeType timeChangeType) { + aapsLogger.warn(LTag.PUMP, getLogPrefix() + "Time, Date and/or TimeZone changed. [changeType=" + timeChangeType.name() + ", eventHandlingEnabled=" + omnipodPumpStatus.timeChangeEventEnabled + "]"); + + if (omnipodUtil.getDriverState() == OmnipodDriverState.Initalized_PodAttached) { + if (omnipodPumpStatus.timeChangeEventEnabled) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "Time,and/or TimeZone changed event received and will be consumed by driver."); + this.hasTimeDateOrTimeZoneChanged = true; + } + } + } + + @Override + public boolean isUnreachableAlertTimeoutExceeded(long unreachableTimeoutMilliseconds) { + if (omnipodPumpStatus.lastConnection != 0 || omnipodPumpStatus.lastErrorConnection != 0) { + if (omnipodPumpStatus.lastConnection + unreachableTimeoutMilliseconds < System.currentTimeMillis()) { + if (omnipodPumpStatus.lastErrorConnection > omnipodPumpStatus.lastConnection) { + // We exceeded the alert threshold, and our last connection failed + // We should show an alert + return true; + } + + // Don't trigger an alert when we exceeded the thresholds, but the last communication was successful + // This happens when we simply didn't need to send any commands to the pump + return false; + } + } + + return false; + } + + + @Override + public RxBusWrapper getRxBus() { + return this.rxBus; + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/BolusProgressIndicationConsumer.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/BolusProgressIndicationConsumer.java new file mode 100644 index 0000000000..35d07e3622 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/BolusProgressIndicationConsumer.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm; + +// TODO replace with Consumer when our min API level >= 24 +@FunctionalInterface +public interface BolusProgressIndicationConsumer { + void accept(double estimatedUnitsDelivered, int percentage); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodCommunicationManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodCommunicationManager.java new file mode 100644 index 0000000000..f8ff351391 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodCommunicationManager.java @@ -0,0 +1,345 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm; + +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.common.data.PumpStatus; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkCommunicationManager; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.RFSpy; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.RileyLinkCommunicationException; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.RLMessage; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RLMessageType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkBLEError; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkServiceData; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks.ServiceTaskExecutor; +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; +import info.nightscout.androidaps.plugins.pump.omnipod.OmnipodPumpPlugin; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.OmnipodAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommunicationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPacketTypeException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalResponseException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NonceOutOfSyncException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NonceResyncException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NotEnoughDataException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodFaultException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodReturnedErrorResponseException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodPacket; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.DeactivatePodCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.ErrorResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.ErrorResponseType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PacketType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +/** + * Created by andy on 6/29/18. + */ +public class OmnipodCommunicationManager extends RileyLinkCommunicationManager { + + @Inject public AAPSLogger aapsLogger; + @Inject OmnipodPumpStatus omnipodPumpStatus; + @Inject OmnipodPumpPlugin omnipodPumpPlugin; + @Inject RileyLinkServiceData rileyLinkServiceData; + @Inject ServiceTaskExecutor serviceTaskExecutor; + + public OmnipodCommunicationManager(HasAndroidInjector injector, RFSpy rfspy) { + super(injector, rfspy); + omnipodPumpStatus.previousConnection = sp.getLong( + RileyLinkConst.Prefs.LastGoodDeviceCommunicationTime, 0L); + } + +// @Override +// protected void configurePumpSpecificSettings() { +// } + + @Override + public boolean tryToConnectToDevice() { + // TODO + return false; + } + + @Override + public byte[] createPumpMessageContent(RLMessageType type) { + return new byte[0]; + } + + @Override public PumpStatus getPumpStatus() { + return null; + } + + @Override public boolean isDeviceReachable() { + return false; + } + + @Override + public boolean hasTunning() { + return false; + } + + @Override + public E createResponseMessage(byte[] payload, Class clazz) { + return (E) new OmnipodPacket(payload); + } + + @Override + public void setPumpDeviceState(PumpDeviceState pumpDeviceState) { + this.omnipodPumpStatus.setPumpDeviceState(pumpDeviceState); + } + + public T sendCommand(Class responseClass, PodState podState, MessageBlock command) { + return sendCommand(responseClass, podState, command, true); + } + + public T sendCommand(Class responseClass, PodState podState, MessageBlock command, boolean automaticallyResyncNone) { + OmnipodMessage message = new OmnipodMessage(podState.getAddress(), Collections.singletonList(command), podState.getMessageNumber()); + return exchangeMessages(responseClass, podState, message, automaticallyResyncNone); + } + + // Convenience method + public T executeAction(OmnipodAction action) { + return action.execute(this); + } + + public T exchangeMessages(Class responseClass, PodState podState, OmnipodMessage message) { + return exchangeMessages(responseClass, podState, message, true); + } + + public T exchangeMessages(Class responseClass, PodState podState, OmnipodMessage message, boolean automaticallyResyncNonce) { + return exchangeMessages(responseClass, podState, message, null, null, automaticallyResyncNonce); + } + + public synchronized T exchangeMessages(Class responseClass, PodState podState, OmnipodMessage message, Integer addressOverride, Integer ackAddressOverride) { + return exchangeMessages(responseClass, podState, message, addressOverride, ackAddressOverride, true); + } + + public synchronized T exchangeMessages(Class responseClass, PodState podState, OmnipodMessage message, Integer addressOverride, Integer ackAddressOverride, boolean automaticallyResyncNonce) { + + aapsLogger.debug(LTag.PUMPCOMM, "Exchanging OmnipodMessage [responseClass={}, podState={}, message={}, addressOverride={}, ackAddressOverride={}, automaticallyResyncNonce={}]: {}", // + responseClass.getSimpleName(), podState, message, addressOverride, ackAddressOverride, automaticallyResyncNonce, message); + + for (int i = 0; 2 > i; i++) { + + if (podState.hasNonceState() && message.isNonceResyncable()) { + podState.advanceToNextNonce(); + } + + MessageBlock responseMessageBlock = transportMessages(podState, message, addressOverride, ackAddressOverride); + + if (responseMessageBlock instanceof StatusResponse) { + podState.updateFromStatusResponse((StatusResponse) responseMessageBlock); + } + + if (responseClass.isInstance(responseMessageBlock)) { + return (T) responseMessageBlock; + } else { + if (responseMessageBlock.getType() == MessageBlockType.ERROR_RESPONSE) { + ErrorResponse error = (ErrorResponse) responseMessageBlock; + if (error.getErrorResponseType() == ErrorResponseType.BAD_NONCE) { + podState.resyncNonce(error.getNonceSearchKey(), message.getSentNonce(), message.getSequenceNumber()); + if (automaticallyResyncNonce) { + message.resyncNonce(podState.getCurrentNonce()); + } else { + throw new NonceOutOfSyncException(); + } + } else { + throw new PodReturnedErrorResponseException((ErrorResponse) responseMessageBlock); + } + } else if (responseMessageBlock.getType() == MessageBlockType.POD_INFO_RESPONSE && ((PodInfoResponse) responseMessageBlock).getSubType() == PodInfoType.FAULT_EVENT) { + PodInfoFaultEvent faultEvent = ((PodInfoResponse) responseMessageBlock).getPodInfo(); + podState.setFaultEvent(faultEvent); + throw new PodFaultException(faultEvent); + } else { + throw new IllegalResponseException(responseClass.getSimpleName(), responseMessageBlock.getType()); + } + } + } + + throw new NonceResyncException(); + } + + private MessageBlock transportMessages(PodState podState, OmnipodMessage message, Integer addressOverride, Integer ackAddressOverride) { + int packetAddress = podState.getAddress(); + if (addressOverride != null) { + packetAddress = addressOverride; + } + + boolean firstPacket = true; + byte[] encodedMessage; + // this does not work well with the deactivate pod command, we somehow either + // receive an ACK instead of a normal response, or a partial response and a communication timeout + if (message.isNonceResyncable() && !message.containsBlock(DeactivatePodCommand.class)) { + OmnipodMessage paddedMessage = new OmnipodMessage(message); + // If messages are nonce resyncable, we want do distinguish between certain and uncertain failures for verification purposes + // However, some commands (e.g. cancel delivery) are single packet command by nature. When we get a timeout with a single packet, + // we are unsure whether or not the command was received by the pod + // However, if we send > 1 packet, we know that the command wasn't received if we never send the subsequent packets, + // because the last packet contains the CRC. + // So we pad the message with get status commands to make it > packet + paddedMessage.padWithGetStatusCommands(PacketType.PDM.getMaxBodyLength()); // First packet is of type PDM + encodedMessage = paddedMessage.getEncoded(); + } else { + encodedMessage = message.getEncoded(); + } + + OmnipodPacket response = null; + while (encodedMessage.length > 0) { + PacketType packetType = firstPacket ? PacketType.PDM : PacketType.CON; + OmnipodPacket packet = new OmnipodPacket(packetAddress, packetType, podState.getPacketNumber(), encodedMessage); + byte[] encodedMessageInPacket = packet.getEncodedMessage(); + + // getting the data remaining to be sent + encodedMessage = ByteUtil.substring(encodedMessage, encodedMessageInPacket.length, encodedMessage.length - encodedMessageInPacket.length); + firstPacket = false; + + try { + // We actually ignore previous (ack) responses if it was not last packet to send + response = exchangePackets(podState, packet); + } catch (Exception ex) { + OmnipodException newException; + if (ex instanceof OmnipodException) { + newException = (OmnipodException) ex; + } else { + newException = new CommunicationException(CommunicationException.Type.UNEXPECTED_EXCEPTION, ex); + } + + boolean lastPacket = encodedMessage.length == 0; + + // If this is not the last packet, the message wasn't fully sent, + // so it's impossible for the pod to have received the message + newException.setCertainFailure(!lastPacket); + + aapsLogger.debug(LTag.PUMPCOMM, "Caught exception in transportMessages. Set certainFailure to {} because encodedMessage.length={}", newException.isCertainFailure(), encodedMessage.length); + + throw newException; + } + } + + if (response.getPacketType() == PacketType.ACK) { + podState.increasePacketNumber(1); + throw new IllegalPacketTypeException(null, PacketType.ACK); + } + + OmnipodMessage receivedMessage = null; + byte[] receivedMessageData = response.getEncodedMessage(); + while (receivedMessage == null) { + try { + receivedMessage = OmnipodMessage.decodeMessage(receivedMessageData); + } catch (NotEnoughDataException ex) { + // Message is (probably) not complete yet + OmnipodPacket ackForCon = createAckPacket(podState, packetAddress, ackAddressOverride); + + try { + OmnipodPacket conPacket = exchangePackets(podState, ackForCon, 3, 40); + if (conPacket.getPacketType() != PacketType.CON) { + throw new IllegalPacketTypeException(PacketType.CON, conPacket.getPacketType()); + } + receivedMessageData = ByteUtil.concat(receivedMessageData, conPacket.getEncodedMessage()); + } catch (OmnipodException ex2) { + throw ex2; + } catch (Exception ex2) { + throw new CommunicationException(CommunicationException.Type.UNEXPECTED_EXCEPTION, ex2); + } + + } + } + + podState.increaseMessageNumber(2); + + ackUntilQuiet(podState, packetAddress, ackAddressOverride); + + List messageBlocks = receivedMessage.getMessageBlocks(); + + if (messageBlocks.size() == 0) { + throw new NotEnoughDataException(receivedMessageData); + } else if (messageBlocks.size() > 1) { + // BS: don't expect this to happen + aapsLogger.error(LTag.PUMPBTCOMM, "Received more than one message block: {}", messageBlocks.toString()); + } + + return messageBlocks.get(0); + } + + private OmnipodPacket createAckPacket(PodState podState, Integer packetAddress, Integer messageAddress) { + int pktAddress = podState.getAddress(); + int msgAddress = podState.getAddress(); + if (packetAddress != null) { + pktAddress = packetAddress; + } + if (messageAddress != null) { + msgAddress = messageAddress; + } + return new OmnipodPacket(pktAddress, PacketType.ACK, podState.getPacketNumber(), ByteUtil.getBytesFromInt(msgAddress)); + } + + private void ackUntilQuiet(PodState podState, Integer packetAddress, Integer messageAddress) { + OmnipodPacket ack = createAckPacket(podState, packetAddress, messageAddress); + boolean quiet = false; + while (!quiet) try { + sendAndListen(ack, 300, 1, 0, 40, OmnipodPacket.class); + } catch (RileyLinkCommunicationException ex) { + if (RileyLinkBLEError.Timeout.equals(ex.getErrorCode())) { + quiet = true; + } else { + aapsLogger.debug(LTag.PUMPBTCOMM, "Ignoring exception in ackUntilQuiet", ex); + } + } catch (OmnipodException ex) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Ignoring exception in ackUntilQuiet", ex); + } catch (Exception ex) { + throw new CommunicationException(CommunicationException.Type.UNEXPECTED_EXCEPTION, ex); + } + + podState.increasePacketNumber(1); + } + + private OmnipodPacket exchangePackets(PodState podState, OmnipodPacket packet) { + return exchangePackets(podState, packet, 0, 333, 9000, 127); + } + + private OmnipodPacket exchangePackets(PodState podState, OmnipodPacket packet, int repeatCount, int preambleExtensionMilliseconds) { + return exchangePackets(podState, packet, repeatCount, 333, 9000, preambleExtensionMilliseconds); + } + + private OmnipodPacket exchangePackets(PodState podState, OmnipodPacket packet, int repeatCount, int responseTimeoutMilliseconds, int exchangeTimeoutMilliseconds, int preambleExtensionMilliseconds) { + long timeoutTime = System.currentTimeMillis() + exchangeTimeoutMilliseconds; + + while (System.currentTimeMillis() < timeoutTime) { + OmnipodPacket response = null; + try { + response = sendAndListen(packet, responseTimeoutMilliseconds, repeatCount, 9, preambleExtensionMilliseconds, OmnipodPacket.class); + } catch (RileyLinkCommunicationException | OmnipodException ex) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Ignoring exception in exchangePackets", ex); + } catch (Exception ex) { + throw new CommunicationException(CommunicationException.Type.UNEXPECTED_EXCEPTION, ex); + } + if (response == null || !response.isValid()) { + continue; + } + if (response.getAddress() != packet.getAddress()) { + continue; + } + if (response.getSequenceNumber() != ((podState.getPacketNumber() + 1) & 0b11111)) { + continue; + } + + podState.increasePacketNumber(2); + return response; + } + throw new CommunicationException(CommunicationException.Type.TIMEOUT); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodManager.java new file mode 100644 index 0000000000..56c8de7626 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodManager.java @@ -0,0 +1,664 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Duration; + +import java.util.EnumSet; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.AcknowledgeAlertsAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.AssignAddressAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.BolusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.CancelDeliveryAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.ConfigurePodAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.DeactivatePodAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.GetPodInfoAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.GetStatusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.InsertCannulaAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.PrimeAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.SetBasalScheduleAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.SetTempBasalAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.InsertCannulaService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.PrimeService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommunicationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalDeliveryStatusException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalSetupProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NonceOutOfSyncException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodFaultException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.CancelDeliveryCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoRecentPulseLog; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodStateChangedHandler; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.utils.sharedPreferences.SP; +import io.reactivex.Completable; +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.SingleSubject; + +public class OmnipodManager { + private static final int ACTION_VERIFICATION_TRIES = 3; + + protected final OmnipodCommunicationManager communicationService; + private final PodStateChangedHandler podStateChangedHandler; + protected PodSessionState podState; + + private ActiveBolusData activeBolusData; + private final Object bolusDataMutex = new Object(); + + //private HasAndroidInjector injector; + AAPSLogger aapsLogger; + SP sp; + + public OmnipodManager(//HasAndroidInjector injector, + AAPSLogger aapsLogger, + SP sp, + OmnipodCommunicationManager communicationService, + PodSessionState podState, + PodStateChangedHandler podStateChangedHandler) { +// this.injector = injector; +// this.injector.androidInjector().inject(this); + if (communicationService == null) { + throw new IllegalArgumentException("Communication service cannot be null"); + } + this.aapsLogger = aapsLogger; + this.sp = sp; + this.communicationService = communicationService; + if (podState != null) { + podState.setStateChangedHandler(podStateChangedHandler); + } + this.podState = podState; + this.podStateChangedHandler = podStateChangedHandler; + } + +// public OmnipodManager(HasAndroidInjector injector, +// OmnipodCommunicationManager communicationService, +// PodSessionState podState) { +// this(injector, communicationService, podState, null); +// } + + public synchronized Single pairAndPrime() { + logStartingCommandExecution("pairAndPrime"); + + try { + if (podState == null) { + podState = communicationService.executeAction( + new AssignAddressAction(podStateChangedHandler)); + } else if (SetupProgress.PRIMING.isBefore(podState.getSetupProgress())) { + throw new IllegalSetupProgressException(SetupProgress.ADDRESS_ASSIGNED, podState.getSetupProgress()); + } + + if (SetupProgress.ADDRESS_ASSIGNED.equals(podState.getSetupProgress())) { + communicationService.executeAction(new ConfigurePodAction(podState)); + } + + communicationService.executeAction(new PrimeAction(new PrimeService(), podState)); + } finally { + logCommandExecutionFinished("pairAndPrime"); + } + + long delayInSeconds = calculateBolusDuration(OmnipodConst.POD_PRIME_BOLUS_UNITS, OmnipodConst.POD_PRIMING_DELIVERY_RATE).getStandardSeconds(); + + return Single.timer(delayInSeconds, TimeUnit.SECONDS) // + .map(o -> verifySetupAction(statusResponse -> + PrimeAction.updatePrimingStatus(podState, statusResponse, aapsLogger), SetupProgress.PRIMING_FINISHED)) // + .observeOn(Schedulers.io()); + } + + public synchronized Single insertCannula(BasalSchedule basalSchedule) { + if (podState == null || podState.getSetupProgress().isBefore(SetupProgress.PRIMING_FINISHED)) { + throw new IllegalSetupProgressException(SetupProgress.PRIMING_FINISHED, podState == null ? null : podState.getSetupProgress()); + } else if (podState.getSetupProgress().isAfter(SetupProgress.CANNULA_INSERTING)) { + throw new IllegalSetupProgressException(SetupProgress.CANNULA_INSERTING, podState.getSetupProgress()); + } + + logStartingCommandExecution("insertCannula [basalSchedule=" + basalSchedule + "]"); + + try { + communicationService.executeAction(new InsertCannulaAction(new InsertCannulaService(), podState, basalSchedule)); + } finally { + logCommandExecutionFinished("insertCannula"); + } + + long delayInSeconds = calculateBolusDuration(OmnipodConst.POD_CANNULA_INSERTION_BOLUS_UNITS, OmnipodConst.POD_CANNULA_INSERTION_DELIVERY_RATE).getStandardSeconds(); + + return Single.timer(delayInSeconds, TimeUnit.SECONDS) // + .map(o -> verifySetupAction(statusResponse -> + InsertCannulaAction.updateCannulaInsertionStatus(podState, statusResponse, aapsLogger), SetupProgress.COMPLETED)) // + .observeOn(Schedulers.io()); + } + + public synchronized StatusResponse getPodStatus() { + if (podState == null) { + throw new IllegalSetupProgressException(SetupProgress.PRIMING_FINISHED, null); + } + + logStartingCommandExecution("getPodStatus"); + + try { + return communicationService.executeAction(new GetStatusAction(podState)); + } finally { + logCommandExecutionFinished("getPodStatus"); + } + } + + public synchronized PodInfoResponse getPodInfo(PodInfoType podInfoType) { + assertReadyForDelivery(); + + logStartingCommandExecution("getPodInfo"); + + try { + return communicationService.executeAction(new GetPodInfoAction(podState, podInfoType)); + } finally { + logCommandExecutionFinished("getPodInfo"); + } + } + + public synchronized StatusResponse acknowledgeAlerts() { + assertReadyForDelivery(); + + logStartingCommandExecution("acknowledgeAlerts"); + + try { + return executeAndVerify(() -> communicationService.executeAction(new AcknowledgeAlertsAction(podState, podState.getActiveAlerts()))); + } finally { + logCommandExecutionFinished("acknowledgeAlerts"); + } + } + + // CAUTION: cancels all delivery + // CAUTION: suspends and then resumes delivery. An OmnipodException[certainFailure=false] indicates that the pod is or might be suspended + public synchronized StatusResponse setBasalSchedule(BasalSchedule schedule, boolean acknowledgementBeep) { + assertReadyForDelivery(); + + logStartingCommandExecution("setBasalSchedule [basalSchedule=" + schedule + ", acknowledgementBeep=" + acknowledgementBeep + "]"); + + try { + cancelDelivery(EnumSet.allOf(DeliveryType.class), acknowledgementBeep); + } catch (Exception ex) { + logCommandExecutionFinished("setBasalSchedule"); + throw ex; + } + + try { + try { + return executeAndVerify(() -> communicationService.executeAction(new SetBasalScheduleAction(podState, schedule, + false, podState.getScheduleOffset(), acknowledgementBeep))); + } catch (OmnipodException ex) { + // Treat all exceptions as uncertain failures, because all delivery has been suspended here. + // Setting this to an uncertain failure will enable for the user to get an appropriate warning + ex.setCertainFailure(false); + throw ex; + } + } finally { + logCommandExecutionFinished("setBasalSchedule"); + } + } + + // CAUTION: cancels temp basal and then sets new temp basal. An OmnipodException[certainFailure=false] indicates that the pod might have cancelled the previous temp basal, but did not set a new temp basal + public synchronized StatusResponse setTemporaryBasal(double rate, Duration duration, boolean acknowledgementBeep, boolean completionBeep) { + assertReadyForDelivery(); + + logStartingCommandExecution("setTemporaryBasal [rate=" + rate + ", duration=" + duration + ", acknowledgementBeep=" + acknowledgementBeep + ", completionBeep=" + completionBeep + "]"); + + try { + cancelDelivery(EnumSet.of(DeliveryType.TEMP_BASAL), acknowledgementBeep); + } catch (Exception ex) { + logCommandExecutionFinished("setTemporaryBasal"); + throw ex; + } + + try { + return executeAndVerify(() -> communicationService.executeAction(new SetTempBasalAction( + podState, rate, duration, + acknowledgementBeep, completionBeep))); + } catch (OmnipodException ex) { + // Treat all exceptions as uncertain failures, because all delivery has been suspended here. + // Setting this to an uncertain failure will enable for the user to get an appropriate warning + ex.setCertainFailure(false); + throw ex; + } finally { + logCommandExecutionFinished("setTemporaryBasal"); + } + } + + public synchronized void cancelTemporaryBasal(boolean acknowledgementBeep) { + cancelDelivery(EnumSet.of(DeliveryType.TEMP_BASAL), acknowledgementBeep); + } + + private synchronized StatusResponse cancelDelivery(EnumSet deliveryTypes, boolean acknowledgementBeep) { + assertReadyForDelivery(); + + logStartingCommandExecution("cancelDelivery [deliveryTypes=" + deliveryTypes + ", acknowledgementBeep=" + acknowledgementBeep + "]"); + + try { + return executeAndVerify(() -> { + StatusResponse statusResponse = communicationService.executeAction(new CancelDeliveryAction(podState, deliveryTypes, acknowledgementBeep)); + aapsLogger.info(LTag.PUMPBTCOMM, "Status response after cancel delivery[types={}]: {}", deliveryTypes.toString(), statusResponse.toString()); + return statusResponse; + }); + } finally { + logCommandExecutionFinished("cancelDelivery"); + } + } + + // Returns a SingleSubject that returns when the bolus has finished. + // When a bolus is cancelled, it will return after cancellation and report the estimated units delivered + // Only throws OmnipodException[certainFailure=false] + public synchronized BolusCommandResult bolus(Double units, boolean acknowledgementBeep, boolean completionBeep, BolusProgressIndicationConsumer progressIndicationConsumer) { + assertReadyForDelivery(); + + logStartingCommandExecution("bolus [units=" + units + ", acknowledgementBeep=" + acknowledgementBeep + ", completionBeep=" + completionBeep + "]"); + + CommandDeliveryStatus commandDeliveryStatus = CommandDeliveryStatus.SUCCESS; + + try { + executeAndVerify(() -> communicationService.executeAction(new BolusAction(podState, units, acknowledgementBeep, completionBeep))); + } catch (OmnipodException ex) { + if (ex.isCertainFailure()) { + throw ex; + } + + // Catch uncertain exceptions as we still want to report bolus progress indication + aapsLogger.error(LTag.PUMPBTCOMM, "Caught exception[certainFailure=false] in bolus", ex); + commandDeliveryStatus = CommandDeliveryStatus.UNCERTAIN_FAILURE; + } finally { + logCommandExecutionFinished("bolus"); + } + + DateTime startDate = DateTime.now().minus(OmnipodConst.AVERAGE_BOLUS_COMMAND_COMMUNICATION_DURATION); + + CompositeDisposable disposables = new CompositeDisposable(); + Duration bolusDuration = calculateBolusDuration(units, OmnipodConst.POD_BOLUS_DELIVERY_RATE); + Duration estimatedRemainingBolusDuration = bolusDuration.minus(OmnipodConst.AVERAGE_BOLUS_COMMAND_COMMUNICATION_DURATION); + + if (progressIndicationConsumer != null) { + int numberOfProgressReports = Math.max(20, Math.min(100, (int) Math.ceil(units) * 10)); + long progressReportInterval = estimatedRemainingBolusDuration.getMillis() / numberOfProgressReports; + + disposables.add(Flowable.intervalRange(0, numberOfProgressReports + 1, 0, progressReportInterval, TimeUnit.MILLISECONDS) // + .observeOn(Schedulers.io()) // + .subscribe(count -> { + int percentage = (int) ((double) count / numberOfProgressReports * 100); + double estimatedUnitsDelivered = activeBolusData == null ? 0 : activeBolusData.estimateUnitsDelivered(); + progressIndicationConsumer.accept(estimatedUnitsDelivered, percentage); + })); + } + + SingleSubject bolusCompletionSubject = SingleSubject.create(); + + synchronized (bolusDataMutex) { + activeBolusData = new ActiveBolusData(units, startDate, bolusCompletionSubject, disposables); + } + + disposables.add(Completable.complete() // + .delay(estimatedRemainingBolusDuration.getMillis() + 250, TimeUnit.MILLISECONDS) // + .observeOn(Schedulers.io()) // + .doOnComplete(() -> { + synchronized (bolusDataMutex) { + double unitsNotDelivered = 0.0d; + + for (int i = 0; i < ACTION_VERIFICATION_TRIES; i++) { + try { + // Retrieve a status response in order to update the pod state + StatusResponse statusResponse = getPodStatus(); + if (statusResponse.getDeliveryStatus().isBolusing()) { + throw new IllegalDeliveryStatusException(DeliveryStatus.NORMAL, statusResponse.getDeliveryStatus()); + } else { + break; + } + } catch (PodFaultException ex) { + // Substract units not delivered in case of a Pod failure + unitsNotDelivered = ex.getFaultEvent().getInsulinNotDelivered(); + + aapsLogger.debug(LTag.PUMPBTCOMM, "Caught PodFaultException in bolus completion verification", ex); + break; + } catch (Exception ex) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Ignoring exception in bolus completion verification", ex); + } + } + + if (hasActiveBolus()) { + activeBolusData.bolusCompletionSubject.onSuccess(new BolusDeliveryResult(units - unitsNotDelivered)); + activeBolusData = null; + } + } + }) + .subscribe()); + + return new BolusCommandResult(commandDeliveryStatus, bolusCompletionSubject); + } + + public synchronized void cancelBolus(boolean acknowledgementBeep) { + assertReadyForDelivery(); + + synchronized (bolusDataMutex) { + if (activeBolusData == null) { + throw new IllegalDeliveryStatusException(DeliveryStatus.BOLUS_IN_PROGRESS, podState.getLastDeliveryStatus()); + } + + logStartingCommandExecution("cancelBolus [acknowledgementBeep=" + acknowledgementBeep + "]"); + + try { + StatusResponse statusResponse = cancelDelivery(EnumSet.of(DeliveryType.BOLUS), acknowledgementBeep); + discardActiveBolusData(statusResponse.getInsulinNotDelivered()); + } catch (PodFaultException ex) { + discardActiveBolusData(ex.getFaultEvent().getInsulinNotDelivered()); + throw ex; + } finally { + logCommandExecutionFinished("cancelBolus"); + } + } + } + + private void discardActiveBolusData(double unitsNotDelivered) { + synchronized (bolusDataMutex) { + activeBolusData.getDisposables().dispose(); + activeBolusData.getBolusCompletionSubject().onSuccess(new BolusDeliveryResult(activeBolusData.getUnits() - unitsNotDelivered)); + activeBolusData = null; + } + } + + public synchronized void suspendDelivery(boolean acknowledgementBeep) { + cancelDelivery(EnumSet.allOf(DeliveryType.class), acknowledgementBeep); + } + + // Same as setting basal schedule, but without suspending delivery first + public synchronized StatusResponse resumeDelivery(boolean acknowledgementBeep) { + assertReadyForDelivery(); + logStartingCommandExecution("resumeDelivery"); + + try { + return executeAndVerify(() -> communicationService.executeAction(new SetBasalScheduleAction(podState, podState.getBasalSchedule(), + false, podState.getScheduleOffset(), acknowledgementBeep))); + } finally { + logCommandExecutionFinished("resumeDelivery"); + } + } + + // CAUTION: cancels all delivery + // CAUTION: suspends and then resumes delivery. An OmnipodException[certainFailure=false] indicates that the pod is or might be suspended + public synchronized void setTime(boolean acknowledgementBeeps) { + assertReadyForDelivery(); + + logStartingCommandExecution("setTime [acknowledgementBeeps=" + acknowledgementBeeps + "]"); + + try { + cancelDelivery(EnumSet.allOf(DeliveryType.class), acknowledgementBeeps); + } catch (Exception ex) { + logCommandExecutionFinished("setTime"); + throw ex; + } + + DateTimeZone oldTimeZone = podState.getTimeZone(); + + try { + // Joda seems to cache the default time zone, so we use the JVM's + DateTimeZone.setDefault(DateTimeZone.forTimeZone(TimeZone.getDefault())); + podState.setTimeZone(DateTimeZone.getDefault()); + + setBasalSchedule(podState.getBasalSchedule(), acknowledgementBeeps); + } catch (OmnipodException ex) { + // Treat all exceptions as uncertain failures, because all delivery has been suspended here. + // Setting this to an uncertain failure will enable for the user to get an appropriate warning + podState.setTimeZone(oldTimeZone); + ex.setCertainFailure(false); + throw ex; + } finally { + logCommandExecutionFinished("setTime"); + } + } + + public synchronized void deactivatePod() { + if (podState == null) { + throw new IllegalSetupProgressException(SetupProgress.ADDRESS_ASSIGNED, null); + } + + logStartingCommandExecution("deactivatePod"); + + // Try to get pulse log for diagnostics + // FIXME replace by storing to file + try { + PodInfoResponse podInfoResponse = communicationService.executeAction(new GetPodInfoAction(podState, PodInfoType.RECENT_PULSE_LOG)); + PodInfoRecentPulseLog pulseLogInfo = podInfoResponse.getPodInfo(); + aapsLogger.info(LTag.PUMPBTCOMM, "Retrieved pulse log from the pod: {}", pulseLogInfo.toString()); + } catch (Exception ex) { + aapsLogger.warn(LTag.PUMPBTCOMM, "Failed to retrieve pulse log from the pod", ex); + } + + try { + // Always send acknowledgement beeps here. Matches the PDM's behavior + communicationService.executeAction(new DeactivatePodAction(podState, true)); + } catch (PodFaultException ex) { + aapsLogger.info(LTag.PUMPBTCOMM, "Ignoring PodFaultException in deactivatePod", ex); + } finally { + logCommandExecutionFinished("deactivatePod"); + } + + resetPodState(false); + } + + public void resetPodState(boolean forcedByUser) { + aapsLogger.warn(LTag.PUMPBTCOMM, "resetPodState has been called. forcedByUser={}", forcedByUser); + podState = null; + sp.remove(OmnipodConst.Prefs.PodState); + } + + public OmnipodCommunicationManager getCommunicationService() { + return communicationService; + } + + public DateTime getTime() { + return podState.getTime(); + } + + public boolean isReadyForDelivery() { + return podState != null && podState.getSetupProgress() == SetupProgress.COMPLETED; + } + + public boolean hasActiveBolus() { + synchronized (bolusDataMutex) { + return activeBolusData != null; + } + } + + // FIXME this is dirty, we should not expose the original pod state + public PodSessionState getPodState() { + return this.podState; + } + + public String getPodStateAsString() { + return podState == null ? "null" : podState.toString(); + } + + // Only works for commands with nonce resyncable message blocks + // FIXME method is too big, needs refactoring + private StatusResponse executeAndVerify(VerifiableAction runnable) { + try { + return runnable.run(); + } catch (Exception originalException) { + if (isCertainFailure(originalException)) { + throw originalException; + } else { + aapsLogger.warn(LTag.PUMPBTCOMM, "Caught exception in executeAndVerify. Verifying command by using cancel none command to verify nonce", originalException); + + try { + logStartingCommandExecution("verifyCommand"); + StatusResponse statusResponse = communicationService.sendCommand(StatusResponse.class, podState, + new CancelDeliveryCommand(podState.getCurrentNonce(), BeepType.NO_BEEP, DeliveryType.NONE), false); + aapsLogger.info(LTag.PUMPBTCOMM, "Command status resolved to SUCCESS. Status response after cancelDelivery[types=DeliveryType.NONE]: {}", statusResponse); + + return statusResponse; + } catch (NonceOutOfSyncException verificationException) { + aapsLogger.error(LTag.PUMPBTCOMM, "Command resolved to FAILURE (CERTAIN_FAILURE)", verificationException); + + if (originalException instanceof OmnipodException) { + ((OmnipodException) originalException).setCertainFailure(true); + throw originalException; + } else { + OmnipodException newException = new CommunicationException(CommunicationException.Type.UNEXPECTED_EXCEPTION, originalException); + newException.setCertainFailure(true); + throw newException; + } + } catch (Exception verificationException) { + aapsLogger.error(LTag.PUMPBTCOMM, "Command unresolved (UNCERTAIN_FAILURE)", verificationException); + throw originalException; + } finally { + logCommandExecutionFinished("verifyCommand"); + } + } + } + } + + private void assertReadyForDelivery() { + if (!isReadyForDelivery()) { + throw new IllegalSetupProgressException(SetupProgress.COMPLETED, podState == null ? null : podState.getSetupProgress()); + } + } + + private SetupActionResult verifySetupAction(StatusResponseConsumer setupActionResponseHandler, SetupProgress expectedSetupProgress) { + SetupActionResult result = null; + for (int i = 0; ACTION_VERIFICATION_TRIES > i; i++) { + try { + StatusResponse delayedStatusResponse = communicationService.executeAction(new GetStatusAction(podState)); + setupActionResponseHandler.accept(delayedStatusResponse); + + if (podState.getSetupProgress().equals(expectedSetupProgress)) { + result = new SetupActionResult(SetupActionResult.ResultType.SUCCESS); + break; + } else { + result = new SetupActionResult(SetupActionResult.ResultType.FAILURE) // + .setupProgress(podState.getSetupProgress()); + break; + } + } catch (Exception ex) { + result = new SetupActionResult(SetupActionResult.ResultType.VERIFICATION_FAILURE) // + .exception(ex); + } + } + return result; + } + + private void logStartingCommandExecution(String action) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Starting command execution for action: " + action); + } + + private void logCommandExecutionFinished(String action) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Command execution finished for action: " + action); + } + + private static Duration calculateBolusDuration(double units, double deliveryRate) { + return Duration.standardSeconds((long) Math.ceil(units / deliveryRate)); + } + + public static Duration calculateBolusDuration(double units) { + return calculateBolusDuration(units, OmnipodConst.POD_BOLUS_DELIVERY_RATE); + } + + public static boolean isCertainFailure(Exception ex) { + return ex instanceof OmnipodException && ((OmnipodException) ex).isCertainFailure(); + } + + public static class BolusCommandResult { + private final CommandDeliveryStatus commandDeliveryStatus; + private final SingleSubject deliveryResultSubject; + + public BolusCommandResult(CommandDeliveryStatus commandDeliveryStatus, SingleSubject deliveryResultSubject) { + this.commandDeliveryStatus = commandDeliveryStatus; + this.deliveryResultSubject = deliveryResultSubject; + } + + public CommandDeliveryStatus getCommandDeliveryStatus() { + return commandDeliveryStatus; + } + + public SingleSubject getDeliveryResultSubject() { + return deliveryResultSubject; + } + } + + public static class BolusDeliveryResult { + private final double unitsDelivered; + + public BolusDeliveryResult(double unitsDelivered) { + this.unitsDelivered = unitsDelivered; + } + + public double getUnitsDelivered() { + return unitsDelivered; + } + } + + public enum CommandDeliveryStatus { + SUCCESS, + CERTAIN_FAILURE, + UNCERTAIN_FAILURE + } + + // TODO replace with Consumer when our min API level >= 24 + @FunctionalInterface + private interface StatusResponseConsumer { + void accept(StatusResponse statusResponse); + } + + private static class ActiveBolusData { + private final double units; + private volatile DateTime startDate; + private volatile SingleSubject bolusCompletionSubject; + private volatile CompositeDisposable disposables; + + private ActiveBolusData(double units, DateTime startDate, SingleSubject bolusCompletionSubject, CompositeDisposable disposables) { + this.units = units; + this.startDate = startDate; + this.bolusCompletionSubject = bolusCompletionSubject; + this.disposables = disposables; + } + + public double getUnits() { + return units; + } + + public DateTime getStartDate() { + return startDate; + } + + public CompositeDisposable getDisposables() { + return disposables; + } + + public SingleSubject getBolusCompletionSubject() { + return bolusCompletionSubject; + } + + public double estimateUnitsDelivered() { + long elapsedMillis = new Duration(startDate, DateTime.now()).getMillis(); + long totalDurationMillis = (long) (units / OmnipodConst.POD_BOLUS_DELIVERY_RATE * 1000); + double factor = (double) elapsedMillis / totalDurationMillis; + double estimatedUnits = Math.min(1D, factor) * units; + + int roundingDivisor = (int) (1 / OmnipodConst.POD_PULSE_SIZE); + return (double) Math.round(estimatedUnits * roundingDivisor) / roundingDivisor; + } + } + + // Could be replaced with Supplier when min API level >= 24 + @FunctionalInterface + private interface VerifiableAction { + StatusResponse run(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/SetupActionResult.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/SetupActionResult.java new file mode 100644 index 0000000000..b4c881bfcb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/SetupActionResult.java @@ -0,0 +1,61 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; + +public class SetupActionResult { + private final ResultType resultType; + private String message; + private Exception exception; + private SetupProgress setupProgress; + + public SetupActionResult(ResultType resultType) { + this.resultType = resultType; + } + + public SetupActionResult message(String message) { + this.message = message; + return this; + } + + public SetupActionResult exception(Exception ex) { + exception = ex; + return this; + } + + public SetupActionResult setupProgress(SetupProgress setupProgress) { + this.setupProgress = setupProgress; + return this; + } + + public ResultType getResultType() { + return resultType; + } + + public String getMessage() { + return message; + } + + public Exception getException() { + return exception; + } + + public SetupProgress getSetupProgress() { + return setupProgress; + } + + public enum ResultType { + SUCCESS(true), + VERIFICATION_FAILURE(false), + FAILURE(false); + + private final boolean success; + + ResultType(boolean success) { + this.success = success; + } + + public boolean isSuccess() { + return success; + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AcknowledgeAlertsAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AcknowledgeAlertsAction.java new file mode 100644 index 0000000000..facdb4b2d5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AcknowledgeAlertsAction.java @@ -0,0 +1,39 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.AcknowledgeAlertsCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class AcknowledgeAlertsAction implements OmnipodAction { + private final PodSessionState podState; + private final AlertSet alerts; + + public AcknowledgeAlertsAction(PodSessionState podState, AlertSet alerts) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (alerts == null) { + throw new ActionInitializationException("Alert set can not be null"); + } else if (alerts.size() == 0) { + throw new ActionInitializationException("Alert set can not be empty"); + } + this.podState = podState; + this.alerts = alerts; + } + + public AcknowledgeAlertsAction(PodSessionState podState, AlertSlot alertSlot) { + this(podState, new AlertSet(Collections.singletonList(alertSlot))); + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + return communicationService.sendCommand(StatusResponse.class, podState, + new AcknowledgeAlertsCommand(podState.getCurrentNonce(), alerts)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AssignAddressAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AssignAddressAction.java new file mode 100644 index 0000000000..b6563fa214 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AssignAddressAction.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.DateTimeZone; + +import java.util.Collections; +import java.util.Random; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.AssignAddressCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.VersionResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSetupState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodStateChangedHandler; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class AssignAddressAction implements OmnipodAction { + private final int address; + private final PodStateChangedHandler podStateChangedHandler; + + public AssignAddressAction(PodStateChangedHandler podStateChangedHandler) { + this.address = generateRandomAddress(); + this.podStateChangedHandler = podStateChangedHandler; + } + + private static int generateRandomAddress() { + return 0x1f000000 | (new Random().nextInt() & 0x000fffff); + } + + @Override + public PodSessionState execute(OmnipodCommunicationManager communicationService) { + PodSetupState setupState = new PodSetupState(address, 0x00, 0x00); + + AssignAddressCommand assignAddress = new AssignAddressCommand(setupState.getAddress()); + OmnipodMessage assignAddressMessage = new OmnipodMessage(OmnipodConst.DEFAULT_ADDRESS, + Collections.singletonList(assignAddress), setupState.getMessageNumber()); + + VersionResponse assignAddressResponse = communicationService.exchangeMessages(VersionResponse.class, setupState, assignAddressMessage, + OmnipodConst.DEFAULT_ADDRESS, setupState.getAddress()); + + DateTimeZone timeZone = DateTimeZone.getDefault(); + + PodSessionState podState = new PodSessionState(timeZone, address, assignAddressResponse.getPiVersion(), + assignAddressResponse.getPmVersion(), assignAddressResponse.getLot(), assignAddressResponse.getTid(), + setupState.getPacketNumber(), 0x00, communicationService.injector); // At this point, for an unknown reason, the pod starts counting messages from 0 again + + podState.setStateChangedHandler(podStateChangedHandler); + return podState; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/BolusAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/BolusAction.java new file mode 100644 index 0000000000..14cee38962 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/BolusAction.java @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.Duration; + +import java.util.Arrays; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.BolusExtraCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.SetInsulinScheduleCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BolusDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class BolusAction implements OmnipodAction { + private final PodSessionState podState; + private final double units; + private final Duration timeBetweenPulses; + private final boolean acknowledgementBeep; + private final boolean completionBeep; + + public BolusAction(PodSessionState podState, double units, Duration timeBetweenPulses, + boolean acknowledgementBeep, boolean completionBeep) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (timeBetweenPulses == null) { + throw new ActionInitializationException("Time between pulses cannot be null"); + } + this.podState = podState; + this.units = units; + this.timeBetweenPulses = timeBetweenPulses; + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + } + + public BolusAction(PodSessionState podState, double units, boolean acknowledgementBeep, boolean completionBeep) { + this(podState, units, Duration.standardSeconds(2), acknowledgementBeep, completionBeep); + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + BolusDeliverySchedule bolusDeliverySchedule = new BolusDeliverySchedule(units, timeBetweenPulses); + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand( + podState.getCurrentNonce(), bolusDeliverySchedule); + BolusExtraCommand bolusExtraCommand = new BolusExtraCommand(units, timeBetweenPulses, + acknowledgementBeep, completionBeep); + OmnipodMessage primeBolusMessage = new OmnipodMessage(podState.getAddress(), + Arrays.asList(setInsulinScheduleCommand, bolusExtraCommand), podState.getMessageNumber()); + return communicationService.exchangeMessages(StatusResponse.class, podState, primeBolusMessage); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/CancelDeliveryAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/CancelDeliveryAction.java new file mode 100644 index 0000000000..ca713bcab6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/CancelDeliveryAction.java @@ -0,0 +1,56 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.CancelDeliveryCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class CancelDeliveryAction implements OmnipodAction { + private final PodSessionState podState; + private final EnumSet deliveryTypes; + private final boolean acknowledgementBeep; + + public CancelDeliveryAction(PodSessionState podState, EnumSet deliveryTypes, + boolean acknowledgementBeep) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (deliveryTypes == null) { + throw new ActionInitializationException("Delivery types cannot be null"); + } + this.podState = podState; + this.deliveryTypes = deliveryTypes; + this.acknowledgementBeep = acknowledgementBeep; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + List messageBlocks = new ArrayList<>(); + + if (acknowledgementBeep && deliveryTypes.size() > 1) { + // Workaround for strange beep behaviour when cancelling multiple delivery types + List deliveryTypeList = new ArrayList<>(deliveryTypes); + + EnumSet deliveryTypeWithBeep = EnumSet.of(deliveryTypeList.remove(deliveryTypeList.size() - 1)); + EnumSet deliveryTypesWithoutBeep = EnumSet.copyOf(deliveryTypeList); + + messageBlocks.add(new CancelDeliveryCommand(podState.getCurrentNonce(), BeepType.NO_BEEP, deliveryTypesWithoutBeep)); + messageBlocks.add(new CancelDeliveryCommand(podState.getCurrentNonce(), BeepType.BEEP, deliveryTypeWithBeep)); + } else { + messageBlocks.add(new CancelDeliveryCommand(podState.getCurrentNonce(), + acknowledgementBeep && deliveryTypes.size() == 1 ? BeepType.BEEP : BeepType.NO_BEEP, deliveryTypes)); + } + + return communicationService.exchangeMessages(StatusResponse.class, podState, + new OmnipodMessage(podState.getAddress(), messageBlocks, podState.getMessageNumber())); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigureAlertsAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigureAlertsAction.java new file mode 100644 index 0000000000..b44fc092a8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigureAlertsAction.java @@ -0,0 +1,36 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.ConfigureAlertsCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class ConfigureAlertsAction implements OmnipodAction { + private final PodSessionState podState; + private final List alertConfigurations; + + public ConfigureAlertsAction(PodSessionState podState, List alertConfigurations) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (alertConfigurations == null) { + throw new ActionInitializationException("Alert configurations cannot be null"); + } + this.podState = podState; + this.alertConfigurations = alertConfigurations; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + ConfigureAlertsCommand configureAlertsCommand = new ConfigureAlertsCommand(podState.getCurrentNonce(), alertConfigurations); + StatusResponse statusResponse = communicationService.sendCommand(StatusResponse.class, podState, configureAlertsCommand); + for (AlertConfiguration alertConfiguration : alertConfigurations) { + podState.putConfiguredAlert(alertConfiguration.getAlertSlot(), alertConfiguration.getAlertType()); + } + return statusResponse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigurePodAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigurePodAction.java new file mode 100644 index 0000000000..7d2cb3b7e3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigurePodAction.java @@ -0,0 +1,59 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.DateTime; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.ConfigurePodCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.VersionResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PacketType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPacketTypeException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPodProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalSetupProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class ConfigurePodAction implements OmnipodAction { + private final PodSessionState podState; + + public ConfigurePodAction(PodSessionState podState) { + this.podState = podState; + } + + @Override + public VersionResponse execute(OmnipodCommunicationManager communicationService) { + if (!podState.getSetupProgress().equals(SetupProgress.ADDRESS_ASSIGNED)) { + throw new IllegalSetupProgressException(SetupProgress.ADDRESS_ASSIGNED, podState.getSetupProgress()); + } + DateTime activationDate = DateTime.now(podState.getTimeZone()); + + ConfigurePodCommand configurePodCommand = new ConfigurePodCommand(podState.getAddress(), activationDate, + podState.getLot(), podState.getTid()); + OmnipodMessage message = new OmnipodMessage(OmnipodConst.DEFAULT_ADDRESS, + Collections.singletonList(configurePodCommand), podState.getMessageNumber()); + VersionResponse configurePodResponse; + try { + configurePodResponse = communicationService.exchangeMessages(VersionResponse.class, podState, + message, OmnipodConst.DEFAULT_ADDRESS, podState.getAddress()); + } catch (IllegalPacketTypeException ex) { + if (PacketType.ACK.equals(ex.getActual())) { + // Pod is already configured + podState.setSetupProgress(SetupProgress.POD_CONFIGURED); + return null; + } + throw ex; + } + + if (configurePodResponse.getPodProgressStatus() != PodProgressStatus.PAIRING_SUCCESS) { + throw new IllegalPodProgressException(PodProgressStatus.PAIRING_SUCCESS, configurePodResponse.getPodProgressStatus()); + } + + podState.setSetupProgress(SetupProgress.POD_CONFIGURED); + + return configurePodResponse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/DeactivatePodAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/DeactivatePodAction.java new file mode 100644 index 0000000000..01162ef434 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/DeactivatePodAction.java @@ -0,0 +1,38 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.EnumSet; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.DeactivatePodCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodFaultException; + +public class DeactivatePodAction implements OmnipodAction { + private final PodSessionState podState; + private final boolean acknowledgementBeep; + + public DeactivatePodAction(PodSessionState podState, boolean acknowledgementBeep) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + this.podState = podState; + this.acknowledgementBeep = acknowledgementBeep; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + if (!podState.isSuspended() && !podState.hasFaultEvent()) { + try { + communicationService.executeAction(new CancelDeliveryAction(podState, + EnumSet.allOf(DeliveryType.class), acknowledgementBeep)); + } catch(PodFaultException ex) { + // Ignore + } + } + + return communicationService.sendCommand(StatusResponse.class, podState, new DeactivatePodCommand(podState.getCurrentNonce())); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetPodInfoAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetPodInfoAction.java new file mode 100644 index 0000000000..0eef011607 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetPodInfoAction.java @@ -0,0 +1,29 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.GetStatusCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class GetPodInfoAction implements OmnipodAction { + private final PodSessionState podState; + private final PodInfoType podInfoType; + + public GetPodInfoAction(PodSessionState podState, PodInfoType podInfoType) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (podInfoType == null) { + throw new ActionInitializationException("Pod info type cannot be null"); + } + this.podState = podState; + this.podInfoType = podInfoType; + } + + @Override + public PodInfoResponse execute(OmnipodCommunicationManager communicationService) { + return communicationService.sendCommand(PodInfoResponse.class, podState, new GetStatusCommand(podInfoType)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetStatusAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetStatusAction.java new file mode 100644 index 0000000000..dfcc1fe043 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetStatusAction.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.GetStatusCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class GetStatusAction implements OmnipodAction { + private final PodSessionState podState; + + public GetStatusAction(PodSessionState podState) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + this.podState = podState; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + return communicationService.sendCommand(StatusResponse.class, podState, new GetStatusCommand(PodInfoType.NORMAL)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/InsertCannulaAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/InsertCannulaAction.java new file mode 100644 index 0000000000..9a433ab86a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/InsertCannulaAction.java @@ -0,0 +1,71 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.InsertCannulaService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalSetupProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class InsertCannulaAction implements OmnipodAction { + + private final PodSessionState podState; + private final InsertCannulaService service; + private final BasalSchedule initialBasalSchedule; + + public InsertCannulaAction(InsertCannulaService insertCannulaService, PodSessionState podState, BasalSchedule initialBasalSchedule) { + if (insertCannulaService == null) { + throw new ActionInitializationException("Insert cannula service cannot be null"); + } + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (initialBasalSchedule == null) { + throw new ActionInitializationException("Initial basal schedule cannot be null"); + } + this.service = insertCannulaService; + this.podState = podState; + this.initialBasalSchedule = initialBasalSchedule; + } + + public static void updateCannulaInsertionStatus(PodSessionState podState, StatusResponse statusResponse, AAPSLogger aapsLogger) { + if (podState.getSetupProgress().equals(SetupProgress.CANNULA_INSERTING) && + statusResponse.getPodProgressStatus().isReadyForDelivery()) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Updating SetupProgress from CANNULA_INSERTING to COMPLETED"); + podState.setSetupProgress(SetupProgress.COMPLETED); + } + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + if (podState.getSetupProgress().isBefore(SetupProgress.PRIMING_FINISHED)) { + throw new IllegalSetupProgressException(SetupProgress.PRIMING_FINISHED, podState.getSetupProgress()); + } + + if (podState.getSetupProgress().isBefore(SetupProgress.INITIAL_BASAL_SCHEDULE_SET)) { + service.programInitialBasalSchedule(communicationService, podState, initialBasalSchedule); + podState.setSetupProgress(SetupProgress.INITIAL_BASAL_SCHEDULE_SET); + } + if (podState.getSetupProgress().isBefore(SetupProgress.STARTING_INSERT_CANNULA)) { + service.executeExpirationRemindersAlertCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.STARTING_INSERT_CANNULA); + } + + if (podState.getSetupProgress().isBefore(SetupProgress.CANNULA_INSERTING)) { + StatusResponse statusResponse = service.executeInsertionBolusCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.CANNULA_INSERTING); + return statusResponse; + } else if (podState.getSetupProgress().equals(SetupProgress.CANNULA_INSERTING)) { + // Check status + StatusResponse statusResponse = communicationService.executeAction(new GetStatusAction(podState)); + updateCannulaInsertionStatus(podState, statusResponse, communicationService.aapsLogger); + return statusResponse; + } else { + throw new IllegalSetupProgressException(null, podState.getSetupProgress()); + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/OmnipodAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/OmnipodAction.java new file mode 100644 index 0000000000..f561a276d4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/OmnipodAction.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; + +public interface OmnipodAction { + T execute(OmnipodCommunicationManager communicationService); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PrimeAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PrimeAction.java new file mode 100644 index 0000000000..1732ddf533 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PrimeAction.java @@ -0,0 +1,61 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.PrimeService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalSetupProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class PrimeAction implements OmnipodAction { + + private final PrimeService service; + private final PodSessionState podState; + + public PrimeAction(PrimeService primeService, PodSessionState podState) { + if (primeService == null) { + throw new ActionInitializationException("Prime service cannot be null"); + } + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + this.service = primeService; + this.podState = podState; + } + + public static void updatePrimingStatus(PodSessionState podState, StatusResponse statusResponse, AAPSLogger aapsLogger) { + if (podState.getSetupProgress().equals(SetupProgress.PRIMING) && statusResponse.getPodProgressStatus().equals(PodProgressStatus.READY_FOR_BASAL_SCHEDULE)) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Updating SetupProgress from PRIMING to PRIMING_FINISHED"); + podState.setSetupProgress(SetupProgress.PRIMING_FINISHED); + } + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + if (podState.getSetupProgress().isBefore(SetupProgress.POD_CONFIGURED)) { + throw new IllegalSetupProgressException(SetupProgress.POD_CONFIGURED, podState.getSetupProgress()); + } + if (podState.getSetupProgress().isBefore(SetupProgress.STARTING_PRIME)) { + service.executeDisableTab5Sub16FaultConfigCommand(communicationService, podState); + service.executeFinishSetupReminderAlertCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.STARTING_PRIME); + } + + if (podState.getSetupProgress().isBefore(SetupProgress.PRIMING)) { + StatusResponse statusResponse = service.executePrimeBolusCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.PRIMING); + return statusResponse; + } else if (podState.getSetupProgress().equals(SetupProgress.PRIMING)) { + // Check status + StatusResponse statusResponse = communicationService.executeAction(new GetStatusAction(podState)); + updatePrimingStatus(podState, statusResponse, communicationService.aapsLogger); + return statusResponse; + } else { + throw new IllegalSetupProgressException(null, podState.getSetupProgress()); + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetBasalScheduleAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetBasalScheduleAction.java new file mode 100644 index 0000000000..0c8ae7f8be --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetBasalScheduleAction.java @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.Duration; + +import java.util.Arrays; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.BasalScheduleExtraCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.SetInsulinScheduleCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class SetBasalScheduleAction implements OmnipodAction { + private final PodSessionState podState; + private final BasalSchedule basalSchedule; + private final boolean confidenceReminder; + private final Duration scheduleOffset; + private final boolean acknowledgementBeep; + + public SetBasalScheduleAction(PodSessionState podState, BasalSchedule basalSchedule, + boolean confidenceReminder, Duration scheduleOffset, boolean acknowledgementBeep) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (basalSchedule == null) { + throw new ActionInitializationException("Basal schedule cannot be null"); + } + if (scheduleOffset == null) { + throw new ActionInitializationException("Schedule offset cannot be null"); + } + this.podState = podState; + this.basalSchedule = basalSchedule; + this.confidenceReminder = confidenceReminder; + this.scheduleOffset = scheduleOffset; + this.acknowledgementBeep = acknowledgementBeep; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + SetInsulinScheduleCommand setBasal = new SetInsulinScheduleCommand(podState.getCurrentNonce(), basalSchedule, scheduleOffset); + BasalScheduleExtraCommand extraCommand = new BasalScheduleExtraCommand(basalSchedule, scheduleOffset, + acknowledgementBeep, confidenceReminder, Duration.ZERO); + OmnipodMessage basalMessage = new OmnipodMessage(podState.getAddress(), Arrays.asList(setBasal, extraCommand), + podState.getMessageNumber()); + + StatusResponse statusResponse = communicationService.exchangeMessages(StatusResponse.class, podState, basalMessage); + podState.setBasalSchedule(basalSchedule); + return statusResponse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetTempBasalAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetTempBasalAction.java new file mode 100644 index 0000000000..d9406a3ed7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetTempBasalAction.java @@ -0,0 +1,48 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.Duration; + +import java.util.Arrays; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.SetInsulinScheduleCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.TempBasalExtraCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class SetTempBasalAction implements OmnipodAction { + private final PodSessionState podState; + private final double rate; + private final Duration duration; + private final boolean acknowledgementBeep; + private final boolean completionBeep; + + public SetTempBasalAction(PodSessionState podState, double rate, Duration duration, + boolean acknowledgementBeep, boolean completionBeep) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (duration == null) { + throw new ActionInitializationException("Duration cannot be null"); + } + this.podState = podState; + this.rate = rate; + this.duration = duration; + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + List messageBlocks = Arrays.asList( // + new SetInsulinScheduleCommand(podState.getCurrentNonce(), rate, duration), + new TempBasalExtraCommand(rate, duration, acknowledgementBeep, completionBeep, Duration.ZERO)); + + OmnipodMessage message = new OmnipodMessage(podState.getAddress(), messageBlocks, podState.getMessageNumber()); + return communicationService.exchangeMessages(StatusResponse.class, podState, message); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/InsertCannulaService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/InsertCannulaService.java new file mode 100644 index 0000000000..b897e448c8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/InsertCannulaService.java @@ -0,0 +1,59 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service; + +import org.joda.time.DateTime; +import org.joda.time.Duration; + +import java.util.Arrays; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.BolusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.ConfigureAlertsAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.SetBasalScheduleAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfigurationFactory; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class InsertCannulaService { + public StatusResponse programInitialBasalSchedule(OmnipodCommunicationManager communicationService, + PodSessionState podState, BasalSchedule basalSchedule) { + return communicationService.executeAction(new SetBasalScheduleAction(podState, basalSchedule, + true, podState.getScheduleOffset(), false)); + } + + public StatusResponse executeExpirationRemindersAlertCommand(OmnipodCommunicationManager communicationService, + PodSessionState podState) { + AlertConfiguration lowReservoirAlertConfiguration = AlertConfigurationFactory.createLowReservoirAlertConfiguration(OmnipodConst.LOW_RESERVOIR_ALERT); + + DateTime endOfServiceTime = podState.getActivatedAt().plus(OmnipodConst.SERVICE_DURATION); + + Duration timeUntilExpirationAdvisoryAlarm = new Duration(DateTime.now(), + endOfServiceTime.minus(OmnipodConst.EXPIRATION_ADVISORY_WINDOW)); + Duration timeUntilShutdownImminentAlarm = new Duration(DateTime.now(), + endOfServiceTime.minus(OmnipodConst.END_OF_SERVICE_IMMINENT_WINDOW)); + + AlertConfiguration expirationAdvisoryAlertConfiguration = AlertConfigurationFactory.createExpirationAdvisoryAlertConfiguration( + timeUntilExpirationAdvisoryAlarm, OmnipodConst.EXPIRATION_ADVISORY_WINDOW); + AlertConfiguration shutdownImminentAlertConfiguration = AlertConfigurationFactory.createShutdownImminentAlertConfiguration( + timeUntilShutdownImminentAlarm); + AlertConfiguration autoOffAlertConfiguration = AlertConfigurationFactory.createAutoOffAlertConfiguration( + false, Duration.ZERO); + + List alertConfigurations = Arrays.asList( // + lowReservoirAlertConfiguration, // + expirationAdvisoryAlertConfiguration, // + shutdownImminentAlertConfiguration, // + autoOffAlertConfiguration // + ); + + return new ConfigureAlertsAction(podState, alertConfigurations).execute(communicationService); + } + + public StatusResponse executeInsertionBolusCommand(OmnipodCommunicationManager communicationService, PodSessionState podState) { + return communicationService.executeAction(new BolusAction(podState, OmnipodConst.POD_CANNULA_INSERTION_BOLUS_UNITS, + Duration.standardSeconds(1), false, false)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PrimeService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PrimeService.java new file mode 100644 index 0000000000..3fc7663398 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PrimeService.java @@ -0,0 +1,37 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service; + +import org.joda.time.Duration; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.BolusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.ConfigureAlertsAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.FaultConfigCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfigurationFactory; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class PrimeService { + + public StatusResponse executeDisableTab5Sub16FaultConfigCommand(OmnipodCommunicationManager communicationService, PodSessionState podState) { + FaultConfigCommand faultConfigCommand = new FaultConfigCommand(podState.getCurrentNonce(), (byte) 0x00, (byte) 0x00); + OmnipodMessage faultConfigMessage = new OmnipodMessage(podState.getAddress(), + Collections.singletonList(faultConfigCommand), podState.getMessageNumber()); + return communicationService.exchangeMessages(StatusResponse.class, podState, faultConfigMessage); + } + + public StatusResponse executeFinishSetupReminderAlertCommand(OmnipodCommunicationManager communicationService, PodSessionState podState) { + AlertConfiguration finishSetupReminderAlertConfiguration = AlertConfigurationFactory.createFinishSetupReminderAlertConfiguration(); + return communicationService.executeAction(new ConfigureAlertsAction(podState, + Collections.singletonList(finishSetupReminderAlertConfiguration))); + } + + public StatusResponse executePrimeBolusCommand(OmnipodCommunicationManager communicationService, PodSessionState podState) { + return communicationService.executeAction(new BolusAction(podState, OmnipodConst.POD_PRIME_BOLUS_UNITS, + Duration.standardSeconds(1), false, false)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/ActionInitializationException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/ActionInitializationException.java new file mode 100644 index 0000000000..5682b0c6c7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/ActionInitializationException.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class ActionInitializationException extends OmnipodException { + public ActionInitializationException(String message) { + super(message, true); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommandInitializationException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommandInitializationException.java new file mode 100644 index 0000000000..cb0553f292 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommandInitializationException.java @@ -0,0 +1,13 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class CommandInitializationException extends OmnipodException { + public CommandInitializationException(String message) { + super(message, true); + } + + public CommandInitializationException(String message, Throwable cause) { + super(message, cause, true); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommunicationException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommunicationException.java new file mode 100644 index 0000000000..c7392fd42e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommunicationException.java @@ -0,0 +1,36 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class CommunicationException extends OmnipodException { + private final Type type; + + public CommunicationException(Type type) { + super(type.getDescription(), false); + this.type = type; + } + + public CommunicationException(Type type, Throwable cause) { + super(type.getDescription() + ": "+ cause, cause, false); + this.type = type; + } + + public Type getType() { + return type; + } + + public enum Type { + TIMEOUT("Communication timeout"), + UNEXPECTED_EXCEPTION("Caught an unexpected Exception"); + + private final String description; + + Type(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CrcMismatchException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CrcMismatchException.java new file mode 100644 index 0000000000..3de98e6f49 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CrcMismatchException.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class CrcMismatchException extends OmnipodException { + private final int expected; + private final int actual; + + public CrcMismatchException(int expected, int actual) { + super(String.format(Locale.getDefault(), "CRC mismatch: expected %d, got %d", expected, actual), false); + this.expected = expected; + this.actual = actual; + } + + public int getExpected() { + return expected; + } + + public int getActual() { + return actual; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalDeliveryStatusException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalDeliveryStatusException.java new file mode 100644 index 0000000000..01bf4db04f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalDeliveryStatusException.java @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class IllegalDeliveryStatusException extends OmnipodException { + private final DeliveryStatus expected; + private final DeliveryStatus actual; + + public IllegalDeliveryStatusException(DeliveryStatus expected, DeliveryStatus actual) { + super(String.format(Locale.getDefault(), "Illegal delivery status: %s, expected: %s", actual, expected), true); + this.expected = expected; + this.actual = actual; + } + + public DeliveryStatus getExpected() { + return expected; + } + + public DeliveryStatus getActual() { + return actual; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPacketTypeException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPacketTypeException.java new file mode 100644 index 0000000000..9856d3368c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPacketTypeException.java @@ -0,0 +1,27 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PacketType; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class IllegalPacketTypeException extends OmnipodException { + private final PacketType expected; + private final PacketType actual; + + public IllegalPacketTypeException(PacketType expected, PacketType actual) { + super(String.format(Locale.getDefault(), "Illegal packet type: %s, expected %s", + actual, expected), false); + this.expected = expected; + this.actual = actual; + } + + public PacketType getExpected() { + return expected; + } + + public PacketType getActual() { + return actual; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPodProgressException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPodProgressException.java new file mode 100644 index 0000000000..e4e575405e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPodProgressException.java @@ -0,0 +1,25 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class IllegalPodProgressException extends OmnipodException { + private final PodProgressStatus expected; + private final PodProgressStatus actual; + + public IllegalPodProgressException(PodProgressStatus expected, PodProgressStatus actual) { + super(String.format(Locale.getDefault(), "Illegal setup state: %s, expected: %s", actual, expected), true); + this.expected = expected; + this.actual = actual; + } + + public PodProgressStatus getExpected() { + return expected; + } + + public PodProgressStatus getActual() { + return actual; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalResponseException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalResponseException.java new file mode 100644 index 0000000000..647309c478 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalResponseException.java @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class IllegalResponseException extends OmnipodException { + private final String actualClass; + private final MessageBlockType expectedType; + + public IllegalResponseException(String actualClass, MessageBlockType expectedType) { + super(String.format(Locale.getDefault(), "Illegal response type: got class of type %s " + + "for message block type %s", actualClass, expectedType), false); + this.actualClass = actualClass; + this.expectedType = expectedType; + } + + public String getActualClass() { + return actualClass; + } + + public MessageBlockType getExpectedType() { + return expectedType; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalSetupProgressException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalSetupProgressException.java new file mode 100644 index 0000000000..852d5ea3c2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalSetupProgressException.java @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class IllegalSetupProgressException extends OmnipodException { + private final SetupProgress expected; + private final SetupProgress actual; + + public IllegalSetupProgressException(SetupProgress expected, SetupProgress actual) { + super(String.format(Locale.getDefault(), "Illegal setup progress: %s, expected: %s", actual, expected), true); + this.expected = expected; + this.actual = actual; + } + + public SetupProgress getExpected() { + return expected; + } + + public SetupProgress getActual() { + return actual; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/MessageDecodingException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/MessageDecodingException.java new file mode 100644 index 0000000000..5e1c4f3523 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/MessageDecodingException.java @@ -0,0 +1,13 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class MessageDecodingException extends OmnipodException { + public MessageDecodingException(String message) { + super(message, false); + } + + public MessageDecodingException(String message, Throwable cause) { + super(message, cause, false); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceOutOfSyncException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceOutOfSyncException.java new file mode 100644 index 0000000000..929afbb855 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceOutOfSyncException.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class NonceOutOfSyncException extends OmnipodException { + public NonceOutOfSyncException() { + super("Nonce out of sync", true); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceResyncException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceResyncException.java new file mode 100644 index 0000000000..a4910c9a42 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceResyncException.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class NonceResyncException extends OmnipodException { + public NonceResyncException() { + super("Nonce resync failed", true); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NotEnoughDataException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NotEnoughDataException.java new file mode 100644 index 0000000000..728fa4d734 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NotEnoughDataException.java @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class NotEnoughDataException extends OmnipodException { + private final byte[] data; + + public NotEnoughDataException(byte[] data) { + super("Not enough data: " + ByteUtil.shortHexString(data), false); + this.data = data; + } + + public byte[] getData() { + return data; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodFaultException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodFaultException.java new file mode 100644 index 0000000000..6269f1c39c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodFaultException.java @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class PodFaultException extends OmnipodException { + private final PodInfoFaultEvent faultEvent; + + public PodFaultException(PodInfoFaultEvent faultEvent) { + super(faultEvent.getFaultEventType().toString(), true); + this.faultEvent = faultEvent; + } + + public PodInfoFaultEvent getFaultEvent() { + return faultEvent; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodReturnedErrorResponseException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodReturnedErrorResponseException.java new file mode 100644 index 0000000000..f43987b755 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodReturnedErrorResponseException.java @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.ErrorResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class PodReturnedErrorResponseException extends OmnipodException { + private final ErrorResponse errorResponse; + + public PodReturnedErrorResponseException(ErrorResponse errorResponse) { + super("Pod returned error response: " + errorResponse.getErrorResponseType(), true); + this.errorResponse = errorResponse; + } + + public ErrorResponse getErrorResponse() { + return errorResponse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/IRawRepresentable.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/IRawRepresentable.java new file mode 100644 index 0000000000..2eec2afaf8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/IRawRepresentable.java @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +public interface IRawRepresentable { + byte[] getRawData(); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/MessageBlock.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/MessageBlock.java new file mode 100644 index 0000000000..dba0035df7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/MessageBlock.java @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public abstract class MessageBlock { + protected byte[] encodedData = new byte[0]; + + public MessageBlock() { + } + + public abstract MessageBlockType getType(); + + //This method returns raw message representation + //It should be rewritten in a derived class if raw representation of a concrete message + //is something else than just message type concatenated with message data + public byte[] getRawData() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try { + stream.write(this.getType().getValue()); + stream.write((byte) encodedData.length); + stream.write(encodedData); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return stream.toByteArray(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/NonceResyncableMessageBlock.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/NonceResyncableMessageBlock.java new file mode 100644 index 0000000000..6170164366 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/NonceResyncableMessageBlock.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +public abstract class NonceResyncableMessageBlock extends MessageBlock { + public abstract int getNonce(); + + public abstract void setNonce(int nonce); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodMessage.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodMessage.java new file mode 100644 index 0000000000..c328997172 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodMessage.java @@ -0,0 +1,148 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.GetStatusCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CrcMismatchException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.MessageDecodingException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NotEnoughDataException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmniCRC; + +public class OmnipodMessage { + + private final int address; + private final List messageBlocks; + private final int sequenceNumber; + + public OmnipodMessage(OmnipodMessage other) { + address = other.address; + messageBlocks = new ArrayList<>(other.messageBlocks); + sequenceNumber = other.sequenceNumber; + } + + public OmnipodMessage(int address, List messageBlocks, int sequenceNumber) { + this.address = address; + this.messageBlocks = messageBlocks; + this.sequenceNumber = sequenceNumber; + } + + public static OmnipodMessage decodeMessage(byte[] data) { + if (data.length < 10) { + throw new NotEnoughDataException(data); + } + + int address = ByteUtil.toInt((int) data[0], (int) data[1], (int) data[2], + (int) data[3], ByteUtil.BitConversion.BIG_ENDIAN); + byte b9 = data[4]; + int bodyLength = ByteUtil.convertUnsignedByteToInt(data[5]); + if (data.length - 8 < bodyLength) { + throw new NotEnoughDataException(data); + } + int sequenceNumber = (((int) b9 >> 2) & 0b11111); + int crc = ByteUtil.toInt(data[data.length - 2], data[data.length - 1]); + int calculatedCrc = OmniCRC.crc16(ByteUtil.substring(data, 0, data.length - 2)); + if (crc != calculatedCrc) { + throw new CrcMismatchException(calculatedCrc, crc); + } + List blocks = decodeBlocks(ByteUtil.substring(data, 6, data.length - 6 - 2)); + if (blocks == null || blocks.size() == 0) { + throw new MessageDecodingException("No blocks decoded"); + } + + return new OmnipodMessage(address, blocks, sequenceNumber); + } + + private static List decodeBlocks(byte[] data) { + List blocks = new ArrayList<>(); + int index = 0; + while (index < data.length) { + try { + MessageBlockType blockType = MessageBlockType.fromByte(data[index]); + MessageBlock block = blockType.decode(ByteUtil.substring(data, index)); + blocks.add(block); + int blockLength = block.getRawData().length; + index += blockLength; + } catch (Exception ex) { + throw new MessageDecodingException("Failed to decode blocks", ex); + } + } + + return blocks; + } + + public byte[] getEncoded() { + byte[] encodedData = new byte[0]; + for (MessageBlock messageBlock : messageBlocks) { + encodedData = ByteUtil.concat(encodedData, messageBlock.getRawData()); + } + + byte[] header = new byte[0]; + //right before the message blocks we have 6 bits of seqNum and 10 bits of length + header = ByteUtil.concat(header, ByteUtil.getBytesFromInt(address)); + header = ByteUtil.concat(header, (byte) (((sequenceNumber & 0x1F) << 2) + ((encodedData.length >> 8) & 0x03))); + header = ByteUtil.concat(header, (byte) (encodedData.length & 0xFF)); + encodedData = ByteUtil.concat(header, encodedData); + int crc = OmniCRC.crc16(encodedData); + encodedData = ByteUtil.concat(encodedData, ByteUtil.substring(ByteUtil.getBytesFromInt(crc), 2, 2)); + return encodedData; + } + + public void padWithGetStatusCommands(int packetSize) { + while (getEncoded().length < packetSize) { + messageBlocks.add(new GetStatusCommand(PodInfoType.NORMAL)); + } + } + + public List getMessageBlocks() { + return messageBlocks; + } + + public int getSequenceNumber() { + return sequenceNumber; + } + + public boolean isNonceResyncable() { + return containsBlock(NonceResyncableMessageBlock.class); + } + + public int getSentNonce() { + for (MessageBlock messageBlock : messageBlocks) { + if (messageBlock instanceof NonceResyncableMessageBlock) { + return ((NonceResyncableMessageBlock) messageBlock).getNonce(); + } + } + throw new UnsupportedOperationException("Message is not nonce resyncable"); + } + + public void resyncNonce(int nonce) { + for (MessageBlock messageBlock : messageBlocks) { + if (messageBlock instanceof NonceResyncableMessageBlock) { + ((NonceResyncableMessageBlock) messageBlock).setNonce(nonce); + } + } + } + + public boolean containsBlock(Class blockType) { + for (MessageBlock messageBlock : messageBlocks) { + if (blockType.isInstance(messageBlock)) { + return true; + } + } + return false; + } + + + @Override + public String toString() { + return "OmnipodMessage{" + + "address=" + address + + ", messageBlocks=" + messageBlocks + + ", encoded=" + ByteUtil.shortHexStringWithoutSpaces(getEncoded()) + + ", sequenceNumber=" + sequenceNumber + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodPacket.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodPacket.java new file mode 100644 index 0000000000..fd90ffb640 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodPacket.java @@ -0,0 +1,82 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.RLMessage; +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PacketType; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CrcMismatchException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPacketTypeException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmniCRC; + +/** + * Created by andy on 6/1/18. + */ +public class OmnipodPacket implements RLMessage { + private int packetAddress = 0; + private PacketType packetType = PacketType.INVALID; + private int sequenceNumber = 0; + private byte[] encodedMessage = null; + private boolean valid = false; + + public OmnipodPacket(byte[] encoded) { + if (encoded.length < 7) { + return; + } + this.packetAddress = ByteUtil.toInt((int) encoded[0], (int) encoded[1], + (int) encoded[2], (int) encoded[3], ByteUtil.BitConversion.BIG_ENDIAN); + try { + this.packetType = PacketType.fromByte((byte) (((int) encoded[4] & 0xFF) >> 5)); + } catch (IllegalArgumentException ex) { + throw new IllegalPacketTypeException(null, null); + } + this.sequenceNumber = (encoded[4] & 0b11111); + byte crc = OmniCRC.crc8(ByteUtil.substring(encoded, 0, encoded.length - 1)); + if (crc != encoded[encoded.length - 1]) { + throw new CrcMismatchException(crc, encoded[encoded.length - 1]); + } + this.encodedMessage = ByteUtil.substring(encoded, 5, encoded.length - 1 - 5); + valid = true; + } + + public OmnipodPacket(int packetAddress, PacketType packetType, int packetNumber, byte[] encodedMessage) { + this.packetAddress = packetAddress; + this.packetType = packetType; + this.sequenceNumber = packetNumber; + this.encodedMessage = encodedMessage; + if (encodedMessage.length > packetType.getMaxBodyLength()) { + this.encodedMessage = ByteUtil.substring(encodedMessage, 0, packetType.getMaxBodyLength()); + } + this.valid = true; + } + + public PacketType getPacketType() { + return packetType; + } + + public int getAddress() { + return packetAddress; + } + + public int getSequenceNumber() { + return sequenceNumber; + } + + public byte[] getEncodedMessage() { + return encodedMessage; + } + + @Override + public byte[] getTxData() { + byte[] output = new byte[0]; + output = ByteUtil.concat(output, ByteUtil.getBytesFromInt(this.packetAddress)); + output = ByteUtil.concat(output, (byte) ((this.packetType.getValue() << 5) + (sequenceNumber & 0b11111))); + output = ByteUtil.concat(output, encodedMessage); + output = ByteUtil.concat(output, OmniCRC.crc8(output)); + return output; + } + + @Override + public boolean isValid() { + return valid; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommand.java new file mode 100644 index 0000000000..f429ed8b6f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommand.java @@ -0,0 +1,54 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class AcknowledgeAlertsCommand extends NonceResyncableMessageBlock { + + private final AlertSet alerts; + private int nonce; + + public AcknowledgeAlertsCommand(int nonce, AlertSet alerts) { + this.nonce = nonce; + this.alerts = alerts; + encode(); + } + + public AcknowledgeAlertsCommand(int nonce, AlertSlot alertSlot) { + this(nonce, new AlertSet(Collections.singletonList(alertSlot))); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.ACKNOWLEDGE_ALERT; + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + encodedData = ByteUtil.concat(encodedData, alerts.getRawValue()); + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "AcknowledgeAlertsCommand{" + + "alerts=" + alerts + + ", nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommand.java new file mode 100644 index 0000000000..e7a942885c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommand.java @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.nio.ByteBuffer; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class AssignAddressCommand extends MessageBlock { + private final int address; + + public AssignAddressCommand(int address) { + this.address = address; + encodedData = ByteBuffer.allocate(4).putInt(this.address).array(); + } + + public int getAddress() { + return address; + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.ASSIGN_ADDRESS; + } + + @Override + public String toString() { + return "AssignAddressCommand{" + + "address=" + address + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommand.java new file mode 100644 index 0000000000..5d628537f9 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommand.java @@ -0,0 +1,127 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.RateEntry; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BasalScheduleExtraCommand extends MessageBlock { + private final boolean acknowledgementBeep; + private final boolean completionBeep; + private final Duration programReminderInterval; + private final byte currentEntryIndex; + private final double remainingPulses; + // We use a double for the delay between pulses because the Joda time API lacks precision for our calculations + private final double delayUntilNextTenthOfPulseInSeconds; + private final List rateEntries; + + public BasalScheduleExtraCommand(boolean acknowledgementBeep, boolean completionBeep, + Duration programReminderInterval, byte currentEntryIndex, + double remainingPulses, double delayUntilNextTenthOfPulseInSeconds, List rateEntries) { + + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + this.currentEntryIndex = currentEntryIndex; + this.remainingPulses = remainingPulses; + this.delayUntilNextTenthOfPulseInSeconds = delayUntilNextTenthOfPulseInSeconds; + this.rateEntries = rateEntries; + encode(); + } + + public BasalScheduleExtraCommand(BasalSchedule schedule, Duration scheduleOffset, + boolean acknowledgementBeep, boolean completionBeep, Duration programReminderInterval) { + rateEntries = new ArrayList<>(); + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + Duration scheduleOffsetNearestSecond = Duration.standardSeconds(Math.round(scheduleOffset.getMillis() / 1000.0)); + + BasalSchedule mergedSchedule = new BasalSchedule(schedule.adjacentEqualRatesMergedEntries()); + List durations = mergedSchedule.getDurations(); + + for (BasalSchedule.BasalScheduleDurationEntry entry : durations) { + rateEntries.addAll(RateEntry.createEntries(entry.getRate(), entry.getDuration())); + } + + BasalSchedule.BasalScheduleLookupResult entryLookupResult = mergedSchedule.lookup(scheduleOffsetNearestSecond); + currentEntryIndex = (byte) entryLookupResult.getIndex(); + double timeRemainingInEntryInSeconds = entryLookupResult.getStartTime().minus(scheduleOffsetNearestSecond.minus(entryLookupResult.getDuration())).getMillis() / 1000.0; + double rate = mergedSchedule.rateAt(scheduleOffsetNearestSecond); + int pulsesPerHour = (int) Math.round(rate / OmnipodConst.POD_PULSE_SIZE); + double timeBetweenPulses = 3600.0 / pulsesPerHour; + delayUntilNextTenthOfPulseInSeconds = (timeRemainingInEntryInSeconds % (timeBetweenPulses / 10.0)); + remainingPulses = pulsesPerHour * (timeRemainingInEntryInSeconds - delayUntilNextTenthOfPulseInSeconds) / 3600.0 + 0.1; + + encode(); + } + + private void encode() { + byte beepOptions = (byte) ((programReminderInterval.getStandardMinutes() & 0x3f) + (completionBeep ? 1 << 6 : 0) + (acknowledgementBeep ? 1 << 7 : 0)); + + encodedData = new byte[]{ + beepOptions, + currentEntryIndex + }; + + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16((int) Math.round(remainingPulses * 10))); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) Math.round(delayUntilNextTenthOfPulseInSeconds * 1000 * 1000))); + + for (RateEntry entry : rateEntries) { + encodedData = ByteUtil.concat(encodedData, entry.getRawData()); + } + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.BASAL_SCHEDULE_EXTRA; + } + + public boolean isAcknowledgementBeep() { + return acknowledgementBeep; + } + + public boolean isCompletionBeep() { + return completionBeep; + } + + public Duration getProgramReminderInterval() { + return programReminderInterval; + } + + public byte getCurrentEntryIndex() { + return currentEntryIndex; + } + + public double getRemainingPulses() { + return remainingPulses; + } + + public double getDelayUntilNextTenthOfPulseInSeconds() { + return delayUntilNextTenthOfPulseInSeconds; + } + + public List getRateEntries() { + return new ArrayList<>(rateEntries); + } + + @Override + public String toString() { + return "BasalScheduleExtraCommand{" + + "acknowledgementBeep=" + acknowledgementBeep + + ", completionBeep=" + completionBeep + + ", programReminderInterval=" + programReminderInterval + + ", currentEntryIndex=" + currentEntryIndex + + ", remainingPulses=" + remainingPulses + + ", delayUntilNextTenthOfPulseInSeconds=" + delayUntilNextTenthOfPulseInSeconds + + ", rateEntries=" + rateEntries + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommand.java new file mode 100644 index 0000000000..9695548f85 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommand.java @@ -0,0 +1,61 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepConfigType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class BeepConfigCommand extends MessageBlock { + private final BeepConfigType beepType; + private final boolean basalCompletionBeep; + private final Duration basalIntervalBeep; + private final boolean tempBasalCompletionBeep; + private final Duration tempBasalIntervalBeep; + private final boolean bolusCompletionBeep; + private final Duration bolusIntervalBeep; + + public BeepConfigCommand(BeepConfigType beepType, boolean basalCompletionBeep, Duration basalIntervalBeep, + boolean tempBasalCompletionBeep, Duration tempBasalIntervalBeep, + boolean bolusCompletionBeep, Duration bolusIntervalBeep) { + this.beepType = beepType; + this.basalCompletionBeep = basalCompletionBeep; + this.basalIntervalBeep = basalIntervalBeep; + this.tempBasalCompletionBeep = tempBasalCompletionBeep; + this.tempBasalIntervalBeep = tempBasalIntervalBeep; + this.bolusCompletionBeep = bolusCompletionBeep; + this.bolusIntervalBeep = bolusIntervalBeep; + + encode(); + } + + public BeepConfigCommand(BeepConfigType beepType) { + this(beepType, false, Duration.ZERO, false, Duration.ZERO, false, Duration.ZERO); + } + + private void encode() { + encodedData = new byte[]{beepType.getValue()}; + encodedData = ByteUtil.concat(encodedData, (byte) ((basalCompletionBeep ? (1 << 6) : 0) + (basalIntervalBeep.getStandardMinutes() & 0x3f))); + encodedData = ByteUtil.concat(encodedData, (byte) ((tempBasalCompletionBeep ? (1 << 6) : 0) + (tempBasalIntervalBeep.getStandardMinutes() & 0x3f))); + encodedData = ByteUtil.concat(encodedData, (byte) ((bolusCompletionBeep ? (1 << 6) : 0) + (bolusIntervalBeep.getStandardMinutes() & 0x3f))); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.BEEP_CONFIG; + } + + @Override + public String toString() { + return "BeepConfigCommand{" + + "beepType=" + beepType + + ", basalCompletionBeep=" + basalCompletionBeep + + ", basalIntervalBeep=" + basalIntervalBeep + + ", tempBasalCompletionBeep=" + tempBasalCompletionBeep + + ", tempBasalIntervalBeep=" + tempBasalIntervalBeep + + ", bolusCompletionBeep=" + bolusCompletionBeep + + ", bolusIntervalBeep=" + bolusIntervalBeep + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommand.java new file mode 100644 index 0000000000..3554f85e0a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommand.java @@ -0,0 +1,76 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommandInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BolusExtraCommand extends MessageBlock { + private final boolean acknowledgementBeep; + private final boolean completionBeep; + private final Duration programReminderInterval; + private final double units; + private final Duration timeBetweenPulses; + private final double squareWaveUnits; + private final Duration squareWaveDuration; + + public BolusExtraCommand(double units, boolean acknowledgementBeep, boolean completionBeep) { + this(units, Duration.standardSeconds(2), acknowledgementBeep, completionBeep); + } + + public BolusExtraCommand(double units, Duration timeBetweenPulses, boolean acknowledgementBeep, boolean completionBeep) { + this(units, 0.0, Duration.ZERO, acknowledgementBeep, completionBeep, Duration.ZERO, timeBetweenPulses); + } + + public BolusExtraCommand(double units, double squareWaveUnits, Duration squareWaveDuration, + boolean acknowledgementBeep, boolean completionBeep, + Duration programReminderInterval, Duration timeBetweenPulses) { + if (units <= 0D) { + throw new CommandInitializationException("Units should be > 0"); + } else if (units > OmnipodConst.MAX_BOLUS) { + throw new CommandInitializationException("Units exceeds max bolus"); + } + this.units = units; + this.squareWaveUnits = squareWaveUnits; + this.squareWaveDuration = squareWaveDuration; + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + this.timeBetweenPulses = timeBetweenPulses; + encode(); + } + + private void encode() { + byte beepOptions = (byte) ((programReminderInterval.getStandardMinutes() & 0x3f) + (completionBeep ? 1 << 6 : 0) + (acknowledgementBeep ? 1 << 7 : 0)); + + int squareWavePulseCountCountX10 = (int) Math.round(squareWaveUnits * 200); + int timeBetweenExtendedPulses = squareWavePulseCountCountX10 > 0 ? (int) squareWaveDuration.getMillis() * 100 / squareWavePulseCountCountX10 : 0; + + encodedData = ByteUtil.concat(encodedData, beepOptions); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16((int) Math.round(units * 200))); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) timeBetweenPulses.getMillis() * 100)); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(squareWavePulseCountCountX10)); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(timeBetweenExtendedPulses)); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.BOLUS_EXTRA; + } + + @Override + public String toString() { + return "BolusExtraCommand{" + + "acknowledgementBeep=" + acknowledgementBeep + + ", completionBeep=" + completionBeep + + ", programReminderInterval=" + programReminderInterval + + ", units=" + units + + ", timeBetweenPulses=" + timeBetweenPulses + + ", squareWaveUnits=" + squareWaveUnits + + ", squareWaveDuration=" + squareWaveDuration + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommand.java new file mode 100644 index 0000000000..451f9205f6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommand.java @@ -0,0 +1,71 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.util.EnumSet; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class CancelDeliveryCommand extends NonceResyncableMessageBlock { + + private final BeepType beepType; + private final EnumSet deliveryTypes; + private int nonce; + + public CancelDeliveryCommand(int nonce, BeepType beepType, EnumSet deliveryTypes) { + this.nonce = nonce; + this.beepType = beepType; + this.deliveryTypes = deliveryTypes; + encode(); + } + + public CancelDeliveryCommand(int nonce, BeepType beepType, DeliveryType deliveryType) { + this(nonce, beepType, EnumSet.of(deliveryType)); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.CANCEL_DELIVERY; + } + + private void encode() { + encodedData = new byte[5]; + System.arraycopy(ByteUtil.getBytesFromInt(nonce), 0, encodedData, 0, 4); + byte beepTypeValue = beepType.getValue(); + if (beepTypeValue > 8) { + beepTypeValue = 0; + } + encodedData[4] = (byte) ((beepTypeValue & 0x0F) << 4); + if (deliveryTypes.contains(DeliveryType.BASAL)) { + encodedData[4] |= 1; + } + if (deliveryTypes.contains(DeliveryType.TEMP_BASAL)) { + encodedData[4] |= 2; + } + if (deliveryTypes.contains(DeliveryType.BOLUS)) { + encodedData[4] |= 4; + } + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "CancelDeliveryCommand{" + + "beepType=" + beepType + + ", deliveryTypes=" + deliveryTypes + + ", nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommand.java new file mode 100644 index 0000000000..ac0a6a9f67 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommand.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class ConfigureAlertsCommand extends NonceResyncableMessageBlock { + private final List configurations; + private int nonce; + + public ConfigureAlertsCommand(int nonce, List configurations) { + this.nonce = nonce; + this.configurations = configurations; + encode(); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.CONFIGURE_ALERTS; + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + for (AlertConfiguration config : configurations) { + encodedData = ByteUtil.concat(encodedData, config.getRawData()); + } + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "ConfigureAlertsCommand{" + + "configurations=" + configurations + + ", nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommand.java new file mode 100644 index 0000000000..0f35a1fa24 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommand.java @@ -0,0 +1,56 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.DateTime; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class ConfigurePodCommand extends MessageBlock { + + private static final byte PACKET_TIMEOUT_LIMIT = 0x04; + + private final int lot; + private final int tid; + private final DateTime date; + private final int address; + + public ConfigurePodCommand(int address, DateTime date, int lot, int tid) { + this.address = address; + this.lot = lot; + this.tid = tid; + this.date = date; + encode(); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.SETUP_POD; + } + + private void encode() { + encodedData = new byte[0]; + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(address)); + encodedData = ByteUtil.concat(encodedData, new byte[]{ // + (byte) 0x14, // unknown + PACKET_TIMEOUT_LIMIT, // + (byte) date.monthOfYear().get(), // + (byte) date.dayOfMonth().get(), // + (byte) (date.year().get() - 2000), // + (byte) date.hourOfDay().get(), // + (byte) date.minuteOfHour().get() // + }); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(lot)); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(tid)); + } + + @Override + public String toString() { + return "ConfigurePodCommand{" + + "lot=" + lot + + ", tid=" + tid + + ", date=" + date + + ", address=" + address + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommand.java new file mode 100644 index 0000000000..554d778d14 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommand.java @@ -0,0 +1,41 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class DeactivatePodCommand extends NonceResyncableMessageBlock { + private int nonce; + + public DeactivatePodCommand(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.DEACTIVATE_POD; + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "DeactivatePodCommand{" + + "nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommand.java new file mode 100644 index 0000000000..9d165583e3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommand.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class FaultConfigCommand extends NonceResyncableMessageBlock { + private final byte tab5sub16; + private final byte tab5sub17; + private int nonce; + + public FaultConfigCommand(int nonce, byte tab5sub16, byte tab5sub17) { + this.nonce = nonce; + this.tab5sub16 = tab5sub16; + this.tab5sub17 = tab5sub17; + + encode(); + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + encodedData = ByteUtil.concat(encodedData, tab5sub16); + encodedData = ByteUtil.concat(encodedData, tab5sub17); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.FAULT_CONFIG; + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "FaultConfigCommand{" + + "tab5sub16=" + tab5sub16 + + ", tab5sub17=" + tab5sub17 + + ", nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommand.java new file mode 100644 index 0000000000..68af71e277 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommand.java @@ -0,0 +1,30 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class GetStatusCommand extends MessageBlock { + private final PodInfoType podInfoType; + + public GetStatusCommand(PodInfoType podInfoType) { + this.podInfoType = podInfoType; + encode(); + } + + private void encode() { + encodedData = new byte[]{podInfoType.getValue()}; + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.GET_STATUS; + } + + @Override + public String toString() { + return "GetStatusCommand{" + + "podInfoType=" + podInfoType + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommand.java new file mode 100644 index 0000000000..301435e6a2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommand.java @@ -0,0 +1,98 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalDeliveryTable; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BolusDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.DeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.TempBasalDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommandInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class SetInsulinScheduleCommand extends NonceResyncableMessageBlock { + + private final DeliverySchedule schedule; + private int nonce; + + // Bolus + public SetInsulinScheduleCommand(int nonce, BolusDeliverySchedule schedule) { + this.nonce = nonce; + this.schedule = schedule; + encode(); + } + + // Basal schedule + public SetInsulinScheduleCommand(int nonce, BasalSchedule schedule, Duration scheduleOffset) { + int scheduleOffsetInSeconds = (int) scheduleOffset.getStandardSeconds(); + + BasalDeliveryTable table = new BasalDeliveryTable(schedule); + double rate = schedule.rateAt(scheduleOffset); + byte segment = (byte) (scheduleOffsetInSeconds / BasalDeliveryTable.SEGMENT_DURATION); + int segmentOffset = scheduleOffsetInSeconds % BasalDeliveryTable.SEGMENT_DURATION; + + int timeRemainingInSegment = BasalDeliveryTable.SEGMENT_DURATION - segmentOffset; + + double timeBetweenPulses = 3600 / (rate / OmnipodConst.POD_PULSE_SIZE); + + double offsetToNextTenth = timeRemainingInSegment % (timeBetweenPulses / 10.0); + + int pulsesRemainingInSegment = (int) ((timeRemainingInSegment + timeBetweenPulses / 10.0 - offsetToNextTenth) / timeBetweenPulses); + + this.nonce = nonce; + this.schedule = new BasalDeliverySchedule(segment, timeRemainingInSegment, pulsesRemainingInSegment, table); + encode(); + } + + // Temp basal + public SetInsulinScheduleCommand(int nonce, double tempBasalRate, Duration duration) { + if (tempBasalRate < 0D) { + throw new CommandInitializationException("Rate should be >= 0"); + } else if (tempBasalRate > OmnipodConst.MAX_BASAL_RATE) { + throw new CommandInitializationException("Rate exceeds max basal rate"); + } + if (duration.isLongerThan(OmnipodConst.MAX_TEMP_BASAL_DURATION)) { + throw new CommandInitializationException("Duration exceeds max temp basal duration"); + } + int pulsesPerHour = (int) Math.round(tempBasalRate / OmnipodConst.POD_PULSE_SIZE); + int pulsesPerSegment = pulsesPerHour / 2; + this.nonce = nonce; + this.schedule = new TempBasalDeliverySchedule(BasalDeliveryTable.SEGMENT_DURATION, pulsesPerSegment, new BasalDeliveryTable(tempBasalRate, duration)); + encode(); + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + encodedData = ByteUtil.concat(encodedData, schedule.getType().getValue()); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(schedule.getChecksum())); + encodedData = ByteUtil.concat(encodedData, schedule.getRawData()); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.SET_INSULIN_SCHEDULE; + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "SetInsulinScheduleCommand{" + + "schedule=" + schedule + + ", nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommand.java new file mode 100644 index 0000000000..212f748bf1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommand.java @@ -0,0 +1,108 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.RateEntry; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommandInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class TempBasalExtraCommand extends MessageBlock { + private final boolean acknowledgementBeep; + private final boolean completionBeep; + private final Duration programReminderInterval; + private final double remainingPulses; + // We use a double for the delay until next pulse because the Joda time API lacks precision for our calculations + private final double delayUntilNextPulse; + private final List rateEntries; + + public TempBasalExtraCommand(double rate, Duration duration, boolean acknowledgementBeep, boolean completionBeep, + Duration programReminderInterval) { + if (rate < 0D) { + throw new CommandInitializationException("Rate should be >= 0"); + } else if (rate > OmnipodConst.MAX_BASAL_RATE) { + throw new CommandInitializationException("Rate exceeds max basal rate"); + } + if (duration.isLongerThan(OmnipodConst.MAX_TEMP_BASAL_DURATION)) { + throw new CommandInitializationException("Duration exceeds max temp basal duration"); + } + + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + + rateEntries = RateEntry.createEntries(rate, duration); + + RateEntry currentRateEntry = rateEntries.get(0); + remainingPulses = currentRateEntry.getTotalPulses(); + delayUntilNextPulse = currentRateEntry.getDelayBetweenPulsesInSeconds(); + + encode(); + } + + private void encode() { + byte beepOptions = (byte) ((programReminderInterval.getStandardMinutes() & 0x3f) + (completionBeep ? 1 << 6 : 0) + (acknowledgementBeep ? 1 << 7 : 0)); + + encodedData = new byte[]{ + beepOptions, + (byte) 0x00 + }; + + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16((int) Math.round(remainingPulses * 10))); + if (remainingPulses == 0) { + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) (delayUntilNextPulse * 1000 * 100) * 10)); + } else { + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) (delayUntilNextPulse * 1000 * 100))); + } + + for (RateEntry entry : rateEntries) { + encodedData = ByteUtil.concat(encodedData, entry.getRawData()); + } + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.TEMP_BASAL_EXTRA; + } + + public boolean isAcknowledgementBeep() { + return acknowledgementBeep; + } + + public boolean isCompletionBeep() { + return completionBeep; + } + + public Duration getProgramReminderInterval() { + return programReminderInterval; + } + + public double getRemainingPulses() { + return remainingPulses; + } + + public double getDelayUntilNextPulse() { + return delayUntilNextPulse; + } + + public List getRateEntries() { + return new ArrayList<>(rateEntries); + } + + @Override + public String toString() { + return "TempBasalExtraCommand{" + + "acknowledgementBeep=" + acknowledgementBeep + + ", completionBeep=" + completionBeep + + ", programReminderInterval=" + programReminderInterval + + ", remainingPulses=" + remainingPulses + + ", delayUntilNextPulse=" + delayUntilNextPulse + + ", rateEntries=" + rateEntries + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponse.java new file mode 100644 index 0000000000..18f02475e1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponse.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.ErrorResponseType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class ErrorResponse extends MessageBlock { + private static final int MESSAGE_LENGTH = 5; + + private final ErrorResponseType errorResponseType; + private final int nonceSearchKey; + + public ErrorResponse(byte[] encodedData) { + if (encodedData.length < MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + this.encodedData = ByteUtil.substring(encodedData, 2, MESSAGE_LENGTH - 2); + + ErrorResponseType errorResponseType = null; + try { + errorResponseType = ErrorResponseType.fromByte(encodedData[2]); + } catch (IllegalArgumentException ex) { + } + + this.errorResponseType = errorResponseType; + this.nonceSearchKey = ByteUtil.makeUnsignedShort((int) encodedData[3], (int) encodedData[4]); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.ERROR_RESPONSE; + } + + public ErrorResponseType getErrorResponseType() { + return errorResponseType; + } + + public int getNonceSearchKey() { + return nonceSearchKey; + } + + @Override + public String toString() { + return "ErrorResponse{" + + "errorResponseType=" + errorResponseType + + ", nonceSearchKey=" + nonceSearchKey + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponse.java new file mode 100644 index 0000000000..a9d831f8ae --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponse.java @@ -0,0 +1,119 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import org.joda.time.Duration; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class StatusResponse extends MessageBlock { + private static final int MESSAGE_LENGTH = 10; + + private final DeliveryStatus deliveryStatus; + private final PodProgressStatus podProgressStatus; + private final Duration timeActive; + private final Double reservoirLevel; + private final double insulinDelivered; + private final double insulinNotDelivered; + private final byte podMessageCounter; + private final AlertSet alerts; + + public StatusResponse(byte[] encodedData) { + if (encodedData.length < MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + this.encodedData = ByteUtil.substring(encodedData, 1, MESSAGE_LENGTH - 1); + + this.deliveryStatus = DeliveryStatus.fromByte((byte) (ByteUtil.convertUnsignedByteToInt(encodedData[1]) >>> 4)); + this.podProgressStatus = PodProgressStatus.fromByte((byte) (encodedData[1] & 0x0F)); + + int minutes = ((encodedData[7] & 0x7F) << 6) | ((encodedData[8] & 0xFC) >>> 2); + this.timeActive = Duration.standardMinutes(minutes); + + int highInsulinBits = (encodedData[2] & 0xF) << 9; + int middleInsulinBits = ByteUtil.convertUnsignedByteToInt(encodedData[3]) << 1; + int lowInsulinBits = ByteUtil.convertUnsignedByteToInt(encodedData[4]) >>> 7; + this.insulinDelivered = OmnipodConst.POD_PULSE_SIZE * (highInsulinBits | middleInsulinBits | lowInsulinBits); + this.podMessageCounter = (byte) ((encodedData[4] >>> 3) & 0xf); + + this.insulinNotDelivered = OmnipodConst.POD_PULSE_SIZE * (((encodedData[4] & 0x03) << 8) | ByteUtil.convertUnsignedByteToInt(encodedData[5])); + this.alerts = new AlertSet((byte) (((encodedData[6] & 0x7f) << 1) | (ByteUtil.convertUnsignedByteToInt(encodedData[7]) >>> 7))); + + double reservoirValue = (((encodedData[8] & 0x3) << 8) + ByteUtil.convertUnsignedByteToInt(encodedData[9])) * OmnipodConst.POD_PULSE_SIZE; + if (reservoirValue > OmnipodConst.MAX_RESERVOIR_READING) { + reservoirLevel = null; + } else { + reservoirLevel = reservoirValue; + } + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.STATUS_RESPONSE; + } + + public DeliveryStatus getDeliveryStatus() { + return deliveryStatus; + } + + public PodProgressStatus getPodProgressStatus() { + return podProgressStatus; + } + + public Duration getTimeActive() { + return timeActive; + } + + public Double getReservoirLevel() { + return reservoirLevel; + } + + public double getInsulinDelivered() { + return insulinDelivered; + } + + public double getInsulinNotDelivered() { + return insulinNotDelivered; + } + + public byte getPodMessageCounter() { + return podMessageCounter; + } + + public AlertSet getAlerts() { + return alerts; + } + + public byte[] getRawData() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try { + stream.write(this.getType().getValue()); + stream.write(encodedData); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return stream.toByteArray(); + } + + @Override + public String toString() { + return "StatusResponse{" + + "deliveryStatus=" + deliveryStatus + + ", podProgressStatus=" + podProgressStatus + + ", timeActive=" + timeActive + + ", reservoirLevel=" + reservoirLevel + + ", insulinDelivered=" + insulinDelivered + + ", insulinNotDelivered=" + insulinNotDelivered + + ", podMessageCounter=" + podMessageCounter + + ", alerts=" + alerts + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponse.java new file mode 100644 index 0000000000..a8c9e9403d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponse.java @@ -0,0 +1,91 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FirmwareVersion; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; + +public class VersionResponse extends MessageBlock { + private final PodProgressStatus podProgressStatus; + private final FirmwareVersion pmVersion; + private final FirmwareVersion piVersion; + private final int lot; + private final int tid; + private final int address; + + public VersionResponse(byte[] encodedData) { + int length = ByteUtil.convertUnsignedByteToInt(encodedData[1]) + 2; + this.encodedData = ByteUtil.substring(encodedData, 2, length - 2); + + boolean extraByte; + byte[] truncatedData; + + switch (length) { + case 0x17: + truncatedData = ByteUtil.substring(encodedData, 2); + extraByte = true; + break; + case 0x1D: + truncatedData = ByteUtil.substring(encodedData, 9); + extraByte = false; + break; + default: + throw new IllegalArgumentException("Unrecognized VersionResponse message length: " + length); + } + + this.podProgressStatus = PodProgressStatus.fromByte(truncatedData[7]); + this.pmVersion = new FirmwareVersion(truncatedData[0], truncatedData[1], truncatedData[2]); + this.piVersion = new FirmwareVersion(truncatedData[3], truncatedData[4], truncatedData[5]); + this.lot = ByteUtil.toInt((int) truncatedData[8], (int) truncatedData[9], + (int) truncatedData[10], (int) truncatedData[11], ByteUtil.BitConversion.BIG_ENDIAN); + this.tid = ByteUtil.toInt((int) truncatedData[12], (int) truncatedData[13], + (int) truncatedData[14], (int) truncatedData[15], ByteUtil.BitConversion.BIG_ENDIAN); + + int indexIncrementor = extraByte ? 1 : 0; + + this.address = ByteUtil.toInt((int) truncatedData[16 + indexIncrementor], (int) truncatedData[17 + indexIncrementor], + (int) truncatedData[18 + indexIncrementor], (int) truncatedData[19 + indexIncrementor], ByteUtil.BitConversion.BIG_ENDIAN); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.VERSION_RESPONSE; + } + + public PodProgressStatus getPodProgressStatus() { + return podProgressStatus; + } + + public FirmwareVersion getPmVersion() { + return pmVersion; + } + + public FirmwareVersion getPiVersion() { + return piVersion; + } + + public int getLot() { + return lot; + } + + public int getTid() { + return tid; + } + + public int getAddress() { + return address; + } + + @Override + public String toString() { + return "VersionResponse{" + + "podProgressStatus=" + podProgressStatus + + ", pmVersion=" + pmVersion + + ", piVersion=" + piVersion + + ", lot=" + lot + + ", tid=" + tid + + ", address=" + address + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfo.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfo.java new file mode 100644 index 0000000000..9002f0d92e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfo.java @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public abstract class PodInfo { + private final byte[] encodedData; + + public PodInfo(byte[] encodedData) { + this.encodedData = encodedData; + } + + public abstract PodInfoType getType(); + + public byte[] getEncodedData() { + return encodedData; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlerts.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlerts.java new file mode 100644 index 0000000000..1f32777fda --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlerts.java @@ -0,0 +1,92 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class PodInfoActiveAlerts extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 11; + + private final byte[] word278; // Unknown use + private final List alertActivations; + + public PodInfoActiveAlerts(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + word278 = ByteUtil.substring(encodedData, 1, 2); + + alertActivations = new ArrayList<>(); + + for (AlertSlot alertSlot : AlertSlot.values()) { + int valueHighBits = ByteUtil.convertUnsignedByteToInt(encodedData[3 + alertSlot.getValue() * 2]); + int valueLowBits = ByteUtil.convertUnsignedByteToInt(encodedData[4 + alertSlot.getValue() * 2]); + int value = (valueHighBits << 8) | valueLowBits; + if (value != 0) { + alertActivations.add(new AlertActivation(alertSlot, value)); + } + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.ACTIVE_ALERTS; + } + + public byte[] getWord278() { + return word278; + } + + public List getAlertActivations() { + return new ArrayList<>(alertActivations); + } + + @Override + public String toString() { + return "PodInfoActiveAlerts{" + + "word278=" + Arrays.toString(word278) + + ", alertActivations=" + alertActivations + + '}'; + } + + public static class AlertActivation { + private final AlertSlot alertSlot; + private final int value; + + private AlertActivation(AlertSlot alertSlot, int value) { + this.alertSlot = alertSlot; + this.value = value; + } + + public double getValueAsUnits() { + return value * OmnipodConst.POD_PULSE_SIZE; + } + + public Duration getValueAsDuration() { + return Duration.standardMinutes(value); + } + + public AlertSlot getAlertSlot() { + return alertSlot; + } + + @Override + public String toString() { + return "AlertActivation{" + + "alertSlot=" + alertSlot + + ", valueAsUnits=" + getValueAsUnits() + + ", valueAsDuration=" + getValueAsDuration() + + '}'; + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLog.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLog.java new file mode 100644 index 0000000000..a8eebc7696 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLog.java @@ -0,0 +1,83 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoDataLog extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 8; + private final FaultEventType faultEventType; + private final Duration timeFaultEvent; + private final Duration timeSinceActivation; + private final byte dataChunkSize; + private final byte maximumNumberOfDwords; + private final List dwords; + + public PodInfoDataLog(byte[] encodedData, int bodyLength) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + faultEventType = FaultEventType.fromByte(encodedData[1]); + timeFaultEvent = Duration.standardMinutes(ByteUtil.toInt(encodedData[2], encodedData[3])); + timeSinceActivation = Duration.standardMinutes(ByteUtil.toInt(encodedData[4], encodedData[5])); + dataChunkSize = encodedData[6]; + maximumNumberOfDwords = encodedData[7]; + + dwords = new ArrayList<>(); + + int numberOfDwords = (bodyLength - 8) / 4; + for (int i = 0; i < numberOfDwords; i++) { + dwords.add(ByteUtil.substring(encodedData, 8 + (4 * i), 4)); + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.DATA_LOG; + } + + public FaultEventType getFaultEventType() { + return faultEventType; + } + + public Duration getTimeFaultEvent() { + return timeFaultEvent; + } + + public Duration getTimeSinceActivation() { + return timeSinceActivation; + } + + public byte getDataChunkSize() { + return dataChunkSize; + } + + public byte getMaximumNumberOfDwords() { + return maximumNumberOfDwords; + } + + public List getDwords() { + return Collections.unmodifiableList(dwords); + } + + @Override + public String toString() { + return "PodInfoDataLog{" + + "faultEventType=" + faultEventType + + ", timeFaultEvent=" + timeFaultEvent + + ", timeSinceActivation=" + timeSinceActivation + + ", dataChunkSize=" + dataChunkSize + + ", maximumNumberOfDwords=" + maximumNumberOfDwords + + ", dwords=" + dwords + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTime.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTime.java new file mode 100644 index 0000000000..747b5e821b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTime.java @@ -0,0 +1,54 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.DateTime; +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoFaultAndInitializationTime extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 17; + private final FaultEventType faultEventType; + private final Duration timeFaultEvent; + private final DateTime initializationTime; + + public PodInfoFaultAndInitializationTime(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + faultEventType = FaultEventType.fromByte(encodedData[1]); + timeFaultEvent = Duration.standardMinutes(((encodedData[2] & 0b1) << 8) + encodedData[3]); + // We ignore time zones here because we don't keep the time zone in which the pod was initially set up + // Which is fine because we don't use the initialization time for anything important anyway + initializationTime = new DateTime(2000 + encodedData[14], encodedData[12], encodedData[13], encodedData[15], encodedData[16]); + } + + @Override + public PodInfoType getType() { + return PodInfoType.FAULT_AND_INITIALIZATION_TIME; + } + + public FaultEventType getFaultEventType() { + return faultEventType; + } + + public Duration getTimeFaultEvent() { + return timeFaultEvent; + } + + public DateTime getInitializationTime() { + return initializationTime; + } + + @Override + public String toString() { + return "PodInfoFaultAndInitializationTime{" + + "faultEventType=" + faultEventType + + ", timeFaultEvent=" + timeFaultEvent + + ", initializationTime=" + initializationTime + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEvent.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEvent.java new file mode 100644 index 0000000000..9db41a12f7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEvent.java @@ -0,0 +1,172 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.LogEventErrorCode; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class PodInfoFaultEvent extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 21; + + private final PodProgressStatus podProgressStatus; + private final DeliveryStatus deliveryStatus; + private final double insulinNotDelivered; + private final byte podMessageCounter; + private final double totalInsulinDelivered; + private final FaultEventType faultEventType; + private final Duration faultEventTime; + private final Double reservoirLevel; + private final Duration timeSinceActivation; + private final AlertSet unacknowledgedAlerts; + private final boolean faultAccessingTables; + private final LogEventErrorCode logEventErrorType; + private final PodProgressStatus logEventErrorPodProgressStatus; + private final byte receiverLowGain; + private final byte radioRSSI; + private final PodProgressStatus podProgressStatusAtTimeOfFirstLoggedFaultEvent; + private final byte[] unknownValue; + + public PodInfoFaultEvent(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + podProgressStatus = PodProgressStatus.fromByte(encodedData[1]); + deliveryStatus = DeliveryStatus.fromByte(encodedData[2]); + insulinNotDelivered = OmnipodConst.POD_PULSE_SIZE * ByteUtil.toInt(encodedData[3], encodedData[4]); + podMessageCounter = encodedData[5]; + totalInsulinDelivered = OmnipodConst.POD_PULSE_SIZE * ByteUtil.toInt(encodedData[6], encodedData[7]); + faultEventType = FaultEventType.fromByte(encodedData[8]); + + int minutesSinceActivation = ByteUtil.toInt(encodedData[9], encodedData[10]); + if (minutesSinceActivation == 0xffff) { + faultEventTime = null; + } else { + faultEventTime = Duration.standardMinutes(minutesSinceActivation); + } + + double reservoirValue = ((encodedData[11] & 0x03) << 8) + + ByteUtil.convertUnsignedByteToInt(encodedData[12]) * OmnipodConst.POD_PULSE_SIZE; + if (reservoirValue > OmnipodConst.MAX_RESERVOIR_READING) { + reservoirLevel = null; + } else { + reservoirLevel = reservoirValue; + } + + int minutesActive = ByteUtil.toInt(encodedData[13], encodedData[14]); + timeSinceActivation = Duration.standardMinutes(minutesActive); + + unacknowledgedAlerts = new AlertSet(encodedData[15]); + faultAccessingTables = encodedData[16] == 0x02; + logEventErrorType = LogEventErrorCode.fromByte((byte) (encodedData[17] >>> 4)); + logEventErrorPodProgressStatus = PodProgressStatus.fromByte((byte) (encodedData[17] & 0x0f)); + receiverLowGain = (byte) (ByteUtil.convertUnsignedByteToInt(encodedData[18]) >>> 6); + radioRSSI = (byte) (encodedData[18] & 0x3f); + podProgressStatusAtTimeOfFirstLoggedFaultEvent = PodProgressStatus.fromByte((byte) (encodedData[19] & 0x0f)); + unknownValue = ByteUtil.substring(encodedData, 20, 2); + } + + @Override + public PodInfoType getType() { + return PodInfoType.FAULT_EVENT; + } + + public PodProgressStatus getPodProgressStatus() { + return podProgressStatus; + } + + public DeliveryStatus getDeliveryStatus() { + return deliveryStatus; + } + + public double getInsulinNotDelivered() { + return insulinNotDelivered; + } + + public byte getPodMessageCounter() { + return podMessageCounter; + } + + public double getTotalInsulinDelivered() { + return totalInsulinDelivered; + } + + public FaultEventType getFaultEventType() { + return faultEventType; + } + + public Duration getFaultEventTime() { + return faultEventTime; + } + + public Double getReservoirLevel() { + return reservoirLevel; + } + + public Duration getTimeSinceActivation() { + return timeSinceActivation; + } + + public AlertSet getUnacknowledgedAlerts() { + return unacknowledgedAlerts; + } + + public boolean isFaultAccessingTables() { + return faultAccessingTables; + } + + public LogEventErrorCode getLogEventErrorType() { + return logEventErrorType; + } + + public PodProgressStatus getLogEventErrorPodProgressStatus() { + return logEventErrorPodProgressStatus; + } + + public byte getReceiverLowGain() { + return receiverLowGain; + } + + public byte getRadioRSSI() { + return radioRSSI; + } + + public PodProgressStatus getPodProgressStatusAtTimeOfFirstLoggedFaultEvent() { + return podProgressStatusAtTimeOfFirstLoggedFaultEvent; + } + + public byte[] getUnknownValue() { + return unknownValue; + } + + @Override + public String toString() { + return "PodInfoFaultEvent{" + + "podProgressStatus=" + podProgressStatus + + ", deliveryStatus=" + deliveryStatus + + ", insulinNotDelivered=" + insulinNotDelivered + + ", podMessageCounter=" + podMessageCounter + + ", totalInsulinDelivered=" + totalInsulinDelivered + + ", faultEventType=" + faultEventType + + ", faultEventTime=" + faultEventTime + + ", reservoirLevel=" + reservoirLevel + + ", timeSinceActivation=" + timeSinceActivation + + ", unacknowledgedAlerts=" + unacknowledgedAlerts + + ", faultAccessingTables=" + faultAccessingTables + + ", logEventErrorType=" + logEventErrorType + + ", logEventErrorPodProgressStatus=" + logEventErrorPodProgressStatus + + ", receiverLowGain=" + receiverLowGain + + ", radioRSSI=" + radioRSSI + + ", podProgressStatusAtTimeOfFirstLoggedFaultEvent=" + podProgressStatusAtTimeOfFirstLoggedFaultEvent + + ", unknownValue=" + ByteUtil.shortHexString(unknownValue) + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderPulseLog.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderPulseLog.java new file mode 100644 index 0000000000..c36b053cce --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderPulseLog.java @@ -0,0 +1,56 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoOlderPulseLog extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 3; + + private final ArrayList dwords; + + public PodInfoOlderPulseLog(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + dwords = new ArrayList<>(); + + int numberOfDwordLogEntries = ByteUtil.toInt(encodedData[1], encodedData[2]); + for (int i = 0; numberOfDwordLogEntries > i; i++) { + byte[] dword = ByteUtil.substring(encodedData, 3 + (4 * i), 4); + dwords.add(dword); + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.OLDER_PULSE_LOG; + } + + public List getDwords() { + return Collections.unmodifiableList(dwords); + } + + @Override + public String toString() { + String out = "PodInfoOlderPulseLog{" + + "dwords=["; + + List hexDwords = new ArrayList<>(); + for (byte[] dword : dwords) { + hexDwords.add(ByteUtil.shortHexStringWithoutSpaces(dword)); + } + out += TextUtils.join(", ", hexDwords); + out += "]}"; + + return out; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentPulseLog.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentPulseLog.java new file mode 100644 index 0000000000..1008cca689 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentPulseLog.java @@ -0,0 +1,64 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoRecentPulseLog extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 3; + + private final ArrayList dwords; + + private final int lastEntryIndex; + + public PodInfoRecentPulseLog(byte[] encodedData, int bodyLength) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + lastEntryIndex = ByteUtil.toInt(encodedData[1], encodedData[2]); + dwords = new ArrayList<>(); + + int numberOfDwords = (bodyLength - 3) / 4; + + for (int i = 0; numberOfDwords > i; i++) { + byte[] dword = ByteUtil.substring(encodedData, 3 + (4 * i), 4); + dwords.add(dword); + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.RECENT_PULSE_LOG; + } + + public List getDwords() { + return Collections.unmodifiableList(dwords); + } + + public int getLastEntryIndex() { + return lastEntryIndex; + } + + @Override + public String toString() { + String out = "PodInfoRecentPulseLog{" + + "lastEntryIndex=" + lastEntryIndex + + ",dwords=["; + + List hexDwords = new ArrayList<>(); + for (byte[] dword : dwords) { + hexDwords.add(ByteUtil.shortHexStringWithoutSpaces(dword)); + } + out += TextUtils.join(", ", hexDwords); + out += "]}"; + return out; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponse.java new file mode 100644 index 0000000000..59322f6209 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponse.java @@ -0,0 +1,40 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoResponse extends MessageBlock { + private final PodInfoType subType; + private final PodInfo podInfo; + + public PodInfoResponse(byte[] encodedData) { + int bodyLength = ByteUtil.convertUnsignedByteToInt(encodedData[1]); + + this.encodedData = ByteUtil.substring(encodedData, 2, bodyLength); + subType = PodInfoType.fromByte(encodedData[2]); + podInfo = subType.decode(this.encodedData, bodyLength); + } + + public PodInfoType getSubType() { + return subType; + } + + public T getPodInfo() { + return (T) podInfo; + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.POD_INFO_RESPONSE; + } + + @Override + public String toString() { + return "PodInfoResponse{" + + "subType=" + subType.name() + + ", podInfo=" + podInfo.toString() + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfiguration.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfiguration.java new file mode 100644 index 0000000000..6c48954cfe --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfiguration.java @@ -0,0 +1,71 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class AlertConfiguration { + private final AlertType alertType; + private final AlertSlot alertSlot; + private final boolean active; + private final boolean autoOffModifier; + private final Duration duration; + private final AlertTrigger alertTrigger; + private final BeepRepeat beepRepeat; + private final BeepType beepType; + + public AlertConfiguration(AlertType alertType, AlertSlot alertSlot, boolean active, boolean autoOffModifier, + Duration duration, AlertTrigger alertTrigger, + BeepType beepType, BeepRepeat beepRepeat) { + this.alertType = alertType; + this.alertSlot = alertSlot; + this.active = active; + this.autoOffModifier = autoOffModifier; + this.duration = duration; + this.alertTrigger = alertTrigger; + this.beepRepeat = beepRepeat; + this.beepType = beepType; + } + + public AlertType getAlertType() { + return alertType; + } + + public AlertSlot getAlertSlot() { + return alertSlot; + } + + public byte[] getRawData() { + int firstByte = (alertSlot.getValue() << 4); + firstByte += active ? (1 << 3) : 0; + + if (alertTrigger instanceof UnitsRemainingAlertTrigger) { + firstByte += 1 << 2; + } + + if (autoOffModifier) { + firstByte += 1 << 1; + } + + firstByte += ((int) duration.getStandardMinutes() >>> 8) & 0x1; + + byte[] encodedData = new byte[]{ + (byte) firstByte, + (byte) duration.getStandardMinutes() + }; + + if (alertTrigger instanceof UnitsRemainingAlertTrigger) { + int ticks = (int) (((UnitsRemainingAlertTrigger) alertTrigger).getValue() / OmnipodConst.POD_PULSE_SIZE / 2); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(ticks)); + } else if (alertTrigger instanceof TimerAlertTrigger) { + int durationInMinutes = (int) ((TimerAlertTrigger) alertTrigger).getValue().getStandardMinutes(); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(durationInMinutes)); + } + + encodedData = ByteUtil.concat(encodedData, beepRepeat.getValue()); + encodedData = ByteUtil.concat(encodedData, beepType.getValue()); + + return encodedData; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfigurationFactory.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfigurationFactory.java new file mode 100644 index 0000000000..400fe18875 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfigurationFactory.java @@ -0,0 +1,32 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.joda.time.Duration; + +public class AlertConfigurationFactory { + public static AlertConfiguration createLowReservoirAlertConfiguration(Double units) { + return new AlertConfiguration(AlertType.LOW_RESERVOIR_ALERT, AlertSlot.SLOT4, true, false, Duration.ZERO, + new UnitsRemainingAlertTrigger(units), BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_MINUTE_FOR_3_MINUTES_REPEAT_EVERY_60_MINUTES); + } + + public static AlertConfiguration createExpirationAdvisoryAlertConfiguration(Duration timeUntilAlert, Duration duration) { + return new AlertConfiguration(AlertType.EXPIRATION_ADVISORY_ALERT, AlertSlot.SLOT7, true, false, duration, + new TimerAlertTrigger(timeUntilAlert), BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_MINUTE_FOR_3_MINUTES_REPEAT_EVERY_15_MINUTES); + } + + public static AlertConfiguration createShutdownImminentAlertConfiguration(Duration timeUntilAlert) { + return new AlertConfiguration(AlertType.SHUTDOWN_IMMINENT_ALARM, AlertSlot.SLOT2, true, false, Duration.ZERO, + new TimerAlertTrigger(timeUntilAlert), BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_15_MINUTES); + } + + public static AlertConfiguration createAutoOffAlertConfiguration(boolean active, Duration countdownDuration) { + return new AlertConfiguration(AlertType.AUTO_OFF_ALARM, AlertSlot.SLOT0, active, true, + Duration.standardMinutes(15), new TimerAlertTrigger(countdownDuration), + BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_MINUTE_FOR_15_MINUTES); + } + + public static AlertConfiguration createFinishSetupReminderAlertConfiguration() { + return new AlertConfiguration(AlertType.FINISH_SETUP_REMINDER, AlertSlot.SLOT7, true, false, + Duration.standardMinutes(55), new TimerAlertTrigger(Duration.standardMinutes(5)), + BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_5_MINUTES); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSet.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSet.java new file mode 100644 index 0000000000..9d65d7d5aa --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSet.java @@ -0,0 +1,44 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.ArrayList; +import java.util.List; + +public class AlertSet { + private final List alertSlots; + + public AlertSet(byte rawValue) { + alertSlots = new ArrayList<>(); + for (AlertSlot alertSlot : AlertSlot.values()) { + if ((alertSlot.getBitMaskValue() & rawValue) != 0) { + alertSlots.add(alertSlot); + } + } + } + + public AlertSet(List alertSlots) { + this.alertSlots = alertSlots; + } + + public List getAlertSlots() { + return new ArrayList<>(alertSlots); + } + + public int size() { + return alertSlots.size(); + } + + public byte getRawValue() { + byte value = 0; + for (AlertSlot alertSlot : alertSlots) { + value |= alertSlot.getBitMaskValue(); + } + return value; + } + + @Override + public String toString() { + return "AlertSet{" + + "alertSlots=" + alertSlots + + '}'; + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSlot.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSlot.java new file mode 100644 index 0000000000..9f76ae0983 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSlot.java @@ -0,0 +1,35 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum AlertSlot { + SLOT0((byte) 0x00), + SLOT1((byte) 0x01), + SLOT2((byte) 0x02), + SLOT3((byte) 0x03), + SLOT4((byte) 0x04), + SLOT5((byte) 0x05), + SLOT6((byte) 0x06), + SLOT7((byte) 0x07); + + private byte value; + + AlertSlot(byte value) { + this.value = value; + } + + public static AlertSlot fromByte(byte value) { + for (AlertSlot type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown AlertSlot: " + value); + } + + public byte getBitMaskValue() { + return (byte) (1 << value); + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertTrigger.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertTrigger.java new file mode 100644 index 0000000000..1dedb458d0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertTrigger.java @@ -0,0 +1,14 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public abstract class AlertTrigger { + protected T value; + + public AlertTrigger(T value) { + this.value = value; + } + + public T getValue() { + return value; + } +} + diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertType.java new file mode 100644 index 0000000000..eb4b068a6f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertType.java @@ -0,0 +1,11 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum AlertType { + FINISH_PAIRING_REMINDER, + FINISH_SETUP_REMINDER, + EXPIRATION_ALERT, + EXPIRATION_ADVISORY_ALERT, + SHUTDOWN_IMMINENT_ALARM, + LOW_RESERVOIR_ALERT, + AUTO_OFF_ALARM +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepConfigType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepConfigType.java new file mode 100644 index 0000000000..3ef731e5c8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepConfigType.java @@ -0,0 +1,41 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + + +// BeepConfigType is used only for the $1E Beep Config Command. +public enum BeepConfigType { + // 0x0 always returns an error response for Beep Config (use 0xF for no beep) + BEEP_BEEP_BEEP_BEEP((byte) 0x01), + BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP((byte) 0x02), + BIP_BIP((byte) 0x03), + BEEP((byte) 0x04), + BEEP_BEEP_BEEP((byte) 0x05), + BEEEEEEP((byte) 0x06), + BIP_BIP_BIP_BIP_BIP_BIP((byte) 0x07), + BEEEP_BEEEP((byte) 0x08), + // 0x9 and 0xA always return an error response for Beep Config + BEEP_BEEP((byte) 0xB), + BEEEP((byte) 0xC), + BIP_BEEEEEP((byte) 0xD), + FIVE_SECONDS_BEEP((byte) 0xE), // can only be used if Pod is currently suspended + NO_BEEP((byte) 0xF); + + private byte value; + + BeepConfigType(byte value) { + this.value = value; + } + + public static BeepConfigType fromByte(byte value) { + for (BeepConfigType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown BeepConfigType: " + value); + } + + public byte getValue() { + return value; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepRepeat.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepRepeat.java new file mode 100644 index 0000000000..3562dd6c18 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepRepeat.java @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum BeepRepeat { + ONCE((byte) 0x00), + EVERY_MINUTE_FOR_3_MINUTES_REPEAT_EVERY_60_MINUTES((byte) 0x01), + EVERY_MINUTE_FOR_15_MINUTES((byte) 0x02), + EVERY_MINUTE_FOR_3_MINUTES_REPEAT_EVERY_15_MINUTES((byte) 0x03), + EVERY_3_MINUTES_DELAYED((byte) 0x04), + EVERY_60_MINUTES((byte) 0x05), + EVERY_15_MINUTES((byte) 0x06), + EVERY_15_MINUTES_DELAYED((byte) 0x07), + EVERY_5_MINUTES((byte) 0x08); + + private byte value; + + BeepRepeat(byte value) { + this.value = value; + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepType.java new file mode 100644 index 0000000000..e230276467 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepType.java @@ -0,0 +1,34 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +// BeepType is used for the $19 Configure Alerts and $1F Cancel Commands +public enum BeepType { + NO_BEEP((byte) 0x00), + BEEP_BEEP_BEEP_BEEP((byte) 0x01), + BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP((byte) 0x02), + BIP_BIP((byte) 0x03), + BEEP((byte) 0x04), + BEEP_BEEP_BEEP((byte) 0x05), + BEEEEEEP((byte) 0x06), + BIP_BIP_BIP_BIP_BIP_BIP((byte) 0x07), + BEEEP_BEEEP((byte) 0x08); + + private byte value; + + BeepType(byte value) { + this.value = value; + } + + public static BeepType fromByte(byte value) { + for (BeepType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown BeepType: " + value); + } + + public byte getValue() { + return value; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryStatus.java new file mode 100644 index 0000000000..9df1d480a5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryStatus.java @@ -0,0 +1,33 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum DeliveryStatus { + SUSPENDED((byte) 0x00), + NORMAL((byte) 0x01), + TEMP_BASAL_RUNNING((byte) 0x02), + PRIMING((byte) 0x04), + BOLUS_IN_PROGRESS((byte) 0x05), + BOLUS_AND_TEMP_BASAL((byte) 0x06); + + private byte value; + + DeliveryStatus(byte value) { + this.value = value; + } + + public static DeliveryStatus fromByte(byte value) { + for (DeliveryStatus type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown DeliveryStatus: " + value); + } + + public byte getValue() { + return value; + } + + public boolean isBolusing() { + return this.equals(BOLUS_IN_PROGRESS) || this.equals(BOLUS_AND_TEMP_BASAL); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryType.java new file mode 100644 index 0000000000..4d595afc15 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryType.java @@ -0,0 +1,27 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum DeliveryType { + NONE((byte) 0x00), + BASAL((byte) 0x01), + TEMP_BASAL((byte) 0x02), + BOLUS((byte) 0x04); + + private byte value; + + DeliveryType(byte value) { + this.value = value; + } + + public static DeliveryType fromByte(byte value) { + for (DeliveryType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown DeliveryType: " + value); + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/ErrorResponseType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/ErrorResponseType.java new file mode 100644 index 0000000000..883ae94821 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/ErrorResponseType.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum ErrorResponseType { + BAD_NONCE((byte) 0x14); + + private byte value; + + ErrorResponseType(byte value) { + this.value = value; + } + + public static ErrorResponseType fromByte(byte value) { + for (ErrorResponseType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown ErrorResponseType: " + value); + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FaultEventType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FaultEventType.java new file mode 100644 index 0000000000..69a74aa4c4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FaultEventType.java @@ -0,0 +1,148 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.Locale; + +public enum FaultEventType { + NO_FAULTS((byte) 0x00), + FAILED_FLASH_ERASE((byte) 0x01), + FAILED_FLASH_STORE((byte) 0x02), + TABLE_CORRUPTION_BASAL_SUBCOMMAND((byte) 0x03), + CORRUPTION_BYTE_720((byte) 0x05), + DATA_CORRUPTION_IN_TEST_RTC_INTERRUPT((byte) 0x06), + RTC_INTERRUPT_HANDLER_INCONSISTENT_STATE((byte) 0x07), + VALUE_GREATER_THAN_8((byte) 0x08), + BF_0_NOT_EQUAL_TO_BF_1((byte) 0x0A), + TABLE_CORRUPTION_TEMP_BASAL_SUBCOMMAND((byte) 0x0B), + RESET_DUE_TO_COP((byte) 0x0D), + RESET_DUE_TO_ILLEGAL_OPCODE((byte) 0x0E), + RESET_DUE_TO_ILLEGAL_ADDRESS((byte) 0x0F), + RESET_DUE_TO_SAWCOP((byte) 0x10), + CORRUPTION_IN_BYTE_866((byte) 0x11), + RESET_DUE_TO_LVD((byte) 0x12), + MESSAGE_LENGTH_TOO_LONG((byte) 0x13), + OCCLUDED((byte) 0x14), + CORRUPTION_IN_WORD_129((byte) 0x15), + CORRUPTION_IN_BYTE_868((byte) 0x16), + CORRUPTION_IN_A_VALIDATED_TABLE((byte) 0x17), + RESERVOIR_EMPTY((byte) 0x18), + BAD_POWER_SWITCH_ARRAY_VALUE_1((byte) 0x19), + BAD_POWER_SWITCH_ARRAY_VALUE_2((byte) 0x1A), + BAD_LOAD_CNTH_VALUE((byte) 0x1B), + EXCEEDED_MAXIMUM_POD_LIFE_80_HRS((byte) 0x1C), + BAD_STATE_COMMAND_1_A_SCHEDULE_PARSE((byte) 0x1D), + UNEXPECTED_STATE_IN_REGISTER_UPON_RESET((byte) 0x1E), + WRONG_SUMMARY_FOR_TABLE_129((byte) 0x1F), + VALIDATE_COUNT_ERROR_WHEN_BOLUSING((byte) 0x20), + BAD_TIMER_VARIABLE_STATE((byte) 0x21), + UNEXPECTED_RTC_MODULE_VALUE_DURING_RESET((byte) 0x22), + PROBLEM_CALIBRATE_TIMER((byte) 0x23), + RTC_INTERRUPT_HANDLER_UNEXPECTED_CALL((byte) 0x26), + MISSING_2_HOUR_ALERT_TO_FILL_TANK((byte) 0x27), + FAULT_EVENT_SETUP_POD((byte) 0x28), + ERROR_MAIN_LOOP_HELPER_0((byte) 0x29), + ERROR_MAIN_LOOP_HELPER_1((byte) 0x2A), + ERROR_MAIN_LOOP_HELPER_2((byte) 0x2B), + ERROR_MAIN_LOOP_HELPER_3((byte) 0x2C), + ERROR_MAIN_LOOP_HELPER_4((byte) 0x2D), + ERROR_MAIN_LOOP_HELPER_5((byte) 0x2E), + ERROR_MAIN_LOOP_HELPER_6((byte) 0x2F), + ERROR_MAIN_LOOP_HELPER_7((byte) 0x30), + INSULIN_DELIVERY_COMMAND_ERROR((byte) 0x31), + BAD_VALUE_STARTUP_TEST((byte) 0x32), + CONNECTED_POD_COMMAND_TIMEOUT((byte) 0x33), + RESET_FROM_UNKNOWN_CAUSE((byte) 0x34), + ERROR_FLASH_INITIALIZATION((byte) 0x36), + BAD_PIEZO_VALUE((byte) 0x37), + UNEXPECTED_VALUE_BYTE_358((byte) 0x38), + PROBLEM_WITH_LOAD_1_AND_2((byte) 0x39), + A_GREATER_THAN_7_IN_MESSAGE((byte) 0x3A), + FAILED_TEST_SAW_RESET((byte) 0x3B), + TEST_IN_PROGRESS((byte) 0x3C), + PROBLEM_WITH_PUMP_ANCHOR((byte) 0x3D), + ERROR_FLASH_WRITE((byte) 0x3E), + ENCODER_COUNT_TOO_HIGH((byte) 0x40), + ENCODER_COUNT_EXCESSIVE_VARIANCE((byte) 0x41), + ENCODER_COUNT_TOO_LOW((byte) 0x42), + ENCODER_COUNT_PROBLEM((byte) 0x43), + CHECK_VOLTAGE_OPEN_WIRE_1((byte) 0x44), + CHECK_VOLTAGE_OPEN_WIRE_2((byte) 0x45), + PROBLEM_WITH_LOAD_1_AND_2_TYPE_46((byte) 0x46), + PROBLEM_WITH_LOAD_1_AND_2_TYPE_47((byte) 0x47), + BAD_TIMER_CALIBRATION((byte) 0x48), + BAD_TIMER_RATIOS((byte) 0x49), + BAD_TIMER_VALUES((byte) 0x4A), + TRIM_ICS_TOO_CLOSE_TO_0_X_1_FF((byte) 0x4B), + PROBLEM_FINDING_BEST_TRIM_VALUE((byte) 0x4C), + BAD_SET_TPM_1_MULTI_CASES_VALUE((byte) 0x4D), + UNEXPECTED_RF_ERROR_FLAG_DURING_RESET((byte) 0x4F), + BAD_CHECK_SDRH_AND_BYTE_11_F_STATE((byte) 0x51), + ISSUE_TXO_KPROCESS_INPUT_BUFFER((byte) 0x52), + WRONG_VALUE_WORD_107((byte) 0x53), + PACKET_FRAME_LENGTH_TOO_LONG((byte) 0x54), + UNEXPECTED_IRQ_HIGHIN_TIMER_TICK((byte) 0x55), + UNEXPECTED_IRQ_LOWIN_TIMER_TICK((byte) 0x56), + BAD_ARG_TO_GET_ENTRY((byte) 0x57), + BAD_ARG_TO_UPDATE_37_A_TABLE((byte) 0x58), + ERROR_UPDATING_37_A_TABLE((byte) 0x59), + OCCLUSION_CHECK_VALUE_TOO_HIGH((byte) 0x5A), + LOAD_TABLE_CORRUPTION((byte) 0x5B), + PRIME_OPEN_COUNT_TOO_LOW((byte) 0x5C), + BAD_VALUE_BYTE_109((byte) 0x5D), + DISABLE_FLASH_SECURITY_FAILED((byte) 0x5E), + CHECK_VOLTAGE_FAILURE((byte) 0x5F), + OCCLUSION_CHECK_STARTUP_1((byte) 0x60), + OCCLUSION_CHECK_STARTUP_2((byte) 0x61), + OCCLUSION_CHECK_TIMEOUTS_1((byte) 0x62), + OCCLUSION_CHECK_TIMEOUTS_2((byte) 0x66), + OCCLUSION_CHECK_TIMEOUTS_3((byte) 0x67), + OCCLUSION_CHECK_PULSE_ISSUE((byte) 0x68), + OCCLUSION_CHECK_BOLUS_PROBLEM((byte) 0x69), + OCCLUSION_CHECK_ABOVE_THRESHOLD((byte) 0x6A), + BASAL_UNDER_INFUSION((byte) 0x80), + BASAL_OVER_INFUSION((byte) 0x81), + TEMP_BASAL_UNDER_INFUSION((byte) 0x82), + TEMP_BASAL_OVER_INFUSION((byte) 0x83), + BOLUS_UNDER_INFUSION((byte) 0x84), + BOLUS_OVER_INFUSION((byte) 0x85), + BASAL_OVER_INFUSION_PULSE((byte) 0x86), + TEMP_BASAL_OVER_INFUSION_PULSE((byte) 0x87), + BOLUS_OVER_INFUSION_PULSE((byte) 0x88), + IMMEDIATE_BOLUS_OVER_INFUSION_PULSE((byte) 0x89), + EXTENDED_BOLUS_OVER_INFUSION_PULSE((byte) 0x8A), + CORRUPTION_OF_TABLES((byte) 0x8B), + BAD_INPUT_TO_VERIFY_AND_START_PUMP((byte) 0x8D), + BAD_PUMP_REQ_5_STATE((byte) 0x8E), + COMMAND_1_A_PARSE_UNEXPECTED_FAILED((byte) 0x8F), + BAD_VALUE_FOR_TABLES((byte) 0x90), + BAD_PUMP_REQ_1_STATE((byte) 0x91), + BAD_PUMP_REQ_2_STATE((byte) 0x92), + BAD_PUMP_REQ_3_STATE((byte) 0x93), + BAD_VALUE_FIELD_6_IN_0_X_1_A((byte) 0x95), + BAD_STATE_IN_CLEAR_BOLUS_IST_2_AND_VARS((byte) 0x96), + BAD_STATE_IN_MAYBE_INC_33_D((byte) 0x97), + VALUES_DO_NOT_MATCH_OR_ARE_GREATER_THAN_0_X_97((byte) 0x98); + + private byte value; + + FaultEventType(byte value) { + this.value = value; + } + + public static FaultEventType fromByte(byte value) { + for (FaultEventType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown FaultEventType: " + value); + } + + public byte getValue() { + return value; + } + + @Override + public String toString() { + return String.format(Locale.getDefault(), "Pod fault (%d): %s", value, name()); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FirmwareVersion.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FirmwareVersion.java new file mode 100644 index 0000000000..27dab2c091 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FirmwareVersion.java @@ -0,0 +1,32 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.Locale; + +public class FirmwareVersion { + private final int major; + private final int minor; + private final int patch; + + public FirmwareVersion(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + @Override + public String toString() { + return String.format(Locale.getDefault(), "%d.%d.%d", major, minor, patch); + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public int getPatch() { + return patch; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/LogEventErrorCode.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/LogEventErrorCode.java new file mode 100644 index 0000000000..1cf366fd61 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/LogEventErrorCode.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum LogEventErrorCode { + NONE((byte) 0x00), + IMMEDIATE_BOLUS_IN_PROGRESS((byte) 0x01), + INTERNAL_2_BIT_VARIABLE_SET_AND_MANIPULATED_IN_MAIN_LOOP_ROUTINES_2((byte) 0x02), + INTERNAL_2_BIT_VARIABLE_SET_AND_MANIPULATED_IN_MAIN_LOOP_ROUTINES_3((byte) 0x03), + INSULIN_STATE_TABLE_CORRUPTION((byte) 0x04); + + private byte value; + + LogEventErrorCode(byte value) { + this.value = value; + } + + public static LogEventErrorCode fromByte(byte value) { + for (LogEventErrorCode type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown LogEventErrorCode: " + value); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/MessageBlockType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/MessageBlockType.java new file mode 100644 index 0000000000..a7ced69305 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/MessageBlockType.java @@ -0,0 +1,63 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.apache.commons.lang3.NotImplementedException; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.ErrorResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.VersionResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; + +public enum MessageBlockType { + VERSION_RESPONSE(0x01), + POD_INFO_RESPONSE(0x02), + SETUP_POD(0x03), + ERROR_RESPONSE(0x06), + ASSIGN_ADDRESS(0x07), + FAULT_CONFIG(0x08), + GET_STATUS(0x0e), + ACKNOWLEDGE_ALERT(0x11), + BASAL_SCHEDULE_EXTRA(0x13), + TEMP_BASAL_EXTRA(0x16), + BOLUS_EXTRA(0x17), + CONFIGURE_ALERTS(0x19), + SET_INSULIN_SCHEDULE(0x1a), + DEACTIVATE_POD(0x1c), + STATUS_RESPONSE(0x1d), + BEEP_CONFIG(0x1e), + CANCEL_DELIVERY(0x1f); + + private byte value; + + MessageBlockType(int value) { + this.value = (byte) value; + } + + public static MessageBlockType fromByte(byte value) { + for (MessageBlockType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown MessageBlockType: " + value); + } + + public byte getValue() { + return value; + } + + public MessageBlock decode(byte[] encodedData) { + switch (this) { + case VERSION_RESPONSE: + return new VersionResponse(encodedData); + case ERROR_RESPONSE: + return new ErrorResponse(encodedData); + case POD_INFO_RESPONSE: + return new PodInfoResponse(encodedData); + case STATUS_RESPONSE: + return new StatusResponse(encodedData); + default: + throw new NotImplementedException(this.name()); + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/NonceState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/NonceState.java new file mode 100644 index 0000000000..4698f88266 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/NonceState.java @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.Arrays; + +public class NonceState { + private final long[] table = new long[21]; + private int index; + + public NonceState(int lot, int tid) { + initializeTable(lot, tid, (byte) 0x00); + } + + public NonceState(int lot, int tid, byte seed) { + initializeTable(lot, tid, seed); + } + + private void initializeTable(int lot, int tid, byte seed) { + table[0] = (long) (lot & 0xFFFF) + 0x55543DC3L + (((long) (lot) & 0xFFFFFFFFL) >> 16); + table[0] = table[0] & 0xFFFFFFFFL; + table[1] = (tid & 0xFFFF) + 0xAAAAE44EL + (((long) (tid) & 0xFFFFFFFFL) >> 16); + table[1] = table[1] & 0xFFFFFFFFL; + index = 0; + table[0] += seed; + for (int i = 0; i < 16; i++) { + table[2 + i] = generateEntry(); + } + index = (int) ((table[0] + table[1]) & 0X0F); + } + + private int generateEntry() { + table[0] = (((table[0] >> 16) + (table[0] & 0xFFFF) * 0x5D7FL) & 0xFFFFFFFFL); + table[1] = (((table[1] >> 16) + (table[1] & 0xFFFF) * 0x8CA0L) & 0xFFFFFFFFL); + return (int) ((table[1] + (table[0] << 16)) & 0xFFFFFFFFL); + } + + public int getCurrentNonce() { + return (int) table[(2 + index)]; + } + + public void advanceToNextNonce() { + int nonce = getCurrentNonce(); + table[(2 + index)] = generateEntry(); + index = (nonce & 0x0F); + } + + @Override + public String toString() { + return "NonceState{" + + "table=" + Arrays.toString(table) + + ", index=" + index + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommandType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommandType.java new file mode 100644 index 0000000000..bb0d40ef59 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommandType.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +/** + * Created by andy on 4.8.2019 + */ +public enum OmnipodCommandType { + + PairAndPrimePod, // + FillCanulaAndSetBasalProfile, // + //InitPod, // + DeactivatePod, // + SetBasalProfile, // + SetBolus, // + CancelBolus, // + SetTemporaryBasal, // + CancelTemporaryBasal, // + ResetPodStatus, // + GetPodStatus, // + SetTime, // + AcknowledgeAlerts, // + GetPodPulseLog; + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommunicationManagerInterface.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommunicationManagerInterface.java new file mode 100644 index 0000000000..3999cd2923 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommunicationManagerInterface.java @@ -0,0 +1,79 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoRecentPulseLog; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; + +public interface OmnipodCommunicationManagerInterface { + + // TODO add methods that can be used by OmniPod Eros and Omnipod Dash + + /** + * Initialize Pod + */ + PumpEnactResult initPod(PodInitActionType podInitActionType, PodInitReceiver podInitReceiver, Profile profile); + + /** + * Get Pod Status (is pod running, battery left ?, reservoir, etc) + */ + // TODO we should probably return a (wrapped) StatusResponse instead of a PumpEnactResult + PumpEnactResult getPodStatus(); + + /** + * Deactivate Pod + */ + PumpEnactResult deactivatePod(PodInitReceiver podInitReceiver); + + /** + * Set Basal Profile + */ + PumpEnactResult setBasalProfile(Profile basalProfile); + + /** + * Reset Pod status (if we forget to disconnect Pod and want to init new pod, and want to forget current pod) + */ + PumpEnactResult resetPodStatus(); + + /** + * Set Bolus + * + * @param detailedBolusInfo DetailedBolusInfo instance with amount and all other required data + */ + PumpEnactResult setBolus(DetailedBolusInfo detailedBolusInfo); + + /** + * Cancel Bolus (if bolus is already stopped, return acknowledgment) + */ + PumpEnactResult cancelBolus(); + + /** + * Set Temporary Basal + * + * @param tempBasalPair TempBasalPair object containg amount and duration in minutes + */ + PumpEnactResult setTemporaryBasal(TempBasalPair tempBasalPair); + + /** + * Cancel Temporary Basal (if TB is already stopped, return acknowledgment) + */ + PumpEnactResult cancelTemporaryBasal(); + + /** + * Acknowledge alerts + */ + PumpEnactResult acknowledgeAlerts(); + + /** + * Set Time on Pod + */ + PumpEnactResult setTime(); + + + void setPumpStatus(OmnipodPumpStatus pumpStatusLocal); + + + PodInfoRecentPulseLog readPulseLog(); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCustomActionType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCustomActionType.java new file mode 100644 index 0000000000..edf7181f18 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCustomActionType.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import info.nightscout.androidaps.plugins.general.actions.defs.CustomActionType; + +/** + * Created by andy on 4.8.2019 + */ + +public enum OmnipodCustomActionType implements CustomActionType { + + ResetRileyLinkConfiguration(), // + PairAndPrime(), // + FillCanulaSetBasalProfile(), // + //InitPod(), // + DeactivatePod(), // + ResetPodStatus(), // + ; + + @Override + public String getKey() { + return this.name(); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPodType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPodType.java new file mode 100644 index 0000000000..849a22e5b0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPodType.java @@ -0,0 +1,6 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum OmnipodPodType { + Eros, // + Dash +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPumpPluginInterface.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPumpPluginInterface.java new file mode 100644 index 0000000000..f2066ebb2b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPumpPluginInterface.java @@ -0,0 +1,15 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import info.nightscout.androidaps.interfaces.PumpInterface; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState; + +public interface OmnipodPumpPluginInterface extends PumpInterface { + + void addPodStatusRequest(OmnipodStatusRequest pumpStatusRequest); + + void setDriverState(OmnipodDriverState state); + + RxBusWrapper getRxBus(); + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodStatusRequest.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodStatusRequest.java new file mode 100644 index 0000000000..80f7e4ecfc --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodStatusRequest.java @@ -0,0 +1,20 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum OmnipodStatusRequest { + ResetState(OmnipodCommandType.ResetPodStatus), // + AcknowledgeAlerts(OmnipodCommandType.AcknowledgeAlerts), // + GetPodState(OmnipodCommandType.GetPodStatus), // + GetPodPulseLog(OmnipodCommandType.GetPodPulseLog) + ; + + private OmnipodCommandType commandType; + + OmnipodStatusRequest(OmnipodCommandType commandType) { + this.commandType = commandType; + } + + + public OmnipodCommandType getCommandType() { + return commandType; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodUIResponseType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodUIResponseType.java new file mode 100644 index 0000000000..b135c3f590 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodUIResponseType.java @@ -0,0 +1,13 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +/** + * Created by andy on 10/18/18. + */ + +public enum OmnipodUIResponseType { + + Data, + Error, + Invalid; + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PacketType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PacketType.java new file mode 100644 index 0000000000..aae862d9b8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PacketType.java @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum PacketType { + INVALID((byte) 0), + POD((byte) 0b111), + PDM((byte) 0b101), + CON((byte) 0b100), + ACK((byte) 0b010); + + private byte value; + + PacketType(byte value) { + this.value = value; + } + + public static PacketType fromByte(byte value) { + for (PacketType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown PacketType: " + value); + } + + public int getMaxBodyLength() { + switch (this) { + case ACK: + return 4; + case CON: + case PDM: + case POD: + return 31; + default: + return 0; + } + } + + public byte getValue() { + return value; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodDeviceState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodDeviceState.java new file mode 100644 index 0000000000..5e903b5ae4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodDeviceState.java @@ -0,0 +1,38 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import info.nightscout.androidaps.R; + +/** + * Created by andy on 4.8.2019 + */ + +public enum PodDeviceState { + + // FIXME + NeverContacted(R.string.medtronic_pump_status_never_contacted), // + Sleeping(R.string.medtronic_pump_status_sleeping), // + WakingUp(R.string.medtronic_pump_status_waking_up), // + Active(R.string.medtronic_pump_status_active), // + ErrorWhenCommunicating(R.string.medtronic_pump_status_error_comm), // + TimeoutWhenCommunicating(R.string.medtronic_pump_status_timeout_comm), // + // ProblemContacting(R.string.medtronic_pump_status_problem_contacting), // + PumpUnreachable(R.string.medtronic_pump_status_pump_unreachable), // + InvalidConfiguration(R.string.medtronic_pump_status_invalid_config); + + Integer resourceId = null; + + + PodDeviceState() { + + } + + + PodDeviceState(int resourceId) { + this.resourceId = resourceId; + } + + + public Integer getResourceId() { + return resourceId; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInfoType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInfoType.java new file mode 100644 index 0000000000..4e2095e711 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInfoType.java @@ -0,0 +1,61 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfo; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoActiveAlerts; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoDataLog; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultAndInitializationTime; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoOlderPulseLog; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoRecentPulseLog; + +public enum PodInfoType { + NORMAL((byte) 0x00), + ACTIVE_ALERTS((byte) 0x01), + FAULT_EVENT((byte) 0x02), + DATA_LOG((byte) 0x03), // Similar to types $50 & $51. Returns up to the last 60 dwords of data. + FAULT_AND_INITIALIZATION_TIME((byte) 0x05), + RECENT_PULSE_LOG((byte) 0x50), // Starting at $4200 + OLDER_PULSE_LOG((byte) 0x51); // Starting at $4200 but dumps entries before the last 50 + + private final byte value; + + PodInfoType(byte value) { + this.value = value; + } + + public static PodInfoType fromByte(byte value) { + for (PodInfoType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown PodInfoType: " + value); + } + + public byte getValue() { + return value; + } + + public PodInfo decode(byte[] encodedData, int bodyLength) { + switch (this) { + case NORMAL: + // We've never observed a PodInfoResponse with 0x00 subtype + // Instead, the pod returns a StatusResponse + throw new UnsupportedOperationException("Cannot decode PodInfoType.NORMAL"); + case ACTIVE_ALERTS: + return new PodInfoActiveAlerts(encodedData); + case FAULT_EVENT: + return new PodInfoFaultEvent(encodedData); + case DATA_LOG: + return new PodInfoDataLog(encodedData, bodyLength); + case FAULT_AND_INITIALIZATION_TIME: + return new PodInfoFaultAndInitializationTime(encodedData); + case RECENT_PULSE_LOG: + return new PodInfoRecentPulseLog(encodedData, bodyLength); + case OLDER_PULSE_LOG: + return new PodInfoOlderPulseLog(encodedData); + default: + throw new IllegalArgumentException("Cannot decode " + this.name()); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitActionType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitActionType.java new file mode 100644 index 0000000000..c0a92ed2d7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitActionType.java @@ -0,0 +1,88 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import info.nightscout.androidaps.R; + +public enum PodInitActionType { + + PairAndPrimeWizardStep(), // + PairPod(R.string.omnipod_init_pod_pair_pod, PairAndPrimeWizardStep), // + PrimePod(R.string.omnipod_init_pod_prime_pod, PairAndPrimeWizardStep), // + + FillCannulaSetBasalProfileWizardStep(), // + FillCannula(R.string.omnipod_init_pod_fill_cannula, FillCannulaSetBasalProfileWizardStep), // + SetBasalProfile(R.string.omnipod_init_pod_set_basal_profile, FillCannulaSetBasalProfileWizardStep), // + + DeactivatePodWizardStep(), // + CancelDelivery(R.string.omnipod_deactivate_pod_cancel_delivery, DeactivatePodWizardStep), // + DeactivatePod(R.string.omnipod_deactivate_pod_deactivate_pod, DeactivatePodWizardStep) // + ; + + + + private int resourceId; + private PodInitActionType parent; + + private static Map> stepsForWizardStep; + + + PodInitActionType(int resourceId, PodInitActionType parent) { + this.resourceId = resourceId; + this.parent = parent; + } + + + PodInitActionType() { + } + + + public boolean isParent() { + return this.parent == null; + } + + + public List getChildren() { + + List outList = new ArrayList<>(); + + for (PodInitActionType value : values()) { + if (value.parent == this) { + outList.add(value); + } + } + + return outList; + } + + + public static List getAvailableWizardSteps(OmnipodPodType podType) { + List outList = new ArrayList<>(); + + if (podType == OmnipodPodType.Eros) { + outList.add(PodInitActionType.PairAndPrimeWizardStep); + outList.add(PodInitActionType.FillCannulaSetBasalProfileWizardStep); + } else { + // TODO we might have different wizard steps, with different handling for Dash + } + + return outList; + } + + + public static List getAvailableActionsForWizardSteps(PodInitActionType wizardStep) { + if (stepsForWizardStep.containsKey(wizardStep)) { + return stepsForWizardStep.get(wizardStep); + } else { + return new ArrayList<>(); + } + } + + + public int getResourceId() { + return resourceId; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitReceiver.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitReceiver.java new file mode 100644 index 0000000000..789da1add2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitReceiver.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public interface PodInitReceiver { + + void returnInitTaskStatus(PodInitActionType podInitActionType, boolean isSuccess, String errorMessage); + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodProgressStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodProgressStatus.java new file mode 100644 index 0000000000..4bd9780118 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodProgressStatus.java @@ -0,0 +1,43 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum PodProgressStatus { + INITIAL_VALUE((byte) 0x00), + TANK_POWER_ACTIVATED((byte) 0x01), + TANK_FILL_COMPLETED((byte) 0x02), + PAIRING_SUCCESS((byte) 0x03), + PRIMING((byte) 0x04), + READY_FOR_BASAL_SCHEDULE((byte) 0x05), + READY_FOR_CANNULA_INSERTION((byte) 0x06), + CANNULA_INSERTING((byte) 0x07), + RUNNING_ABOVE_FIFTY_UNITS((byte) 0x08), + RUNNING_BELOW_FIFTY_UNITS((byte) 0x09), + ONE_NOT_USED_BUT_IN_33((byte) 0x0a), + TWO_NOT_USED_BUT_IN_33((byte) 0x0b), + THREE_NOT_USED_BUT_IN_33((byte) 0x0c), + FAULT_EVENT_OCCURRED((byte) 0x0d), + FAILED_TO_INITIALIZE_IN_TIME((byte) 0x0e), + INACTIVE((byte) 0x0f); + + private byte value; + + PodProgressStatus(byte value) { + this.value = value; + } + + public static PodProgressStatus fromByte(byte value) { + for (PodProgressStatus type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown PodProgressStatus: " + value); + } + + public byte getValue() { + return value; + } + + public boolean isReadyForDelivery() { + return this == RUNNING_ABOVE_FIFTY_UNITS || this == RUNNING_BELOW_FIFTY_UNITS; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodResponseType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodResponseType.java new file mode 100644 index 0000000000..7314ae9591 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodResponseType.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum PodResponseType { + + Acknowledgment, // set commands would just acknowledge if data was sent + Data, // query commands would return data + Error, // communication/response produced an error + Invalid // invalid response (not supported, should never be returned) +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/SetupProgress.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/SetupProgress.java new file mode 100644 index 0000000000..72a3b5438a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/SetupProgress.java @@ -0,0 +1,21 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum SetupProgress { + ADDRESS_ASSIGNED, + POD_CONFIGURED, + STARTING_PRIME, + PRIMING, + PRIMING_FINISHED, + INITIAL_BASAL_SCHEDULE_SET, + STARTING_INSERT_CANNULA, + CANNULA_INSERTING, + COMPLETED; + + public boolean isBefore(SetupProgress other) { + return this.ordinal() < other.ordinal(); + } + + public boolean isAfter(SetupProgress other) { + return this.ordinal() > other.ordinal(); + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/TimerAlertTrigger.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/TimerAlertTrigger.java new file mode 100644 index 0000000000..8d4096de59 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/TimerAlertTrigger.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.joda.time.Duration; + +public class TimerAlertTrigger extends AlertTrigger { + public TimerAlertTrigger(Duration value) { + super(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/UnitsRemainingAlertTrigger.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/UnitsRemainingAlertTrigger.java new file mode 100644 index 0000000000..6668be13c8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/UnitsRemainingAlertTrigger.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public class UnitsRemainingAlertTrigger extends AlertTrigger { + public UnitsRemainingAlertTrigger(Double value) { + super(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliverySchedule.java new file mode 100644 index 0000000000..ab46dacd76 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliverySchedule.java @@ -0,0 +1,61 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public class BasalDeliverySchedule extends DeliverySchedule implements IRawRepresentable { + + private final byte currentSegment; + private final int secondsRemaining; + private final int pulsesRemaining; + private final BasalDeliveryTable basalTable; + + public BasalDeliverySchedule(byte currentSegment, int secondsRemaining, int pulsesRemaining, + BasalDeliveryTable basalTable) { + this.currentSegment = currentSegment; + this.secondsRemaining = secondsRemaining; + this.pulsesRemaining = pulsesRemaining; + this.basalTable = basalTable; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[0]; + rawData = ByteUtil.concat(rawData, currentSegment); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(secondsRemaining << 3)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(pulsesRemaining)); + for (BasalTableEntry entry : basalTable.getEntries()) { + rawData = ByteUtil.concat(rawData, entry.getRawData()); + } + return rawData; + } + + @Override + public InsulinScheduleType getType() { + return InsulinScheduleType.BASAL_SCHEDULE; + } + + @Override + public int getChecksum() { + int checksum = 0; + byte[] rawData = getRawData(); + for (int i = 0; i < rawData.length && i < 5; i++) { + checksum += ByteUtil.convertUnsignedByteToInt(rawData[i]); + } + for (BasalTableEntry entry : basalTable.getEntries()) { + checksum += entry.getChecksum(); + } + + return checksum; + } + + @Override + public String toString() { + return "BasalDeliverySchedule{" + + "currentSegment=" + currentSegment + + ", secondsRemaining=" + secondsRemaining + + ", pulsesRemaining=" + pulsesRemaining + + ", basalTable=" + basalTable + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliveryTable.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliveryTable.java new file mode 100644 index 0000000000..3e18d6b5d0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliveryTable.java @@ -0,0 +1,112 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BasalDeliveryTable { + + public static final int SEGMENT_DURATION = 30 * 60; + public static final int MAX_PULSES_PER_RATE_ENTRY = 6400; + + private static final int NUM_SEGMENTS = 48; + private static final int MAX_SEGMENTS_PER_ENTRY = 16; + + private List entries = new ArrayList<>(); + + public BasalDeliveryTable(BasalSchedule schedule) { + TempSegment[] expandedSegments = new TempSegment[48]; + + boolean halfPulseRemainder = false; + for (int i = 0; i < NUM_SEGMENTS; i++) { + double rate = schedule.rateAt(Duration.standardMinutes(i * 30)); + int pulsesPerHour = (int) Math.round(rate / OmnipodConst.POD_PULSE_SIZE); + int pulsesPerSegment = pulsesPerHour >>> 1; + boolean halfPulse = (pulsesPerHour & 0b1) != 0; + + expandedSegments[i] = new TempSegment(pulsesPerSegment + (halfPulseRemainder && halfPulse ? 1 : 0)); + halfPulseRemainder = halfPulseRemainder != halfPulse; + } + + List segmentsToMerge = new ArrayList<>(); + + boolean altSegmentPulse = false; + for (TempSegment segment : expandedSegments) { + if (segmentsToMerge.isEmpty()) { + segmentsToMerge.add(segment); + continue; + } + + TempSegment firstSegment = segmentsToMerge.get(0); + + int delta = segment.getPulses() - firstSegment.getPulses(); + if (segmentsToMerge.size() == 1) { + altSegmentPulse = delta == 1; + } + + int expectedDelta = altSegmentPulse ? segmentsToMerge.size() % 2 : 0; + + if (expectedDelta != delta || segmentsToMerge.size() == MAX_SEGMENTS_PER_ENTRY) { + addBasalTableEntry(segmentsToMerge, altSegmentPulse); + segmentsToMerge.clear(); + } + + segmentsToMerge.add(segment); + } + + addBasalTableEntry(segmentsToMerge, altSegmentPulse); + } + + public BasalDeliveryTable(double tempBasalRate, Duration duration) { + int pulsesPerHour = (int) Math.round(tempBasalRate / OmnipodConst.POD_PULSE_SIZE); + int pulsesPerSegment = pulsesPerHour >> 1; + boolean alternateSegmentPulse = (pulsesPerHour & 0b1) != 0; + + int remaining = (int) Math.round(duration.getStandardSeconds() / (double) BasalDeliveryTable.SEGMENT_DURATION); + + while (remaining > 0) { + int segments = Math.min(MAX_SEGMENTS_PER_ENTRY, remaining); + entries.add(new BasalTableEntry(segments, pulsesPerSegment, segments > 1 && alternateSegmentPulse)); + remaining -= segments; + } + } + + private void addBasalTableEntry(List segments, boolean alternateSegmentPulse) { + entries.add(new BasalTableEntry(segments.size(), segments.get(0).getPulses(), alternateSegmentPulse)); + } + + public BasalTableEntry[] getEntries() { + return entries.toArray(new BasalTableEntry[0]); + } + + byte numSegments() { + byte numSegments = 0; + for (BasalTableEntry entry : entries) { + numSegments += entry.getSegments(); + } + return numSegments; + } + + @Override + public String toString() { + return "BasalDeliveryTable{" + + "entries=" + entries + + '}'; + } + + private class TempSegment { + private int pulses; + + public TempSegment(int pulses) { + this.pulses = pulses; + } + + public int getPulses() { + return pulses; + } + } +} + diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalSchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalSchedule.java new file mode 100644 index 0000000000..140c584c0b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalSchedule.java @@ -0,0 +1,147 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class BasalSchedule { + private final List entries; + + public BasalSchedule(List entries) { + if (entries == null || entries.size() == 0) { + throw new IllegalArgumentException("Entries can not be empty"); + } else if (!entries.get(0).getStartTime().isEqual(Duration.ZERO)) { + throw new IllegalArgumentException("First basal schedule entry should have 0 offset"); + } + this.entries = entries; + } + + public double rateAt(Duration offset) { + return lookup(offset).getBasalScheduleEntry().getRate(); + } + + public List getEntries() { + return new ArrayList<>(entries); + } + + public BasalScheduleLookupResult lookup(Duration offset) { + if (offset.isLongerThan(Duration.standardHours(24)) || offset.isShorterThan(Duration.ZERO)) { + throw new IllegalArgumentException("Invalid duration"); + } + + List reversedBasalScheduleEntries = reversedBasalScheduleEntries(); + + Duration last = Duration.standardHours(24); + int index = 0; + for (BasalScheduleEntry entry : reversedBasalScheduleEntries) { + if (entry.getStartTime().isShorterThan(offset) || entry.getStartTime().equals(offset)) { + return new BasalScheduleLookupResult( // + reversedBasalScheduleEntries.size() - (index + 1), // + entry, // + entry.getStartTime(), // + last.minus(entry.getStartTime())); + } + last = entry.getStartTime(); + index++; + } + + throw new IllegalArgumentException("Basal schedule incomplete"); + } + + private List reversedBasalScheduleEntries() { + List reversedEntries = new ArrayList<>(entries); + Collections.reverse(reversedEntries); + return reversedEntries; + } + + public List adjacentEqualRatesMergedEntries() { + List mergedEntries = new ArrayList<>(); + Double lastRate = null; + for (BasalScheduleEntry entry : entries) { + if (lastRate == null || entry.getRate() != lastRate) { + mergedEntries.add(entry); + } + lastRate = entry.getRate(); + } + return mergedEntries; + } + + public List getDurations() { + List durations = new ArrayList<>(); + Duration last = Duration.standardHours(24); + List basalScheduleEntries = reversedBasalScheduleEntries(); + for (BasalScheduleEntry entry : basalScheduleEntries) { + durations.add(new BasalScheduleDurationEntry( // + entry.getRate(), // + entry.getStartTime(), // + last.minus(entry.getStartTime()))); + last = entry.getStartTime(); + } + + Collections.reverse(durations); + return durations; + } + + @Override + public String toString() { + return "BasalSchedule{" + + "entries=" + entries + + '}'; + } + + public static class BasalScheduleDurationEntry { + private final double rate; + private final Duration duration; + private final Duration startTime; + + public BasalScheduleDurationEntry(double rate, Duration startTime, Duration duration) { + this.rate = rate; + this.duration = duration; + this.startTime = startTime; + } + + public double getRate() { + return rate; + } + + public Duration getDuration() { + return duration; + } + + public Duration getStartTime() { + return startTime; + } + } + + public static class BasalScheduleLookupResult { + private final int index; + private final BasalScheduleEntry basalScheduleEntry; + private final Duration startTime; + private final Duration duration; + + public BasalScheduleLookupResult(int index, BasalScheduleEntry basalScheduleEntry, Duration startTime, Duration duration) { + this.index = index; + this.basalScheduleEntry = basalScheduleEntry; + this.startTime = startTime; + this.duration = duration; + } + + public int getIndex() { + return index; + } + + public BasalScheduleEntry getBasalScheduleEntry() { + return basalScheduleEntry; + } + + public Duration getStartTime() { + return startTime; + } + + public Duration getDuration() { + return duration; + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleEntry.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleEntry.java new file mode 100644 index 0000000000..f8065e0f4b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleEntry.java @@ -0,0 +1,40 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BasalScheduleEntry { + private final double rate; + private final Duration startTime; + + public BasalScheduleEntry(double rate, Duration startTime) { + if (startTime.isLongerThan(Duration.standardHours(24).minus(Duration.standardSeconds(1))) || startTime.isShorterThan(Duration.ZERO) || startTime.getStandardSeconds() % 1800 != 0) { + throw new IllegalArgumentException("Invalid start time"); + } else if (rate < 0D) { + throw new IllegalArgumentException("Rate should be >= 0"); + } else if (rate > OmnipodConst.MAX_BASAL_RATE) { + throw new IllegalArgumentException("Rate exceeds max basal rate"); + } else if (rate % OmnipodConst.POD_PULSE_SIZE > 0.000001 && rate % OmnipodConst.POD_PULSE_SIZE - OmnipodConst.POD_PULSE_SIZE < -0.000001) { + throw new IllegalArgumentException("Unsupported basal rate precision"); + } + this.rate = rate; + this.startTime = startTime; + } + + public double getRate() { + return rate; + } + + public Duration getStartTime() { + return startTime; + } + + @Override + public String toString() { + return "BasalScheduleEntry{" + + "rate=" + rate + + ", startTime=" + startTime.getStandardSeconds() + "s" + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalTableEntry.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalTableEntry.java new file mode 100644 index 0000000000..55339865a0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalTableEntry.java @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public class BasalTableEntry implements IRawRepresentable { + + private final int segments; + private final int pulses; + private final boolean alternateSegmentPulse; + + public BasalTableEntry(int segments, int pulses, boolean alternateSegmentPulse) { + this.segments = segments; + this.pulses = pulses; + this.alternateSegmentPulse = alternateSegmentPulse; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[2]; + byte pulsesHighByte = (byte) ((pulses >>> 8) & 0b11); + byte pulsesLowByte = (byte) pulses; + rawData[0] = (byte) ((byte) ((segments - 1) << 4) + (byte) ((alternateSegmentPulse ? 1 : 0) << 3) + pulsesHighByte); + rawData[1] = pulsesLowByte; + return rawData; + } + + public int getChecksum() { + int checksumPerSegment = ByteUtil.convertUnsignedByteToInt((byte) pulses) + (pulses >>> 8); + return (checksumPerSegment * segments + (alternateSegmentPulse ? segments / 2 : 0)); + } + + public int getSegments() { + return this.segments; + } + + public int getPulses() { + return pulses; + } + + public boolean isAlternateSegmentPulse() { + return alternateSegmentPulse; + } + + @Override + public String toString() { + return "BasalTableEntry{" + + "segments=" + segments + + ", pulses=" + pulses + + ", alternateSegmentPulse=" + alternateSegmentPulse + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BolusDeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BolusDeliverySchedule.java new file mode 100644 index 0000000000..167f1bcce5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BolusDeliverySchedule.java @@ -0,0 +1,60 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BolusDeliverySchedule extends DeliverySchedule implements IRawRepresentable { + + private final double units; + private final Duration timeBetweenPulses; + + public BolusDeliverySchedule(double units, Duration timeBetweenPulses) { + if (units <= 0D) { + throw new IllegalArgumentException("Units should be > 0"); + } else if (units > OmnipodConst.MAX_BOLUS) { + throw new IllegalArgumentException("Units exceeds max bolus"); + } + this.units = units; + this.timeBetweenPulses = timeBetweenPulses; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[]{1}; // Number of half hour segments + + int pulseCount = (int) Math.round(units / OmnipodConst.POD_PULSE_SIZE); + int multiplier = (int) timeBetweenPulses.getStandardSeconds() * 8; + int fieldA = pulseCount * multiplier; + + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(fieldA)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(pulseCount)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(pulseCount)); + return rawData; + } + + @Override + public InsulinScheduleType getType() { + return InsulinScheduleType.BOLUS; + } + + @Override + public int getChecksum() { + int checksum = 0; + byte[] rawData = getRawData(); + for (int i = 0; i < rawData.length && i < 7; i++) { + checksum += ByteUtil.convertUnsignedByteToInt(rawData[i]); + } + return checksum; + } + + @Override + public String toString() { + return "BolusDeliverySchedule{" + + "units=" + units + + ", timeBetweenPulses=" + timeBetweenPulses + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/DeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/DeliverySchedule.java new file mode 100644 index 0000000000..325b7f19e4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/DeliverySchedule.java @@ -0,0 +1,10 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public abstract class DeliverySchedule implements IRawRepresentable { + + public abstract InsulinScheduleType getType(); + + public abstract int getChecksum(); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/InsulinScheduleType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/InsulinScheduleType.java new file mode 100644 index 0000000000..6c7a364e2d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/InsulinScheduleType.java @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +public enum InsulinScheduleType { + BASAL_SCHEDULE(0), + TEMP_BASAL_SCHEDULE(1), + BOLUS(2); + + private byte value; + + InsulinScheduleType(int value) { + this.value = (byte) value; + } + + public static InsulinScheduleType fromByte(byte input) { + for (InsulinScheduleType type : values()) { + if (type.value == input) { + return type; + } + } + return null; + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/RateEntry.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/RateEntry.java new file mode 100644 index 0000000000..c9bda11562 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/RateEntry.java @@ -0,0 +1,77 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class RateEntry implements IRawRepresentable { + + private final double totalPulses; + // We use a double for the delay between pulses because the Joda time API lacks precision for our calculations + private final double delayBetweenPulsesInSeconds; + + public RateEntry(double totalPulses, double delayBetweenPulsesInSeconds) { + this.totalPulses = totalPulses; + this.delayBetweenPulsesInSeconds = delayBetweenPulsesInSeconds; + } + + public static List createEntries(double rate, Duration duration) { + List entries = new ArrayList<>(); + int remainingSegments = (int) Math.round(duration.getStandardSeconds() / 1800.0); + double pulsesPerSegment = (int) Math.round(rate / OmnipodConst.POD_PULSE_SIZE) / 2.0; + int maxSegmentsPerEntry = pulsesPerSegment > 0 ? (int) (BasalDeliveryTable.MAX_PULSES_PER_RATE_ENTRY / pulsesPerSegment) : 1; + + double durationInHours = duration.getStandardSeconds() / 3600.0; + + double remainingPulses = rate * durationInHours / OmnipodConst.POD_PULSE_SIZE; + double delayBetweenPulses = 3600 / rate * OmnipodConst.POD_PULSE_SIZE; + + while (remainingSegments > 0) { + if (rate == 0.0) { + entries.add(new RateEntry(0, 30D * 60)); + remainingSegments -= 1; + } else { + int numSegments = Math.min(maxSegmentsPerEntry, (int) Math.round(remainingPulses / pulsesPerSegment)); + double totalPulses = pulsesPerSegment * numSegments; + entries.add(new RateEntry(totalPulses, delayBetweenPulses)); + remainingSegments -= numSegments; + remainingPulses -= totalPulses; + } + } + + return entries; + } + + public double getTotalPulses() { + return totalPulses; + } + + public double getDelayBetweenPulsesInSeconds() { + return delayBetweenPulsesInSeconds; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[0]; + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16((int) Math.round(totalPulses * 10))); + if (totalPulses == 0) { + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt((int) (delayBetweenPulsesInSeconds * 1000 * 1000))); + } else { + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt((int) (delayBetweenPulsesInSeconds * 1000 * 100))); + } + return rawData; + } + + @Override + public String toString() { + return "RateEntry{" + + "totalPulses=" + totalPulses + + ", delayBetweenPulsesInSeconds=" + delayBetweenPulsesInSeconds + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/TempBasalDeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/TempBasalDeliverySchedule.java new file mode 100644 index 0000000000..a4c320b1d3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/TempBasalDeliverySchedule.java @@ -0,0 +1,69 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public class TempBasalDeliverySchedule extends DeliverySchedule implements IRawRepresentable { + + private final int secondsRemaining; + private final int firstSegmentPulses; + private final BasalDeliveryTable basalTable; + + public TempBasalDeliverySchedule(int secondsRemaining, int firstSegmentPulses, BasalDeliveryTable basalTable) { + this.secondsRemaining = secondsRemaining; + this.firstSegmentPulses = firstSegmentPulses; + this.basalTable = basalTable; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[0]; + rawData = ByteUtil.concat(rawData, basalTable.numSegments()); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(secondsRemaining << 3)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(firstSegmentPulses)); + for (BasalTableEntry entry : basalTable.getEntries()) { + rawData = ByteUtil.concat(rawData, entry.getRawData()); + } + return rawData; + } + + @Override + public InsulinScheduleType getType() { + return InsulinScheduleType.TEMP_BASAL_SCHEDULE; + } + + @Override + public int getChecksum() { + int checksum = 0; + byte[] rawData = getRawData(); + for (int i = 0; i < rawData.length && i < 5; i++) { + checksum += ByteUtil.convertUnsignedByteToInt(rawData[i]); + } + for (BasalTableEntry entry : basalTable.getEntries()) { + checksum += entry.getChecksum(); + } + + return checksum; + } + + public int getSecondsRemaining() { + return secondsRemaining; + } + + public int getFirstSegmentPulses() { + return firstSegmentPulses; + } + + public BasalDeliveryTable getBasalTable() { + return basalTable; + } + + @Override + public String toString() { + return "TempBasalDeliverySchedule{" + + "secondsRemaining=" + secondsRemaining + + ", firstSegmentPulses=" + firstSegmentPulses + + ", basalTable=" + basalTable + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSessionState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSessionState.java new file mode 100644 index 0000000000..22bd4293fe --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSessionState.java @@ -0,0 +1,299 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.state; + +import com.google.gson.Gson; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Duration; + +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FirmwareVersion; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.NonceState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmniCRC; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.sharedPreferences.SP; + + +public class PodSessionState extends PodState { + + @Inject AAPSLogger aapsLogger; + @Inject SP sp; + @Inject OmnipodUtil omnipodUtil; + + private final Map configuredAlerts; + private transient PodStateChangedHandler stateChangedHandler; + private DateTime activatedAt; + private DateTime expiresAt; + private final FirmwareVersion piVersion; + private final FirmwareVersion pmVersion; + private final int lot; + private final int tid; + private Double reservoirLevel; + private boolean suspended; + + private DateTimeZone timeZone; + private NonceState nonceState; + private SetupProgress setupProgress; + private AlertSet activeAlerts; + private BasalSchedule basalSchedule; + private DeliveryStatus lastDeliveryStatus; + + public PodSessionState(DateTimeZone timeZone, int address, FirmwareVersion piVersion, + FirmwareVersion pmVersion, int lot, int tid, int packetNumber, int messageNumber, HasAndroidInjector injector) { + super(address, messageNumber, packetNumber); + injectDaggerClass(injector); + if (timeZone == null) { + throw new IllegalArgumentException("Time zone can not be null"); + } + + suspended = false; + configuredAlerts = new HashMap<>(); + configuredAlerts.put(AlertSlot.SLOT7, AlertType.FINISH_SETUP_REMINDER); + + this.timeZone = timeZone; + this.setupProgress = SetupProgress.ADDRESS_ASSIGNED; + this.piVersion = piVersion; + this.pmVersion = pmVersion; + this.lot = lot; + this.tid = tid; + this.nonceState = new NonceState(lot, tid); + handleUpdates(); + } + + public void injectDaggerClass(HasAndroidInjector injector) { + injector.androidInjector().inject(this); + } + + public void setStateChangedHandler(PodStateChangedHandler handler) { + // FIXME this is an ugly workaround for not being able to serialize the PodStateChangedHandler + if (stateChangedHandler != null) { + throw new IllegalStateException("A PodStateChangedHandler has already been already registered"); + } + stateChangedHandler = handler; + } + + public AlertType getConfiguredAlertType(AlertSlot alertSlot) { + return configuredAlerts.get(alertSlot); + } + + public void putConfiguredAlert(AlertSlot alertSlot, AlertType alertType) { + configuredAlerts.put(alertSlot, alertType); + handleUpdates(); + } + + public void removeConfiguredAlert(AlertSlot alertSlot) { + configuredAlerts.remove(alertSlot); + handleUpdates(); + } + + public DateTime getActivatedAt() { + return activatedAt == null ? null : activatedAt.withZone(timeZone); + } + + public DateTime getExpiresAt() { + return expiresAt == null ? null : expiresAt.withZone(timeZone); + } + + public String getExpiryDateAsString() { + return expiresAt == null ? "???" : DateUtil.dateAndTimeString(expiresAt.toDate()); + } + + public FirmwareVersion getPiVersion() { + return piVersion; + } + + public FirmwareVersion getPmVersion() { + return pmVersion; + } + + public int getLot() { + return lot; + } + + public int getTid() { + return tid; + } + + public Double getReservoirLevel() { + return reservoirLevel; + } + + public synchronized void resyncNonce(int syncWord, int sentNonce, int sequenceNumber) { + int sum = (sentNonce & 0xFFFF) + + OmniCRC.crc16lookup[sequenceNumber] + + (this.lot & 0xFFFF) + + (this.tid & 0xFFFF); + int seed = ((sum & 0xFFFF) ^ syncWord); + + this.nonceState = new NonceState(lot, tid, (byte) (seed & 0xFF)); + handleUpdates(); + } + + public int getCurrentNonce() { + return nonceState.getCurrentNonce(); + } + + public synchronized void advanceToNextNonce() { + nonceState.advanceToNextNonce(); + handleUpdates(); + } + + public SetupProgress getSetupProgress() { + return setupProgress; + } + + public synchronized void setSetupProgress(SetupProgress setupProgress) { + if (setupProgress == null) { + throw new IllegalArgumentException("Setup state cannot be null"); + } + this.setupProgress = setupProgress; + handleUpdates(); + } + + public boolean isSuspended() { + return suspended; + } + + public boolean hasActiveAlerts() { + return activeAlerts != null && activeAlerts.size() > 0; + } + + public AlertSet getActiveAlerts() { + return activeAlerts; + } + + public DateTimeZone getTimeZone() { + return timeZone; + } + + public void setTimeZone(DateTimeZone timeZone) { + if (timeZone == null) { + throw new IllegalArgumentException("Time zone can not be null"); + } + this.timeZone = timeZone; + handleUpdates(); + } + + public DateTime getTime() { + DateTime now = DateTime.now(); + return now.withZone(timeZone); + } + + public Duration getScheduleOffset() { + DateTime now = getTime(); + DateTime startOfDay = new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), + 0, 0, 0, timeZone); + return new Duration(startOfDay, now); + } + + public boolean hasNonceState() { + return true; + } + + @Override + public void setPacketNumber(int packetNumber) { + super.setPacketNumber(packetNumber); + handleUpdates(); + } + + @Override + public void setMessageNumber(int messageNumber) { + super.setMessageNumber(messageNumber); + handleUpdates(); + } + + public BasalSchedule getBasalSchedule() { + return basalSchedule; + } + + public void setBasalSchedule(BasalSchedule basalSchedule) { + this.basalSchedule = basalSchedule; + handleUpdates(); + } + + public DeliveryStatus getLastDeliveryStatus() { + return lastDeliveryStatus; + } + + @Override + public void setFaultEvent(PodInfoFaultEvent faultEvent) { + super.setFaultEvent(faultEvent); + suspended = true; + handleUpdates(); + } + + @Override + public void updateFromStatusResponse(StatusResponse statusResponse) { + DateTime activatedAtCalculated = getTime().minus(statusResponse.getTimeActive()); + if (activatedAt == null) { + activatedAt = activatedAtCalculated; + } + DateTime expiresAtCalculated = activatedAtCalculated.plus(OmnipodConst.NOMINAL_POD_LIFE); + if (expiresAt == null || expiresAtCalculated.isBefore(expiresAt) || expiresAtCalculated.isAfter(expiresAt.plusMinutes(1))) { + expiresAt = expiresAtCalculated; + } + + boolean newSuspendedState = statusResponse.getDeliveryStatus() == DeliveryStatus.SUSPENDED; + if (suspended != newSuspendedState) { + aapsLogger.info(LTag.PUMPCOMM, "Updating pod suspended state in updateFromStatusResponse. newSuspendedState={}, statusResponse={}", newSuspendedState, statusResponse.toString()); + suspended = newSuspendedState; + } + activeAlerts = statusResponse.getAlerts(); + lastDeliveryStatus = statusResponse.getDeliveryStatus(); + reservoirLevel = statusResponse.getReservoirLevel(); + handleUpdates(); + } + + private void handleUpdates() { + Gson gson = omnipodUtil.getGsonInstance(); + String gsonValue = gson.toJson(this); + aapsLogger.info(LTag.PUMPCOMM, "PodSessionState-SP: Saved Session State to SharedPreferences: " + gsonValue); + sp.putString(OmnipodConst.Prefs.PodState, gsonValue); + if (stateChangedHandler != null) { + stateChangedHandler.handle(this); + } + } + + @Override + public String toString() { + return "PodSessionState{" + + "configuredAlerts=" + configuredAlerts + + ", stateChangedHandler=" + stateChangedHandler + + ", activatedAt=" + activatedAt + + ", expiresAt=" + expiresAt + + ", piVersion=" + piVersion + + ", pmVersion=" + pmVersion + + ", lot=" + lot + + ", tid=" + tid + + ", reservoirLevel=" + reservoirLevel + + ", suspended=" + suspended + + ", timeZone=" + timeZone + + ", nonceState=" + nonceState + + ", setupProgress=" + setupProgress + + ", activeAlerts=" + activeAlerts + + ", basalSchedule=" + basalSchedule + + ", lastDeliveryStatus=" + lastDeliveryStatus + + ", address=" + address + + ", packetNumber=" + packetNumber + + ", messageNumber=" + messageNumber + + ", faultEvent=" + faultEvent + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSetupState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSetupState.java new file mode 100644 index 0000000000..26b5802258 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSetupState.java @@ -0,0 +1,43 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.state; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; + +public class PodSetupState extends PodState { + public PodSetupState(int address, int packetNumber, int messageNumber) { + super(address, packetNumber, messageNumber); + } + + @Override + public boolean hasNonceState() { + return false; + } + + @Override + public int getCurrentNonce() { + throw new UnsupportedOperationException("PodSetupState does not have a nonce state"); + } + + @Override + public void advanceToNextNonce() { + throw new UnsupportedOperationException("PodSetupState does not have a nonce state"); + } + + @Override + public void resyncNonce(int syncWord, int sentNonce, int sequenceNumber) { + throw new UnsupportedOperationException("PodSetupState does not have a nonce state"); + } + + @Override + public void updateFromStatusResponse(StatusResponse statusResponse) { + } + + @Override + public String toString() { + return "PodSetupState{" + + "address=" + address + + ", packetNumber=" + packetNumber + + ", messageNumber=" + messageNumber + + ", faultEvent=" + faultEvent + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodState.java new file mode 100644 index 0000000000..2e19de1286 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodState.java @@ -0,0 +1,68 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.state; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; + +public abstract class PodState { + protected final int address; + protected int packetNumber; + protected int messageNumber; + + protected PodInfoFaultEvent faultEvent; + + public PodState(int address, int packetNumber, int messageNumber) { + this.address = address; + this.packetNumber = packetNumber; + this.messageNumber = messageNumber; + } + + public abstract boolean hasNonceState(); + + public abstract int getCurrentNonce(); + + public abstract void advanceToNextNonce(); + + public abstract void resyncNonce(int syncWord, int sentNonce, int sequenceNumber); + + public abstract void updateFromStatusResponse(StatusResponse statusResponse); + + public int getAddress() { + return address; + } + + public int getMessageNumber() { + return messageNumber; + } + + public void setMessageNumber(int messageNumber) { + this.messageNumber = messageNumber; + } + + public int getPacketNumber() { + return packetNumber; + } + + public void setPacketNumber(int packetNumber) { + this.packetNumber = packetNumber; + } + + public void increaseMessageNumber(int increment) { + setMessageNumber((messageNumber + increment) & 0b1111); + } + + public void increasePacketNumber(int increment) { + setPacketNumber((packetNumber + increment) & 0b11111); + } + + public boolean hasFaultEvent() { + return faultEvent != null; + } + + public PodInfoFaultEvent getFaultEvent() { + return faultEvent; + } + + public void setFaultEvent(PodInfoFaultEvent faultEvent) { + this.faultEvent = faultEvent; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodStateChangedHandler.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodStateChangedHandler.java new file mode 100644 index 0000000000..6e706131a2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodStateChangedHandler.java @@ -0,0 +1,6 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.state; + +@FunctionalInterface +public interface PodStateChangedHandler { + void handle(PodSessionState podState); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodHistoryActivity.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodHistoryActivity.java new file mode 100644 index 0000000000..d98721ff15 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodHistoryActivity.java @@ -0,0 +1,339 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs; + +import android.os.Bundle; +import android.os.SystemClock; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.List; + +import javax.inject.Inject; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.activities.NoSplashAppCompatActivity; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpHistoryEntryGroup; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; +import info.nightscout.androidaps.plugins.pump.common.utils.ProfileUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.db.PodHistory; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; +import info.nightscout.androidaps.utils.resources.ResourceHelper; + +public class PodHistoryActivity extends NoSplashAppCompatActivity { + + @Inject AAPSLogger aapsLogger; + @Inject OmnipodUtil omnipodUtil; + @Inject ResourceHelper resourceHelper; + + private Spinner historyTypeSpinner; + private TextView statusView; + private RecyclerView recyclerView; + private LinearLayoutManager linearLayoutManager; + + static TypeList showingType = null; + static PumpHistoryEntryGroup selectedGroup = PumpHistoryEntryGroup.All; + List fullHistoryList = new ArrayList<>(); + List filteredHistoryList = new ArrayList<>(); + + RecyclerViewAdapter recyclerViewAdapter; + boolean manualChange = false; + + List typeListFull; + + + public PodHistoryActivity() { + super(); + } + + + private void prepareData() { + GregorianCalendar gc = new GregorianCalendar(); + gc.add(Calendar.HOUR_OF_DAY, -24); + + MainApp.getDbHelper().getPodHistoryFromTime(gc.getTimeInMillis(), false); + + fullHistoryList.addAll(MainApp.getDbHelper().getPodHistoryFromTime(gc.getTimeInMillis(), true)); + } + + + private void filterHistory(PumpHistoryEntryGroup group) { + + this.filteredHistoryList.clear(); + + aapsLogger.debug(LTag.PUMP, "Items on full list: {}", fullHistoryList.size()); + + if (group == PumpHistoryEntryGroup.All) { + this.filteredHistoryList.addAll(fullHistoryList); + } else { + for (PodHistory pumpHistoryEntry : fullHistoryList) { + if (pumpHistoryEntry.getPodDbEntryType().getGroup() == group) { + this.filteredHistoryList.add(pumpHistoryEntry); + } + } + } + + if (this.recyclerViewAdapter != null) { + this.recyclerViewAdapter.setHistoryList(this.filteredHistoryList); + this.recyclerViewAdapter.notifyDataSetChanged(); + } + + aapsLogger.debug(LTag.PUMP, "Items on filtered list: {}", filteredHistoryList.size()); + } + + + @Override + protected void onResume() { + super.onResume(); + filterHistory(selectedGroup); + setHistoryTypeSpinner(); + } + + + private void setHistoryTypeSpinner() { + this.manualChange = true; + + for (int i = 0; i < typeListFull.size(); i++) { + if (typeListFull.get(i).entryGroup == selectedGroup) { + historyTypeSpinner.setSelection(i); + break; + } + } + + SystemClock.sleep(200); + this.manualChange = false; + } + + + @Override + protected void onPause() { + super.onPause(); + } + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.omnipod_pod_history_activity); + + historyTypeSpinner = findViewById(R.id.omnipod_historytype); + statusView = findViewById(R.id.omnipod_historystatus); + recyclerView = findViewById(R.id.omnipod_history_recyclerview); + recyclerView.setHasFixedSize(true); + + linearLayoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(linearLayoutManager); + + prepareData(); + + recyclerViewAdapter = new RecyclerViewAdapter(filteredHistoryList); + recyclerView.setAdapter(recyclerViewAdapter); + + statusView.setVisibility(View.GONE); + + typeListFull = getTypeList(PumpHistoryEntryGroup.getList()); + + ArrayAdapter spinnerAdapter = new ArrayAdapter<>(this, R.layout.spinner_centered, typeListFull); + historyTypeSpinner.setAdapter(spinnerAdapter); + + historyTypeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (manualChange) + return; + TypeList selected = (TypeList) historyTypeSpinner.getSelectedItem(); + showingType = selected; + selectedGroup = selected.entryGroup; + filterHistory(selectedGroup); + } + + + @Override + public void onNothingSelected(AdapterView parent) { + if (manualChange) + return; + filterHistory(PumpHistoryEntryGroup.All); + } + }); + + } + + + private List getTypeList(List list) { + + ArrayList typeList = new ArrayList<>(); + + for (PumpHistoryEntryGroup pumpHistoryEntryGroup : list) { + typeList.add(new TypeList(pumpHistoryEntryGroup)); + } + + return typeList; + } + + public static class TypeList { + + PumpHistoryEntryGroup entryGroup; + String name; + + TypeList(PumpHistoryEntryGroup entryGroup) { + this.entryGroup = entryGroup; + this.name = entryGroup.getTranslated(); + } + + @NotNull + @Override + public String toString() { + return name; + } + } + + public class RecyclerViewAdapter extends RecyclerView.Adapter { + + List historyList; + + RecyclerViewAdapter(List historyList) { + this.historyList = historyList; + } + + + public void setHistoryList(List historyList) { + this.historyList = historyList; + Collections.sort(this.historyList); + } + + + @NotNull + @Override + public HistoryViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.omnipod_pod_history_item, // + viewGroup, false); + return new HistoryViewHolder(v); + } + + + @Override + public void onBindViewHolder(@NotNull HistoryViewHolder holder, int position) { + PodHistory record = historyList.get(position); + + if (record != null) { + holder.timeView.setText(record.getDateTimeString()); + holder.typeView.setText(record.getPodDbEntryType().getResourceId()); + setValue(record, holder.valueView); + } + } + + + private void setValue(PodHistory historyEntry, TextView valueView) { + //valueView.setText(""); + + if (historyEntry.isSuccess()) { + switch (historyEntry.getPodDbEntryType()) { + + case SetTemporaryBasal: { + TempBasalPair tempBasalPair = omnipodUtil.getGsonInstance().fromJson(historyEntry.getData(), TempBasalPair.class); + valueView.setText(resourceHelper.gs(R.string.omnipod_cmd_tbr_value, tempBasalPair.getInsulinRate(), tempBasalPair.getDurationMinutes())); + } + break; + + case FillCannulaSetBasalProfile: + case SetBasalSchedule: { + if (historyEntry.getData() != null) { + setProfileValue(historyEntry.getData(), valueView); + } + } + break; + + case SetBolus: { + if (historyEntry.getData().contains(";")) { + String[] splitVal = historyEntry.getData().split(";"); + valueView.setText(resourceHelper.gs(R.string.omnipod_cmd_bolus_value_with_carbs, Double.valueOf(splitVal[0]), Double.valueOf(splitVal[1]))); + } else { + valueView.setText(resourceHelper.gs(R.string.omnipod_cmd_bolus_value, Double.valueOf(historyEntry.getData()))); + } + } + break; + + case GetPodStatus: + case GetPodInfo: + case SetTime: + case PairAndPrime: + case CancelTemporaryBasal: + case CancelTemporaryBasalForce: + case ConfigureAlerts: + case CancelBolus: + case DeactivatePod: + case ResetPodState: + case AcknowledgeAlerts: + case SuspendDelivery: + case ResumeDelivery: + case UnknownEntryType: + default: + valueView.setText(""); + break; + + } + } else { + valueView.setText(historyEntry.getData()); + } + + } + + private void setProfileValue(String data, TextView valueView) { + aapsLogger.debug(LTag.PUMP, "Profile json:\n" + data); + + try { + Profile.ProfileValue[] profileValuesArray = omnipodUtil.getGsonInstance().fromJson(data, Profile.ProfileValue[].class); + valueView.setText(ProfileUtil.getBasalProfilesDisplayable(profileValuesArray, PumpType.Insulet_Omnipod)); + } catch (Exception e) { + aapsLogger.error(LTag.PUMP, "Problem parsing Profile json. Ex: {}, Data:\n{}", e.getMessage(), data); + valueView.setText(""); + } + } + + + @Override + public int getItemCount() { + return historyList.size(); + } + + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + } + + + class HistoryViewHolder extends RecyclerView.ViewHolder { + + TextView timeView; + TextView typeView; + TextView valueView; + + HistoryViewHolder(View itemView) { + super(itemView); + timeView = itemView.findViewById(R.id.omnipod_history_time); + typeView = itemView.findViewById(R.id.omnipod_history_source); + valueView = itemView.findViewById(R.id.omnipod_history_description); + } + } + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodManagementActivity.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodManagementActivity.kt new file mode 100644 index 0000000000..c3a7bfeccd --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodManagementActivity.kt @@ -0,0 +1,170 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs + +import android.content.Intent +import android.os.Bundle +import com.atech.android.library.wizardpager.WizardPagerActivity +import com.atech.android.library.wizardpager.WizardPagerContext +import com.atech.android.library.wizardpager.data.WizardPagerSettings +import com.atech.android.library.wizardpager.defs.WizardStepsWayType +import dagger.android.HasAndroidInjector +import info.nightscout.androidaps.R +import info.nightscout.androidaps.activities.NoSplashAppCompatActivity +import info.nightscout.androidaps.events.EventRefreshOverview +import info.nightscout.androidaps.interfaces.CommandQueueProvider +import info.nightscout.androidaps.logging.AAPSLogger +import info.nightscout.androidaps.plugins.bus.RxBusWrapper +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.defs.PodActionType +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model.FullInitPodWizardModel +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model.RemovePodWizardModel +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model.ShortInitPodWizardModel +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages.InitPodRefreshAction +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState +import info.nightscout.androidaps.plugins.pump.omnipod.driver.comm.AapsOmnipodManager +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil +import info.nightscout.androidaps.utils.FabricPrivacy +import info.nightscout.androidaps.utils.alertDialogs.OKDialog +import info.nightscout.androidaps.utils.resources.ResourceHelper +import kotlinx.android.synthetic.main.omnipod_pod_mgmt.* +import javax.inject.Inject + +/** + * Created by andy on 30/08/2019 + */ +class PodManagementActivity : NoSplashAppCompatActivity() { + + @Inject lateinit var rxBus: RxBusWrapper + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var resourceHelper: ResourceHelper + @Inject lateinit var profileFunction: ProfileFunction + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var commandQueue: CommandQueueProvider + @Inject lateinit var omnipodUtil: OmnipodUtil + @Inject lateinit var injector: HasAndroidInjector + + private var initPodChanged = false + private var podSessionFullyInitalized = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.omnipod_pod_mgmt) + + initpod_init_pod.setOnClickListener { + initPodAction() + initPodChanged = true + } + + initpod_remove_pod.setOnClickListener { + removePodAction() + initPodChanged = true + } + + initpod_reset_pod.setOnClickListener { + resetPodAction() + initPodChanged = true + } + + initpod_pod_history.setOnClickListener { + showPodHistory() + } + + refreshButtons(); + } + + override fun onDestroy() { + super.onDestroy() + + if (initPodChanged) { + rxBus.send(EventOmnipodPumpValuesChanged()) + rxBus.send(EventRefreshOverview("Omnipod Pod Management")) + } + } + + fun initPodAction() { + + val pagerSettings = WizardPagerSettings() + var refreshAction = InitPodRefreshAction(injector, this, PodActionType.InitPod) + + pagerSettings.setWizardStepsWayType(WizardStepsWayType.CancelNext) + pagerSettings.setFinishStringResourceId(R.string.close) + pagerSettings.setFinishButtonBackground(R.drawable.finish_background) + pagerSettings.setNextButtonBackground(R.drawable.selectable_item_background) + pagerSettings.setBackStringResourceId(R.string.cancel) + pagerSettings.cancelAction = refreshAction + pagerSettings.finishAction = refreshAction + + val wizardPagerContext = WizardPagerContext.getInstance() + + wizardPagerContext.clearContext() + wizardPagerContext.pagerSettings = pagerSettings + val podSessionState = omnipodUtil.getPodSessionState() + val isFullInit = podSessionState == null || podSessionState.setupProgress.isBefore(SetupProgress.PRIMING_FINISHED) + if (isFullInit) { + wizardPagerContext.wizardModel = FullInitPodWizardModel(applicationContext) + } else { + wizardPagerContext.wizardModel = ShortInitPodWizardModel(applicationContext) + } + + val myIntent = Intent(this@PodManagementActivity, WizardPagerActivity::class.java) + this@PodManagementActivity.startActivity(myIntent) + } + + fun removePodAction() { + val pagerSettings = WizardPagerSettings() + var refreshAction = InitPodRefreshAction(injector, this, PodActionType.RemovePod) + + pagerSettings.setWizardStepsWayType(WizardStepsWayType.CancelNext) + pagerSettings.setFinishStringResourceId(R.string.close) + pagerSettings.setFinishButtonBackground(R.drawable.finish_background) + pagerSettings.setNextButtonBackground(R.drawable.selectable_item_background) + pagerSettings.setBackStringResourceId(R.string.cancel) + pagerSettings.cancelAction = refreshAction + pagerSettings.finishAction = refreshAction + + val wizardPagerContext = WizardPagerContext.getInstance(); + + wizardPagerContext.clearContext() + wizardPagerContext.pagerSettings = pagerSettings + wizardPagerContext.wizardModel = RemovePodWizardModel(applicationContext) + + val myIntent = Intent(this@PodManagementActivity, WizardPagerActivity::class.java) + this@PodManagementActivity.startActivity(myIntent) + + } + + fun resetPodAction() { + OKDialog.showConfirmation(this, + resourceHelper.gs(R.string.omnipod_cmd_reset_pod_desc), Thread { + AapsOmnipodManager.getInstance().resetPodStatus() + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_NoPod) + refreshButtons() + }) + } + + fun showPodHistory() { +// OKDialog.showConfirmation(this, +// MainApp.gs(R.string.omnipod_cmd_pod_history_na), null) + + startActivity(Intent(applicationContext, PodHistoryActivity::class.java)) + } + + fun refreshButtons() { + initpod_init_pod.isEnabled = (omnipodUtil.getPodSessionState() == null || + omnipodUtil.getPodSessionState().getSetupProgress().isBefore(SetupProgress.COMPLETED)) + + val isPodSessionActive = (omnipodUtil.getPodSessionState() != null) + + initpod_remove_pod.isEnabled = isPodSessionActive + initpod_reset_pod.isEnabled = isPodSessionActive + + if (omnipodUtil.getDriverState() == OmnipodDriverState.NotInitalized) { + // if rileylink is not running we disable all operations + initpod_init_pod.isEnabled = false + initpod_remove_pod.isEnabled = false + initpod_reset_pod.isEnabled = false + } + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/defs/PodActionType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/defs/PodActionType.java new file mode 100644 index 0000000000..674703855a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/defs/PodActionType.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.defs; + +public enum PodActionType { + InitPod, + RemovePod, + ResetPod +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionFragment.java new file mode 100644 index 0000000000..7cb106ca47 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionFragment.java @@ -0,0 +1,241 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod; + +import android.app.Activity; +import android.graphics.Color; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.fragment.app.Fragment; + +import com.atech.android.library.wizardpager.util.WizardPagesUtil; +import com.tech.freak.wizardpager.model.Page; +import com.tech.freak.wizardpager.ui.PageFragmentCallbacks; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.inject.Inject; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitReceiver; + +/** + * Created by andy on 12/11/2019 + */ +public class InitActionFragment extends Fragment implements PodInitReceiver { + private static final String ARG_KEY = "key"; + + protected PageFragmentCallbacks mCallbacks; + protected String mKey; + protected InitActionPage mPage; + + protected ProgressBar progressBar; + protected TextView errorView; + protected Button retryButton; + + protected PodInitActionType podInitActionType; + protected List children; + protected Map mapCheckBoxes; + protected InitActionFragment instance; + + protected PumpEnactResult callResult; + + + public static InitActionFragment create(String key, PodInitActionType podInitActionType) { + Bundle args = new Bundle(); + args.putString(ARG_KEY, key); + + InitActionFragment fragment = new InitActionFragment(); + fragment.setArguments(args); + fragment.setPodInitActionType(podInitActionType); + return fragment; + } + + public InitActionFragment() { + this.instance = this; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + mKey = args.getString(ARG_KEY); + mPage = (InitActionPage) mCallbacks.onGetPage(mKey); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.omnipod_initpod_init_action, container, false); + WizardPagesUtil.setTitle(mPage, rootView); + + this.progressBar = rootView.findViewById(R.id.initAction_progressBar); + this.errorView = rootView.findViewById(R.id.initAction_textErrorMessage); + + TextView headerView = rootView.findViewById(R.id.initAction_header); + + LinearLayout linearLayout = rootView.findViewById(R.id.initAction_ItemsHolder); + + children = podInitActionType.getChildren(); + mapCheckBoxes = new HashMap<>(); + + for (PodInitActionType child : children) { + + CheckBox checkBox1 = new CheckBox(getContext()); + checkBox1.setText(child.getResourceId()); + checkBox1.setClickable(false); + checkBox1.setTextAppearance(R.style.WizardPagePodListItem); + checkBox1.setHeight(120); + checkBox1.setTextSize(15); + checkBox1.setTextColor(headerView.getTextColors().getDefaultColor()); + + linearLayout.addView(checkBox1); + + mapCheckBoxes.put(child, checkBox1); + } + + if (podInitActionType == PodInitActionType.FillCannulaSetBasalProfileWizardStep) { + headerView.setText(R.string.omnipod_init_pod_wizard_step4_action_header); + } else if (podInitActionType == PodInitActionType.DeactivatePodWizardStep) { + headerView.setText(R.string.omnipod_remove_pod_wizard_step2_action_header); + } + + this.retryButton = rootView.findViewById(R.id.initAction_RetryButton); + + this.retryButton.setOnClickListener(view -> { + + getActivity().runOnUiThread(() -> { + for (PodInitActionType actionType : mapCheckBoxes.keySet()) { + mapCheckBoxes.get(actionType).setChecked(false); + mapCheckBoxes.get(actionType).setTextColor(headerView.getTextColors().getDefaultColor()); + } + }); + + new InitPodTask(instance).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + }); + + return rootView; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (!(activity instanceof PageFragmentCallbacks)) { + throw new ClassCastException("Activity must implement PageFragmentCallbacks"); + } + + mCallbacks = (PageFragmentCallbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + + + public PodInitActionType getPodInitActionType() { + return podInitActionType; + } + + + public void setPodInitActionType(PodInitActionType podInitActionType) { + this.podInitActionType = podInitActionType; + } + + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + //System.out.println("ACTION: setUserVisibleHint="+ isVisibleToUser); + if (isVisibleToUser) { + //System.out.println("ACTION: Visible"); + new InitPodTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + + } else { + System.out.println("ACTION: Not visible"); + } + } + + public void actionOnReceiveResponse(String result) { +// System.out.println("ACTION: actionOnReceiveResponse: " + result); +// +// boolean isOk = callResult.success; +// +// progressBar.setVisibility(View.GONE); +// +// if (!isOk) { +// errorView.setVisibility(View.VISIBLE); +// errorView.setText(callResult.comment); +// } +// +// mPage.setActionCompleted(isOk); +// +// mPage.getData().putString(Page.SIMPLE_DATA_KEY, "ddd"); +// mPage.notifyDataChanged(); + } + + @Override + public void returnInitTaskStatus(PodInitActionType podInitActionType, boolean isSuccess, String errorMessage) { + if (podInitActionType.isParent()) { + for (PodInitActionType actionType : mapCheckBoxes.keySet()) { + setCheckBox(actionType, isSuccess); + } + + // special handling for init + processOnFinishedActions(isSuccess, errorMessage); + + } else { + setCheckBox(podInitActionType, isSuccess); + } + } + + + private void processOnFinishedActions(boolean isOk, String errorMessage) { + + getActivity().runOnUiThread(() -> { + + progressBar.setVisibility(View.GONE); + + if (!isOk) { + errorView.setVisibility(View.VISIBLE); + errorView.setText(errorMessage); + + retryButton.setVisibility(View.VISIBLE); + } + + mPage.setActionCompleted(isOk); + + mPage.getData().putString(Page.SIMPLE_DATA_KEY, UUID.randomUUID().toString()); + mPage.notifyDataChanged(); + + }); + + } + + + public void setCheckBox(PodInitActionType podInitActionType, boolean isSuccess) { + getActivity().runOnUiThread(() -> { + mapCheckBoxes.get(podInitActionType).setChecked(isSuccess); + mapCheckBoxes.get(podInitActionType).setTextColor(isSuccess ? Color.rgb(34, 135, 91) : + Color.rgb(168, 36, 15)); + }); + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionPage.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionPage.java new file mode 100644 index 0000000000..8b34b99193 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionPage.java @@ -0,0 +1,73 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod; + +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; + +import com.tech.freak.wizardpager.model.ModelCallbacks; +import com.tech.freak.wizardpager.model.Page; +import com.tech.freak.wizardpager.model.ReviewItem; + +import java.util.ArrayList; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; + + +/** + * Created by andy on 12/11/2019 + * + * This page is for InitPod and RemovePod, but Fragments called for this 2 actions are different + */ +public class InitActionPage extends Page { + + protected PodInitActionType podInitActionType; + + protected boolean actionCompleted = false; + protected boolean actionSuccess = false; + + public InitActionPage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + public InitActionPage(ModelCallbacks callbacks, @StringRes int titleId, PodInitActionType podInitActionType) { + super(callbacks, titleId); + this.podInitActionType = podInitActionType; + } + + @Override + public Fragment createFragment() { + return InitActionFragment.create(getKey(), this.podInitActionType); + } + + @Override + public void getReviewItems(ArrayList dest) { + } + + @Override + public boolean isCompleted() { + return actionCompleted; + } + + public void setActionCompleted(boolean success) { + this.actionCompleted = success; + this.actionSuccess = success; + } + + /** + * This is used just if we want to override default behavior (for example when we enter Page we want prevent any action, until something happens. + * + * @return + */ + public boolean isBackActionPossible() { + return actionCompleted; + } + + /** + * This is used just if we want to override default behavior (for example when we enter Page we want prevent any action, until something happens. + * + * @return + */ + public boolean isNextActionPossible() { + return actionSuccess; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitPodTask.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitPodTask.java new file mode 100644 index 0000000000..32f6eabac4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitPodTask.java @@ -0,0 +1,62 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod; + +import android.os.AsyncTask; +import android.os.SystemClock; +import android.view.View; + +import javax.inject.Inject; + +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.comm.AapsOmnipodManager; + +/** + * Created by andy on 11/12/2019 + */ +public class InitPodTask extends AsyncTask { + + @Inject ProfileFunction profileFunction; + private InitActionFragment initActionFragment; + + public InitPodTask(InitActionFragment initActionFragment) { + + this.initActionFragment = initActionFragment; + } + + protected void onPreExecute() { + initActionFragment.progressBar.setVisibility(View.VISIBLE); + initActionFragment.errorView.setVisibility(View.GONE); + initActionFragment.retryButton.setVisibility(View.GONE); + } + + @Override + protected String doInBackground(Void... params) { + + if (initActionFragment.podInitActionType == PodInitActionType.PairAndPrimeWizardStep) { + initActionFragment.callResult = AapsOmnipodManager.getInstance().initPod( + initActionFragment.podInitActionType, + initActionFragment.instance, + null + ); + } else if (initActionFragment.podInitActionType == PodInitActionType.FillCannulaSetBasalProfileWizardStep) { + initActionFragment.callResult = AapsOmnipodManager.getInstance().initPod( + initActionFragment.podInitActionType, + initActionFragment.instance, + profileFunction.getProfile() + ); + } else if (initActionFragment.podInitActionType == PodInitActionType.DeactivatePodWizardStep) { + initActionFragment.callResult = AapsOmnipodManager.getInstance().deactivatePod(initActionFragment.instance); + } + + return "OK"; + } + + @Override + protected void onPostExecute(String result) { + super.onPostExecute(result); + + initActionFragment.actionOnReceiveResponse(result); + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/FullInitPodWizardModel.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/FullInitPodWizardModel.java new file mode 100644 index 0000000000..a2e7383091 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/FullInitPodWizardModel.java @@ -0,0 +1,48 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model; + +import android.content.Context; + +import com.atech.android.library.wizardpager.model.DisplayTextPage; +import com.tech.freak.wizardpager.model.PageList; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod.InitActionPage; + +/** + * Created by andy on 12/11/2019 + */ +// Full init pod wizard model +// Cannot be merged with ShortInitPodWizardModel, because we can't set any instance variables +// before the onNewRootPageList method is called (which happens in the super constructor) +public class FullInitPodWizardModel extends InitPodWizardModel { + + public FullInitPodWizardModel(Context context) { + super(context); + } + + @Override + protected PageList onNewRootPageList() { + return new PageList( + new DisplayTextPage(this, + R.string.omnipod_init_pod_wizard_step1_title, + R.string.omnipod_init_pod_wizard_step1_desc, + R.style.WizardPagePodContent).setRequired(true).setCancelReason("None"), + + new InitActionPage(this, + R.string.omnipod_init_pod_wizard_step2_title, + PodInitActionType.PairAndPrimeWizardStep + ).setRequired(true).setCancelReason("Cancel"), + + new DisplayTextPage(this, + R.string.omnipod_init_pod_wizard_step3_title, + R.string.omnipod_init_pod_wizard_step3_desc, + R.style.WizardPagePodContent).setRequired(true).setCancelReason("Cancel"), + + new InitActionPage(this, + R.string.omnipod_init_pod_wizard_step4_title, + PodInitActionType.FillCannulaSetBasalProfileWizardStep + ).setRequired(true).setCancelReason("Cancel") + ); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/InitPodWizardModel.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/InitPodWizardModel.java new file mode 100644 index 0000000000..f5208a48d8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/InitPodWizardModel.java @@ -0,0 +1,21 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import com.tech.freak.wizardpager.model.AbstractWizardModel; + +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages.PodInfoFragment; + +public abstract class InitPodWizardModel extends AbstractWizardModel { + public InitPodWizardModel(Context context) { + super(context); + } + + @Override + public Fragment getReviewFragment() { + PodInfoFragment.isInitPod = true; + return new PodInfoFragment(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/RemovePodWizardModel.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/RemovePodWizardModel.java new file mode 100644 index 0000000000..57a7f81414 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/RemovePodWizardModel.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012 Roman Nurik + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import com.atech.android.library.wizardpager.model.DisplayTextPage; +import com.tech.freak.wizardpager.model.AbstractWizardModel; +import com.tech.freak.wizardpager.model.PageList; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages.PodInfoFragment; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.removepod.RemovePodActionPage; + +/** + * Created by andy on 12/11/2019 + */ +public class RemovePodWizardModel extends AbstractWizardModel { + + public RemovePodWizardModel(Context context) { + super(context); + } + + @Override + protected PageList onNewRootPageList() { + + return new PageList( + + new DisplayTextPage(this, + R.string.omnipod_remove_pod_wizard_step1_title, + R.string.omnipod_remove_pod_wizard_step1_desc, + R.style.WizardPagePodContent).setRequired(true).setCancelReason("None"), + + new RemovePodActionPage(this, + R.string.omnipod_remove_pod_wizard_step2_title, + PodInitActionType.DeactivatePodWizardStep + ).setRequired(true).setCancelReason("Cancel") + + ); + } + + + public Fragment getReviewFragment() { + PodInfoFragment.isInitPod = false; + return new PodInfoFragment(); + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/ShortInitPodWizardModel.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/ShortInitPodWizardModel.java new file mode 100644 index 0000000000..5499fdf8ec --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/ShortInitPodWizardModel.java @@ -0,0 +1,39 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model; + +import android.content.Context; + +import com.atech.android.library.wizardpager.model.DisplayTextPage; +import com.tech.freak.wizardpager.model.PageList; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod.InitActionPage; + +/** + * Created by andy on 12/11/2019 + */ +// Init pod wizard model without the pair and prime step +// Cannot be merged with FullInitPodWizardModel, because we can't set any instance variables +// before the onNewRootPageList method is called (which happens in the super constructor) +public class ShortInitPodWizardModel extends InitPodWizardModel { + + public ShortInitPodWizardModel(Context context) { + super(context); + } + + @Override + protected PageList onNewRootPageList() { + return new PageList( + new DisplayTextPage(this, + R.string.omnipod_init_pod_wizard_step3_title, + R.string.omnipod_init_pod_wizard_step3_desc, + R.style.WizardPagePodContent).setRequired(true).setCancelReason("Cancel"), + + new InitActionPage(this, + R.string.omnipod_init_pod_wizard_step4_title, + PodInitActionType.FillCannulaSetBasalProfileWizardStep + ).setRequired(true).setCancelReason("Cancel") + ); + + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/InitPodRefreshAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/InitPodRefreshAction.java new file mode 100644 index 0000000000..37c6d420f7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/InitPodRefreshAction.java @@ -0,0 +1,100 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages; + +import com.atech.android.library.wizardpager.defs.action.AbstractCancelAction; +import com.atech.android.library.wizardpager.defs.action.FinishActionInterface; + +import org.json.JSONException; +import org.json.JSONObject; + +import javax.inject.Inject; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.db.CareportalEvent; +import info.nightscout.androidaps.db.Source; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.general.nsclient.NSUpload; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.PodManagementActivity; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.defs.PodActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.sharedPreferences.SP; + + +/** + * Created by andy on 12/11/2019 + */ +public class InitPodRefreshAction extends AbstractCancelAction implements FinishActionInterface { + + private PodManagementActivity podManagementActivity; + private PodActionType actionType; + + @Inject OmnipodUtil omnipodUtil; + @Inject AAPSLogger aapsLogger; + @Inject SP sp; + + public InitPodRefreshAction(HasAndroidInjector injector, PodManagementActivity podManagementActivity, PodActionType actionType) { + injector.androidInjector().inject(this); + this.podManagementActivity = podManagementActivity; + this.actionType = actionType; + } + + @Override + public void execute(String cancelReason) { + if (cancelReason != null && cancelReason.trim().length() > 0) { + this.cancelActionText = cancelReason; + } + + if (this.cancelActionText.equals("Cancel")) { + //AapsOmnipodManager.getInstance().resetPodStatus(); + } + + podManagementActivity.refreshButtons(); + } + + @Override + public void execute() { + if (actionType == PodActionType.InitPod) { + if (omnipodUtil.getPodSessionState().getSetupProgress().isBefore(SetupProgress.COMPLETED)) { + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_PodInitializing); + } else { + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_PodAttached); + uploadCareportalEvent(System.currentTimeMillis(), CareportalEvent.SITECHANGE); + } + } else { + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_NoPod); + } + + podManagementActivity.refreshButtons(); + } + + private void uploadCareportalEvent(long date, String event) { + if (MainApp.getDbHelper().getCareportalEventFromTimestamp(date) != null) + return; + try { + JSONObject data = new JSONObject(); + String enteredBy = sp.getString("careportal_enteredby", ""); + if (!enteredBy.equals("")) data.put("enteredBy", enteredBy); + data.put("created_at", DateUtil.toISOString(date)); + data.put("eventType", event); + CareportalEvent careportalEvent = new CareportalEvent(); + careportalEvent.date = date; + careportalEvent.source = Source.USER; + careportalEvent.eventType = event; + careportalEvent.json = data.toString(); + MainApp.getDbHelper().createOrUpdate(careportalEvent); + NSUpload.uploadCareportalEntryToNS(data); + } catch (JSONException e) { + aapsLogger.error(LTag.PUMPCOMM, "Unhandled exception when uploading SiteChange event.", e); + } + } + + + @Override + public String getFinishActionText() { + return "Finish_OK"; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoFragment.java new file mode 100644 index 0000000000..65d032739d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoFragment.java @@ -0,0 +1,189 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.tech.freak.wizardpager.model.ReviewItem; +import com.tech.freak.wizardpager.ui.PageFragmentCallbacks; + +import java.util.ArrayList; + +import javax.inject.Inject; + +import dagger.android.support.DaggerFragment; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; + + +/** + * Created by andy on 12/11/2019 + */ +public class PodInfoFragment extends DaggerFragment { + private static final String ARG_KEY = "key"; + + @Inject OmnipodUtil omnipodUtil; + + private PageFragmentCallbacks mCallbacks; + private String mKey; + private PodInfoPage mPage; + public static boolean isInitPod = false; + private ArrayList mCurrentReviewItems; + + public static PodInfoFragment create(String key, boolean initPod) { + Bundle args = new Bundle(); + args.putString(ARG_KEY, key); + isInitPod = initPod; + + PodInfoFragment fragment = new PodInfoFragment(); + fragment.setArguments(args); + return fragment; + } + + public PodInfoFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.omnipod_initpod_pod_info, container, false); + + TextView titleView = (TextView) rootView.findViewById(R.id.podInfoTitle); + titleView.setText(R.string.omnipod_init_pod_wizard_pod_info_title); + titleView.setTextColor(getResources().getColor(com.tech.freak.wizardpager.R.color.review_green)); + + TextView headerText = rootView.findViewById(R.id.podInfoText); + headerText.setText(isInitPod ? // + R.string.omnipod_init_pod_wizard_pod_info_init_pod_description : // + R.string.omnipod_init_pod_wizard_pod_info_remove_pod_description); + + + if (isInitPod) { + if (createDataOfPod()) { + + ListView listView = (ListView) rootView.findViewById(R.id.podInfoList); + listView.setAdapter(new PodInfoAdapter(mCurrentReviewItems, getContext())); + listView.setChoiceMode(ListView.CHOICE_MODE_NONE); + } + } + + + return rootView; + } + + private boolean createDataOfPod() { + + PodSessionState podSessionState = omnipodUtil.getPodSessionState(); + +// PodSessionState podSessionState = new PodSessionState(DateTimeZone.UTC, +// 483748738, +// new DateTime(), +// new FirmwareVersion(1,0,0), +// new FirmwareVersion(1,0,0), +// 574875, +// 5487584, +// 1, +// 1 +// ); + + if (podSessionState == null) + return false; + + mCurrentReviewItems = new ArrayList<>(); + mCurrentReviewItems.add(new ReviewItem("Pod Address", "" + podSessionState.getAddress(), "33")); + mCurrentReviewItems.add(new ReviewItem("Activated At", podSessionState.getActivatedAt().toString("dd.MM.yyyy HH:mm:ss"), "34")); + mCurrentReviewItems.add(new ReviewItem("Firmware Version", podSessionState.getPiVersion().toString(), "35")); + mCurrentReviewItems.add(new ReviewItem("LOT", "" + podSessionState.getLot(), "36")); + + return true; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (!(activity instanceof PageFragmentCallbacks)) { + throw new ClassCastException("Activity must implement PageFragmentCallbacks"); + } + + mCallbacks = (PageFragmentCallbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } + + + private class PodInfoAdapter extends ArrayAdapter { + + private ArrayList dataSet; + Context mContext; + private int lastPosition = -1; + + // View lookup cache + + public PodInfoAdapter(ArrayList data, Context context) { + super(context, com.tech.freak.wizardpager.R.layout.list_item_review, data); + this.dataSet = data; + this.mContext = context; + } + + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // Get the data item for this position + ReviewItem dataModel = getItem(position); + // Check if an existing view is being reused, otherwise inflate the view + ViewHolder viewHolder; // view lookup cache stored in tag + + final View result; + + if (convertView == null) { + + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(getContext()); + convertView = inflater.inflate(R.layout.omnipod_initpod_pod_info_item, parent, false); + viewHolder.txtName = (TextView) convertView.findViewById(android.R.id.text1); + viewHolder.txtType = (TextView) convertView.findViewById(android.R.id.text2); + + result = convertView; + + convertView.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) convertView.getTag(); + result = convertView; + } + + viewHolder.txtName.setText(dataModel.getTitle()); + viewHolder.txtType.setText(dataModel.getDisplayValue()); + + // Return the completed view to render on screen + return convertView; + } + } + + private static class ViewHolder { + TextView txtName; + TextView txtType; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoPage.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoPage.java new file mode 100644 index 0000000000..04ca08a0f3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoPage.java @@ -0,0 +1,34 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages; + +import androidx.fragment.app.Fragment; + +import com.tech.freak.wizardpager.model.ModelCallbacks; +import com.tech.freak.wizardpager.model.Page; +import com.tech.freak.wizardpager.model.ReviewItem; + +import java.util.ArrayList; + + +/** + * Created by andy on 12/11/2019 + */ +public class PodInfoPage extends Page { + + public PodInfoPage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + @Override + public Fragment createFragment() { + return PodInfoFragment.create(getKey(), true); + } + + @Override + public void getReviewItems(ArrayList dest) { + } + + @Override + public boolean isCompleted() { + return true; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemoveActionFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemoveActionFragment.java new file mode 100644 index 0000000000..2531185697 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemoveActionFragment.java @@ -0,0 +1,72 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.removepod; + +import android.os.Bundle; +import android.view.View; + +import com.tech.freak.wizardpager.model.Page; + +import java.util.UUID; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitReceiver; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod.InitActionFragment; + +/** + * Created by andy on 29/11/2019 + */ +public class RemoveActionFragment extends InitActionFragment implements PodInitReceiver { + private static final String ARG_KEY = "key"; + + public static RemoveActionFragment create(String key, PodInitActionType podInitActionType) { + Bundle args = new Bundle(); + args.putString(ARG_KEY, key); + + RemoveActionFragment fragment = new RemoveActionFragment(); + fragment.setArguments(args); + fragment.setPodInitActionType(podInitActionType); + return fragment; + } + + public RemoveActionFragment() { + this.instance = this; + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + } + + public void actionOnReceiveResponse(String result) { + System.out.println("ACTION: actionOnReceiveResponse: " + result); + + boolean isOk = callResult.success; + + progressBar.setVisibility(View.GONE); + + if (!isOk) { + errorView.setVisibility(View.VISIBLE); + errorView.setText(callResult.comment); + + retryButton.setVisibility(View.VISIBLE); + } + + mPage.setActionCompleted(isOk); + + mPage.getData().putString(Page.SIMPLE_DATA_KEY, UUID.randomUUID().toString()); + mPage.notifyDataChanged(); + } + + + @Override + public void returnInitTaskStatus(PodInitActionType podInitActionType, boolean isSuccess, String errorMessage) { + if (podInitActionType.isParent()) { + for (PodInitActionType actionType : mapCheckBoxes.keySet()) { + setCheckBox(actionType, isSuccess); + } + } else { + setCheckBox(podInitActionType, isSuccess); + } + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemovePodActionPage.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemovePodActionPage.java new file mode 100644 index 0000000000..7f5b2b8503 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemovePodActionPage.java @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.removepod; + +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; + +import com.tech.freak.wizardpager.model.ModelCallbacks; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod.InitActionPage; + + +/** + * Created by andy on 12/11/2019 + * + */ +public class RemovePodActionPage extends InitActionPage { + + public RemovePodActionPage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + public RemovePodActionPage(ModelCallbacks callbacks, @StringRes int titleId, PodInitActionType podInitActionType) { + super(callbacks, titleId, podInitActionType); + } + + @Override + public Fragment createFragment() { + return RemoveActionFragment.create(getKey(), this.podInitActionType); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodDriverState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodDriverState.java new file mode 100644 index 0000000000..c2910dab89 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodDriverState.java @@ -0,0 +1,10 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver; + +public enum OmnipodDriverState { + + NotInitalized, // when we start + Initalized_NoPod, // driver is initalized, but there is no pod + Initalized_PodInitializing, // driver is initalized, pod is initalizing + Initalized_PodAttached, // driver is initalized, pod is there + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodPumpStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodPumpStatus.java new file mode 100644 index 0000000000..1262304203 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodPumpStatus.java @@ -0,0 +1,193 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver; + +import java.util.Arrays; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.pump.common.data.PumpStatus; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.data.RLHistoryItem; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkError; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkTargetDevice; +import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; +import info.nightscout.androidaps.plugins.pump.medtronic.events.EventMedtronicDeviceStatusChange; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodDeviceState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.utils.resources.ResourceHelper; +import info.nightscout.androidaps.utils.sharedPreferences.SP; + +/** + * Created by andy on 4.8.2019 + */ +@Singleton +public class OmnipodPumpStatus extends PumpStatus { + + private final ResourceHelper resourceHelper; + private final SP sp; + private final RileyLinkUtil rileyLinkUtil; + private final RxBusWrapper rxBus; + + public String errorDescription = null; + public String rileyLinkAddress = null; + public boolean inPreInit = true; + + // statuses + public RileyLinkServiceState rileyLinkServiceState = RileyLinkServiceState.NotStarted; + public RileyLinkError rileyLinkError; + public double currentBasal = 0; + public long tempBasalStart; + public long tempBasalEnd; + public Double tempBasalAmount = 0.0d; + public Integer tempBasalLength; + public long tempBasalPumpId; + public PodSessionState podSessionState; + public PumpType pumpType; + + public String regexMac = "([\\da-fA-F]{1,2}(?:\\:|$)){6}"; + + public String podNumber; + public PodDeviceState podDeviceState = PodDeviceState.NeverContacted; + public boolean podAvailable = false; + public boolean podAvailibityChecked = false; + public boolean ackAlertsAvailable = false; + public String ackAlertsText = null; + + public boolean beepBolusEnabled = true; + public boolean beepBasalEnabled = true; + public boolean beepSMBEnabled = true; + public boolean beepTBREnabled = true; + public boolean podDebuggingOptionsEnabled = false; + public String podLotNumber = "???"; + public boolean timeChangeEventEnabled = true; + + public OmnipodDriverState driverState = OmnipodDriverState.NotInitalized; + private PumpDeviceState pumpDeviceState; + + @Inject + public OmnipodPumpStatus(ResourceHelper resourceHelper, + info.nightscout.androidaps.utils.sharedPreferences.SP sp, + RxBusWrapper rxBus, + RileyLinkUtil rileyLinkUtil) { + super(PumpType.Insulet_Omnipod); + this.resourceHelper = resourceHelper; + this.sp = sp; + this.rxBus = rxBus; + this.rileyLinkUtil = rileyLinkUtil; + initSettings(); + } + + + @Override + public void initSettings() { + this.activeProfileName = ""; + this.reservoirRemainingUnits = 75d; + this.batteryRemaining = 75; + this.lastConnection = sp.getLong(OmnipodConst.Statistics.LastGoodPumpCommunicationTime, 0L); + this.lastDataTime = this.lastConnection; + this.pumpType = PumpType.Insulet_Omnipod; + this.podAvailable = false; + } + + + public String getErrorInfo() { + //verifyConfiguration(); + + return (this.errorDescription == null) ? "-" : this.errorDescription; + } + + +// public boolean setNotInPreInit() { +// this.inPreInit = false; +// +// return reconfigureService(); +// } + + + public void clearTemporaryBasal() { + this.tempBasalStart = 0L; + this.tempBasalEnd = 0L; + this.tempBasalAmount = 0.0d; + this.tempBasalLength = 0; + } + + + public TempBasalPair getTemporaryBasal() { + + TempBasalPair tbr = new TempBasalPair(); + tbr.setDurationMinutes(tempBasalLength); + tbr.setInsulinRate(tempBasalAmount); + tbr.setStartTime(tempBasalStart); + tbr.setEndTime(tempBasalEnd); + + return tbr; + } + + @Override + public String toString() { + return "OmnipodPumpStatus{" + + "errorDescription='" + errorDescription + '\'' + + ", rileyLinkAddress='" + rileyLinkAddress + '\'' + + ", inPreInit=" + inPreInit + + ", rileyLinkServiceState=" + rileyLinkServiceState + + ", rileyLinkError=" + rileyLinkError + + ", currentBasal=" + currentBasal + + ", tempBasalStart=" + tempBasalStart + + ", tempBasalEnd=" + tempBasalEnd + + ", tempBasalAmount=" + tempBasalAmount + + ", tempBasalLength=" + tempBasalLength + + ", podSessionState=" + podSessionState + + ", regexMac='" + regexMac + '\'' + + ", podNumber='" + podNumber + '\'' + + ", podDeviceState=" + podDeviceState + + ", podAvailable=" + podAvailable + + ", ackAlertsAvailable=" + ackAlertsAvailable + + ", ackAlertsText='" + ackAlertsText + '\'' + + ", lastDataTime=" + lastDataTime + + ", lastConnection=" + lastConnection + + ", previousConnection=" + previousConnection + + ", lastBolusTime=" + lastBolusTime + + ", lastBolusAmount=" + lastBolusAmount + + ", activeProfileName='" + activeProfileName + '\'' + + ", reservoirRemainingUnits=" + reservoirRemainingUnits + + ", reservoirFullUnits=" + reservoirFullUnits + + ", batteryRemaining=" + batteryRemaining + + ", batteryVoltage=" + batteryVoltage + + ", iob='" + iob + '\'' + + ", dailyTotalUnits=" + dailyTotalUnits + + ", maxDailyTotalUnits='" + maxDailyTotalUnits + '\'' + + ", validBasalRateProfileSelectedOnPump=" + validBasalRateProfileSelectedOnPump + + ", pumpType=" + pumpType + + ", profileStore=" + profileStore + + ", units='" + units + '\'' + + ", pumpStatusType=" + pumpStatusType + + ", basalsByHour=" + Arrays.toString(basalsByHour) + + ", currentBasal=" + currentBasal + + ", tempBasalInProgress=" + tempBasalInProgress + + ", tempBasalRatio=" + tempBasalRatio + + ", tempBasalRemainMin=" + tempBasalRemainMin + + ", tempBasalStart=" + tempBasalStart + + ", pumpType=" + pumpType + + "} "; + } + + + public PumpDeviceState getPumpDeviceState() { + return pumpDeviceState; + } + + + public void setPumpDeviceState(PumpDeviceState pumpDeviceState) { + this.pumpDeviceState = pumpDeviceState; + + rileyLinkUtil.getRileyLinkHistory().add(new RLHistoryItem(pumpDeviceState, RileyLinkTargetDevice.Omnipod)); + + rxBus.send(new EventMedtronicDeviceStatusChange(pumpDeviceState)); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/comm/AapsOmnipodManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/comm/AapsOmnipodManager.java new file mode 100644 index 0000000000..f9fdcf8a83 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/comm/AapsOmnipodManager.java @@ -0,0 +1,757 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.comm; + +import android.content.Intent; +import android.text.TextUtils; + +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.activities.ErrorHelperActivity; +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.db.Source; +import info.nightscout.androidaps.db.TemporaryBasal; +import info.nightscout.androidaps.events.Event; +import info.nightscout.androidaps.events.EventRefreshOverview; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; +import info.nightscout.androidaps.interfaces.TreatmentsInterface; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.general.overview.events.EventNewNotification; +import info.nightscout.androidaps.plugins.general.overview.events.EventOverviewBolusProgress; +import info.nightscout.androidaps.plugins.general.overview.notifications.Notification; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpStatusType; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.common.utils.DateTimeUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.SetupActionResult; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommandInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommunicationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CrcMismatchException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalDeliveryStatusException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPacketTypeException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPodProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalResponseException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalSetupProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.MessageDecodingException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NonceOutOfSyncException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NonceResyncException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NotEnoughDataException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodFaultException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodReturnedErrorResponseException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoRecentPulseLog; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommunicationManagerInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitReceiver; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalScheduleEntry; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.db.PodHistory; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.db.PodHistoryEntryType; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodAcknowledgeAlertsChanged; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; +import info.nightscout.androidaps.utils.resources.ResourceHelper; +import info.nightscout.androidaps.utils.sharedPreferences.SP; +import io.reactivex.disposables.Disposable; + +public class AapsOmnipodManager implements OmnipodCommunicationManagerInterface { + + private OmnipodUtil omnipodUtil; + private AAPSLogger aapsLogger; + private RxBusWrapper rxBus; + private ResourceHelper resourceHelper; + private HasAndroidInjector injector; + private ActivePluginProvider activePlugin; + private OmnipodPumpStatus pumpStatus; + + private final OmnipodManager delegate; + + private static AapsOmnipodManager instance; + + private Date lastBolusTime; + private Double lastBolusUnits; + + public static AapsOmnipodManager getInstance() { + return instance; + } + + public AapsOmnipodManager(OmnipodCommunicationManager communicationService, + PodSessionState podState, + OmnipodPumpStatus _pumpStatus, + OmnipodUtil omnipodUtil, + AAPSLogger aapsLogger, + RxBusWrapper rxBus, + SP sp, + ResourceHelper resourceHelper, + HasAndroidInjector injector, + ActivePluginProvider activePlugin) { + this.omnipodUtil = omnipodUtil; + this.aapsLogger = aapsLogger; + this.rxBus = rxBus; + this.resourceHelper = resourceHelper; + this.injector = injector; + this.activePlugin = activePlugin; + this.pumpStatus = _pumpStatus; + + delegate = new OmnipodManager(aapsLogger, sp, communicationService, podState, podSessionState -> { + // Handle pod state changes + omnipodUtil.setPodSessionState(podSessionState); + updatePumpStatus(podSessionState); + }); + instance = this; + } + + private void updatePumpStatus(PodSessionState podSessionState) { + if (pumpStatus != null) { + if (podSessionState == null) { + pumpStatus.ackAlertsText = null; + pumpStatus.ackAlertsAvailable = false; + pumpStatus.lastBolusTime = null; + pumpStatus.lastBolusAmount = null; + pumpStatus.reservoirRemainingUnits = 0.0; + pumpStatus.pumpStatusType = PumpStatusType.Suspended; + sendEvent(new EventOmnipodAcknowledgeAlertsChanged()); + sendEvent(new EventOmnipodPumpValuesChanged()); + sendEvent(new EventRefreshOverview("Omnipod Pump", false)); + } else { + // Update active alerts + if (podSessionState.hasActiveAlerts()) { + List alerts = translateActiveAlerts(podSessionState); + String alertsText = TextUtils.join("\n", alerts); + + if (!pumpStatus.ackAlertsAvailable || !alertsText.equals(pumpStatus.ackAlertsText)) { + pumpStatus.ackAlertsAvailable = true; + pumpStatus.ackAlertsText = TextUtils.join("\n", alerts); + + sendEvent(new EventOmnipodAcknowledgeAlertsChanged()); + } + } else { + if (pumpStatus.ackAlertsAvailable || StringUtils.isNotEmpty(pumpStatus.ackAlertsText)) { + pumpStatus.ackAlertsText = null; + pumpStatus.ackAlertsAvailable = false; + sendEvent(new EventOmnipodAcknowledgeAlertsChanged()); + } + } + + // Update other info: last bolus, units remaining, suspended + if (!Objects.equals(lastBolusTime, pumpStatus.lastBolusTime) // + || !Objects.equals(lastBolusUnits, pumpStatus.lastBolusAmount) // + || !isReservoirStatusUpToDate(pumpStatus, podSessionState.getReservoirLevel()) + || podSessionState.isSuspended() != PumpStatusType.Suspended.equals(pumpStatus.pumpStatusType)) { + pumpStatus.lastBolusTime = lastBolusTime; + pumpStatus.lastBolusAmount = lastBolusUnits; + pumpStatus.reservoirRemainingUnits = podSessionState.getReservoirLevel() == null ? 75.0 : podSessionState.getReservoirLevel(); + pumpStatus.pumpStatusType = podSessionState.isSuspended() ? PumpStatusType.Suspended : PumpStatusType.Running; + sendEvent(new EventOmnipodPumpValuesChanged()); + + if (podSessionState.isSuspended() != PumpStatusType.Suspended.equals(pumpStatus.pumpStatusType)) { + sendEvent(new EventRefreshOverview("Omnipod Pump", false)); + } + } + } + } + } + + private static boolean isReservoirStatusUpToDate(OmnipodPumpStatus pumpStatus, Double unitsRemaining) { + double expectedUnitsRemaining = unitsRemaining == null ? 75.0 : unitsRemaining; + return Math.abs(expectedUnitsRemaining - pumpStatus.reservoirRemainingUnits) < 0.000001; + } + + private List translateActiveAlerts(PodSessionState podSessionState) { + List alerts = new ArrayList<>(); + for (AlertSlot alertSlot : podSessionState.getActiveAlerts().getAlertSlots()) { + alerts.add(translateAlertType(podSessionState.getConfiguredAlertType(alertSlot))); + } + return alerts; + } + + @Override + public PumpEnactResult initPod(PodInitActionType podInitActionType, PodInitReceiver podInitReceiver, Profile profile) { + long time = System.currentTimeMillis(); + if (PodInitActionType.PairAndPrimeWizardStep.equals(podInitActionType)) { + try { + Disposable disposable = delegate.pairAndPrime().subscribe(res -> // + handleSetupActionResult(podInitActionType, podInitReceiver, res, time, null)); + return new PumpEnactResult(injector).success(true).enacted(true); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + podInitReceiver.returnInitTaskStatus(podInitActionType, false, comment); + addFailureToHistory(time, PodHistoryEntryType.PairAndPrime, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + } else if (PodInitActionType.FillCannulaSetBasalProfileWizardStep.equals(podInitActionType)) { + try { + BasalSchedule basalSchedule; + try { + basalSchedule = mapProfileToBasalSchedule(profile); + } catch (Exception ex) { + throw new CommandInitializationException("Basal profile mapping failed", ex); + } + Disposable disposable = delegate.insertCannula(basalSchedule).subscribe(res -> // + handleSetupActionResult(podInitActionType, podInitReceiver, res, time, profile)); + return new PumpEnactResult(injector).success(true).enacted(true); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + podInitReceiver.returnInitTaskStatus(podInitActionType, false, comment); + addFailureToHistory(time, PodHistoryEntryType.FillCannulaSetBasalProfile, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + } + + return new PumpEnactResult(injector).success(false).enacted(false).comment(getStringResource(R.string.omnipod_error_illegal_init_action_type, podInitActionType.name())); + } + + @Override + public PumpEnactResult getPodStatus() { + long time = System.currentTimeMillis(); + try { + StatusResponse statusResponse = delegate.getPodStatus(); + addSuccessToHistory(time, PodHistoryEntryType.GetPodStatus, statusResponse); + return new PumpEnactResult(injector).success(true).enacted(false); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + addFailureToHistory(time, PodHistoryEntryType.GetPodStatus, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + } + + @Override + public PumpEnactResult deactivatePod(PodInitReceiver podInitReceiver) { + long time = System.currentTimeMillis(); + try { + delegate.deactivatePod(); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + podInitReceiver.returnInitTaskStatus(PodInitActionType.DeactivatePodWizardStep, false, comment); + addFailureToHistory(time, PodHistoryEntryType.DeactivatePod, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + reportImplicitlyCanceledTbr(); + + addSuccessToHistory(time, PodHistoryEntryType.DeactivatePod, null); + + podInitReceiver.returnInitTaskStatus(PodInitActionType.DeactivatePodWizardStep, true, null); + + this.omnipodUtil.setPodSessionState(null); + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public PumpEnactResult setBasalProfile(Profile profile) { + long time = System.currentTimeMillis(); + try { + BasalSchedule basalSchedule; + try { + basalSchedule = mapProfileToBasalSchedule(profile); + } catch (Exception ex) { + throw new CommandInitializationException("Basal profile mapping failed", ex); + } + delegate.setBasalSchedule(basalSchedule, isBasalBeepsEnabled()); + // Because setting a basal profile actually suspends and then resumes delivery, TBR is implicitly cancelled + reportImplicitlyCanceledTbr(); + addSuccessToHistory(time, PodHistoryEntryType.SetBasalSchedule, profile.getBasalValues()); + } catch (Exception ex) { + if ((ex instanceof OmnipodException) && !((OmnipodException) ex).isCertainFailure()) { + reportImplicitlyCanceledTbr(); + addToHistory(time, PodHistoryEntryType.SetBasalSchedule, "Uncertain failure", false); + return new PumpEnactResult(injector).success(false).enacted(false).comment(getStringResource(R.string.omnipod_error_set_basal_failed_uncertain)); + } + String comment = handleAndTranslateException(ex); + reportImplicitlyCanceledTbr(); + addFailureToHistory(time, PodHistoryEntryType.SetBasalSchedule, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public PumpEnactResult resetPodStatus() { + delegate.resetPodState(true); + + reportImplicitlyCanceledTbr(); + + this.omnipodUtil.setPodSessionState(null); + + addSuccessToHistory(System.currentTimeMillis(), PodHistoryEntryType.ResetPodState, null); + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public PumpEnactResult setBolus(DetailedBolusInfo detailedBolusInfo) { + OmnipodManager.BolusCommandResult bolusCommandResult; + + boolean beepsEnabled = detailedBolusInfo.isSMB ? isSmbBeepsEnabled() : isBolusBeepsEnabled(); + + Date bolusStarted; + try { + bolusCommandResult = delegate.bolus(PumpType.Insulet_Omnipod.determineCorrectBolusSize(detailedBolusInfo.insulin), beepsEnabled, beepsEnabled, detailedBolusInfo.isSMB ? null : + (estimatedUnitsDelivered, percentage) -> { + EventOverviewBolusProgress progressUpdateEvent = EventOverviewBolusProgress.INSTANCE; + progressUpdateEvent.setStatus(getStringResource(R.string.bolusdelivering, detailedBolusInfo.insulin)); + progressUpdateEvent.setPercent(percentage); + sendEvent(progressUpdateEvent); + }); + + bolusStarted = new Date(); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + addFailureToHistory(System.currentTimeMillis(), PodHistoryEntryType.SetBolus, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + if (OmnipodManager.CommandDeliveryStatus.UNCERTAIN_FAILURE.equals(bolusCommandResult.getCommandDeliveryStatus())) { + // For safety reasons, we treat this as a bolus that has successfully been delivered, in order to prevent insulin overdose + + showErrorDialog(getStringResource(R.string.omnipod_bolus_failed_uncertain), R.raw.boluserror); + } + + // Wait for the bolus to finish + OmnipodManager.BolusDeliveryResult bolusDeliveryResult = + bolusCommandResult.getDeliveryResultSubject().blockingGet(); + + double unitsDelivered = bolusDeliveryResult.getUnitsDelivered(); + + if (pumpStatus != null && !detailedBolusInfo.isSMB) { + lastBolusTime = pumpStatus.lastBolusTime = bolusStarted; + lastBolusUnits = pumpStatus.lastBolusAmount = unitsDelivered; + } + + long pumpId = addSuccessToHistory(bolusStarted.getTime(), PodHistoryEntryType.SetBolus, unitsDelivered + ";" + detailedBolusInfo.carbs); + + detailedBolusInfo.date = bolusStarted.getTime(); + detailedBolusInfo.insulin = unitsDelivered; + detailedBolusInfo.pumpId = pumpId; + detailedBolusInfo.source = Source.PUMP; + + activePlugin.getActiveTreatments().addToHistoryTreatment(detailedBolusInfo, false); + + if (delegate.getPodState().hasFaultEvent()) { + showPodFaultErrorDialog(delegate.getPodState().getFaultEvent().getFaultEventType(), R.raw.urgentalarm); + } + + return new PumpEnactResult(injector).success(true).enacted(true).bolusDelivered(unitsDelivered); + } + + @Override + public PumpEnactResult cancelBolus() { + long time = System.currentTimeMillis(); + String comment = null; + while (delegate.hasActiveBolus()) { + try { + delegate.cancelBolus(isBolusBeepsEnabled()); + addSuccessToHistory(time, PodHistoryEntryType.CancelBolus, null); + return new PumpEnactResult(injector).success(true).enacted(true); + } catch (PodFaultException ex) { + showPodFaultErrorDialog(ex.getFaultEvent().getFaultEventType(), null); + addSuccessToHistory(time, PodHistoryEntryType.CancelBolus, null); + return new PumpEnactResult(injector).success(true).enacted(true); + } catch (Exception ex) { + comment = handleAndTranslateException(ex); + } + } + + addFailureToHistory(time, PodHistoryEntryType.CancelBolus, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + @Override + public PumpEnactResult setTemporaryBasal(TempBasalPair tempBasalPair) { + boolean beepsEnabled = isTempBasalBeepsEnabled(); + long time = System.currentTimeMillis(); + try { + delegate.setTemporaryBasal(PumpType.Insulet_Omnipod.determineCorrectBasalSize(tempBasalPair.getInsulinRate()), Duration.standardMinutes(tempBasalPair.getDurationMinutes()), beepsEnabled, beepsEnabled); + time = System.currentTimeMillis(); + } catch (Exception ex) { + if ((ex instanceof OmnipodException) && !((OmnipodException) ex).isCertainFailure()) { + addToHistory(time, PodHistoryEntryType.SetTemporaryBasal, "Uncertain failure", false); + return new PumpEnactResult(injector).success(false).enacted(false).comment(getStringResource(R.string.omnipod_error_set_temp_basal_failed_uncertain)); + } + String comment = handleAndTranslateException(ex); + addFailureToHistory(time, PodHistoryEntryType.SetTemporaryBasal, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + reportImplicitlyCanceledTbr(); + + long pumpId = addSuccessToHistory(time, PodHistoryEntryType.SetTemporaryBasal, tempBasalPair); + + pumpStatus.tempBasalStart = time; + pumpStatus.tempBasalAmount = tempBasalPair.getInsulinRate(); + pumpStatus.tempBasalLength = tempBasalPair.getDurationMinutes(); + pumpStatus.tempBasalEnd = DateTimeUtil.getTimeInFutureFromMinutes(time, tempBasalPair.getDurationMinutes()); + pumpStatus.tempBasalPumpId = pumpId; + + TemporaryBasal tempStart = new TemporaryBasal() // + .date(time) // + .duration(tempBasalPair.getDurationMinutes()) // + .absolute(tempBasalPair.getInsulinRate()) // + .pumpId(pumpId) + .source(Source.PUMP); + + activePlugin.getActiveTreatments().addToHistoryTempBasal(tempStart); + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public PumpEnactResult cancelTemporaryBasal() { + long time = System.currentTimeMillis(); + try { + delegate.cancelTemporaryBasal(isTempBasalBeepsEnabled()); + addSuccessToHistory(time, PodHistoryEntryType.CancelTemporaryBasalForce, null); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + addFailureToHistory(time, PodHistoryEntryType.CancelTemporaryBasalForce, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public PumpEnactResult acknowledgeAlerts() { + long time = System.currentTimeMillis(); + try { + delegate.acknowledgeAlerts(); + addSuccessToHistory(time, PodHistoryEntryType.AcknowledgeAlerts, null); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + addFailureToHistory(time, PodHistoryEntryType.AcknowledgeAlerts, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public void setPumpStatus(OmnipodPumpStatus pumpStatus) { + this.pumpStatus = pumpStatus; + updatePumpStatus(delegate.getPodState()); + } + + // TODO should we add this to the OmnipodCommunicationManager interface? + public PumpEnactResult getPodInfo(PodInfoType podInfoType) { + long time = System.currentTimeMillis(); + try { + // TODO how can we return the PodInfo response? + // This method is useless unless we return the PodInfoResponse, + // because the pod state we keep, doesn't get updated from a PodInfoResponse. + // We use StatusResponses for that, which can be obtained from the getPodStatus method + PodInfoResponse podInfo = delegate.getPodInfo(podInfoType); + addSuccessToHistory(time, PodHistoryEntryType.GetPodInfo, podInfo); + return new PumpEnactResult(injector).success(true).enacted(true); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + addFailureToHistory(time, PodHistoryEntryType.GetPodInfo, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + } + + public PumpEnactResult suspendDelivery() { + try { + delegate.suspendDelivery(isBasalBeepsEnabled()); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + public PumpEnactResult resumeDelivery() { + try { + delegate.resumeDelivery(isBasalBeepsEnabled()); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + // TODO should we add this to the OmnipodCommunicationManager interface? + // Updates the pods current time based on the device timezone and the pod's time zone + public PumpEnactResult setTime() { + long time = System.currentTimeMillis(); + try { + delegate.setTime(isBasalBeepsEnabled()); + // Because set time actually suspends and then resumes delivery, TBR is implicitly cancelled + reportImplicitlyCanceledTbr(); + addSuccessToHistory(time, PodHistoryEntryType.SetTime, null); + } catch (Exception ex) { + if ((ex instanceof OmnipodException) && !((OmnipodException) ex).isCertainFailure()) { + reportImplicitlyCanceledTbr(); + addFailureToHistory(time, PodHistoryEntryType.SetTime, "Uncertain failure"); + return new PumpEnactResult(injector).success(false).enacted(false).comment(getStringResource(R.string.omnipod_error_set_time_failed_uncertain)); + } + String comment = handleAndTranslateException(ex); + reportImplicitlyCanceledTbr(); + addFailureToHistory(time, PodHistoryEntryType.SetTime, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + public PodInfoRecentPulseLog readPulseLog() { + PodInfoResponse response = delegate.getPodInfo(PodInfoType.RECENT_PULSE_LOG); + return response.getPodInfo(); + } + + public OmnipodCommunicationManager getCommunicationService() { + return delegate.getCommunicationService(); + } + + public DateTime getTime() { + return delegate.getTime(); + } + + public boolean isInitialized() { + return delegate.isReadyForDelivery(); + } + + public String getPodStateAsString() { + return delegate.getPodStateAsString(); + } + + private void reportImplicitlyCanceledTbr() { + //TreatmentsPlugin plugin = TreatmentsPlugin.getPlugin(); + TreatmentsInterface plugin = activePlugin.getActiveTreatments(); + if (plugin.isTempBasalInProgress()) { + aapsLogger.debug(LTag.PUMP, "Reporting implicitly cancelled TBR to Treatments plugin"); + + long time = System.currentTimeMillis() - 1000; + + addSuccessToHistory(time, PodHistoryEntryType.CancelTemporaryBasal, null); + + TemporaryBasal temporaryBasal = new TemporaryBasal() // + .date(time) // + .duration(0) // + .pumpId(pumpStatus.tempBasalPumpId) + .source(Source.PUMP); + + plugin.addToHistoryTempBasal(temporaryBasal); + } + } + + + public long addSuccessToHistory(long requestTime, PodHistoryEntryType entryType, Object data) { + return addToHistory(requestTime, entryType, data, true); + } + + public long addFailureToHistory(long requestTime, PodHistoryEntryType entryType, Object data) { + return addToHistory(requestTime, entryType, data, false); + } + + + public long addToHistory(long requestTime, PodHistoryEntryType entryType, Object data, boolean success) { + + PodHistory podHistory = new PodHistory(requestTime, entryType); + + if (data != null) { + if (data instanceof String) { + podHistory.setData((String) data); + } else { + podHistory.setData(omnipodUtil.getGsonInstance().toJson(data)); + } + } + + podHistory.setSuccess(success); + podHistory.setPodSerial(pumpStatus.podNumber); + + MainApp.getDbHelper().createOrUpdate(podHistory); + + return podHistory.getPumpId(); + + } + + private void handleSetupActionResult(PodInitActionType podInitActionType, PodInitReceiver podInitReceiver, SetupActionResult res, long time, Profile profile) { + String comment = null; + switch (res.getResultType()) { + case FAILURE: { + aapsLogger.error(LTag.PUMP, "Setup action failed: illegal setup progress: {}", res.getSetupProgress()); + comment = getStringResource(R.string.omnipod_driver_error_invalid_progress_state, res.getSetupProgress()); + } + break; + case VERIFICATION_FAILURE: { + aapsLogger.error(LTag.PUMP, "Setup action verification failed: caught exception", res.getException()); + comment = getStringResource(R.string.omnipod_driver_error_setup_action_verification_failed); + } + break; + } + + if (podInitActionType == PodInitActionType.PairAndPrimeWizardStep) { + addToHistory(time, PodHistoryEntryType.PairAndPrime, comment, res.getResultType().isSuccess()); + } else { + addToHistory(time, PodHistoryEntryType.FillCannulaSetBasalProfile, res.getResultType().isSuccess() ? profile.getBasalValues() : comment, res.getResultType().isSuccess()); + } + + podInitReceiver.returnInitTaskStatus(podInitActionType, res.getResultType().isSuccess(), comment); + } + + private String handleAndTranslateException(Exception ex) { + String comment; + + if (ex instanceof OmnipodException) { + if (ex instanceof ActionInitializationException || ex instanceof CommandInitializationException) { + comment = getStringResource(R.string.omnipod_driver_error_invalid_parameters); + } else if (ex instanceof CommunicationException) { + comment = getStringResource(R.string.omnipod_driver_error_communication_failed); + } else if (ex instanceof CrcMismatchException) { + comment = getStringResource(R.string.omnipod_driver_error_crc_mismatch); + } else if (ex instanceof IllegalPacketTypeException) { + comment = getStringResource(R.string.omnipod_driver_error_invalid_packet_type); + } else if (ex instanceof IllegalPodProgressException || ex instanceof IllegalSetupProgressException + || ex instanceof IllegalDeliveryStatusException) { + comment = getStringResource(R.string.omnipod_driver_error_invalid_progress_state); + } else if (ex instanceof IllegalResponseException) { + comment = getStringResource(R.string.omnipod_driver_error_invalid_response); + } else if (ex instanceof MessageDecodingException) { + comment = getStringResource(R.string.omnipod_driver_error_message_decoding_failed); + } else if (ex instanceof NonceOutOfSyncException) { + comment = getStringResource(R.string.omnipod_driver_error_nonce_out_of_sync); + } else if (ex instanceof NonceResyncException) { + comment = getStringResource(R.string.omnipod_driver_error_nonce_resync_failed); + } else if (ex instanceof NotEnoughDataException) { + comment = getStringResource(R.string.omnipod_driver_error_not_enough_data); + } else if (ex instanceof PodFaultException) { + FaultEventType faultEventType = ((PodFaultException) ex).getFaultEvent().getFaultEventType(); + showPodFaultErrorDialog(faultEventType, R.raw.urgentalarm); + comment = createPodFaultErrorMessage(faultEventType); + } else if (ex instanceof PodReturnedErrorResponseException) { + comment = getStringResource(R.string.omnipod_driver_error_pod_returned_error_response); + } else { + // Shouldn't be reachable + comment = getStringResource(R.string.omnipod_driver_error_unexpected_exception_type, ex.getClass().getName()); + } + aapsLogger.error(LTag.PUMP, String.format("Caught OmnipodException[certainFailure=%s] from OmnipodManager (user-friendly error message: %s)", ((OmnipodException) ex).isCertainFailure(), comment), ex); + } else { + comment = getStringResource(R.string.omnipod_driver_error_unexpected_exception_type, ex.getClass().getName()); + aapsLogger.error(LTag.PUMP, String.format("Caught unexpected exception type[certainFailure=false] from OmnipodManager (user-friendly error message: %s)", comment), ex); + } + + return comment; + } + + private String createPodFaultErrorMessage(FaultEventType faultEventType) { + String comment; + comment = getStringResource(R.string.omnipod_driver_error_pod_fault, + ByteUtil.convertUnsignedByteToInt(faultEventType.getValue()), faultEventType.name()); + return comment; + } + + private void sendEvent(Event event) { + rxBus.send(event); + } + + private void showPodFaultErrorDialog(FaultEventType faultEventType, Integer sound) { + showErrorDialog(createPodFaultErrorMessage(faultEventType), sound); + } + + private void showErrorDialog(String message, Integer sound) { + Intent intent = new Intent(MainApp.instance(), ErrorHelperActivity.class); + intent.putExtra("soundid", sound == null ? 0 : sound); + intent.putExtra("status", message); + intent.putExtra("title", MainApp.gs(R.string.treatmentdeliveryerror)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + MainApp.instance().startActivity(intent); + } + + private void showNotification(String message, int urgency, Integer sound) { + Notification notification = new Notification( // + Notification.OMNIPOD_PUMP_ALARM, // + message, // + urgency); + if (sound != null) { + notification.soundId = sound; + } + sendEvent(new EventNewNotification(notification)); + } + + private String translateAlertType(AlertType alertType) { + if (alertType == null) { + return getStringResource(R.string.omnipod_alert_unknown_alert); + } + switch (alertType) { + case FINISH_PAIRING_REMINDER: + return getStringResource(R.string.omnipod_alert_finish_pairing_reminder); + case FINISH_SETUP_REMINDER: + return getStringResource(R.string.omnipod_alert_finish_setup_reminder_reminder); + case EXPIRATION_ALERT: + return getStringResource(R.string.omnipod_alert_expiration); + case EXPIRATION_ADVISORY_ALERT: + return getStringResource(R.string.omnipod_alert_expiration_advisory); + case SHUTDOWN_IMMINENT_ALARM: + return getStringResource(R.string.omnipod_alert_shutdown_imminent); + case LOW_RESERVOIR_ALERT: + return getStringResource(R.string.omnipod_alert_low_reservoir); + default: + return alertType.name(); + } + } + + private boolean isBolusBeepsEnabled() { + return this.pumpStatus.beepBolusEnabled; + } + + private boolean isSmbBeepsEnabled() { + return this.pumpStatus.beepSMBEnabled; + } + + private boolean isBasalBeepsEnabled() { + return this.pumpStatus.beepBasalEnabled; + } + + private boolean isTempBasalBeepsEnabled() { + return this.pumpStatus.beepTBREnabled; + } + + private String getStringResource(int id, Object... args) { + return resourceHelper.gs(id, args); + } + + static BasalSchedule mapProfileToBasalSchedule(Profile profile) { + if (profile == null) { + throw new IllegalArgumentException("Profile can not be null"); + } + Profile.ProfileValue[] basalValues = profile.getBasalValues(); + if (basalValues == null) { + throw new IllegalArgumentException("Basal values can not be null"); + } + List entries = new ArrayList<>(); + for (Profile.ProfileValue basalValue : basalValues) { + entries.add(new BasalScheduleEntry(PumpType.Insulet_Omnipod.determineCorrectBasalSize(basalValue.value), + Duration.standardSeconds(basalValue.timeAsSeconds))); + } + + return new BasalSchedule(entries); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistory.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistory.java new file mode 100644 index 0000000000..e064fb5c24 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistory.java @@ -0,0 +1,136 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.db; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; + +import info.nightscout.androidaps.db.DatabaseHelper; +import info.nightscout.androidaps.db.DbObjectBase; +import info.nightscout.androidaps.plugins.pump.common.utils.DateTimeUtil; + +/** + * Created by andy on 30.11.2019. + */ +@DatabaseTable(tableName = DatabaseHelper.DATABASE_POD_HISTORY) +public class PodHistory implements DbObjectBase, Comparable { + + @DatabaseField(id = true) + public long date; + + private PodHistoryEntryType podHistoryEntryType; + + @DatabaseField + private long podEntryTypeCode; + + @DatabaseField + private String data; + + @DatabaseField + private boolean success; + + @DatabaseField + private long pumpId; + + @DatabaseField + private String podSerial; + + @DatabaseField + private Boolean successConfirmed; + + public PodHistory() { + generatePumpId(); + } + + + public PodHistory(PodHistoryEntryType podDbEntryType) { + this.date = System.currentTimeMillis(); + this.podHistoryEntryType = podDbEntryType; + this.podEntryTypeCode = podDbEntryType.getCode(); + generatePumpId(); + } + + + public PodHistory(long dateTimeInMillis, PodHistoryEntryType podDbEntryType) { + this.date = dateTimeInMillis; + this.podHistoryEntryType = podDbEntryType; + this.podEntryTypeCode = podDbEntryType.getCode(); + generatePumpId(); + } + + + @Override + public long getDate() { + return this.date; + } + + public void setDate(long date) { + this.date = date; + } + + public String getDateTimeString() { + return DateTimeUtil.toStringFromTimeInMillis(this.date); + } + + public PodHistoryEntryType getPodDbEntryType() { + return PodHistoryEntryType.getByCode((int) this.podEntryTypeCode); + } + + public void setPodDbEntryType(PodHistoryEntryType podDbEntryType) { + //this.podHistoryEntryType = podDbEntryType; + this.podEntryTypeCode = podDbEntryType.getCode(); + } + + public long getPodEntryTypeCode() { + return podEntryTypeCode; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public void setPumpId(long pumpId) { + this.pumpId = pumpId; + } + + public Boolean getSuccessConfirmed() { + return successConfirmed; + } + + public void setSuccessConfirmed(Boolean successConfirmed) { + this.successConfirmed = successConfirmed; + } + + @Override + public long getPumpId() { + return pumpId; + } + + private void generatePumpId() { + this.pumpId = (DateTimeUtil.toATechDate(this.date) * 100L) + podEntryTypeCode; + } + + + public String getPodSerial() { + return podSerial; + } + + public void setPodSerial(String podSerial) { + this.podSerial = podSerial; + } + + @Override + public int compareTo(PodHistory otherOne) { + return (int) (otherOne.date - this.date); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistoryEntryType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistoryEntryType.java new file mode 100644 index 0000000000..e776de681f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistoryEntryType.java @@ -0,0 +1,93 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.db; + +import androidx.annotation.IdRes; +import androidx.annotation.StringRes; + +import java.util.HashMap; +import java.util.Map; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpHistoryEntryGroup; + +/** + * Created by andy on 24.11.2019 + */ +public enum PodHistoryEntryType { + + PairAndPrime(1, R.string.omnipod_init_pod_wizard_step2_title, PumpHistoryEntryGroup.Prime), + FillCannulaSetBasalProfile(2, R.string.omnipod_init_pod_wizard_step4_title, PumpHistoryEntryGroup.Prime), + DeactivatePod(3, R.string.omnipod_cmd_deactivate_pod, PumpHistoryEntryGroup.Prime), + ResetPodState(4, R.string.omnipod_cmd_reset_pod, PumpHistoryEntryGroup.Prime), + + SetTemporaryBasal(10, R.string.omnipod_cmd_set_tbr, PumpHistoryEntryGroup.Basal), + CancelTemporaryBasal(11, R.string.omnipod_cmd_cancel_tbr, PumpHistoryEntryGroup.Basal), + CancelTemporaryBasalForce(12, R.string.omnipod_cmd_cancel_tbr_forced, PumpHistoryEntryGroup.Basal), + + SetBasalSchedule(20, R.string.omnipod_cmd_set_basal_schedule, PumpHistoryEntryGroup.Basal), + + GetPodStatus(30, R.string.omnipod_cmd_get_pod_status, PumpHistoryEntryGroup.Configuration), + GetPodInfo(31, R.string.omnipod_cmd_get_pod_info, PumpHistoryEntryGroup.Configuration), + SetTime(32, R.string.omnipod_cmd_set_time, PumpHistoryEntryGroup.Configuration), + + SetBolus(40, R.string.omnipod_cmd_set_bolus, PumpHistoryEntryGroup.Bolus), + CancelBolus(41, R.string.omnipod_cmd_cancel_bolus, PumpHistoryEntryGroup.Bolus), + + ConfigureAlerts(50, R.string.omnipod_cmd_configure_alerts, PumpHistoryEntryGroup.Alarm), + AcknowledgeAlerts(51, R.string.omnipod_cmd_acknowledge_alerts, PumpHistoryEntryGroup.Alarm), + + SuspendDelivery(60, R.string.omnipod_cmd_suspend_delivery, PumpHistoryEntryGroup.Basal), + ResumeDelivery(61, R.string.omnipod_cmd_resume_delivery, PumpHistoryEntryGroup.Basal), + + UnknownEntryType(99, R.string.omnipod_cmd_unknown_entry) + ; + + private int code; + private static Map instanceMap; + + @StringRes + private int resourceId; + + private PumpHistoryEntryGroup group; + + + static { + instanceMap = new HashMap<>(); + + for (PodHistoryEntryType value : values()) { + instanceMap.put(value.code, value); + } + } + + + PodHistoryEntryType(int code, @StringRes int resourceId) { + this.code = code; + this.resourceId = resourceId; + } + + PodHistoryEntryType(int code, @StringRes int resourceId, PumpHistoryEntryGroup group) { + this.code = code; + this.resourceId = resourceId; + this.group = group; + } + + public int getCode() { + return code; + } + + public PumpHistoryEntryGroup getGroup() { + return this.group; + } + + + public static PodHistoryEntryType getByCode(int code) { + if (instanceMap.containsKey(code)) { + return instanceMap.get(code); + } else { + return UnknownEntryType; + } + } + + public int getResourceId() { + return resourceId; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIComm.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIComm.java new file mode 100644 index 0000000000..698762bb15 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIComm.java @@ -0,0 +1,83 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.ui; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommandType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommunicationManagerInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; + +/** + * Created by andy on 4.8.2019 + */ +public class OmnipodUIComm { + + private final HasAndroidInjector injector; + private final AAPSLogger aapsLogger; + private final OmnipodUtil omnipodUtil; + private final OmnipodCommunicationManagerInterface omnipodCommunicationManager; + private final OmnipodUIPostprocessor omnipodUIPostprocessor; + + + public OmnipodUIComm( + HasAndroidInjector injector, + AAPSLogger aapsLogger, + OmnipodUtil omnipodUtil, + OmnipodUIPostprocessor omnipodUIPostprocessor, + OmnipodCommunicationManagerInterface omnipodCommunicationManager + ) { + this.injector = injector; + this.aapsLogger = aapsLogger; + this.omnipodUtil = omnipodUtil; + this.omnipodUIPostprocessor = omnipodUIPostprocessor; + this.omnipodCommunicationManager = omnipodCommunicationManager; + } + + + public OmnipodUITask executeCommand(OmnipodCommandType commandType, Object... parameters) { + + aapsLogger.warn(LTag.PUMP, "Execute Command: " + commandType.name()); + + OmnipodUITask task = new OmnipodUITask(injector, commandType, parameters); + + omnipodUtil.setCurrentCommand(commandType); + + // new Thread(() -> { + // LOG.warn("@@@ Start Thread"); + // + // task.execute(getCommunicationManager()); + // + // LOG.warn("@@@ End Thread"); + // }); + + task.execute(this.omnipodCommunicationManager); + + // for (int i = 0; i < getMaxWaitTime(commandType); i++) { + // synchronized (task) { + // // try { + // // + // // //task.wait(1000); + // // } catch (InterruptedException e) { + // // LOG.error("executeCommand InterruptedException", e); + // // } + // + // + // SystemClock.sleep(1000); + // } + // + // if (task.isReceived()) { + // break; + // } + // } + + if (!task.isReceived()) { + aapsLogger.warn(LTag.PUMP, "Reply not received for " + commandType); + } + + task.postProcess(omnipodUIPostprocessor); + + return task; + + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIPostprocessor.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIPostprocessor.java new file mode 100644 index 0000000000..cc5878c27e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIPostprocessor.java @@ -0,0 +1,111 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.ui; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.logging.L; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodPumpPluginInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; + +/** + * Created by andy on 4.8.2019 + */ + +public class OmnipodUIPostprocessor { + + + private static final Logger LOG = LoggerFactory.getLogger(L.PUMP); + + private OmnipodPumpStatus pumpStatus; + private OmnipodPumpPluginInterface omnipodPumpPlugin; + private RxBusWrapper rxBus; + + + public OmnipodUIPostprocessor(OmnipodPumpPluginInterface plugin, OmnipodPumpStatus pumpStatus) { + this.pumpStatus = pumpStatus; + this.omnipodPumpPlugin = plugin; + this.rxBus = plugin.getRxBus(); + } + + + // this is mostly intended for command that return certain statuses (Remaining Insulin, ...), and + // where responses won't be directly used + public void postProcessData(OmnipodUITask uiTask) { + + switch (uiTask.commandType) { + + case SetBolus: { + if (uiTask.returnData != null) { + + PumpEnactResult result = uiTask.returnData; + + DetailedBolusInfo detailedBolusInfo = (DetailedBolusInfo) uiTask.getObjectFromParameters(0); + + if (result.success) { + boolean isSmb = detailedBolusInfo.isSMB; + + if (!isSmb) { + pumpStatus.lastBolusAmount = detailedBolusInfo.insulin; + pumpStatus.lastBolusTime = new Date(); + } + } + } + } + break; + + case CancelTemporaryBasal: { + pumpStatus.tempBasalStart = 0; + pumpStatus.tempBasalEnd = 0; + pumpStatus.tempBasalAmount = null; + pumpStatus.tempBasalLength = null; + } + break; + +// case PairAndPrimePod: { +// if (uiTask.returnData.success) { +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.PairAndPrime, false); +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.FillCanulaSetBasalProfile, true); +// } +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.DeactivatePod, true); +// } +// break; +// +// case FillCanulaAndSetBasalProfile: { +// if (uiTask.returnData.success) { +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.FillCanulaSetBasalProfile, false); +// } +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.DeactivatePod, true); +// } +// break; +// +// case DeactivatePod: +// case ResetPodStatus: { +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.PairAndPrime, true); +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.DeactivatePod, false); +// } +// break; + + + default: + if (isLogEnabled()) + LOG.trace("Post-processing not implemented for {}.", uiTask.commandType.name()); + + } + + + } + + + private boolean isLogEnabled() { + return L.isEnabled(L.PUMP); + } + + public RxBusWrapper getRxBus() { + return this.rxBus; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUITask.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUITask.java new file mode 100644 index 0000000000..3479a299eb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUITask.java @@ -0,0 +1,248 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.ui; + +import javax.inject.Inject; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommandType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommunicationManagerInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodDeviceState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitReceiver; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodResponseType; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodDeviceStatusChange; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; + +/** + * Created by andy on 4.8.2019 + */ + +public class OmnipodUITask { + + @Inject RxBusWrapper rxBus; + @Inject AAPSLogger aapsLogger; + @Inject OmnipodPumpStatus omnipodPumpStatus; + @Inject OmnipodUtil omnipodUtil; + + private final HasAndroidInjector injector; + + public OmnipodCommandType commandType; + public PumpEnactResult returnData; + private String errorDescription; + private Object[] parameters; + public PodResponseType responseType; + public Object returnDataObject; + + + public OmnipodUITask(HasAndroidInjector injector, OmnipodCommandType commandType) { + this.injector = injector; + this.injector.androidInjector().inject(this); + this.commandType = commandType; + } + + + public OmnipodUITask(HasAndroidInjector injector, OmnipodCommandType commandType, Object... parameters) { + this.injector = injector; + this.injector.androidInjector().inject(this); + this.commandType = commandType; + this.parameters = parameters; + } + + + public void execute(OmnipodCommunicationManagerInterface communicationManager) { + + + aapsLogger.debug(LTag.PUMP, "OmnipodUITask: @@@ In execute. {}", commandType); + + switch (commandType) { + + case PairAndPrimePod: + returnData = communicationManager.initPod((PodInitActionType) parameters[0], (PodInitReceiver) parameters[1], null); + break; + + case FillCanulaAndSetBasalProfile: + returnData = communicationManager.initPod((PodInitActionType) parameters[0], (PodInitReceiver) parameters[1], (Profile) parameters[2]); + break; + + case DeactivatePod: + returnData = communicationManager.deactivatePod((PodInitReceiver) parameters[0]); + break; + + case ResetPodStatus: + returnData = communicationManager.resetPodStatus(); + break; + + case SetBasalProfile: + returnData = communicationManager.setBasalProfile((Profile) parameters[0]); + break; + + case SetBolus: { + DetailedBolusInfo detailedBolusInfo = (DetailedBolusInfo) parameters[0]; + + if (detailedBolusInfo != null) + returnData = communicationManager.setBolus(detailedBolusInfo); + } + break; + + case GetPodPulseLog: + // This command is very error prone, so retry a few times if it fails + // Can take some time, but that's ok since this is a very specific feature for experts + // And will not be used by normal users + for (int i = 0; 3 > i; i++) { + try { + returnDataObject = communicationManager.readPulseLog(); + responseType = PodResponseType.Acknowledgment; + break; + } catch (Exception ex) { + { + aapsLogger.warn(LTag.PUMP, "Failed to retrieve pulse log", ex); + } + returnDataObject = null; + responseType = PodResponseType.Error; + } + } + break; + + case GetPodStatus: + returnData = communicationManager.getPodStatus(); + break; + + case CancelBolus: + returnData = communicationManager.cancelBolus(); + break; + + case SetTemporaryBasal: { + TempBasalPair tbr = getTBRSettings(); + if (tbr != null) { + returnData = communicationManager.setTemporaryBasal(tbr); + } + } + break; + + case CancelTemporaryBasal: + returnData = communicationManager.cancelTemporaryBasal(); + break; + + case AcknowledgeAlerts: + returnData = communicationManager.acknowledgeAlerts(); + break; + + case SetTime: + returnData = communicationManager.setTime(); + break; + + default: { + aapsLogger.warn(LTag.PUMP, "This commandType is not supported (yet) - {}.", commandType); + responseType = PodResponseType.Error; + } + + } + + if (returnData != null) { + responseType = returnData.success ? PodResponseType.Acknowledgment : PodResponseType.Error; + } + + } + + + private TempBasalPair getTBRSettings() { + return new TempBasalPair(getDoubleFromParameters(0), // + false, // + getIntegerFromParameters(1)); + } + + + private Float getFloatFromParameters(int index) { + return (Float) parameters[index]; + } + + public Object getObjectFromParameters(int index) { + return parameters[index]; + } + + public Double getDoubleFromParameters(int index) { + return (Double) parameters[index]; + } + + public boolean getBooleanFromParameters(int index) { + return (boolean) parameters[index]; + } + + public Integer getIntegerFromParameters(int index) { + return (Integer) parameters[index]; + } + + + public T getResult() { + return (T) returnData; + } + + + public boolean isReceived() { + return (returnData != null || errorDescription != null); + } + + + public void postProcess(OmnipodUIPostprocessor postprocessor) { + + EventOmnipodDeviceStatusChange statusChange; + + aapsLogger.debug(LTag.PUMP, "OmnipodUITask: @@@ postProcess. {}", commandType); + aapsLogger.debug(LTag.PUMP, "OmnipodUITask: @@@ postProcess. responseType={}", responseType); + + if (responseType == PodResponseType.Data || responseType == PodResponseType.Acknowledgment) { + postprocessor.postProcessData(this); + } + + aapsLogger.debug(LTag.PUMP, "OmnipodUITask: @@@ postProcess. responseType={}", responseType); + + if (responseType == PodResponseType.Invalid) { + statusChange = new EventOmnipodDeviceStatusChange(PodDeviceState.ErrorWhenCommunicating, + "Unsupported command in OmnipodUITask"); + omnipodPumpStatus.setLastFailedCommunicationToNow(); + rxBus.send(statusChange); + } else if (responseType == PodResponseType.Error) { + statusChange = new EventOmnipodDeviceStatusChange(PodDeviceState.ErrorWhenCommunicating, + errorDescription); + omnipodPumpStatus.setLastFailedCommunicationToNow(); + rxBus.send(statusChange); + } else { + omnipodPumpStatus.setLastCommunicationToNow(); + rxBus.send(new EventOmnipodPumpValuesChanged()); + } + + omnipodUtil.setPodDeviceState(PodDeviceState.Sleeping); + } + + + public boolean hasData() { + return (responseType == PodResponseType.Data || responseType == PodResponseType.Acknowledgment); + } + + + public Object getParameter(int index) { + return parameters[index]; + } + + + public PodResponseType getResponseType() { + return this.responseType; + } + + public boolean wasCommandSuccessful() { + if (returnData == null) { + return false; + } + return returnData.success; + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodAcknowledgeAlertsChanged.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodAcknowledgeAlertsChanged.kt new file mode 100644 index 0000000000..9938319858 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodAcknowledgeAlertsChanged.kt @@ -0,0 +1,8 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.events + +import info.nightscout.androidaps.events.Event + +/** + * Created by andy on 04.06.2018. + */ +class EventOmnipodAcknowledgeAlertsChanged : Event() \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodDeviceStatusChange.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodDeviceStatusChange.kt new file mode 100644 index 0000000000..db46550a05 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodDeviceStatusChange.kt @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.events + +import info.nightscout.androidaps.events.Event +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkError +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodDeviceState +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState + +/** + * Created by andy on 4.8.2019 + */ +class EventOmnipodDeviceStatusChange : Event { + + var rileyLinkServiceState: RileyLinkServiceState? = null + var rileyLinkError: RileyLinkError? = null + var podSessionState: PodSessionState? = null + var errorDescription: String? = null + var podDeviceState: PodDeviceState? = null + + @JvmOverloads + constructor(rileyLinkServiceState: RileyLinkServiceState?, rileyLinkError: RileyLinkError? = null) { + this.rileyLinkServiceState = rileyLinkServiceState + this.rileyLinkError = rileyLinkError + } + + constructor(podSessionState: PodSessionState?) { + this.podSessionState = podSessionState + } + + constructor(errorDescription: String?) { + this.errorDescription = errorDescription + } + + constructor(podDeviceState: PodDeviceState?, errorDescription: String?) { + this.podDeviceState = podDeviceState + this.errorDescription = errorDescription + } + + override fun toString(): String { + return ("EventOmnipodDeviceStatusChange [" // + + "rileyLinkServiceState=" + rileyLinkServiceState + + ", rileyLinkError=" + rileyLinkError // + + ", podSessionState=" + podSessionState // + + ", podDeviceState=" + podDeviceState + "]") + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodPumpValuesChanged.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodPumpValuesChanged.kt new file mode 100644 index 0000000000..5b0f03a241 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodPumpValuesChanged.kt @@ -0,0 +1,8 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.events + +import info.nightscout.androidaps.events.Event + +/** + * Created by andy on 04.06.2018. + */ +class EventOmnipodPumpValuesChanged : Event() \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodRefreshButtonState.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodRefreshButtonState.kt new file mode 100644 index 0000000000..8bbbfd33e6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodRefreshButtonState.kt @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.events + +import info.nightscout.androidaps.events.Event + +class EventOmnipodRefreshButtonState (val newState : Boolean): Event() \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/OmnipodException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/OmnipodException.java new file mode 100644 index 0000000000..22cc9cd020 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/OmnipodException.java @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.exception; + +public abstract class OmnipodException extends RuntimeException { + private boolean certainFailure; + + public OmnipodException(String message, boolean certainFailure) { + super(message); + this.certainFailure = certainFailure; + } + + public OmnipodException(String message, Throwable cause, boolean certainFailure) { + super(message, cause); + this.certainFailure = certainFailure; + } + + public boolean isCertainFailure() { + return certainFailure; + } + + public void setCertainFailure(boolean certainFailure) { + this.certainFailure = certainFailure; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/service/RileyLinkOmnipodService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/service/RileyLinkOmnipodService.java new file mode 100644 index 0000000000..abaaaf737a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/service/RileyLinkOmnipodService.java @@ -0,0 +1,240 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.service; + +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.os.Binder; +import android.os.IBinder; + +import com.google.gson.Gson; + +import javax.inject.Inject; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkCommunicationManager; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.RFSpy; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.RileyLinkBLE; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkEncodingType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkTargetDevice; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkService; +import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; +import info.nightscout.androidaps.plugins.pump.omnipod.OmnipodPumpPlugin; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.comm.AapsOmnipodManager; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; + + +/** + * Created by andy on 4.8.2019 + * RileyLinkOmnipodService is intended to stay running when the gui-app is closed. + */ +public class RileyLinkOmnipodService extends RileyLinkService { + + @Inject OmnipodPumpPlugin omnipodPumpPlugin; + @Inject OmnipodPumpStatus omnipodPumpStatus; + @Inject OmnipodUtil omnipodUtil; + + private static RileyLinkOmnipodService instance; + + private OmnipodCommunicationManager omnipodCommunicationManager; + private AapsOmnipodManager aapsOmnipodManager; + + private IBinder mBinder = new LocalBinder(); + private boolean rileyLinkAddressChanged = false; + private boolean inPreInit = true; + + + public RileyLinkOmnipodService() { + super(); + instance = this; + } + + + public static RileyLinkOmnipodService getInstance() { + return instance; + } + + + @Override + public void onConfigurationChanged(Configuration newConfig) { + aapsLogger.warn(LTag.PUMPCOMM, "onConfigurationChanged"); + super.onConfigurationChanged(newConfig); + } + + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + + @Override + public RileyLinkEncodingType getEncoding() { + return RileyLinkEncodingType.Manchester; + } + + + /** + * If you have customized RileyLinkServiceData you need to override this + */ + public void initRileyLinkServiceData() { + + rileyLinkServiceData.targetDevice = RileyLinkTargetDevice.Omnipod; + + // get most recently used RileyLink address + rileyLinkServiceData.rileylinkAddress = sp.getString(RileyLinkConst.Prefs.RileyLinkAddress, ""); + + rileyLinkBLE = new RileyLinkBLE(injector, this); // or this + rfspy = new RFSpy(injector, rileyLinkBLE); + rfspy.startReader(); + + initializeErosOmnipodManager(); + + aapsLogger.debug(LTag.PUMPCOMM, "RileyLinkOmnipodService newly constructed"); + //omnipodPumpStatus = (OmnipodPumpStatus) omnipodPumpPlugin.getPumpStatusData(); + } + + private void initializeErosOmnipodManager() { + if (AapsOmnipodManager.getInstance() == null) { + PodSessionState podState = null; + if (sp.contains(OmnipodConst.Prefs.PodState) && omnipodUtil.getPodSessionState() == null) { + try { + Gson gson = omnipodUtil.getGsonInstance(); + String storedPodState = sp.getString(OmnipodConst.Prefs.PodState, null); + aapsLogger.info(LTag.PUMPCOMM, "PodSessionState-SP: loaded from SharedPreferences: " + storedPodState); + podState = gson.fromJson(storedPodState, PodSessionState.class); + podState.injectDaggerClass(injector); + omnipodUtil.setPodSessionState(podState); + } catch (Exception ex) { + aapsLogger.error(LTag.PUMPCOMM, "Could not deserialize Pod state", ex); + } + } + OmnipodCommunicationManager omnipodCommunicationService = new OmnipodCommunicationManager(injector, rfspy); + //omnipodCommunicationService.setPumpStatus(omnipodPumpStatus); + this.omnipodCommunicationManager = omnipodCommunicationService; + + this.aapsOmnipodManager = new AapsOmnipodManager(omnipodCommunicationService, podState, omnipodPumpStatus, + omnipodUtil, aapsLogger, rxBus, sp, resourceHelper, injector, activePlugin); + } else { + aapsOmnipodManager = AapsOmnipodManager.getInstance(); + } + } + + + public void resetRileyLinkConfiguration() { + rfspy.resetRileyLinkConfiguration(); + } + + + @Override + public RileyLinkCommunicationManager getDeviceCommunicationManager() { + return omnipodCommunicationManager; + } + + @Override + public void setPumpDeviceState(PumpDeviceState pumpDeviceState) { + this.omnipodPumpStatus.setPumpDeviceState(pumpDeviceState); + } + + + public class LocalBinder extends Binder { + + public RileyLinkOmnipodService getServiceInstance() { + return RileyLinkOmnipodService.this; + } + } + + + /* private functions */ + + // PumpInterface - REMOVE + + public boolean isInitialized() { + return RileyLinkServiceState.isReady(rileyLinkServiceData.rileyLinkServiceState); + } + + + @Override + public String getDeviceSpecificBroadcastsIdentifierPrefix() { + return null; + } + + + public boolean handleDeviceSpecificBroadcasts(Intent intent) { + return false; + } + + + @Override + public void registerDeviceSpecificBroadcasts(IntentFilter intentFilter) { + } + + + public boolean verifyConfiguration() { + try { + omnipodPumpStatus.errorDescription = "-"; + + String rileyLinkAddress = sp.getString(RileyLinkConst.Prefs.RileyLinkAddress, null); + + if (rileyLinkAddress == null) { + aapsLogger.debug(LTag.PUMPCOMM, "RileyLink address invalid: null"); + omnipodPumpStatus.errorDescription = resourceHelper.gs(R.string.medtronic_error_rileylink_address_invalid); + return false; + } else { + if (!rileyLinkAddress.matches(omnipodPumpStatus.regexMac)) { + omnipodPumpStatus.errorDescription = resourceHelper.gs(R.string.medtronic_error_rileylink_address_invalid); + aapsLogger.debug(LTag.PUMPCOMM, "RileyLink address invalid: {}", rileyLinkAddress); + } else { + if (!rileyLinkAddress.equals(this.omnipodPumpStatus.rileyLinkAddress)) { + this.omnipodPumpStatus.rileyLinkAddress = rileyLinkAddress; + rileyLinkAddressChanged = true; + } + } + } + + this.omnipodPumpStatus.beepBasalEnabled = sp.getBoolean(OmnipodConst.Prefs.BeepBasalEnabled, true); + this.omnipodPumpStatus.beepBolusEnabled = sp.getBoolean(OmnipodConst.Prefs.BeepBolusEnabled, true); + this.omnipodPumpStatus.beepSMBEnabled = sp.getBoolean(OmnipodConst.Prefs.BeepSMBEnabled, true); + this.omnipodPumpStatus.beepTBREnabled = sp.getBoolean(OmnipodConst.Prefs.BeepTBREnabled, true); + this.omnipodPumpStatus.podDebuggingOptionsEnabled = sp.getBoolean(OmnipodConst.Prefs.PodDebuggingOptionsEnabled, false); + this.omnipodPumpStatus.timeChangeEventEnabled = sp.getBoolean(OmnipodConst.Prefs.TimeChangeEventEnabled, true); + + aapsLogger.debug(LTag.PUMPCOMM, "Beeps [basal={}, bolus={}, SMB={}, TBR={}]", this.omnipodPumpStatus.beepBasalEnabled, this.omnipodPumpStatus.beepBolusEnabled, this.omnipodPumpStatus.beepSMBEnabled, this.omnipodPumpStatus.beepTBREnabled); + + reconfigureService(); + + return true; + + } catch (Exception ex) { + this.omnipodPumpStatus.errorDescription = ex.getMessage(); + aapsLogger.error(LTag.PUMPCOMM, "Error on Verification: " + ex.getMessage(), ex); + return false; + } + } + + + private boolean reconfigureService() { + + if (!inPreInit) { + + if (rileyLinkAddressChanged) { + rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.Intents.RileyLinkNewAddressSet, this); + rileyLinkAddressChanged = false; + } + } + + return (!rileyLinkAddressChanged); + } + + public boolean setNotInPreInit() { + this.inPreInit = false; + + return reconfigureService(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmniCRC.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmniCRC.java new file mode 100644 index 0000000000..ba5c91c0b5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmniCRC.java @@ -0,0 +1,74 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.util; + +public class OmniCRC { + public static final int[] crc16lookup = new int[] { + 0x0000, 0x8005, 0x800f, 0x000a, 0x801b, 0x001e, 0x0014, 0x8011, + 0x8033, 0x0036, 0x003c, 0x8039, 0x0028, 0x802d, 0x8027, 0x0022, + 0x8063, 0x0066, 0x006c, 0x8069, 0x0078, 0x807d, 0x8077, 0x0072, + 0x0050, 0x8055, 0x805f, 0x005a, 0x804b, 0x004e, 0x0044, 0x8041, + 0x80c3, 0x00c6, 0x00cc, 0x80c9, 0x00d8, 0x80dd, 0x80d7, 0x00d2, + 0x00f0, 0x80f5, 0x80ff, 0x00fa, 0x80eb, 0x00ee, 0x00e4, 0x80e1, + 0x00a0, 0x80a5, 0x80af, 0x00aa, 0x80bb, 0x00be, 0x00b4, 0x80b1, + 0x8093, 0x0096, 0x009c, 0x8099, 0x0088, 0x808d, 0x8087, 0x0082, + 0x8183, 0x0186, 0x018c, 0x8189, 0x0198, 0x819d, 0x8197, 0x0192, + 0x01b0, 0x81b5, 0x81bf, 0x01ba, 0x81ab, 0x01ae, 0x01a4, 0x81a1, + 0x01e0, 0x81e5, 0x81ef, 0x01ea, 0x81fb, 0x01fe, 0x01f4, 0x81f1, + 0x81d3, 0x01d6, 0x01dc, 0x81d9, 0x01c8, 0x81cd, 0x81c7, 0x01c2, + 0x0140, 0x8145, 0x814f, 0x014a, 0x815b, 0x015e, 0x0154, 0x8151, + 0x8173, 0x0176, 0x017c, 0x8179, 0x0168, 0x816d, 0x8167, 0x0162, + 0x8123, 0x0126, 0x012c, 0x8129, 0x0138, 0x813d, 0x8137, 0x0132, + 0x0110, 0x8115, 0x811f, 0x011a, 0x810b, 0x010e, 0x0104, 0x8101, + 0x8303, 0x0306, 0x030c, 0x8309, 0x0318, 0x831d, 0x8317, 0x0312, + 0x0330, 0x8335, 0x833f, 0x033a, 0x832b, 0x032e, 0x0324, 0x8321, + 0x0360, 0x8365, 0x836f, 0x036a, 0x837b, 0x037e, 0x0374, 0x8371, + 0x8353, 0x0356, 0x035c, 0x8359, 0x0348, 0x834d, 0x8347, 0x0342, + 0x03c0, 0x83c5, 0x83cf, 0x03ca, 0x83db, 0x03de, 0x03d4, 0x83d1, + 0x83f3, 0x03f6, 0x03fc, 0x83f9, 0x03e8, 0x83ed, 0x83e7, 0x03e2, + 0x83a3, 0x03a6, 0x03ac, 0x83a9, 0x03b8, 0x83bd, 0x83b7, 0x03b2, + 0x0390, 0x8395, 0x839f, 0x039a, 0x838b, 0x038e, 0x0384, 0x8381, + 0x0280, 0x8285, 0x828f, 0x028a, 0x829b, 0x029e, 0x0294, 0x8291, + 0x82b3, 0x02b6, 0x02bc, 0x82b9, 0x02a8, 0x82ad, 0x82a7, 0x02a2, + 0x82e3, 0x02e6, 0x02ec, 0x82e9, 0x02f8, 0x82fd, 0x82f7, 0x02f2, + 0x02d0, 0x82d5, 0x82df, 0x02da, 0x82cb, 0x02ce, 0x02c4, 0x82c1, + 0x8243, 0x0246, 0x024c, 0x8249, 0x0258, 0x825d, 0x8257, 0x0252, + 0x0270, 0x8275, 0x827f, 0x027a, 0x826b, 0x026e, 0x0264, 0x8261, + 0x0220, 0x8225, 0x822f, 0x022a, 0x823b, 0x023e, 0x0234, 0x8231, + 0x8213, 0x0216, 0x021c, 0x8219, 0x0208, 0x820d, 0x8207, 0x0202 + }; + public static final int[] crc8lookup = new int[]{ + 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D, + 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D, + 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD, + 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD, + 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA, + 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, + 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, + 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, + 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, + 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, + 0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, + 0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, + 0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, + 0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, + 0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, + 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3 + }; + + + public static int crc16(byte[] bytes) { + int crc = 0x0000; + for (byte b : bytes) { + crc = (crc >> 8) ^ crc16lookup[(crc ^ b) & 0xff]; + } + return crc; + } + + public static byte crc8(byte[] bytes) { + byte crc = 0x00; + for (byte b : bytes) { + crc = (byte) crc8lookup[(crc ^ b) & 0xff]; + } + return crc; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodConst.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodConst.java new file mode 100644 index 0000000000..f7b78250c3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodConst.java @@ -0,0 +1,56 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.util; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.R; + +/** + * Created by andy on 4.8.2019 + */ + +public class OmnipodConst { + + static final String Prefix = "AAPS.Omnipod."; + + public class Prefs { + public static final String PodState = Prefix + "pod_state"; + public static final int BeepBasalEnabled = R.string.key_omnipod_beep_basal_enabled; + public static final int BeepBolusEnabled = R.string.key_omnipod_beep_bolus_enabled; + public static final int BeepSMBEnabled = R.string.key_omnipod_beep_smb_enabled; + public static final int BeepTBREnabled = R.string.key_omnipod_beep_tbr_enabled; + public static final int PodDebuggingOptionsEnabled = R.string.key_omnipod_pod_debugging_options_enabled; + public static final int TimeChangeEventEnabled = R.string.key_omnipod_timechange_enabled; + } + + public class Statistics { + public static final String StatsPrefix = "omnipod_"; + public static final String FirstPumpStart = Prefix + "first_pump_use"; + public static final String LastGoodPumpCommunicationTime = Prefix + "lastGoodPumpCommunicationTime"; + //public static final String LastGoodPumpFrequency = Prefix + "LastGoodPumpFrequency"; + public static final String TBRsSet = StatsPrefix + "tbrs_set"; + public static final String StandardBoluses = StatsPrefix + "std_boluses_delivered"; + public static final String SMBBoluses = StatsPrefix + "smb_boluses_delivered"; + //public static final String LastPumpHistoryEntry = StatsPrefix + "pump_history_entry"; + } + + public static final double POD_PULSE_SIZE = 0.05; + public static final double POD_BOLUS_DELIVERY_RATE = 0.025; // units per second + public static final double POD_PRIMING_DELIVERY_RATE = 0.05; // units per second + public static final double POD_CANNULA_INSERTION_DELIVERY_RATE = 0.05; // units per second + public static final double MAX_RESERVOIR_READING = 50.0; + public static final double MAX_BOLUS = 30.0; + public static final double MAX_BASAL_RATE = 30.0; + public static final Duration MAX_TEMP_BASAL_DURATION = Duration.standardHours(12); + public static final int DEFAULT_ADDRESS = 0xffffffff; + + public static final Duration AVERAGE_BOLUS_COMMAND_COMMUNICATION_DURATION = Duration.millis(1500); + + public static final Duration SERVICE_DURATION = Duration.standardHours(80); + public static final Duration EXPIRATION_ADVISORY_WINDOW = Duration.standardHours(9); + public static final Duration END_OF_SERVICE_IMMINENT_WINDOW = Duration.standardHours(1); + public static final Duration NOMINAL_POD_LIFE = Duration.standardHours(72); + public static final double LOW_RESERVOIR_ALERT = 20.0; + + public static final double POD_PRIME_BOLUS_UNITS = 2.6; + public static final double POD_CANNULA_INSERTION_BOLUS_UNITS = 0.5; +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodUtil.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodUtil.java new file mode 100644 index 0000000000..65d9428ffc --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodUtil.java @@ -0,0 +1,195 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.util; + +import android.content.Context; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializer; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.ISODateTimeFormat; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.data.RLHistoryItem; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommandType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodPodType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodDeviceState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodDeviceStatusChange; +import info.nightscout.androidaps.utils.alertDialogs.OKDialog; + +/** + * Created by andy on 4/8/19. + */ +@Singleton +public class OmnipodUtil { + + private final AAPSLogger aapsLogger; + private final RxBusWrapper rxBus; + private final RileyLinkUtil rileyLinkUtil; + private final OmnipodPumpStatus omnipodPumpStatus; + private final ActivePluginProvider activePlugins; + + + private boolean lowLevelDebug = true; + private OmnipodCommandType currentCommand; + private Gson gsonInstance = createGson(); + //private static PodSessionState podSessionState; + //private static PodDeviceState podDeviceState; + private OmnipodPodType omnipodPodType; + private OmnipodDriverState driverState = OmnipodDriverState.NotInitalized; + + + @Inject + public OmnipodUtil( + AAPSLogger aapsLogger, + RxBusWrapper rxBus, + RileyLinkUtil rileyLinkUtil, + OmnipodPumpStatus omnipodPumpStatus, + ActivePluginProvider activePlugins + ) { + this.aapsLogger = aapsLogger; + this.rxBus = rxBus; + this.rileyLinkUtil = rileyLinkUtil; + this.omnipodPumpStatus = omnipodPumpStatus; + this.activePlugins = activePlugins; + } + + + public boolean isLowLevelDebug() { + return lowLevelDebug; + } + + + public void setLowLevelDebug(boolean lowLevelDebug) { + this.lowLevelDebug = lowLevelDebug; + } + + + public OmnipodCommandType getCurrentCommand() { + return currentCommand; + } + + + public void setCurrentCommand(OmnipodCommandType currentCommand) { + this.currentCommand = currentCommand; + + if (currentCommand != null) + rileyLinkUtil.getRileyLinkHistory().add(new RLHistoryItem(currentCommand)); + } + + + public static void displayNotConfiguredDialog(Context context) { + OKDialog.showConfirmation(context, MainApp.gs(R.string.combo_warning), + MainApp.gs(R.string.omnipod_error_operation_not_possible_no_configuration), (Runnable) null); + } + + + public OmnipodDriverState getDriverState() { + return driverState; + } + + + public void setDriverState(OmnipodDriverState state) { + if (driverState == state) + return; + + driverState = state; + omnipodPumpStatus.driverState = state; + + // TODO maybe remove +// if (OmnipodUtil.omnipodPumpStatus != null) { +// OmnipodUtil.omnipodPumpStatus.driverState = state; +// } +// +// if (OmnipodUtil.omnipodPumpPlugin != null) { +// OmnipodUtil.omnipodPumpPlugin.setDriverState(state); +// } + } + + + private Gson createGson() { + GsonBuilder gsonBuilder = new GsonBuilder() + .registerTypeAdapter(DateTime.class, (JsonSerializer) (dateTime, typeOfSrc, context) -> + new JsonPrimitive(ISODateTimeFormat.dateTime().print(dateTime))) + .registerTypeAdapter(DateTime.class, (JsonDeserializer) (json, typeOfT, context) -> + ISODateTimeFormat.dateTime().parseDateTime(json.getAsString())) + .registerTypeAdapter(DateTimeZone.class, (JsonSerializer) (timeZone, typeOfSrc, context) -> + new JsonPrimitive(timeZone.getID())) + .registerTypeAdapter(DateTimeZone.class, (JsonDeserializer) (json, typeOfT, context) -> + DateTimeZone.forID(json.getAsString())); + + return gsonBuilder.create(); + } + + + public void setPodSessionState(PodSessionState podSessionState) { + omnipodPumpStatus.podSessionState = podSessionState; + rxBus.send(new EventOmnipodDeviceStatusChange(podSessionState)); + } + + + public void setPodDeviceState(PodDeviceState podDeviceState) { + omnipodPumpStatus.podDeviceState = podDeviceState; + } + + + public void setOmnipodPodType(OmnipodPodType omnipodPodType) { + this.omnipodPodType = omnipodPodType; + } + + + public OmnipodPodType getOmnipodPodType() { + return this.omnipodPodType; + } + + + public PodDeviceState getPodDeviceState() { + return omnipodPumpStatus.podDeviceState; + } + + + public PodSessionState getPodSessionState() { + return omnipodPumpStatus.podSessionState; + } + + + public boolean isOmnipodEros() { + return this.activePlugins.getActivePump().model() == PumpType.Insulet_Omnipod; + } + + + public boolean isOmnipodDash() { + return this.activePlugins.getActivePump().model() == PumpType.Insulet_Omnipod_Dash; + } + + + public void setPumpType(PumpType pumpType_) { + omnipodPumpStatus.pumpType = pumpType_; + } + + + public PumpType getPumpType() { + return omnipodPumpStatus.pumpType; + } + + + public Gson getGsonInstance() { + return this.gsonInstance; + } + +} diff --git a/app/src/main/res/layout/omnipod_fragment.xml b/app/src/main/res/layout/omnipod_fragment.xml new file mode 100644 index 0000000000..2068f90557 --- /dev/null +++ b/app/src/main/res/layout/omnipod_fragment.xml @@ -0,0 +1,633 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +