diff --git a/app/build.gradle b/app/build.gradle index e94c3d61ef..8790d898fe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -163,6 +163,10 @@ android { } useLibrary "org.apache.http.legacy" + + dataBinding { + enabled = true + } } allprojects { @@ -193,6 +197,7 @@ dependencies { implementation project(':omnipod-dash') implementation project(':diaconn') implementation project(':openhumans') + implementation project(':eopatch') implementation fileTree(include: ['*.jar'], dir: 'libs') diff --git a/app/libs/eopatch_core.aar b/app/libs/eopatch_core.aar new file mode 100644 index 0000000000..1a8aafe38f Binary files /dev/null and b/app/libs/eopatch_core.aar differ diff --git a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt index 62435f5d19..ac0b07554d 100644 --- a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt @@ -37,6 +37,7 @@ import info.nightscout.androidaps.plugins.general.wear.WearPlugin import info.nightscout.androidaps.plugins.general.xdripStatusline.StatusLinePlugin import info.nightscout.androidaps.plugins.insulin.InsulinOrefFreePeakPlugin import info.nightscout.androidaps.plugins.pump.combo.ComboPlugin +import info.nightscout.androidaps.plugins.pump.eopatch.EopatchPumpPlugin import info.nightscout.androidaps.plugins.pump.insight.LocalInsightPlugin import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin import info.nightscout.androidaps.plugins.pump.virtual.VirtualPumpPlugin @@ -98,6 +99,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang @Inject lateinit var virtualPumpPlugin: VirtualPumpPlugin @Inject lateinit var wearPlugin: WearPlugin @Inject lateinit var maintenancePlugin: MaintenancePlugin + @Inject lateinit var eopatchPumpPlugin: EopatchPumpPlugin @Inject lateinit var passwordCheck: PasswordCheck @Inject lateinit var nsSettingStatus: NSSettingsStatus @@ -180,6 +182,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang addPreferencesFromResourceIfEnabled(medtronicPumpPlugin, rootKey, config.PUMPDRIVERS) addPreferencesFromResourceIfEnabled(diaconnG8Plugin, rootKey, config.PUMPDRIVERS) addPreferencesFromResource(R.xml.pref_pump, rootKey, config.PUMPDRIVERS) + addPreferencesFromResourceIfEnabled(eopatchPumpPlugin, rootKey, config.PUMPDRIVERS) addPreferencesFromResourceIfEnabled(virtualPumpPlugin, rootKey) addPreferencesFromResourceIfEnabled(insulinOrefFreePeakPlugin, rootKey) addPreferencesFromResourceIfEnabled(nsClientPlugin, rootKey) diff --git a/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt b/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt index c206de3837..f2156a9b11 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt @@ -18,6 +18,7 @@ import info.nightscout.androidaps.insight.di.InsightModule import info.nightscout.androidaps.plugin.general.openhumans.dagger.OpenHumansModule import info.nightscout.androidaps.plugins.pump.common.di.PumpCommonModule import info.nightscout.androidaps.plugins.pump.common.di.RileyLinkModule +import info.nightscout.androidaps.plugins.pump.eopatch.dagger.EopatchModule import info.nightscout.androidaps.plugins.pump.medtronic.di.MedtronicModule import info.nightscout.androidaps.plugins.pump.omnipod.dash.dagger.OmnipodDashModule import info.nightscout.androidaps.plugins.pump.omnipod.eros.dagger.OmnipodErosModule @@ -62,6 +63,7 @@ import javax.inject.Singleton WorkersModule::class, DiaconnG8Module::class, OpenHumansModule::class, + EopatchModule::class, SharedModule::class ] ) diff --git a/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt b/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt index 1942773a03..67d21de1f2 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt @@ -40,6 +40,7 @@ import info.nightscout.androidaps.plugins.insulin.InsulinOrefUltraRapidActingPlu import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin import info.nightscout.androidaps.plugins.profile.local.LocalProfilePlugin import info.nightscout.androidaps.plugins.pump.combo.ComboPlugin +import info.nightscout.androidaps.plugins.pump.eopatch.EopatchPumpPlugin import info.nightscout.androidaps.plugins.pump.insight.LocalInsightPlugin import info.nightscout.androidaps.plugins.pump.mdi.MDIPlugin import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin @@ -182,6 +183,12 @@ abstract class PluginsModule { @IntKey(155) abstract fun bindDiaconnG8Plugin(plugin: DiaconnG8Plugin): PluginBase + @Binds + @PumpDriver + @IntoMap + @IntKey(156) + abstract fun bindEopatchPumpPlugin(plugin: EopatchPumpPlugin): PluginBase + @Binds @NotNSClient @IntoMap diff --git a/build.gradle b/build.gradle index fa14964fbc..c7e414e5b0 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,10 @@ buildscript { androidx_junit = '1.1.2' androidx_rules = '1.4.0-alpha04' + + timber_version = "4.7.1" + rxandroidble_version = '1.12.1' + replayshare_version = '2.2.0' } repositories { google() diff --git a/core/src/main/java/info/nightscout/androidaps/plugins/common/ManufacturerType.kt b/core/src/main/java/info/nightscout/androidaps/plugins/common/ManufacturerType.kt index a7ac2b5596..e56f421819 100644 --- a/core/src/main/java/info/nightscout/androidaps/plugins/common/ManufacturerType.kt +++ b/core/src/main/java/info/nightscout/androidaps/plugins/common/ManufacturerType.kt @@ -10,5 +10,6 @@ enum class ManufacturerType(val description: String) { Cellnovo("Cellnovo"), Roche("Roche"), Ypsomed("Ypsomed"), - G2e("G2e"); + G2e("G2e"), + Eoflow("Eoflow"); } \ No newline at end of file diff --git a/core/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.kt b/core/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.kt index fc0d025e80..3e97afc8d0 100644 --- a/core/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.kt +++ b/core/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.kt @@ -130,6 +130,7 @@ open class Notification { const val MDT_INVALID_HISTORY_DATA = 76 const val IDENTIFICATION_NOT_SET = 77 const val PERMISSION_BT = 78 + const val EOELOW_PATCH_ALERTS = 79 const val USER_MESSAGE = 1000 diff --git a/core/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpCapability.kt b/core/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpCapability.kt index 1ceac452df..5f9d9c0571 100644 --- a/core/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpCapability.kt +++ b/core/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpCapability.kt @@ -24,6 +24,7 @@ enum class PumpCapability { OmnipodCapabilities(arrayOf(Bolus, TempBasal, BasalProfileSet, BasalRate30min)), YpsomedCapabilities(arrayOf(Bolus, ExtendedBolus, TempBasal, BasalProfileSet, Refill, ReplaceBattery, TDD, ManualTDDLoad)), // BasalRates (separately grouped) DiaconnCapabilities(arrayOf(Bolus, ExtendedBolus, TempBasal, BasalProfileSet, Refill, ReplaceBattery, TDD, ManualTDDLoad)), // + EopatchCapabilities(arrayOf(Bolus, ExtendedBolus, TempBasal, BasalProfileSet, BasalRate30min)), BasalRate_Duration15minAllowed, BasalRate_Duration30minAllowed, BasalRate_Duration15and30minAllowed(arrayOf(BasalRate_Duration15minAllowed, BasalRate_Duration30minAllowed)), diff --git a/core/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.kt b/core/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.kt index a36883adae..79df319936 100644 --- a/core/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.kt +++ b/core/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.kt @@ -311,7 +311,24 @@ enum class PumpType { baseBasalStep = 0.01, baseBasalSpecialSteps = null, pumpCapability = PumpCapability.DanaWithHistoryCapabilities, - source = Sources.DiaconnG8); + source = Sources.DiaconnG8), + + //EOPatch Pump + EOFLOW_EOPATCH2(description = "Eoflow Eopatch2", + manufacturer = ManufacturerType.Eoflow, + model = "Eopatch", + bolusSize = 0.05, + specialBolusSize = null, + extendedBolusSettings = DoseSettings(0.05, 30, 8 * 60, 0.05, 25.0), + pumpTempBasalType = PumpTempBasalType.Absolute, + tbrSettings = DoseSettings(0.05, 30, 12 * 60, 0.0, 15.0), + specialBasalDurations = PumpCapability.BasalRate_Duration30minAllowed, + baseBasalMinValue = 0.05, + baseBasalMaxValue = 15.0, + baseBasalStep = 0.05, + baseBasalSpecialSteps = null, + pumpCapability = PumpCapability.EopatchCapabilities, + source = Sources.EOPatch2); val description: String var manufacturer: ManufacturerType? = null @@ -393,6 +410,7 @@ enum class PumpType { InterfaceIDs.PumpType.MDI -> MDI InterfaceIDs.PumpType.USER -> USER InterfaceIDs.PumpType.DIACONN_G8 -> DIACONN_G8 + InterfaceIDs.PumpType.EOPATCH2 -> EOFLOW_EOPATCH2 } } @@ -511,5 +529,6 @@ enum class PumpType { MDI -> InterfaceIDs.PumpType.MDI USER -> InterfaceIDs.PumpType.USER DIACONN_G8 -> InterfaceIDs.PumpType.DIACONN_G8 + EOFLOW_EOPATCH2 -> InterfaceIDs.PumpType.EOPATCH2 } } \ No newline at end of file diff --git a/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryMapper.kt b/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryMapper.kt index fb667823e4..2e55678069 100644 --- a/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryMapper.kt +++ b/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryMapper.kt @@ -137,6 +137,7 @@ class UserEntryMapper { Omnipod (UserEntry.Sources.Omnipod), OmnipodEros (UserEntry.Sources.OmnipodEros), OmnipodDash (UserEntry.Sources.OmnipodDash), + EOPatch2 (UserEntry.Sources.EOPatch2), MDI (UserEntry.Sources.MDI), VirtualPump (UserEntry.Sources.VirtualPump), SMS (UserEntry.Sources.SMS), diff --git a/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryPresentationHelper.kt b/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryPresentationHelper.kt index 0b1ac8a3a4..112a0d4f9f 100644 --- a/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryPresentationHelper.kt +++ b/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryPresentationHelper.kt @@ -92,6 +92,7 @@ class UserEntryPresentationHelper @Inject constructor( Sources.Omnipod -> R.drawable.ic_pod_128 Sources.OmnipodEros -> R.drawable.ic_pod_128 Sources.OmnipodDash -> R.drawable.ic_pod_128 + Sources.EOPatch2 -> R.drawable.ic_eopatch2_128 Sources.MDI -> R.drawable.ic_ict Sources.VirtualPump -> R.drawable.ic_virtual_pump Sources.SMS -> R.drawable.ic_sms diff --git a/core/src/main/res/drawable/ic_eopatch2_128.xml b/core/src/main/res/drawable/ic_eopatch2_128.xml new file mode 100644 index 0000000000..1f3e107e05 --- /dev/null +++ b/core/src/main/res/drawable/ic_eopatch2_128.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/crowdin.yml b/crowdin.yml index d310b71a91..74a11cb377 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -41,6 +41,8 @@ files: translation: /automation/src/main/res/values-%android_code%/strings.xml - source: /diaconn/src/main/res/values/strings.xml translation: /diaconn/src/main/res/values-%android_code%/strings.xml + - source: /eopatch/src/main/res/values/strings.xml + translation: /eopatch/src/main/res/values-%android_code%/strings.xml - source: /openhumans/src/main/res/values/strings.xml translation: /openhumans/src/main/res/values-%android_code%/strings.xml translate_attributes: 0 diff --git a/database/src/main/java/info/nightscout/androidaps/database/embedments/InterfaceIDs.kt b/database/src/main/java/info/nightscout/androidaps/database/embedments/InterfaceIDs.kt index 0477dc1c8d..7c8ea6ea22 100644 --- a/database/src/main/java/info/nightscout/androidaps/database/embedments/InterfaceIDs.kt +++ b/database/src/main/java/info/nightscout/androidaps/database/embedments/InterfaceIDs.kt @@ -42,6 +42,7 @@ data class InterfaceIDs( YPSOPUMP, MDI, DIACONN_G8, + EOPATCH2, USER; companion object { diff --git a/database/src/main/java/info/nightscout/androidaps/database/entities/UserEntry.kt b/database/src/main/java/info/nightscout/androidaps/database/entities/UserEntry.kt index 0b0db5bf82..f18fd16a79 100644 --- a/database/src/main/java/info/nightscout/androidaps/database/entities/UserEntry.kt +++ b/database/src/main/java/info/nightscout/androidaps/database/entities/UserEntry.kt @@ -168,6 +168,7 @@ data class UserEntry( Omnipod, //No entry currently OmnipodEros, OmnipodDash, //No entry currently + EOPatch2, MDI, VirtualPump, SMS, //From SMS plugin diff --git a/eopatch/.gitignore b/eopatch/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/eopatch/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/eopatch/build.gradle b/eopatch/build.gradle new file mode 100644 index 0000000000..e94c2bd4bd --- /dev/null +++ b/eopatch/build.gradle @@ -0,0 +1,49 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-allopen' +apply plugin: 'com.hiya.jacoco-android' + +apply from: "${project.rootDir}/gradle/android_dependencies.gradle" +apply from: "${project.rootDir}/gradle/android_module_dependencies.gradle" +apply from: "${project.rootDir}/gradle/test_dependencies.gradle" +apply from: "${project.rootDir}/gradle/jacoco_global.gradle" + +android { + dataBinding { + enabled = true + } +} + +allprojects { + repositories { + flatDir { + dirs 'libs' + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation (name: 'eopatch_core', ext: 'aar') + implementation project(':core') + implementation project(':shared') + implementation project(':database') + + implementation 'androidx.core:core-ktx:1.6.0' + implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + + // RxJava + implementation "io.reactivex.rxjava2:rxjava:$rxjava_version" + implementation "io.reactivex.rxjava2:rxkotlin:$rxkotlin_version" + implementation "io.reactivex.rxjava2:rxandroid:$rxandroid_version" + + //RxAndroidBle + implementation "com.polidea.rxandroidble2:rxandroidble:$rxandroidble_version" + implementation "com.jakewharton.rx2:replaying-share:$replayshare_version" + + // Log + implementation "com.jakewharton.timber:timber:$timber_version" +} \ No newline at end of file diff --git a/eopatch/libs/eopatch_core.aar b/eopatch/libs/eopatch_core.aar new file mode 100644 index 0000000000..1a8aafe38f Binary files /dev/null and b/eopatch/libs/eopatch_core.aar differ diff --git a/eopatch/proguard-rules.pro b/eopatch/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/eopatch/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/eopatch/src/androidTest/java/info/nightscout/androidaps/plugins/pump/eopatch/ExampleInstrumentedTest.kt b/eopatch/src/androidTest/java/info/nightscout/androidaps/plugins/pump/eopatch/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000..5a38981b80 --- /dev/null +++ b/eopatch/src/androidTest/java/info/nightscout/androidaps/plugins/pump/eopatch/ExampleInstrumentedTest.kt @@ -0,0 +1,25 @@ +package info.nightscout.androidaps.plugins.pump.eopatch + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.eoflow.patch", appContext.packageName) + } +} \ No newline at end of file diff --git a/eopatch/src/main/AndroidManifest.xml b/eopatch/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..bd032c2f01 --- /dev/null +++ b/eopatch/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/AppConstant.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/AppConstant.kt new file mode 100644 index 0000000000..c12f177563 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/AppConstant.kt @@ -0,0 +1,87 @@ +package info.nightscout.androidaps.plugins.pump.eopatch + +import java.util.concurrent.TimeUnit + +interface AppConstant { + companion object { + + val BASAL_MIN_AMOUNT = 0.05f + val CLICK_THROTTLE = 600L + + /** + * Bluetooth Connection State + */ + val BT_STATE_NOT_CONNECT = 1 + val BT_STATE_CONNECTED = 2 + + + val INSULIN_DECIMAL_PLACE_VAR = 100f // 10.f; 소수점자리수 계산상수 (100.f 는 두자리) + + // 패치 1P = 1 cycle = 0.1U + const val INSULIN_UNIT_P = 0.05f // 최소 주입 단위 + + val INSULIN_UNIT_MIN_U = 0f + val INSULIN_UNIT_STEP_U = INSULIN_UNIT_P + + /** + * On/Off + */ + val OFF = 0 + val ON = 1 + + /** + * Pump Duration, Interval + */ + val PUMP_DURATION_MILLI = TimeUnit.SECONDS.toMillis(4) // 15; + val PUMP_RESOLUTION = INSULIN_UNIT_P + + /** + * Basal + */ + val BASAL_RATE_PER_HOUR_MIN = BASAL_MIN_AMOUNT + val BASAL_RATE_PER_HOUR_STEP = INSULIN_UNIT_STEP_U + val BASAL_RATE_PER_HOUR_MAX = 15.0f // 30.0f; 30.00U/hr + + val SEGMENT_MAX_SIZE_48 = 48 + val SEGMENT_MAX_SIZE = SEGMENT_MAX_SIZE_48 + + val SEGMENT_COUNT_MAX = SEGMENT_MAX_SIZE_48 + + /** + * Bolus + */ + val BOLUS_NORMAL_ID = 0x1 + val BOLUS_EXTENDED_ID = 0x2 + + val BOLUS_ACTIVE_OFF = OFF + val BOLUS_ACTIVE_NORMAL = 0x01 + val BOLUS_ACTIVE_EXTENDED_WAIT = 0x2 + val BOLUS_ACTIVE_EXTENDED = 0x04 + val BOLUS_ACTIVE_DISCONNECTED = 0x08 + + val BOLUS_UNIT_MIN = BASAL_MIN_AMOUNT + val BOLUS_UNIT_STEP = INSULIN_UNIT_STEP_U + val BOLUS_UNIT_MAX = 25.0f // 30.0f; + val BOLUS_DELIVER_MIN = 0.0f + + + + /* Wizard */ + val WIZARD_STEP_MAX = 24 + + val INFO_REMINDER_DEFAULT_VALUE = 1 + + + val DAY_START_MINUTE = 0 * 60 + val DAY_END_MINUTE = 24 * 60 + + + val SNOOZE_INTERVAL_STEP = 5 + + /* Insulin Duration */ + val INSULIN_DURATION_MIN = 2.0f + val INSULIN_DURATION_MAX = 8.0f + val INSULIN_DURATION_STEP = 0.5f + + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/CommonUtils.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/CommonUtils.kt new file mode 100644 index 0000000000..f2d9c2fb5b --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/CommonUtils.kt @@ -0,0 +1,178 @@ +package info.nightscout.androidaps.plugins.pump.eopatch + + +import android.content.Context +import info.nightscout.androidaps.plugins.pump.common.utils.DateTimeUtil +import io.reactivex.disposables.Disposable +import java.util.* +import java.util.function.Function + +object CommonUtils { + val TO_INT = Function { it.toInt() } + val TO_FLOAT = Function { it.toFloat() } + val TO_STRING = Function { it.toString() } + val TO_CLOCK = Function{ num -> String.format(Locale.US, "%d:%02d", num.toInt() / 60, num.toInt() % 60) } + + @JvmStatic fun dispose(vararg disposable: Disposable?) { + for (d in disposable){ + d?.let { + if (!it.isDisposed()) { + it.dispose() + } + } + } + } + + @JvmStatic fun nullSafe(ch: CharSequence?): String { + if (ch == null) + return "" + val str = ch.toString() + return str + } + + @JvmStatic fun hasText(str: CharSequence?): Boolean { + if (str == null || str.length == 0) { + return false + } + val strLen = str.length + for (i in 0 until strLen) { + if (!Character.isWhitespace(str[i])) { + return true + } + } + return false + } + + @JvmStatic fun hasText(str: String?): Boolean { + return str?.let{hasText(it as CharSequence)}?:false + } + + @JvmStatic fun isStringEmpty(cs: CharSequence?): Boolean { + return cs == null || cs.length == 0 + } + + @JvmStatic fun dateString(millis: Long): String { + if(millis == 0L) return "" + + val c = Calendar.getInstance() + c.timeInMillis = millis + return dateString(c) + } + + fun dateString(c: Calendar): String { + return String.format(Locale.US, "%04d-%02d-%02d %02d:%02d:%02d", + c.get(Calendar.YEAR), + c.get(Calendar.MONTH) + 1, + c.get(Calendar.DAY_OF_MONTH), + c.get(Calendar.HOUR_OF_DAY), + c.get(Calendar.MINUTE), + c.get(Calendar.SECOND)) + } + + fun getTimeString(millis: Long): String { + val c = Calendar.getInstance() + c.timeInMillis = millis + return getTimeString(c) + } + + fun getTimeString(c: Calendar): String { + return String.format(Locale.US, "%02d:%02d", + c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE)) + } + + fun bytesToStringArray(byteArray: ByteArray?): String { + if (byteArray == null || byteArray.size == 0) { + return "null" + } + + val sb = StringBuilder() + for (b in byteArray) { + if (sb.length > 0) { + sb.append(String.format(" %02x", b)) + } else { + sb.append(String.format("0x%02x", b)) + } + } + return sb.toString() + } + + fun insulinFormat(): String { + if (AppConstant.INSULIN_UNIT_STEP_U == 0.1f) { + return "%.1f" + } + return if (AppConstant.INSULIN_UNIT_STEP_U == 0.05f) { + "%.2f" + } else "%.2f 인슐린출력형식추가하셈" + + } + + fun convertInsulinFormat(context: Context, strResId: Int): String { + if (AppConstant.INSULIN_UNIT_STEP_U == 0.1f) { + return context.getString(strResId).replace(".2f", ".1f") + } + if (AppConstant.INSULIN_UNIT_STEP_U == 0.05f) { + return context.getString(strResId) + } + + return context.getString(strResId).replace(".2f", ".2f 인슐린출력형식추가하셈") + + } + + fun getRemainHourMin(timeMillis: Long): Pair { + val diffHours: Long + var diffMinutes: Long + + if (timeMillis >= 0) { + diffMinutes = Math.abs(timeMillis / (60 * 1000) % 60) + 1 + if (diffMinutes == 60L) { + diffMinutes = 0 + diffHours = Math.abs(timeMillis / (60 * 60 * 1000)) + 1 + } else { + diffHours = Math.abs(timeMillis / (60 * 60 * 1000)) + } + } else { + diffMinutes = Math.abs(timeMillis / (60 * 1000) % 60) + diffHours = Math.abs(timeMillis / (60 * 60 * 1000)) + } + return Pair(diffHours, diffMinutes) + } + + fun getTimeString(minutes: Int): String { + return String.format("%d:%02d", minutes / 60, minutes % 60) + } + + fun getTimeString_hhmm(minutes: Int): String { + return String.format("%02d:%02d", minutes / 60, minutes % 60) + } + + @JvmStatic + fun generatePumpId(date: Long, typeCode: Long = 0): Long { + return DateTimeUtil.toATechDate(date) * 100L + typeCode + } + + @JvmStatic + fun nearlyEqual(a: Float, b: Float, epsilon: Float): Boolean { + val absA = Math.abs(a) + val absB = Math.abs(b) + val diff = Math.abs(a - b) + return if (a == b) { // shortcut, handles infinities + true + } else if (a == 0f || b == 0f || absA + absB < java.lang.Float.MIN_NORMAL) { + // a or b is zero or both are extremely close to it + // relative error is less meaningful here + diff < epsilon * java.lang.Float.MIN_NORMAL + } else { // use relative error + diff / Math.min(absA + absB, Float.MAX_VALUE) < epsilon + } + } + + @JvmStatic + fun nearlyNotEqual(a: Float, b: Float, epsilon: Float): Boolean { + return !nearlyEqual(a, b, epsilon) + } + + @JvmStatic + fun clone(src: T): T { + return GsonHelper.sharedGson().fromJson(GsonHelper.sharedGson().toJson(src), src.javaClass) + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EONotification.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EONotification.kt new file mode 100644 index 0000000000..f2ab48588e --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EONotification.kt @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.pump.eopatch + +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.androidaps.plugins.general.overview.notifications.Notification +import javax.inject.Inject + +class EONotification constructor() : Notification() { + + @Inject lateinit var aapsLogger: AAPSLogger + + constructor(id: Int, text: String, level: Int) : this() { + this.id = id + date = System.currentTimeMillis() + this.text = text + this.level = level + } + + fun action(buttonText: Int, action: Runnable) { + this.buttonText = buttonText + this.action = action + } + +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EoPatchRxBus.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EoPatchRxBus.kt new file mode 100644 index 0000000000..be24015956 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EoPatchRxBus.kt @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.eopatch + +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject + +object EoPatchRxBus { + private val publishSubject: PublishSubject = PublishSubject.create() + + fun publish(event: Any) { + publishSubject.onNext(event) + } + + fun listen(eventType: Class): Observable { + return publishSubject.ofType(eventType) + } + +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EopatchPumpPlugin.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EopatchPumpPlugin.kt new file mode 100644 index 0000000000..a829124005 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EopatchPumpPlugin.kt @@ -0,0 +1,588 @@ +package info.nightscout.androidaps.plugins.pump.eopatch + +import android.content.Context +import android.os.SystemClock +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import dagger.android.HasAndroidInjector +import info.nightscout.androidaps.data.DetailedBolusInfo +import info.nightscout.androidaps.interfaces.Profile +import info.nightscout.androidaps.data.PumpEnactResult +import info.nightscout.androidaps.events.EventAppInitialized +import info.nightscout.androidaps.events.EventPreferenceChange +import info.nightscout.androidaps.interfaces.* +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.shared.logging.LTag +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.common.ManufacturerType +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.EventOverviewBolusProgress +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.IAlarmManager +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPatchManager +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager +import info.nightscout.androidaps.plugins.pump.eopatch.code.BolusExDuration +import info.nightscout.androidaps.plugins.pump.eopatch.code.SettingKeys +import info.nightscout.androidaps.plugins.pump.eopatch.extension.takeOne +import info.nightscout.androidaps.plugins.pump.eopatch.extension.with +import info.nightscout.androidaps.plugins.pump.eopatch.ui.EopatchOverviewFragment +import info.nightscout.androidaps.plugins.pump.eopatch.vo.TempBasal +import info.nightscout.androidaps.queue.commands.CustomCommand +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.FabricPrivacy +import info.nightscout.androidaps.utils.T +import info.nightscout.androidaps.utils.TimeChangeType +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.shared.sharedPreferences.SP +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.Consumer +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import org.json.JSONObject +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.math.min +import kotlin.math.roundToInt + +@Singleton +class EopatchPumpPlugin @Inject constructor( + injector: HasAndroidInjector, + aapsLogger: AAPSLogger, + rh: ResourceHelper, + commandQueue: CommandQueue, + private val context: Context, + private val rxBus: RxBus, + private val sp: SP, + private val profileFunction: ProfileFunction, + private val activePlugin: ActivePlugin, + private val fabricPrivacy: FabricPrivacy, + private val dateUtil: DateUtil, + private val pumpSync: PumpSync, + private val patchmanager: IPatchManager, + private val alarmManager: IAlarmManager, + private val preferenceManager: IPreferenceManager +):PumpPluginBase(PluginDescription() + .mainType(PluginType.PUMP) + .fragmentClass(EopatchOverviewFragment::class.java.getName()) + .pluginIcon(R.drawable.ic_eopatch2_128) + .pluginName(R.string.eopatch) + .shortName(R.string.eopatch_shortname) + .preferencesId(R.xml.pref_eopatch) + .description(R.string.eopatch_pump_description), injector, aapsLogger, rh, commandQueue +), Pump { + + private val mDisposables = CompositeDisposable() + + var mPumpType: PumpType = PumpType.EOFLOW_EOPATCH2 + private set + private var mLastDataTime: Long = 0 + private val mPumpDescription = PumpDescription(mPumpType) + + init { + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } + + override fun onStart() { + super.onStart() + mDisposables.add(rxBus + .toObservable(EventPreferenceChange::class.java) + .observeOn(Schedulers.io()) + .subscribe({ event: EventPreferenceChange -> + if (event.isChanged(rh, SettingKeys.LOW_RESERVIOR_REMINDERS) || event.isChanged(rh, SettingKeys.EXPIRATION_REMINDERS)) { + patchmanager.changeReminderSetting() + } else if (event.isChanged(rh, SettingKeys.BUZZER_REMINDERS)) { + patchmanager.changeBuzzerSetting() + } + }) { throwable: Throwable -> fabricPrivacy.logException(throwable) } + ) + + mDisposables.add(rxBus + .toObservable(EventAppInitialized::class.java) + .observeOn(Schedulers.io()) + .subscribe({ event: EventAppInitialized? -> + aapsLogger.debug(LTag.PUMP,"EventAppInitialized") + preferenceManager.init() + patchmanager.init() + alarmManager.init() + }) { throwable: Throwable -> fabricPrivacy.logException(throwable) } + ) + } + + override fun specialEnableCondition(): Boolean { + //BG -> FG 시 패치 활성화 재진행 및 미처리 알람 발생 + if(preferenceManager.isInitDone()) { + patchmanager.checkActivationProcess() + alarmManager.restartAll() + } + return super.specialEnableCondition() + } + + override fun specialShowInListCondition(): Boolean { + return super.specialShowInListCondition() + } + + override fun onStop() { + super.onStop() + aapsLogger.debug(LTag.PUMP, "EOPatchPumpPlugin onStop()"); + } + + override fun onStateChange(type: PluginType?, oldState: State?, newState: State?) { + super.onStateChange(type, oldState, newState) + } + + override fun preprocessPreferences(preferenceFragment: PreferenceFragmentCompat) { + super.preprocessPreferences(preferenceFragment) + } + + override fun updatePreferenceSummary(pref: Preference) { + super.updatePreferenceSummary(pref) + } + + override fun isUnreachableAlertTimeoutExceeded(alertTimeoutMilliseconds: Long): Boolean { + return super.isUnreachableAlertTimeoutExceeded(alertTimeoutMilliseconds) + } + + override fun setNeutralTempAtFullHour(): Boolean { + return super.setNeutralTempAtFullHour() + } + + override fun isInitialized(): Boolean { + val isInit = isConnected() && patchmanager.isActivated() + return isInit + } + + override fun isSuspended(): Boolean { + return patchmanager.patchState.isNormalBasalPaused + } + + override fun isBusy(): Boolean { + return false + } + + override fun isConnected(): Boolean { + return patchmanager.patchConnectionState.isConnected + } + + override fun isConnecting(): Boolean { + return patchmanager.patchConnectionState.isConnecting + } + + override fun isHandshakeInProgress(): Boolean { + return false + } + + override fun finishHandshaking() { + } + + override fun connect(reason: String) { + aapsLogger.debug(LTag.PUMP,"EOPatch connect - reason:$reason") + mLastDataTime = System.currentTimeMillis() + } + + override fun disconnect(reason: String) { + aapsLogger.debug(LTag.PUMP,"EOPatch disconnect - reason:$reason") + } + + override fun stopConnecting() { + } + + override fun getPumpStatus(reason: String) { + if (patchmanager.isActivated()) { + if ("SMS" == reason) { + aapsLogger.debug("Acknowledged AAPS getPumpStatus request it was requested through an SMS") + }else{ + aapsLogger.debug("Acknowledged AAPS getPumpStatus request") + } + patchmanager.updateConnection().subscribe(Consumer { + mLastDataTime = System.currentTimeMillis() + }) + } + } + + override fun setNewBasalProfile(profile: Profile): PumpEnactResult { + mLastDataTime = System.currentTimeMillis() + if(patchmanager.isActivated){ + if(patchmanager.patchState.isTempBasalActive || patchmanager.patchState.isBolusActive){ + return PumpEnactResult(injector) + }else{ + var isSuccess: Boolean? = null + val result: BehaviorSubject = BehaviorSubject.create() + val disposable = result.hide() + .subscribe { + isSuccess = it + } + + val nb = preferenceManager.getNormalBasalManager().convertProfileToNormalBasal(profile) + patchmanager.startBasal(nb) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ response -> + if (response.isSuccess) { + preferenceManager.getNormalBasalManager().normalBasal = nb + preferenceManager.flushNormalBasalManager() + } + result.onNext(response.isSuccess) + }, { throwable -> + result.onNext(false) + }) + + do{ + SystemClock.sleep(100) + }while(isSuccess == null) + + disposable.dispose() + aapsLogger.info(LTag.PUMP, "Basal Profile was set: ${isSuccess?:false}"); + return PumpEnactResult(injector).apply{ success = isSuccess?:false } + } + }else{ + preferenceManager.getNormalBasalManager().setNormalBasal(profile) + preferenceManager.flushNormalBasalManager() + return PumpEnactResult(injector) + } + } + + override fun isThisProfileSet(profile: Profile): Boolean { + if (!patchmanager.isActivated()) { + return true + } + + val ret = preferenceManager.getNormalBasalManager().isEqual(profile) + aapsLogger.info(LTag.PUMP, "Is this profile set? ${ret}"); + return ret + } + + override fun lastDataTime(): Long { + return mLastDataTime + } + + override val baseBasalRate: Double + get() { + if (!patchmanager.isActivated || patchmanager.patchState.isNormalBasalPaused) { + return 0.0 + } + + return preferenceManager.getNormalBasalManager().normalBasal.getCurrentSegment()?.doseUnitPerHour?.toDouble()?:0.05 + } + + override val reservoirLevel: Double + get() { + if (!patchmanager.isActivated) { + return 0.0 + } + val reserviorLevel = patchmanager.patchState.remainedInsulin.toDouble() + + return (reserviorLevel > 50.0).takeOne(50.0, reserviorLevel) + } + + override val batteryLevel: Int + get() { + if(patchmanager.isActivated) { + return patchmanager.patchState.batteryLevel() + }else{ + return 0 + } + } + + override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult { + + if (detailedBolusInfo.insulin == 0.0 && detailedBolusInfo.carbs == 0.0) { + // neither carbs nor bolus requested + aapsLogger.error("deliverTreatment: Invalid input: neither carbs nor insulin are set in treatment") + return PumpEnactResult(injector).success(false).enacted(false).bolusDelivered(0.0).carbsDelivered(0.0) + .comment(rh.gs(R.string.invalidinput)) + } else if (detailedBolusInfo.insulin > 0.0) { + var isSuccess = true + val result = BehaviorSubject.createDefault(true) + val disposable = result.hide() + .subscribe { + isSuccess = it + } + + patchmanager.startCalculatorBolus(detailedBolusInfo) + .doOnSuccess { + mLastDataTime = System.currentTimeMillis() + }.subscribe({ + result.onNext(it.isSuccess) + }, { + result.onNext(false) + }) + + do{ + SystemClock.sleep(100) + if(patchmanager.patchConnectionState.isConnected) { + var delivering = patchmanager.bolusCurrent.nowBolus.injected + rxBus.send(EventOverviewBolusProgress.apply { + status = rh.gs(R.string.bolusdelivering, delivering) + percent = min((delivering / detailedBolusInfo.insulin * 100).toInt(), 100) + }) + } + }while(!patchmanager.bolusCurrent.nowBolus.endTimeSynced && isSuccess) + + rxBus.send(EventOverviewBolusProgress.apply { + status = rh.gs(R.string.bolusdelivered, detailedBolusInfo.insulin) + percent = 100 + }) + + detailedBolusInfo.insulin = patchmanager.bolusCurrent.nowBolus.injected.toDouble() + patchmanager.addBolusToHistory(detailedBolusInfo) + + disposable.dispose() + + return if(isSuccess) + PumpEnactResult(injector).success(true)/*.enacted(true)*/.carbsDelivered(detailedBolusInfo.carbs).bolusDelivered(detailedBolusInfo.insulin) + else + PumpEnactResult(injector).success(false)/*.enacted(false)*/.carbsDelivered(0.0).bolusDelivered(detailedBolusInfo.insulin) + + } else { + // no bolus required, carb only treatment + patchmanager.addBolusToHistory(detailedBolusInfo); + + return PumpEnactResult(injector).success(true).enacted(true).bolusDelivered(0.0) + .carbsDelivered(detailedBolusInfo.carbs).comment(rh.gs(info.nightscout.androidaps.core.R.string.ok)); + } + } + + override fun stopBolusDelivering() { + patchmanager.stopNowBolus() + .with() + .subscribe { it -> + rxBus.send(EventOverviewBolusProgress.apply { + status = rh.gs(R.string.bolusdelivered, (it.injectedBolusAmount * 0.05f)) //todo stoped 메세지로 변경 필요 + }) + } + } + + override fun setTempBasalAbsolute(absoluteRate: Double, durationInMinutes: Int, profile: Profile, enforceNew: Boolean, tbrType: PumpSync.TemporaryBasalType): PumpEnactResult { + aapsLogger.info(LTag.PUMP, "setTempBasalAbsolute - absoluteRate: ${absoluteRate.toFloat()}, durationInMinutes: ${durationInMinutes.toLong()}, enforceNew: $enforceNew") + if(patchmanager.patchState.isNormalBasalAct){ + mLastDataTime = System.currentTimeMillis() + val tb = TempBasal.createAbsolute(durationInMinutes.toLong(), absoluteRate.toFloat()) + return patchmanager.startTempBasal(tb) + .with() + .doOnSuccess { + preferenceManager.getTempBasalManager().startedBasal = tb + preferenceManager.getTempBasalManager().startedBasal?.startTimestamp = System.currentTimeMillis() + pumpSync.syncTemporaryBasalWithPumpId( + timestamp = dateUtil.now(), + rate = absoluteRate, + duration = T.mins(durationInMinutes.toLong()).msecs(), + isAbsolute = true, + type = tbrType, + pumpId = dateUtil.now(), + pumpType = PumpType.EOFLOW_EOPATCH2, + pumpSerial = serialNumber() + ) + aapsLogger.info(LTag.PUMP,"setTempBasalAbsolute - tbrCurrent:${readTBR()}") + } + .map { it -> PumpEnactResult(injector).success(true).enacted(true).duration(durationInMinutes).absolute(absoluteRate).isPercent(false).isTempCancel(false) } + .onErrorReturnItem(PumpEnactResult(injector).success(false).enacted(false) + .comment("Internal error")) + .blockingGet() + }else{ + aapsLogger.info(LTag.PUMP,"setTempBasalAbsolute - normal basal is not active") + return PumpEnactResult(injector).success(false).enacted(false) + } + } + + override fun setTempBasalPercent(percent: Int, durationInMinutes: Int, profile: Profile, enforceNew: Boolean, tbrType: PumpSync.TemporaryBasalType): PumpEnactResult { + aapsLogger.info(LTag.PUMP,"setTempBasalPercent - percent: $percent, durationInMinutes: $durationInMinutes, enforceNew: $enforceNew") + if(patchmanager.patchState.isNormalBasalAct && percent != 0){ + mLastDataTime = System.currentTimeMillis() + val tb = TempBasal.createPercent(durationInMinutes.toLong(), percent) + return patchmanager.startTempBasal(tb) + .with() + .doOnSuccess { + preferenceManager.getTempBasalManager().startedBasal = tb + preferenceManager.getTempBasalManager().startedBasal?.startTimestamp = System.currentTimeMillis() + pumpSync.syncTemporaryBasalWithPumpId( + timestamp = dateUtil.now(), + rate = percent.toDouble(), + duration = T.mins(durationInMinutes.toLong()).msecs(), + isAbsolute = false, + type = tbrType, + pumpId = dateUtil.now(), + pumpType = PumpType.EOFLOW_EOPATCH2, + pumpSerial = serialNumber() + ) + aapsLogger.info(LTag.PUMP,"setTempBasalPercent - tbrCurrent:${readTBR()}") + } + .map { it -> PumpEnactResult(injector).success(true).enacted(true).duration(durationInMinutes).percent(percent).isPercent(true).isTempCancel(false) } + .onErrorReturnItem(PumpEnactResult(injector).success(false).enacted(false) + .comment("Internal error")) + .blockingGet() + }else{ + aapsLogger.info(LTag.PUMP,"setTempBasalPercent - normal basal is not active") + return PumpEnactResult(injector).success(false).enacted(false) + } + } + + override fun setExtendedBolus(insulin: Double, durationInMinutes: Int): PumpEnactResult { + aapsLogger.info(LTag.PUMP,"setExtendedBolus - insulin: $insulin, durationInMinutes: $durationInMinutes") + + return patchmanager.startQuickBolus(0f, insulin.toFloat(), BolusExDuration.ofRaw(durationInMinutes)) + .doOnSuccess { + mLastDataTime = System.currentTimeMillis() + pumpSync.syncExtendedBolusWithPumpId( + timestamp = dateUtil.now(), + amount = insulin, + duration = T.mins(durationInMinutes.toLong()).msecs(), + isEmulatingTB = false, + pumpId = dateUtil.now(), + pumpType = PumpType.EOFLOW_EOPATCH2, + pumpSerial = serialNumber() + ) + } + .map { it -> PumpEnactResult(injector).success(true).enacted(true)} + .onErrorReturnItem(PumpEnactResult(injector).success(false).enacted(false).bolusDelivered(0.0) + .comment(rh.gs(info.nightscout.androidaps.core.R.string.error))) + .blockingGet() + } + + override fun cancelTempBasal(enforceNew: Boolean): PumpEnactResult { + val tbrCurrent = readTBR() + + if (tbrCurrent == null ) { + aapsLogger.debug(LTag.PUMP,"cancelTempBasal - TBR already false.") + return PumpEnactResult(injector).success(true).enacted(false) + } + + if (!patchmanager.patchState.isTempBasalActive) { + return if (pumpSync.expectedPumpState().temporaryBasal != null) { + PumpEnactResult(injector).success(true).enacted(true).isTempCancel(true) + }else + PumpEnactResult(injector).success(true).isTempCancel(true) + } + + return patchmanager.stopTempBasal() + .doOnSuccess { + mLastDataTime = System.currentTimeMillis() + aapsLogger.debug(LTag.PUMP,"cancelTempBasal - $it") + pumpSync.syncStopTemporaryBasalWithPumpId( + timestamp = dateUtil.now(), + endPumpId = dateUtil.now(), + pumpType = PumpType.EOFLOW_EOPATCH2, + pumpSerial = serialNumber() + ) + } + .doOnError{ + aapsLogger.error(LTag.PUMP,"cancelTempBasal() - $it") + } + .map { it -> PumpEnactResult(injector).success(true).enacted(true).isTempCancel(true)} + .onErrorReturnItem(PumpEnactResult(injector).success(false).enacted(false) + .comment(rh.gs(info.nightscout.androidaps.core.R.string.error))) + .blockingGet() + } + + override fun cancelExtendedBolus(): PumpEnactResult { + if(patchmanager.patchState.isExtBolusActive){ + return patchmanager.stopExtBolus() + .doOnSuccess { + aapsLogger.debug(LTag.PUMP,"cancelExtendedBolus - success") + mLastDataTime = System.currentTimeMillis() + pumpSync.syncStopExtendedBolusWithPumpId( + timestamp = dateUtil.now(), + endPumpId = dateUtil.now(), + pumpType = PumpType.EOFLOW_EOPATCH2, + pumpSerial = serialNumber() + ) + } + .map { it -> PumpEnactResult(injector).success(true).enacted(true).isTempCancel(true)} + .onErrorReturnItem(PumpEnactResult(injector).success(false).enacted(false) + .comment(rh.gs(info.nightscout.androidaps.core.R.string.error))) + .blockingGet() + }else{ + aapsLogger.debug(LTag.PUMP,"cancelExtendedBolus - nothing stops") + return if (pumpSync.expectedPumpState().extendedBolus != null) { + pumpSync.syncStopExtendedBolusWithPumpId( + timestamp = dateUtil.now(), + endPumpId = dateUtil.now(), + pumpType = PumpType.EOFLOW_EOPATCH2, + pumpSerial = serialNumber() + ) + PumpEnactResult(injector).success(true).enacted(true).isTempCancel(true) + }else + PumpEnactResult(injector) + } + } + + override fun getJSONStatus(profile: Profile, profileName: String, version: String): JSONObject { + return JSONObject() + } + + override fun manufacturer(): ManufacturerType { + return ManufacturerType.Eoflow + } + + override fun model(): PumpType { + return PumpType.EOFLOW_EOPATCH2 + } + + override fun serialNumber(): String { + return patchmanager.patchConfig.patchSerialNumber + } + + override val pumpDescription: PumpDescription + get() = mPumpDescription + + override fun shortStatus(veryShort: Boolean): String { + if(patchmanager.isActivated) { + var ret = "" + val activeTemp = pumpSync.expectedPumpState().temporaryBasal + if (activeTemp != null) + ret += "Temp: ${activeTemp.rate} U/hr" + + val activeExtendedBolus = pumpSync.expectedPumpState().extendedBolus + if (activeExtendedBolus != null) + ret += "Extended: ${activeExtendedBolus.amount} U\n" + + val reservoirStr = patchmanager.patchState.remainedInsulin.let { + when { + it > 50f -> "50+ U" + it < 1f -> "0 U" + else -> "${it.roundToInt()} U" + } + } + + ret += "Reservoir: $reservoirStr" + ret += "Batt: ${patchmanager.patchState.batteryLevel()}" + return ret + }else{ + return "EOPatch is not enabled." + } + } + + override val isFakingTempsByExtendedBoluses: Boolean = false + + override fun loadTDDs(): PumpEnactResult { + return PumpEnactResult(injector) + } + + override fun canHandleDST(): Boolean { + return false + } + + override fun getCustomActions(): List? { + return null + } + + override fun executeCustomAction(customActionType: CustomActionType) { + + } + + override fun executeCustomCommand(customCommand: CustomCommand): PumpEnactResult? { + return null + } + + + override fun timezoneOrDSTChanged(timeChangeType: TimeChangeType) { + + } + + private fun readTBR(): PumpSync.PumpState.TemporaryBasal? { + return pumpSync.expectedPumpState().temporaryBasal + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/FloatFormatters.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/FloatFormatters.kt new file mode 100644 index 0000000000..badd141124 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/FloatFormatters.kt @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.plugins.pump.eopatch + +import java.util.* +import java.util.function.Function + +object FloatFormatters { + val INSULIN = Function{ value -> String.format(Locale.US, CommonUtils.insulinFormat(), value.toFloat()) } + val FAT = Function{ value -> String.format(Locale.US, "%.1f", value.toFloat()) } + val DURATION = Function{ value -> String.format(Locale.US, "%.1f", value.toFloat()) } + + fun insulin(value: Float): String { + return INSULIN.apply(value) + } + + fun insulin(value: Float, suffix: String?): String { + return if (CommonUtils.isStringEmpty(suffix)) { + INSULIN.apply(value) + } else { + INSULIN.apply(value).toString() +" "+ suffix!! + } + } + + fun fat(value: Float): String { + return FAT.apply(value) + } + + fun fat(value: Float, suffix: String?): String { + return if (CommonUtils.isStringEmpty(suffix)) { + FAT.apply(value) + } else { + FAT.apply(value).toString() + suffix!! + } + } + + fun duration(value: Float): String { + return DURATION.apply(value) + } + + fun duration(value: Float, suffix: String?): String { + return if (CommonUtils.isStringEmpty(suffix)) { + DURATION.apply(value) + } else { + DURATION.apply(value).toString() +" " + suffix!! + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/GsonHelper.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/GsonHelper.kt new file mode 100644 index 0000000000..9586dd5b85 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/GsonHelper.kt @@ -0,0 +1,20 @@ +package info.nightscout.androidaps.plugins.pump.eopatch + +import com.google.gson.Gson +import com.google.gson.GsonBuilder + +object GsonHelper { + private var defaultGson: Gson? = null + + init { + defaultGson = GsonBuilder().serializeSpecialFloatingPointValues().create() + } + + fun sharedGson(): Gson { + if (defaultGson == null) { + throw RuntimeException("Not configured gson") + } + + return defaultGson!! + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/OsAlarmReceiver.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/OsAlarmReceiver.kt new file mode 100644 index 0000000000..77ca7204ea --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/OsAlarmReceiver.kt @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.eopatch + +import android.content.Context +import android.content.Intent +import dagger.android.DaggerBroadcastReceiver +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode.Companion.fromIntent +import info.nightscout.androidaps.plugins.pump.eopatch.event.EventEoPatchAlarm + +class OsAlarmReceiver : DaggerBroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + fromIntent(intent)?.let { alarmCode -> + EoPatchRxBus.publish(EventEoPatchAlarm(HashSet().apply { add(alarmCode) })) + } + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/OsAlarmService.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/OsAlarmService.java new file mode 100644 index 0000000000..141031e8cf --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/OsAlarmService.java @@ -0,0 +1,81 @@ +package info.nightscout.androidaps.plugins.pump.eopatch; + +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; + +import java.util.Objects; + +import io.reactivex.disposables.CompositeDisposable; + +public class OsAlarmService extends Service { + + public static final int FOREGROUND_NOTIFICATION_ID = 34534554; + + private CompositeDisposable compositeDisposable; + + private boolean foreground = false; + + @Override + public void onCreate() { + super.onCreate(); + + compositeDisposable = new CompositeDisposable(); + + startForeground(); + + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + startForeground(); + + String action = null; + + if (action == null) { + return Service.START_NOT_STICKY; + } + + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + ((NotificationManager) Objects.requireNonNull(getSystemService(Context.NOTIFICATION_SERVICE))).cancel(FOREGROUND_NOTIFICATION_ID); + + compositeDisposable.dispose(); + } + + public synchronized void startForeground() { + if (!foreground) { +//// CommonUtils.dispose(mNotificationDisposable); +// Notification builder = getNotification(this); +// startForeground(FOREGROUND_NOTIFICATION_ID, builder); +// startExerciseOrSleepMode(this); + foreground = true; + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + public static void start(Context context) { + Intent intent = new Intent(context, OsAlarmService.class); + +// context.startForegroundService(intent); + } + + public static void notifyNotification(Context context, boolean isNetworkAvailable) { + notifyNotification(context); + } + + public static void notifyNotification(Context context) { +// Notification builder = getNotification(context); +// ((NotificationManager) Objects.requireNonNull(context.getSystemService(Context.NOTIFICATION_SERVICE))).notify(FOREGROUND_NOTIFICATION_ID, builder); + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/RxAction.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/RxAction.kt new file mode 100644 index 0000000000..052f23df8b --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/RxAction.kt @@ -0,0 +1,113 @@ +package info.nightscout.androidaps.plugins.pump.eopatch + +import io.reactivex.* +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import org.reactivestreams.Subscription +import timber.log.Timber +import java.util.concurrent.TimeUnit + +enum class RxVoid { + INSTANCE +} + +class SilentObserver : MaybeObserver, SingleObserver, Observer, FlowableSubscriber { + override fun onSubscribe(d: Disposable) {} + override fun onSuccess(t: T) {} + override fun onError(e: Throwable) = Timber.d(e, "SilentObserver.onError() ignore") + override fun onComplete() {} + override fun onNext(t: T) {} + override fun onSubscribe(s: Subscription) {} +} + +object RxAction { + private fun msleep(millis: Long) { + if (millis <= 0) + return + try { + Thread.sleep(millis) + } catch (e: InterruptedException) { + } + + } + + private fun delay(delayMs: Long): Single<*> { + return if (delayMs <= 0) { + Single.just(1) + } else Single.timer(delayMs, TimeUnit.MILLISECONDS) + + } + + fun single(action: Runnable, delayMs: Long, scheduler: Scheduler): Single<*> { + return delay(delayMs) + .observeOn(scheduler) + .flatMap { o -> + Single.fromCallable { + action.run() + RxVoid.INSTANCE + } + } + } + + fun safeSingle(action: Runnable, delayMs: Long, scheduler: Scheduler): Single<*> { + return single(action, delayMs, scheduler) + } + + @JvmOverloads + fun runOnComputationThread(action: Runnable, delayMs: Long = 0) { + single(action, delayMs, Schedulers.computation()).subscribe(SilentObserver()) + } + + @JvmOverloads + fun runOnIoThread(action: Runnable, delayMs: Long = 0) { + single(action, delayMs, Schedulers.io()).subscribe(SilentObserver()) + } + + @JvmOverloads + fun runOnNewThread(action: Runnable, delayMs: Long = 0) { + single(action, delayMs, Schedulers.newThread()).subscribe(SilentObserver()) + } + + @JvmOverloads + fun runOnMainThread(action: Runnable, delayMs: Long = 0) { + single(action, delayMs, AndroidSchedulers.mainThread()).subscribe(SilentObserver()) + } + + @JvmOverloads + fun safeRunOnComputationThread(action: Runnable, delayMs: Long = 0) { + safeSingle(action, delayMs, Schedulers.computation()).subscribe(SilentObserver()) + } + + @JvmOverloads + fun safeRunOnIoThread(action: Runnable, delayMs: Long = 0) { + safeSingle(action, delayMs, Schedulers.io()).subscribe(SilentObserver()) + } + + @JvmOverloads + fun safeRunOnNewThread(action: Runnable, delayMs: Long = 0) { + safeSingle(action, delayMs, Schedulers.newThread()).subscribe(SilentObserver()) + } + + @JvmOverloads + fun safeRunOnMainThread(action: Runnable, delayMs: Long = 0) { + safeSingle(action, delayMs, AndroidSchedulers.mainThread()).subscribe(SilentObserver()) + } + + + fun singleOnMainThread(action: Runnable, delayMs: Long): Single<*> { + return single(action, delayMs, AndroidSchedulers.mainThread()) + } + + fun singleOnComputationThread(action: Runnable, delayMs: Long): Single<*> { + return single(action, delayMs, Schedulers.computation()) + } + + fun singleOnIoThread(action: Runnable, delayMs: Long): Single<*> { + return single(action, delayMs, Schedulers.io()) + } + + fun singleOnNewThread(action: Runnable, delayMs: Long): Single<*> { + return single(action, delayMs, Schedulers.newThread()) + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmCode.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmCode.kt new file mode 100644 index 0000000000..cb410327bd --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmCode.kt @@ -0,0 +1,129 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.alarm + +import android.content.Intent +import android.net.Uri +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.code.AlarmCategory +import java.util.* +import java.util.function.Function +import java.util.stream.Collectors +import java.util.stream.Stream + +enum class AlarmCode(defaultName: String, messageResId: Int) { + A002("Empty reservoir", R.string.string_a002), + A003("Patch expired", R.string.string_a003), + A004("Occlusion", R.string.string_a004), + A005("Power on self test failure", R.string.string_a005), + A007("Inappropriate temperature", R.string.string_a007), + A016("Needle insertion Error", R.string.string_a016), + A018("Patch battery Error", R.string.string_a018), + A019("Patch battery Error", R.string.string_a019), + A020("Patch activation Error", R.string.string_a020), + A022("Patch Error", R.string.string_a022), + A023("Patch Error", R.string.string_a023), + A034("Patch Error", R.string.string_a034), + A041("Patch Error", R.string.string_a041), + A042("Patch Error", R.string.string_a042), + A043("Patch Error", R.string.string_a043), + A044("Patch Error", R.string.string_a044), + A106("Patch Error", R.string.string_a106), + A107("Patch Error", R.string.string_a107), + A108("Patch Error", R.string.string_a108), + A116("Patch Error", R.string.string_a116), + A117("Patch Error", R.string.string_a117), + A118("Patch Error", R.string.string_a118), + B001("End of insulin suspend", R.string.string_b001), + B003("Low reservoir", R.string.string_b003), + B005("Patch operating life expired", R.string.string_b005), + B006("Patch will expire soon", R.string.string_b006), + B012("Incomplete Patch activation", R.string.string_b012), + B018("Patch battery low", R.string.string_b018); + + val type: Char + val code: Int + val resId: Int + val alarmCategory: AlarmCategory + get() = when (type) { + TYPE_ALARM -> AlarmCategory.ALARM + TYPE_ALERT -> AlarmCategory.ALERT + else -> AlarmCategory.NONE + } + val aeCode: Int + get() { + when (type) { + TYPE_ALARM -> return code + 100 + TYPE_ALERT -> return code + } + return -1 + } + + val osAlarmId: Int + get() = (when (type) { + TYPE_ALARM -> 10000 + TYPE_ALERT -> 20000 + else -> 0 + } + code ) * 1000 + 1 + + val isPatchOccurrenceAlert: Boolean + get() = this == B003 || this == B005 || this == B006 || this == B018 + + val isPatchOccurrenceAlarm: Boolean + get() = this == A002 || this == A003 || this == A004 || this == A018 || this == A019 || this == A022 + || this == A023 || this == A034 || this == A041 || this == A042 || this == A043 || this == A044 || this == A106 + || this == A107 || this == A108 || this == A116 || this == A117 || this == A118 + + init { + type = name[0] + this.code = name.substring(1).toInt() + resId = messageResId + } + + companion object { + const val TYPE_ALARM = 'A' + const val TYPE_ALERT = 'B' + + private const val SCHEME = "alarmkey" + private const val ALARM_KEY_PATH = "alarmkey" + private const val QUERY_CODE = "alarmcode" + + private val NAME_MAP = Stream.of(*values()) + .collect(Collectors.toMap({ obj: AlarmCode -> obj.name }, Function.identity())) + + fun fromStringToCode(name: String): AlarmCode? { + return NAME_MAP[name] + } + + fun findByPatchAeCode(aeCode: Int): AlarmCode? { + return if (aeCode > 100) { + fromStringToCode(String.format(Locale.US, "A%03d", aeCode - 100)) + } else fromStringToCode(String.format(Locale.US, "B%03d", aeCode)) + } + + @JvmStatic + fun getUri(alarmCode: AlarmCode): Uri { + return Uri.Builder() + .scheme(SCHEME) + .authority("com.eoflow.eomapp") + .path(ALARM_KEY_PATH) + .appendQueryParameter(QUERY_CODE, alarmCode.name) + .build(); + } + + @JvmStatic + fun getAlarmCode(uri: Uri): AlarmCode? { + if (SCHEME == uri.scheme && ALARM_KEY_PATH == uri.lastPathSegment) { + val code = uri.getQueryParameter(QUERY_CODE) + if (code.isNullOrBlank()) { + return null + } + return fromStringToCode(code) + } + return null + } + + @JvmStatic + fun fromIntent(intent: Intent): AlarmCode? { + return intent.data?.let { getAlarmCode(it) } + } + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmManager.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmManager.kt new file mode 100644 index 0000000000..3929625d1c --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmManager.kt @@ -0,0 +1,171 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.alarm + +import android.content.Context +import android.content.Intent +import info.nightscout.androidaps.interfaces.ActivePlugin +import info.nightscout.androidaps.interfaces.CommandQueue +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.shared.logging.LTag +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.general.overview.events.EventNewNotification +import info.nightscout.androidaps.plugins.general.overview.notifications.Notification +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode.* +import info.nightscout.androidaps.plugins.pump.eopatch.EONotification +import info.nightscout.androidaps.plugins.pump.eopatch.EoPatchRxBus +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPatchManager +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager +import info.nightscout.androidaps.plugins.pump.eopatch.code.AlarmCategory +import info.nightscout.androidaps.plugins.pump.eopatch.event.EventEoPatchAlarm +import info.nightscout.androidaps.plugins.pump.eopatch.ui.AlarmHelperActivity +import info.nightscout.androidaps.plugins.pump.eopatch.vo.Alarms +import info.nightscout.androidaps.utils.FabricPrivacy +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.shared.sharedPreferences.SP +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +interface IAlarmManager { + fun init() + fun restartAll() +} + +@Singleton +class AlarmManager @Inject constructor() : IAlarmManager { + @Inject lateinit var patchManager: IPatchManager + @Inject lateinit var activePlugin: ActivePlugin + @Inject lateinit var commandQueue: CommandQueue + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var resourceHelper: ResourceHelper + @Inject lateinit var rxBus: RxBus + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var sp: SP + @Inject lateinit var context: Context + + @Inject lateinit var pm: IPreferenceManager + @Inject lateinit var mAlarmRegistry: IAlarmRegistry + + private lateinit var mAlarmProcess: AlarmProcess + + private var compositeDisposable: CompositeDisposable = CompositeDisposable() + + @Inject + fun onInit() { + mAlarmProcess = AlarmProcess(patchManager, rxBus) + } + + override fun init(){ + EoPatchRxBus.listen(EventEoPatchAlarm::class.java) + .map { it -> it.alarmCodes } + .doOnNext { aapsLogger.info(LTag.PUMP,"EventEoPatchAlarm Received") } + .concatMap { + Observable.fromArray(it) + .observeOn(Schedulers.io()) + .subscribeOn(AndroidSchedulers.mainThread()) + .doOnNext { alarmCodes -> + alarmCodes.forEach { + aapsLogger.info(LTag.PUMP,"alarmCode: ${it.name}") + val valid = isValid(it) + if (valid) { + if (it.alarmCategory == AlarmCategory.ALARM || it == B012) { + showAlarmDialog(it) + } else { + showNotification(it) + } + + updateState(it, AlarmState.FIRED) + }else{ + updateState(it, AlarmState.HANDLE) + } + } + } + + } + .subscribe({}, { throwable: Throwable -> fabricPrivacy.logException(throwable) }) + } + + override fun restartAll() { + val now = System.currentTimeMillis() + val occuredAlarm = pm.getAlarms().occured.clone() as HashMap + val registeredAlarm = pm.getAlarms().registered.clone() as HashMap + compositeDisposable.clear() + if(occuredAlarm.isNotEmpty()){ + EoPatchRxBus.publish(EventEoPatchAlarm(occuredAlarm.keys)) + } + + if(registeredAlarm.isNotEmpty()){ + registeredAlarm.forEach { raEntry -> + compositeDisposable.add( + mAlarmRegistry.add(raEntry.key, Math.max(OS_REGISTER_GAP, raEntry.value.triggerTimeMilli - now)) + .subscribe() + ) + } + } + } + + private fun isValid(code: AlarmCode): Boolean{ + return when(code){ + A005, A016, A020, B012 -> { + aapsLogger.info(LTag.PUMP,"Is ${code} valid? ${pm.getPatchConfig().hasMacAddress() && pm.getPatchConfig().lifecycleEvent.isSubStepRunning}") + pm.getPatchConfig().hasMacAddress() && pm.getPatchConfig().lifecycleEvent.isSubStepRunning + } + else -> { + aapsLogger.info(LTag.PUMP,"Is ${code} valid? ${pm.getPatchConfig().isActivated}") + pm.getPatchConfig().isActivated + } + } + } + + private fun showAlarmDialog(alarmCode: AlarmCode){ + val i = Intent(context, AlarmHelperActivity::class.java) + i.putExtra("soundid", R.raw.error) + i.putExtra("code", alarmCode.name) + i.putExtra("status", resourceHelper.gs(alarmCode.resId)) + i.putExtra("title", resourceHelper.gs(R.string.string_alarm)) + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(i) + } + + private fun showNotification(alarmCode: AlarmCode, timeOffset: Long = 0L){ + var occurredTimestamp: Long = pm.getPatchConfig().patchWakeupTimestamp + TimeUnit.SECONDS.toMillis(timeOffset) + val notification = EONotification(Notification.EOELOW_PATCH_ALERTS + (alarmCode.aeCode + 10000), resourceHelper.gs(alarmCode.resId), Notification.URGENT) + + notification.action(R.string.confirm) { + Single.just(isValid(alarmCode)) + .flatMap { isValid -> + return@flatMap if(isValid) mAlarmProcess.doAction(context, alarmCode) + else Single.just(IAlarmProcess.ALARM_HANDLED) + } + .subscribe { ret -> + if(ret == IAlarmProcess.ALARM_HANDLED){ + updateState(alarmCode, AlarmState.HANDLE) + }else{ + rxBus.send(EventNewNotification(notification)) + } + } + } + notification.soundId = R.raw.error + notification.date = occurredTimestamp + rxBus.send(EventNewNotification(notification)) + } + + private fun updateState(alarmCode: AlarmCode, state: AlarmState){ + when(state){ + AlarmState.REGISTER -> pm.getAlarms().register(alarmCode, 0) + AlarmState.FIRED -> pm.getAlarms().occured(alarmCode) + AlarmState.HANDLE -> pm.getAlarms().handle(alarmCode) + } + pm.flushAlarms() + } + + companion object { + + private const val OS_REGISTER_GAP = 3 * 1000L + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmProcess.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmProcess.kt new file mode 100644 index 0000000000..7f440d6527 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmProcess.kt @@ -0,0 +1,135 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.alarm + +import android.content.Context +import android.content.DialogInterface +import info.nightscout.androidaps.plugins.pump.eopatch.ui.EopatchActivity.Companion.createIntentForCheckConnection +import info.nightscout.androidaps.plugins.pump.eopatch.ui.EopatchActivity.Companion.createIntentForDiscarded +import info.nightscout.androidaps.plugins.pump.eopatch.ui.EopatchActivity.Companion.createIntentForChangePatch +import info.nightscout.androidaps.plugins.pump.eopatch.ui.EopatchActivity.Companion.createIntentForCanularInsertionError +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPatchManager +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode.* +import android.content.Intent +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.ui.EopatchActivity +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BaseResponse +import info.nightscout.androidaps.plugins.pump.eopatch.code.DeactivationStatus +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.TemperatureResponse +import info.nightscout.androidaps.plugins.pump.eopatch.event.EventDialog +import info.nightscout.androidaps.plugins.pump.eopatch.event.EventProgressDialog +import info.nightscout.androidaps.plugins.pump.eopatch.extension.takeOne +import info.nightscout.androidaps.plugins.pump.eopatch.ui.dialogs.CommonDialog +import io.reactivex.Single +import java.lang.Exception +import java.util.concurrent.Callable + +interface IAlarmProcess { + fun doAction(context: Context, code: AlarmCode): Single + + companion object { + const val ALARM_UNHANDLED = 0 + const val ALARM_PAUSE = 1 + const val ALARM_HANDLED = 2 + } +} + +class AlarmProcess(val patchManager: IPatchManager, val rxBus: RxBus) : IAlarmProcess { + override fun doAction(context: Context, code: AlarmCode): Single { + return when (code) { + B001 -> resumeBasalAction(context) + A002, A003, A004, A005, A018, A019, + A020, A022, A023, A034, A041, A042, + A043, A044, A106, A107, A108, A116, + A117, A118 -> patchDeactivationAction(context, true) + A007 -> inappropriateTemperatureAction(context) + A016 -> needleInsertionErrorAction(context) + B003, B018 -> Single.just(IAlarmProcess.ALARM_HANDLED) + B005, B006 -> Single.just(IAlarmProcess.ALARM_HANDLED) + B012 -> Single.just(IAlarmProcess.ALARM_HANDLED) + else -> Single.just(IAlarmProcess.ALARM_HANDLED) + } + } + + private fun startActivityWithSingleTop(context: Context, intent: Intent) { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + context.startActivity(intent) + } + + private fun showCommunicationFailedDialog(onConfirmed: Runnable) { + var dialog = CommonDialog().apply { + title = R.string.patch_communication_failed + message = R.string.patch_communication_check_helper_1 + positiveBtn = R.string.string_communication_check + positiveListener = DialogInterface.OnClickListener { dialog, which -> + onConfirmed.run() + dismiss() + } + } + + rxBus.send(EventDialog(dialog, true)) + } + + private fun actionWithPatchCheckConnection(context: Context, action: Callable>): Single { + return if (patchManager.patchConnectionState.isConnected) { + try { + action.call() + } catch (e: Exception) { + Single.just(IAlarmProcess.ALARM_PAUSE) + } + } else { + Single.fromCallable { + showCommunicationFailedDialog { + startActivityWithSingleTop(context, + createIntentForCheckConnection(context, true, true)) + } + IAlarmProcess.ALARM_PAUSE + } + } + } + + private fun resumeBasalAction(context: Context): Single { + return actionWithPatchCheckConnection(context) { + patchManager.resumeBasal() + .map { obj: BaseResponse -> obj.isSuccess } + .flatMap { Single.just(it.takeOne(IAlarmProcess.ALARM_HANDLED, IAlarmProcess.ALARM_UNHANDLED)) } + } + } + + private fun patchDeactivationAction(context: Context, goHome: Boolean): Single { + return actionWithPatchCheckConnection(context) { + rxBus.send(EventProgressDialog(true, R.string.string_in_progress)) + patchManager.deactivate(6000, true) + .doFinally { + rxBus.send(EventProgressDialog(false, R.string.string_in_progress)) + startActivityWithSingleTop(context, createIntentForDiscarded(context, goHome)) + } + .flatMap { ok: DeactivationStatus? -> Single.just(IAlarmProcess.ALARM_HANDLED) } + } + } + + private fun changPatchAction(context: Context): Single { + return Single.fromCallable { + startActivityWithSingleTop(context, createIntentForChangePatch(context)) + IAlarmProcess.ALARM_HANDLED + } + } + + private fun needleInsertionErrorAction(context: Context): Single { + return Single.fromCallable { + startActivityWithSingleTop(context, createIntentForCanularInsertionError(context)) + IAlarmProcess.ALARM_HANDLED + } + } + + private fun inappropriateTemperatureAction(context: Context): Single { + return actionWithPatchCheckConnection(context) { + patchManager.temperature + .map(TemperatureResponse::getTemperature) + .map { temp -> (temp >= EopatchActivity.NORMAL_TEMPERATURE_MIN && temp <= EopatchActivity.NORMAL_TEMPERATURE_MAX) } + .filter{ok -> ok} + .flatMap { patchManager.resumeBasal().map { it.isSuccess.takeOne(IAlarmProcess.ALARM_HANDLED, IAlarmProcess.ALARM_UNHANDLED) }.toMaybe() } + .defaultIfEmpty(IAlarmProcess.ALARM_UNHANDLED) + .toSingle() + } + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmRegistry.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmRegistry.kt new file mode 100644 index 0000000000..c5e985087d --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmRegistry.kt @@ -0,0 +1,164 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.alarm + +import android.app.AlarmManager +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode.Companion.getUri +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.pump.eopatch.event.EventEoPatchAlarm +import android.app.PendingIntent +import android.app.AlarmManager.AlarmClockInfo +import android.content.Context +import android.content.Intent +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.androidaps.plugins.general.overview.events.EventDismissNotification +import info.nightscout.androidaps.plugins.general.overview.notifications.Notification +import info.nightscout.androidaps.plugins.pump.eopatch.EoPatchRxBus +import info.nightscout.androidaps.plugins.pump.eopatch.OsAlarmReceiver +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchLifecycle +import info.nightscout.androidaps.plugins.pump.eopatch.core.code.PatchAeCode +import info.nightscout.androidaps.plugins.pump.eopatch.extension.observeOnMainThread +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +interface IAlarmRegistry { + fun add(alarmCode: AlarmCode, triggerAfter: Long, isFirst: Boolean = false): Maybe + fun add(patchAeCodes: Set) + fun remove(alarmKey: AlarmCode): Maybe +} + +@Singleton +class AlarmRegistry @Inject constructor() : IAlarmRegistry { + @Inject lateinit var mContext: Context + @Inject lateinit var pm: IPreferenceManager + @Inject lateinit var rxBus: RxBus + @Inject lateinit var aapsLogger: AAPSLogger + + private lateinit var mOsAlarmManager: AlarmManager + private var mDisposable: Disposable? = null + private var compositeDisposable: CompositeDisposable = CompositeDisposable() + + @Inject fun onInit() { + mOsAlarmManager = mContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager + mDisposable = pm.observePatchLifeCycle() + .observeOnMainThread() + .subscribe { + when(it){ + PatchLifecycle.REMOVE_NEEDLE_CAP -> { + val triggerAfter = pm.getPatchConfig().patchWakeupTimestamp + TimeUnit.HOURS.toMillis(1) - System.currentTimeMillis() + compositeDisposable.add(add(AlarmCode.A020, triggerAfter).subscribe()) + } + PatchLifecycle.ACTIVATED -> { + + } + PatchLifecycle.SHUTDOWN -> { + val sources = ArrayList>() + sources.add(Maybe.just(true)) + pm.getAlarms().occured.let{ + if(it.isNotEmpty()){ + it.keys.forEach { + sources.add( + Maybe.just(it) + .observeOnMainThread() + .doOnSuccess { rxBus.send(EventDismissNotification(Notification.EOELOW_PATCH_ALERTS + (it.aeCode + 10000))) } + ) + } + } + } + pm.getAlarms().registered.let{ + if(it.isNotEmpty()){ + it.keys.forEach { + sources.add(remove(it)) + } + } + } + Maybe.concat(sources) + .subscribe { + pm.getAlarms().clear() + pm.flushAlarms() + } + } + + else -> Unit + } + } + } + + override fun add(alarmCode: AlarmCode, triggerAfter: Long, isFirst: Boolean): Maybe { + if(pm.getAlarms().occured.containsKey(alarmCode)){ + return Maybe.just(alarmCode) + }else { + val triggerTimeMilli = System.currentTimeMillis() + triggerAfter + pm.getAlarms().register(alarmCode, triggerAfter) + pm.flushAlarms() + if (triggerAfter <= 0L) { + EoPatchRxBus.publish(EventEoPatchAlarm(HashSet().apply { add(alarmCode) }, isFirst)) + return Maybe.just(alarmCode) + } + return registerOsAlarm(alarmCode, triggerTimeMilli) + } + } + + override fun add(patchAeCodes: Set) { + compositeDisposable.add( + Observable.fromIterable(patchAeCodes) + .filter{patchAeCodeItem -> AlarmCode.Companion.findByPatchAeCode(patchAeCodeItem.getAeValue()) != null} + .observeOn(AndroidSchedulers.mainThread()) + .filter { patchAeCodes -> AlarmCode.findByPatchAeCode(patchAeCodes.getAeValue()) != null } + .flatMapMaybe{aeCodeResponse -> add(AlarmCode.findByPatchAeCode(aeCodeResponse.getAeValue())!!,0L, true)} + .subscribe() + ) + } + + private fun registerOsAlarm(alarmCode: AlarmCode, triggerTime: Long): Maybe { + return Maybe.fromCallable { + cancelOsAlarmInternal(alarmCode) + val pendingIntent = createPendingIntent(alarmCode, 0) + val now = System.currentTimeMillis() + mOsAlarmManager.setAlarmClock(AlarmClockInfo(triggerTime, pendingIntent), pendingIntent) + alarmCode + } + } + + override fun remove(alarmCode: AlarmCode): Maybe { + if(pm.getAlarms().registered.containsKey(alarmCode)) { + return cancelOsAlarms(alarmCode) + .doOnSuccess { + pm.getAlarms().unregister(alarmCode) + pm.flushAlarms() + } + .map { integer: Int? -> alarmCode } + }else{ + return Maybe.just(alarmCode) + } + } + + private fun cancelOsAlarms(vararg alarmCodes: AlarmCode): Maybe { + return Observable.fromArray(*alarmCodes) + .map(this::cancelOsAlarmInternal) + .reduce(Integer::sum) + } + + private fun cancelOsAlarmInternal(alarmCode: AlarmCode): Int { + val old = createPendingIntent(alarmCode, PendingIntent.FLAG_NO_CREATE) + return if (old != null) { + mOsAlarmManager.cancel(old) + old.cancel() + aapsLogger.debug("[${alarmCode}] OS Alarm canceled.") + 1 + } else { + aapsLogger.debug("[${alarmCode}] OS Alarm not canceled, not registered.") + 0 + } + } + + private fun createPendingIntent(alarmCode: AlarmCode, flag: Int): PendingIntent { + val intent = Intent(mContext, OsAlarmReceiver::class.java).setData(getUri(alarmCode)) + return PendingIntent.getBroadcast(mContext, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmState.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmState.java new file mode 100644 index 0000000000..4029b27300 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmState.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.alarm; + +public enum AlarmState { + REGISTER, + FIRED, + HANDLE +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/bindingadapters/OnSafeClickListener.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/bindingadapters/OnSafeClickListener.kt new file mode 100644 index 0000000000..236f5a5d65 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/bindingadapters/OnSafeClickListener.kt @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.bindingadapters + +import android.view.View +import java.util.concurrent.atomic.AtomicBoolean + +class OnSafeClickListener( + private val clickListener: View.OnClickListener, + private val intervalMs: Long = MIN_CLICK_INTERVAL +) : View.OnClickListener { + private var canClick = AtomicBoolean(true) + + override fun onClick(v: View?) { + if (canClick.getAndSet(false)) { + v?.run { + postDelayed({ + canClick.set(true) + }, intervalMs) + clickListener.onClick(v) + } + } + } + companion object { + // 중복 클릭 방지 시간 설정 + private val MIN_CLICK_INTERVAL: Long = 1000 + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/bindingadapters/ViewBindingAdapter.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/bindingadapters/ViewBindingAdapter.kt new file mode 100644 index 0000000000..bae87baf98 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/bindingadapters/ViewBindingAdapter.kt @@ -0,0 +1,40 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.bindingadapters + +import android.view.View +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import androidx.databinding.BindingAdapter +import info.nightscout.androidaps.plugins.pump.eopatch.extension.check +import info.nightscout.androidaps.plugins.pump.eopatch.extension.setVisibleOrGone + +@BindingAdapter("android:visibility") +fun setVisibility(view: View, visible: Boolean) { + view.setVisibleOrGone(visible) +} + +@BindingAdapter("visibleOrGone") +fun setVisibleOrGone(view: View, visibleOrGone: Boolean) { + view.setVisibleOrGone(visibleOrGone) +} + +@BindingAdapter("onSafeClick") +fun View.setOnSafeClickListener(clickListener: View.OnClickListener?) { + clickListener?.also { + setOnClickListener(OnSafeClickListener(it)) + } ?: setOnClickListener(null) +} + +@BindingAdapter("textColor") +fun setTextColor(view: TextView, @ColorRes colorResId: Int) { + view.setTextColor(view.context.getColor(colorResId)) +} + +@BindingAdapter("android:text") +fun setText(view: TextView, @StringRes resId: Int?) { + val text = resId?.let { view.context.getString(it) } ?: "" + val oldText = view.text + if (text.check(oldText)) { + view.text = text + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/IPatchManager.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/IPatchManager.java new file mode 100644 index 0000000000..7a6ead66b7 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/IPatchManager.java @@ -0,0 +1,128 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble; + + +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.BleConnectionState; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.PatchSelfTestResult; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.ScanList; +import info.nightscout.androidaps.plugins.pump.eopatch.code.BolusExDuration; +import info.nightscout.androidaps.plugins.pump.eopatch.code.DeactivationStatus; +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchLifecycle; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BasalScheduleSetResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BaseResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BolusResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BolusStopResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.ComboBolusStopResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.PatchBooleanResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.TempBasalScheduleSetResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.TemperatureResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.BolusCurrent; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.NormalBasal; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchConfig; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchLifecycleEvent; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.TempBasal; +import io.reactivex.Observable; +import io.reactivex.Single; + +public interface IPatchManager { + + /* + @Deprecated + static IPatchManager shared() { + return BaseApplication.instance.getDataComponent().getPatchManager(); + } + */ + void init(); + + IPreferenceManager getPreferenceManager(); + + PatchConfig getPatchConfig(); + + boolean isActivated(); + + Single resumeBasal(); + + Observable observePatchLifeCycle(); + + Observable observePatchState(); + + BleConnectionState getPatchConnectionState(); + + void connect(); + + void disconnect(); + + PatchState getPatchState(); + + void updatePatchState(PatchState state); + + BolusCurrent getBolusCurrent(); + + Single deactivate(long timeout, boolean force); + + Observable observePatchConnectionState(); + + Observable observeBolusCurrent(); + + void setConnection(); + + Single stopNowBolus(); + + Single stopExtBolus(); + + Single stopComboBolus(); + + Single startQuickBolus(float nowDoseU, float exDoseU, + BolusExDuration exDuration); + + Single startCalculatorBolus(DetailedBolusInfo detailedBolusInfo); + + + Single infoReminderSet(boolean infoReminder); + + Single setLowReservoir(int doseUnit, int hours); + + Single updateConnection(); + + long getPatchExpiredTime(); + + Single startBasal(NormalBasal basal); + + void updatePatchLifeCycle(PatchLifecycleEvent event); + + Single startBond(String mac); + + Single getPatchInfo(long timeout); + + Single selfTest(long timeout); + + Observable startPriming(long timeout, long count); + + Single checkNeedleSensing(long timeout); + + Single patchActivation(long timeout); + + Single stopAeBeep(int aeCode); + + Single startTempBasal(TempBasal tempBasal); + + Single pauseBasal(float pauseDurationHour); + + Single scan(long timeout); + + Single stopTempBasal(); + + Single getTemperature(); + + void initBasalSchedule(); + + void addBolusToHistory(DetailedBolusInfo originalDetailedBolusInfo); + + void changeBuzzerSetting(); + + void changeReminderSetting(); + + void checkActivationProcess(); +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchManager.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchManager.java new file mode 100644 index 0000000000..d153642a93 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchManager.java @@ -0,0 +1,451 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble; + +import android.content.Context; +import android.content.Intent; + +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.events.EventCustomActionsChanged; +import info.nightscout.androidaps.events.EventPumpStatusChanged; +import info.nightscout.androidaps.events.EventRefreshOverview; +import info.nightscout.androidaps.interfaces.ActivePlugin; +import info.nightscout.androidaps.interfaces.CommandQueue; +import info.nightscout.androidaps.interfaces.ProfileFunction; +import info.nightscout.androidaps.interfaces.PumpSync; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.shared.logging.AAPSLogger; +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.bus.RxBus; +import info.nightscout.androidaps.plugins.pump.eopatch.CommonUtils; +import info.nightscout.androidaps.plugins.pump.eopatch.R; +import info.nightscout.androidaps.plugins.pump.eopatch.RxAction; +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.BleConnectionState; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.IPatchScanner; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.PatchScanner; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.PatchSelfTestResult; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.ScanList; +import info.nightscout.androidaps.plugins.pump.eopatch.code.BolusExDuration; +import info.nightscout.androidaps.plugins.pump.eopatch.code.DeactivationStatus; +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchLifecycle; +import com.polidea.rxandroidble2.RxBleClient; +import com.polidea.rxandroidble2.exceptions.BleException; +import com.polidea.rxandroidble2.internal.RxBleLog; + +import java.util.concurrent.TimeUnit; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BasalScheduleSetResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BaseResponse; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BolusResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BolusStopResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.ComboBolusStopResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.PatchBooleanResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.TempBasalScheduleSetResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.TemperatureResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.code.SettingKeys; +import info.nightscout.androidaps.plugins.pump.eopatch.event.EventPatchActivationNotComplete; +import info.nightscout.androidaps.plugins.pump.eopatch.ui.DialogHelperActivity; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.BolusCurrent; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.NormalBasal; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchConfig; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchLifecycleEvent; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.TempBasal; +import info.nightscout.androidaps.queue.commands.Command; +import info.nightscout.androidaps.utils.resources.ResourceHelper; +import info.nightscout.shared.sharedPreferences.SP; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.exceptions.OnErrorNotImplementedException; +import io.reactivex.exceptions.UndeliverableException; +import io.reactivex.plugins.RxJavaPlugins; +import io.reactivex.schedulers.Schedulers; + +@Singleton +public class PatchManager implements IPatchManager { + + @Inject PatchManagerImpl patchManager; + @Inject IPreferenceManager pm; + @Inject ProfileFunction profileFunction; + @Inject ActivePlugin activePlugin; + @Inject CommandQueue commandQueue; + @Inject AAPSLogger aapsLogger; + @Inject ResourceHelper resourceHelper; + @Inject RxBus rxBus; + @Inject Context context; + @Inject SP sp; + @Inject PumpSync pumpSync; + @Inject DateUtil dateUtil; + + private IPatchScanner patchScanner; + private CompositeDisposable mCompositeDisposable = new CompositeDisposable(); + private Disposable mConnectingDisposable = null; + + @Inject + public PatchManager() { + setupRxAndroidBle(); + } + + private void setupRxAndroidBle() { + RxJavaPlugins.setErrorHandler(throwable -> { + if (throwable instanceof UndeliverableException) { + if (throwable.getCause() instanceof BleException) { + return; + } + aapsLogger.error(LTag.PUMPBTCOMM, "rx UndeliverableException Error Handler"); + return; + } else if (throwable instanceof OnErrorNotImplementedException) { + aapsLogger.error(LTag.PUMPBTCOMM, "rx exception Error Handler"); + return; + } + throw new RuntimeException("Unexpected Throwable in RxJavaPlugins error handler", throwable); + }); + } + + @Inject + void onInit() { + patchScanner = new PatchScanner(context); + + mCompositeDisposable.add(observePatchConnectionState() + .subscribe(bleConnectionState -> { + switch (bleConnectionState) { + case DISCONNECTED: + rxBus.send(new EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED)); + rxBus.send(new EventRefreshOverview("Eopatch connection state: " + bleConnectionState.name(), true)); + rxBus.send(new EventCustomActionsChanged()); + stopObservingConnection(); + break; + + case CONNECTED: + rxBus.send(new EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED)); + rxBus.send(new EventRefreshOverview("Eopatch connection state: " + bleConnectionState.name(), true)); + rxBus.send(new EventCustomActionsChanged()); + stopObservingConnection(); + break; + + case CONNECTING: + mConnectingDisposable = Observable.interval(0, 1, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .takeUntil(n -> getPatchConnectionState().isConnected() || n > 10 * 60) + .subscribe(n -> rxBus.send(new EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTING, n.intValue()))); + break; + + default: + stopObservingConnection(); + } + }) + ); + mCompositeDisposable.add(rxBus + .toObservable(EventPatchActivationNotComplete.class) + .observeOn(Schedulers.io()) + .subscribeOn(AndroidSchedulers.mainThread()) + .subscribe(eventPatchActivationNotComplete -> { + Intent i = new Intent(context, DialogHelperActivity.class); + i.putExtra("title", resourceHelper.gs(R.string.patch_activate_reminder_title)); + i.putExtra("message", resourceHelper.gs(R.string.patch_activate_reminder_desc)); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); + }) + ); + + } + + @Override + public void init() { + initBasalSchedule(); + setConnection(); + } + + private void stopObservingConnection(){ + if(mConnectingDisposable != null) { + mConnectingDisposable.dispose(); + mConnectingDisposable = null; + } + } + + @Override + public IPreferenceManager getPreferenceManager() { + return pm; + } + + @Override + public PatchConfig getPatchConfig() { + return pm.getPatchConfig(); + } + + @Override + public Observable observePatchLifeCycle() { + return pm.observePatchLifeCycle(); + } + + @Override + public synchronized void updatePatchLifeCycle(PatchLifecycleEvent event) { + pm.updatePatchLifeCycle(event); + } + + @Override + public BleConnectionState getPatchConnectionState() { + return patchManager.getPatchConnectionState(); + } + + @Override + public Observable observePatchConnectionState() { + return patchManager.observePatchConnectionState(); + } + + @Override + public PatchState getPatchState() { + return pm.getPatchState(); + } + + @Override + public void updatePatchState(PatchState state) { + pm.getPatchState().update(state); + pm.flushPatchState(); + } + + @Override + public Observable observePatchState() { + return pm.observePatchState(); + } + + @Override + public long getPatchExpiredTime() { + return pm.getPatchConfig().getPatchExpiredTime(); + } + + @Override + public BolusCurrent getBolusCurrent() { + return pm.getBolusCurrent(); + } + + @Override + public Observable observeBolusCurrent() { + return pm.observeBolusCurrent(); + } + + + public void connect() { + // Nothing (Auto Connect mode) + } + + public void disconnect() { + // Nothing (Auto Connect mode) + } + + @Override + public void setConnection() { + if(pm.getPatchConfig().hasMacAddress()){ + patchManager.updateMacAddress(pm.getPatchConfig().getMacAddress(), false); + } + } + + public boolean isActivated() { + return pm.getPatchConfig().isActivated(); + } + + public Single startBond(String mac) { + return patchManager.startBond(mac); + } + + public Single getPatchInfo(long timeout) { + return patchManager.getPatchInfo(timeout); + } + + public Single selfTest(long timeout) { + return patchManager.selfTest(timeout); + } + + public Single getTemperature() { + return patchManager.getTemperature(); + } + + public Observable startPriming(long timeout, long count) { + return patchManager.startPriming(timeout, count); + } + + public Single checkNeedleSensing(long timeout) { + return patchManager.checkNeedleSensing(timeout); + } + + public Single patchActivation(long timeout) { + return patchManager.patchActivation(timeout); + } + + public Single startBasal(NormalBasal basal) { + return patchManager.startBasal(basal); + } + + public Single resumeBasal() { + return patchManager.resumeBasal(); + } + + + public Single pauseBasal(float pauseDurationHour) { + return patchManager.pauseBasal(pauseDurationHour); + } + + //============================================================================================== + // IPatchManager interface [TEMP BASAL] + //============================================================================================== + + public Single startTempBasal(TempBasal tempBasal) { + return patchManager.startTempBasal(tempBasal); + } + + // 템프베이젤 주입 정지 + // 템프베이젤이 정지되면 자동으로 노멀베이젤이 활성화된다 + // 외부에서 호출된다. 즉 명시적으로 tempBasal 정지. 이 때는 normalBasal resume 은 PatchState 보고 처리. + + public Single stopTempBasal() { + return patchManager.stopTempBasal(); + } + + + public Single startQuickBolus(float nowDoseU, + float exDoseU, BolusExDuration exDuration) { + return patchManager.startQuickBolus(nowDoseU, exDoseU, exDuration); + } + + + public Single startCalculatorBolus(DetailedBolusInfo detailedBolusInfo) { + return patchManager.startCalculatorBolus(detailedBolusInfo); + } + + + public Single stopNowBolus() { + return patchManager.stopNowBolus(); + } + + + public Single stopExtBolus() { + return patchManager.stopExtBolus(); + } + + + public Single stopComboBolus(){ + return patchManager.stopComboBolus(); + } + + public Single deactivate(long timeout, boolean force) { + return patchManager.deactivate(timeout, force); + } + + public Single stopBuzz() { + return patchManager.stopBuzz(); + } + + public Single infoReminderSet(boolean infoReminder) { + return patchManager.infoReminderSet(infoReminder); + } + + public Single setLowReservoir(int doseUnit, int hours) { + return patchManager.setLowReservoir(doseUnit, hours); + } + + public Single updateConnection() { + return patchManager.updateConnection(); + } + + public Single stopAeBeep(int aeCode) { + return patchManager.stopAeBeep(aeCode); + } + + @Override + public Single scan(long timeout) { + patchManager.updateMacAddress("", false); + pm.getPatchConfig().setMacAddress(""); + return patchScanner.scan(timeout); + } + + @Override + public void initBasalSchedule() { + if(pm.getNormalBasalManager().getNormalBasal() == null){ + pm.getNormalBasalManager().setNormalBasal(profileFunction.getProfile()); + pm.flushNormalBasalManager(); + } + } + + @Override + public void addBolusToHistory(DetailedBolusInfo originalDetailedBolusInfo) { + DetailedBolusInfo detailedBolusInfo = originalDetailedBolusInfo.copy(); + + if(detailedBolusInfo.insulin > 0) { + pumpSync.syncBolusWithPumpId( + detailedBolusInfo.timestamp, + detailedBolusInfo.insulin, + detailedBolusInfo.getBolusType(), + dateUtil.now(), + PumpType.EOFLOW_EOPATCH2, + patchManager.pm.getPatchSerial() + ); + } + if (detailedBolusInfo.carbs > 0) { + pumpSync.syncCarbsWithTimestamp( + detailedBolusInfo.getCarbsTimestamp() != null ? detailedBolusInfo.getCarbsTimestamp() : detailedBolusInfo.timestamp, + detailedBolusInfo.carbs, + null, + PumpType.USER, + patchManager.pm.getPatchSerial() + ); + } + } + + @Override + public void changeBuzzerSetting() { + boolean buzzer = sp.getBoolean(SettingKeys.Companion.getBUZZER_REMINDERS(), false); + if(pm.getPatchConfig().getInfoReminder() != buzzer) { + if (isActivated()) { + infoReminderSet(buzzer) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(patchBooleanResponse -> { + pm.getPatchConfig().setInfoReminder(buzzer); + pm.flushPatchConfig(); + }); + } else { + pm.getPatchConfig().setInfoReminder(buzzer); + pm.flushPatchConfig(); + } + } + } + + @Override + public void changeReminderSetting() { + int doseUnit = sp.getInt(SettingKeys.Companion.getLOW_RESERVIOR_REMINDERS(), 0); + int hours = sp.getInt(SettingKeys.Companion.getEXPIRATION_REMINDERS(), 0); + PatchConfig pc = pm.getPatchConfig(); + if(pc.getLowReservoirAlertAmount() != doseUnit || pc.getPatchExpireAlertTime() != hours) { + if (isActivated()) { + setLowReservoir(doseUnit, hours) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(patchBooleanResponse -> { + pc.setLowReservoirAlertAmount(doseUnit); + pc.setPatchExpireAlertTime(hours); + pm.flushPatchConfig(); + }); + } else { + pc.setLowReservoirAlertAmount(doseUnit); + pc.setPatchExpireAlertTime(hours); + pm.flushPatchConfig(); + } + } + } + + @Override + public void checkActivationProcess(){ + if(getPatchConfig().getLifecycleEvent().isSubStepRunning() + && !pm.getAlarms().isOccuring(AlarmCode.A005) + && !pm.getAlarms().isOccuring(AlarmCode.A020)) { + RxAction.INSTANCE.runOnMainThread(() -> { + rxBus.send(new EventPatchActivationNotComplete()); + }); + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchManagerImpl.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchManagerImpl.java new file mode 100644 index 0000000000..f77da843b8 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchManagerImpl.java @@ -0,0 +1,793 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble; + +import static android.content.Intent.ACTION_DATE_CHANGED; +import static android.content.Intent.ACTION_TIMEZONE_CHANGED; +import static android.content.Intent.ACTION_TIME_CHANGED; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import androidx.annotation.Nullable; + +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.shared.logging.AAPSLogger; +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.EoPatchRxBus; +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.ActivateTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.DeactivateTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.GetPatchInfoTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.InfoReminderTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.NeedleSensingTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.PauseBasalTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.PrimingTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.ResumeBasalTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.SelfTestTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.SetLowReservoirTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.StartBondTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.StartCalcBolusTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.StartNormalBasalTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.StartQuickBolusTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.StartTempBasalTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.StopComboBolusTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.StopExtBolusTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.StopNowBolusTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.StopTempBasalTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.SyncBasalHistoryTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.TaskBase; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.TaskFunc; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.UpdateConnectionTask; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.BleConnectionState; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.IBleDevice; +import info.nightscout.androidaps.plugins.pump.eopatch.core.Patch; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.PatchSelfTestResult; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BasalStop; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BuzzerStop; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetTemperature; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.PublicKeySend; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.SequenceGet; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.StopAeBeep; +import info.nightscout.androidaps.plugins.pump.eopatch.core.code.BolusType; +import info.nightscout.androidaps.plugins.pump.eopatch.core.noti.AlarmNotification; +import info.nightscout.androidaps.plugins.pump.eopatch.core.noti.BaseNotification; +import info.nightscout.androidaps.plugins.pump.eopatch.core.noti.InfoNotification; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.*; + +import info.nightscout.androidaps.plugins.pump.eopatch.code.BolusExDuration; +import info.nightscout.androidaps.plugins.pump.eopatch.code.DeactivationStatus; + +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.InvalidParameterSpecException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.crypto.KeyAgreement; +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.util.HexString; +import info.nightscout.androidaps.plugins.pump.eopatch.code.SettingKeys; +import info.nightscout.androidaps.plugins.pump.eopatch.event.EventEoPatchAlarm; +import info.nightscout.androidaps.plugins.pump.eopatch.ui.receiver.RxBroadcastReceiver; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.BolusCurrent; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.NormalBasal; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchConfig; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.TempBasal; +import info.nightscout.shared.sharedPreferences.SP; +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import io.reactivex.Single; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; + +@Singleton +public class PatchManagerImpl/* implements IPatchConstant*/ { + @Inject IPreferenceManager pm; + @Inject Context context; + @Inject SP sp; + @Inject AAPSLogger aapsLogger; + + @Inject StartBondTask START_BOND; + @Inject GetPatchInfoTask GET_PATCH_INFO; + @Inject SelfTestTask SELF_TEST; + @Inject PrimingTask START_PRIMING; + @Inject NeedleSensingTask START_NEEDLE_CHECK; + + IBleDevice patch; + HexString hexString; + + private CompositeDisposable compositeDisposable; + + private Observable dateTimeChanged; + + private static final long DEFAULT_API_TIME_OUT = 10; // SECONDS + + private BuzzerStop BUZZER_STOP; + private GetTemperature TEMPERATURE_GET; + private BasalStop BASAL_STOP; + private StopAeBeep ALARM_ALERT_ERROR_BEEP_STOP; + private PublicKeySend PUBLIC_KEY_SET; + private SequenceGet SEQUENCE_GET; + + @Inject + public PatchManagerImpl() { + compositeDisposable = new CompositeDisposable(); + hexString = new HexString(); + + BUZZER_STOP = new BuzzerStop(); + TEMPERATURE_GET = new GetTemperature(); + BASAL_STOP = new BasalStop(); + ALARM_ALERT_ERROR_BEEP_STOP = new StopAeBeep(); + PUBLIC_KEY_SET = new PublicKeySend(); + SEQUENCE_GET = new SequenceGet(); + } + + @Inject + void onInit() { + patch = Patch.getInstance(); + patch.init(context); + patch.setSeq(pm.getPatchConfig().getSeq15()); + + IntentFilter filter = new IntentFilter(ACTION_TIME_CHANGED); + filter.addAction(ACTION_DATE_CHANGED); + filter.addAction(ACTION_TIMEZONE_CHANGED); + + dateTimeChanged = RxBroadcastReceiver.Companion.create(context, filter); + + compositeDisposable.add( + Observable.combineLatest(patch.observeConnected(), pm.observePatchLifeCycle(), + (connected, lifeCycle) -> (connected && lifeCycle.isActivated())) + .subscribeOn(Schedulers.io()) + .filter(ok -> ok) + .observeOn(Schedulers.io()) + .doOnNext(v -> TaskBase.enqueue(TaskFunc.UPDATE_CONNECTION)) + .retry() + .subscribe()); + + compositeDisposable.add( + Observable.combineLatest(patch.observeConnected(), pm.observePatchLifeCycle().distinctUntilChanged(), dateTimeChanged.startWith(new Intent()), + (connected, lifeCycle, value) -> (connected && lifeCycle.isActivated())) + .subscribeOn(Schedulers.io()) + .doOnNext(v -> aapsLogger.debug(LTag.PUMP,"Has the date or time changed? "+v)) + .filter(ok -> ok) + .doOnNext(v -> TaskBase.enqueue(TaskFunc.SET_GLOBAL_TIME)) + .doOnError(e -> aapsLogger.error(LTag.PUMP, "Failed to set EOPatch time.")) + .retry() + .subscribe()); + + compositeDisposable.add( + patch.observeConnected() + .doOnNext(it -> onPatchConnected(it)) + .subscribe()); + + compositeDisposable.add( + pm.getPatchConfig().observe().doOnNext(config -> { + byte[] newKey = config.getSharedKey(); + patch.updateEncryptionParam(newKey); + }).subscribe() + ); + + compositeDisposable.add( + EoPatchRxBus.INSTANCE.listen(EventEoPatchAlarm.class) + .filter(it -> it.isFirst()) + .filter(it -> !pm.getPatchConfig().isDeactivated()) + .filter(it -> patch.getConnectionState().isConnected()) + .concatMapIterable(it -> it.getAlarmCodes()) + .filter(it -> it.isPatchOccurrenceAlert()) + .flatMap(it -> stopAeBeep(it.getAeCode()).toObservable()) + .subscribe() + ); + + compositeDisposable.add( + EoPatchRxBus.INSTANCE.listen(EventEoPatchAlarm.class) + .filter(it -> it.isFirst()) + .filter(it -> !pm.getPatchConfig().isDeactivated()) + .filter(it -> patch.getConnectionState().isConnected()) + .concatMapIterable(it -> it.getAlarmCodes()) + .filter(it -> it.isPatchOccurrenceAlarm()) + .flatMap(it -> pauseBasalImpl(0.0f, System.currentTimeMillis(), it).toObservable()) + .subscribe() + ); + + + monitorPatchNotification(); + onConnectedUpdateSequence(); + } + + private void onPatchConnected(boolean connected) { + boolean activated = pm.getPatchConfig().isActivated(); + boolean useEncryption = pm.getPatchConfig().getSharedKey() != null; + int doseUnit = sp.getInt(SettingKeys.Companion.getLOW_RESERVIOR_REMINDERS(), 0); + int hours = sp.getInt(SettingKeys.Companion.getEXPIRATION_REMINDERS(), 0); + boolean buzzer = sp.getBoolean(SettingKeys.Companion.getBUZZER_REMINDERS(), false); + PatchConfig pc = pm.getPatchConfig(); + + if (connected && activated && useEncryption) { + compositeDisposable.add( + SEQUENCE_GET.get() + .map(KeyResponse::getSequence) + .doOnSuccess(sequence -> { + if (sequence >= 0) { + saveSequence(sequence); + } + }) + .flatMap(integer -> { + if(pc.getLowReservoirAlertAmount() != doseUnit || pc.getPatchExpireAlertTime() != hours) { + return setLowReservoir(doseUnit, hours) + .doOnSuccess(patchBooleanResponse -> { + pc.setLowReservoirAlertAmount(doseUnit); + pc.setPatchExpireAlertTime(hours); + pm.flushPatchConfig(); + }).map(patchBooleanResponse -> true); + } + return Single.just(true); + }) + .flatMap(ret -> { + if(pc.getInfoReminder() != buzzer) { + return infoReminderSet(buzzer) + .doOnSuccess(patchBooleanResponse -> { + pc.setInfoReminder(buzzer); + pm.flushPatchConfig(); + }).map(patchBooleanResponse -> true); + } + return Single.just(true); + }) + .subscribe()); + } + + if(connected == false && activated == true){ + pm.getPatchConfig().updatetDisconnectedTime(); + } + } + + private void monitorPatchNotification() { + compositeDisposable.addAll( + patch.observeAlarmNotification() + .subscribe( + this::onAlarmNotification, + throwable -> aapsLogger.error(LTag.PUMP, throwable.getMessage()) + ), + patch.observeInfoNotification() + .filter(state -> pm.getPatchConfig().isActivated()) + .subscribe( + this::onInfoNotification, + throwable -> aapsLogger.error(LTag.PUMP, throwable.getMessage()) + ) + ); + } + + + private void onConnectedUpdateSequence() { + + } + + //============================================================================================== + // preference database update helper + //============================================================================================== + + // synchronized lock + private final Object lock = new Object(); + + private void updatePatchConfig(Consumer consumer, boolean needSave) throws Exception { + synchronized (lock) { + consumer.accept(pm.getPatchConfig()); + if (needSave) { + pm.flushPatchConfig(); + } else { + pm.flushPatchConfig(); + } + } + } + + synchronized void updateBasal() { + + NormalBasal normalBasal = pm.getNormalBasalManager().getNormalBasal(); + + if (normalBasal != null) { + // 아래 코드를 실행하면 isDoseUChanged 가 false 가 된다. + if(normalBasal.updateNormalBasalIndex()) { + pm.flushNormalBasalManager(); + } + } + } + + public void connect() { + + } + + public void disconnect() { + } + + /** + * getPatchConnection() 을 사용해야 한다. + * 아직 Life Cycle 이 Activated 가 아님. + * + * Activation Process task #1 Get Patch Information from Patch + * Fragment: fragment_patch_connect_new + */ + + public Single startBond(String mac) { + return START_BOND.start(mac); + } + + public Single getPatchInfo(long timeout) { + return GET_PATCH_INFO.get().timeout(timeout, TimeUnit.MILLISECONDS); + } + + + /** + * Activation Process task #2 Check Patch is O.K + * Fragment: fragment_patch_connect_new + */ + public Single selfTest(long timeout) { + return SELF_TEST.start().timeout(timeout, TimeUnit.MILLISECONDS); + } + + /** + * Activation Process task #3 PRIMING + * Fragment: fragment_patch_priming + */ + + public Single getTemperature() { + return TEMPERATURE_GET.get() + .timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + public Observable startPriming(long timeout, long count) { + return START_PRIMING.start(count) + .timeout(timeout, TimeUnit.MILLISECONDS); + } + + /** + * Activation Process task #4 NEEDLE SENSING + * Fragment: fragment_patch_rotate_knob + */ + public Single checkNeedleSensing(long timeout) { //TODO: Timeout 추가? + return START_NEEDLE_CHECK.start() + .timeout(timeout, TimeUnit.MILLISECONDS); + } + + /** + * Activation Process task #5 Activation Secure Key, Basal writing + * Fragment: fragment_patch_check_patch + */ + @Inject + ActivateTask ACTIVATE; + + public Single patchActivation(long timeout) { + + return ACTIVATE.start().timeout(timeout, TimeUnit.MILLISECONDS) + .flatMap(success -> sharedKey()) + .flatMap(success -> getSequence()) + .doOnSuccess(success -> { + if (success) { + TaskBase.enqueue(TaskFunc.LOW_RESERVOIR); + TaskBase.enqueue(TaskFunc.INFO_REMINDER); + } + }); + } + + + //============================================================================================== + // IPatchManager interface [NORMAL BASAL] + //============================================================================================== + + @Inject + StartNormalBasalTask startNormalBasalTask; + + public Single startBasal(NormalBasal basal) { + + return startNormalBasalTask.start(basal, false) + .timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + @Inject + ResumeBasalTask resumeBasalTask; + + public Single resumeBasal() { + return resumeBasalTask.resume() + .timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + public Single pauseBasal(float pauseDurationHour) { + return pauseBasalImpl(pauseDurationHour, 0, null) + .observeOn(SS) + .timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + //============================================================================================== + // IPatchManager implementation [NORMAL BASAL] + //============================================================================================== + + @Inject + PauseBasalTask pauseBasalTask; + + private Single pauseBasalImpl(float pauseDurationHour, long alarmOccurredTime, @Nullable AlarmCode alarmCode) { + return pauseBasalTask.pause(pauseDurationHour, alarmOccurredTime, alarmCode); + } + + private Single stopBasal() { + return BASAL_STOP.stop(); + } + + private void insertBasalStart() throws SQLException { + insertBasalStart(System.currentTimeMillis()); + } + + private void insertBasalStart(long timestamp) throws SQLException { + NormalBasal startedBasal = pm.getNormalBasalManager().getNormalBasal(); + if (startedBasal != null) { + startedBasal.updateNormalBasalIndex(); + pm.flushNormalBasalManager(); + } + } + + //============================================================================================== + // IPatchManager interface [TEMP BASAL] + //============================================================================================== + + @Inject + StartTempBasalTask startTempBasalTask; + + public Single startTempBasal(TempBasal tempBasal) { + return startTempBasalTask.start(tempBasal).timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + // 템프베이젤 주입 정지 + // 템프베이젤이 정지되면 자동으로 노멀베이젤이 활성화된다 + // 외부에서 호출된다. 즉 명시적으로 tempBasal 정지. 이 때는 normalBasal resume 은 PatchState 보고 처리. + + @Inject + StopTempBasalTask stopTempBasalTask; + + public Single stopTempBasal() { + return stopTempBasalTask.stop().timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + @Inject + StartQuickBolusTask startQuickBolusTask; + + @Inject + StartCalcBolusTask startCalcBolusTask; + + @Inject + StopComboBolusTask stopComboBolusTask; + + @Inject + StopNowBolusTask stopNowBolusTask; + + @Inject + StopExtBolusTask stopExtBolusTask; + + + public Single startQuickBolus(float nowDoseU, float exDoseU, + BolusExDuration exDuration) { + return startQuickBolusTask.start(nowDoseU, exDoseU, exDuration) + .timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + public Single startCalculatorBolus(DetailedBolusInfo detailedBolusInfo) { + return startCalcBolusTask.start(detailedBolusInfo) + .timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + public Single stopNowBolus() { + return stopNowBolusTask.stop().timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + public Single stopExtBolus() { + return stopExtBolusTask.stop().timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + public Single stopComboBolus(){ + return stopComboBolusTask.stop().timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + private Single stopNowAndExtBolus() { + + boolean nowActive = pm.getPatchState().isNowBolusActive(); + boolean extActive = pm.getPatchState().isExtBolusActive(); + + if (nowActive && extActive) { + return stopComboBolus(); + } else if (nowActive) { + return stopNowBolus(); + } else if (extActive) { + return stopExtBolus(); + } + + return Single.just(new PatchBooleanResponse(true)); + } + + //============================================================================================== + // IPatchManager implementation [BOLUS] + //============================================================================================== + + public void readBolusStatusFromNotification(InfoNotification noti) { + if (noti.isBolusRegAct()) { + BolusCurrent bolusCurrent = pm.getBolusCurrent(); + + Arrays.asList(BolusType.NOW, BolusType.EXT).forEach(type -> { + if (noti.isBolusRegAct(type)) { // 완료되었어도 업데이트 필요. + int injectedPumpCount = noti.getInjected(type); + int remainPumpCount = noti.getRemain(type); + bolusCurrent.updateBolusFromPatch(type, injectedPumpCount, remainPumpCount); + } + }); + pm.flushBolusCurrent(); + } + } + + + @Inject + DeactivateTask deactivateTask; + // Patch Activation Tasks + public Single deactivate(long timeout, boolean force) { + return deactivateTask.run(force, timeout); + } + + public Single stopAll(){ + List> sources = new ArrayList<>(); + + // 노멀볼루스 또는 확장볼루스가 동작중이면 정지 + + if (pm.getPatchState().isNowBolusActive() || pm.getPatchState().isExtBolusActive()) { + sources.add(stopNowAndExtBolus()); + } + + // 템프베이젤이 동작중이면 중지 + if (pm.getPatchState().isTempBasalActive()) { + sources.add(stopTempBasal()); + } + + sources.add(stopBasal()); + + return Single.concat(sources).lastOrError(); + } + + public Single stopBuzz() { + return BUZZER_STOP.stop(); + } + + @Inject + InfoReminderTask infoReminderTask; + + public Single infoReminderSet(boolean infoReminder) { + return infoReminderTask.set(infoReminder).timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + @Inject + SetLowReservoirTask setLowReservoirTask; + public Single setLowReservoir(int doseUnit, int hours) { + return setLowReservoirTask.set(doseUnit, hours).timeout(DEFAULT_API_TIME_OUT, TimeUnit.SECONDS); + } + + @Inject + UpdateConnectionTask updateConnectionTask; + public Single updateConnection() { + return updateConnectionTask.update(); + } + + public Single stopAeBeep(int aeCode) { + return ALARM_ALERT_ERROR_BEEP_STOP.stop(aeCode); + } + + synchronized void fetchPatchState() { + updateConnectionTask.enqueue(); + } + + @Inject + PatchStateManager patchStateManager; + + @Inject + SyncBasalHistoryTask syncBasalHistoryTask; + + void onAlarmNotification(AlarmNotification notification) throws Exception { + patchStateManager.updatePatchState(PatchState.create(notification.patchState, System.currentTimeMillis())); + + if (pm.getPatchConfig().isActivated()) { + if(!patch.isSeqReady()){ + getSequence().subscribe(); + } + updateBasal(); + updateInjected(notification, true); + fetchPatchState(); + } + } + + private void onInfoNotification(InfoNotification notification) throws Exception { + readBolusStatusFromNotification(notification); + updateInjected(notification, false); + if (notification.isBolusDone()) { + fetchPatchState(); + } + } + + void updateInjected(BaseNotification notification, boolean needSave) throws Exception { + updatePatchConfig(patchConfig -> { + patchConfig.setInjectCount(notification.getTotalInjected()); + patchConfig.setStandardBolusInjectCount(notification.getSB_CNT()); + patchConfig.setExtendedBolusInjectCount(notification.getEB_CNT()); + patchConfig.setBasalInjectCount(notification.getBasal_CNT()); + }, needSave); + } + + //============================================================================================== + // Security + //============================================================================================== + private static final String SECP256R1 = "secp256r1"; + private static final String EC = "EC"; + private static final String ECDH = "ECDH"; + + public Single sharedKey() { + return genKeyPair().flatMap(keyPair -> ECPublicToRawBytes(keyPair) + .flatMap(bytes -> PUBLIC_KEY_SET.send(bytes) + .map(KeyResponse::getPublicKey) + .map(bytes2 -> rawToEncodedECPublicKey(SECP256R1, bytes2)) + .map(publicKey -> generateSharedSecret(keyPair.getPrivate(), publicKey)) + .doOnSuccess(this::saveShared).map(v2 -> true))) + .doOnError(e -> aapsLogger.error(LTag.PUMP, "sharedKey error")); + } + + public Single getSequence() { + return SEQUENCE_GET.get() + .map(KeyResponse::getSequence) + .doOnSuccess(sequence -> { + if (sequence >= 0) { + saveSequence(sequence); + } + }) + .flatMap(v -> Single.just(true)); + } + + private void saveShared(byte[] v) { + pm.getPatchConfig().setSharedKey(v); + pm.flushPatchConfig(); + } + + private void saveSequence(int sequence) { + patch.setSeq(sequence); + pm.getPatchConfig().setSeq15(sequence); + pm.flushPatchConfig(); + } + + public Single genKeyPair() { + return Single.fromCallable(() -> { + ECGenParameterSpec ecSpec_named = new ECGenParameterSpec(SECP256R1); + KeyPairGenerator kpg = KeyPairGenerator.getInstance(EC); + kpg.initialize(ecSpec_named); + KeyPair pair = kpg.generateKeyPair(); + return pair; + }); + } + + public Single ECPublicToRawBytes(KeyPair keyPair) { + return Single.just(keyPair.getPublic()).cast(ECPublicKey.class) + .map(PatchManagerImpl::encodeECPublicKey); + } + + private static byte[] encodeECPublicKey(ECPublicKey pubKey) { + int keyLengthBytes = pubKey.getParams().getOrder().bitLength() + / Byte.SIZE; + byte[] publicKeyEncoded = new byte[2 * keyLengthBytes]; + + int offset = 0; + + BigInteger x = pubKey.getW().getAffineX(); + byte[] xba = x.toByteArray(); + if (xba.length > keyLengthBytes + 1 || xba.length == keyLengthBytes + 1 + && xba[0] != 0) { + throw new IllegalStateException( + "X coordinate of EC public key has wrong size"); + } + + if (xba.length == keyLengthBytes + 1) { + System.arraycopy(xba, 1, publicKeyEncoded, offset, keyLengthBytes); + } else { + System.arraycopy(xba, 0, publicKeyEncoded, offset + keyLengthBytes + - xba.length, xba.length); + } + offset += keyLengthBytes; + + BigInteger y = pubKey.getW().getAffineY(); + byte[] yba = y.toByteArray(); + if (yba.length > keyLengthBytes + 1 || yba.length == keyLengthBytes + 1 + && yba[0] != 0) { + throw new IllegalStateException( + "Y coordinate of EC public key has wrong size"); + } + + if (yba.length == keyLengthBytes + 1) { + System.arraycopy(yba, 1, publicKeyEncoded, offset, keyLengthBytes); + } else { + System.arraycopy(yba, 0, publicKeyEncoded, offset + keyLengthBytes + - yba.length, yba.length); + } + + return publicKeyEncoded; + } + + public static ECPublicKey rawToEncodedECPublicKey(String curveName, byte[] rawBytes) throws + NoSuchAlgorithmException, InvalidKeySpecException, InvalidParameterSpecException, + InvalidAlgorithmParameterException { + KeyFactory kf = KeyFactory.getInstance(EC); + int mid = rawBytes.length / 2; + byte[] x = Arrays.copyOfRange(rawBytes, 0, mid); + byte[] y = Arrays.copyOfRange(rawBytes, mid, rawBytes.length); + ECPoint w = new ECPoint(new BigInteger(1, x), new BigInteger(1, y)); + return (ECPublicKey) kf.generatePublic(new ECPublicKeySpec(w, ecParameterSpecForCurve(curveName))); + } + + public static ECParameterSpec ecParameterSpecForCurve(String curveName) throws + NoSuchAlgorithmException, InvalidParameterSpecException, InvalidAlgorithmParameterException { + AlgorithmParameters params = AlgorithmParameters.getInstance(EC); + params.init(new ECGenParameterSpec(curveName)); + return params.getParameterSpec(ECParameterSpec.class); + } + + public static byte[] generateSharedSecret(PrivateKey privateKey, + PublicKey publicKey) { + try { + KeyAgreement keyAgreement = KeyAgreement.getInstance(ECDH); + keyAgreement.init(privateKey); + keyAgreement.doPhase(publicKey, true); + + return keyAgreement.generateSecret(); + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return null; + } + } + + //============================================================================================== + // Single Scheduler all callback must be observed on + //============================================================================================== + + private static final Scheduler SS = Schedulers.single(); + + public BleConnectionState getPatchConnectionState() { + BleConnectionState result = patch.getConnectionState(); + return result; + } + + public Observable observePatchConnectionState() { + return patch.observeConnectionState(); + } + + public void updateMacAddress(String mac, boolean b){ + patch.updateMacAddress(mac, b); + } +} + +class AlarmFiredEventInfo +{ + public AlarmCode code; + public long createTimestamp; + + public AlarmFiredEventInfo(AlarmCode code, long createTimestamp) { + this.code = code; + this.createTimestamp = createTimestamp; + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchStateManager.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchStateManager.java new file mode 100644 index 0000000000..0efaa50521 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchStateManager.java @@ -0,0 +1,290 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble; + +import java.util.stream.Stream; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.interfaces.CommandQueue; +import info.nightscout.shared.logging.AAPSLogger; +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.code.BolusType; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.FetchAlarmTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.InternalSuspendedTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.ReadBolusFinishTimeTask; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.task.ReadTempBasalFinishTimeTask; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.BolusCurrent; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.NormalBasal; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.TempBasal; +import io.reactivex.Maybe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + + +@Singleton +public class PatchStateManager { + + @Inject IPreferenceManager pm; + @Inject ReadBolusFinishTimeTask readBolusFinishTimeTask; + @Inject ReadTempBasalFinishTimeTask readTempBasalFinishTimeTask; + @Inject InternalSuspendedTask internalSuspendedTask; + @Inject FetchAlarmTask FETCH_ALARM; + @Inject CommandQueue commandQueue; + @Inject AAPSLogger aapsLogger; + + @Inject + public PatchStateManager() { + + } + + public synchronized void updatePatchState(PatchState newState) { + Maybe.fromCallable(() -> newState).observeOn(Schedulers.single()) + .doOnSuccess(patchState -> updatePatchStateInner(patchState)) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess(patchState -> aapsLogger.debug(LTag.PUMP, patchState.toString())) + .subscribe(); + } + + /* Schedulers.io() */ + public synchronized void updatePatchStateInner(PatchState newState) { + + final PatchState oldState = pm.getPatchState(); + + int diff = newState.currentTime() - oldState.currentTime(); + if (0 <= diff && diff < 10) { // TODO 상수로 변경. + /* 10초 안에 같은 PatchState update 시 skip */ + if (oldState.equalState(newState)) { + return; + } + } else if (-5 < diff && diff < 0) { + /* 이전 State 가 새로운 State 를 덮어 쓰는 것을 방지 -4초 까지 */ + return; + } + + newState.setUpdatedTimestamp(System.currentTimeMillis()); + + if (newState.isNewAlertAlarm()) { + FETCH_ALARM.enqueue(); + } + + if (newState.isPatchInternalSuspended()){ + onPatchInternalSuspended(newState); + } + + /* Normal Basal --------------------------------------------------------------------------------------------- */ + + if (newState.isNormalBasalAct()) { + if (oldState.isNormalBasalPaused()) { + // Resume --> onBasalResume + onBasalResumeState(); + + } else if (oldState.isNormalBasalAct() == false) { + // Start --> onBasalStarted + } + } else if (oldState.isNormalBasalPaused() == false && newState.isNormalBasalPaused()) { + if (newState.isTempBasalAct()) { + } else { + // pause + + } + } + + /* Temp Basal ------------------------------------------------------------------------------------------- */ + if (newState.isTempBasalAct()) { + if (oldState.isTempBasalAct() == false) { + // Start + onTempBasalStartState(); + } + } + + boolean tempBasalStopped = false; + boolean tempBasalFinished = false; + + if (newState.isTempBasalDone() && !newState.isPatchInternalSuspended()) { + tempBasalFinished = true; + } + + if (oldState.isTempBasalDone() == false) { + if (newState.isTempBasalDone()) { + tempBasalStopped = true; + + onTempBasalDoneState(); + } else if (oldState.isTempBasalAct() && newState.isTempBasalAct() == false) { + tempBasalStopped = true; + + onTempBasalCancelState(); + } + } + + if (tempBasalStopped) { + if (newState.isNormalBasalAct()) { + if (!newState.isPatchInternalSuspended()) { + onNormalBasalResumed(tempBasalFinished); + } + } + } + + if (newState.isTempBasalAct() == false && pm.getTempBasalManager().getStartedBasal() != null) { + pm.getTempBasalManager().updateBasalStopped(); + } + + /* Now Bolus -------------------------------------------------------------------------------------------- */ + if (oldState.isNowBolusRegAct() == false && newState.isNowBolusRegAct() == true) { + // Start + } else if (oldState.isNowBolusDone() == false) { + if (oldState.isNowBolusRegAct() && newState.isNowBolusRegAct() == false) { + // Cancel + } else if (newState.isNowBolusDone()) { + // Done + } + } + + BolusCurrent bolusCurrent = pm.getBolusCurrent(); + + if (newState.isNowBolusRegAct() == false && bolusCurrent.historyId(BolusType.NOW) > 0 + && bolusCurrent.endTimeSynced(BolusType.NOW)) { + bolusCurrent.clearBolus(BolusType.NOW); + } + + /* Extended Bolus --------------------------------------------------------------------------------------- */ + if (oldState.isExtBolusRegAct() == false && newState.isExtBolusRegAct() == true) { + // Start + } else if (oldState.isExtBolusDone() == false) { + if (oldState.isExtBolusRegAct() && newState.isExtBolusRegAct() == false) { + // Cancel + } else if (newState.isExtBolusDone()) { + // Done + } + } + + if (newState.isExtBolusRegAct() == false && bolusCurrent.historyId(BolusType.EXT) > 0 + && bolusCurrent.endTimeSynced(BolusType.EXT)) { + bolusCurrent.clearBolus(BolusType.EXT); + } + + /* Finish Time Sync and remained insulin update*/ + /* Bolus Done -> update finish time */ + if (Stream.of(BolusType.NOW, BolusType.EXT).anyMatch(type -> + newState.isBolusDone(type) && !bolusCurrent.endTimeSynced(type))) { + readBolusFinishTime(); + } + + /* TempBasal Done -> update finish time */ + if (tempBasalFinished) { + readTempBasalFinishTime(); + } + + /* Remained Insulin update */ + if (newState.getRemainedInsulin() != oldState.getRemainedInsulin()) { + pm.getPatchConfig().setRemainedInsulin(newState.getRemainedInsulin()); + pm.flushPatchConfig(); + } + + pm.getPatchState().update(newState); + pm.flushPatchState(); + } + + private void onTempBasalStartState() { + TempBasal tempBasal = pm.getTempBasalManager().getStartedBasal(); + + if (tempBasal != null) { + pm.getPatchConfig().updateTempBasalStarted(); + + NormalBasal normalBasal = pm.getNormalBasalManager().getNormalBasal(); + + if (normalBasal != null) { + pm.getNormalBasalManager().updateBasalPaused(); + } + + pm.flushPatchConfig(); + pm.flushNormalBasalManager(); + } + } + + void onTempBasalDoneState() { + TempBasal tempBasal = pm.getTempBasalManager().getStartedBasal(); + + if (tempBasal != null) { + pm.getTempBasalManager().updateBasalStopped(); + pm.flushTempBasalManager(); + } + } + + private void onTempBasalCancelState() { + TempBasal tempBasal = pm.getTempBasalManager().getStartedBasal(); + + if (tempBasal != null) { + pm.getTempBasalManager().updateBasalStopped(); + pm.flushTempBasalManager(); + } + } + + + private void readBolusFinishTime() { + readBolusFinishTimeTask.enqueue(); + } + + private void readTempBasalFinishTime() { + readTempBasalFinishTimeTask.enqueue(); + } + + private synchronized void onBasalResumeState() { + + if (!pm.getNormalBasalManager().isStarted()) { + long timestamp = System.currentTimeMillis(); + onBasalResumed(timestamp + 1000); + } + } + + void onNormalBasalResumed(boolean tempBasalFinished) { + NormalBasal normalBasal = pm.getNormalBasalManager().getNormalBasal(); + if (normalBasal != null) { + pm.getNormalBasalManager().updateBasalStarted(); + normalBasal.updateNormalBasalIndex(); + pm.flushNormalBasalManager();; + } + } + + public synchronized void onBasalResumed(long timestamp) { + if (!pm.getNormalBasalManager().isStarted()) { + pm.getNormalBasalManager().updateBasalStarted(); + + pm.getPatchConfig().updateNormalBasalStarted(); + pm.getPatchConfig().setNeedSetBasalSchedule(false); + + NormalBasal basal = pm.getNormalBasalManager().getNormalBasal(); + + if (basal != null) { + basal.updateNormalBasalIndex(); + } + + pm.flushPatchConfig(); + pm.flushNormalBasalManager(); + } + } + + public synchronized void onBasalStarted(NormalBasal basal, long timestamp) { + if (basal != null) { + pm.getNormalBasalManager().updateBasalStarted(); + + basal.updateNormalBasalIndex(); //normal basal index를 업데이트하여 basal change 이력이 또 발생하는것을 방지(df_356) + } + + pm.getPatchConfig().updateNormalBasalStarted(); // updateNormalBasalStarted 도 동일함... + pm.getPatchConfig().setNeedSetBasalSchedule(false); + + pm.flushPatchConfig(); + pm.flushNormalBasalManager(); + } + + private void onPatchInternalSuspended(PatchState state) { + boolean isNowBolusActive = state.isNowBolusActive(); + boolean isExtBolusActive = state.isExtBolusActive(); + boolean isTempBasalActive = state.isTempBasalActive(); + + if (isNowBolusActive || isExtBolusActive || isTempBasalActive) { + internalSuspendedTask.enqueue(isNowBolusActive, isExtBolusActive, isTempBasalActive); + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PreferenceManager.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PreferenceManager.kt new file mode 100644 index 0000000000..97164f6c81 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PreferenceManager.kt @@ -0,0 +1,254 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble + +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.shared.logging.LTag +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.pump.eopatch.GsonHelper +import info.nightscout.androidaps.plugins.pump.eopatch.code.SettingKeys +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchLifecycle +import info.nightscout.androidaps.plugins.pump.eopatch.vo.* +import info.nightscout.shared.sharedPreferences.SP +import io.reactivex.Observable +import javax.inject.Inject +import javax.inject.Singleton + +interface IPreferenceManager { + fun getPatchConfig(): PatchConfig + fun getPatchState(): PatchState + fun getBolusCurrent(): BolusCurrent + fun getNormalBasalManager(): NormalBasalManager + fun getTempBasalManager(): TempBasalManager + fun getAlarms(): Alarms + fun init() + fun flushPatchConfig() + fun flushPatchState() + fun flushBolusCurrent() + fun flushNormalBasalManager() + fun flushTempBasalManager() + fun flushAlarms() + fun updatePatchLifeCycle(event: PatchLifecycleEvent) + fun updatePatchState(newState: PatchState) + fun getPatchSerial(): String + fun getPatchMac(): String? + fun isActivated(): Boolean + fun setMacAddress(mac: String) + fun getPatchExpiredTime(): Long + fun setSharedKey(bytes: ByteArray?) + fun setSeq15(seq15: Int) + fun getSeq15(): Int + fun increaseSeq15() + fun getPatchWakeupTimestamp(): Long + fun observePatchLifeCycle(): Observable + fun observePatchConfig(): Observable + fun observePatchState(): Observable + fun observeBolusCurrent(): Observable + fun observeAlarm(): Observable + fun isInitDone(): Boolean +} + + +/** + * patch2 패키지에서 사용하는 프리퍼런스의 작업을 대신 처리하는 클래스 + */ +@Singleton +class PreferenceManager @Inject constructor(): IPreferenceManager { + @Inject lateinit var sp: SP + @Inject lateinit var rxBus: RxBus + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var mPatchConfig: PatchConfig + @Inject lateinit var mNormalBasalMgr: NormalBasalManager + @Inject lateinit var mTempBasalMgr: TempBasalManager + @Inject lateinit var mAlarms: Alarms + + private var mPatchState = PatchState() + private var mBolusCurrent = BolusCurrent() + private lateinit var observePatchLifeCycle: Observable + private var initialized = false + + @Inject + fun onInit() { + observePatchLifeCycle = mPatchConfig.observe() + .map { patchConfig -> patchConfig.lifecycleEvent.lifeCycle } + .distinctUntilChanged() + .replay(1).refCount() + } + + override fun getPatchConfig(): PatchConfig { + return mPatchConfig + } + + override fun getPatchState(): PatchState { + return mPatchState + } + + override fun getBolusCurrent(): BolusCurrent { + return mBolusCurrent + } + + override fun getNormalBasalManager(): NormalBasalManager { + return mNormalBasalMgr + } + + override fun getTempBasalManager(): TempBasalManager { + return mTempBasalMgr + } + + override fun getAlarms(): Alarms { + return mAlarms + } + + override fun init() { + try { + val jsonStr = sp.getString(SettingKeys.PATCH_STATE, "") + val savedState = GsonHelper.sharedGson().fromJson(jsonStr, PatchState::class.java) + mPatchState = savedState + } catch (ex: Exception) { + mPatchState = PatchState() + aapsLogger.error(LTag.PUMP, ex.message?:"PatchState load error") + } + + try { + val jsonStr = sp.getString(SettingKeys.BOLUS_CURRENT, "") + val savedBolusCurrent = GsonHelper.sharedGson().fromJson(jsonStr, BolusCurrent::class.java) + mBolusCurrent = savedBolusCurrent + } catch (ex: Exception) { + mBolusCurrent = BolusCurrent() + aapsLogger.error(LTag.PUMP, ex.message?:"BolusCurrent load error") + } + + try { + val jsonStr = sp.getString(SettingKeys.PATCH_CONFIG, "") + val savedConfig = GsonHelper.sharedGson().fromJson(jsonStr, PatchConfig::class.java) + mPatchConfig.update(savedConfig) + } catch (ex: Exception) { + aapsLogger.error(LTag.PUMP, ex.message?:"PatchConfig load error") + } + + try { + val jsonStr = sp.getString(SettingKeys.NORMAL_BASAL, "") + val normalBasalManager = GsonHelper.sharedGson().fromJson(jsonStr, NormalBasalManager::class.java) + mNormalBasalMgr.update(normalBasalManager) + } catch (ex: Exception) { + aapsLogger.error(LTag.PUMP, ex.message?:"NormalBasal load error") + } + + try { + val jsonStr = sp.getString(SettingKeys.TEMP_BASAL, "") + val tempBasalManager = GsonHelper.sharedGson().fromJson(jsonStr, TempBasalManager::class.java) + mTempBasalMgr.update(tempBasalManager) + } catch (ex: Exception) { + aapsLogger.error(LTag.PUMP, ex.message?:"TempBasal load error") + } + + try { + val jsonStr = sp.getString(SettingKeys.ALARMS, "") + val alarms = GsonHelper.sharedGson().fromJson(jsonStr, Alarms::class.java) + mAlarms.update(alarms) + } catch (ex: Exception) { + aapsLogger.error(LTag.PUMP, ex.message?:"Alarms load error") + } + + aapsLogger.info(LTag.PUMP,"Load from PatchConfig preference: ${mPatchConfig}") + aapsLogger.info(LTag.PUMP,"Load from PatchState preference: ${mPatchState}") + aapsLogger.info(LTag.PUMP,"Load from BolusCurrent preference: ${mBolusCurrent}") + aapsLogger.info(LTag.PUMP,"Load from NormalBasal preference: ${mNormalBasalMgr}") + aapsLogger.info(LTag.PUMP,"Load from TempBasal preference: ${mTempBasalMgr}") + aapsLogger.info(LTag.PUMP,"Load from Alarms preference: ${mAlarms}") + initialized = true + } + + override fun isInitDone() = initialized + + override fun flushPatchConfig() = mPatchConfig.flush(sp) + override fun flushPatchState() = mPatchState.flush(sp) + override fun flushBolusCurrent() = mBolusCurrent.flush(sp) + override fun flushNormalBasalManager() = mNormalBasalMgr.flush(sp) + override fun flushTempBasalManager() = mTempBasalMgr.flush(sp) + override fun flushAlarms() = mAlarms.flush(sp) + + @Synchronized + override fun updatePatchLifeCycle(event: PatchLifecycleEvent) { + mPatchConfig.updateLifecycle(event) + flushPatchConfig() + + when (event.lifeCycle) { + PatchLifecycle.SHUTDOWN -> { + mPatchState.clear() + flushPatchState() + mBolusCurrent.clearAll() + flushBolusCurrent() + mTempBasalMgr.clear() + flushTempBasalManager() + // mAlarms.clear() + // flushAlarms() + } + } + + } + + override fun updatePatchState(newState: PatchState) { + mPatchState = newState + flushPatchState() + } + + override fun getPatchSerial(): String { + return mPatchConfig.patchSerialNumber + } + + override fun getPatchMac(): String? { + return mPatchConfig.macAddress + } + + override fun isActivated(): Boolean { + return mPatchConfig.isActivated + } + + override fun setMacAddress(mac: String) { + mPatchConfig.macAddress = mac + flushPatchConfig() + } + + override fun getPatchExpiredTime(): Long { + return mPatchConfig.getPatchExpiredTime() + } + + override fun setSharedKey(bytes: ByteArray?) { + mPatchConfig.sharedKey = bytes + } + + override fun setSeq15(seq15: Int) { + mPatchConfig.seq15 = seq15 + } + + override fun getSeq15(): Int { + return mPatchConfig.seq15 + } + + override fun increaseSeq15() { + mPatchConfig.incSeq() + } + + override fun getPatchWakeupTimestamp(): Long { + return mPatchConfig.patchWakeupTimestamp + } + + override fun observePatchLifeCycle(): Observable { + return observePatchLifeCycle + } + + override fun observePatchConfig(): Observable { + return mPatchConfig.observe() + } + + override fun observePatchState(): Observable { + return mPatchState.observe() + } + + override fun observeBolusCurrent(): Observable{ + return mBolusCurrent.observe() + } + + override fun observeAlarm(): Observable { + return mAlarms.observe() + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ActivateTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ActivateTask.java new file mode 100644 index 0000000000..8e2c3710f1 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ActivateTask.java @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BasalScheduleSetBig; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.SetKey; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BaseResponse; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.vo.NormalBasal; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchLifecycleEvent; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +@Singleton +public class ActivateTask extends TaskBase { + @Inject StartNormalBasalTask startBasalTask; + + private SetKey SET_KEY; + private BasalScheduleSetBig BASAL_SCHEDULE_SET_BIG; + + + @Inject + public ActivateTask() { + super(TaskFunc.ACTIVATE); + SET_KEY = new SetKey(); + BASAL_SCHEDULE_SET_BIG = new BasalScheduleSetBig(); + } + + public Single start() { + NormalBasal enabled = pm.getNormalBasalManager().getNormalBasal(); + return isReady() + .concatMapSingle(v -> SET_KEY.setKey()) + .doOnNext(this::checkResponse) + .firstOrError() + .observeOn(Schedulers.io()).doOnSuccess(this::onActivated) + .flatMap(v -> startBasalTask.start(enabled, false)) + .map(BaseResponse::isSuccess) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private void onActivated(BaseResponse response) { + pm.updatePatchLifeCycle(PatchLifecycleEvent.createActivated()); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/BolusTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/BolusTask.java new file mode 100644 index 0000000000..016ea308a6 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/BolusTask.java @@ -0,0 +1,124 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.androidaps.plugins.pump.eopatch.code.BolusExDuration; + +import info.nightscout.androidaps.plugins.pump.eopatch.AppConstant; +import info.nightscout.androidaps.plugins.pump.eopatch.core.code.BolusType; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.BolusCurrent; + +abstract class BolusTask extends TaskBase { + + public BolusTask(TaskFunc func) { + super(func); + } + + public void onQuickBolusStarted(float nowDoseU, float exDoseU, BolusExDuration exDuration) { + boolean now = (nowDoseU > 0); + boolean ext = (exDoseU > 0); + + long startTimestamp = now ? System.currentTimeMillis() : 0; // dm_1720 + long endTimestamp = startTimestamp + getPumpDuration(nowDoseU); + + long nowHistoryID = 1L; //record no + long exStartTimestamp = 0; + + if (now) { + pm.getBolusCurrent().startNowBolus(nowHistoryID, nowDoseU, startTimestamp, endTimestamp); + } + if (ext) { + long estimatedExStartTimestamp = 0; + + if (now) { + exStartTimestamp = 0; + } + else { + estimatedExStartTimestamp = System.currentTimeMillis(); + exStartTimestamp = estimatedExStartTimestamp; + } + long exEndTimestamp = exStartTimestamp + exDuration.milli(); + + long extHistoryID = 2L; //record no + pm.getBolusCurrent().startExtBolus(extHistoryID, exDoseU, exStartTimestamp, + exEndTimestamp, exDuration.milli()); + } + + pm.flushBolusCurrent(); + } + + + public void onCalcBolusStarted(float nowDoseU, float correctionBolus, float exDoseU, + BolusExDuration exDuration) { + + boolean now = (nowDoseU > 0); + boolean ext = (exDoseU > 0); + + + long startTimestamp = now ? System.currentTimeMillis() : 0; // dm_1720 + long endTimestamp = startTimestamp + getPumpDuration(nowDoseU); + + long nowHistoryID = 1L; //record no + + if (now) { + pm.getBolusCurrent().startNowBolus(nowHistoryID, nowDoseU, startTimestamp, endTimestamp); + } + + long exStartTimestamp = 0; + + if (ext) { + long estimatedExStartTimestamp = 0; + + if (now) { + exStartTimestamp = 0; + } + else { + estimatedExStartTimestamp = System.currentTimeMillis(); + exStartTimestamp = estimatedExStartTimestamp; + } + long exEndTimestamp = exStartTimestamp + exDuration.milli(); + long extHistoryID = 2L; //record no + pm.getBolusCurrent().startExtBolus(extHistoryID, exDoseU, exStartTimestamp, + exEndTimestamp, exDuration.milli()); + } + + pm.flushBolusCurrent(); + } + + public void updateNowBolusStopped(int injected) { + updateNowBolusStopped(injected, 0); + } + + public void updateNowBolusStopped(int injected, long suspendedTimestamp) { + + BolusCurrent bolusCurrent = pm.getBolusCurrent(); + long nowID = bolusCurrent.historyId(BolusType.NOW); + if (nowID > 0 && !bolusCurrent.endTimeSynced(BolusType.NOW)) { +// long stopTime = (suspendedTimestamp > 0) ? suspendedTimestamp : System.currentTimeMillis(); +// float injectedDoseU = FloatAdjusters.INSTANCE.getFLOOR2_BOLUS().apply(injected * AppConstant.INSULIN_UNIT_P); + bolusCurrent.setEndTimeSynced(BolusType.NOW, true); + pm.flushBolusCurrent(); + } + } + + public void updateExtBolusStopped(int injected) { + updateExtBolusStopped(injected, 0); + } + + public void updateExtBolusStopped(int injected, long suspendedTimestamp) { + BolusCurrent bolusCurrent = pm.getBolusCurrent(); + long extID = bolusCurrent.historyId(BolusType.EXT); + if (extID > 0 && !bolusCurrent.endTimeSynced(BolusType.EXT)) { +// long stopTime = (suspendedTimestamp > 0) ? suspendedTimestamp : System.currentTimeMillis(); +// float injectedDoseU = FloatAdjusters.INSTANCE.getFLOOR2_BOLUS().apply(injected * AppConstant.INSULIN_UNIT_P); + bolusCurrent.setEndTimeSynced(BolusType.EXT, true); + pm.flushBolusCurrent(); + } + } + + private long getPumpDuration(float doseU) { + if (doseU > 0) { + long pumpDuration = pm.getPatchConfig().getPumpDurationSmallMilli(); //todo + return (long) ((doseU / AppConstant.Companion.getBOLUS_UNIT_STEP()) * pumpDuration); + } + return 0L; + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/DeactivateTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/DeactivateTask.java new file mode 100644 index 0000000000..1299a99a6b --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/DeactivateTask.java @@ -0,0 +1,132 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager; +import info.nightscout.androidaps.plugins.pump.eopatch.core.code.BolusType; +import info.nightscout.androidaps.plugins.pump.eopatch.code.DeactivationStatus; + +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.DeActivation; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.BolusCurrent; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchLifecycleEvent; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.TempBasal; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +@Singleton +public class DeactivateTask extends TaskBase { + + @Inject + StopBasalTask stopBasalTask; + + @Inject + IPreferenceManager pm; + + private DeActivation DEACTIVATION; + + @Inject + public DeactivateTask() { + super(TaskFunc.DEACTIVATE); + DEACTIVATION = new DeActivation(); + } + + public Single run(boolean forced, long timeout) { + return isReadyCheckActivated() + .timeout(timeout, TimeUnit.MILLISECONDS) + .concatMapSingle(v -> + DEACTIVATION.start() + .doOnSuccess(this::checkResponse) + .observeOn(Schedulers.io()) + .doOnSuccess(response -> onDeactivated(false))) + .map(response -> DeactivationStatus.of(response.isSuccess(), forced)) + .firstOrError() + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())) + .onErrorResumeNext(e -> { + if (forced) { + try { + onDeactivated(true); + } catch (Exception t) { + aapsLogger.error(LTag.PUMPCOMM, e.getMessage()); + } + } + + return Single.just(DeactivationStatus.of(false, forced)); + }); + } + + private Observable isReadyCheckActivated() { + if (pm.getPatchConfig().isActivated()) { + enqueue(TaskFunc.UPDATE_CONNECTION); + + stopBasalTask.enqueue(); + + return isReady2(); + } + + return isReady(); + } + + /* Schedulers.io() */ + private void onDeactivated(boolean forced) throws SQLException { + synchronized (lock) { + patch.updateMacAddress(null, false); + + if (pm.getPatchConfig().getLifecycleEvent().isShutdown()) { + return; + } + + cleanUpRepository(); + + pm.getNormalBasalManager().updateForDeactivation(); + + pm.updatePatchLifeCycle(PatchLifecycleEvent.createShutdown()); + + } + } + + private void cleanUpRepository() throws SQLException { + updateNowBolusStopped(); + updateExtBolusStopped(); + updateTempBasalStopped(); + } + + private void updateTempBasalStopped() throws SQLException { + TempBasal tempBasal = pm.getTempBasalManager().getStartedBasal(); + + if (tempBasal != null) { + pm.getTempBasalManager().updateBasalStopped(); + pm.flushTempBasalManager(); + } + } + + /* copied from BolusTask. */ + private void updateNowBolusStopped() { + BolusCurrent bolusCurrent = pm.getBolusCurrent(); + long nowID = bolusCurrent.historyId(BolusType.NOW); + + if (nowID > 0 && !bolusCurrent.endTimeSynced(BolusType.NOW)) { + bolusCurrent.setEndTimeSynced(BolusType.NOW, true); + pm.flushBolusCurrent(); + } + } + + /* copied from BolusTask. */ + private void updateExtBolusStopped() { + BolusCurrent bolusCurrent = pm.getBolusCurrent(); + long extID = bolusCurrent.historyId(BolusType.EXT); + + if (extID > 0 && !bolusCurrent.endTimeSynced(BolusType.EXT)) { + bolusCurrent.setEndTimeSynced(BolusType.EXT, true); + pm.flushBolusCurrent(); + } + } + + +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/FetchAlarmTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/FetchAlarmTask.java new file mode 100644 index 0000000000..13a9326480 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/FetchAlarmTask.java @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.bus.RxBus; +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode; +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.IAlarmRegistry; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetErrorCodes; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.AeCodeResponse; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; + +@Singleton +public class FetchAlarmTask extends TaskBase { + + @Inject + RxBus rxBus; + + @Inject + IAlarmRegistry alarmRegistry; + + private GetErrorCodes ALARM_ALERT_ERROR_CODE_GET; + + @Inject + public FetchAlarmTask() { + super(TaskFunc.FETCH_ALARM); + ALARM_ALERT_ERROR_CODE_GET = new GetErrorCodes(); + } + + public Single getPatchAlarm() { + return isReady() + .concatMapSingle(v -> ALARM_ALERT_ERROR_CODE_GET.get()) + .doOnNext(this::checkResponse) + .firstOrError() + .doOnSuccess(aeCodeResponse -> alarmRegistry.add(aeCodeResponse.getAlarmCodes())) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + public synchronized void enqueue() { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = getPatchAlarm() + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/GetPatchInfoTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/GetPatchInfoTask.java new file mode 100644 index 0000000000..e84f9d649f --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/GetPatchInfoTask.java @@ -0,0 +1,116 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetFirmwareVersion; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetLOT; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetModelName; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetPumpDuration; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetSerialNumber; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetWakeUpTime; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.SetGlobalTime; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BaseResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.FirmwareVersionResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.LotNumberResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.ModelNameResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.PumpDurationResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.SerialNumberResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.WakeUpTimeResponse; + +import java.util.Arrays; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +@Singleton +public class GetPatchInfoTask extends TaskBase { + + @Inject + UpdateConnectionTask updateConnectionTask; + + private SetGlobalTime SET_GLOBAL_TIME; + private GetSerialNumber SERIAL_NUMBER_GET; + private GetLOT LOT_NUMBER_GET; + private GetFirmwareVersion FIRMWARE_VERSION_GET; + private GetWakeUpTime WAKE_UP_TIME_GET; + private GetPumpDuration PUMP_DURATION_GET; + private GetModelName GET_MODEL_NAME; + + @Inject + public GetPatchInfoTask() { + super(TaskFunc.GET_PATCH_INFO); + + SET_GLOBAL_TIME = new SetGlobalTime(); + SERIAL_NUMBER_GET = new GetSerialNumber(); + LOT_NUMBER_GET = new GetLOT(); + FIRMWARE_VERSION_GET = new GetFirmwareVersion(); + WAKE_UP_TIME_GET = new GetWakeUpTime(); + PUMP_DURATION_GET = new GetPumpDuration(); + GET_MODEL_NAME = new GetModelName(); + } + + public Single get() { + Single tasks = Single.concat(Arrays.asList( + SET_GLOBAL_TIME.set(), + SERIAL_NUMBER_GET.get().doOnSuccess(this::onSerialNumberResponse), + LOT_NUMBER_GET.get().doOnSuccess(this::onLotNumberResponse), + FIRMWARE_VERSION_GET.get().doOnSuccess(this::onFirmwareResponse), + WAKE_UP_TIME_GET.get().doOnSuccess(this::onWakeupTimeResponse), + PUMP_DURATION_GET.get().doOnSuccess(this::onPumpDurationResponse), + GET_MODEL_NAME.get().doOnSuccess(this::onModelNameResponse))) + .map(BaseResponse::isSuccess) + .filter(v -> !v) // fail 시 false 가 아래로 내려간다. + .first(true); + + return isReady() + .concatMapSingle(it -> tasks) + .firstOrError() +// .flatMap(v -> updateConnectionTask.update()).map(v -> true) + .observeOn(Schedulers.io()) + .doOnSuccess(this::onPatchWakeupSuccess) + .doOnError(this::onPatchWakeupFailed) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private void onSerialNumberResponse(SerialNumberResponse v) { + pm.getPatchConfig().setPatchSerialNumber(v.getSerialNumber()); + } + + private void onLotNumberResponse(LotNumberResponse v) { + pm.getPatchConfig().setPatchLotNumber(v.getLotNumber()); + } + + private void onFirmwareResponse(FirmwareVersionResponse v) { + pm.getPatchConfig().setPatchFirmwareVersion(v.getFirmwareVersionString()); + } + + private void onWakeupTimeResponse(WakeUpTimeResponse v) { + pm.getPatchConfig().setPatchWakeupTimestamp(v.getTimeInMillis()); + } + + private void onPumpDurationResponse(PumpDurationResponse v) { + pm.getPatchConfig().setPumpDurationLargeMilli(v.getDurationL() * 100); // 0.1 초 단위 + pm.getPatchConfig().setPumpDurationMediumMilli(v.getDurationM() * 100); + pm.getPatchConfig().setPumpDurationSmallMilli(v.getDurationS() * 100); + } + + private void onModelNameResponse(ModelNameResponse modelNameResponse) { + pm.getPatchConfig().setPatchModelName(modelNameResponse.getModelName()); + } + + /* Schedulers.io() */ + private void onPatchWakeupSuccess(Boolean result) { + synchronized (lock) { + pm.flushPatchConfig(); + } + } + + /* Schedulers.io() */ + private void onPatchWakeupFailed(Throwable e) { + patch.setSeq(-1); + pm.getPatchConfig().updateDeactivated(); + pm.flushPatchConfig(); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/InfoReminderTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/InfoReminderTask.java new file mode 100644 index 0000000000..f2a32536e8 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/InfoReminderTask.java @@ -0,0 +1,48 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.InfoReminderSet; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.PatchBooleanResponse; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.Single; + +@Singleton +public class InfoReminderTask extends TaskBase { + + @Inject + IPreferenceManager pm; + + private InfoReminderSet INFO_REMINDER_SET; + + @Inject + public InfoReminderTask() { + super(TaskFunc.INFO_REMINDER); + INFO_REMINDER_SET = new InfoReminderSet(); + } + + /* alert delay 사용안함 */ + public Single set(boolean infoReminder) { + return isReady() + .concatMapSingle(v -> INFO_REMINDER_SET.set(infoReminder)) + .doOnNext(this::checkResponse) + .firstOrError() + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + public synchronized void enqueue() { + + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = set(pm.getPatchConfig().getInfoReminder()) + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/InternalSuspendedTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/InternalSuspendedTask.java new file mode 100644 index 0000000000..9e73ead46e --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/InternalSuspendedTask.java @@ -0,0 +1,183 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import android.os.SystemClock; + +import info.nightscout.androidaps.interfaces.PumpSync; +import info.nightscout.androidaps.logging.UserEntryLogger; +import info.nightscout.androidaps.utils.userEntry.UserEntryMapper; +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.define.IPatchConstant; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.interfaces.CommandQueue; +import info.nightscout.shared.logging.AAPSLogger; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BolusStop; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetInternalSuspendTime; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.TempBasalScheduleStop; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.PatchInternalSuspendTimeResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState; +import info.nightscout.androidaps.queue.Callback; +import info.nightscout.androidaps.queue.commands.Command; + +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.subjects.BehaviorSubject; + +@Singleton +public class InternalSuspendedTask extends BolusTask { + + @Inject CommandQueue commandQueue; + @Inject AAPSLogger aapsLogger; + @Inject PumpSync pumpSync; + @Inject UserEntryLogger uel; + + private GetInternalSuspendTime INTERNAL_SUSPEND_TIME_GET; + private BolusStop BOLUS_STOP; + private TempBasalScheduleStop TEMP_BASAL_SCHEDULE_STOP; + private BehaviorSubject bolusCheckSubject = BehaviorSubject.create(); + private BehaviorSubject exbolusCheckSubject = BehaviorSubject.create(); + private BehaviorSubject basalCheckSubject = BehaviorSubject.create(); + + @Inject + public InternalSuspendedTask() { + super(TaskFunc.INTERNAL_SUSPEND); + + INTERNAL_SUSPEND_TIME_GET = new GetInternalSuspendTime(); + BOLUS_STOP = new BolusStop(); + TEMP_BASAL_SCHEDULE_STOP = new TempBasalScheduleStop(); + } + + private Observable getBolusSebject(){ + return bolusCheckSubject.hide(); + } + + private Observable getExbolusSebject(){ + return exbolusCheckSubject.hide(); + } + + private Observable getBasalSebject(){ + return basalCheckSubject.hide(); + } + + public Single start(boolean isNowBolusActive, boolean isExtBolusActive, boolean isTempBasalActive) { + PatchState patchState = pm.getPatchState(); + + if (isNowBolusActive || isExtBolusActive) { + enqueue(TaskFunc.READ_BOLUS_FINISH_TIME); + } + + if (commandQueue.isRunning(Command.CommandType.BOLUS)) { + uel.log(UserEntryMapper.Action.CANCEL_BOLUS, UserEntryMapper.Sources.EOPatch2); + commandQueue.cancelAllBoluses(); + SystemClock.sleep(650); + } + bolusCheckSubject.onNext(true); + + if (pumpSync.expectedPumpState().getExtendedBolus() != null) { + uel.log(UserEntryMapper.Action.CANCEL_EXTENDED_BOLUS, UserEntryMapper.Sources.EOPatch2); + commandQueue.cancelExtended(new Callback() { + @Override + public void run() { + exbolusCheckSubject.onNext(true); + } + }); + }else{ + exbolusCheckSubject.onNext(true); + } + + if (pumpSync.expectedPumpState().getTemporaryBasal() != null) { + uel.log(UserEntryMapper.Action.CANCEL_TEMP_BASAL, UserEntryMapper.Sources.EOPatch2); + commandQueue.cancelTempBasal(true, new Callback() { + @Override + public void run() { + basalCheckSubject.onNext(true); + } + }); + }else{ + basalCheckSubject.onNext(true); + } + + return Observable.zip(getBolusSebject(), getExbolusSebject(), getBasalSebject(), (bolusReady, exbolusReady, basalReady) -> { + return (bolusReady && exbolusReady && basalReady); + }) + .filter(ready -> ready) + .flatMap(v -> isReady()) + .concatMapSingle(v -> getInternalSuspendTime()) + .firstOrError() + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private Single getInternalSuspendTime() { + return INTERNAL_SUSPEND_TIME_GET.get() + .doOnSuccess(this::checkResponse) + .map(PatchInternalSuspendTimeResponse::getTotalSeconds); + } + + private Single stopNowBolus(long suspendTime, boolean isNowBolusActive) { + if (isNowBolusActive) { + long suspendedTimestamp = pm.getPatchConfig().getPatchWakeupTimestamp() + suspendTime; + + return BOLUS_STOP.stop(IPatchConstant.NOW_BOLUS_ID) + .doOnSuccess(this::checkResponse) + .doOnSuccess(v -> onNowBolusStopped(v.getInjectedBolusAmount(), suspendedTimestamp)) + .map(v -> suspendTime); + } + + return Single.just(suspendTime); + } + + private Single stopExtBolus(long suspendTime, boolean isExtBolusActive) { + if (isExtBolusActive) { + long suspendedTimestamp = pm.getPatchConfig().getPatchWakeupTimestamp() + suspendTime; + + return BOLUS_STOP.stop(IPatchConstant.EXT_BOLUS_ID) + .doOnSuccess(this::checkResponse) + .doOnSuccess(v -> onExtBolusStopped(v.getInjectedBolusAmount(), suspendedTimestamp)) + .map(v -> suspendTime); + } + + return Single.just(suspendTime); + } + + private Single stopTempBasal(long suspendTime, boolean isTempBasalActive) { + if (isTempBasalActive) { + return TEMP_BASAL_SCHEDULE_STOP.stop() + .doOnSuccess(this::checkResponse) + .doOnSuccess(v -> onTempBasalCanceled()) + .map(v -> suspendTime); + } + + return Single.just(suspendTime); + } + + private void onNowBolusStopped(int injectedBolusAmount, long suspendedTimestamp) { + updateNowBolusStopped(injectedBolusAmount, suspendedTimestamp); + } + + private void onExtBolusStopped(int injectedBolusAmount, long suspendedTimestamp) { + updateExtBolusStopped(injectedBolusAmount, suspendedTimestamp); + } + + private void onTempBasalCanceled() { + pm.getTempBasalManager().updateBasalStopped(); + pm.flushTempBasalManager(); + } + + public synchronized void enqueue(boolean isNowBolusActive, boolean isExtBolusActive, boolean isTempBasalActive) { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = start(isNowBolusActive, isExtBolusActive, isTempBasalActive) + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(v -> { + bolusCheckSubject.onNext(false); + exbolusCheckSubject.onNext(false); + basalCheckSubject.onNext(false); + }); + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/NeedleSensingTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/NeedleSensingTask.java new file mode 100644 index 0000000000..d924bc3659 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/NeedleSensingTask.java @@ -0,0 +1,51 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode; +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.IAlarmRegistry; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.StartNeedleCheck; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.UpdateConnection; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState; +import info.nightscout.shared.logging.LTag; +import io.reactivex.Single; + +@Singleton +public class NeedleSensingTask extends TaskBase { + + @Inject + IAlarmRegistry alarmRegistry; + + StartNeedleCheck START_NEEDLE_CHECK; + UpdateConnection UPDATE_CONNECTION; + + @Inject + public NeedleSensingTask() { + super(TaskFunc.NEEDLE_SENSING); + START_NEEDLE_CHECK = new StartNeedleCheck(); + UPDATE_CONNECTION = new UpdateConnection(); + } + + public Single start() { + + return isReady() + .concatMapSingle(v -> START_NEEDLE_CHECK.start()) + .doOnNext(this::checkResponse) + .concatMapSingle(v -> UPDATE_CONNECTION.get()) + .doOnNext(this::checkResponse) + .map(updateConnectionResponse -> PatchState.Companion.create(updateConnectionResponse.getPatchState(), System.currentTimeMillis())) + .doOnNext(this::onResponse) + .map(patchState -> !patchState.isNeedNeedleSensing()) + .firstOrError() + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private void onResponse(PatchState v) { + if (v.isNeedNeedleSensing()) { + alarmRegistry.add(AlarmCode.A016, 0, false).subscribe(); + } else { + alarmRegistry.remove(AlarmCode.A016).subscribe(); + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/PauseBasalTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/PauseBasalTask.java new file mode 100644 index 0000000000..ebd32d3aec --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/PauseBasalTask.java @@ -0,0 +1,225 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + + +import android.os.SystemClock; + +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.interfaces.CommandQueue; +import info.nightscout.androidaps.interfaces.PumpSync; +import info.nightscout.androidaps.logging.UserEntryLogger; +import info.nightscout.androidaps.utils.userEntry.UserEntryMapper; +import info.nightscout.shared.logging.AAPSLogger; +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode; +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.IAlarmRegistry; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager; +import info.nightscout.androidaps.plugins.pump.eopatch.core.define.IPatchConstant; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BasalPause; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BolusStop; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.TempBasalScheduleStop; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.PatchBooleanResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.TempBasal; +import info.nightscout.androidaps.queue.Callback; +import info.nightscout.androidaps.queue.commands.Command; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.subjects.BehaviorSubject; + +@Singleton +public class PauseBasalTask extends BolusTask { + @Inject IAlarmRegistry alarmRegistry; + @Inject IPreferenceManager pm; + @Inject CommandQueue commandQueue; + @Inject AAPSLogger aapsLogger; + @Inject PumpSync pumpSync; + @Inject UserEntryLogger uel; + + private BasalPause BASAL_PAUSE; + private BolusStop BOLUS_STOP; + private TempBasalScheduleStop TEMP_BASAL_SCHEDULE_STOP; + + private BehaviorSubject bolusCheckSubject = BehaviorSubject.create(); + private BehaviorSubject exbolusCheckSubject = BehaviorSubject.create(); + private BehaviorSubject basalCheckSubject = BehaviorSubject.create(); + + + @Inject + public PauseBasalTask() { + super(TaskFunc.PAUSE_BASAL); + + BASAL_PAUSE = new BasalPause(); + BOLUS_STOP = new BolusStop(); + TEMP_BASAL_SCHEDULE_STOP = new TempBasalScheduleStop(); + } + + private Observable getBolusSebject(){ + return bolusCheckSubject.hide(); + } + + private Observable getExbolusSebject(){ + return exbolusCheckSubject.hide(); + } + + private Observable getBasalSebject(){ + return basalCheckSubject.hide(); + } + + public Single pause(float pauseDurationHour, long pausedTimestamp, @Nullable AlarmCode alarmCode) { + PatchState patchState = pm.getPatchState(); + + if(patchState.isNormalBasalPaused()) + return Single.just(new PatchBooleanResponse(true)); + + enqueue(TaskFunc.UPDATE_CONNECTION); + + if (commandQueue.isRunning(Command.CommandType.BOLUS)) { + uel.log(UserEntryMapper.Action.CANCEL_BOLUS, UserEntryMapper.Sources.EOPatch2); + commandQueue.cancelAllBoluses(); + SystemClock.sleep(650); + } + bolusCheckSubject.onNext(true); + + if (pumpSync.expectedPumpState().getExtendedBolus() != null) { + uel.log(UserEntryMapper.Action.CANCEL_EXTENDED_BOLUS, UserEntryMapper.Sources.EOPatch2); + commandQueue.cancelExtended(new Callback() { + @Override + public void run() { + exbolusCheckSubject.onNext(true); + } + }); + }else{ + exbolusCheckSubject.onNext(true); + } + + if (pumpSync.expectedPumpState().getTemporaryBasal() != null) { + uel.log(UserEntryMapper.Action.CANCEL_TEMP_BASAL, UserEntryMapper.Sources.EOPatch2); + commandQueue.cancelTempBasal(true, new Callback() { + @Override + public void run() { + basalCheckSubject.onNext(true); + } + }); + }else{ + basalCheckSubject.onNext(true); + } + + return Observable.zip(getBolusSebject(), getExbolusSebject(), getBasalSebject(), (bolusReady, exbolusReady, basalReady) -> { + return (bolusReady && exbolusReady && basalReady); + }) + .filter(ready -> ready) + .flatMap(v -> isReady()) + .concatMapSingle(v -> getSuspendedTime(pausedTimestamp, alarmCode)) + .concatMapSingle(suspendedTimestamp -> pauseBasal(pauseDurationHour, suspendedTimestamp, alarmCode)) + .firstOrError() + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private Single getSuspendedTime(long pausedTimestamp, @Nullable AlarmCode alarmCode) { + return Single.just(pausedTimestamp); + } + + private Single stopNowBolus(long pausedTimestamp, boolean isNowBolusActive) { + if (isNowBolusActive) { + return BOLUS_STOP.stop(IPatchConstant.NOW_BOLUS_ID) + .doOnSuccess(this::checkResponse) + .doOnSuccess(v -> onNowBolusStopped(v.getInjectedBolusAmount(), pausedTimestamp)) + .map(v -> pausedTimestamp); + } + + return Single.just(pausedTimestamp); + } + + private Single stopExtBolus(long pausedTimestamp, boolean isExtBolusActive) { + if (isExtBolusActive) { + return BOLUS_STOP.stop(IPatchConstant.EXT_BOLUS_ID) + .doOnSuccess(this::checkResponse) + .doOnSuccess(v -> onExtBolusStopped(v.getInjectedBolusAmount(), pausedTimestamp)) + .map(v -> pausedTimestamp); + } + + return Single.just(pausedTimestamp); + } + + private Single stopTempBasal(long pausedTimestamp, boolean isTempBasalActive) { + if (isTempBasalActive) { + return TEMP_BASAL_SCHEDULE_STOP.stop() + .doOnSuccess(this::checkResponse) + .doOnSuccess(v -> onTempBasalCanceled(pausedTimestamp)) + .map(v -> pausedTimestamp); + } + + return Single.just(pausedTimestamp); + } + + private Single pauseBasal(float pauseDurationHour, long suspendedTimestamp, @Nullable AlarmCode alarmCode) { + if(alarmCode == null) { + return BASAL_PAUSE.pause(pauseDurationHour) + .doOnSuccess(this::checkResponse) + .doOnSuccess(v -> onBasalPaused(pauseDurationHour, suspendedTimestamp, null)); + } + + // 정지 알람 발생 시 basal pause 커맨드 전달하지 않음 - 주입 정지 이력만 생성 + onBasalPaused(pauseDurationHour, suspendedTimestamp, alarmCode); + + return Single.just(new PatchBooleanResponse(true)); + } + + private void onBasalPaused(float pauseDurationHour, long suspendedTimestamp, @Nullable AlarmCode alarmCode) { + if (!pm.getNormalBasalManager().isSuspended()) { + String strCode = (alarmCode != null) ? alarmCode.name() : null; + + if (alarmCode != null) { + pm.getPatchConfig().updateNormalBasalPausedSilently(); + } + else { + pm.getPatchConfig().updateNormalBasalPaused(pauseDurationHour); + } + pm.getNormalBasalManager().updateBasalSuspended(); + + pm.flushNormalBasalManager(); + pm.flushPatchConfig(); + + if((alarmCode == null || alarmCode.getType() == AlarmCode.TYPE_ALERT) && pauseDurationHour != 0) + alarmRegistry.add(AlarmCode.B001, TimeUnit.MINUTES.toMillis((long)(pauseDurationHour * 60)), false).subscribe(); + } + + enqueue(TaskFunc.UPDATE_CONNECTION); + } + + private void onNowBolusStopped(int injectedBolusAmount, long suspendedTimestamp) { + updateNowBolusStopped(injectedBolusAmount, suspendedTimestamp); + } + + private void onExtBolusStopped(int injectedBolusAmount, long suspendedTimestamp) { + updateExtBolusStopped(injectedBolusAmount, suspendedTimestamp); + } + + private void onTempBasalCanceled(long suspendedTimestamp) { + TempBasal tempBasal = pm.getTempBasalManager().getStartedBasal(); + + if (tempBasal != null) { + pm.getTempBasalManager().updateBasalStopped(); + pm.flushTempBasalManager(); + } + } + + public synchronized void enqueue(float pauseDurationHour, long pausedTime, @Nullable AlarmCode alarmCode) { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = pause(pauseDurationHour, pausedTime, alarmCode) + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(v -> { + bolusCheckSubject.onNext(false); + exbolusCheckSubject.onNext(false); + basalCheckSubject.onNext(false); + }); + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/PrimingTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/PrimingTask.java new file mode 100644 index 0000000000..57d318ac67 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/PrimingTask.java @@ -0,0 +1,55 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.StartPriming; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.UpdateConnection; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState; +import io.reactivex.Observable; + +@Singleton +public class PrimingTask extends TaskBase { + + private UpdateConnection UPDATE_CONNECTION; + private StartPriming START_PRIMING; + + @Inject + public PrimingTask() { + super(TaskFunc.PRIMING); + + UPDATE_CONNECTION = new UpdateConnection(); + START_PRIMING = new StartPriming(); + } + + public Observable start(long count) { + return isReady().concatMapSingle(v -> START_PRIMING.start()) + .doOnNext(this::checkResponse) + .flatMap(v -> observePrimingSuccess(count)) + .takeUntil(value -> (value == count)) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private Observable observePrimingSuccess(long count) { + + return Observable.merge( + Observable.interval(1, TimeUnit.SECONDS).take(count + 10) + .map(v -> v * 3) // 현재 20초 니깐 60 정도에서 꽉 채워짐. *4 도 괜찮을 듯. + .doOnNext(v -> { + if (v >= count) { + throw new Exception("Priming failed"); + } + }), // 프로그래스바 용. + + Observable.interval(3, TimeUnit.SECONDS) + .concatMapSingle(v -> UPDATE_CONNECTION.get()) + .map(response -> PatchState.Companion.create(response.getPatchState(), System.currentTimeMillis())) + .filter(patchState -> patchState.isPrimingSuccess()) + .map(result -> count) // 프라이밍 체크 용 성공시 count 값 리턴 + ); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ReadBolusFinishTimeTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ReadBolusFinishTimeTask.java new file mode 100644 index 0000000000..6f9d1a212a --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ReadBolusFinishTimeTask.java @@ -0,0 +1,64 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BolusFinishTimeGet; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BolusFinishTimeResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.code.BolusType; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.BolusCurrent; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState; +import io.reactivex.Single; + +@Singleton +public class ReadBolusFinishTimeTask extends BolusTask { + + private BolusFinishTimeGet BOLUS_FINISH_TIME_GET; + + @Inject + public ReadBolusFinishTimeTask() { + super(TaskFunc.READ_BOLUS_FINISH_TIME); + BOLUS_FINISH_TIME_GET = new BolusFinishTimeGet(); + } + + Single read() { + return isReady() + .concatMapSingle(v -> BOLUS_FINISH_TIME_GET.get()) + .firstOrError() + .doOnSuccess(this::checkResponse) + .doOnSuccess(this::onResponse) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + void onResponse(BolusFinishTimeResponse response) { + PatchState patchState = pm.getPatchState(); + BolusCurrent bolusCurrent = pm.getBolusCurrent(); + long nowHistoryID = bolusCurrent.historyId(BolusType.NOW); + long extHistoryID = bolusCurrent.historyId(BolusType.EXT); + + if (nowHistoryID > 0 && patchState.isBolusDone(BolusType.NOW) && response.getNowBolusFinishTime() > 0) { + bolusCurrent.setEndTimeSynced(BolusType.NOW, true); + enqueue(TaskFunc.STOP_NOW_BOLUS); + } + + if (extHistoryID > 0 && patchState.isBolusDone(BolusType.EXT) && response.getExtBolusFinishTime() > 0) { + bolusCurrent.setEndTimeSynced(BolusType.EXT, true); + enqueue(TaskFunc.STOP_EXT_BOLUS); + } + + pm.flushBolusCurrent(); + } + + public synchronized void enqueue() { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = read() + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ReadTempBasalFinishTimeTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ReadTempBasalFinishTimeTask.java new file mode 100644 index 0000000000..0d42506fa0 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ReadTempBasalFinishTimeTask.java @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.TempBasalFinishTimeGet; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.TempBasalFinishTimeResponse; +import io.reactivex.Single; + +@Singleton +public class ReadTempBasalFinishTimeTask extends TaskBase { + + private TempBasalFinishTimeGet TEMP_BASAL_FINISH_TIME_GET; + + @Inject + public ReadTempBasalFinishTimeTask() { + super(TaskFunc.READ_TEMP_BASAL_FINISH_TIME); + TEMP_BASAL_FINISH_TIME_GET = new TempBasalFinishTimeGet(); + } + + public Single read() { + return isReady() + .concatMapSingle(v -> TEMP_BASAL_FINISH_TIME_GET.get()) + .firstOrError() + .doOnSuccess(this::checkResponse) + .doOnSuccess(this::onResponse) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private void onResponse(TempBasalFinishTimeResponse response) { + } + + public synchronized void enqueue() { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = read() + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ResumeBasalTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ResumeBasalTask.java new file mode 100644 index 0000000000..a25febb404 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ResumeBasalTask.java @@ -0,0 +1,64 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.AAPSLogger; +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.PatchStateManager; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BasalResume; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BaseResponse; + +import java.sql.SQLException; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.PatchBooleanResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.NormalBasal; +import io.reactivex.Single; + +@Singleton +public class ResumeBasalTask extends TaskBase { + @Inject + StartNormalBasalTask startNormalBasalTask; + + @Inject + PatchStateManager patchStateManager; + + private BasalResume BASAL_RESUME; + + @Inject + public ResumeBasalTask() { + super(TaskFunc.RESUME_BASAL); + BASAL_RESUME = new BasalResume(); + } + + public synchronized Single resume() { + + if (pm.getPatchConfig().getNeedSetBasalSchedule()) { + NormalBasal normalBasal = pm.getNormalBasalManager().getNormalBasal(); + + if (normalBasal != null) { + return startNormalBasalTask.start(normalBasal, true); + } + } + + return isReady().concatMapSingle(v -> BASAL_RESUME.resume()) + .doOnNext(this::checkResponse) + .firstOrError() + .doOnSuccess(v -> onResumeResponse(v)) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private void onResumeResponse(PatchBooleanResponse v) throws SQLException { + if (v.isSuccess()) { + patchStateManager.onBasalResumed(v.getTimestamp() + 1000); + } + enqueue(TaskFunc.UPDATE_CONNECTION); + } + + @Override + protected void preCondition() throws Exception { + checkPatchActivated(); + checkPatchConnected(); + } + +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SelfTestTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SelfTestTask.java new file mode 100644 index 0000000000..cafe9e1911 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SelfTestTask.java @@ -0,0 +1,67 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.PatchSelfTestResult; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetGlobalTime; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetTemperature; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetVoltageLevelB4Priming; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BatteryVoltageLevelPairingResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.GlobalTimeResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.TemperatureResponse; + +import java.util.Arrays; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.Single; + +@Singleton +public class SelfTestTask extends TaskBase { + + private GetTemperature TEMPERATURE_GET; + private GetVoltageLevelB4Priming BATTERY_LEVEL_GET_BEFORE_PRIMING; + private GetGlobalTime GET_GLOBAL_TIME; + + @Inject + public SelfTestTask() { + super(TaskFunc.SELF_TEST); + + TEMPERATURE_GET = new GetTemperature(); + BATTERY_LEVEL_GET_BEFORE_PRIMING = new GetVoltageLevelB4Priming(); + GET_GLOBAL_TIME = new GetGlobalTime(); + } + + public Single start() { + Single tasks = Single.concat(Arrays.asList( + TEMPERATURE_GET.get() + .map(TemperatureResponse::getResult) + .doOnSuccess(this::onTemperatureResult), + BATTERY_LEVEL_GET_BEFORE_PRIMING.get() + .map(BatteryVoltageLevelPairingResponse::getResult) + .doOnSuccess(this::onBatteryResult), + GET_GLOBAL_TIME.get(false) + .map(GlobalTimeResponse::getResult) + .doOnSuccess(this::onTimeResult))) + .filter(result -> result != PatchSelfTestResult.TEST_SUCCESS) + .first(PatchSelfTestResult.TEST_SUCCESS); + + return isReady() + .concatMapSingle(v -> tasks) + .firstOrError() + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private void onTemperatureResult(PatchSelfTestResult patchSelfTestResult) { + if (patchSelfTestResult != PatchSelfTestResult.TEST_SUCCESS) { + } + } + + private void onBatteryResult(PatchSelfTestResult patchSelfTestResult) { + if (patchSelfTestResult != PatchSelfTestResult.TEST_SUCCESS) { + } + } + + private void onTimeResult(PatchSelfTestResult patchSelfTestResult) { + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SetGlobalTimeTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SetGlobalTimeTask.java new file mode 100644 index 0000000000..0626764c8c --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SetGlobalTimeTask.java @@ -0,0 +1,75 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.GetGlobalTime; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.SetGlobalTime; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.GlobalTimeResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.PatchBooleanResponse; + +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.Single; + +@Singleton +public class SetGlobalTimeTask extends TaskBase { + + private SetGlobalTime SET_GLOBAL_TIME; + private GetGlobalTime GET_GLOBAL_TIME; + + @Inject + public SetGlobalTimeTask() { + super(TaskFunc.SET_GLOBAL_TIME); + + SET_GLOBAL_TIME = new SetGlobalTime(); + GET_GLOBAL_TIME = new GetGlobalTime(); + } + + public Single set() { + return isReady() + .concatMapSingle(v -> GET_GLOBAL_TIME.get(false)) + .doOnNext(this::checkResponse) + .doOnNext(this::checkPatchTime) + .concatMapSingle(v -> SET_GLOBAL_TIME.set()) + .doOnNext(this::checkResponse) + .firstOrError() + .doOnSuccess(v -> onSuccess()) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private boolean checkPatchTime(GlobalTimeResponse response) throws Exception { + + long newMilli = System.currentTimeMillis(); + long oldMilli = response.getGlobalTimeInMilli(); + long oldOffset = response.getTimeZoneOffset(); + int offset = TimeZone.getDefault().getOffset(newMilli); + int minutes = (int) TimeUnit.MILLISECONDS.toMinutes(offset); + // TimeZoneOffset (8bit / signed): 타임존 offset 15분 단위을 1로 환산, Korea 의 경우 36값(+9:00) + int newOffset = minutes / 15; + + long diff = Math.abs(oldMilli - newMilli); + + if (diff > 60000 || oldOffset != newOffset) { + aapsLogger.debug(LTag.PUMPCOMM, String.format("checkPatchTime %s %s %s", diff, oldOffset, newOffset)); + return true; + } + + throw new Exception("No time set required"); + } + + public synchronized void enqueue() { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = set() + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(v -> {}, e -> {}); // Exception 을 사용하기에... + } + } + + private void onSuccess() { + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SetLowReservoirTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SetLowReservoirTask.java new file mode 100644 index 0000000000..14fd2510bb --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SetLowReservoirTask.java @@ -0,0 +1,57 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.SetLowReservoirLevelAndExpireAlert; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.PatchBooleanResponse; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.Single; + +@Singleton +public class SetLowReservoirTask extends TaskBase { + + @Inject + IPreferenceManager pm; + + private SetLowReservoirLevelAndExpireAlert SET_LOW_RESERVOIR_N_EXPIRE_ALERT; + + @Inject + public SetLowReservoirTask() { + super(TaskFunc.LOW_RESERVOIR); + SET_LOW_RESERVOIR_N_EXPIRE_ALERT = new SetLowReservoirLevelAndExpireAlert(); + } + + public Single set(int doseUnit, int hours) { + return isReady() + .concatMapSingle(v -> SET_LOW_RESERVOIR_N_EXPIRE_ALERT.set( + doseUnit, + hours)) + .doOnNext(this::checkResponse) + .firstOrError() + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + public synchronized void enqueue() { + + int alertTime = pm.getPatchConfig().getPatchExpireAlertTime(); + int alertSetting = pm.getPatchConfig().getLowReservoirAlertAmount(); + + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = set(alertSetting, alertTime) + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } + + @Override + protected void preCondition() throws Exception { + checkPatchConnected(); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartBondTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartBondTask.java new file mode 100644 index 0000000000..4b4e9e02ef --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartBondTask.java @@ -0,0 +1,58 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import android.bluetooth.BluetoothDevice; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.StartBonding; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.Single; + +import static info.nightscout.androidaps.plugins.pump.eopatch.core.api.StartBonding.OPTION_NUMERIC; + +/** + * (주의) API 호출 후 본딩을 위해서 밑단 연결이 끊어짐. + */ +@Singleton +public class StartBondTask extends TaskBase { + private StartBonding START_BOND; + + @Inject + public StartBondTask() { + super(TaskFunc.START_BOND); + START_BOND = new StartBonding(); + } + + public Single start(String mac) { + prefSetMacAddress(mac); + patch.updateMacAddress(mac, false); + + return isReady() + .concatMapSingle(v -> START_BOND.start(OPTION_NUMERIC)) + .doOnNext(this::checkResponse) + .concatMap(response -> patch.observeBondState()) + .doOnNext(state -> { + if(state == BluetoothDevice.BOND_NONE) throw new Exception(); + }) + .filter(result -> result == BluetoothDevice.BOND_BONDED) + .map(result -> true) + .timeout(60, TimeUnit.SECONDS) + .doOnNext(v -> prefSetMacAddress(mac)) + .doOnError(e -> { + prefSetMacAddress(""); + aapsLogger.error(LTag.PUMPCOMM, e.getMessage()); + }) + .firstOrError(); + } + + private synchronized void prefSetMacAddress(String mac) { + pm.getPatchConfig().setMacAddress(mac); + } +} + + + diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartCalcBolusTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartCalcBolusTask.java new file mode 100644 index 0000000000..6cdbb99329 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartCalcBolusTask.java @@ -0,0 +1,62 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.code.BolusExDuration; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BolusStart; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.ComboBolusStart; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.ExtBolusStart; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BolusResponse; +import io.reactivex.Single; + +@Singleton +public class StartCalcBolusTask extends BolusTask { + + private BolusStart NOW_BOLUS_START; + private ExtBolusStart EXT_BOLUS_START; + private ComboBolusStart COMBO_BOLUS_START; + + @Inject + public StartCalcBolusTask() { + super(TaskFunc.START_CALC_BOLUS); + + NOW_BOLUS_START = new BolusStart(); + EXT_BOLUS_START = new ExtBolusStart(); + COMBO_BOLUS_START = new ComboBolusStart(); + } + + public Single start(DetailedBolusInfo detailedBolusInfo) { + return isReady().concatMapSingle(v -> startBolusImpl((float)detailedBolusInfo.insulin, 0f, BolusExDuration.OFF)) + .doOnNext(this::checkResponse) + .firstOrError() + .doOnSuccess(v -> onSuccess((float)detailedBolusInfo.insulin, (float)detailedBolusInfo.insulin, 0f, BolusExDuration.OFF)) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private Single startBolusImpl(float nowDoseU, float exDoseU, + BolusExDuration exDuration) { + if (nowDoseU > 0 && exDoseU > 0) { + return COMBO_BOLUS_START.start(nowDoseU, exDoseU, exDuration.getMinute()); + } else if (exDoseU > 0) { + return EXT_BOLUS_START.start(exDoseU, exDuration.getMinute()); + } else { + return NOW_BOLUS_START.start(nowDoseU); + } + } + + private void onSuccess(float nowDoseU, float correctionBolus, float exDoseU, BolusExDuration exDuration) { + onCalcBolusStarted(nowDoseU, correctionBolus, exDoseU, exDuration); + enqueue(TaskFunc.UPDATE_CONNECTION); + } + + @Override + protected void preCondition() throws Exception { + //checkPatchActivated(); + checkPatchConnected(); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartNormalBasalTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartNormalBasalTask.java new file mode 100644 index 0000000000..d7a232e14b --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartNormalBasalTask.java @@ -0,0 +1,55 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.PatchStateManager; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BasalScheduleSetBig; + +import java.sql.SQLException; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BasalScheduleSetResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.NormalBasal; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +@Singleton +public class StartNormalBasalTask extends TaskBase { + + private BasalScheduleSetBig BASAL_SCHEDULE_SET_BIG; + + @Inject + PatchStateManager patchStateManager; + + @Inject + public StartNormalBasalTask() { + super(TaskFunc.START_NORMAL_BASAL); + BASAL_SCHEDULE_SET_BIG = new BasalScheduleSetBig(); + } + + public Single start(NormalBasal basal, boolean resume) { + return isReady().concatMapSingle(v -> startJob(basal, resume)).firstOrError(); + } + + public Single startJob(NormalBasal basal, boolean resume) { + return BASAL_SCHEDULE_SET_BIG.set(basal.getDoseUnitPerSegmentArray()) + .doOnSuccess(this::checkResponse) + .observeOn(Schedulers.io()) + .doOnSuccess(v -> onStartNormalBasalResponse(v, basal, resume)) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private void onStartNormalBasalResponse(BasalScheduleSetResponse response, + NormalBasal basal, boolean resume) throws SQLException { + + long timeStamp = response.getTimestamp(); + patchStateManager.onBasalStarted(basal, timeStamp+1000); + enqueue(TaskFunc.UPDATE_CONNECTION); + } + + @Override + protected void preCondition() throws Exception { + checkPatchConnected(); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartQuickBolusTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartQuickBolusTask.java new file mode 100644 index 0000000000..87967f8da2 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartQuickBolusTask.java @@ -0,0 +1,62 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.code.BolusExDuration; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BolusStart; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.ComboBolusStart; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.ExtBolusStart; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BolusResponse; +import io.reactivex.Single; + +@Singleton +public class StartQuickBolusTask extends BolusTask { + + private BolusStart NOW_BOLUS_START; + private ExtBolusStart EXT_BOLUS_START; + private ComboBolusStart COMBO_BOLUS_START; + + @Inject + public StartQuickBolusTask() { + super(TaskFunc.START_QUICK_BOLUS); + + NOW_BOLUS_START = new BolusStart(); + EXT_BOLUS_START = new ExtBolusStart(); + COMBO_BOLUS_START = new ComboBolusStart(); + } + + public Single start(float nowDoseU, float exDoseU, + BolusExDuration exDuration) { + return isReady().concatMapSingle(v -> startBolusImpl(nowDoseU, exDoseU, exDuration)) + .doOnNext(this::checkResponse) + .firstOrError() + .doOnSuccess(v -> onSuccess(nowDoseU, exDoseU, exDuration)) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private Single startBolusImpl(float nowDoseU, float exDoseU, + BolusExDuration exDuration) { + if (nowDoseU > 0 && exDoseU > 0) { + return COMBO_BOLUS_START.start(nowDoseU, exDoseU, exDuration.getMinute()); + } else if (exDoseU > 0) { + return EXT_BOLUS_START.start(exDoseU, exDuration.getMinute()); + } else { + return NOW_BOLUS_START.start(nowDoseU); + } + } + + private void onSuccess(float nowDoseU, float exDoseU, BolusExDuration exDuration) { + onQuickBolusStarted(nowDoseU, exDoseU, exDuration); + enqueue(TaskFunc.UPDATE_CONNECTION); + } + + @Override + protected void preCondition() throws Exception { + //checkPatchActivated(); + checkPatchConnected(); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartTempBasalTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartTempBasalTask.java new file mode 100644 index 0000000000..e3d6a4fb72 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartTempBasalTask.java @@ -0,0 +1,44 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.TempBasalScheduleStart; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.TempBasalScheduleSetResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.TempBasal; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +@Singleton +public class StartTempBasalTask extends TaskBase { + + private TempBasalScheduleStart TEMP_BASAL_SCHEDULE_START; + + @Inject + public StartTempBasalTask() { + super(TaskFunc.START_TEMP_BASAL); + + TEMP_BASAL_SCHEDULE_START = new TempBasalScheduleStart(); + } + + public Single start(TempBasal tempBasal) { + return isReady() + .concatMapSingle(v -> TEMP_BASAL_SCHEDULE_START.start(tempBasal.getDurationMinutes(), tempBasal.getDoseUnitPerHour(), tempBasal.getPercent())) + .doOnNext(this::checkResponse) + .firstOrError() + .observeOn(Schedulers.io()) + .doOnSuccess(v -> onTempBasalStarted(tempBasal)) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private void onTempBasalStarted(TempBasal tempBasal) { + enqueue(TaskFunc.UPDATE_CONNECTION); + } + + @Override + protected void preCondition() throws Exception { + checkPatchConnected(); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopBasalTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopBasalTask.java new file mode 100644 index 0000000000..ff8fa8996f --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopBasalTask.java @@ -0,0 +1,126 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import android.os.SystemClock; + +import info.nightscout.androidaps.interfaces.PumpSync; +import info.nightscout.androidaps.logging.UserEntryLogger; +import info.nightscout.androidaps.utils.userEntry.UserEntryMapper; +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BasalStop; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BasalStopResponse; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.interfaces.CommandQueue; +import info.nightscout.shared.logging.AAPSLogger; +import info.nightscout.androidaps.queue.Callback; +import info.nightscout.androidaps.queue.commands.Command; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.subjects.BehaviorSubject; + +@Singleton +public class StopBasalTask extends TaskBase { + + @Inject IPreferenceManager pm; + @Inject CommandQueue commandQueue; + @Inject AAPSLogger aapsLogger; + @Inject PumpSync pumpSync; + @Inject UserEntryLogger uel; + + private BasalStop BASAL_STOP; + private BehaviorSubject bolusCheckSubject = BehaviorSubject.create(); + private BehaviorSubject exbolusCheckSubject = BehaviorSubject.create(); + private BehaviorSubject basalCheckSubject = BehaviorSubject.create(); + + @Inject + public StopBasalTask() { + super(TaskFunc.STOP_BASAL); + + BASAL_STOP = new BasalStop(); + } + + private Observable getBolusSebject(){ + return bolusCheckSubject.hide(); + } + + private Observable getExbolusSebject(){ + return exbolusCheckSubject.hide(); + } + + private Observable getBasalSebject(){ + return basalCheckSubject.hide(); + } + + public Single stop() { + + if (commandQueue.isRunning(Command.CommandType.BOLUS)) { + uel.log(UserEntryMapper.Action.CANCEL_BOLUS, UserEntryMapper.Sources.EOPatch2); + commandQueue.cancelAllBoluses(); + SystemClock.sleep(650); + } + bolusCheckSubject.onNext(true); + + if (pumpSync.expectedPumpState().getExtendedBolus() != null) { + uel.log(UserEntryMapper.Action.CANCEL_EXTENDED_BOLUS, UserEntryMapper.Sources.EOPatch2); + commandQueue.cancelExtended(new Callback() { + @Override + public void run() { + exbolusCheckSubject.onNext(true); + } + }); + }else{ + exbolusCheckSubject.onNext(true); + } + + if (pumpSync.expectedPumpState().getTemporaryBasal() != null) { + uel.log(UserEntryMapper.Action.CANCEL_TEMP_BASAL, UserEntryMapper.Sources.EOPatch2); + commandQueue.cancelTempBasal(true, new Callback() { + @Override + public void run() { + basalCheckSubject.onNext(true); + } + }); + }else{ + basalCheckSubject.onNext(true); + } + + return Observable.zip(getBolusSebject(), getExbolusSebject(), getBasalSebject(), (bolusReady, exbolusReady, basalReady) -> { + return (bolusReady && exbolusReady && basalReady); + }) + .filter(ready -> ready) + .flatMap(v -> isReady()) + .concatMapSingle(v -> BASAL_STOP.stop()) + .doOnNext(this::checkResponse) + .firstOrError() + .doOnSuccess(this::onBasalStopped) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private void onBasalStopped(BasalStopResponse response) { + enqueue(TaskFunc.UPDATE_CONNECTION); + } + + public synchronized void enqueue() { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = stop() + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(v -> { + bolusCheckSubject.onNext(false); + exbolusCheckSubject.onNext(false); + basalCheckSubject.onNext(false); + }); + } + } + + @Override + protected void preCondition() throws Exception { + checkPatchConnected(); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopComboBolusTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopComboBolusTask.java new file mode 100644 index 0000000000..fec4f7ba8c --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopComboBolusTask.java @@ -0,0 +1,85 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.define.IPatchConstant; +import info.nightscout.androidaps.plugins.pump.eopatch.core.code.PatchBleResultCode; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BolusStop; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BolusStopResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.ComboBolusStopResponse; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.Single; + +@Singleton +public class StopComboBolusTask extends BolusTask { + + private BolusStop BOLUS_STOP; + + @Inject + public StopComboBolusTask() { + super(TaskFunc.STOP_COMBO_BOLUS); + BOLUS_STOP = new BolusStop(); + } + + public Single stop() { + return isReady() + .concatMapSingle(v -> stopJob()) + .firstOrError() + .doOnSuccess(this::checkResponse) + .doOnSuccess(this::onComboBolusStopped) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + public Single stopJob() { + return Single.zip( + BOLUS_STOP.stop(IPatchConstant.EXT_BOLUS_ID), + BOLUS_STOP.stop(IPatchConstant.NOW_BOLUS_ID), + (ext, now) -> createStopComboBolusResponse(now, ext)); + } + + private ComboBolusStopResponse createStopComboBolusResponse(BolusStopResponse now, BolusStopResponse ext) { + int idNow = now.isSuccess() ? IPatchConstant.NOW_BOLUS_ID : 0; + int idExt = ext.isSuccess() ? IPatchConstant.EXT_BOLUS_ID : 0; + + int injectedAmount = now.getInjectedBolusAmount(); + int injectingAmount = now.getInjectingBolusAmount(); + + int injectedExAmount = ext.getInjectedBolusAmount(); + int injectingExAmount = ext.getInjectingBolusAmount(); + + if (idNow == 0 && idExt == 0) { + return new ComboBolusStopResponse(IPatchConstant.NOW_BOLUS_ID, PatchBleResultCode.BOLUS_UNKNOWN_ID); + } + + return new ComboBolusStopResponse(idNow, injectedAmount, injectingAmount, idExt, injectedExAmount, injectingExAmount); + } + + private void onComboBolusStopped(ComboBolusStopResponse response) { + if (response.getId() != 0) + updateNowBolusStopped(response.getInjectedBolusAmount()); + + if (response.getExtId() != 0) + updateExtBolusStopped(response.getInjectedExBolusAmount()); + + enqueue(TaskFunc.UPDATE_CONNECTION); + } + + public synchronized void enqueue() { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = stop() + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } + + @Override + protected void preCondition() throws Exception { + checkPatchConnected(); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopExtBolusTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopExtBolusTask.java new file mode 100644 index 0000000000..f7a44ed3d8 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopExtBolusTask.java @@ -0,0 +1,59 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.define.IPatchConstant; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BolusStop; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BolusStopResponse; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.Single; + +@Singleton +public class StopExtBolusTask extends BolusTask { + + private BolusStop BOLUS_STOP; + + @Inject + public StopExtBolusTask() { + super(TaskFunc.STOP_EXT_BOLUS); + + BOLUS_STOP = new BolusStop(); + } + + public Single stop() { + return isReady().concatMapSingle(v -> stopJob()).firstOrError() + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + public Single stopJob() { + return BOLUS_STOP.stop(IPatchConstant.EXT_BOLUS_ID) + .doOnSuccess(this::checkResponse) + .doOnSuccess(this::onExtBolusStopped); + } + + + private void onExtBolusStopped(BolusStopResponse response) { + updateExtBolusStopped(response.getInjectedBolusAmount()); + enqueue(TaskFunc.UPDATE_CONNECTION); + } + + public synchronized void enqueue() { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = stop() + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } + + @Override + protected void preCondition() throws Exception { + //checkPatchActivated(); + checkPatchConnected(); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopNowBolusTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopNowBolusTask.java new file mode 100644 index 0000000000..28c7e5ad95 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopNowBolusTask.java @@ -0,0 +1,60 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.define.IPatchConstant; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BolusStop; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BolusStopResponse; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; + +@Singleton +public class StopNowBolusTask extends BolusTask { + + private BolusStop BOLUS_STOP; + + @Inject + public StopNowBolusTask() { + super(TaskFunc.STOP_NOW_BOLUS); + + BOLUS_STOP = new BolusStop(); + } + + public Single stop() { + return isReady() + .observeOn(AndroidSchedulers.mainThread()) + .concatMapSingle(v -> stopJob()).firstOrError() + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + public Single stopJob() { + return BOLUS_STOP.stop(IPatchConstant.NOW_BOLUS_ID) + .doOnSuccess(this::checkResponse) + .doOnSuccess(this::onNowBolusStopped); + } + + private void onNowBolusStopped(BolusStopResponse response) { + updateNowBolusStopped(response.getInjectedBolusAmount()); + enqueue(TaskFunc.UPDATE_CONNECTION); + } + + public synchronized void enqueue() { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = stop() + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } + + @Override + protected void preCondition() throws Exception { + checkPatchConnected(); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopTempBasalTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopTempBasalTask.java new file mode 100644 index 0000000000..691c7f27c1 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopTempBasalTask.java @@ -0,0 +1,56 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.TempBasalScheduleStop; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.PatchBooleanResponse; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.Single; + +@Singleton +public class StopTempBasalTask extends TaskBase { + + private TempBasalScheduleStop TEMP_BASAL_SCHEDULE_STOP; + + @Inject + public StopTempBasalTask() { + super(TaskFunc.STOP_TEMP_BASAL); + + TEMP_BASAL_SCHEDULE_STOP = new TempBasalScheduleStop(); + } + + public Single stop() { + return isReady().concatMapSingle(v -> stopJob()).firstOrError() + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + public Single stopJob() { + return TEMP_BASAL_SCHEDULE_STOP.stop() + .doOnSuccess(this::checkResponse) + .doOnSuccess(v -> onTempBasalCanceled()); + } + + private void onTempBasalCanceled() { + enqueue(TaskFunc.UPDATE_CONNECTION); + } + + public synchronized void enqueue() { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = stop() + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } + + @Override + protected void preCondition() throws Exception { + //checkPatchActivated(); + checkPatchConnected(); + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SyncBasalHistoryTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SyncBasalHistoryTask.java new file mode 100644 index 0000000000..d66ea87d82 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SyncBasalHistoryTask.java @@ -0,0 +1,151 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BasalHistoryGetExBig; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.BasalHistoryIndexGet; +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.TempBasalHistoryGetExBig; + +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BasalHistoryIndexResponse; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BasalHistoryResponse; +import io.reactivex.Single; + +import static info.nightscout.androidaps.plugins.pump.eopatch.core.define.IPatchConstant.BASAL_HISTORY_SIZE_BIG; + +@Singleton +public class SyncBasalHistoryTask extends TaskBase { + + @Inject + IPreferenceManager pm; + + private BasalHistoryIndexGet BASAL_HISTORY_INDEX_GET; + private BasalHistoryGetExBig BASAL_HISTORY_GET_EX_BIG; + private TempBasalHistoryGetExBig TEMP_BASAL_HISTORY_GET_EX_BIG; + + @Inject + public SyncBasalHistoryTask() { + super(TaskFunc.SYNC_BASAL_HISTORY); + + BASAL_HISTORY_INDEX_GET = new BasalHistoryIndexGet(); + BASAL_HISTORY_GET_EX_BIG = new BasalHistoryGetExBig(); + TEMP_BASAL_HISTORY_GET_EX_BIG = new TempBasalHistoryGetExBig(); + } + + public Single sync(int end) { + return Single.just(1); // 베이젤 싱크 사용 안함 +// return isReady() +// .concatMapSingle(v -> syncBoth(pm.getPatchConfig().getLastIndex(), end)) +// .firstOrError() +// .doOnSuccess(this::updatePatchLastIndex); + } + + public Single sync() { + return Single.just(1); // 베이젤 싱크 사용 안함 +// return isReady() +// .concatMapSingle(v -> getLastIndex()) +// .concatMapSingle(end -> syncBoth(pm.getPatchConfig().getLastIndex(), end)) +// .firstOrError() +// .doOnSuccess(this::updatePatchLastIndex); + } + + private Single getLastIndex() { + return BASAL_HISTORY_INDEX_GET.get() + .doOnSuccess(this::checkResponse) + .map(BasalHistoryIndexResponse::getLastFinishedIndex); + } + + private Single syncBoth(int start, int end) { + int count = end - start + 1; + + if (count > 0) { + return Single.zip( + BASAL_HISTORY_GET_EX_BIG.get(start, count), + TEMP_BASAL_HISTORY_GET_EX_BIG.get(start, count), + (normal, temp) -> onBasalHistoryResponse(normal, temp, start, end)); + } else { + return Single.just(-1); + } + } + + public synchronized void enqueue(int end) { + + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = sync(end) + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } + + public synchronized void enqueue() { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = sync() + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } + + private int onBasalHistoryResponse(BasalHistoryResponse n, BasalHistoryResponse t, + int startRequested, int end) throws SQLException { + + if (!n.isSuccess() || !t.isSuccess() || n.getSeq() != t.getSeq()) { + return -1; + } + + int start = n.getSeq(); + + float[] normal = n.getInjectedDoseValues(); + float[] temp = t.getInjectedDoseValues(); + + int count = Math.min(end - start + 1, BASAL_HISTORY_SIZE_BIG); + count = Math.min(count, normal.length); + count = Math.min(count, temp.length); + + return updateInjected(normal, temp, start, end); + } + + public synchronized int updateInjected(float[] normal, float[] temp, int start, int end) throws SQLException { + if (pm.getPatchState().isPatchInternalSuspended() && pm.getPatchConfig().isInBasalPausedTime() == false) { + return -1; + } + + int lastUpdatedIndex = -1; + int count = end - start + 1; + + if (count > normal.length) { + count = normal.length; + } + + if (count > 0) { + int lastSyncIndex = pm.getPatchConfig().getLastIndex(); + for (int i = 0;i < count;i++) { + int seq = start + i; + if (seq < lastSyncIndex) + continue; + + if (start <= seq && seq <= end) { + lastUpdatedIndex = seq; + } + } + } + + return lastUpdatedIndex; + } + + private void updatePatchLastIndex(int newIndex) { + int lastIndex = pm.getPatchConfig().getLastIndex(); + + if (lastIndex < newIndex) { + pm.getPatchConfig().setLastIndex(newIndex); + pm.flushPatchConfig(); + } + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskBase.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskBase.java new file mode 100644 index 0000000000..b92163309b --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskBase.java @@ -0,0 +1,98 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.BleConnectionState; +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.IBleDevice; +import info.nightscout.androidaps.plugins.pump.eopatch.core.Patch; +import info.nightscout.androidaps.plugins.pump.eopatch.core.exception.NoActivatedPatchException; +import info.nightscout.androidaps.plugins.pump.eopatch.core.exception.PatchDisconnectedException; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.BaseResponse; + +import java.util.HashMap; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.shared.logging.AAPSLogger; +import io.reactivex.Observable; +import io.reactivex.disposables.Disposable; + +@Singleton +public class TaskBase { + protected IBleDevice patch; + + @Inject AAPSLogger aapsLogger; + @Inject protected IPreferenceManager pm; + @Inject TaskQueue taskQueue; + + TaskFunc func; + + static HashMap maps = new HashMap<>(); + + /* enqueue 시 사용 */ + protected Disposable disposable; + + protected final Object lock = new Object(); + + protected static final long TASK_ENQUEUE_TIME_OUT = 60; // SECONDS + + @Inject + public TaskBase(TaskFunc func) { + this.func = func; + maps.put(func, this); + patch = Patch.getInstance(); + } + + /* Task 들의 작업 순서 및 조건 체크 */ + protected Observable isReady() { + return taskQueue.isReady(func).doOnNext(v -> preCondition()); + } + + protected Observable isReady2() { + return taskQueue.isReady2(func).doOnNext(v -> preCondition()); + } + + protected void checkResponse(BaseResponse response) throws Exception { + if (!response.isSuccess()) { + throw new Exception("Response failed! - "+response.resultCode.name()); + } + } + + public static void enqueue(TaskFunc func) { + TaskBase task = maps.get(func); + + if (task != null) { + task.enqueue(); + } + } + + public static void enqueue(TaskFunc func, Boolean flag) { + TaskBase task = maps.get(func); + + if (task != null) { + task.enqueue(flag); + } + } + + protected synchronized void enqueue() { + } + + protected synchronized void enqueue(Boolean flag) { + } + + protected void preCondition() throws Exception { + + } + + protected void checkPatchConnected() throws Exception { + if (patch.getConnectionState() == BleConnectionState.DISCONNECTED) { + throw new PatchDisconnectedException(); + } + } + + protected void checkPatchActivated() throws Exception { + if (pm.getPatchConfig().isDeactivated()) { + throw new NoActivatedPatchException(); + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskFunc.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskFunc.java new file mode 100644 index 0000000000..2e1f8f77b8 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskFunc.java @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +public enum TaskFunc { + START_BOND, + GET_PATCH_INFO, + SELF_TEST, + PRIMING, + NEEDLE_SENSING, + ACTIVATE, + DEACTIVATE, + UPDATE_CONNECTION, + START_NORMAL_BASAL, + START_TEMP_BASAL, + STOP_TEMP_BASAL, + RESUME_BASAL, + PAUSE_BASAL, + STOP_BASAL, + STOP_NOW_BOLUS, + STOP_EXT_BOLUS, + STOP_COMBO_BOLUS, + START_QUICK_BOLUS, + START_CALC_BOLUS, + SYNC_BASAL_HISTORY, + READ_BOLUS_FINISH_TIME, + READ_TEMP_BASAL_FINISH_TIME, + FETCH_ALARM, + LOW_RESERVOIR, + SET_GLOBAL_TIME, + INFO_REMINDER, + INTERNAL_SUSPEND +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskQueue.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskQueue.java new file mode 100644 index 0000000000..8b9ef9e8ab --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskQueue.java @@ -0,0 +1,115 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Queue; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.shared.logging.AAPSLogger; +import info.nightscout.shared.logging.LTag; +import io.reactivex.Observable; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.BehaviorSubject; + +@Singleton +public class TaskQueue { + @Inject AAPSLogger aapsLogger; + + Queue queue = new LinkedList<>(); + + private int sequence = 0; + private final BehaviorSubject ticketSubject = BehaviorSubject.create(); + private final BehaviorSubject sizeSubject = BehaviorSubject.createDefault(0); + + @Inject + public TaskQueue() { + } + + protected Observable observeQueue() { + return sizeSubject.distinctUntilChanged(); + } + + protected synchronized Observable isReady(final TaskFunc function) { + return Observable.fromCallable(() -> publishTicket(function)) + .concatMap(v -> { + return ticketSubject + .takeUntil(it -> it.number > v) + .filter(it -> it.number == v); + }) + .doOnNext(v -> aapsLogger.debug(LTag.PUMPCOMM, String.format("Task #:%s started func:%s", v.number, v.func.name()))) + .observeOn(Schedulers.io()) + .map(it -> it.func) + .doFinally(this::done); + } + + protected synchronized Observable isReady2(final TaskFunc function) { + return observeQueue() + .filter(size -> size == 0).concatMap(v -> isReady(function)); + } + + private synchronized int publishTicket(final TaskFunc function) { + int turn = sequence++; + aapsLogger.debug(LTag.PUMPCOMM, String.format("publishTicket() Task #:%s is assigned func:%s", turn, function.name())); + + PatchTask task = new PatchTask(turn, function); + addQueue(task); + return turn; + } + + private synchronized void addQueue(PatchTask task) { + queue.add(task); + int size = queue.size(); + sizeSubject.onNext(size); + + if (size == 1) { + ticketSubject.onNext(task); + } + } + + private synchronized void done() { + if (queue.size() > 0) { + PatchTask done = queue.remove(); + aapsLogger.debug(LTag.PUMPCOMM, String.format("done() Task #:%s completed func:%s task remaining:%s", + done.number, done.func.name(), queue.size())); + } + + int size = queue.size(); + sizeSubject.onNext(size); + + PatchTask next = queue.peek(); + if (next != null) { + ticketSubject.onNext(next); + } + } + + public synchronized boolean has(TaskFunc func) { + if (queue.size() > 1) { + Iterator itor = queue.iterator(); + + /* remove 1st queue */ + itor.next(); + + while (itor.hasNext()) { + PatchTask item = itor.next(); + if (item.func == func) { + return true; + } + } + } + + return false; + } + + static class PatchTask { + + int number; + TaskFunc func; + + PatchTask(int number, TaskFunc func) { + this.number = number; + this.func = func; + } + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/UpdateConnectionTask.java b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/UpdateConnectionTask.java new file mode 100644 index 0000000000..729bbb4651 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/UpdateConnectionTask.java @@ -0,0 +1,57 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ble.task; + +import info.nightscout.shared.logging.LTag; +import info.nightscout.androidaps.plugins.pump.eopatch.ble.PatchStateManager; +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.pump.eopatch.core.api.UpdateConnection; +import info.nightscout.androidaps.plugins.pump.eopatch.core.response.UpdateConnectionResponse; +import io.reactivex.Single; + +@Singleton +public class UpdateConnectionTask extends TaskBase { + + @Inject + PatchStateManager patchStateManager; + + private UpdateConnection UPDATE_CONNECTION; + + @Inject + public UpdateConnectionTask() { + super(TaskFunc.UPDATE_CONNECTION); + + UPDATE_CONNECTION = new UpdateConnection(); + } + + public Single update() { + return isReady().concatMapSingle(v -> updateJob()).firstOrError(); + } + + public Single updateJob() { + return UPDATE_CONNECTION.get() + .doOnSuccess(this::checkResponse) + .map(UpdateConnectionResponse::getPatchState) + .map(bytes -> PatchState.Companion.create(bytes, System.currentTimeMillis())) + .doOnSuccess(state -> onUpdateConnection(state)) + .doOnError(e -> aapsLogger.error(LTag.PUMPCOMM, e.getMessage())); + } + + private void onUpdateConnection(PatchState patchState) { + patchStateManager.updatePatchState(patchState); + } + + public synchronized void enqueue() { + boolean ready = (disposable == null || disposable.isDisposed()); + + if (ready) { + disposable = update() + .timeout(TASK_ENQUEUE_TIME_OUT, TimeUnit.SECONDS) + .subscribe(); + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/AlarmCategory.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/AlarmCategory.kt new file mode 100644 index 0000000000..10d3115a01 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/AlarmCategory.kt @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + +import com.google.android.gms.common.internal.Preconditions + +enum class AlarmCategory private constructor(val rawValue: Int) { + NONE(0), + ALARM(1), + ALERT(2); + + companion object { + + + /** + * rawValue로 값을 찾기, 못찾으면 null을 리턴 + */ + fun ofRaw(rawValue: Int?): AlarmCategory? { + if (rawValue == null) { + return null + } + + for (t in AlarmCategory.values()) { + if (t.rawValue == rawValue) { + return t + } + } + return null + } + + /** + * rawValue로 값을 찾기, 못찾으면 defaultValue을 리턴 + */ + fun ofRaw(rawValue: Int?, defaultValue: AlarmCategory): AlarmCategory { + Preconditions.checkNotNull(defaultValue) + if (rawValue == null) { + return defaultValue + } + + for (t in AlarmCategory.values()) { + if (t.rawValue == rawValue) { + return t + } + } + return defaultValue + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/AlarmSource.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/AlarmSource.kt new file mode 100644 index 0000000000..c433488b7b --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/AlarmSource.kt @@ -0,0 +1,66 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + + +import com.google.android.gms.common.internal.Preconditions +import com.google.gson.annotations.SerializedName + +enum class AlarmSource private constructor(val rawValue: Int) { + @SerializedName("0") + NONE(0), + @SerializedName("1") + PATCH(1), + @SerializedName("2") + ADM(2), + @SerializedName("3") + CGM(3), + @SerializedName("9") + TEST(9); + + val isTest: Boolean + get() = this == TEST + + val isCgm: Boolean + get() = this == CGM + + companion object { + + /** + * rawValue로 값을 찾기, 못찾으면 null을 리턴 + */ + @JvmStatic + fun ofRaw(rawValue: Int?): AlarmSource { + if (rawValue == null) { + return NONE + } + + for (t in AlarmSource.values()) { + if (t.rawValue == rawValue) { + return t + } + } + return NONE + } + + @JvmStatic + fun toRaw(source: AlarmSource?): Int { + return if (source != null) source.rawValue else NONE.rawValue + } + + /** + * rawValue로 값을 찾기, 못찾으면 defaultValue을 리턴 + */ + fun ofRaw(rawValue: Int?, defaultValue: AlarmSource): AlarmSource { + Preconditions.checkNotNull(defaultValue) + if (rawValue == null) { + return defaultValue + } + + for (t in AlarmSource.values()) { + if (t.rawValue == rawValue) { + return t + } + } + return defaultValue + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BasalStatus.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BasalStatus.kt new file mode 100644 index 0000000000..4225e0e523 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BasalStatus.kt @@ -0,0 +1,43 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + +enum class BasalStatus constructor(val rawValue: Int) { + STOPPED(0), + PAUSED(1), //템프베이젤 주입중 + SUSPENDED(2), //주입 정지 + STARTED(3), //주입중 + SELECTED(4); //패치 폐기 + + val isStarted: Boolean + get() = this == STARTED + + val isPaused: Boolean + get() = this == PAUSED + + val isSuspended: Boolean + get() = this == SUSPENDED + + val isStopped: Boolean + get() = this == STOPPED + + val isSelected: Boolean + get() = this == SELECTED + + val isSelectedGroup: Boolean + get() = isStarted || isPaused || isSuspended || isSelected + + companion object { + @JvmStatic + fun ofRaw(rawValue: Int?): BasalStatus { + if (rawValue == null) { + return STOPPED + } + + for (t in values()) { + if (t.rawValue == rawValue) { + return t + } + } + return STOPPED + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BgUnit.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BgUnit.kt new file mode 100644 index 0000000000..b1bd632d51 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BgUnit.kt @@ -0,0 +1,37 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + +enum class BgUnit private constructor(val rawValue: Int, val unitStr: String) { + GRAM(1, "mg/dL"), + MMOL(2, "mmol/L"); + + fun isGram() = GRAM == this + fun isMmol() = MMOL == this + + fun getUnit() = unitStr + + companion object { + /** + * rawValue로 값을 찾기, 못찾으면 null을 리턴 + */ + fun ofRaw(rawValue: Int?): BgUnit { + for (t in BgUnit.values()) { + if (t.rawValue == rawValue) { + return t + } + } + return GRAM + } + + /** + * rawValue로 값을 찾기, 못찾으면 defaultValue을 리턴 + */ + fun ofRaw(rawValue: Int, defaultValue: BgUnit): BgUnit { + for (t in BgUnit.values()) { + if (t.rawValue == rawValue) { + return t + } + } + return defaultValue + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BolusExDuration.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BolusExDuration.kt new file mode 100644 index 0000000000..30c0ae7116 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BolusExDuration.kt @@ -0,0 +1,65 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + +import android.content.Context +import androidx.collection.SparseArrayCompat +import info.nightscout.androidaps.plugins.pump.eopatch.CommonUtils +import info.nightscout.androidaps.plugins.pump.eopatch.R +import java.util.* +import java.util.concurrent.TimeUnit + +enum class BolusExDuration constructor(val index: Int, val minute: Int, val hour: Float) { + OFF(0, 0, 0f), + MINUTE_30(1, 30, 0.5f), + MINUTE_60(2, 60, 1.0f), + MINUTE_90(3, 90, 1.5f), + MINUTE_120(4, 120, 2.0f), + MINUTE_150(5, 150, 2.5f), + MINUTE_180(6, 180, 3.0f), + MINUTE_210(7, 210, 3.5f), + MINUTE_240(8, 240, 4.0f), + MINUTE_270(9, 270, 4.5f), + MINUTE_300(10, 300, 5.0f), + MINUTE_330(11, 330, 5.5f), + MINUTE_360(12, 360, 6.0f), + MINUTE_390(13, 390, 6.5f), + MINUTE_420(14, 420, 7.0f), + MINUTE_450(15, 450, 7.5f), + MINUTE_480(16, 480, 8.0f); + + val isOff: Boolean + get() = this == OFF + + val isNotOff: Boolean + get() = this != OFF + + fun milli(): Long { + return TimeUnit.MINUTES.toMillis(this.minute.toLong()) + } + + companion object { + @JvmStatic + fun getItemFromIndex(index: Int): BolusExDuration { + var reverseIndices = SparseArrayCompat() + for (duration in values()) { + reverseIndices.put(duration.index, duration) + } + val result = reverseIndices.get(index) + return result ?: OFF + } + + @JvmStatic + fun ofRaw(rawValue: Int): BolusExDuration { + if (rawValue == null) { + return OFF + } + + for (t in BolusExDuration.values()) { + if (t.minute == rawValue) { + return t + } + } + return OFF + } + } + +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/DeactivationStatus.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/DeactivationStatus.kt new file mode 100644 index 0000000000..8373e828df --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/DeactivationStatus.kt @@ -0,0 +1,28 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + + +enum class DeactivationStatus constructor(val rawValue: Int) { + DEACTIVATION_FAILED(0), + NORMAL_DEACTIVATED(1), + FORCE_DEACTIVATED(2); + + val isDeactivated: Boolean + get() = this == NORMAL_DEACTIVATED || this == FORCE_DEACTIVATED + + val isNormalSuccess: Boolean + get() = this == NORMAL_DEACTIVATED + + val isNormalFailed: Boolean + get() = this == DEACTIVATION_FAILED || this == FORCE_DEACTIVATED + + companion object { + @JvmStatic + fun of(isSuccess: Boolean, forced: Boolean): DeactivationStatus { + return when { + isSuccess -> NORMAL_DEACTIVATED + forced -> FORCE_DEACTIVATED + else -> DEACTIVATION_FAILED + } + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/Dummy.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/Dummy.kt new file mode 100644 index 0000000000..f81dff583e --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/Dummy.kt @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + +enum class Dummy { + INSTANCE +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/EventType.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/EventType.kt new file mode 100644 index 0000000000..be02c5fd07 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/EventType.kt @@ -0,0 +1,25 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + +enum class EventType { + ACTIVTION_CLICKED, + DEACTIVTION_CLICKED, + SUSPEND_CLICKED, + RESUME_CLICKED, + INVALID_BASAL_RATE, + PROFILE_NOT_SET, + SHOW_PATCH_COMM_DIALOG, + DISMISS_PATCH_COMM_DIALOG, + SHOW_PATCH_COMM_ERROR_DIALOG, + SHOW_BONDED_DIALOG, + SHOW_CHANGE_PATCH_DIALOG, + SHOW_PROGRESS_DIALOG, + DISMISS_PROGRESS_DIALOG, + FINISH_ACTIVITY, + SHOW_DISCARD_DIALOG, + PAUSE_BASAL_FAILED, + RESUME_BASAL_FAILED, + CALCULATED_BOLUS_CLICKED, + EXTENDED_BOLUS_SETTINGS_CLICKED, + SAVE_CLICKED + ; +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchExpireAlertTime.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchExpireAlertTime.kt new file mode 100644 index 0000000000..9c3a685294 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchExpireAlertTime.kt @@ -0,0 +1,52 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + +import android.content.Context +import info.nightscout.androidaps.plugins.pump.eopatch.R + +enum class PatchExpireAlertTime private constructor(val index: Int, val hour: Int) { + HOUR_1(0, 1), + HOUR_2(1, 2), + HOUR_3(2, 3), + HOUR_4(3, 4), + HOUR_5(4, 5), + HOUR_6(5, 6), + HOUR_7(6, 7), + HOUR_8(7, 8), + HOUR_9(8, 9), + HOUR_10(9, 10), + HOUR_11(10, 11), + HOUR_12(11, 12), + HOUR_13(12, 13), + HOUR_14(13, 14), + HOUR_15(14, 15), + HOUR_16(15, 16), + HOUR_17(16, 17), + HOUR_18(17, 18), + HOUR_19(18, 19), + HOUR_20(19, 20), + HOUR_21(20, 21), + HOUR_22(21, 22), + HOUR_23(22, 23), + HOUR_24(23, 24); + + companion object { + + fun atIndex(index: Int): PatchExpireAlertTime { + for (i in values()) { + if (i.index == index) { + return i + } + } + return HOUR_1 + } + + fun atHour(hour: Int): PatchExpireAlertTime { + for (i in values()) { + if (i.hour == hour) { + return i + } + } + return HOUR_1 + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchLifecycle.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchLifecycle.kt new file mode 100644 index 0000000000..99c8d287c0 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchLifecycle.kt @@ -0,0 +1,30 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + +enum class PatchLifecycle private constructor(val rawValue: Int) { + SHUTDOWN(1), + BONDED(2), + SAFETY_CHECK(3), + REMOVE_NEEDLE_CAP(4), + REMOVE_PROTECTION_TAPE(5), + ROTATE_KNOB(6), + BASAL_SETTING(7), + ACTIVATED(8); + + val isShutdown: Boolean + get() = this == SHUTDOWN + + val isActivated: Boolean + get() = this == ACTIVATED + + companion object { + @JvmStatic + fun ofRaw(rawValue: Int): PatchLifecycle { + for (type in values()) { + if (type.rawValue == rawValue) { + return type + } + } + return SHUTDOWN + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchStep.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchStep.kt new file mode 100644 index 0000000000..eed465a75a --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchStep.kt @@ -0,0 +1,32 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + +enum class PatchStep { + SAFE_DEACTIVATION, + MANUALLY_TURNING_OFF_ALARM, + DISCARDED, + DISCARDED_FOR_CHANGE, + DISCARDED_FROM_ALARM, + WAKE_UP, + CONNECT_NEW, + REMOVE_NEEDLE_CAP, + REMOVE_PROTECTION_TAPE, + SAFETY_CHECK, + ROTATE_KNOB, + ROTATE_KNOB_NEEDLE_INSERTION_ERROR, + BASAL_SCHEDULE, + SETTING_REMINDER_TIME, + CHECK_CONNECTION, + CANCEL, + COMPLETE, + BACK_TO_HOME, + FINISH; + + val isConnectNew: Boolean + get() = this == CONNECT_NEW + + val isSafeDeactivation: Boolean + get() = this == SAFE_DEACTIVATION + + val isCheckConnection: Boolean + get() = this == CHECK_CONNECTION +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/SettingKeys.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/SettingKeys.kt new file mode 100644 index 0000000000..13f6a5474f --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/SettingKeys.kt @@ -0,0 +1,18 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + +import info.nightscout.androidaps.plugins.pump.eopatch.R + +class SettingKeys { + companion object{ + val LOW_RESERVIOR_REMINDERS: Int = R.string.key_eopatch_low_reservior_reminders + val EXPIRATION_REMINDERS: Int = R.string.key_eopatch_expiration_reminders + val BUZZER_REMINDERS: Int = R.string.key_eopatch_patch_buzzer_reminders + + val PATCH_CONFIG: Int = R.string.key_eopatch_patch_config + val PATCH_STATE: Int = R.string.key_eopatch_patch_state + val BOLUS_CURRENT: Int = R.string.key_eopatch_bolus_current + val NORMAL_BASAL: Int = R.string.key_eopatch_normal_basal + val TEMP_BASAL: Int = R.string.key_eopatch_temp_basal + val ALARMS: Int = R.string.key_eopatch_bolus_current + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/UnitOrPercent.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/UnitOrPercent.kt new file mode 100644 index 0000000000..56e27148bb --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/UnitOrPercent.kt @@ -0,0 +1,52 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.code + +import com.google.android.gms.common.internal.Preconditions + +enum class UnitOrPercent private constructor(val rawValue: Int, val symbol: String) { + P(0, "%"), + U(1, "U"); + + + fun isPercentage() = this == P + fun isU() = this == U + + companion object { + + val U_PER_HOUR = "U/hr" + val U_PER_DAY = "U/day" + + + /** + * rawValue로 값을 찾기, 못찾으면 null을 리턴 + */ + fun ofRaw(rawValue: Int?): UnitOrPercent? { + if (rawValue == null) { + return null + } + + for (t in UnitOrPercent.values()) { + if (t.rawValue == rawValue) { + return t + } + } + return null + } + + /** + * rawValue로 값을 찾기, 못찾으면 defaultValue을 리턴 + */ + fun ofRaw(rawValue: Int?, defaultValue: UnitOrPercent): UnitOrPercent { + Preconditions.checkNotNull(defaultValue) + if (rawValue == null) { + return defaultValue + } + + for (t in UnitOrPercent.values()) { + if (t.rawValue == rawValue) { + return t + } + } + return defaultValue + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchInjectHelpers.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchInjectHelpers.kt new file mode 100644 index 0000000000..ca9592d836 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchInjectHelpers.kt @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.dagger + +import javax.inject.Qualifier +import javax.inject.Scope + +@Qualifier +annotation class EopatchPluginQualifier + +@MustBeDocumented +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class ActivityScope + +@MustBeDocumented +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class FragmentScope diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchModule.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchModule.kt new file mode 100644 index 0000000000..e971078b5d --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchModule.kt @@ -0,0 +1,140 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.dagger + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.android.ContributesAndroidInjector +import dagger.multibindings.IntoMap +import info.nightscout.androidaps.plugins.pump.eopatch.OsAlarmReceiver +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmManager +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmRegistry +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.IAlarmManager +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.IAlarmRegistry +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPatchManager +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager +import info.nightscout.androidaps.plugins.pump.eopatch.ble.PatchManager +import info.nightscout.androidaps.plugins.pump.eopatch.ble.PreferenceManager +import info.nightscout.androidaps.plugins.pump.eopatch.ui.* +import info.nightscout.androidaps.plugins.pump.eopatch.ui.dialogs.AlarmDialog +import info.nightscout.androidaps.plugins.pump.eopatch.ui.dialogs.ActivationNotCompleteDialog +import info.nightscout.androidaps.plugins.pump.eopatch.ui.dialogs.CommonDialog +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchOverviewViewModel +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.ViewModelFactory +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.ViewModelKey +import javax.inject.Provider +import javax.inject.Singleton + +@Module(includes = [EopatchPrefModule::class]) +@Suppress("unused") +abstract class EopatchModule { + companion object { + @Provides + @EopatchPluginQualifier + fun providesViewModelFactory(@EopatchPluginQualifier viewModels: MutableMap, @JvmSuppressWildcards Provider>): ViewModelProvider.Factory { + return ViewModelFactory(viewModels) + } + + } + + + @Binds + @Singleton + abstract fun bindPatchManager(patchManager: PatchManager): IPatchManager + + @Binds + @Singleton + abstract fun bindAlarmManager(alarmManager: AlarmManager): IAlarmManager + + @Binds + @Singleton + abstract fun bindAlarmRegistry(alarmRegistry: AlarmRegistry): IAlarmRegistry + + @Binds + @Singleton + abstract fun bindPreferenceManager(preferenceManager: PreferenceManager): IPreferenceManager + + // #### VIEW MODELS ############################################################################ + @Binds + @IntoMap + @EopatchPluginQualifier + @ViewModelKey(EopatchOverviewViewModel::class) + internal abstract fun bindsEopatchOverviewViewmodel(viewModel: EopatchOverviewViewModel): ViewModel + + @Binds + @IntoMap + @EopatchPluginQualifier + @ViewModelKey(EopatchViewModel::class) + internal abstract fun bindsEopatchViewModel(viewModel: EopatchViewModel): ViewModel + + // #### FRAGMENTS ############################################################################## + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesEopatchOverviewFragment(): EopatchOverviewFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesEopatchSafeDeactivationFragment(): EopatchSafeDeactivationFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesEopatchTurningOffAlarmFragment(): EopatchTurningOffAlarmFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesEopatchRemoveFragment(): EopatchRemoveFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesEopatchWakeUpFragment(): EopatchWakeUpFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesEopatchConnectNewFragment(): EopatchConnectNewFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesEopatchRemoveNeedleCapFragment(): EopatchRemoveNeedleCapFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesEopatchRemoveProtectionTapeFragment(): EopatchRemoveProtectionTapeFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesEopatchSafetyCheckFragment(): EopatchSafetyCheckFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesEopatchRotateKnobFragment(): EopatchRotateKnobFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesEopatchBasalScheduleFragment(): EopatchBasalScheduleFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesAlarmDialog(): AlarmDialog + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesCommonDialog(): ActivationNotCompleteDialog + + // Activities + @ContributesAndroidInjector + abstract fun contributesEopatchActivity(): EopatchActivity + + @ContributesAndroidInjector + abstract fun contributesAlarmHelperActivity(): AlarmHelperActivity + + @ContributesAndroidInjector + abstract fun contributesDialogHelperActivity(): DialogHelperActivity + + @ContributesAndroidInjector + abstract fun contributesEoDialog(): CommonDialog + + @ContributesAndroidInjector + abstract fun contributesOsAlarmReceiver(): OsAlarmReceiver +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchPrefModule.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchPrefModule.kt new file mode 100644 index 0000000000..94b63103d4 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchPrefModule.kt @@ -0,0 +1,44 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.dagger + +import android.app.Application +import android.content.Context +import dagger.Binds +import dagger.Module +import dagger.Provides +import info.nightscout.androidaps.plugins.pump.eopatch.ble.* +import info.nightscout.androidaps.plugins.pump.eopatch.vo.Alarms +import info.nightscout.androidaps.plugins.pump.eopatch.vo.NormalBasalManager +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchConfig +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState +import info.nightscout.androidaps.plugins.pump.eopatch.vo.TempBasalManager +import info.nightscout.shared.sharedPreferences.SP +import javax.inject.Singleton + +@Module +class EopatchPrefModule { + @Provides + @Singleton + internal fun providePatchConfig(): PatchConfig { + return PatchConfig() + } + + @Provides + @Singleton + internal fun provideNormalBasalManager(sp: SP): NormalBasalManager { + return NormalBasalManager() + } + + @Provides + @Singleton + internal fun provideTempBasalManager(sp: SP): TempBasalManager { + return TempBasalManager() + } + + @Provides + @Singleton + internal fun provideAlarms(): Alarms { + return Alarms() + } +} + + diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/event/EoPatchEvents.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/event/EoPatchEvents.kt new file mode 100644 index 0000000000..cd8656fc77 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/event/EoPatchEvents.kt @@ -0,0 +1,11 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.event + +import androidx.annotation.StringRes +import androidx.fragment.app.DialogFragment +import info.nightscout.androidaps.events.Event +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode + +class EventEoPatchAlarm(var alarmCodes: Set, var isFirst: Boolean = false) : Event() +class EventDialog(val dialog: DialogFragment, val show: Boolean) : Event() +class EventProgressDialog(val show: Boolean, @StringRes val resId: Int = 0) : Event() +class EventPatchActivationNotComplete() : Event() \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/AppCompatActivityExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/AppCompatActivityExtension.kt new file mode 100644 index 0000000000..46cb3c2cd5 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/AppCompatActivityExtension.kt @@ -0,0 +1,62 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import java.io.Serializable + +fun AppCompatActivity.replaceFragmentInActivity(fragment: Fragment, frameId: Int, addToBackStack: Boolean = false) { + supportFragmentManager.transact { + replace(frameId, fragment) + if (addToBackStack) addToBackStack(null) + } +} + +private inline fun FragmentManager.transact(action: FragmentTransaction.() -> Unit) { + beginTransaction().apply { + action() + }.commit() +} + +fun Intent.fillExtras(params: Array>){ + fillIntentArguments(this, params) +} + +private fun fillIntentArguments(intent: Intent, params: Array>) { + params.forEach { + when (val value = it.second) { + null -> intent.putExtra(it.first, null as Serializable?) + is Int -> intent.putExtra(it.first, value) + is Long -> intent.putExtra(it.first, value) + is CharSequence -> intent.putExtra(it.first, value) + is String -> intent.putExtra(it.first, value) + is Float -> intent.putExtra(it.first, value) + is Double -> intent.putExtra(it.first, value) + is Char -> intent.putExtra(it.first, value) + is Short -> intent.putExtra(it.first, value) + is Boolean -> intent.putExtra(it.first, value) + is Serializable -> intent.putExtra(it.first, value) + is Bundle -> intent.putExtra(it.first, value) + is Parcelable -> intent.putExtra(it.first, value) + is Array<*> -> when { + value.isArrayOf() -> intent.putExtra(it.first, value) + value.isArrayOf() -> intent.putExtra(it.first, value) + value.isArrayOf() -> intent.putExtra(it.first, value) + else -> throw Exception("Intent extra ${it.first} has wrong type ${value.javaClass.name}") + } + is IntArray -> intent.putExtra(it.first, value) + is LongArray -> intent.putExtra(it.first, value) + is FloatArray -> intent.putExtra(it.first, value) + is DoubleArray -> intent.putExtra(it.first, value) + is CharArray -> intent.putExtra(it.first, value) + is ShortArray -> intent.putExtra(it.first, value) + is BooleanArray -> intent.putExtra(it.first, value) + else -> throw Exception("Intent extra ${it.first} has wrong type ${value.javaClass.name}") + } + return@forEach + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/BooleanExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/BooleanExtension.kt new file mode 100644 index 0000000000..1b560c6e0a --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/BooleanExtension.kt @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +fun Boolean.takeOne(whenTrue: T, whenFalse: T): T { + return if(this) whenTrue else whenFalse +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/CharSequenceExtesnsion.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/CharSequenceExtesnsion.kt new file mode 100644 index 0000000000..5763bf815d --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/CharSequenceExtesnsion.kt @@ -0,0 +1,37 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +import android.text.Spanned + +fun CharSequence?.check(oldText: CharSequence?): Boolean { + val text = this + if (text === oldText || text == null && oldText?.length == 0) { + return false + } + if (text is Spanned) { + if (text == oldText) { + return false // No change in the spans, so don't set anything. + } + } else if (!text.haveContentsChanged(oldText)) { + return false // No content changes, so don't set anything. + } + return true +} + +fun CharSequence?.haveContentsChanged(str2: CharSequence?): Boolean { + val str1: CharSequence? = this + if (str1 == null != (str2 == null)) { + return true + } else if (str1 == null) { + return false + } + val length = str1.length + if (length != str2!!.length) { + return true + } + for (i in 0 until length) { + if (str1[i] != str2[i]) { + return true + } + } + return false +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/CompletableExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/CompletableExtension.kt new file mode 100644 index 0000000000..29c3bbccdb --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/CompletableExtension.kt @@ -0,0 +1,29 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +import io.reactivex.Completable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + +fun Completable.observeOnMainThread(): Completable = observeOn(AndroidSchedulers.mainThread()) + +fun Completable.observeOnComputation(): Completable = observeOn(Schedulers.computation()) + +fun Completable.observeOnIo(): Completable = observeOn(Schedulers.io()) + +fun Completable.subscribeEmpty(): Disposable { + return subscribe({}, {}) +} + +fun Completable.subscribeEmpty(onComplete: () -> Unit, onError: (Throwable) -> Unit): Disposable { + return subscribe(onComplete, onError) +} + +fun Completable.subscribeDefault(): Disposable { + return subscribe({ Timber.d("onComplete") }, { Timber.e(it, "onError") }) +} + +fun Completable.subscribeDefault(onComplete: () -> Unit): Disposable { + return subscribe(onComplete, { Timber.e(it, "onError") }) +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/FloatExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/FloatExtension.kt new file mode 100644 index 0000000000..e8341c7273 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/FloatExtension.kt @@ -0,0 +1,18 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +fun Float.nearlyEqual(b: Float, epsilon: Float): Boolean { + val absA = Math.abs(this) + val absB = Math.abs(b) + val diff = Math.abs(this - b) + return if (this == b) { + true + } else if (this == 0f || b == 0f || absA + absB < java.lang.Float.MIN_NORMAL) { + diff < epsilon * java.lang.Float.MIN_NORMAL + } else { + diff / Math.min(absA + absB, Float.MAX_VALUE) < epsilon + } +} + +fun Float.nearlyNotEqual(b: Float, epsilon: Float): Boolean { + return !nearlyEqual(b, epsilon) +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/LongExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/LongExtension.kt new file mode 100644 index 0000000000..c3de482c8c --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/LongExtension.kt @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +import java.util.* +import java.util.concurrent.TimeUnit + +internal val Long.date: Date + get() = Calendar.getInstance().also { it.timeInMillis = this }.time + +fun Long.getDiffTime(isRelative: Boolean = false): Triple { + val inputTimeMillis = this + val currentTimeMillis = System.currentTimeMillis() + val diffTimeMillis = if (inputTimeMillis > currentTimeMillis) inputTimeMillis - currentTimeMillis else isRelative.takeOne(currentTimeMillis - inputTimeMillis, 0) + + val hours = TimeUnit.MILLISECONDS.toHours(diffTimeMillis) + val minutes = TimeUnit.MILLISECONDS.toMinutes(diffTimeMillis - TimeUnit.HOURS.toMillis(hours)) + val seconds = TimeUnit.MILLISECONDS.toSeconds(diffTimeMillis - TimeUnit.HOURS.toMillis(hours) - TimeUnit.MINUTES.toMillis(minutes)) + + return Triple(hours, minutes, seconds) +} + +fun Long.getDiffTime(startTimeMillis: Long): Triple { + val inputTimeMillis = this + val diffTimeMillis = if (inputTimeMillis > startTimeMillis) inputTimeMillis - startTimeMillis else 0 + + val hours = TimeUnit.MILLISECONDS.toHours(diffTimeMillis) + val minutes = TimeUnit.MILLISECONDS.toMinutes(diffTimeMillis - TimeUnit.HOURS.toMillis(hours)) + val seconds = TimeUnit.MILLISECONDS.toSeconds(diffTimeMillis - TimeUnit.HOURS.toMillis(hours) - TimeUnit.MINUTES.toMillis(minutes)) + + return Triple(hours, minutes, seconds) +} + +fun Long.getDiffDays(isRelative: Boolean = false): Long { + val inputTimeMillis = this + val currentTimeMillis = System.currentTimeMillis() + val diffTimeMillis = if (inputTimeMillis > currentTimeMillis) inputTimeMillis - currentTimeMillis else isRelative.takeOne(currentTimeMillis - inputTimeMillis, 0) + + return TimeUnit.MILLISECONDS.toDays(diffTimeMillis) +} + +fun Long.getSeconds() : Long { + return (this/1000)*1000 +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/MaybeExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/MaybeExtension.kt new file mode 100644 index 0000000000..d0e79e3033 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/MaybeExtension.kt @@ -0,0 +1,27 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +import io.reactivex.Maybe +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + +fun Maybe.observeOnMainThread(): Maybe = observeOn(AndroidSchedulers.mainThread()) + +fun Maybe.observeOnComputation(): Maybe = observeOn(Schedulers.computation()) + +fun Maybe.observeOnIo(): Maybe = observeOn(Schedulers.io()) + +fun Maybe.subscribeEmpty(): Disposable = subscribe({}, {}, {}) + +fun Maybe.subscribeEmpty(onSuccess: (T) -> Unit): Disposable = subscribe(onSuccess, {}, {}) + +fun Maybe.subscribeEmpty(onSuccess: (T) -> Unit, onError: (Throwable) -> Unit): Disposable = subscribe(onSuccess, onError, {}) + +fun Maybe.subscribeDefault(): Disposable = subscribe({ Timber.d("onSuccess") }, { Timber.e(it, "onError") }, { Timber.d("onComplete") }) + +fun Maybe.subscribeDefault(onSuccess: (T) -> Unit): Disposable = subscribe(onSuccess, { Timber.e(it, "onError") }, { Timber.d("onComplete") }) + +fun Maybe.subscribeDefault(onSuccess: (T) -> Unit, onError: (Throwable) -> Unit): Disposable = subscribe(onSuccess, onError, { Timber.d("onComplete") }) + +fun Maybe.with(): Maybe = subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/ObservableExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/ObservableExtension.kt new file mode 100644 index 0000000000..d11a4cd368 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/ObservableExtension.kt @@ -0,0 +1,27 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + +fun Observable.observeOnMainThread(): Observable = observeOn(AndroidSchedulers.mainThread()) + +fun Observable.observeOnComputation(): Observable = observeOn(Schedulers.computation()) + +fun Observable.observeOnIo(): Observable = observeOn(Schedulers.io()) + +fun Observable.subscribeEmpty(): Disposable = subscribe({}, {}, {}) + +fun Observable.subscribeEmpty(onSuccess: (T) -> Unit): Disposable = subscribe(onSuccess, {}, {}) + +fun Observable.subscribeEmpty(onSuccess: (T) -> Unit, onError: (Throwable) -> Unit): Disposable = subscribe(onSuccess, onError, {}) + +fun Observable.subscribeDefault(): Disposable = subscribe({ Timber.d("onSuccess") }, { Timber.e(it, "onError") }, { Timber.d("onComplete") }) + +fun Observable.subscribeDefault(onSuccess: (T) -> Unit): Disposable = subscribe(onSuccess, { Timber.e(it, "onError") }, { Timber.d("onComplete") }) + +fun Observable.subscribeDefault(onSuccess: (T) -> Unit, onError: (Throwable) -> Unit): Disposable = subscribe(onSuccess, onError, { Timber.d("onComplete") }) + +fun Observable.with(): Observable = subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/SharedPreferencesExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/SharedPreferencesExtension.kt new file mode 100644 index 0000000000..ff08b717cb --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/SharedPreferencesExtension.kt @@ -0,0 +1,40 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +import android.content.SharedPreferences +import androidx.core.content.edit + +/** + * puts a key value pair in shared prefs if doesn't exists, otherwise updates value on given [key] + */ +operator fun SharedPreferences.set(key: String, commit: Boolean = false, value: Any?) { + when (value) { + is String? -> edit(commit) { putString(key, value) } + is Int -> edit(commit) { putInt(key, value) } + is Long -> edit(commit) { putLong(key, value) } + is Float -> edit(commit) { putFloat(key, value) } + is Boolean -> edit(commit) { putBoolean(key, value) } + else -> throw UnsupportedOperationException("Not yet implemented") + } +} + +/** + * finds value on given key. + * [T] is the type of value + * @param defaultValue optional default value - will take null for strings, false for bool and -1 for numeric values if [defaultValue] is not specified + */ +inline operator fun SharedPreferences.get(key: String, defaultValue: T? = null): T? { + return when (T::class) { + String::class -> getString(key, defaultValue as? String) as T? + Int::class -> getInt(key, defaultValue as? Int ?: -1) as T? + Long::class -> getLong(key, defaultValue as? Long ?: -1) as T? + Float::class -> getFloat(key, defaultValue as? Float ?: -1f) as T? + Boolean::class -> getBoolean(key, defaultValue as? Boolean ?: false) as T? + else -> throw UnsupportedOperationException("Not yet implemented") + } +} + +fun SharedPreferences.getString(key: String): String? = this[key] +fun SharedPreferences.getInt(key: String): Int? = this[key] +fun SharedPreferences.getFloat(key: String): Float? = this[key] +fun SharedPreferences.getLong(key: String): Long? = this[key] +fun SharedPreferences.getBoolean(key: String): Boolean? = this[key] diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/SingleExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/SingleExtension.kt new file mode 100644 index 0000000000..ac97170543 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/SingleExtension.kt @@ -0,0 +1,15 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + +fun Single.observeOnMainThread(): Single = observeOn(AndroidSchedulers.mainThread()) + +fun Single.subscribeDefault(onSuccess: (T) -> Unit): Disposable = subscribe(onSuccess, { + Timber.e(it, "onError") +}) + +fun Single.with(): Single = subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/StringExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/StringExtension.kt new file mode 100644 index 0000000000..ad8a1b9b07 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/StringExtension.kt @@ -0,0 +1,48 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +import android.text.Html +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.text.SimpleDateFormat +import java.util.* + +@Throws(JSONException::class) +fun String.toJson(): String = + when (get(0)) { + '{' -> JSONObject(this).toString(4) + '[' -> JSONArray(this).toString(4) + else -> "" + } + +fun String.fromHtml(): CharSequence = Html.fromHtml(this, Html.FROM_HTML_MODE_COMPACT) + +fun String.isEmpty(): Boolean{ + return this.length == 0 +} + +fun String.getSeparatorForLog(): String { + return StringBuilder().let { + for (i in 0 until length) { + it.append("=") + } + it.toString() + } +} + +fun String.convertUtcToLocalDate(): Date { + val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) + val convertDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val timeZone = TimeZone.getDefault() + + var parseDate = format.parse(this) + + val convertedDate = convertDateFormat.format(parseDate) + parseDate = convertDateFormat.parse(convertedDate) + + val locTime = convertDateFormat.format(parseDate.time + timeZone.getOffset(parseDate.time)).replace("+0000", "") + + val retDate = convertDateFormat.parse(locTime) + + return retDate +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/TextViewExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/TextViewExtension.kt new file mode 100644 index 0000000000..1292fe116e --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/TextViewExtension.kt @@ -0,0 +1,20 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +import android.text.InputFilter +import android.widget.EditText + +internal fun EditText.setRange(min: Int, max: Int) { + filters = arrayOf(InputFilter { source, _, _, dest, _, _ -> + try { + val input = Integer.parseInt(dest.toString() + source.toString()) + + if (input in min..max) { + return@InputFilter null + } + } catch (e: NumberFormatException) { + e.printStackTrace() + } + + return@InputFilter "" + }) +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/ViewExtension.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/ViewExtension.kt new file mode 100644 index 0000000000..041ce8e2f6 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/ViewExtension.kt @@ -0,0 +1,34 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.extension + +import android.view.View + +fun View?.visible() = this?.run { visibility = View.VISIBLE } + +fun View?.visible(vararg views: View?) { + visible() + for (view in views) + view.visible() +} + +fun View?.invisible() = this?.run { visibility = View.INVISIBLE } + +fun View?.invisible(vararg views: View?) { + invisible() + for (view in views) + view.invisible() +} + +fun View?.gone() = this?.run { visibility = View.GONE } + +fun View?.gone(vararg views: View?) { + gone() + for (view in views) + view.gone() +} + +fun View?.setVisibleOrGone(visibleOrGone: Boolean, vararg views: View?) { + for (view in views) + if (visibleOrGone) view.visible() else view.gone() +} + +fun View?.setVisibleOrGone(visibleOrGone: Boolean) = setVisibleOrGone(visibleOrGone, this) diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/AlarmHelperActivity.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/AlarmHelperActivity.kt new file mode 100644 index 0000000000..d166a3d6e5 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/AlarmHelperActivity.kt @@ -0,0 +1,80 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.app.AlertDialog +import android.app.ProgressDialog +import android.os.Bundle +import info.nightscout.androidaps.activities.DialogAppCompatActivity +import info.nightscout.androidaps.core.R +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode +import info.nightscout.androidaps.plugins.pump.eopatch.event.EventDialog +import info.nightscout.androidaps.plugins.pump.eopatch.event.EventProgressDialog +import info.nightscout.androidaps.plugins.pump.eopatch.ui.dialogs.AlarmDialog +import info.nightscout.shared.sharedPreferences.SP +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import javax.inject.Inject + +class AlarmHelperActivity : DialogAppCompatActivity() { + @Inject lateinit var sp : SP + @Inject lateinit var rxBus: RxBus + + private var disposable: CompositeDisposable = CompositeDisposable() + private var mProgressDialog: ProgressDialog? = null + + @Override + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setTheme(R.style.AppTheme_NoActionBar) + + val alarmDialog = AlarmDialog() + alarmDialog.helperActivity = this + intent.getStringExtra("code")?.let{ + alarmDialog.code = it + alarmDialog.alarmCode = AlarmCode.fromStringToCode(it) + } + + alarmDialog.status = intent.getStringExtra("status")?:"" + alarmDialog.sound = intent.getIntExtra("soundid", R.raw.error) + alarmDialog.title = intent.getStringExtra("title")?:"" + if(alarmDialog.code != null) + alarmDialog.show(supportFragmentManager, "Alarm") + + disposable.add(rxBus + .toObservable(EventProgressDialog::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + if(it.show){ + showProgressDialog(it.resId) + }else{ + dismissProgressDialog() + } + }, { }) + ) + + disposable.add(rxBus + .toObservable(EventDialog::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + if(it.show) it.dialog.show(supportFragmentManager, "") + }, { }) + ) + } + + + private fun showProgressDialog(resId: Int){ + if (mProgressDialog == null && resId != 0) { + mProgressDialog = ProgressDialog(this).apply { + setMessage(getString(resId)) + setCancelable(false) + setProgressStyle(android.R.style.Widget_ProgressBar_Horizontal) + } + mProgressDialog?.show() + } + } + + private fun dismissProgressDialog(){ + mProgressDialog?.dismiss() + mProgressDialog = null + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/DialogHelperActivity.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/DialogHelperActivity.kt new file mode 100644 index 0000000000..cc30cfd3b4 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/DialogHelperActivity.kt @@ -0,0 +1,25 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.os.Bundle +import info.nightscout.androidaps.activities.DialogAppCompatActivity +import info.nightscout.androidaps.core.R +import info.nightscout.androidaps.plugins.pump.eopatch.ui.dialogs.ActivationNotCompleteDialog + +class DialogHelperActivity : DialogAppCompatActivity() { + @Override + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setTheme(R.style.AppTheme_NoActionBar) + + val dialog = ActivationNotCompleteDialog() + dialog.helperActivity = this + + dialog.title = intent.getStringExtra("title")?:"" + dialog.message = intent.getStringExtra("message")?:"" + dialog.show(supportFragmentManager, "Dialog") + } + + override fun onDestroy() { + super.onDestroy() + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseActivity.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseActivity.kt new file mode 100644 index 0000000000..badb13211f --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseActivity.kt @@ -0,0 +1,106 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.annotation.LayoutRes +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatDelegate +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.activities.NoSplashAppCompatActivity +import info.nightscout.androidaps.core.R +import info.nightscout.androidaps.plugins.pump.eopatch.EoPatchRxBus +import info.nightscout.androidaps.plugins.pump.eopatch.dagger.EopatchPluginQualifier +import info.nightscout.androidaps.plugins.pump.eopatch.extension.fillExtras +import info.nightscout.androidaps.plugins.pump.eopatch.extension.observeOnMainThread +import info.nightscout.androidaps.plugins.pump.eopatch.extension.subscribeDefault +import info.nightscout.androidaps.plugins.pump.eopatch.vo.ActivityResultEvent +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import javax.inject.Inject +import io.reactivex.rxkotlin.addTo + +abstract class EoBaseActivity : NoSplashAppCompatActivity(), EoBaseNavigator { + @Inject + @EopatchPluginQualifier + lateinit var viewModelFactory: ViewModelProvider.Factory + + protected lateinit var binding: B + + private val compositeDisposable = CompositeDisposable() + + @LayoutRes + abstract fun getLayoutId(): Int + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setTheme(R.style.AppTheme_NoActionBar) + + binding = DataBindingUtil.setContentView(this, getLayoutId()) + binding.lifecycleOwner = this + + } + + override fun onStart() { + super.onStart() + window.decorView.systemUiVisibility = if(AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_NO) + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + else + View.SYSTEM_UI_FLAG_VISIBLE + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + } + + override fun toast(message: String) { + Toast.makeText(this@EoBaseActivity, message, Toast.LENGTH_SHORT).show() + } + + override fun toast(@StringRes message: Int) { + Toast.makeText(this@EoBaseActivity, message, Toast.LENGTH_SHORT).show() + } + + override fun back() { + if(supportFragmentManager.backStackEntryCount == 0) { + finish() + } else { + supportFragmentManager.popBackStack() + } + } + + override fun finish(finishAffinity: Boolean) { + if(finishAffinity) { + finishAffinity() + } else { + finish() + } + } + + override fun startActivityForResult(action: Context.() -> Intent, requestCode: Int, vararg params: Pair) { + val intent = action(this) + if(params.isNotEmpty()) intent.fillExtras(params) + startActivityForResult(intent, requestCode) + } + + override fun checkCommunication(onSuccess: () -> Unit, onCancel: (() -> Unit)?, onDiscard: (() -> Unit)?, goHomeAfterDiscard: Boolean) { + EoPatchRxBus.listen(ActivityResultEvent::class.java) + .doOnSubscribe { startActivityForResult({ EopatchActivity.createIntentForCheckConnection(this, goHomeAfterDiscard) }, 10001) } + .observeOnMainThread() + .subscribeDefault { + if (it.requestCode == 10001) { + when (it.resultCode) { + RESULT_OK -> onSuccess.invoke() + RESULT_CANCELED -> onCancel?.invoke() + EopatchActivity.RESULT_DISCARDED -> onDiscard?.invoke() + } + } + }.addTo() + } + + fun Disposable.addTo() = addTo(compositeDisposable) +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseFragment.kt new file mode 100644 index 0000000000..14887253fc --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseFragment.kt @@ -0,0 +1,87 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.CallSuper +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.lifecycle.ViewModelProvider +import dagger.android.support.DaggerFragment +import info.nightscout.androidaps.plugins.pump.eopatch.dagger.EopatchPluginQualifier +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import javax.inject.Inject +import io.reactivex.rxkotlin.addTo + +abstract class EoBaseFragment : DaggerFragment(), EoBaseNavigator { + @Inject + @EopatchPluginQualifier + lateinit var viewModelFactory: ViewModelProvider.Factory + + protected var baseActivity: EoBaseActivity<*>? = null + + protected lateinit var binding: B + + private val compositeDisposable = CompositeDisposable() + + @LayoutRes + abstract fun getLayoutId(): Int + + @CallSuper + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is EoBaseActivity<*>) { + baseActivity = context + } + } + + @CallSuper + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false) + binding.lifecycleOwner = viewLifecycleOwner + return binding.root + } + + @CallSuper + override fun onDestroy() { + super.onDestroy() + compositeDisposable.dispose() + } + + @CallSuper + override fun onDetach() { + super.onDetach() + baseActivity = null + } + + override fun toast(message: String) { + baseActivity?.toast(message) + } + + override fun toast(message: Int) { + baseActivity?.toast(message) + } + + override fun back() { + baseActivity?.back() + } + + override fun finish(finishAffinity: Boolean) { + baseActivity?.finish(finishAffinity) + } + override fun startActivityForResult(action: Context.() -> Intent, requestCode: Int, vararg params: Pair) { + baseActivity?.startActivityForResult(action, requestCode, *params) + } + + override fun checkCommunication(onSuccess: () -> Unit, onCancel: (() -> Unit)?, onDiscard: (() -> Unit)?, goHomeAfterDiscard: Boolean) { + baseActivity?.checkCommunication(onSuccess, onCancel, onDiscard, goHomeAfterDiscard) + } + + fun Disposable.addTo() = addTo(compositeDisposable) + +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseNavigator.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseNavigator.kt new file mode 100644 index 0000000000..33e85f15e4 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseNavigator.kt @@ -0,0 +1,20 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.annotation.StringRes + +interface EoBaseNavigator { + fun toast(message: String) + + fun toast(@StringRes message: Int) + + fun back() + + fun finish(finishAffinity: Boolean = false) + + fun startActivityForResult(action: Context.() -> Intent, requestCode: Int, vararg params: Pair) + + fun checkCommunication(onSuccess: () -> Unit, onCancel: (() -> Unit)? = null, onDiscard: (() -> Unit)? = null, goHomeAfterDiscard: Boolean = true) +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchActivity.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchActivity.kt new file mode 100644 index 0000000000..da62afe986 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchActivity.kt @@ -0,0 +1,389 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.app.Dialog +import android.app.ProgressDialog +import android.content.Context +import android.content.Intent +import android.media.MediaPlayer +import android.media.RingtoneManager +import android.os.Bundle +import android.view.MotionEvent +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchLifecycle +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchStep +import info.nightscout.androidaps.plugins.pump.eopatch.code.EventType +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.ActivityEopatchBinding +import info.nightscout.androidaps.plugins.pump.eopatch.extension.replaceFragmentInActivity +import info.nightscout.androidaps.plugins.pump.eopatch.extension.takeOne +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel +import info.nightscout.androidaps.utils.alertDialogs.AlertDialogHelper + +class EopatchActivity : EoBaseActivity() { + + private var mediaPlayer: MediaPlayer? = null + private var mPatchCommCheckDialog: Dialog? = null + private var mProgressDialog: ProgressDialog? = null + + override fun getLayoutId(): Int = R.layout.activity_eopatch + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (event.actionMasked == MotionEvent.ACTION_UP) { + binding.viewModel?.updateIncompletePatchActivationReminder() + } + + return super.dispatchTouchEvent(event) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding.apply { + viewModel = ViewModelProvider(this@EopatchActivity, viewModelFactory).get(EopatchViewModel::class.java) + viewModel?.apply { + processIntent(intent) + + patchStep.observe(this@EopatchActivity, { + when (it) { + PatchStep.SAFE_DEACTIVATION -> { + if(isActivated.value?:false){ + setupViewFragment(EopatchSafeDeactivationFragment.newInstance()) + }else{ + this@EopatchActivity.finish() + } + } + PatchStep.MANUALLY_TURNING_OFF_ALARM -> setupViewFragment(EopatchTurningOffAlarmFragment.newInstance()) + PatchStep.DISCARDED_FOR_CHANGE, + PatchStep.DISCARDED_FROM_ALARM, + PatchStep.DISCARDED -> setupViewFragment(EopatchRemoveFragment.newInstance()) + PatchStep.WAKE_UP -> setupViewFragment(EopatchWakeUpFragment.newInstance()) + PatchStep.CONNECT_NEW -> setupViewFragment(EopatchConnectNewFragment.newInstance()) + PatchStep.REMOVE_NEEDLE_CAP -> setupViewFragment(EopatchRemoveNeedleCapFragment.newInstance()) + PatchStep.REMOVE_PROTECTION_TAPE -> setupViewFragment(EopatchRemoveProtectionTapeFragment.newInstance()) + PatchStep.SAFETY_CHECK -> setupViewFragment(EopatchSafetyCheckFragment.newInstance()) + PatchStep.ROTATE_KNOB_NEEDLE_INSERTION_ERROR, + PatchStep.ROTATE_KNOB -> setupViewFragment(EopatchRotateKnobFragment.newInstance()) + PatchStep.BASAL_SCHEDULE -> setupViewFragment(EopatchBasalScheduleFragment.newInstance()) + // PatchStep.SETTING_REMINDER_TIME -> setupViewFragment(PatchExpirationReminderSettingFragment.newInstance()) + PatchStep.CHECK_CONNECTION -> { + checkCommunication({ + setResult(RESULT_OK) + this@EopatchActivity.finish() + }, { + setResult(RESULT_CANCELED) + this@EopatchActivity.finish() + }, { + setResult(RESULT_DISCARDED) + + if (intent.getBooleanExtra(EXTRA_GO_HOME, true)) { + backToHome(false) + } else { + this@EopatchActivity.finish() + } + }, doIntercept = true) + } + PatchStep.COMPLETE -> backToHome(true) + PatchStep.FINISH -> { + if (!intent.getBooleanExtra(EXTRA_START_FROM_MENU, false) + || intent.getBooleanExtra(EXTRA_GO_HOME, true)) { + backToHome(false) + } else { + this@EopatchActivity.finish() + } + } + PatchStep.BACK_TO_HOME -> backToHome(false) + PatchStep.CANCEL -> this@EopatchActivity.finish() + else -> Unit + } + }) + } + } + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + processIntent(intent) + } + + private fun processIntent(intent: Intent?) { + binding.viewModel?.apply { + + intent?.run { + val step = intent.getSerializableExtra(EXTRA_START_PATCH_STEP) as PatchStep? + + forceDiscard = intent.getBooleanExtra(EXTRA_FORCE_DISCARD, false) + if (intent.getBooleanExtra(EXTRA_START_WITH_COMM_CHECK, false)) { + checkCommunication({ + initializePatchStep(step) + }, { + setResult(RESULT_CANCELED) + this@EopatchActivity.finish() + }, { + setResult(RESULT_DISCARDED) + this@EopatchActivity.finish() + }, doPreCheck = true) + } else { + initializePatchStep(step) + } + } + + UIEventTypeHandler.observe(this@EopatchActivity, Observer { evt -> + when (evt.peekContent()) { + EventType.SHOW_PATCH_COMM_DIALOG -> { + if (mProgressDialog == null) { + mProgressDialog = ProgressDialog(this@EopatchActivity).apply { + setMessage(getString(evt.value as Int)) + setCancelable(false) + setProgressStyle(android.R.style.Widget_ProgressBar_Horizontal) + } + mProgressDialog?.show() + } + } + + EventType.DISMISS_PATCH_COMM_DIALOG -> { + dismissProgressDialog() + // dismissRetryDialog() + } + + EventType.SHOW_PATCH_COMM_ERROR_DIALOG -> { + dismissRetryDialog() + if (patchStep.value?.isSafeDeactivation?:false || connectionTryCnt >= 2) { + val cancelLabel = commCheckCancelLabel.value?:getString(R.string.cancel) + val message = "${getString(R.string.patch_comm_error_during_discard_desc_2)}\n${getString(R.string.patch_communication_check_helper_2)}" + mPatchCommCheckDialog = AlertDialogHelper.Builder(this@EopatchActivity) + .setTitle(R.string.patch_communication_failed) + .setMessage(message) + .setCancelable(false) + .setPositiveButton(R.string.discard, { dialog, which -> + discardPatch() + }) + .setNegativeButton(cancelLabel, { dialog, which -> + cancelPatchCommCheck() + }) + .show() + }else{ + val cancelLabel = commCheckCancelLabel.value?:getString(R.string.cancel) + val message = "${getString(R.string.patch_communication_check_helper_1)}\n${getString(R.string.patch_communication_check_helper_2)}" + mPatchCommCheckDialog = AlertDialogHelper.Builder(this@EopatchActivity) + .setTitle(R.string.patch_communication_failed) + .setMessage(message) + .setCancelable(false) + .setPositiveButton(R.string.retry, { dialog, which -> + retryCheckCommunication() + }) + .setNegativeButton(cancelLabel, { dialog, which -> + cancelPatchCommCheck() + }) + .show() + } + } + + EventType.SHOW_BONDED_DIALOG -> { + dismissProgressDialog() + AlertDialogHelper.Builder(this@EopatchActivity) + .setTitle(R.string.patch_communication_succeed) + .setMessage(R.string.patch_communication_succeed_message) + .setPositiveButton(R.string.confirm, { dialog, which -> + dismissPatchCommCheckDialogInternal(true) + }).show() + } + + EventType.SHOW_CHANGE_PATCH_DIALOG -> { + AlertDialogHelper.Builder(this@EopatchActivity).apply { + setTitle(R.string.string_discard_patch) + setMessage(when { + patchState.isBolusActive && patchState.isTempBasalActive -> { + R.string.patch_change_confirm_bolus_and_temp_basal_are_active_desc + } + patchState.isBolusActive -> R.string.patch_change_confirm_bolus_is_active_desc + patchState.isTempBasalActive -> R.string.patch_change_confirm_temp_basal_is_active_desc + else -> R.string.patch_change_confirm_desc + }) + setPositiveButton(R.string.string_discard_patch, { dialog, which -> + deactivatePatch() + }) + setNegativeButton(R.string.cancel, { dialog, which -> + + }) + }.show() + } + EventType.SHOW_BONDED_DIALOG -> this@EopatchActivity.finish() + EventType.SHOW_DISCARD_DIALOG -> { + AlertDialogHelper.Builder(this@EopatchActivity).apply { + setTitle(R.string.string_discard_patch) + if (isBolusActive) { + setMessage(R.string.patch_change_confirm_bolus_is_active_desc) + } else { + setMessage(R.string.string_are_you_sure_to_discard_current_patch) + } + setPositiveButton(R.string.discard, { dialog, which -> + deactivate(true) { + dismissPatchCommCheckDialogInternal() + + try { + moveStep(isConnected.takeOne(PatchStep.DISCARDED, PatchStep.MANUALLY_TURNING_OFF_ALARM)) + } catch (e: IllegalStateException) { + this@EopatchActivity.finish() + } + } + }) + setNegativeButton(R.string.cancel, { dialog, which -> + dismissProgressDialog() + updateIncompletePatchActivationReminder() + }) + }.show() + + } + else -> Unit + } + }) + } + } + + private fun dismissProgressDialog(){ + mProgressDialog?.let { + try { + mProgressDialog?.dismiss() + } catch (e: IllegalStateException) { + } + mProgressDialog = null + } + } + + private fun dismissRetryDialog(){ + mPatchCommCheckDialog?.let { + try { + mPatchCommCheckDialog?.dismiss() + } catch (e: IllegalStateException) { + } + mPatchCommCheckDialog = null + } + } + + private fun backToHome(isActivated: Boolean) { + if (isActivated) { + mediaPlayer = MediaPlayer.create(this, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))?.apply { + setOnCompletionListener { + this@EopatchActivity.finish() + } + start() + return + } + } + + this@EopatchActivity.finish() + } + + override fun onDestroy() { + super.onDestroy() + + mediaPlayer?.let { + it.stop() + it.release() + mediaPlayer = null + } + } + + override fun onBackPressed() { + binding.viewModel?.apply{ + when(patchStep.value){ + PatchStep.SAFE_DEACTIVATION -> this@EopatchActivity.finish() + else -> Unit + } + } + } + + companion object { + const val RESULT_DISCARDED = RESULT_FIRST_USER + 1 + const val EXTRA_START_PATCH_STEP = "EXTRA_START_PATCH_FRAGMENT_UI" + const val EXTRA_START_FROM_MENU = "EXTRA_START_FROM_MENU" + const val EXTRA_START_WITH_COMM_CHECK = "EXTRA_START_WITH_COMM_CHECK" + const val EXTRA_GO_HOME = "EXTRA_GO_HOME" + const val EXTRA_FORCE_DISCARD = "EXTRA_FORCE_DISCARD" + @JvmField val PATCH_INITIAL_VOLTAGE_MIN = 2700 + @JvmField val NORMAL_TEMPERATURE_MIN = 4 + @JvmField val NORMAL_TEMPERATURE_MAX = 45 + + @JvmStatic + @JvmOverloads + fun createIntentForCheckConnection(context: Context, goHomeAfterDiscard: Boolean = true, forceDiscard: Boolean = false): Intent { + return Intent(context, EopatchActivity::class.java).apply { + putExtra(EXTRA_START_PATCH_STEP, PatchStep.CHECK_CONNECTION) + putExtra(EXTRA_GO_HOME, goHomeAfterDiscard) + putExtra(EXTRA_FORCE_DISCARD, forceDiscard) + } + } + + @JvmStatic + fun createIntentForActivatePatch(context: Context): Intent { + return createIntent(context, PatchStep.WAKE_UP, false) + } + + @JvmStatic + fun createIntentForChangePatch(context: Context): Intent { + return createIntent(context, PatchStep.SAFE_DEACTIVATION, false) + } + + @JvmStatic + @JvmOverloads + fun createIntentForDiscarded(context: Context, goHome: Boolean = true): Intent { + return createIntent(context, PatchStep.DISCARDED_FROM_ALARM, false).apply { + putExtra(EXTRA_GO_HOME, goHome) + } + } + + @JvmStatic + fun createIntentForCanularInsertionError(context: Context): Intent { + return createIntent(context, PatchStep.ROTATE_KNOB_NEEDLE_INSERTION_ERROR, false) + } + + @JvmStatic + @JvmOverloads + fun createIntent(context: Context, patchStep: PatchStep, doCommCheck: Boolean = true): Intent { + return Intent(context, EopatchActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) + putExtra(EXTRA_START_PATCH_STEP, patchStep) + putExtra(EXTRA_START_WITH_COMM_CHECK, doCommCheck) + } + } + + @JvmStatic + fun createIntentFromMenu(context: Context, patchStep: PatchStep): Intent { + return Intent(context, EopatchActivity::class.java).apply { + putExtra(EXTRA_START_PATCH_STEP, patchStep) + putExtra(EXTRA_START_FROM_MENU, true) + } + } + + @JvmStatic + fun createIntent(context: Context, lifecycle: PatchLifecycle, doCommCheck: Boolean): Intent? { + return when (lifecycle) { + PatchLifecycle.SHUTDOWN -> { + // if (PatchConfig().hasMacAddress()) { + // createIntent(context, PatchStep.SAFE_DEACTIVATION, doCommCheck) + // } else { + createIntent(context, PatchStep.WAKE_UP, false) + // } + } + PatchLifecycle.BONDED -> createIntent(context, PatchStep.CONNECT_NEW, doCommCheck) + PatchLifecycle.REMOVE_NEEDLE_CAP -> createIntent(context, PatchStep.REMOVE_NEEDLE_CAP, doCommCheck) + PatchLifecycle.REMOVE_PROTECTION_TAPE -> createIntent(context, PatchStep.REMOVE_PROTECTION_TAPE, doCommCheck) + PatchLifecycle.SAFETY_CHECK -> createIntent(context, PatchStep.SAFETY_CHECK, doCommCheck) + PatchLifecycle.ROTATE_KNOB -> { + // val nextStep = PatchConfig().rotateKnobNeedleSensingError.takeOne( + // PatchStep.ROTATE_KNOB_NEEDLE_INSERTION_ERROR, PatchStep.ROTATE_KNOB) + // createIntent(context, nextStep, doCommCheck) + createIntent(context, PatchStep.ROTATE_KNOB, doCommCheck) + } + PatchLifecycle.BASAL_SETTING -> createIntent(context, PatchStep.ROTATE_KNOB, doCommCheck) + else -> null + } + } + } + + fun setupViewFragment(baseFragment: EoBaseFragment<*>) { + replaceFragmentInActivity(baseFragment, R.id.framelayout_fragment, false) + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchBasalScheduleFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchBasalScheduleFragment.kt new file mode 100644 index 0000000000..dbc40c87c4 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchBasalScheduleFragment.kt @@ -0,0 +1,25 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.FragmentEopatchBasalScheduleBinding +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel + +class EopatchBasalScheduleFragment : EoBaseFragment() { + + companion object { + fun newInstance(): EopatchBasalScheduleFragment = EopatchBasalScheduleFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_eopatch_basal_schedule + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(EopatchViewModel::class.java) + viewModel?.initPatchStep() + } + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchConnectNewFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchConnectNewFragment.kt new file mode 100644 index 0000000000..42c58e22ee --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchConnectNewFragment.kt @@ -0,0 +1,41 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel.SetupStep.* +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchStep +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.FragmentEopatchConnectNewBinding +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel + +class EopatchConnectNewFragment : EoBaseFragment() { + + companion object { + fun newInstance(): EopatchConnectNewFragment = EopatchConnectNewFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_eopatch_connect_new + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(EopatchViewModel::class.java) + viewModel?.apply { + setupStep.observe(viewLifecycleOwner, { + when (it) { + SCAN_FAILED, + BONDING_FAILED -> checkCommunication ({ retryScan() }, { moveStep(PatchStep.WAKE_UP) }) + GET_PATCH_INFO_FAILED -> checkCommunication ({ getPatchInfo() }, { moveStep(PatchStep.WAKE_UP) }) + SELF_TEST_FAILED -> checkCommunication ({ selfTest() }, { moveStep(PatchStep.WAKE_UP) }) + ACTIVATION_FAILED -> Toast.makeText(requireContext(), "Activation failed!", Toast.LENGTH_LONG).show() + else -> Unit + } + }) + + startScan() + } + } + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchOverviewFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchOverviewFragment.kt new file mode 100644 index 0000000000..063ffca8cb --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchOverviewFragment.kt @@ -0,0 +1,182 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.app.AlertDialog +import android.app.ProgressDialog +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import dagger.android.support.DaggerAppCompatActivity +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.EoPatchRxBus +import info.nightscout.androidaps.plugins.pump.eopatch.core.code.BolusType +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchStep +import info.nightscout.androidaps.plugins.pump.eopatch.code.EventType +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.FragmentEopatchOverviewBinding +import info.nightscout.androidaps.plugins.pump.eopatch.extension.fillExtras +import info.nightscout.androidaps.plugins.pump.eopatch.extension.observeOnMainThread +import info.nightscout.androidaps.plugins.pump.eopatch.extension.subscribeDefault +import info.nightscout.androidaps.plugins.pump.eopatch.extension.takeOne +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchOverviewViewModel +import info.nightscout.androidaps.plugins.pump.eopatch.vo.ActivityResultEvent +import io.reactivex.disposables.CompositeDisposable +import javax.inject.Inject + +class EopatchOverviewFragment: EoBaseFragment() { + @Inject lateinit var rxBus: RxBus + + private var disposable: CompositeDisposable = CompositeDisposable() + + private var mProgressDialog: ProgressDialog? = null + + override fun getLayoutId(): Int = R.layout.fragment_eopatch_overview + + override fun onDestroy() { + super.onDestroy() + disposable.clear() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.apply { + viewmodel = ViewModelProvider(this@EopatchOverviewFragment, viewModelFactory).get(EopatchOverviewViewModel::class.java) + viewmodel?.apply { + UIEventTypeHandler.observe(viewLifecycleOwner, Observer { evt -> + when(evt.peekContent()){ + EventType.ACTIVTION_CLICKED -> requireContext().let { startActivity(EopatchActivity.createIntentFromMenu(it, PatchStep.WAKE_UP)) } + EventType.DEACTIVTION_CLICKED -> requireContext().let { startActivity(EopatchActivity.createIntentForChangePatch(it)) } + EventType.SUSPEND_CLICKED -> suspend() + EventType.RESUME_CLICKED -> resume() + EventType.INVALID_BASAL_RATE -> Toast.makeText(activity, R.string.unsupported_basal_rate, Toast.LENGTH_SHORT).show() + EventType.PROFILE_NOT_SET -> Toast.makeText(activity, R.string.no_profile_selected, Toast.LENGTH_SHORT).show() + else -> Unit + } + }) + } + } + + + } + + private fun suspend() { + binding.viewmodel?.apply { + activity?.let { + val builder = AlertDialog.Builder(it) + val msg = getSuspendDialogText() + + val dialog = builder.setTitle(R.string.string_suspend) + .setMessage(msg) + .setPositiveButton(R.string.confirm, DialogInterface.OnClickListener { dialog, which -> + openPauseTimePicker() + }) + .setNegativeButton(R.string.cancel, DialogInterface.OnClickListener { dialog, which -> + + }).create() + dialog.show() + } + } + } + + private fun resume() { + binding.viewmodel?.apply { + activity?.let { + val builder = AlertDialog.Builder(it) + val dialog = builder.setTitle(R.string.string_resume_insulin_delivery_title) + .setMessage(R.string.string_resume_insulin_delivery_message) + .setPositiveButton(R.string.confirm, DialogInterface.OnClickListener { dialog, which -> + if(isPatchConnected) { + resumeBasal() + }else{ + checkCommunication({ + resumeBasal() + }) + } + }) + .setNegativeButton(R.string.cancel, DialogInterface.OnClickListener { dialog, which -> + + }).create() + dialog.show() + } + } + } + + private fun openPauseTimePicker() { + binding.viewmodel?.apply { + activity?.let{ + val builder = AlertDialog.Builder(it) + val listArr = requireContext().resources.getStringArray(R.array.suspend_duration_array) + var select = 0 + val dialog = builder.setTitle(R.string.string_suspend_time_insulin_delivery_title) + .setSingleChoiceItems(listArr, 0, DialogInterface.OnClickListener { dialog, which -> + select = which + }) + .setPositiveButton(R.string.confirm, DialogInterface.OnClickListener { dialog, which -> + if (isPatchConnected) { + pauseBasal((select + 1) * 0.5f) + } else { + checkCommunication({ + pauseBasal((select + 1) * 0.5f) + }) + } + }) + .setNegativeButton(R.string.cancel, DialogInterface.OnClickListener { dialog, which -> + + }).create() + dialog.show() + } + } + } + + private fun getSuspendDialogText(): String{ + binding.viewmodel?.apply { + val isBolusActive = patchManager.patchState.isBolusActive + val isTempBasalActive = patchManager.patchState.isTempBasalActive + val tempRate = patchManager.preferenceManager.getTempBasalManager().startedBasal?.doseUnitText ?: "" + val tempRemainTime = patchManager.preferenceManager.getTempBasalManager().startedBasal?.remainTimeText ?: "" + var remainBolus = patchManager.patchState.isNowBolusActive.takeOne(patchManager.bolusCurrent.remain(BolusType.NOW), 0f) + remainBolus += patchManager.patchState.isExtBolusActive.takeOne(patchManager.bolusCurrent.remain(BolusType.EXT), 0f) + + val sbMsg = StringBuilder() + + if (isBolusActive && isTempBasalActive) { + sbMsg.append(getString(R.string.insulin_suspend_msg1, tempRate, tempRemainTime, remainBolus)) + } else if (isBolusActive) { + sbMsg.append(getString(R.string.insulin_suspend_msg2, remainBolus)) + } else if (isTempBasalActive) { + sbMsg.append(getString(R.string.insulin_suspend_msg3, tempRate, tempRemainTime)) + } else { + sbMsg.append(getString(R.string.insulin_suspend_msg4)) + } + return sbMsg.toString() + } + return "" + } + + override fun startActivityForResult(action: Context.() -> Intent, requestCode: Int, vararg params: Pair) { + val intent = action(requireContext()) + if(params.isNotEmpty()) intent.fillExtras(params) + startActivityForResult(intent, requestCode) + } + + override fun checkCommunication(onSuccess: () -> Unit, onCancel: (() -> Unit)?, onDiscard: (() -> Unit)?, goHomeAfterDiscard: Boolean) { + EoPatchRxBus.listen(ActivityResultEvent::class.java) + .doOnSubscribe { startActivityForResult({ EopatchActivity.createIntentForCheckConnection(this, goHomeAfterDiscard) }, 10001) } + .observeOnMainThread() + .subscribeDefault { + if (it.requestCode == 10001) { + when (it.resultCode) { + DaggerAppCompatActivity.RESULT_OK -> onSuccess.invoke() + DaggerAppCompatActivity.RESULT_CANCELED -> onCancel?.invoke() + EopatchActivity.RESULT_DISCARDED -> onDiscard?.invoke() + } + } + }.addTo() + } + +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveFragment.kt new file mode 100644 index 0000000000..45ea3dbf13 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveFragment.kt @@ -0,0 +1,22 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.FragmentEopatchRemoveBinding +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel + +class EopatchRemoveFragment : EoBaseFragment() { + + companion object { + fun newInstance(): EopatchRemoveFragment = EopatchRemoveFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_eopatch_remove + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(EopatchViewModel::class.java) + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveNeedleCapFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveNeedleCapFragment.kt new file mode 100644 index 0000000000..b8cfe53ba2 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveNeedleCapFragment.kt @@ -0,0 +1,22 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.FragmentEopatchRemoveNeedleCapBinding +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel + +class EopatchRemoveNeedleCapFragment : EoBaseFragment() { + + companion object { + fun newInstance(): EopatchRemoveNeedleCapFragment = EopatchRemoveNeedleCapFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_eopatch_remove_needle_cap + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(EopatchViewModel::class.java) + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveProtectionTapeFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveProtectionTapeFragment.kt new file mode 100644 index 0000000000..85d822dc61 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveProtectionTapeFragment.kt @@ -0,0 +1,22 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.FragmentEopatchRemoveProtectionTapeBinding +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel + +class EopatchRemoveProtectionTapeFragment : EoBaseFragment() { + + companion object { + fun newInstance(): EopatchRemoveProtectionTapeFragment = EopatchRemoveProtectionTapeFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_eopatch_remove_protection_tape + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(EopatchViewModel::class.java) + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRotateKnobFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRotateKnobFragment.kt new file mode 100644 index 0000000000..cbeb1bc935 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRotateKnobFragment.kt @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.core.view.updateLayoutParams +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchStep +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.FragmentEopatchRotateKnobBinding +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel + +class EopatchRotateKnobFragment : EoBaseFragment() { + + companion object { + fun newInstance(): EopatchRotateKnobFragment = EopatchRotateKnobFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_eopatch_rotate_knob + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(EopatchViewModel::class.java) + viewModel?.apply { + initPatchStep() + + if (patchStep.value == PatchStep.ROTATE_KNOB_NEEDLE_INSERTION_ERROR) { + btnNegative.visibility = View.VISIBLE + guidelineButton.setGuidelinePercent(0.4f) + + btnPositive.apply { + updateLayoutParams { leftMargin = 3 } + text = getString(R.string.retry) + } + + layoutNeedleInsertionError.visibility = View.VISIBLE + textRotateKnobDesc2.visibility = View.GONE + textRotateKnobDesc2NeedleInsertionError.visibility = View.VISIBLE + } + + setupStep.observe(viewLifecycleOwner, { + when (it) { + EopatchViewModel.SetupStep.NEEDLE_SENSING_FAILED -> { + checkCommunication({ startNeedleSensing() }) + } + else -> Unit + } + }) + } + } + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchSafeDeactivationFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchSafeDeactivationFragment.kt new file mode 100644 index 0000000000..16d3ec0992 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchSafeDeactivationFragment.kt @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.FragmentEopatchSafeDeativationBinding +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel + +class EopatchSafeDeactivationFragment : EoBaseFragment() { + + companion object { + fun newInstance(): EopatchSafeDeactivationFragment = EopatchSafeDeactivationFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_eopatch_safe_deativation + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(EopatchViewModel::class.java).apply { + updateExpirationTime() + } + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchSafetyCheckFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchSafetyCheckFragment.kt new file mode 100644 index 0000000000..c4858f90af --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchSafetyCheckFragment.kt @@ -0,0 +1,38 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchStep +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.FragmentEopatchSafetyCheckBinding +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel.SetupStep.* + +class EopatchSafetyCheckFragment : EoBaseFragment() { + + companion object { + fun newInstance(): EopatchSafetyCheckFragment = EopatchSafetyCheckFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_eopatch_safety_check + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(EopatchViewModel::class.java) + viewModel?.apply { + initPatchStep() + + setupStep.observe(viewLifecycleOwner, { + when (it) { + SAFETY_CHECK_FAILED -> checkCommunication ({ retrySafetyCheck() }, { moveStep(PatchStep.SAFETY_CHECK) }) + else -> Unit + } + }) + + startSafetyCheck() + } + } + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchTurningOffAlarmFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchTurningOffAlarmFragment.kt new file mode 100644 index 0000000000..edaada43ca --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchTurningOffAlarmFragment.kt @@ -0,0 +1,22 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.FragmentEopatchTurningOffAlarmBinding +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel + +class EopatchTurningOffAlarmFragment : EoBaseFragment() { + + companion object { + fun newInstance(): EopatchTurningOffAlarmFragment = EopatchTurningOffAlarmFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_eopatch_turning_off_alarm + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(EopatchViewModel::class.java) + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchWakeUpFragment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchWakeUpFragment.kt new file mode 100644 index 0000000000..c95fcedd8b --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchWakeUpFragment.kt @@ -0,0 +1,25 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.FragmentEopatchWakeUpBinding +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel + +class EopatchWakeUpFragment : EoBaseFragment() { + + companion object { + fun newInstance(): EopatchWakeUpFragment = EopatchWakeUpFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_eopatch_wake_up + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(EopatchViewModel::class.java) + viewModel?.initPatchStep() + } + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/ActivationNotCompleteDialog.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/ActivationNotCompleteDialog.kt new file mode 100644 index 0000000000..e0c4bd0223 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/ActivationNotCompleteDialog.kt @@ -0,0 +1,82 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui.dialogs + +import android.os.Bundle +import android.view.* +import dagger.android.support.DaggerDialogFragment +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.pump.eopatch.bindingadapters.setOnSafeClickListener +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPatchManager +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.DialogCommonBinding +import info.nightscout.androidaps.plugins.pump.eopatch.ui.DialogHelperActivity +import info.nightscout.androidaps.plugins.pump.eopatch.ui.EopatchActivity +import io.reactivex.disposables.Disposable +import javax.inject.Inject + +class ActivationNotCompleteDialog : DaggerDialogFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var patchManager: IPatchManager + @Inject lateinit var rxBus: RxBus + + var helperActivity: DialogHelperActivity? = null + var message: String = "" + var title: String = "" + + private var _binding: DialogCommonBinding? = null + private var disposable: Disposable? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View { + dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE) + dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) + isCancelable = false + dialog?.setCanceledOnTouchOutside(false) + + savedInstanceState?.let { bundle -> + bundle.getString("title")?.let { title = it } + bundle.getString("message")?.let { message = it } + } + _binding = DialogCommonBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.title.text = title + binding.ok.setOnSafeClickListener { + helperActivity?.apply { + startActivity(EopatchActivity.createIntent(this, patchManager.patchConfig.lifecycleEvent.lifeCycle, false)) + } + dismiss() + } + } + + override fun onSaveInstanceState(bundle: Bundle) { + super.onSaveInstanceState(bundle) + bundle.putString("message", message) + bundle.putString("title", title) + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onResume() { + super.onResume() + binding.message.text = message + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun dismiss() { + super.dismissAllowingStateLoss() + helperActivity?.finish() + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/AlarmDialog.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/AlarmDialog.kt new file mode 100644 index 0000000000..9412d5a6f1 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/AlarmDialog.kt @@ -0,0 +1,169 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui.dialogs + +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.view.* +import dagger.android.support.DaggerDialogFragment +import info.nightscout.androidaps.core.R +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmProcess +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.IAlarmProcess +import info.nightscout.androidaps.plugins.pump.eopatch.bindingadapters.setOnSafeClickListener +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPatchManager +import info.nightscout.androidaps.plugins.pump.eopatch.databinding.DialogAlarmBinding +import info.nightscout.androidaps.plugins.pump.eopatch.extension.observeOnMainThread +import info.nightscout.androidaps.plugins.pump.eopatch.ui.AlarmHelperActivity +import info.nightscout.androidaps.services.AlarmSoundServiceHelper +import info.nightscout.androidaps.utils.T +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class AlarmDialog : DaggerDialogFragment() { + + @Inject lateinit var alarmSoundServiceHelper: AlarmSoundServiceHelper + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var patchManager: IPatchManager + @Inject lateinit var rxBus: RxBus + + var helperActivity: AlarmHelperActivity? = null + var alarmCode: AlarmCode? = null + var code: String = "" + var status: String = "" + var title: String = "" + var sound: Int = 0 + + private lateinit var mAlarmProcess: IAlarmProcess + private var loopHandler = Handler() + + private var _binding: DialogAlarmBinding? = null + private var disposable: Disposable? = null + private val binding get() = _binding!! + + private var isHolding = false + private var isMute = false + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View { + mAlarmProcess = AlarmProcess(patchManager, rxBus) + + dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE) + dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) + isCancelable = false + dialog?.setCanceledOnTouchOutside(false) + + savedInstanceState?.let { bundle -> + bundle.getString("status")?.let { status = it } + bundle.getString("title")?.let { title = it } + bundle.getString("code")?.let { + code = it + alarmCode = AlarmCode.fromStringToCode(it) + } + sound = bundle.getInt("sound", R.raw.error) + } + aapsLogger.debug("Alarm dialog displayed") + _binding = DialogAlarmBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.title.text = title + binding.ok.setOnSafeClickListener { + aapsLogger.debug("USER ENTRY: Alarm dialog ok button pressed") + alarmCode?.let { ac -> + mAlarmProcess.doAction(requireContext(), ac) + .subscribeOn(Schedulers.io()) + .subscribe ({ ret -> + aapsLogger.debug("Alarm processing result :${ret}") + if (ret == IAlarmProcess.ALARM_HANDLED) { + alarmCode?.let{ + patchManager.preferenceManager.getAlarms().handle(it) + patchManager.preferenceManager.flushAlarms() + } + dismiss() + }else if (ret == IAlarmProcess.ALARM_PAUSE) { + isHolding = true + }else if (ret == IAlarmProcess.ALARM_UNHANDLED) { + if(!isMute){ + startAlarm() + } + } + }, { t -> aapsLogger.error("${t.printStackTrace()}") }) + } + stopAlarm() + } + binding.mute.setOnSafeClickListener { + aapsLogger.debug("USER ENTRY: Error dialog mute button pressed") + isMute = true + stopAlarm() + } + binding.mute5min.setOnSafeClickListener { + aapsLogger.debug("USER ENTRY: Error dialog mute 5 min button pressed") + stopAlarm() + isMute = true + loopHandler.postDelayed(this::startAlarm, T.mins(5).msecs()) + } + startAlarm() + + disposable = patchManager.observePatchLifeCycle() + .observeOnMainThread() + .subscribe { + if(it.isShutdown) { + activity?.finish() + } + } + + } + + override fun onSaveInstanceState(bundle: Bundle) { + super.onSaveInstanceState(bundle) + bundle.putString("status", status) + bundle.putString("title", title) + bundle.putInt("sound", sound) + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onResume() { + super.onResume() + if(isHolding && !isMute){ + startAlarm() + } + binding.status.text = status + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + disposable?.dispose() + disposable = null + } + + override fun dismiss() { + super.dismissAllowingStateLoss() + loopHandler.removeCallbacksAndMessages(null) + helperActivity?.finish() + } + + private fun startAlarm() { + if (sound != 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context?.let { context -> alarmSoundServiceHelper.startAlarm(context, sound) } + } + } + } + + private fun stopAlarm() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context?.let { context -> alarmSoundServiceHelper.stopService(context) } + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/CommonDialog.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/CommonDialog.kt new file mode 100644 index 0000000000..5becbcf5f2 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/CommonDialog.kt @@ -0,0 +1,45 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui.dialogs + +import android.app.AlertDialog +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import dagger.android.support.DaggerDialogFragment +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.androidaps.plugins.pump.eopatch.R +import java.lang.IllegalStateException +import javax.inject.Inject + +class CommonDialog : DaggerDialogFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + + var title: Int = 0 + var message: Int = 0 + var positiveBtn: Int = R.string.confirm + var negativeBtn: Int = 0 + + var positiveListener: DialogInterface.OnClickListener? = null + var negativeListener: DialogInterface.OnClickListener? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return activity?.let{ + val builder = AlertDialog.Builder(it).apply { + if(title != 0) setTitle(title) + if(message != 0) setMessage(message) + setPositiveButton(positiveBtn, + positiveListener?:DialogInterface.OnClickListener { dialog, which -> + dismiss() + }) + if(negativeBtn != 0) { + setNegativeButton(negativeBtn, + negativeListener ?: DialogInterface.OnClickListener { dialog, which -> + dismiss() + }) + } + } + + builder.create() + } ?: throw IllegalStateException("Activity is null") + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/event/SingleLiveEvent.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/event/SingleLiveEvent.kt new file mode 100644 index 0000000000..17a1d965f3 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/event/SingleLiveEvent.kt @@ -0,0 +1,30 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui.event + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.annotation.MainThread +import android.util.Log +import java.util.concurrent.atomic.AtomicBoolean + +open class SingleLiveEvent : MutableLiveData() { + private val mPending = AtomicBoolean(false) + override fun observe(owner: LifecycleOwner, observer: Observer) { + super.observe(owner, Observer { t -> + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t) + } + }) + } + + @MainThread + override fun setValue(t: T?) { + mPending.set(true) + super.setValue(t) + } + + @MainThread + fun call() { + value = null + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/event/UIEvent.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/event/UIEvent.kt new file mode 100644 index 0000000000..5b36648367 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/event/UIEvent.kt @@ -0,0 +1,8 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui.event + + +open class UIEvent(private val content: T) { + var value: Any? = null + fun peekContent(): T = content +} + diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/receiver/RxBroadcastReceiver.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/receiver/RxBroadcastReceiver.kt new file mode 100644 index 0000000000..5b6733ed4c --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/receiver/RxBroadcastReceiver.kt @@ -0,0 +1,64 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.annotation.CheckResult +import io.reactivex.android.MainThreadDisposable +import info.nightscout.androidaps.plugins.pump.eopatch.ui.receiver.RxBroadcastReceiver.BroadcastReceiverObservable +import io.reactivex.Observable +import io.reactivex.Observer +import java.lang.AssertionError + +class RxBroadcastReceiver private constructor() { + internal class BroadcastReceiverObservable : Observable { + + private val context: Context + private val intentFilter: IntentFilter + private val abortBroadcast: Boolean + + constructor(context: Context, intentFilter: IntentFilter) { + this.context = context + this.intentFilter = intentFilter + abortBroadcast = false + } + + constructor(context: Context, intentFilter: IntentFilter, abortBroadcast: Boolean) { + this.context = context + this.intentFilter = intentFilter + this.abortBroadcast = abortBroadcast + } + + override fun subscribeActual(observer: Observer) { + val listener: Listener = Listener(context, observer) + observer.onSubscribe(listener) + context.registerReceiver(listener.receiver, intentFilter) + } + + internal inner class Listener(private val context: Context, private val observer: Observer) : MainThreadDisposable() { + + val receiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (!isDisposed) { + observer.onNext(intent) + if (abortBroadcast) abortBroadcast() + } + } + } + + override fun onDispose() { + context.unregisterReceiver(receiver) + } + } + } + + companion object { + @CheckResult + fun create(context: Context, intentFilter: IntentFilter): Observable + = BroadcastReceiverObservable(context, intentFilter) + @CheckResult + fun create(context: Context, intentFilter: IntentFilter, abortBroadcast: Boolean): Observable + = BroadcastReceiverObservable(context, intentFilter, abortBroadcast) + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EoBaseViewModel.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EoBaseViewModel.kt new file mode 100644 index 0000000000..ff417e8b02 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EoBaseViewModel.kt @@ -0,0 +1,39 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel + +import android.view.MotionEvent +import android.view.View +import androidx.annotation.Keep +import androidx.lifecycle.ViewModel +import info.nightscout.androidaps.plugins.pump.eopatch.ui.EoBaseNavigator +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.rxkotlin.addTo +import java.lang.ref.WeakReference + +abstract class EoBaseViewModel : ViewModel() { + + private var _navigator: WeakReference? = null + var navigator: N? + set(value) { + _navigator = WeakReference(value) + } + get() = _navigator?.get() + + private val compositeDisposable = CompositeDisposable() + + override fun onCleared() { + compositeDisposable.clear() + super.onCleared() + } + + fun blockTouchEvent(view: View, motionEvent: MotionEvent): Boolean { + return true + } + + fun back() = navigator?.back() + + fun finish() = navigator?.finish() + + fun Disposable.addTo() = addTo(compositeDisposable) + +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EopatchOverviewViewModel.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EopatchOverviewViewModel.kt new file mode 100644 index 0000000000..471297965e --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EopatchOverviewViewModel.kt @@ -0,0 +1,233 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import info.nightscout.androidaps.interfaces.ActivePlugin +import info.nightscout.androidaps.interfaces.ProfileFunction +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.BleConnectionState +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPatchManager +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPreferenceManager +import info.nightscout.androidaps.plugins.pump.eopatch.code.EventType +import info.nightscout.androidaps.plugins.pump.eopatch.extension.observeOnMainThread +import info.nightscout.androidaps.plugins.pump.eopatch.extension.with +import info.nightscout.androidaps.plugins.pump.eopatch.ui.EoBaseNavigator +import info.nightscout.androidaps.plugins.pump.eopatch.ui.event.UIEvent +import info.nightscout.androidaps.plugins.pump.eopatch.ui.event.SingleLiveEvent +import info.nightscout.androidaps.plugins.pump.eopatch.vo.Alarms +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchConfig +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchState +import io.reactivex.Observable +import io.reactivex.disposables.Disposable +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlin.math.roundToInt + +class EopatchOverviewViewModel @Inject constructor( + private val context: Context, + val patchManager: IPatchManager, + val preferenceManager: IPreferenceManager, + val profileFunction: ProfileFunction, + val activePlugin: ActivePlugin +) : EoBaseViewModel() { + private val _eventHandler = SingleLiveEvent>() + val UIEventTypeHandler : LiveData> + get() = _eventHandler + + private val _patchConfig = SingleLiveEvent() + val patchConfig : LiveData + get() = _patchConfig + + private val _patchState = SingleLiveEvent() + val patchState : LiveData + get() = _patchState + + private val _normalBasal = SingleLiveEvent() + val normalBasal : LiveData + get() = _normalBasal + + private val _tempBasal = SingleLiveEvent() + val tempBasal : LiveData + get() = _tempBasal + + private val _bleStatus = SingleLiveEvent() + val bleStatus : LiveData + get() = _bleStatus + + private val _status = SingleLiveEvent() + val status : LiveData + get() = _status + + private val _alarms = SingleLiveEvent() + val alarms : LiveData + get() = _alarms + + private val _patchRemainingInsulin = MutableLiveData(0f) + + private var mDisposable: Disposable? = null + + val patchRemainingInsulin: LiveData + get() = Transformations.map(_patchRemainingInsulin) { insulin -> + when { + insulin > 50f -> "50+ U" + insulin < 1f -> "0 U" + else -> "${insulin.roundToInt()} U" + } + } + + val isPatchConnected: Boolean + get() = patchManager.patchConnectionState.isConnected + + init { + preferenceManager.observePatchConfig() + .observeOnMainThread() + .subscribe { _patchConfig.value = it } + .addTo() + + preferenceManager.observePatchState() + .observeOnMainThread() + .subscribe { + _patchState.value = it + _patchRemainingInsulin.value = it.remainedInsulin + updateBasalInfo() + updatePatchStatus() + } + .addTo() + + patchManager.observePatchConnectionState() + .observeOnMainThread() + .subscribe { + _bleStatus.value = when(it){ + BleConnectionState.CONNECTED -> "{fa-bluetooth}" + BleConnectionState.DISCONNECTED -> "{fa-bluetooth-b}" + else -> "{fa-bluetooth-b spin} ${context.getString(R.string.string_connecting)}" + } + } + .addTo() + + patchManager.observePatchLifeCycle() + .observeOnMainThread() + .subscribe { + updatePatchStatus() + } + .addTo() + + preferenceManager.observeAlarm() + .observeOnMainThread() + .subscribe { + _alarms.value = it + } + .addTo() + + if(preferenceManager.getPatchState().isNormalBasalPaused){ + startPeriodicallyUpdate() + }else { + updateBasalInfo() + } + } + + private fun updatePatchStatus(){ + if(patchManager.isActivated){ + var finishTimeMillis = patchConfig.value?.basalPauseFinishTimestamp?:System.currentTimeMillis() + var remainTimeMillis = Math.max(finishTimeMillis - System.currentTimeMillis(), 0L) + val h = TimeUnit.MILLISECONDS.toHours(remainTimeMillis) + val m = TimeUnit.MILLISECONDS.toMinutes(remainTimeMillis - TimeUnit.HOURS.toMillis(h)) + _status.value = if(patchManager.patchState.isNormalBasalPaused) + "${context.getString(R.string.string_suspended)}\n" + + "${context.getString(R.string.string_temp_basal_remained_hhmm, h.toString(), m.toString())}" + else + context.getString(R.string.string_running) + }else{ + _status.value = "" + } + } + + private fun updateBasalInfo(){ + if(patchManager.isActivated){ + _normalBasal.value = if(patchManager.patchState.isNormalBasalRunning) + "${preferenceManager.getNormalBasalManager().normalBasal.currentSegmentDoseUnitPerHour} U/hr" + else + "" + _tempBasal.value = if(patchManager.patchState.isTempBasalActive) + "${preferenceManager.getTempBasalManager().startedBasal?.doseUnitPerHour} U/hr" + else + "" + + }else{ + _normalBasal.value = "" + _tempBasal.value = "" + } + } + + fun onClickActivation(){ + val profile = profileFunction.getProfile() + + if(profile != null && profile.getBasal() >= 0.05) { + patchManager.preferenceManager.getNormalBasalManager().setNormalBasal(profile) + patchManager.preferenceManager.flushNormalBasalManager() + + _eventHandler.postValue(UIEvent(EventType.ACTIVTION_CLICKED)) + }else if(profile != null && profile.getBasal() < 0.05){ + _eventHandler.postValue(UIEvent(EventType.INVALID_BASAL_RATE)) + }else{ + _eventHandler.postValue(UIEvent(EventType.PROFILE_NOT_SET)) + } + } + + fun onClickDeactivation(){ + _eventHandler.postValue(UIEvent(EventType.DEACTIVTION_CLICKED)) + } + + fun onClickSuspendOrResume(){ + if(patchManager.patchState.isNormalBasalPaused) { + _eventHandler.postValue(UIEvent(EventType.RESUME_CLICKED)) + }else{ + _eventHandler.postValue(UIEvent(EventType.SUSPEND_CLICKED)) + } + } + + fun pauseBasal(pauseDurationHour: Float){ + patchManager.pauseBasal(pauseDurationHour) + .with() + .subscribe({ + if (it.isSuccess) { + navigator?.toast(R.string.string_suspended_insulin_delivery_message) + startPeriodicallyUpdate() + } else { + UIEvent(EventType.PAUSE_BASAL_FAILED).apply { value = pauseDurationHour }.let { _eventHandler.postValue(it) } + } + }, { + UIEvent(EventType.PAUSE_BASAL_FAILED).apply { value = pauseDurationHour }.let { _eventHandler.postValue(it) } + }).addTo() + } + + fun resumeBasal() { + patchManager.resumeBasal() + .with() + .subscribe({ + if (it.isSuccess) { + navigator?.toast(R.string.string_resumed_insulin_delivery_message) + stopPeriodicallyUpdate() + } else { + _eventHandler.postValue(UIEvent(EventType.RESUME_BASAL_FAILED)) + } + },{ + _eventHandler.postValue(UIEvent(EventType.RESUME_BASAL_FAILED)) + }).addTo() + } + + private fun startPeriodicallyUpdate(){ + if(mDisposable == null) { + mDisposable = Observable.interval(30, TimeUnit.SECONDS) + .observeOnMainThread() + .subscribe { updatePatchStatus() } + } + } + + private fun stopPeriodicallyUpdate(){ + mDisposable?.dispose() + mDisposable = null + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EopatchViewModel.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EopatchViewModel.kt new file mode 100644 index 0000000000..f6d73de6b0 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EopatchViewModel.kt @@ -0,0 +1,830 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel + +import android.content.Context +import android.content.res.Resources +import androidx.annotation.StringRes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.shared.logging.LTag +import info.nightscout.androidaps.plugins.pump.eopatch.CommonUtils +import info.nightscout.androidaps.plugins.pump.eopatch.R +import info.nightscout.androidaps.plugins.pump.eopatch.RxAction +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.IAlarmRegistry +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.BleConnectionState +import info.nightscout.androidaps.plugins.pump.eopatch.core.define.IPatchConstant +import info.nightscout.androidaps.plugins.pump.eopatch.ble.IPatchManager +import info.nightscout.androidaps.plugins.pump.eopatch.core.scan.PatchSelfTestResult.* +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchLifecycle +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchStep +import info.nightscout.androidaps.plugins.pump.eopatch.code.EventType +import info.nightscout.androidaps.plugins.pump.eopatch.extension.* +import info.nightscout.androidaps.plugins.pump.eopatch.ui.EoBaseNavigator +import info.nightscout.androidaps.plugins.pump.eopatch.ui.event.UIEvent +import info.nightscout.androidaps.plugins.pump.eopatch.ui.event.SingleLiveEvent +import info.nightscout.androidaps.plugins.pump.eopatch.vo.PatchLifecycleEvent +import info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel.EopatchViewModel.SetupStep.* +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.disposables.Disposable +import io.reactivex.subjects.PublishSubject +import java.lang.ref.WeakReference +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.roundToInt + +class EopatchViewModel @Inject constructor( + private val context: Context, + val patchManager: IPatchManager, + private val alarmRegistry: IAlarmRegistry, + private val aapsLogger: AAPSLogger +) : EoBaseViewModel() { + companion object { + private const val MAX_ELAPSED_MILLIS_AFTER_EXPIRATION = -12L * 60 * 60 * 1000 + } + var forceDiscard = false + var connectionTryCnt = 0 + + val patchConfig = patchManager.patchConfig + + val patchState = patchManager.patchState + + private val _isActivated = MutableLiveData(patchConfig.isActivated) + + private val _eventHandler = SingleLiveEvent>() + val UIEventTypeHandler : LiveData> + get() = _eventHandler + + fun onClickActivation(){ + _eventHandler.postValue(UIEvent(EventType.ACTIVTION_CLICKED)) + } + + private val mContentRef = WeakReference(context) + + private val mContext: Context? + get() = mContentRef.get() + + val patchStep = MutableLiveData() + + val isActivated = MutableLiveData(patchManager.isActivated) + val isBolusActive = patchManager.getPatchState().isBolusActive + val isConnected = patchManager.patchConnectionState.isConnected + + val patchRemainedInsulin: LiveData + get() = Transformations.map(_isActivated) { + it.takeOne(patchManager.patchState.remainedInsulin.let { insulin -> + when { + insulin > 50f -> 51 + insulin < 1f -> 0 + else -> insulin.roundToInt() + } + }, 0) + } + + private val _patchExpirationTimestamp = MutableLiveData(patchManager.patchExpiredTime) + + val patchRemainedDays: LiveData + get() = Transformations.map(_patchExpirationTimestamp) { + it.getDiffDays().toInt() + } + + val patchRemainedTime: LiveData + get() = Transformations.map(_patchExpirationTimestamp) { + it.diffTime(MAX_ELAPSED_MILLIS_AFTER_EXPIRATION) + } + + private val _title = MutableLiveData() + val title: LiveData + get() = _title + + private val _safetyCheckProgress = MutableLiveData(0) + val safetyCheckProgress: LiveData + get() = _safetyCheckProgress + + private val _patchExpirationReminderTime = MutableLiveData() + val patchExpirationReminderTime: LiveData + get() = _patchExpirationReminderTime + + private val _patchExpirationTime = MutableLiveData() + val patchExpirationTime: LiveData + get() = _patchExpirationTime + + private val _isCommCheckFailed = MutableLiveData(false) + val isCommCheckFailed: LiveData + get() = _isCommCheckFailed + + val isBonded: Boolean + get() = !patchConfig.lifecycleEvent.isShutdown + + val commCheckCancelLabel: LiveData + get() = Transformations.map(patchStep) { + mContext?.getString(when (it) { + PatchStep.CONNECT_NEW -> { + isBonded.takeOne(R.string.cancel, R.string.patch_cancel_pairing) + } + PatchStep.SAFE_DEACTIVATION -> R.string.patch_forced_discard + else -> R.string.cancel + }) ?: "" + } + + val programEnabledMessage: String + // get() = """'기초1' program has been enabled.""" + get() = mContext?.getString(R.string.patch_basal_schedule_desc_1,"기초1") ?: "" + + val patchStepIsSafeDeactivation: Boolean + get() = patchStep.value?.isSafeDeactivation ?: false + + private val _isDiscardedWithNotConn = MutableLiveData(false) + val isDiscardedWithNotConn: LiveData + get() = _isDiscardedWithNotConn + + private val isSubStepRunning: Boolean + get() = patchConfig.lifecycleEvent.isSubStepRunning + + private val initPatchStepIsSafeDeactivation: Boolean + get() = mInitPatchStep?.isSafeDeactivation ?: false + + private val initPatchStepIsCheckConnection: Boolean + get() = mInitPatchStep?.isCheckConnection ?: false + + // private var mProgressDialog: PatchProgressDialog? = null + + private var mCommCheckDisposable: Disposable? = null + + private var mOnCommCheckSuccessListener: (() -> Unit)? = null + + private var mOnCommCheckCancelListener: (() -> Unit)? = null + + private var mOnCommCheckDiscardListener: (() -> Unit)? = null + + private var mInitPatchStep: PatchStep? = null + + private val mMaxRetryCount = 3 + + private var mRetryCount = 0 + + private var mUpdateDisposable: Disposable? = null + + private var mB012UpdateDisposable: Disposable? = null + + private val mB012UpdateSubject = PublishSubject.create() + + // private var mCurrentTextDialog: TextDialog? = null + + init { + mB012UpdateDisposable = mB012UpdateSubject.hide() + .throttleFirst(500, TimeUnit.MILLISECONDS) + .delay(100, TimeUnit.MILLISECONDS) + .filter { isSubStepRunning } + .observeOnMainThread() + .flatMapMaybe { alarmRegistry.remove(AlarmCode.B012) } + .flatMapMaybe { alarmRegistry.add(AlarmCode.B012, TimeUnit.MINUTES.toMillis(3)) } + .subscribeDefault {} + + patchManager.observePatchLifeCycle() + .observeOnMainThread() + .subscribe { + isActivated.value = patchManager.isActivated + } + .addTo() + } + + private fun Long.diffTime(maxElapsed: Long): String { + val current = System.currentTimeMillis() + + return abs((this - current).let { (it > maxElapsed).takeOne(it, maxElapsed) }).let { millis -> + val hours = TimeUnit.MILLISECONDS.toHours(millis) + val minutes = TimeUnit.MILLISECONDS.toMinutes(millis + - TimeUnit.HOURS.toMillis(hours)) + val seconds = TimeUnit.MILLISECONDS.toSeconds(millis + - TimeUnit.HOURS.toMillis(hours) - TimeUnit.MINUTES.toMillis(minutes)) + + (this < current).takeOne("- ", "") +String.format( + "%02d:%02d:%02d", hours % 24, minutes, seconds) + } + } + + fun updateExpirationTime() { + CommonUtils.dispose(mUpdateDisposable) + + mUpdateDisposable = Observable.interval(0, 1, TimeUnit.SECONDS) + .observeOnMainThread() + .takeUntil { !patchConfig.isActivated } + .subscribeDefault { + _patchExpirationTimestamp.value = patchManager.patchExpiredTime + } + } + + @Synchronized + fun checkCommunication(onSuccessListener: () -> Unit, onCancelListener: (() -> Unit)? = null, + onDiscardListener: (() -> Unit)? = null, doPreCheck: Boolean = false, doIntercept: Boolean = false) { + // mPatchCommCheckDialog?.let { + // if (doIntercept) { + // mOnCommCheckSuccessListener = onSuccessListener + // mOnCommCheckCancelListener = onCancelListener + // mOnCommCheckDiscardListener = onDiscardListener + // + // if (_isCommCheckFailed.value == true) { + // retryCheckCommunication() + // } + // } + // + // } + + if (doPreCheck && patchManager.patchConnectionState.isConnected) { + onSuccessListener.invoke() + return + } + + mOnCommCheckSuccessListener = onSuccessListener + mOnCommCheckCancelListener = onCancelListener + mOnCommCheckDiscardListener = onDiscardListener + checkCommunicationInternal() + } + + fun retryCheckCommunication() { + updateIncompletePatchActivationReminder() + checkCommunicationInternal() + } + + private fun checkCommunicationInternal(timeout: Long = 8000) { + CommonUtils.dispose(mCommCheckDisposable) + + if(forceDiscard) + connectionTryCnt++ + + mCommCheckDisposable = if (isBonded) { + patchManager.observePatchConnectionState() + .timeout(timeout, TimeUnit.MILLISECONDS, + Observable.just(BleConnectionState.DISCONNECTED)) + .takeUntil { it == BleConnectionState.CONNECTED } + .last(BleConnectionState.DISCONNECTED) + .map { it == BleConnectionState.CONNECTED } + } else { + patchManager.scan(timeout) + .flatMap { + if (it.nearestDevice == null) + Single.error(Resources.NotFoundException()) + else + Single.just(true) + } + .retry(1) + } + .with() + .onErrorReturnItem(false) + .doOnSubscribe { showPatchCommCheckDialog() } + .doFinally { dismissPatchCommCheckDialog() } + .doOnError { aapsLogger.error(LTag.PUMP, it.message?:"Error") } + .subscribeDefault { + _isCommCheckFailed.value = !it + } + } + + fun showPatchCommCheckDialog(defaultFailedCondition: Boolean = false, @StringRes title: Int = R.string.string_connecting) { + _isCommCheckFailed.postValue(defaultFailedCondition) + _eventHandler.postValue(UIEvent(EventType.SHOW_PATCH_COMM_DIALOG).apply { + value = title + }) + } + + fun dismissPatchCommCheckDialogInternal(doOnSuccessOrCancel: Boolean? = null) { + _eventHandler.postValue(UIEvent(EventType.DISMISS_PATCH_COMM_DIALOG)) + doOnSuccessOrCancel?.let { + if (it) { + mOnCommCheckSuccessListener?.invoke() + } else { + mOnCommCheckCancelListener?.invoke() + } + } + mOnCommCheckSuccessListener = null + mOnCommCheckCancelListener = null + } + + private fun dismissPatchCommCheckDialog() { + if (_isCommCheckFailed.value == false) { + if (isBonded) { + _eventHandler.postValue(UIEvent(EventType.SHOW_BONDED_DIALOG)) + } else { + dismissPatchCommCheckDialogInternal(true) + // _eventHandler.postValue(Event(UserEvent.DISMISS_PATCH_COMM_DIALOG)) + } + } else { + // dismissPatchCommCheckDialogInternal(false) + _eventHandler.postValue(UIEvent(EventType.DISMISS_PATCH_COMM_DIALOG)) + _eventHandler.postValue(UIEvent(EventType.SHOW_PATCH_COMM_ERROR_DIALOG)) + } + } + + fun cancelPatchCommCheck() { + CommonUtils.dispose(mCommCheckDisposable) + updateIncompletePatchActivationReminder() + dismissPatchCommCheckDialogInternal(false) + } + + @Synchronized + private fun showProgressDialog(@StringRes label: Int) { + _eventHandler.postValue(UIEvent(EventType.SHOW_PATCH_COMM_DIALOG).apply { + value = label + }) + // if (mProgressDialog == null) { + // mProgressDialog = PatchProgressDialog() + // + // mProgressDialog?.let { + // navigator?.showDialog(it.apply { + // setLabel(label) + // }) + // } + // } + } + + @Synchronized + private fun dismissProgressDialog() { + _eventHandler.postValue(UIEvent(EventType.DISMISS_PATCH_COMM_DIALOG)) + // try { + // mProgressDialog?.dismiss() + // mProgressDialog = null + // navigator?.dismissProgressDialog() + // } catch (e: IllegalStateException) { } + } + + fun changePatch() { + _eventHandler.postValue(UIEvent(EventType.SHOW_CHANGE_PATCH_DIALOG)) + } + + fun discardPatchWithCommCheck() { + checkCommunication({ discardPatchInternal() }, doPreCheck = true) + } + + fun deactivatePatch(){ + if (patchManager.patchConnectionState.isConnected) { + deactivate(false) { + try { + moveStep(PatchStep.DISCARDED_FOR_CHANGE) + } catch (e: IllegalStateException) { + _eventHandler.postValue(UIEvent(EventType.FINISH_ACTIVITY)) + } + } + } else { + mOnCommCheckSuccessListener = { + deactivate(true) { + moveStep((PatchStep.DISCARDED_FOR_CHANGE)) + } + } + showPatchCommCheckDialog(true) + Single.timer(10, TimeUnit.SECONDS) + .doFinally{dismissPatchCommCheckDialog()} + .subscribe() + } + } + fun discardPatch() { + updateIncompletePatchActivationReminder() + discardPatchInternal() + } + + private fun discardPatchInternal() { + val isBolusActive = patchManager.preferenceManager.getPatchState().isBolusActive + + if (patchStep.value == PatchStep.SAFE_DEACTIVATION && !isBolusActive) { + deactivate(true) { + dismissPatchCommCheckDialogInternal() + moveStep(PatchStep.MANUALLY_TURNING_OFF_ALARM) + } + + return + } + + _eventHandler.postValue(UIEvent(EventType.SHOW_DISCARD_DIALOG)) + } + + fun onConfirm() { + when (patchStep.value) { + PatchStep.DISCARDED_FOR_CHANGE -> PatchStep.WAKE_UP + PatchStep.DISCARDED_FROM_ALARM -> PatchStep.FINISH + PatchStep.DISCARDED -> { + if (initPatchStepIsCheckConnection) { + mOnCommCheckDiscardListener?.invoke() /*?: navigator?.finish()*/ + mOnCommCheckDiscardListener = null + null + } else { + PatchStep.BACK_TO_HOME + } + } + PatchStep.MANUALLY_TURNING_OFF_ALARM -> { + initPatchStepIsSafeDeactivation.takeOne(PatchStep.DISCARDED_FOR_CHANGE, PatchStep.DISCARDED) + } + PatchStep.BASAL_SCHEDULE -> { + if (!patchManager.patchConnectionState.isConnected) { + checkCommunication({ moveStep(PatchStep.COMPLETE) }, { moveStep(PatchStep.BASAL_SCHEDULE) }) + null + } else { + PatchStep.COMPLETE + } + } + else -> null + }?.let { + moveStep(it) + } + } + + fun initPatchStep() { + when (patchStep.value) { + PatchStep.WAKE_UP -> { + setupStep.value = WAKE_UP_READY + } + PatchStep.SAFETY_CHECK -> { + setupStep.value = SAFETY_CHECK_READY + } + PatchStep.ROTATE_KNOB, + PatchStep.ROTATE_KNOB_NEEDLE_INSERTION_ERROR -> { + setupStep.value = NEEDLE_SENSING_READY + } + else -> Unit + } + } + + fun moveStep(newPatchStep: PatchStep) { + val oldPatchStep = patchStep.value + + if (oldPatchStep != newPatchStep) { + when (newPatchStep) { + PatchStep.REMOVE_NEEDLE_CAP -> PatchLifecycleEvent.createRemoveNeedleCap() + PatchStep.REMOVE_PROTECTION_TAPE -> PatchLifecycleEvent.createRemoveProtectionTape() + PatchStep.SAFETY_CHECK -> PatchLifecycleEvent.createSafetyCheck() + PatchStep.ROTATE_KNOB -> PatchLifecycleEvent.createRotateKnob() + PatchStep.WAKE_UP -> { + patchConfig.apply { + rotateKnobNeedleSensingError = false + } + PatchLifecycleEvent.createShutdown() + } + PatchStep.CANCEL -> { + if (!patchConfig.isActivated) { + PatchLifecycleEvent.createShutdown() + } else { + null + } + } + else -> null + }?.let { + patchManager.updatePatchLifeCycle(it) + } + } + + prepareStep(newPatchStep) + + aapsLogger.info(LTag.PUMP, "moveStep: $oldPatchStep -> $newPatchStep") + } + + fun initializePatchStep(step: PatchStep?, withAlarmHandle: Boolean = true) { + mInitPatchStep = prepareStep(step, withAlarmHandle) + dismissPatchCommCheckDialogInternal(false) + // dismissTextDialog() + } + + private fun prepareStep(step: PatchStep?, withAlarmHandle: Boolean = true): PatchStep { + (step ?: convertToPatchStep(patchConfig.lifecycleEvent.lifeCycle)).let { newStep -> + when (newStep) { + PatchStep.SAFE_DEACTIVATION -> R.string.string_discard_patch + PatchStep.DISCARDED, + PatchStep.DISCARDED_FROM_ALARM, + PatchStep.DISCARDED_FOR_CHANGE -> R.string.patch_discard_complete_title + PatchStep.MANUALLY_TURNING_OFF_ALARM -> R.string.patch_manually_turning_off_alarm_title + PatchStep.WAKE_UP, + PatchStep.CONNECT_NEW, + PatchStep.REMOVE_NEEDLE_CAP, + PatchStep.REMOVE_PROTECTION_TAPE, + PatchStep.SAFETY_CHECK, + PatchStep.ROTATE_KNOB, + PatchStep.ROTATE_KNOB_NEEDLE_INSERTION_ERROR, + PatchStep.BASAL_SCHEDULE -> R.string.string_activate_patch + PatchStep.SETTING_REMINDER_TIME -> R.string.patch_expiration_reminder_setting_title + else -> _title.value + }.let { + if (_title.value != it) { + _title.postValue(it) + } + } + + patchStep.postValue(newStep) + + if (withAlarmHandle) { + /* Alarm reset */ + when (newStep) { + PatchStep.REMOVE_NEEDLE_CAP, + PatchStep.REMOVE_PROTECTION_TAPE, PatchStep.SAFETY_CHECK, PatchStep.ROTATE_KNOB -> { + updateIncompletePatchActivationReminder(true) + } + PatchStep.COMPLETE, PatchStep.BASAL_SCHEDULE -> { + val now = System.currentTimeMillis() + val expireTimeStamp = patchConfig.expireTimestamp + Maybe.just(AlarmCode.B012) + .flatMap { alarmRegistry.remove(it) } + .flatMap { alarmRegistry.remove(AlarmCode.A020) } + .flatMap { alarmRegistry.add(AlarmCode.B005, expireTimeStamp - now) } + .flatMap { alarmRegistry.add(AlarmCode.B006, expireTimeStamp - now + IPatchConstant.SERVICE_TIME_MILLI - TimeUnit.HOURS.toMillis(1)) } + .flatMap { alarmRegistry.add(AlarmCode.A003, expireTimeStamp - now + IPatchConstant.SERVICE_TIME_MILLI) } + .subscribe() + } + + PatchStep.ROTATE_KNOB_NEEDLE_INSERTION_ERROR -> { + patchConfig.apply { + rotateKnobNeedleSensingError = true + } + + updateIncompletePatchActivationReminder(true) + + } + + PatchStep.CANCEL -> { + alarmRegistry.remove(AlarmCode.B012).subscribe() + } + + else -> { + } + } + } + + return newStep + } + } + + fun convertToPatchStep(lifecycle: PatchLifecycle) = when (lifecycle) { + PatchLifecycle.SHUTDOWN -> patchConfig.isDeactivated.takeOne( + PatchStep.WAKE_UP, PatchStep.SAFE_DEACTIVATION) + PatchLifecycle.BONDED -> PatchStep.CONNECT_NEW + PatchLifecycle.REMOVE_NEEDLE_CAP -> PatchStep.REMOVE_NEEDLE_CAP + PatchLifecycle.REMOVE_PROTECTION_TAPE -> PatchStep.REMOVE_PROTECTION_TAPE + PatchLifecycle.SAFETY_CHECK -> PatchStep.SAFETY_CHECK + PatchLifecycle.ROTATE_KNOB -> PatchStep.ROTATE_KNOB + PatchLifecycle.BASAL_SETTING -> PatchStep.ROTATE_KNOB + PatchLifecycle.ACTIVATED -> PatchStep.SAFE_DEACTIVATION + } + + private fun onClear() { + // _patchExpirationTime.value = null + // _rotateKnobRawRes.value = null + // _patchExpirationReminderTime.value = null + // _title.value = null + // mProgressDialog = null + // mPatchCommCheckDialog = null + // mCurrentTextDialog = null + mOnCommCheckSuccessListener = null + mOnCommCheckCancelListener = null + mOnCommCheckDiscardListener = null + CommonUtils.dispose(mCommCheckDisposable) + CommonUtils.dispose(mUpdateDisposable) + CommonUtils.dispose(mB012UpdateDisposable) + } + + override fun onCleared() { + super.onCleared() + onClear() + } + + enum class SetupStep { + WAKE_UP_READY, + SCAN_STARTED, + SCAN_FAILED, + BONDING_STARTED, + BONDING_FAILED, + GET_PATCH_INFO_STARTED, + GET_PATCH_INFO_FAILED, + SELF_TEST_STARTED, + SELF_TEST_FAILED, + SAFETY_CHECK_READY, + SAFETY_CHECK_STARTED, + SAFETY_CHECK_FAILED, + NEEDLE_SENSING_READY, + NEEDLE_SENSING_STARTED, + NEEDLE_SENSING_FAILED, + ACTIVATION_STARTED, + ACTIVATION_FAILED + } + + // 셋업 단계, UI 변경이 아닌 BLE 로직을 위한 SetupStep + val setupStep = MutableLiveData() + + private fun updateSetupStep(newSetupStep: SetupStep) { + aapsLogger.info(LTag.PUMP, "curSetupStep: ${setupStep.value}, newSetupStep: $newSetupStep") + setupStep.postValue(newSetupStep) + } + + @Synchronized + fun deactivate(force: Boolean, onSuccessListener: () -> Unit) { + patchManager.deactivate(6000, force) + .doOnSubscribe { + showProgressDialog(force.takeOne(R.string.string_in_progress, R.string.string_changing)) + } + .doFinally { + dismissProgressDialog() + } + .subscribeDefault { status -> + if (status.isDeactivated) { + onSuccessListener.invoke() + } else { + RxAction.runOnMainThread({ + checkCommunication({ deactivate(false, onSuccessListener) }, + { _eventHandler.postValue(UIEvent(EventType.FINISH_ACTIVITY)) }) + }, 100) + } + } + .addTo() + } + + fun deactivateInDisconnected() { + mOnCommCheckSuccessListener = { + deactivate(false) { moveStep((PatchStep.DISCARDED_FOR_CHANGE)) } + } + showPatchCommCheckDialog(true, R.string.string_discard_patch) + } + + @Synchronized + fun retryScan() { + if (mRetryCount <= mMaxRetryCount) { + startScanInternal() + } else { + moveStep(PatchStep.WAKE_UP) + } + } + + @Synchronized + fun startScan() { + if (isBonded) { + getPatchInfo() + } else { + mRetryCount = 0 + + startScanInternal() + } + } + + private fun startScanInternal() { + patchManager.scan(5000) + .flatMap { + if (it.nearestDevice == null) + Single.error(Resources.NotFoundException()) + else + Single.just(it.nearestDevice) + } + .onErrorReturnItem("") + .doOnSubscribe { updateSetupStep(SCAN_STARTED) } + .subscribeDefault { + if (!it.isNullOrEmpty()) { + startBond(it) + } else { + updateSetupStep(SCAN_FAILED) + } + }.addTo() + } + + @Synchronized + private fun startBond(scannedMacAddress: String) { + aapsLogger.info(LTag.PUMP, "startBond: $scannedMacAddress") + + patchManager.startBond(scannedMacAddress) + .doOnSubscribe { updateSetupStep(BONDING_STARTED) } + .filter { result -> result } + .toSingle() // 실패시 에러 반환. + .doOnSuccess { patchManager.updatePatchLifeCycle(PatchLifecycleEvent.createbonded()) } + .doOnError { + if (it is TimeoutException) { + moveStep(PatchStep.WAKE_UP) + } else { + updateSetupStep(BONDING_FAILED) + } + } + .subscribeDefault { + if (it) { + getPatchInfo() + } else { + updateSetupStep(BONDING_FAILED) + } + }.addTo() + } + + @Synchronized + fun getPatchInfo(timeout: Long = 60000) { + patchManager.getPatchInfo(timeout) + .doOnSubscribe { updateSetupStep(GET_PATCH_INFO_STARTED) } + .onErrorReturnItem(false) + .subscribeDefault { + if (it) { + selfTest(delayMs = 1000) + } else { + updateSetupStep(GET_PATCH_INFO_FAILED) + } + }.addTo() + } + + @Synchronized + fun selfTest(timeout: Long = 20000, delayMs: Long = 0) { + RxAction.runOnMainThread({ + patchManager.selfTest(timeout) + .doOnSubscribe { updateSetupStep(SELF_TEST_STARTED) } + .map { it == TEST_SUCCESS } + .onErrorReturnItem(false) + .subscribeDefault { + if (it) { + moveStep(PatchStep.REMOVE_NEEDLE_CAP) + } else if (!patchManager.patchConnectionState.isConnected) { + updateSetupStep(SELF_TEST_FAILED) + } + }.addTo() + }, delayMs) + } + + @Synchronized + fun retrySafetyCheck() { + if (mRetryCount <= mMaxRetryCount) { + startSafetyCheckInternal() + } else { + moveStep(PatchStep.REMOVE_NEEDLE_CAP) + } + } + + @Synchronized + fun startSafetyCheck() { + mRetryCount = 0 + + startSafetyCheckInternal() + } + + private fun startSafetyCheckInternal() { + patchManager.startPriming(10000, 100) + .doOnSubscribe { + _safetyCheckProgress.postValue(0) + updateSetupStep(SAFETY_CHECK_STARTED) + } + .doOnNext { _safetyCheckProgress.postValue(it.toInt()) } + .doOnError { updateSetupStep(SAFETY_CHECK_FAILED) } + .doOnComplete { moveStep(PatchStep.ROTATE_KNOB) } + .subscribeEmpty() + .addTo() + } + + @Synchronized + fun startNeedleSensing() { + patchManager.checkNeedleSensing(20000) + .toObservable() + .debounce(500, TimeUnit.MILLISECONDS) + .doOnSubscribe { + showProgressDialog(R.string.string_connecting) + updateSetupStep(NEEDLE_SENSING_STARTED) + } + .onErrorReturnItem(false) + .subscribeDefault { + if (it) { + startActivation() + } else { + if (!patchManager.patchConnectionState.isConnected) { + updateSetupStep(NEEDLE_SENSING_FAILED) + } + dismissProgressDialog() + } + }.addTo() + } + + @Synchronized + fun startActivation() { + patchManager.patchActivation(20000) + .doOnSubscribe { + showProgressDialog(R.string.string_connecting) + updateSetupStep(ACTIVATION_STARTED) + } + .doFinally { dismissProgressDialog() } + .onErrorReturnItem(false) + .subscribeDefault { + if (it) { + moveStep(PatchStep.COMPLETE) + } else { + updateSetupStep(ACTIVATION_FAILED) + } + }.addTo() + } + + fun updateIncompletePatchActivationReminder(forced: Boolean = false) { + if (forced || isSubStepRunning) { + mB012UpdateSubject.onNext(Unit) + } + } + + // @Synchronized + // private fun createTextDialog(): TextDialog { + // dismissTextDialog() + // + // return TextDialog().apply { + // mCurrentTextDialog = this + // } + // } + + // @Synchronized + // private fun dismissTextDialog() { + // mCurrentTextDialog?.dismiss() + // mCurrentTextDialog = null + // } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/ViewModelFactory.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/ViewModelFactory.kt new file mode 100644 index 0000000000..eb155e423a --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/ViewModelFactory.kt @@ -0,0 +1,39 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.MapKey +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton +import kotlin.reflect.KClass + +@MustBeDocumented +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@MapKey +internal annotation class ViewModelKey(val value: KClass) + +@Singleton +class ViewModelFactory @Inject constructor(private val creators: Map, @JvmSuppressWildcards Provider>) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + var creator: Provider? = creators[modelClass] + if (creator == null) { + for ((key, value) in creators) { + if (modelClass.isAssignableFrom(key)) { + creator = value + break + } + } + } + if (creator == null) { + throw IllegalArgumentException("unknown model class $modelClass") + } + try { + return creator.get() as T + } catch (e: Exception) { + throw IllegalStateException(e) + } + + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/ActivityResultEvent.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/ActivityResultEvent.kt new file mode 100644 index 0000000000..e67b291490 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/ActivityResultEvent.kt @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import android.content.Intent + +data class ActivityResultEvent( + val requestCode: Int, + val resultCode: Int, + val data: Intent? = null +) \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/Alarms.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/Alarms.kt new file mode 100644 index 0000000000..b4b011fc3c --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/Alarms.kt @@ -0,0 +1,108 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import info.nightscout.androidaps.plugins.pump.eopatch.GsonHelper +import info.nightscout.androidaps.plugins.pump.eopatch.alarm.AlarmCode +import info.nightscout.androidaps.plugins.pump.eopatch.code.SettingKeys +import info.nightscout.shared.sharedPreferences.SP +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject +import java.util.* + +class Alarms(): IPreference { + @Transient + private val subject: BehaviorSubject = BehaviorSubject.create() + + class AlarmItem { + lateinit var alarmCode: AlarmCode + var createTimestamp = 0L + var triggerTimeMilli = 0L + + override fun toString(): String { + return "AlarmItem(alarmCode=$alarmCode, createTimestamp=$createTimestamp, triggerTimeMilli=$triggerTimeMilli)" + } + } + + var registered = HashMap() + + var occured = HashMap() + + init { + initObject() + } + + fun initObject() { + } + + fun clear(){ + registered.clear() + occured.clear() + } + + fun update(other: Alarms) { + registered = other.registered + occured = other.occured + } + + fun register(alarmcode: AlarmCode, triggerAfter: Long) { + val item = AlarmItem().apply { + alarmCode = alarmcode + createTimestamp = System.currentTimeMillis() + triggerTimeMilli = createTimestamp + triggerAfter + } + if (isRegistered(alarmcode)){ + registered.remove(alarmcode) + } + registered.put(alarmcode, item) + + } + + fun unregister(alarmcode: AlarmCode) { + if (isRegistered(alarmcode)){ + registered.remove(alarmcode) + } + } + + fun occured(alarmcode: AlarmCode) { + val item: AlarmItem? = registered.get(alarmcode) + if (!isOccuring(alarmcode) && item != null) + occured.put(alarmcode, item) + if (isRegistered(alarmcode)) + registered.remove(alarmcode) + } + + fun handle(alarmcode: AlarmCode) { + if (isOccuring(alarmcode)) + occured.remove(alarmcode) + } + + fun isRegistered(alarmcode: AlarmCode): Boolean{ + return registered.containsKey(alarmcode) + } + + fun isOccuring(alarmcode: AlarmCode): Boolean{ + return occured.containsKey(alarmcode) + } + + override fun observe(): Observable { + return subject.hide() + } + + override fun flush(sp: SP){ + val jsonStr = GsonHelper.sharedGson().toJson(this) + sp.putString(SettingKeys.ALARMS, jsonStr) + subject.onNext(this) + } + + override fun toString(): String { + return "Alarms(subject=$subject, registered=${registered.keys}, occured=${occured.keys}" + } + + companion object { + const val NAME = "ALARMS" + @JvmStatic + fun createEmpty(): Alarms { + return Alarms() + } + } + +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/BasalSegment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/BasalSegment.kt new file mode 100644 index 0000000000..e0571ecc7a --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/BasalSegment.kt @@ -0,0 +1,45 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import com.google.android.gms.common.internal.Preconditions +import info.nightscout.androidaps.plugins.pump.eopatch.AppConstant + +data class BasalSegment (var start: Long, var end: Long, var doseUnitPerHour: Float) : SegmentEntity() { + + + override val isEmpty: Boolean + get() = doseUnitPerHour == 0f + + init { + Preconditions.checkArgument(start >= 0 && end > 0 && start < end) + Preconditions.checkArgument(start % 30 == 0L && end % 30 == 0L) + Preconditions.checkArgument(doseUnitPerHour >= 0) + this.startMinute = start + this.endMinute = end + } + + internal override fun duplicate(startMinute: Long, endMinute: Long): BasalSegment { + return BasalSegment(startMinute, endMinute, doseUnitPerHour) + } + + internal override fun deep(): BasalSegment { + return BasalSegment(startMinute, endMinute, doseUnitPerHour) + } + + override fun apply(segment: JoinedSegment) { + segment.doseUnitPerHour = doseUnitPerHour + } + + internal override fun equalValue(segment: BasalSegment): Boolean { + return segment != null && doseUnitPerHour == segment.doseUnitPerHour + } + + companion object { + fun create(startMinute: Long, endMinute: Long, doseUnitPerHour: Float): BasalSegment { + return BasalSegment(startMinute, endMinute, doseUnitPerHour) + } + + fun create(doseUnitPerHour: Float): BasalSegment { + return BasalSegment(AppConstant.DAY_START_MINUTE.toLong(), AppConstant.DAY_END_MINUTE.toLong(), doseUnitPerHour) + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/BolusCurrent.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/BolusCurrent.kt new file mode 100644 index 0000000000..3ddb1b66f3 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/BolusCurrent.kt @@ -0,0 +1,194 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import android.content.Context +import info.nightscout.androidaps.plugins.pump.eopatch.AppConstant +import info.nightscout.androidaps.plugins.pump.eopatch.GsonHelper +import info.nightscout.androidaps.plugins.pump.eopatch.core.code.BolusType +import info.nightscout.androidaps.plugins.pump.eopatch.core.util.FloatAdjusters +import info.nightscout.androidaps.plugins.pump.eopatch.code.SettingKeys +import info.nightscout.shared.sharedPreferences.SP +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject + +/** + * 볼루스 주입 형태 2가지 모드 + * + * + * Bolus : '즉시 주입 볼루스' 와 '연장 주입 볼루스' 를 포함한 의미. + * Now Bolus : '즉시 주입 볼루스' 를 의미. + * Extended Bolus : '연장 주입 볼루스' 를 의미. + * + * + * BolusCurrent : 현재 패치에서 진행 중인 '볼루스의 정보'를 표현한 클래스. + */ + +class BolusCurrent(): IPreference { + @Transient + private val subject: BehaviorSubject = BehaviorSubject.create() + + class Bolus { + var historyId: Long = 0L + + var injected = 0f + + var remain = 0f + + var startTimestamp = 0L + + var endTimestamp = 0L + + // 즉시 주입 볼루스의 종료시간을 보정했는지 여부 + var endTimeSynced = false + + var duration = 0L + + val totalDoseU: Float + get() = injected + remain + + fun startBolus(id: Long, targetDoseU: Float, start: Long, end: Long, duration: Long = 0L) { + this.historyId = id + this.injected = 0f + this.remain = targetDoseU // 남은 양에 설정한다 + this.startTimestamp = start + this.endTimestamp = end + this.endTimeSynced = false + this.duration = duration + } + + fun clearBolus() { + this.historyId = 0 + this.injected = 0f + this.remain = 0f + this.startTimestamp = 0 + this.endTimestamp = 0 + this.endTimeSynced = false + this.duration = 0L + } + + fun update(injected: Int, remain: Int) { + this.injected = FloatAdjusters.FLOOR2_BOLUS.apply(injected * AppConstant.INSULIN_UNIT_P) + this.remain = FloatAdjusters.FLOOR2_BOLUS.apply(remain * AppConstant.INSULIN_UNIT_P) + } + + fun updateTimeStamp(start: Long, end: Long) { + this.startTimestamp = start + this.endTimestamp = end + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Bolus + + if (historyId != other.historyId) return false + if (injected != other.injected) return false + if (remain != other.remain) return false + if (startTimestamp != other.startTimestamp) return false + if (endTimestamp != other.endTimestamp) return false + if (endTimeSynced != other.endTimeSynced) return false + return true + } + + override fun hashCode(): Int { + var result = historyId.hashCode() + result = 31 * result + injected.hashCode() + result = 31 * result + remain.hashCode() + result = 31 * result + startTimestamp.hashCode() + result = 31 * result + endTimestamp.hashCode() + result = 31 * result + endTimeSynced.hashCode() + return result + } + + override fun toString(): String = + when (historyId) { + 0L -> "Bolus(NONE)" + else -> "Bolus(id=$historyId, i=$injected, r=$remain, start=$startTimestamp, end=$endTimestamp, synced=$endTimeSynced)" + } + } + + var nowBolus: Bolus = Bolus() + var extBolus: Bolus = Bolus() + + private fun getBolus(type: BolusType): Bolus = + when (type) { + BolusType.NOW -> nowBolus + BolusType.EXT -> extBolus + else -> nowBolus + } + + fun historyId(t: BolusType) = getBolus(t).historyId + fun injected(t: BolusType) = getBolus(t).injected + fun remain(t: BolusType) = getBolus(t).remain + fun startTimestamp(t: BolusType) = getBolus(t).startTimestamp + fun endTimestamp(t: BolusType) = getBolus(t).endTimestamp + fun endTimeSynced(t: BolusType) = getBolus(t).endTimeSynced + fun totalDoseU(t: BolusType) = getBolus(t).totalDoseU + fun duration(t: BolusType) = getBolus(t).duration + + fun clearBolus(t: BolusType) = getBolus(t).clearBolus() + + fun clearAll() { + clearBolus(BolusType.NOW) + clearBolus(BolusType.EXT) + } + + fun setEndTimeSynced(t: BolusType, synced: Boolean) { + getBolus(t).endTimeSynced = synced + } + + fun startNowBolus(nowHistoryId: Long, targetDoseU: Float, startTimestamp: Long, endTimestamp: Long) { + nowBolus.startBolus(nowHistoryId, targetDoseU, startTimestamp, endTimestamp) + } + + fun startExtBolus(exHistoryId: Long, targetDoseU: Float, startTimestamp: Long, endTimestamp: Long, duration: Long) { + extBolus.startBolus(exHistoryId, targetDoseU, startTimestamp, endTimestamp, duration) + } + + fun updateBolusFromPatch(type: BolusType, injected: Int, remain: Int) { + getBolus(type).update(injected, remain) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BolusCurrent + + if (nowBolus != other.nowBolus) return false + if (extBolus != other.extBolus) return false + return true + } + + override fun observe(): Observable { + return subject.hide() + } + + override fun flush(sp: SP){ + val jsonStr = GsonHelper.sharedGson().toJson(this) + sp.putString(SettingKeys.BOLUS_CURRENT, jsonStr) + subject.onNext(this) + } + + override fun hashCode(): Int { + var result = nowBolus.hashCode() + result = 31 * result + extBolus.hashCode() + return result + } + + override fun toString(): String { + return "BolusCurrent(nowBolus=$nowBolus, extBolus=$extBolus)" + } + + companion object { + + const val NAME = "BOLUS_CURRENT" + + @JvmStatic + fun createEmpty(): BolusCurrent { + return BolusCurrent() + } + + } + +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/IPreference.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/IPreference.kt new file mode 100644 index 0000000000..225da1a390 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/IPreference.kt @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import info.nightscout.shared.sharedPreferences.SP +import io.reactivex.Observable + +interface IPreference{ + open fun flush(sp: SP) + open fun observe(): Observable +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/NormalBasal.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/NormalBasal.kt new file mode 100644 index 0000000000..016e98776c --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/NormalBasal.kt @@ -0,0 +1,213 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import android.util.MutableFloat +import info.nightscout.androidaps.plugins.pump.eopatch.AppConstant +import info.nightscout.androidaps.plugins.pump.eopatch.core.util.FloatAdjusters +import info.nightscout.androidaps.plugins.pump.eopatch.code.BasalStatus +import java.util.* +import java.util.concurrent.TimeUnit +import java.util.function.BiFunction + +class NormalBasal : SegmentsEntity() { + var status: BasalStatus = BasalStatus.SELECTED + @Synchronized + set(status) { + field = status + if (status == BasalStatus.STOPPED) { + this.segmentIndex = SEGMENT_INDEX_DEFAULT + } + } + + var segmentIndex = SEGMENT_INDEX_DEFAULT + + val maxDoseUnitPerHour: Float + get() { + val max = list.stream().map({ it.doseUnitPerHour }).mapToDouble({ it.toDouble() }).max().orElse(0.0).toFloat() + return FloatAdjusters.ROUND2_INSULIN.apply(max) + } + + val doseUnitPerSegmentArray: FloatArray + get() { + val doseArray = FloatArray(AppConstant.SEGMENT_COUNT_MAX) + + eachSegmentItem(BiFunction { index, segment -> + val dose = segment.doseUnitPerHour / 2 + if (index % 2 == 0) { + doseArray[index] = FloatAdjusters.CEIL2_BASAL_RATE.apply(dose) + } else { + doseArray[index] = FloatAdjusters.FLOOR2_BASAL_RATE.apply(dose) + } + true + }) + return doseArray + } + + val doseUnitPerSegmentArrayForGraph: FloatArray + get() { + val doseArray = FloatArray(AppConstant.SEGMENT_COUNT_MAX) + + eachSegmentItem(BiFunction { index, segment -> + doseArray[index] = FloatAdjusters.CEIL2_BASAL_RATE.apply(segment.doseUnitPerHour) + true + }) + return doseArray + } + + val doseUnitPerDay: Float + get() { + val total = MutableFloat(0f) + eachSegmentItem(BiFunction { index, segment -> + val dose = segment.doseUnitPerHour / 2 + if (index % 2 == 0) { + total.value += FloatAdjusters.CEIL2_BASAL_RATE.apply(dose) + } else { + total.value += FloatAdjusters.FLOOR2_BASAL_RATE.apply(dose) + } + true + }) + return total.value + } + + val currentSegmentDoseUnitPerHour: Float + get() = FloatAdjusters.ROUND2_INSULIN.apply(getSegmentDoseUnitPerHourByIndex(currentSegmentIndex)) + + val firstSegmentDoseUnitPerHour: Float + get() = FloatAdjusters.ROUND2_INSULIN.apply(getSegmentDoseUnitPerHourByIndex(0)) + + val currentSegmentIndex: Int + get() { + val cal = Calendar.getInstance() + var idx = cal.get(Calendar.HOUR_OF_DAY) * 2 + if (cal.get(Calendar.MINUTE) >= 30) { + idx += 1 + } + return idx + } + + val isDoseUChanged: Boolean + get() { + val currentSegmentIndex = currentSegmentIndex + if (segmentIndex != SEGMENT_INDEX_DEFAULT && segmentIndex != currentSegmentIndex) { + val beforeDoesU = getSegmentDoseUnitPerHourByIndex(segmentIndex) + val currentDoseU = getSegmentDoseUnitPerHourByIndex(currentSegmentIndex) + if (beforeDoesU != currentDoseU) { + return true + } + } + return false + } + + val startTime: Long + get() = getStartTime(currentSegmentIndex) + + init { + initObject() + } + + fun initObject() { + status = BasalStatus.SELECTED + list.add(BasalSegment.create(AppConstant.BASAL_RATE_PER_HOUR_MIN)) + } + + fun getSegmentDoseUnitPerHour(time: Long): Float { + return FloatAdjusters.ROUND2_INSULIN.apply(getSegmentDoseUnitPerHourByIndex(getSegmentIndex(time))) + } + + fun getSegmentDoseUnitPerHourByIndex(idx: Int): Float { + val defaultValue = 0f + for (seg in list) { + val startIndex = seg.startIndex + val endIndex = seg.endIndex + if (startIndex <= idx && idx < endIndex) { + return seg.doseUnitPerHour + } + } + return defaultValue + } + + private fun getSegmentIndex(time: Long): Int { + val cal = Calendar.getInstance() + cal.timeInMillis = time + var idx = cal.get(Calendar.HOUR_OF_DAY) * 2 + if (cal.get(Calendar.MINUTE) >= 30) { + idx += 1 + } + return idx + } + + fun getMaxBasal(durationMinutes: Long): Float { + val calendar = Calendar.getInstance() + calendar.timeInMillis = System.currentTimeMillis() + var hours = calendar.get(Calendar.HOUR_OF_DAY) + var minutes = calendar.get(Calendar.MINUTE) + + var startIndex = hours * 2 + minutes / 30 + startIndex = startIndex % AppConstant.SEGMENT_COUNT_MAX + + hours = (hours + durationMinutes / 60).toInt() + minutes = (minutes + durationMinutes % 60).toInt() + + var endIndex = hours * 2 + minutes / 30 + endIndex = endIndex % AppConstant.SEGMENT_COUNT_MAX + + val segments = doseUnitPerSegmentArrayForGraph + var maxBasal = segments[startIndex] + var i = startIndex + while (i != endIndex + 1) { + if (i >= AppConstant.SEGMENT_COUNT_MAX) { + i = i % AppConstant.SEGMENT_COUNT_MAX + } + maxBasal = Math.max(maxBasal, segments[i]) + i++ + } + return maxBasal + } + + fun isIndexChanged(): Boolean { + return (segmentIndex != SEGMENT_INDEX_DEFAULT && segmentIndex != currentSegmentIndex) + } + + @Synchronized + fun updateNormalBasalIndex(): Boolean { + val currentSegmentIndex = currentSegmentIndex + if (segmentIndex != SEGMENT_INDEX_DEFAULT) { + if (segmentIndex != currentSegmentIndex) { + segmentIndex = currentSegmentIndex + return true + } + } else { + segmentIndex = currentSegmentIndex + return true + } + return false + } + + fun getStartTime(segmentIndex: Int): Long { + val curIndexTime: Long + val calendar = Calendar.getInstance() + calendar.set(Calendar.HOUR_OF_DAY, 0) + calendar.set(Calendar.MINUTE, 0) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) + + curIndexTime = calendar.timeInMillis + TimeUnit.MINUTES.toMillis((segmentIndex * 30).toLong()) + return curIndexTime + } + + override fun toString(): String { + return "NormalBasal(status=$status, segmentIndex=$segmentIndex, list=$list)" + } + + companion object { + + private val HALF_HOUR = 0.5f + private val SEGMENT_INDEX_DEFAULT = -1 + + fun create(firstSegmentDoseUnitPerHour: Float): NormalBasal { + val b = NormalBasal() + b.status = BasalStatus.SELECTED + b.list[0].doseUnitPerHour = firstSegmentDoseUnitPerHour + return b + } + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/NormalBasalManager.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/NormalBasalManager.kt new file mode 100644 index 0000000000..a562e50d38 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/NormalBasalManager.kt @@ -0,0 +1,132 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import info.nightscout.androidaps.interfaces.Profile +import info.nightscout.androidaps.plugins.pump.eopatch.CommonUtils +import info.nightscout.androidaps.plugins.pump.eopatch.GsonHelper +import info.nightscout.androidaps.plugins.pump.eopatch.code.SettingKeys +import info.nightscout.androidaps.plugins.pump.eopatch.code.BasalStatus +import info.nightscout.shared.sharedPreferences.SP +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject +import java.util.concurrent.TimeUnit + +class NormalBasalManager() : IPreference { + @Transient + private val subject: BehaviorSubject = BehaviorSubject.create() + + var normalBasal: NormalBasal = NormalBasal() + + val isStarted: Boolean + get() = normalBasal.status.isStarted + + + init { + initObject() + } + + + fun initObject() { + } + + fun isEqual(profile: Profile?): Boolean{ + if(profile == null) false + + if(profile?.getBasalValues()?.size?:0 != normalBasal.list.size) false + + return profile?.let{prof -> + for(i in prof.getBasalValues().indices){ + if(TimeUnit.SECONDS.toMinutes(prof.getBasalValues()[i].timeAsSeconds.toLong()) != normalBasal.list.get(i).start){ + return false + } + if(CommonUtils.nearlyNotEqual(prof.getBasalValues()[i].value.toFloat(), normalBasal.list.get(i).doseUnitPerHour, 0.0000001f)){ + return false + } + } + return true + }?:false + } + + fun convertProfileToNormalBasal(profile: Profile): NormalBasal { + val tmpNormalBasal = NormalBasal() + tmpNormalBasal.list.clear() + + val size = profile.getBasalValues().size + for(idx in profile.getBasalValues().indices){ + val next_idx = if(idx == (size - 1)) 0 else idx + 1 + val st_mins = TimeUnit.SECONDS.toMinutes(profile.getBasalValues()[idx].timeAsSeconds.toLong()) + val et_mins = if(next_idx == 0) 1440 else TimeUnit.SECONDS.toMinutes(profile.getBasalValues()[next_idx].timeAsSeconds.toLong()) + + tmpNormalBasal.list.add(BasalSegment(st_mins, et_mins, profile.getBasalValues()[idx].value.toFloat())) + } + + return tmpNormalBasal + } + + fun setNormalBasal(profile: Profile) { + normalBasal.list.clear() + + val size = profile.getBasalValues().size + for(idx in profile.getBasalValues().indices){ + val next_idx = if(idx == (size - 1)) 0 else idx + 1 + val st_mins = TimeUnit.SECONDS.toMinutes(profile.getBasalValues()[idx].timeAsSeconds.toLong()) + val et_mins = if(next_idx == 0) 1440 else TimeUnit.SECONDS.toMinutes(profile.getBasalValues()[next_idx].timeAsSeconds.toLong()) + + normalBasal.list.add(BasalSegment(st_mins, et_mins, profile.getBasalValues()[idx].value.toFloat())) + } + } + + @Synchronized + fun updateBasalStarted() { + normalBasal.status = BasalStatus.STARTED + } + + @Synchronized + fun updateBasalPaused() { + normalBasal.status = BasalStatus.PAUSED + } + + @Synchronized + fun updateBasalSuspended() { + normalBasal.status = BasalStatus.SUSPENDED + } + + @Synchronized + fun isSuspended(): Boolean { + return normalBasal.status == BasalStatus.SUSPENDED + } + + @Synchronized + fun updateBasalSelecteded(index: Int) { + normalBasal.status = BasalStatus.SELECTED + } + + @Synchronized + fun updateBasalSelecteded() { + normalBasal.status = BasalStatus.SELECTED + } + + fun updateForDeactivation() { + // deactivation 할때는 SELECTED 상태로 변경 + updateBasalSelecteded() + } + + fun update(other: NormalBasalManager){ + normalBasal = other.normalBasal + } + + override fun observe(): Observable { + return subject.hide() + } + + override fun flush(sp: SP){ + val jsonStr = GsonHelper.sharedGson().toJson(this) + sp.putString(SettingKeys.NORMAL_BASAL, jsonStr) + subject.onNext(this) + } + + + override fun toString(): String { + return "NormalBasalManager(normalBasal=$normalBasal)" + } + +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchConfig.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchConfig.kt new file mode 100644 index 0000000000..c9389e7273 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchConfig.kt @@ -0,0 +1,401 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import com.google.android.gms.common.internal.Preconditions +import info.nightscout.androidaps.plugins.pump.eopatch.AppConstant +import info.nightscout.androidaps.plugins.pump.eopatch.CommonUtils +import info.nightscout.androidaps.plugins.pump.eopatch.FloatFormatters +import info.nightscout.androidaps.plugins.pump.eopatch.GsonHelper +import info.nightscout.androidaps.plugins.pump.eopatch.core.define.IPatchConstant.WARRANTY_OPERATING_LIFE_MILLI +import info.nightscout.androidaps.plugins.pump.eopatch.code.SettingKeys +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchLifecycle +import info.nightscout.shared.sharedPreferences.SP +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject +import java.util.concurrent.TimeUnit + +// @Singleton +class PatchConfig: IPreference { + @Transient + private val subject: BehaviorSubject = BehaviorSubject.create() + var securityValue: ByteArray = byteArrayOf(0, 0) + + var macAddress: String? = null + var lifecycleEvent: PatchLifecycleEvent = PatchLifecycleEvent() + var bolusNormalStartTimestamp = 0L + var bolusNormalEndTimestamp = 0L + var bolusNormalDoseU = 0f + var bolusExStartTimestamp = 0L + var bolusExEndTimestamp = 0L + var bolusExDoseU = 0f + var injectCount = 0 + var bgReminderMinute = 0L + var lastIndex = 0 + var lastDisconnectedTimestamp = 0L // 마지막 연결 종료 시간 + + var standardBolusInjectCount = 0 + var extendedBolusInjectCount = 0 + var basalInjectCount = 0 + + var patchFirmwareVersion: String? = null + var patchSerialNumber: String = "" + var patchLotNumber: String? = null + var patchModelName: String? = null + + var patchWakeupTimestamp = 0L + set(wakeupTimestamp) { + field = wakeupTimestamp + expireDurationMilli = WARRANTY_OPERATING_LIFE_MILLI + } + + var activatedTimestamp = 0L // 최초 연결 시간 + var expireDurationMilli = 0L // 패치 만료 기간 , 3일 (wake-up 시간을 기준으로 3일) + var basalPauseFinishTimestamp = 0L // 베이젤 일시중지 만료 시간 + var needleInsertionTryCount = 0 // 바늘삽입 시도 횟수 + + /* 패치와 API 통신으로 업데이트 값을 여기에 기록 중복 API 호출이 생기면 안되는 경우 여기에 */ + // SET_LOW_RESERVOIR_TASK + var LowReservoirAlertAmount = 10 + var patchExpireAlertTime = 4 + var infoReminder = false + + var pumpDurationSmallMilli = 0L // small + get(): Long = if (field != 0L) field else AppConstant.PUMP_DURATION_MILLI + var pumpDurationMediumMilli = 0L // medium + get(): Long = if (field != 0L) field else AppConstant.PUMP_DURATION_MILLI + var pumpDurationLargeMilli = 0L // large + get(): Long = if (field != 0L) field else AppConstant.PUMP_DURATION_MILLI + //var pumpDurationOcclusion = 0L // occul, 사용안함 + + var isEnterPrimaryScreen =false + // 기초 프로그램 변경시 BLE로 패치에 보내야 하기 때문에 마크한다. + var needSetBasalSchedule =false + + var sharedKey: ByteArray? = null + var seq15: Int = -1 + + var rotateKnobNeedleSensingError = false + + var remainedInsulin = 0f + //wake-up 시간을 기준으로 3.5일 + val expireTimestamp: Long + get() = patchWakeupTimestamp + expireDurationMilli + + val isExpired: Boolean + get() = System.currentTimeMillis() >= expireTimestamp + + val isActivated: Boolean + get() = this.lifecycleEvent.isActivated + + val isSubStepRunning: Boolean + get() = this.lifecycleEvent.isSubStepRunning + + val isBasalSetting: Boolean + get() = this.lifecycleEvent.isBasalSetting + + val isDeactivated: Boolean + get() = hasMacAddress() == false + + val isInBasalPausedTime: Boolean + get() = this.basalPauseFinishTimestamp > 0 && basalPauseFinishTimestamp > System.currentTimeMillis() + + val insulinInjectionAmount: Float + get() = injectCount * AppConstant.INSULIN_UNIT_P + + val insulinInjectionAmountStr: String + get() = FloatFormatters.insulin(injectCount * AppConstant.INSULIN_UNIT_P, "U") + + val bolusInjectionAmount: Float + get() = (standardBolusInjectCount + extendedBolusInjectCount) * AppConstant.INSULIN_UNIT_P + + val basalInjectionAmount: Float + get() = basalInjectCount * AppConstant.INSULIN_UNIT_P + + init { + initObject() + } + + fun initObject() { + this.lifecycleEvent = PatchLifecycleEvent() + this.lastIndex = 0 + } + + fun updateDeactivated() { + this.macAddress = null + this.patchFirmwareVersion = null + this.patchSerialNumber = "" + this.patchLotNumber = null + this.patchWakeupTimestamp = 0 + this.activatedTimestamp = 0 + this.expireDurationMilli = 0 + this.lifecycleEvent = PatchLifecycleEvent() + this.needleInsertionTryCount = 0 + this.bolusNormalStartTimestamp = 0 + this.bolusNormalEndTimestamp = 0 + this.bolusExStartTimestamp = 0 + this.bolusExEndTimestamp = 0 + this.injectCount = 0 + this.lastIndex = 0 + this.pumpDurationSmallMilli = 0 + this.pumpDurationMediumMilli = 0 + this.pumpDurationLargeMilli = 0 + this.needSetBasalSchedule = false + this.sharedKey = null + this.seq15 = -1 + this.standardBolusInjectCount = 0 + this.extendedBolusInjectCount = 0 + this.basalInjectCount = 0 + this.LowReservoirAlertAmount = 10 + this.patchExpireAlertTime = 4 + this.remainedInsulin = 0f + } + + fun patchFirmwareVersionString(): String? { + patchFirmwareVersion?.let { + var count = 0 + var i: Int = 0 + while (i < it.length) { + if (it[i] == '.') { + count++ + if (count == 3) { + return it.substring(0, i) + } + } + i++ + } + } + + return patchFirmwareVersion + } + + fun getPatchExpiredTime(): Long = + if (isActivated) expireTimestamp else -1L + + @Synchronized + fun incSeq() { + if (seq15 >= 0) { + seq15++ + } + if (seq15 > 0x7FFF) { + seq15 = 0 + } + } + + fun updateLifecycle(event: PatchLifecycleEvent) { + Preconditions.checkNotNull(event) + + /* 마지막 이력을 기록해 두어야 알람이 재발생하는 것을 막아야 함 */ + if (event.lifeCycle == lifecycleEvent.lifeCycle) { + return + } + + this.lifecycleEvent = event + when (event.lifeCycle) { + PatchLifecycle.SHUTDOWN -> { + updateDeactivated() + } + + PatchLifecycle.BONDED -> { + } + + PatchLifecycle.SAFETY_CHECK -> { + } + + PatchLifecycle.REMOVE_NEEDLE_CAP -> { + } + + PatchLifecycle.REMOVE_PROTECTION_TAPE -> { + } + + PatchLifecycle.ROTATE_KNOB -> { + } + + PatchLifecycle.BASAL_SETTING -> { + } + + PatchLifecycle.ACTIVATED -> { + // updateFirstConnected 이 부분으로 옮김. + this.activatedTimestamp = System.currentTimeMillis() + //this.expireDurationMilli = WARRANTY_OPERATING_LIFE_MILLI + //this.patchWakeupTimestamp = 0 getwakeuptime response 로 업데이트 됨. + this.needleInsertionTryCount = 0 + this.bolusNormalStartTimestamp = 0 + this.bolusNormalEndTimestamp = 0 + this.bolusExStartTimestamp = 0 + this.bolusExEndTimestamp = 0 + this.lastIndex = 0 + this.needSetBasalSchedule = false + } + else -> { + } + } + } + + fun updateNormalBasalPaused(pauseDurationHour: Float) { + Preconditions.checkArgument(pauseDurationHour == 0.5f || pauseDurationHour == 1.0f || pauseDurationHour == 1.5f || pauseDurationHour == 2.0f) + this.basalPauseFinishTimestamp = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis((pauseDurationHour * 60).toLong()) + } + + fun updateNormalBasalPausedSilently() { + this.basalPauseFinishTimestamp = 0L + } + + fun updateNormalBasalResumed() { + this.basalPauseFinishTimestamp = 0 + } + + fun updateNormalBasalStarted() { + this.basalPauseFinishTimestamp = 0 + } + + fun updateTempBasalStarted() { + this.basalPauseFinishTimestamp = 0 + } + + fun hasMacAddress(): Boolean { + return CommonUtils.hasText(macAddress) + } + + + fun updatetDisconnectedTime(){ + this.lastDisconnectedTimestamp = System.currentTimeMillis() + } + + fun update(other: PatchConfig){ + macAddress = other.macAddress + lifecycleEvent = other.lifecycleEvent + bolusNormalStartTimestamp = other.bolusNormalStartTimestamp + bolusNormalEndTimestamp = other.bolusNormalEndTimestamp + bolusNormalDoseU = other.bolusNormalDoseU + bolusExStartTimestamp = other.bolusExStartTimestamp + bolusExEndTimestamp = other.bolusExEndTimestamp + bolusExDoseU = other.bolusExDoseU + bgReminderMinute = other.bgReminderMinute + lastIndex = other.lastIndex + lastDisconnectedTimestamp = other.lastDisconnectedTimestamp + patchFirmwareVersion = other.patchFirmwareVersion + patchSerialNumber = other.patchSerialNumber + patchLotNumber = other.patchLotNumber + patchWakeupTimestamp = other.patchWakeupTimestamp + activatedTimestamp = other.activatedTimestamp + expireDurationMilli = other.expireDurationMilli + basalPauseFinishTimestamp = other.basalPauseFinishTimestamp + needleInsertionTryCount = other.needleInsertionTryCount + isEnterPrimaryScreen = other.isEnterPrimaryScreen + needSetBasalSchedule = other.needSetBasalSchedule + sharedKey = other.sharedKey + seq15 = other.seq15 + patchModelName = other.patchModelName + needleInsertionTryCount = other.needleInsertionTryCount + injectCount = other.injectCount + pumpDurationSmallMilli = other.pumpDurationSmallMilli + pumpDurationMediumMilli = other.pumpDurationMediumMilli + pumpDurationLargeMilli = other.pumpDurationLargeMilli + standardBolusInjectCount = other.standardBolusInjectCount + extendedBolusInjectCount = other.extendedBolusInjectCount + basalInjectCount = other.basalInjectCount + LowReservoirAlertAmount = other.LowReservoirAlertAmount + patchExpireAlertTime = other.patchExpireAlertTime + remainedInsulin = other.remainedInsulin + + subject.onNext(this) + } + + override fun observe(): Observable { + return subject.hide() + } + + override fun flush(sp: SP){ + val jsonStr = GsonHelper.sharedGson().toJson(this) + sp.putString(SettingKeys.PATCH_CONFIG, jsonStr) + subject.onNext(this) + } + + + + override fun toString(): String { + return "PatchConfig(securityValue=${securityValue.contentToString()}, macAddress=$macAddress, lifecycleEvent=$lifecycleEvent, bolusNormalStartTimestamp=$bolusNormalStartTimestamp, bolusNormalEndTimestamp=$bolusNormalEndTimestamp, bolusNormalDoseU=$bolusNormalDoseU, bolusExStartTimestamp=$bolusExStartTimestamp, bolusExEndTimestamp=$bolusExEndTimestamp, bolusExDoseU=$bolusExDoseU, injectCount=$injectCount, bgReminderMinute=$bgReminderMinute, lastIndex=$lastIndex, lastDisconnectedTimestamp=$lastDisconnectedTimestamp, standardBolusInjectCount=$standardBolusInjectCount, extendedBolusInjectCount=$extendedBolusInjectCount, basalInjectCount=$basalInjectCount, patchFirmwareVersion=$patchFirmwareVersion, patchSerialNumber='$patchSerialNumber', patchLotNumber=$patchLotNumber, patchModelName=$patchModelName, patchWakeupTimestamp=$patchWakeupTimestamp, activatedTimestamp=$activatedTimestamp, expireDurationMilli=$expireDurationMilli, basalPauseFinishTimestamp=$basalPauseFinishTimestamp, needleInsertionTryCount=$needleInsertionTryCount, LowReservoirAlertAmount=$LowReservoirAlertAmount, patchExpireAlertTime=$patchExpireAlertTime, isEnterPrimaryScreen=$isEnterPrimaryScreen, needSetBasalSchedule=$needSetBasalSchedule, sharedKey=${sharedKey?.contentToString()}, seq15=$seq15, rotateKnobNeedleSensingError=$rotateKnobNeedleSensingError, remainedInsulin=$remainedInsulin)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PatchConfig + + if (!securityValue.contentEquals(other.securityValue)) return false + if (macAddress != other.macAddress) return false + if (lifecycleEvent != other.lifecycleEvent) return false + if (bolusNormalStartTimestamp != other.bolusNormalStartTimestamp) return false + if (bolusNormalEndTimestamp != other.bolusNormalEndTimestamp) return false + if (bolusNormalDoseU != other.bolusNormalDoseU) return false + if (bolusExStartTimestamp != other.bolusExStartTimestamp) return false + if (bolusExEndTimestamp != other.bolusExEndTimestamp) return false + if (bolusExDoseU != other.bolusExDoseU) return false + if (injectCount != other.injectCount) return false + if (bgReminderMinute != other.bgReminderMinute) return false + if (lastIndex != other.lastIndex) return false + if (lastDisconnectedTimestamp != other.lastDisconnectedTimestamp) return false + if (standardBolusInjectCount != other.standardBolusInjectCount) return false + if (extendedBolusInjectCount != other.extendedBolusInjectCount) return false + if (basalInjectCount != other.basalInjectCount) return false + if (patchFirmwareVersion != other.patchFirmwareVersion) return false + if (patchSerialNumber != other.patchSerialNumber) return false + if (patchLotNumber != other.patchLotNumber) return false + if (patchModelName != other.patchModelName) return false + if (patchWakeupTimestamp != other.patchWakeupTimestamp) return false + if (activatedTimestamp != other.activatedTimestamp) return false + if (expireDurationMilli != other.expireDurationMilli) return false + if (basalPauseFinishTimestamp != other.basalPauseFinishTimestamp) return false + if (needleInsertionTryCount != other.needleInsertionTryCount) return false + if (LowReservoirAlertAmount != other.LowReservoirAlertAmount) return false + if (patchExpireAlertTime != other.patchExpireAlertTime) return false + if (isEnterPrimaryScreen != other.isEnterPrimaryScreen) return false + if (needSetBasalSchedule != other.needSetBasalSchedule) return false + if (sharedKey != null) { + if (other.sharedKey == null) return false + if (!sharedKey.contentEquals(other.sharedKey)) return false + } else if (other.sharedKey != null) return false + if (seq15 != other.seq15) return false + if (rotateKnobNeedleSensingError != other.rotateKnobNeedleSensingError) return false + if (remainedInsulin != other.remainedInsulin) return false + + return true + } + + override fun hashCode(): Int { + var result = securityValue.contentHashCode() + result = 31 * result + (macAddress?.hashCode() ?: 0) + result = 31 * result + lifecycleEvent.hashCode() + result = 31 * result + bolusNormalStartTimestamp.hashCode() + result = 31 * result + bolusNormalEndTimestamp.hashCode() + result = 31 * result + bolusNormalDoseU.hashCode() + result = 31 * result + bolusExStartTimestamp.hashCode() + result = 31 * result + bolusExEndTimestamp.hashCode() + result = 31 * result + bolusExDoseU.hashCode() + result = 31 * result + injectCount + result = 31 * result + bgReminderMinute.hashCode() + result = 31 * result + lastIndex + result = 31 * result + lastDisconnectedTimestamp.hashCode() + result = 31 * result + standardBolusInjectCount + result = 31 * result + extendedBolusInjectCount + result = 31 * result + basalInjectCount + result = 31 * result + (patchFirmwareVersion?.hashCode() ?: 0) + result = 31 * result + patchSerialNumber.hashCode() + result = 31 * result + (patchLotNumber?.hashCode() ?: 0) + result = 31 * result + (patchModelName?.hashCode() ?: 0) + result = 31 * result + patchWakeupTimestamp.hashCode() + result = 31 * result + activatedTimestamp.hashCode() + result = 31 * result + expireDurationMilli.hashCode() + result = 31 * result + basalPauseFinishTimestamp.hashCode() + result = 31 * result + needleInsertionTryCount + result = 31 * result + LowReservoirAlertAmount + result = 31 * result + patchExpireAlertTime + result = 31 * result + isEnterPrimaryScreen.hashCode() + result = 31 * result + needSetBasalSchedule.hashCode() + result = 31 * result + (sharedKey?.contentHashCode() ?: 0) + result = 31 * result + seq15 + result = 31 * result + rotateKnobNeedleSensingError.hashCode() + result = 31 * result + remainedInsulin.hashCode() + return result + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchLifecycleEvent.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchLifecycleEvent.kt new file mode 100644 index 0000000000..ad095fcc61 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchLifecycleEvent.kt @@ -0,0 +1,108 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import android.os.Parcel +import android.os.Parcelable +import info.nightscout.androidaps.plugins.pump.eopatch.code.PatchLifecycle +import com.google.android.gms.common.internal.Preconditions + +/** + * 이 객체는 Immutable객체로 사용할 것 + */ +class PatchLifecycleEvent { + + var lifeCycle: PatchLifecycle = PatchLifecycle.SHUTDOWN + + var timestamp = 0L + + val isSafetyCheck: Boolean + get() = this.lifeCycle == PatchLifecycle.SAFETY_CHECK + + val isBasalSetting: Boolean + get() = this.lifeCycle == PatchLifecycle.BASAL_SETTING + + val isSubStepRunning: Boolean + get() = this.lifeCycle.rawValue > PatchLifecycle.SHUTDOWN.rawValue && this.lifeCycle.rawValue < PatchLifecycle.ACTIVATED.rawValue + + val isRotateKnob: Boolean + get() = this.lifeCycle == PatchLifecycle.ROTATE_KNOB + + val isShutdown: Boolean + get() = this.lifeCycle.rawValue == PatchLifecycle.SHUTDOWN.rawValue + + val isActivated: Boolean + get() = this.lifeCycle == PatchLifecycle.ACTIVATED + + constructor() { + initObject() + } + + fun initObject() { + this.lifeCycle = PatchLifecycle.SHUTDOWN + this.timestamp = System.currentTimeMillis() + } + + override fun toString(): String { + return "PatchLifecycleEvent(lifeCycle=$lifeCycle, timestamp=$timestamp)" + } + + constructor(lifeCycle: PatchLifecycle) { + Preconditions.checkNotNull(lifeCycle) + this.lifeCycle = lifeCycle + this.timestamp = System.currentTimeMillis() + } + + companion object { + fun create(lifecycle: PatchLifecycle): PatchLifecycleEvent { + Preconditions.checkNotNull(lifecycle) + + return PatchLifecycleEvent(lifecycle) + } + + @JvmStatic + fun createShutdown(): PatchLifecycleEvent { + return PatchLifecycleEvent(PatchLifecycle.SHUTDOWN) + } + + @JvmStatic + fun createbonded(): PatchLifecycleEvent { + return PatchLifecycleEvent(PatchLifecycle.BONDED) + } + + @JvmStatic + fun createRemoveNeedleCap(): PatchLifecycleEvent { + return PatchLifecycleEvent(PatchLifecycle.REMOVE_NEEDLE_CAP) + } + + @JvmStatic + fun createRemoveProtectionTape(): PatchLifecycleEvent { + return PatchLifecycleEvent(PatchLifecycle.REMOVE_PROTECTION_TAPE) + } + + @JvmStatic + fun createSafetyCheck(): PatchLifecycleEvent { + return PatchLifecycleEvent(PatchLifecycle.SAFETY_CHECK) + } + + @JvmStatic + fun createRotateKnob(): PatchLifecycleEvent { + return PatchLifecycleEvent(PatchLifecycle.ROTATE_KNOB) + } + + @JvmStatic + fun createBasalSetting(): PatchLifecycleEvent { + return PatchLifecycleEvent(PatchLifecycle.BASAL_SETTING) + } + + @JvmStatic + fun createActivated(): PatchLifecycleEvent { + return PatchLifecycleEvent(PatchLifecycle.ACTIVATED) + } + + // @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + // override fun createFromParcel(source: Parcel) = TREntityUtils.createFromBundle(source.readBundle(javaClass.classLoader), PatchLifecycleEvent::class.java) + // override fun newArray(size: Int): Array = arrayOfNulls(size) + // } + } + + +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchState.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchState.kt new file mode 100644 index 0000000000..e80dfb4f3d --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchState.kt @@ -0,0 +1,483 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import android.os.Build +import android.util.Base64 +import com.google.gson.stream.JsonWriter +import info.nightscout.androidaps.plugins.pump.eopatch.AppConstant +import info.nightscout.androidaps.plugins.pump.eopatch.GsonHelper +import info.nightscout.androidaps.plugins.pump.eopatch.core.code.BolusType +import info.nightscout.androidaps.plugins.pump.eopatch.core.util.FloatAdjusters +import info.nightscout.androidaps.plugins.pump.eopatch.code.SettingKeys +import info.nightscout.shared.sharedPreferences.SP +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject +import java.io.IOException +import java.io.Serializable +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.TimeUnit +import java.util.function.Consumer +import java.util.stream.IntStream +import kotlin.math.roundToInt + +class PatchState: IPreference { + @Transient + private val subject: BehaviorSubject = BehaviorSubject.create() + + val stateBytes: ByteArray + var updatedTimestamp: Long = 0 + + constructor(): this(ByteArray(SIZE), 0) { + } + + constructor(stateBytes: ByteArray, updatedTimestamp: Long) { + this.stateBytes = stateBytes + this.updatedTimestamp = updatedTimestamp + } + + fun update(newValue: ByteArray, timestamp: Long) { + if (newValue.size == 17) { + stateBytes[D0] = 0x00 + stateBytes[D1] = 0x00 + stateBytes[D2] = 0x22 + System.arraycopy(newValue, 0, stateBytes, 3, 17) + } else { + System.arraycopy(newValue, 0, stateBytes, 0, stateBytes.size) + } + updatedTimestamp = timestamp + subject.onNext(this) + } + + fun clear(){ + update(ByteArray(SIZE), 0) + } + + fun update(other: PatchState) { + if (other.stateBytes.size == 17) { + stateBytes[D0] = 0x00 + stateBytes[D1] = 0x00 + stateBytes[D2] = 0x22 + System.arraycopy(other.stateBytes, 0, stateBytes, 3, 17) + } else { + System.arraycopy(other.stateBytes, 0, stateBytes, 0, stateBytes.size) + } + updatedTimestamp = other.updatedTimestamp + subject.onNext(this) + } + + val isEmpty: Boolean + get() = updatedTimestamp == 0L + + private fun get(index: Int): Int { + return stateBytes[index].toInt() and 0xFF + } + + private fun getBoolean(index: Int, bit: Int): Boolean { + return bitwiseAnd(stateBytes[index], bit) != 0 + } + + /** + * 참고표 + * 0 = 0x01 + * 1 = 0x02 + * 2 = 0x04 + * 3 = 0x08 + * 4 = 0x10 + * 5 = 0x20 + * 6 = 0x40 + * 7 = 0x80 + */ + private fun bitwiseAnd(value: Byte, bit: Int): Int { + return value.toInt() and (1 shl bit) + } + + val isNeedSyncTime: Boolean + get() = getBoolean(D3, 0) + val isNeedPriming: Boolean + get() = getBoolean(D3, 1) + val isNeedNeedleSensing: Boolean + get() = getBoolean(D3, 2) + + fun useEncryption(): Boolean { + return getBoolean(D3, 3) + } + + val isPrimingSuccess: Boolean + get() = getBoolean(D3, 6) + + fun primingState(): Int { + return stateBytes[D3].toInt() and 0x70 shr 4 + } + + val isNowBolusRegAct: Boolean + get() = getBoolean(D4, 0) + val isExtBolusRegAct: Boolean + get() = getBoolean(D4, 1) + val isNormalBasalReg: Boolean + get() = getBoolean(D4, 2) + val isTempBasalReg: Boolean + get() = getBoolean(D4, 3) + val isNormalBasalAct: Boolean + get() = getBoolean(D4, 4) + val isTempBasalAct: Boolean + get() = getBoolean(D4, 5) + + fun _isExtBolusInjecting(): Boolean { + return getBoolean(D4, 6) + } + + val isNewAlertAlarm: Boolean + get() = getBoolean(D5, 0) + val isCriticalAlarm: Boolean + get() = getBoolean(D5, 7) + + val isPumpAct: Boolean + get() = getBoolean(D6, 0) + val isPatchInternalSuspended: Boolean + get() = getBoolean(D6, 2) + val isNowBolusDone: Boolean + get() = getBoolean(D6, 4) + + val isExtBolusTime: Boolean + get() = getBoolean(D6, 5) + val isExtBolusDone: Boolean + get() = getBoolean(D6, 6) + val isTempBasalDone: Boolean + get() = getBoolean(D6, 7) + + fun battery(): String { + return String.format("%.2fV", (get(D7) + 145) / 100.0f) + } + + fun batteryLevel(): Int { + if((get(D7) + 145) > 300) return 100 + + return ((get(D7) + 145 - 210) * 100.0 / 90).roundToInt() + } + + fun bootCount(): Int { + return get(D8) + } + + fun aeCount(): Int { + return get(D11) + } + + //============================================================================================== + // PUMP COUNT + //============================================================================================== + private fun remainedPumpCycle(): Int { + return stateBytes[D12].toInt() and 0xFF shl 8 or (stateBytes[D12 + 1].toInt() and 0xFF) + } + + //============================================================================================== + // CURRENT TIME (TimeUnit.SECOND) + //============================================================================================== + fun currentTime(): Int { + return byteToInt(stateBytes, D14) + } + + //============================================================================================== + // REMAINED INSULIN + //============================================================================================== + private fun remainedInsulin(): Int { + return get(D18) + } + + val remainedInsulin: Float + get() { + val remainedPumpCycle = remainedPumpCycle() + return if (remainedPumpCycle > 0) { + FloatAdjusters.FLOOR2_INSULIN.apply( + remainedPumpCycle * AppConstant.INSULIN_UNIT_P) + } else { + remainedInsulin().toFloat() + } + } + + //============================================================================================== + // RUNNING TIME + //============================================================================================== + fun runningTime(): Int { + return get(D19) + } + + //============================================================================================== + // Helper methods + //============================================================================================== + val isNormalBasalPaused: Boolean + get() = isNormalBasalReg && !isNormalBasalAct + val isNormalBasalRunning: Boolean + get() = isNormalBasalReg && isNormalBasalAct + + /* + 템프베이젤 Active(동작) 상태 + - tempBasalReg:1, tempBasalAct:1, tempBasalDone:0 + 템프베이젤 No Active (정지) 상태 + - tempBasalReg:0, tempBasalAct:0, tempBasalDone:0 + - tempBasalReg:1, tempBasalAct:0, tempBasalDone:1 + */ + val isTempBasalActive: Boolean + get() = isTempBasalReg && isTempBasalAct && !isTempBasalDone + + /* + Bolus + */ + private val isRecentPatchState: Boolean + private get() = System.currentTimeMillis() - updatedTimestamp < UPDATE_CONNECTION_INTERVAL_MILLI + val isBolusActive: Boolean + get() = isNowBolusActive || isExtBolusActive + val isNowBolusActive: Boolean + get() = isNowBolusRegAct && !isNowBolusDone + val isNowBolusFinished: Boolean + get() = isNowBolusRegAct && isNowBolusDone + val isExtBolusActive: Boolean + get() = isExtBolusRegAct && !isExtBolusDone + val isExtBolusFinished: Boolean + get() = isExtBolusRegAct && isExtBolusDone + + fun isBolusActive(type: BolusType?): Boolean { + return when (type) { + BolusType.NOW -> isNowBolusRegAct + BolusType.EXT -> isExtBolusRegAct + BolusType.COMBO -> isNowBolusRegAct && isExtBolusRegAct + else -> isNowBolusRegAct && isExtBolusRegAct + } + } + + fun isBolusDone(type: BolusType?): Boolean { + return when (type) { + BolusType.NOW -> isNowBolusDone + BolusType.EXT -> isExtBolusDone + BolusType.COMBO -> isNowBolusDone || isExtBolusDone + else -> isNowBolusDone || isExtBolusDone + } + } + + val isExtBolusInjectionWaiting: Boolean + get() = isExtBolusActive && !_isExtBolusInjecting() + val isExtBolusInjecting: Boolean + get() = isExtBolusActive && _isExtBolusInjecting() + val isBolusNotActive: Boolean + get() = !isBolusActive + + // 0 이면 reset + val isResetAutoOffTime: Boolean + get() = stateBytes[D3].toInt() and 0x80 == 0 + + private fun b(value: Boolean): String { + return if (value) ON else " " + } + + /** + * toString 심플 버전. + */ + fun t(): String { + return "PatchState{" + convertHumanTimeWithStandard(currentTime()) + "}" + } + + override fun toString(): String { + val sb = StringBuilder() + val indent = " " + sb.append("PatchState") + if (isCriticalAlarm || isNewAlertAlarm) { + sb.append(indent).append("#### Error:") + .append(if (isCriticalAlarm) "Critical" else "") + .append(if (isNewAlertAlarm) "Alert" else "") + } + sb.append(indent).append("GlobalTime:").append(convertHumanTimeWithStandard(currentTime())) + sb.append(" --> ") + IntStream.of(D3, D4, D5, D6, D7, D8, D9, D10, D11, D12, D13, D18) + .forEach { i: Int -> sb.append(String.format(" %02X ", stateBytes[i])) } + if (isPatchInternalSuspended) { + Arrays.asList(indent, "isPatchInternalSuspended:", ON).forEach(Consumer { str: String? -> sb.append(str) }) + } + if (isNeedPriming) { + Arrays.asList(indent, "NeedPriming:", ON).forEach(Consumer { str: String? -> sb.append(str) }) + } + if (isNeedNeedleSensing) { + Arrays.asList(indent, "NeedNeedleSensing:", ON).forEach(Consumer { str: String? -> sb.append(str) }) + } + if (isNowBolusRegAct || isNowBolusDone) { + Arrays.asList( + indent, "[NowBolus] RegAct:", b(isNowBolusRegAct), " Done:", b(isNowBolusDone)) + .forEach(Consumer { str: String? -> sb.append(str) }) + } + if (isExtBolusRegAct || isExtBolusDone || isExtBolusTime || _isExtBolusInjecting()) { + Arrays.asList( + indent, "[ExtBolus] RegAct:", b(isExtBolusRegAct), " Done:", b(isExtBolusDone), " Time:", b(isExtBolusTime), " Injecting:", b(_isExtBolusInjecting())) + .forEach(Consumer { str: String? -> sb.append(str) }) + } + if (isTempBasalReg || isTempBasalAct || isTempBasalDone) { + Arrays.asList( + indent, "[TempBasal] Reg:", b(isTempBasalReg), " Act:", b(isTempBasalAct), " Done:", b(isTempBasalDone)) + .forEach(Consumer { str: String? -> sb.append(str) }) + } + Arrays.asList( + indent, "[NormalBasal] Reg:", b(isNormalBasalReg), " Act:", b(isNormalBasalAct), " Paused:", b(isNormalBasalPaused), + indent, "remainedInsulin:", remainedInsulin(), " remainedPumpCycle:", remainedPumpCycle(), "(", remainedInsulin, ")", " battery:", battery()) + .forEach(Consumer { obj: Serializable? -> sb.append(obj) }) + return sb.toString() + } + + fun info(): String { + val sb = StringBuilder() + val format = SimpleDateFormat("yyyy/MM/dd HH:mm:ss") + sb.append("\n업데이트 된 시간\n(패치 시간 아니에요)") + sb.append(""" + + ${format.format(updatedTimestamp)} + + """.trimIndent()) + if (isCriticalAlarm || isNewAlertAlarm) { + sb.append(String.format("%nAlarm: %s %s", + if (isCriticalAlarm) "Critical" else "", + if (isNewAlertAlarm) "Alert" else "")) + } + sb.append(""" + + GlobalTime: ${convertHumanTimeWithStandard(currentTime())} + """.trimIndent()) + sb.append("\nNeedPriming: $isNeedPriming") + sb.append("\nNeedNeedleSensing: $isNeedNeedleSensing") + sb.append("\nPrimingSuccess: $isPrimingSuccess") + sb.append("\nBasalReg: $isNormalBasalReg") + sb.append("\nTempBasalReg: $isTempBasalReg") + sb.append("\nBasalAct: $isNormalBasalAct") + sb.append("\nisNowBolusRegAct: $isNowBolusRegAct") + sb.append("\nisNowBolusDone: $isNowBolusDone") + sb.append("\nisExtBolusRegAct: $isExtBolusRegAct") + sb.append("\nisExtBolusDone: $isExtBolusDone") + sb.append("\nisNormalBasalReg: $isNormalBasalReg") + sb.append("\nisNormalBasalAct: $isNormalBasalAct") + sb.append("\nisNormalBasalPaused: $isNormalBasalPaused") + sb.append("\nisTempBasalReg: $isTempBasalReg") + sb.append("\nisTempBasalAct: $isTempBasalAct") + sb.append("\nisTempBasalDone: $isTempBasalDone") + sb.append("\nExBolusTime: $isExtBolusTime") + sb.append("\nPumpAct: $isPumpAct") + sb.append(""" + + remainedInsulin: ${remainedInsulin()} + """.trimIndent()) + sb.append(""" + + remainedPumpCycle:${remainedPumpCycle()}($remainedInsulin) + """.trimIndent()) + sb.append(""" + + boot count :${bootCount()} + """.trimIndent()) + sb.append(""" + + aeCount : ${aeCount()} + """.trimIndent()) + sb.append(""" + + runningTime : ${runningTime()}hr + """.trimIndent()) + sb.append("\nisPumpInternalSuspended : $isPatchInternalSuspended") + sb.append("\nisResetAutoOffTime : $isResetAutoOffTime") + sb.append("\n\n\n") + return sb.toString() + } + + fun convertHumanTimeWithStandard(timeSec: Int): String { + val calendar = Calendar.getInstance() + val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss") + calendar.timeInMillis = TimeUnit.SECONDS.toMillis(timeSec.toLong()) + return dateFormat.format(calendar.time) + } + + /** + * 다른 PatchState 와 비교해서 같은 값인지 확인. + * API 키[0-1], FUNC[2], 시간[14-17] 은 비교하지 않음. + * @param other 비교할 PatchState + * @return 같으면 true 다르면 false. + */ + fun equalState(other: PatchState): Boolean { + return IntStream.of(D3, D4, D5, D6, D7, D8, D9, D10, D11, D12, D13, D18, D19) + .allMatch { i: Int -> other.stateBytes[i] == stateBytes[i] } + } + + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + val that = o as PatchState + return Arrays.equals(stateBytes, that.stateBytes) + } + + override fun observe(): Observable { + return subject.hide() + } + + override fun flush(sp: SP){ + val jsonStr = GsonHelper.sharedGson().toJson(this) + sp.putString(SettingKeys.PATCH_STATE, jsonStr) + subject.onNext(this) + } + + override fun hashCode(): Int { + return Arrays.hashCode(stateBytes) + } + + @Throws(IOException::class) fun writeJson(out: JsonWriter) { + out.beginObject() + out.name(NAME) + out.beginObject() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + out.name("stateBytes").value(java.util.Base64.getEncoder().encodeToString(stateBytes)) + } else { + out.name("stateBytes").value(Arrays.toString(Base64.encode(stateBytes, + Base64.DEFAULT))) + } + out.name("updateTimestamp").value(updatedTimestamp) + out.endObject() + out.endObject() + } + + companion object { + + val UPDATE_CONNECTION_INTERVAL_MILLI = TimeUnit.SECONDS.toMillis(10) + const val NAME = "PATCH_STATE" + const val SIZE = 20 + @JvmStatic fun create(bytes: ByteArray?, updatedTimestamp: Long): PatchState { + var stateBytes = bytes + if (stateBytes == null || stateBytes.size < SIZE) { + stateBytes = ByteArray(SIZE) + } + stateBytes[D0] = 0x00 + stateBytes[D1] = 0x00 + stateBytes[D2] = 0x22 + return PatchState(stateBytes, updatedTimestamp) + } + + private const val ON = "On" + + private fun byteToInt(bs: ByteArray, startPos: Int): Int { + return bs[startPos + 0].toInt() and 0xFF shl 24 or (bs[startPos + 1].toInt() and 0xFF shl 16) or (bs[startPos + 2].toInt() and 0xFF shl 8) or (bs[startPos + 3].toInt() and 0xFF) + } + + private const val D0 = 0 // DUMMY + private const val D1 = 1 // DUMMY + private const val D2 = 2 // FUNCTION CODE 0x22 + private const val D3 = 3 // SETUP + private const val D4 = D3 + 1 // BOLUS + private const val D5 = D3 + 2 // ALERT + private const val D6 = D3 + 3 // INSULIN + private const val D7 = D3 + 4 // BATTERY + private const val D8 = D3 + 5 // BOOT + private const val D9 = D3 + 6 // APS + private const val D10 = D3 + 7 // APS + private const val D11 = D3 + 8 // New Alarm Count + private const val D12 = D3 + 9 // Remain Pump Count + private const val D13 = D3 + 10 // Remain Pump Count + private const val D14 = D3 + 11 // Current Time + private const val D18 = D3 + 15 // Remained Insulin + private const val D19 = D3 + 16 // Running Time + } +} \ No newline at end of file diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/Segment.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/Segment.kt new file mode 100644 index 0000000000..0db5e513d3 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/Segment.kt @@ -0,0 +1,48 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +internal class JoinedSegment(var index: Int) { + + var no = 0 + + var startMinute = 0 + var endMinute = 0 + + + //BasalSegment + var doseUnitPerHour = 0f + + init { + startMinute = index * SegmentEntity.TIME_BASE + endMinute = startMinute + SegmentEntity.TIME_BASE + } +} + +internal class JoinedSegments { + var activeBasal: String + var mList: Array + + init { + activeBasal = "" + mList = Array(DEF_COUNT, { i -> JoinedSegment(i)}) + } + + + fun apply(segments: SegmentsEntity>) { + var i = 0 + var no = 0 + + for (item in segments.list) { + while (i < DEF_COUNT && item.includes(mList[i])) { // i> { + + var startMinute = 0L + var endMinute = 0L + + internal val startIndex: Int + get() = (startMinute / TIME_BASE).toInt() + + internal val endIndex: Int + get() = (endMinute / TIME_BASE).toInt() + + fun getDuration(): Long { + return endMinute - startMinute + } + /** + * Empty Segment 여부 돌려주기 + */ + internal abstract val isEmpty: Boolean + + internal fun isMinuteIncluding(minute: Long): Boolean { + return startMinute <= minute && minute < endMinute + } + + internal fun isSame(target: SegmentEntity<*>): Boolean { + return startMinute == target.startMinute && target.endMinute == endMinute + } + + internal fun hasSame(target: SegmentEntity<*>): Boolean { + return startMinute == target.startMinute || target.endMinute == endMinute + } + + /** + * 타겟을 완전히 포함? + */ + internal fun canCover(target: SegmentEntity<*>): Boolean { + return startMinute <= target.startMinute && target.endMinute <= endMinute + } + + /** + * 타겟에 완전히 덮임. + */ + internal fun isCoveredBy(target: SegmentEntity<*>): Boolean { + return target.canCover(this) + } + + /** + * 타겟과 걸침? 주의 canCover 또는 isCoveredBy 이 true 이면 이것도 true 임. + * 즉 canCover 와 isCoveredBy 가 미리 확인된 이후에 호출 할 것. + */ + internal fun isOverlapped(target: SegmentEntity<*>): Boolean { + return startMinute < target.endMinute && target.startMinute < endMinute + } + + /** + * 타겟을 완전히 포함하고 있지 않고 걸침. + */ + internal fun isPartiallyNotFullyIncluding(target: SegmentEntity<*>): Boolean { + return isOverlapped(target) && !canCover(target) && !isCoveredBy(target) + } + + /** + * target segment 를 뺸다. target 은 한방향으로만 걸쳐야 함. + * 즉 isPartiallyNotFullyIncluding(target) 이 false 여야 한다. + * 양 끝단 중 한 쪽이 같은 경우가 발생하는데 그 경우는 splitBy 에서 한쪽만 생성되므로 OK! + */ + internal fun subtract(target: SegmentEntity<*>, validCheck: Boolean) { + + if (validCheck) { + if (!isPartiallyNotFullyIncluding(target)) { + return + } + } + + if (target.startMinute <= startMinute) { + startMinute = target.endMinute + } else if (endMinute <= target.endMinute) { + endMinute = target.startMinute + } + } + + /** + * target segment 에 의해서 쪼개져서 생성되는 segment list 를 돌려준다. + */ + internal fun splitBy(target: T, validCheck: Boolean): List? { + + if (validCheck) { + if (!canCover(target)) { + return null + } + } + + val result = ArrayList() + + if (startMinute < target.startMinute) { + result.add(duplicate(startMinute, target.startMinute)) + } + + if (target.endMinute < endMinute) { + result.add(duplicate(target.endMinute, endMinute)) + } + + return result + } + + /** + * JoinedSegment 를 포함하는가? + */ + internal fun includes(segment: JoinedSegment): Boolean { + return startMinute <= segment.startMinute && segment.endMinute <= endMinute + } + + /** + * 같은 값을 가지는 주어진 시간으로 새로운 세그먼트 생성. + */ + internal abstract fun duplicate(startMinute: Long, endMinute: Long): T + + /** + * copy constructor + */ + internal abstract fun deep(): T + + /** + * 값이 같은가? + */ + internal abstract fun equalValue(segment: T): Boolean + + /** + * 이 세그먼트의 값을 JoinedSegment 에 적용한다. + */ + internal abstract fun apply(segment: JoinedSegment) + + companion object { + + internal val TIME_BASE = 30 + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/SegmentsEntity.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/SegmentsEntity.kt new file mode 100644 index 0000000000..26ff1b5393 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/SegmentsEntity.kt @@ -0,0 +1,306 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import info.nightscout.androidaps.plugins.pump.eopatch.AppConstant +import java.util.* +import java.util.function.BiFunction + +abstract class SegmentsEntity> { + + var list: ArrayList + + val segmentCount: Int + get() = list.size + + /** + * shallow copied list + */ + val copiedSegmentList: ArrayList + get() = ArrayList(list) + + /** + * deep copied list + */ + val deepCopiedSegmentList: ArrayList + get() { + val copied = ArrayList() + + for (seg in list) { + copied.add(seg.deep()) + } + + return copied + } + + private val timeMinute: Long + get() { + val c = Calendar.getInstance() + val hour = c.get(Calendar.HOUR_OF_DAY) + val min = c.get(Calendar.MINUTE) + + val segmentIndex = (hour * 60 + min) / 30 + + return (segmentIndex * 30).toLong() + } + + init { + list = ArrayList() + } + + fun hasSegments(): Boolean { + return list.isNotEmpty() + } + + fun eachSegmentItem(eachFunc: BiFunction) { + for (seg in list) { + val startIndex = seg.startIndex + val endIndex = seg.endIndex + for (i in startIndex until endIndex) { + val shouldContinue = eachFunc.apply(i, seg) + if (!shouldContinue) { + break + } + } + } + } + + fun isValid(allowEmpty: Boolean): Boolean { + if (!allowEmpty) { + if (list.isEmpty()) { + return false + } + } + + // 중복은 체크할 필요 없음, 세그먼트 추가할 때 중복체크함 +// var lastIndex = -1 + for (seg in list) { + if (seg.isEmpty) { + return false + } + +// val startIndex = seg.startIndex +// val endIndex = seg.endIndex +//// lastIndex += 1 +// if (lastIndex != startIndex) { +// return false +// } +// lastIndex = endIndex + } + return true + } + + private fun isTimeOverlapped(target: T): Boolean { + for (seg in list) { + if (seg.isOverlapped(target)) { + return true + } + } + return false + } + + // private fun millsToLocalDateTime(millis: Long): LocalDateTime { + // val instant = Instant.ofEpochMilli(millis) + // return instant.atZone(ZoneId.systemDefault()).toLocalDateTime() + // } + + // fun getCurrentSegmentRemainMinute(timeStamp: Long): Long { + // val localDateTime = millsToLocalDateTime(timeStamp) + // val minute = localDateTime.minute + // + // getSegment(minute.toLong())?.let { + // val hourMinutes = TimeUnit.HOURS.toMinutes(localDateTime.hour.toLong()) + // val totalMinutes = hourMinutes + minute + // val remain = it.endMinute - totalMinutes + // + // return it.endMinute - totalMinutes + // } + // + // return 0 + // } + + // fun getSegmentByTimeStamp(timeStamp: Long): T? { + // val minute = millsToLocalDateTime(timeStamp).minute.toLong() + // return getSegment(minute) + // } + + fun getSegment(minute: Long): T? { + for (seg in list) { + if (seg.isMinuteIncluding(minute)) { + return seg + } + } + + return null + } + + fun getCurrentSegment(): T? { + return getSegment(timeMinute) + } + + fun addSegment(seg: T) { + if (isTimeOverlapped(seg)) { + throw Exception() + } + list.add(seg) + Collections.sort(list) { t1, t2 -> java.lang.Long.compare(t1.startMinute, t2.startMinute) } + } + + + /** + * 세그먼트가 한개인 경우 삭제 불가, 첫 번째 세그먼트는 삭제 불가이므로 + * 항상 mSegmentList.size() >= 2이고 idx는 0이 될 수 없음 + * @param segment + */ + fun removeSegment(segment: T): Int { + return _remove(segment, list) + } + + /** + * 기존 세그먼트 리스트에 덮으면서 추가함. 호환성을 위한 wrapper 함수. + * @param segment + */ + + fun addSegmentWithTimeOverlapped(segment: T) { + _union(segment, list) + } + + fun addSegmentWithTimeOverlapped(segment: T, oldSegment: T?) { + if(oldSegment != null) { // 편집인 경우는 기존 세그먼트 제거 + list.remove(oldSegment) + } + _union(segment, list) + } + + /** + * 삭제 + * @param target + */ + @Synchronized + private fun _remove(target: T, list: ArrayList): Int { + val idx = list.indexOf(target) + + // idx는 최소 1 + if (idx > 0) { +// list[idx - 1].endMinute = target.endMinute + list.remove(target) + _arrange(list) + return idx + } else { + return -1 + } + } + + /** + * 기존 세그먼트 리스트에 덮으면서 추가함. + * @param target + */ + @Synchronized + private fun _union(target: T, list: ArrayList) { + + val toBeAdded = ArrayList() + + val iterator = list.iterator() + + while (iterator.hasNext()) { + val item = iterator.next() + + if (target.canCover(item)) { + iterator.remove() + } else if (target.isCoveredBy(item)) { + toBeAdded.addAll(item.splitBy(target, false)!!) + iterator.remove() + } else if (target.isOverlapped(item)) { + item.subtract(target, false) + } + } + + list.addAll(toBeAdded) + list.add(target) + + _arrange(list) + } + + /** + * 동일 세그먼트 합치기 + */ + + @Synchronized + private fun _arrange(list: ArrayList) { + val size = list.size + + if (size < 2) { + return + } + + val resultList = ArrayList() + + Collections.sort(list) { t1, t2 -> java.lang.Long.compare(t1.startMinute, t2.startMinute) } + + // ADM df_385. 연속된 세그먼트에 동일한 값 설정 가능하도록 합치는 작업 하지 않음 - EOMAPP은 합친다 + var left = list[0] + + for (i in 1 until size) { + val right = list[i] + if((left.endMinute == right.startMinute) && left.equalValue(right)) { + left.endMinute = right.endMinute + }else { + resultList.add(left) + left = right + } + } + + resultList.add(left) + + list.clear() + list.addAll(resultList) + } + + fun isFullSegment(): Boolean { + var start = 0L + var end = 1440L + for(seg in list){ + if(seg.startMinute == start){ + start = seg.endMinute + }else{ + return false + } + } + return start == end + } + + fun getEmptySegment(): Pair { + if(list.isNullOrEmpty()) { + return Pair(0, AppConstant.SEGMENT_COUNT_MAX) + } + if(list[0].startIndex != 0) { + return Pair(0, list[0].startIndex) + } + + if(list.size == 1) { + return Pair(list[0].endIndex, AppConstant.SEGMENT_COUNT_MAX) + } + + for(i in 0 until list.size-1) { + if(list[i].endIndex != list[i+1].startIndex) { + return Pair(list[i].endIndex, list[i+1].startIndex) + } + } + + return Pair(list[list.size-1].endIndex, 48) + } + + fun isChangeSegmentByNewSegment(newSegment: T, oldSegment: T?): Boolean { + list.forEach loop@{ + if(it == oldSegment) { + return@loop + } + if(newSegment.canCover(it)) { + return true + }else if(newSegment.isCoveredBy(it)) { + return true + }else if(newSegment.isOverlapped(it)) { + return true + } + } + return false + } +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/TempBasal.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/TempBasal.kt new file mode 100644 index 0000000000..ed1ce2add4 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/TempBasal.kt @@ -0,0 +1,118 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import info.nightscout.androidaps.plugins.pump.eopatch.CommonUtils +import info.nightscout.androidaps.plugins.pump.eopatch.FloatFormatters +import info.nightscout.androidaps.plugins.pump.eopatch.core.util.FloatAdjusters +import info.nightscout.androidaps.plugins.pump.eopatch.code.UnitOrPercent +import java.util.concurrent.TimeUnit + +class TempBasal { + var startTimestamp = 0L + var durationMinutes: Long = 0L + var doseUnitPerHour: Float = -1f + var percent: Int = 0 + var unitDefinition: UnitOrPercent? = null // percenr or U + set(_unitDefinition: UnitOrPercent?) { + field = _unitDefinition + } + + + var running =false + + // val isGreaterThanMaxBasal: Boolean + // get() = isGreaterThan(25f) //todo + + val percentUs: FloatArray + get() { + val doseUs: FloatArray + + doseUs = FloatArray((this.durationMinutes / 30).toInt()) + for (i in doseUs.indices) { + doseUs[i] = this.percent.toFloat() + } + + return doseUs + } + + val doseUnitPerHourArray: FloatArray + get() { + val doseUs: FloatArray + + doseUs = FloatArray((this.durationMinutes / 30).toInt()) + + val value = FloatAdjusters.ROUND2_INSULIN.apply(doseUnitPerHour) + for (i in doseUs.indices) { + doseUs[i] = value + } + return doseUs + } + + val endTimestamp: Long + get() = if (this.startTimestamp == 0L) 0 else this.startTimestamp + TimeUnit.MINUTES.toMillis(this.durationMinutes) + + + val doseUnitText: String + get() = String.format("%s U/hr", FloatFormatters.insulin(doseUnitPerHour)) + + val remainTimeText: String + get() { + var diff = endTimestamp - System.currentTimeMillis() + if (diff < 0) diff = 0 + val remainTime = CommonUtils.getRemainHourMin(diff) + return String.format("%02d:%02d", remainTime.first, remainTime.second) + } + + init { + initObject() + } + + fun initObject() { + this.unitDefinition = UnitOrPercent.U + this.doseUnitPerHour = 0f + this.percent = 0 + this.durationMinutes = 0 + this.startTimestamp = 0 + } + + fun getDoseUnitPerHourWithPercent(doseUnitPerHour: Float): Float { + return doseUnitPerHour * this.percent / 100f + } + + fun isGreaterThan(maxBasal: Float, normalBasalManager: NormalBasalManager): Boolean { + var maxTempBasal = 0f + if (this.unitDefinition == UnitOrPercent.U) { + maxTempBasal = this.doseUnitPerHour + } else if (this.unitDefinition == UnitOrPercent.P) { + val normalBasal = normalBasalManager.normalBasal + if (normalBasal != null) { + val maxNormalBasal = normalBasal.getMaxBasal(durationMinutes) + maxTempBasal = FloatAdjusters.ROUND2_TEMP_BASAL_PROGRAM_RATE.apply(maxNormalBasal + getDoseUnitPerHourWithPercent(maxNormalBasal)) + } + } + return if (maxTempBasal > maxBasal) { + true + } else false + } + + override fun toString(): String { + return "TempBasal(startTimestamp=$startTimestamp, durationMinutes=$durationMinutes, doseUnitPerHour=$doseUnitPerHour, percent=$percent)" + } + + companion object { + fun createAbsolute( _durationMinutes: Long, _doseUnitPerHour: Float): TempBasal { + val b = TempBasal() + b.durationMinutes = _durationMinutes + b.doseUnitPerHour = _doseUnitPerHour + b.unitDefinition = UnitOrPercent.U + return b + } + fun createPercent( _durationMinutes: Long, _percent: Int): TempBasal { + val b = TempBasal() + b.durationMinutes = _durationMinutes + b.percent = _percent + b.unitDefinition = UnitOrPercent.P + return b + } + } + +} diff --git a/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/TempBasalManager.kt b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/TempBasalManager.kt new file mode 100644 index 0000000000..4ebafb2204 --- /dev/null +++ b/eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/TempBasalManager.kt @@ -0,0 +1,101 @@ +package info.nightscout.androidaps.plugins.pump.eopatch.vo + +import com.google.common.base.Preconditions +import info.nightscout.androidaps.plugins.pump.eopatch.CommonUtils +import info.nightscout.androidaps.plugins.pump.eopatch.GsonHelper +import info.nightscout.androidaps.plugins.pump.eopatch.code.SettingKeys +import info.nightscout.androidaps.plugins.pump.eopatch.code.UnitOrPercent +import info.nightscout.shared.sharedPreferences.SP +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject + +class TempBasalManager : IPreference{ + @Transient + private val subject: BehaviorSubject = BehaviorSubject.create() + + var startedBasal: TempBasal? = null + + private var startTimestamp = 0L + + private var endTimestamp = 0L + + var unit = UnitOrPercent.P + + init { + + } + + + fun clear(){ + startedBasal = null + startTimestamp = 0L + endTimestamp = 0L + } + + fun updateBasalRunning(tempBasal: TempBasal) { + Preconditions.checkNotNull(tempBasal) + + this.startedBasal = CommonUtils.clone(tempBasal) + this.startedBasal?.running = true + } + + + /** + * 특정 베이젤의 인덱스 찾기 + * + * @param basal + * @return + */ + + + fun updateBasalStopped() { + // 모두 정지 + this.startedBasal?.running = false + this.startedBasal?.startTimestamp = 0 + // subject.onNext(this) + } + + fun updateForDeactivation() { + // deactivation할때는 모두 정지 + updateBasalStopped() + // subject.onNext(this) + } + + + + fun updateDeactivation() { + updateBasalStopped() + } + + + + fun update(other: TempBasalManager){ + this.startedBasal = other.startedBasal + startTimestamp = other.startTimestamp + endTimestamp = other.endTimestamp + unit = other.unit + } + + override fun observe(): Observable { + return subject.hide() + } + + override fun flush(sp: SP){ + val jsonStr = GsonHelper.sharedGson().toJson(this) + sp.putString(SettingKeys.TEMP_BASAL, jsonStr) + subject.onNext(this) + } + + override fun toString(): String { + return "TempBasalManager(startedBasal=$startedBasal, startTimestamp=$startTimestamp, endTimestamp=$endTimestamp, unit=$unit)" + } + + companion object { + + const val NAME = "TEMP_BASAL_MANAGER" + + val MAX_BASAL_SEQ = 20 + val MANUAL_BASAL_SEQ = MAX_BASAL_SEQ + 1 + } + +} diff --git a/eopatch/src/main/res/layout/activity_eopatch.xml b/eopatch/src/main/res/layout/activity_eopatch.xml new file mode 100644 index 0000000000..f01206f3ff --- /dev/null +++ b/eopatch/src/main/res/layout/activity_eopatch.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eopatch/src/main/res/layout/dialog_alarm.xml b/eopatch/src/main/res/layout/dialog_alarm.xml new file mode 100644 index 0000000000..38661005c1 --- /dev/null +++ b/eopatch/src/main/res/layout/dialog_alarm.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + +