From cfaf9f228e7f0122c64011cf7996eb9671b4c01a Mon Sep 17 00:00:00 2001 From: jungsomyeong Date: Fri, 11 Feb 2022 13:58:07 +0900 Subject: [PATCH] EOPatch2 support --- app/build.gradle | 5 + app/libs/eopatch_core.aar | Bin 0 -> 122017 bytes .../activities/MyPreferenceFragment.kt | 3 + .../nightscout/androidaps/di/AppComponent.kt | 2 + .../nightscout/androidaps/di/PluginsModule.kt | 7 + build.gradle | 4 + .../plugins/common/ManufacturerType.kt | 3 +- .../overview/notifications/Notification.kt | 1 + .../pump/common/defs/PumpCapability.kt | 1 + .../plugins/pump/common/defs/PumpType.kt | 21 +- .../utils/userEntry/UserEntryMapper.kt | 1 + .../userEntry/UserEntryPresentationHelper.kt | 1 + .../src/main/res/drawable/ic_eopatch2_128.xml | 43 + crowdin.yml | 2 + .../database/embedments/InterfaceIDs.kt | 1 + .../androidaps/database/entities/UserEntry.kt | 1 + eopatch/.gitignore | 1 + eopatch/build.gradle | 49 ++ eopatch/libs/eopatch_core.aar | Bin 0 -> 122017 bytes eopatch/proguard-rules.pro | 21 + .../pump/eopatch/ExampleInstrumentedTest.kt | 25 + eopatch/src/main/AndroidManifest.xml | 13 + .../plugins/pump/eopatch/AppConstant.kt | 87 ++ .../plugins/pump/eopatch/CommonUtils.kt | 178 ++++ .../plugins/pump/eopatch/EONotification.kt | 23 + .../plugins/pump/eopatch/EoPatchRxBus.kt | 17 + .../plugins/pump/eopatch/EopatchPumpPlugin.kt | 588 +++++++++++++ .../plugins/pump/eopatch/FloatFormatters.kt | 46 + .../plugins/pump/eopatch/GsonHelper.kt | 20 + .../plugins/pump/eopatch/OsAlarmReceiver.kt | 17 + .../plugins/pump/eopatch/OsAlarmService.java | 81 ++ .../plugins/pump/eopatch/RxAction.kt | 113 +++ .../plugins/pump/eopatch/alarm/AlarmCode.kt | 129 +++ .../pump/eopatch/alarm/AlarmManager.kt | 171 ++++ .../pump/eopatch/alarm/AlarmProcess.kt | 135 +++ .../pump/eopatch/alarm/AlarmRegistry.kt | 164 ++++ .../pump/eopatch/alarm/AlarmState.java | 7 + .../bindingadapters/OnSafeClickListener.kt | 26 + .../bindingadapters/ViewBindingAdapter.kt | 40 + .../pump/eopatch/ble/IPatchManager.java | 128 +++ .../pump/eopatch/ble/PatchManager.java | 451 ++++++++++ .../pump/eopatch/ble/PatchManagerImpl.java | 793 +++++++++++++++++ .../pump/eopatch/ble/PatchStateManager.java | 290 ++++++ .../pump/eopatch/ble/PreferenceManager.kt | 254 ++++++ .../pump/eopatch/ble/task/ActivateTask.java | 46 + .../pump/eopatch/ble/task/BolusTask.java | 124 +++ .../pump/eopatch/ble/task/DeactivateTask.java | 132 +++ .../pump/eopatch/ble/task/FetchAlarmTask.java | 53 ++ .../eopatch/ble/task/GetPatchInfoTask.java | 116 +++ .../eopatch/ble/task/InfoReminderTask.java | 48 + .../ble/task/InternalSuspendedTask.java | 183 ++++ .../eopatch/ble/task/NeedleSensingTask.java | 51 ++ .../pump/eopatch/ble/task/PauseBasalTask.java | 225 +++++ .../pump/eopatch/ble/task/PrimingTask.java | 55 ++ .../ble/task/ReadBolusFinishTimeTask.java | 64 ++ .../ble/task/ReadTempBasalFinishTimeTask.java | 46 + .../eopatch/ble/task/ResumeBasalTask.java | 64 ++ .../pump/eopatch/ble/task/SelfTestTask.java | 67 ++ .../eopatch/ble/task/SetGlobalTimeTask.java | 75 ++ .../eopatch/ble/task/SetLowReservoirTask.java | 57 ++ .../pump/eopatch/ble/task/StartBondTask.java | 58 ++ .../eopatch/ble/task/StartCalcBolusTask.java | 62 ++ .../ble/task/StartNormalBasalTask.java | 55 ++ .../eopatch/ble/task/StartQuickBolusTask.java | 62 ++ .../eopatch/ble/task/StartTempBasalTask.java | 44 + .../pump/eopatch/ble/task/StopBasalTask.java | 126 +++ .../eopatch/ble/task/StopComboBolusTask.java | 85 ++ .../eopatch/ble/task/StopExtBolusTask.java | 59 ++ .../eopatch/ble/task/StopNowBolusTask.java | 60 ++ .../eopatch/ble/task/StopTempBasalTask.java | 56 ++ .../ble/task/SyncBasalHistoryTask.java | 151 ++++ .../pump/eopatch/ble/task/TaskBase.java | 98 +++ .../pump/eopatch/ble/task/TaskFunc.java | 31 + .../pump/eopatch/ble/task/TaskQueue.java | 115 +++ .../ble/task/UpdateConnectionTask.java | 57 ++ .../pump/eopatch/code/AlarmCategory.kt | 46 + .../plugins/pump/eopatch/code/AlarmSource.kt | 66 ++ .../plugins/pump/eopatch/code/BasalStatus.kt | 43 + .../plugins/pump/eopatch/code/BgUnit.kt | 37 + .../pump/eopatch/code/BolusExDuration.kt | 65 ++ .../pump/eopatch/code/DeactivationStatus.kt | 28 + .../plugins/pump/eopatch/code/Dummy.kt | 5 + .../plugins/pump/eopatch/code/EventType.kt | 25 + .../pump/eopatch/code/PatchExpireAlertTime.kt | 52 ++ .../pump/eopatch/code/PatchLifecycle.kt | 30 + .../plugins/pump/eopatch/code/PatchStep.kt | 32 + .../plugins/pump/eopatch/code/SettingKeys.kt | 18 + .../pump/eopatch/code/UnitOrPercent.kt | 52 ++ .../eopatch/dagger/EopatchInjectHelpers.kt | 17 + .../pump/eopatch/dagger/EopatchModule.kt | 140 +++ .../pump/eopatch/dagger/EopatchPrefModule.kt | 44 + .../pump/eopatch/event/EoPatchEvents.kt | 11 + .../extension/AppCompatActivityExtension.kt | 62 ++ .../eopatch/extension/BooleanExtension.kt | 5 + .../extension/CharSequenceExtesnsion.kt | 37 + .../eopatch/extension/CompletableExtension.kt | 29 + .../pump/eopatch/extension/FloatExtension.kt | 18 + .../pump/eopatch/extension/LongExtension.kt | 42 + .../pump/eopatch/extension/MaybeExtension.kt | 27 + .../eopatch/extension/ObservableExtension.kt | 27 + .../extension/SharedPreferencesExtension.kt | 40 + .../pump/eopatch/extension/SingleExtension.kt | 15 + .../pump/eopatch/extension/StringExtension.kt | 48 + .../eopatch/extension/TextViewExtension.kt | 20 + .../pump/eopatch/extension/ViewExtension.kt | 34 + .../pump/eopatch/ui/AlarmHelperActivity.kt | 80 ++ .../pump/eopatch/ui/DialogHelperActivity.kt | 25 + .../plugins/pump/eopatch/ui/EoBaseActivity.kt | 106 +++ .../plugins/pump/eopatch/ui/EoBaseFragment.kt | 87 ++ .../pump/eopatch/ui/EoBaseNavigator.kt | 20 + .../pump/eopatch/ui/EopatchActivity.kt | 389 ++++++++ .../ui/EopatchBasalScheduleFragment.kt | 25 + .../eopatch/ui/EopatchConnectNewFragment.kt | 41 + .../eopatch/ui/EopatchOverviewFragment.kt | 182 ++++ .../pump/eopatch/ui/EopatchRemoveFragment.kt | 22 + .../ui/EopatchRemoveNeedleCapFragment.kt | 22 + .../ui/EopatchRemoveProtectionTapeFragment.kt | 22 + .../eopatch/ui/EopatchRotateKnobFragment.kt | 53 ++ .../ui/EopatchSafeDeactivationFragment.kt | 24 + .../eopatch/ui/EopatchSafetyCheckFragment.kt | 38 + .../ui/EopatchTurningOffAlarmFragment.kt | 22 + .../pump/eopatch/ui/EopatchWakeUpFragment.kt | 25 + .../ui/dialogs/ActivationNotCompleteDialog.kt | 82 ++ .../pump/eopatch/ui/dialogs/AlarmDialog.kt | 169 ++++ .../pump/eopatch/ui/dialogs/CommonDialog.kt | 45 + .../pump/eopatch/ui/event/SingleLiveEvent.kt | 30 + .../plugins/pump/eopatch/ui/event/UIEvent.kt | 8 + .../ui/receiver/RxBroadcastReceiver.kt | 64 ++ .../eopatch/ui/viewmodel/EoBaseViewModel.kt | 39 + .../ui/viewmodel/EopatchOverviewViewModel.kt | 233 +++++ .../eopatch/ui/viewmodel/EopatchViewModel.kt | 830 ++++++++++++++++++ .../eopatch/ui/viewmodel/ViewModelFactory.kt | 39 + .../pump/eopatch/vo/ActivityResultEvent.kt | 9 + .../plugins/pump/eopatch/vo/Alarms.kt | 108 +++ .../plugins/pump/eopatch/vo/BasalSegment.kt | 45 + .../plugins/pump/eopatch/vo/BolusCurrent.kt | 194 ++++ .../plugins/pump/eopatch/vo/IPreference.kt | 9 + .../plugins/pump/eopatch/vo/NormalBasal.kt | 213 +++++ .../pump/eopatch/vo/NormalBasalManager.kt | 132 +++ .../plugins/pump/eopatch/vo/PatchConfig.kt | 401 +++++++++ .../pump/eopatch/vo/PatchLifecycleEvent.kt | 108 +++ .../plugins/pump/eopatch/vo/PatchState.kt | 483 ++++++++++ .../plugins/pump/eopatch/vo/Segment.kt | 48 + .../plugins/pump/eopatch/vo/SegmentEntity.kt | 140 +++ .../plugins/pump/eopatch/vo/SegmentsEntity.kt | 306 +++++++ .../plugins/pump/eopatch/vo/TempBasal.kt | 118 +++ .../pump/eopatch/vo/TempBasalManager.kt | 101 +++ .../src/main/res/layout/activity_eopatch.xml | 63 ++ eopatch/src/main/res/layout/dialog_alarm.xml | 84 ++ eopatch/src/main/res/layout/dialog_common.xml | 57 ++ .../fragment_eopatch_basal_schedule.xml | 85 ++ .../layout/fragment_eopatch_connect_new.xml | 60 ++ .../res/layout/fragment_eopatch_overview.xml | 547 ++++++++++++ .../res/layout/fragment_eopatch_remove.xml | 111 +++ .../fragment_eopatch_remove_needle_cap.xml | 180 ++++ ...ragment_eopatch_remove_protection_tape.xml | 97 ++ .../layout/fragment_eopatch_rotate_knob.xml | 254 ++++++ .../fragment_eopatch_safe_deativation.xml | 148 ++++ .../layout/fragment_eopatch_safety_check.xml | 94 ++ .../fragment_eopatch_turning_off_alarm.xml | 92 ++ .../res/layout/fragment_eopatch_wake_up.xml | 175 ++++ eopatch/src/main/res/values-ko/strings.xml | 137 +++ .../src/main/res/values-ko/strings_alarm.xml | 31 + eopatch/src/main/res/values/arrays.xml | 86 ++ eopatch/src/main/res/values/colors.xml | 5 + eopatch/src/main/res/values/strings.xml | 148 ++++ eopatch/src/main/res/values/strings_alarm.xml | 31 + eopatch/src/main/res/xml/pref_eopatch.xml | 24 + .../plugins/pump/eopatch/ExampleUnitTest.kt | 18 + settings.gradle | 2 +- 170 files changed, 15266 insertions(+), 3 deletions(-) create mode 100644 app/libs/eopatch_core.aar create mode 100644 core/src/main/res/drawable/ic_eopatch2_128.xml create mode 100644 eopatch/.gitignore create mode 100644 eopatch/build.gradle create mode 100644 eopatch/libs/eopatch_core.aar create mode 100644 eopatch/proguard-rules.pro create mode 100644 eopatch/src/androidTest/java/info/nightscout/androidaps/plugins/pump/eopatch/ExampleInstrumentedTest.kt create mode 100644 eopatch/src/main/AndroidManifest.xml create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/AppConstant.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/CommonUtils.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EONotification.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EoPatchRxBus.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/EopatchPumpPlugin.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/FloatFormatters.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/GsonHelper.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/OsAlarmReceiver.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/OsAlarmService.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/RxAction.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmCode.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmManager.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmProcess.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmRegistry.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/alarm/AlarmState.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/bindingadapters/OnSafeClickListener.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/bindingadapters/ViewBindingAdapter.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/IPatchManager.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchManager.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchManagerImpl.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PatchStateManager.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/PreferenceManager.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ActivateTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/BolusTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/DeactivateTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/FetchAlarmTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/GetPatchInfoTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/InfoReminderTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/InternalSuspendedTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/NeedleSensingTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/PauseBasalTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/PrimingTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ReadBolusFinishTimeTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ReadTempBasalFinishTimeTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/ResumeBasalTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SelfTestTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SetGlobalTimeTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SetLowReservoirTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartBondTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartCalcBolusTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartNormalBasalTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartQuickBolusTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StartTempBasalTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopBasalTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopComboBolusTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopExtBolusTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopNowBolusTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/StopTempBasalTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/SyncBasalHistoryTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskBase.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskFunc.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/TaskQueue.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ble/task/UpdateConnectionTask.java create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/AlarmCategory.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/AlarmSource.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BasalStatus.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BgUnit.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/BolusExDuration.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/DeactivationStatus.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/Dummy.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/EventType.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchExpireAlertTime.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchLifecycle.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/PatchStep.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/SettingKeys.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/code/UnitOrPercent.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchInjectHelpers.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchModule.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/dagger/EopatchPrefModule.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/event/EoPatchEvents.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/AppCompatActivityExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/BooleanExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/CharSequenceExtesnsion.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/CompletableExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/FloatExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/LongExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/MaybeExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/ObservableExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/SharedPreferencesExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/SingleExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/StringExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/TextViewExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/extension/ViewExtension.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/AlarmHelperActivity.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/DialogHelperActivity.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseActivity.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EoBaseNavigator.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchActivity.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchBasalScheduleFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchConnectNewFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchOverviewFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveNeedleCapFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRemoveProtectionTapeFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchRotateKnobFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchSafeDeactivationFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchSafetyCheckFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchTurningOffAlarmFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/EopatchWakeUpFragment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/ActivationNotCompleteDialog.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/AlarmDialog.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/dialogs/CommonDialog.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/event/SingleLiveEvent.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/event/UIEvent.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/receiver/RxBroadcastReceiver.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EoBaseViewModel.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EopatchOverviewViewModel.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/EopatchViewModel.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/ui/viewmodel/ViewModelFactory.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/ActivityResultEvent.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/Alarms.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/BasalSegment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/BolusCurrent.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/IPreference.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/NormalBasal.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/NormalBasalManager.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchConfig.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchLifecycleEvent.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/PatchState.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/Segment.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/SegmentEntity.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/SegmentsEntity.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/TempBasal.kt create mode 100644 eopatch/src/main/java/info/nightscout/androidaps/plugins/pump/eopatch/vo/TempBasalManager.kt create mode 100644 eopatch/src/main/res/layout/activity_eopatch.xml create mode 100644 eopatch/src/main/res/layout/dialog_alarm.xml create mode 100644 eopatch/src/main/res/layout/dialog_common.xml create mode 100644 eopatch/src/main/res/layout/fragment_eopatch_basal_schedule.xml create mode 100644 eopatch/src/main/res/layout/fragment_eopatch_connect_new.xml create mode 100644 eopatch/src/main/res/layout/fragment_eopatch_overview.xml create mode 100644 eopatch/src/main/res/layout/fragment_eopatch_remove.xml create mode 100644 eopatch/src/main/res/layout/fragment_eopatch_remove_needle_cap.xml create mode 100644 eopatch/src/main/res/layout/fragment_eopatch_remove_protection_tape.xml create mode 100644 eopatch/src/main/res/layout/fragment_eopatch_rotate_knob.xml create mode 100644 eopatch/src/main/res/layout/fragment_eopatch_safe_deativation.xml create mode 100644 eopatch/src/main/res/layout/fragment_eopatch_safety_check.xml create mode 100644 eopatch/src/main/res/layout/fragment_eopatch_turning_off_alarm.xml create mode 100644 eopatch/src/main/res/layout/fragment_eopatch_wake_up.xml create mode 100644 eopatch/src/main/res/values-ko/strings.xml create mode 100644 eopatch/src/main/res/values-ko/strings_alarm.xml create mode 100644 eopatch/src/main/res/values/arrays.xml create mode 100644 eopatch/src/main/res/values/colors.xml create mode 100644 eopatch/src/main/res/values/strings.xml create mode 100644 eopatch/src/main/res/values/strings_alarm.xml create mode 100644 eopatch/src/main/res/xml/pref_eopatch.xml create mode 100644 eopatch/src/test/java/info/nightscout/androidaps/plugins/pump/eopatch/ExampleUnitTest.kt 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 0000000000000000000000000000000000000000..1a8aafe38ff2b48d61af97bb393395ce9ebea2d9 GIT binary patch literal 122017 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ⅇ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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1a8aafe38ff2b48d61af97bb393395ce9ebea2d9 GIT binary patch literal 122017 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ⅇ^;;_v17N(J`nm@3la`n~rwm@17xiKfz)R;I?EANM%VJbp%l z5r0GN7&kTcWraFEmu-F@RPH6NiD+;vLvc2GsNRi6wKcNx=klosxR<-rAtD;lQV#SG zQ%gQJ#>Iia>A2KTxspRl)rLI<9jWcmQEwza91 zS)HDjPQmdIeE2__O~XI{^6Z^cU;D-bFrCVFdrV&{@{YkHPn%<9BpUJ%8cOnPCx$aq~uo&A*$ zRXxv$ZQO#^gL6wf_5%}K9osD>6eCf4f(mwng#~mJ7b+pw{Gtt4Re$!6C+|WcD^+yDCaa1_Yx6L4F@-uE| z2~ZBTH9->v32o)@L5D4+8&Ga{RXI-)TyeJvfyxQAHo4~qCw741wl64Xr9nKOc@7YC3@Y-ST406PnEnQK|L%VdQa!zL$>|VxlI?At~W?^KK@!*@WkIs+3UIA56HA#O85r}tV zsLQ&GG6V~CPRhnIZ{GBQS7q4HiDYF2d{WD2fo*v>ocHA%)(`w83e+Icl9^|@7*!hy zaT&|T%wLfUg!AmR9#xINh-w$fBrAatWE$F0chzr_r8>dZ)dSYFtFzS_JT-Lizf!47 zdGvel-gBri+2`4ykUklzJK?mK6yF22haYen)~#$GU|3gtVRVcAJXAFD`{Q_D48=~_ z3EbE|0{5l(QDYxgz)^klOXiRBCF(D+9)1Sl%I#s_Z3ZXNbM`|^FS;jCG%5s(iPkia zq~BqWXdK(EWOF%X#Bp2xh7~Wy)`_kaW^@lO>pn>+q_)165v-pN08Y{zV35Ng}DI*YrupZPaL^W2P47U$5A>kwJ=!GZ#)xo}3=m;8~2b6RljLTC73BlN_ z#v$?nq0Q9J!s&0~52*fBQg_A_?Q33yjdH;AglvV!B;POu2~#h~SmKK$4^YYeF`@l~ zBsgGXqIv*!{SSvdfP;Zu{;kRWlfHqPnUkBD zy~+PJz-!cWl@^6DK0smJ-(1^6JSuk#w4;LIzK9NhY)QuYj%8qj0T=C52+0QN@iX`0sNIx?XlnD1!n@>j@)*CSu-Q&=2k)R&XSM1CDm{LBJQrB(ogbrjxO>UgO{Rl)YetOz;~L@?;V? zeH9o-X>?)FhgWUS?a>`!>#k`9lpXx-HObx#i|tuF5vCs{eG26tz+{Nhw>4yKGAvs zlEzevV}|mOpiQ@S;;L&6?b&x4EAN+|2l%d{yhH2QS(Z zt+t@{1nZW6;;P)I>=)`!ez)*d*7I#F=vLbE ziMoZZH!x^bAQ&e)))0S38vrbS>JHJRY&v!MLr20mv;i#WJf0Y}I`e>)S$|{!y4`8* z0qL=dyG!^z{uzo^d7@G$J%=pytYV)zK-?|dUT z(6$(AXlRsvFWis>KdVrZRXOo_jvka@8Lo6d<5!xfbetI|W(9kWCF}RNp8{}9Y=HZ~ z^gpy>E4{>BdTG~qBQPwWWuHr%JQHmNICS+CB|578H1e)~6d}%Z1|{||GGG$y=ApTk zdMmDEs&j|)#CP(2J8G7$F!td;G4A@@l~!C!vBD_{6q=w@;d@*|LdEKC=!^| zBs+MA{sYC=it;_#7j>;oUMrtOKTM%G-`LKYf;~1C3>1q9ygbJ?3qF6VkH|t_)AISQ z@NfCCkGN>%pK=A*bjOaC*MnszP<&)$lU$lICB|xRvNwuLA z!F~AROA$8&2C!>VZAVmMWLUResM-D&W7_?mfXND!#jw)hv!zmF=a;Pm%|@FG?FW6j zD2}thPF8z@`4=bTn*K#3OUZ>jgXKa-q*tRciQBfG9}|4|UG_;t3%_fNqs;~}G74yt zET11pF5|H;hGT`J6BWfIog-|;xW!tOq-_fF?ZIwW z9GKH1n8Te5iIq475#tX53ABffD-`+F^(Syg z;022v8wrAqAB<@bU%0oK?T~Z^(Zs50-k*1y$HJp`Q^{~E0J0uFJE+%7P2Z=KPlV1@ zAg?Vb-v-L9u3aw>JG5kZgGmI8%fE08-Pft@{Q0 zswd8psQmk7{$_qH>w_0+1PoMkeg#u@#hE4Z8}ef4z+V7_Frv>!t$as{kO z6X(x|;qkoSrk8GD?(G1u0k7mc{8;K0PZGhhR3Wu82_%~dhPY#6!f8PE0~L8qA{?da zJ7Me}v(>-pm}Tbct6gOVA^kNPV$K2E<>x=+SUk+OZy?88x=VyfpsD~--CESP>rG3v z5ESL(U6I=(P7pTg^1l?lX8k*E|6ID?Te;P#(r8>-YG1vVUvJ~@xYD)_D|0~eWvJFQ zekwOa3RwY-+m~?7O~0jY239*#Ips&EmtDLdD%D!umIQdj0|_YRi69Z)zFL-?KDE+f^>%)2y%xWYs`bY{x)3QjaJi^}); zYJM9p(`-yOp}VS266(7;3@9KlDt`Nt9slD;_=@+s!e(JwjNdQ-lp%`A5P}6iMEYD7 zsl=N(#-EqZC*Ba!>s{2TKiNvvD*d*V_=FoJecd4667njr!AzuFg(k|l0y%fjI;wop z6aESD?`$0y=iV^*n=i?sz`(Tr;ap17!PXUMVJ2_pZe}aWuIy}OXJv2kFXDEq|M!5Z z>6C4%`*xl<_d_433Sw2abzLi*59vTePJ(?+htB54;&$aL+zL9?fL--fcq5g7 zR6q6LDG^sdET@8x&sBVdC@NGBHr$W?eeF;O|Kp8gR`QV$4At%LY%zu;ngz_Jt)*1^ zgdo&nPj<*d+P0BC^1*}DpPL<^^R~ngP&R;V1V#8-S`2C5t0Kb)2f-5u4ix9KLH+UC z6kf8R!Yp>$M1xg4N6pExF`!5EBf|oRLv`4=sBQYsjiFS<4x?M2C1-XBFT-&IWUlva zL#;R zB;Ptz^TPpKn$9gkbb8w(V{>;r%mGJ3u44DGYz;yRqcWkeH9MQ9_6h|aTWRK31(0cr zdQac}7sh1LAFgzi&If$|4r%lRL@30+kY4|5LfZd0i7NkBP1M1`)(mL>A6pyLHtiRf zaRlrq=Sy*8=B~ML$e1l#-7?EaQZd`KIT*h-CDfDb7(1)APNxm2?EN-3zUoJJL_kM_ zg(C`-5{2DuLdOiD%V4q%7|oyMX%Px|zdl6>#7u!b`RLKuTp+v*uayd$42haump5D@ zJ7U3O^Aow6Yumfa_<>I4UtQ;3f&BwY;J2nPp5{i&RQF+1+8P~0X6Iy)c9OKMmy=b4 z&XVDw2&}MCyGS^G(E3H>%5EgH<Cf@h1i4hSJFB1)F{u~Wf1kKbn zU!O{UGrX-TeN0gNXN}x3V{R0ZUtmRD$~%XQCFoh`=+Wro_WJz+$q1$jV9dw-M)geT0AFXsUOM;S-7@-E8+tgduw{i|J4ch@ws(o={k^9c zy_KnxDD&s$gQiuL-Vqn--&gMvaiXra`W=NORH@H-90NfBqD6?GdHNs+WczM1dAo9C|#*+f33!t!e-Q|ph zK1Tgkn|zoBaT2BOdB9Rj;#l+q>t1L2!VU2o3qvHwjTVcJNLH|cs8ePKh8Z59Fcw=p ze>4<#2;~#`R*O8J(h>d?PEXtjU?KU(|I9tZFI3h_$cKxR&6LL4NQ`{+Q)+K0|M5jU z=V0y?CNzaAZ-%>0JBt>R`NgAJ88`6eKdkSJSNN*yuSQV7{ofbK`d^C_miotBUC`yxn0-KSPM7fFmEe)Ng(TtwYmcn)WG56B%1mr)goSU{-mz=^C>x^@vcg8=tQ{&cu;XfMM8-}Z%Oa0F@&sn-KXt| z7Aj0UO(lv8*$pz~z4>WGo%L*QjFT0Q7us*sok(>VM^2k0yFYXHa2PC8sJS-IRDS@M znSY24hV4Lp&t)U~6k>^)Q6UkrIOQ+qc5_pq(ub8yv=WMk={-XlxHHKlJPs zJI6TOwaeB%#&^gN3*Dk8Vtf}T8Q$6@)nec@RC8kxyjA*$dr|f}#4HE^p|VI2|0UK` z`4@B+iF~%T;tVDTO5>;=^Cm5pZD{#gfm_02zo=)C;=FIIM;x&_F2jyG+U2d&dzrzh zX5)51$|Jkcf%#<_mTJRrM`>-g1n6Pjuifh&M?Uda-8~ME-LsGW+wE6;zT2m-fmfZr zqeF7UcYg6#K7QNBukTI7(JxBI6y-2KkwafXPIw95T9FHdFwT&RiQ(^ocZn#UurIsH z#1cxN7o3-VWu?M$F+TzU^fUC6Rpr@2Vz_&mJKqYWLZ$E*z{`iSP}GNI1+iEF{bGD^ zKJ0z+$&W(8@I{GoOQH|9hd0Xn3h~ybOuoA(JDmgUQlW9J5~1n*)DQjum6ypb>h7V* z`DIUujzOFG^DdI^ajp`>-Q|+r0j|>C3B&Xc%79lSKgVpry?p7dAi|Iz>0@W}H~hC26Gkp=&fUg;kP135FV|HuQX`Znky7y=9A3XS@9sx~z; z%^gn(LuI-pqL#zqn4uL$R)r`p?3uZgA+LuswbaIUFwX@>G}KQJZz^Y-Zgz&gKRW9l zFQ<5pw|va~-`-vb`#9}c{;b*t{P~4eNK6w?MGJWGKasT=jVh&TPx}SpIP735Q|t zyh5T<6D2s>L1vEw<`k&szCIA(7FoR@DLlnBE(iFSP`G$Xl*R0{DvTLTXK@#sysx?i@5&-}?c5!`*PPg$4caD` z1xH8y0l8Qv(M;jK$LQIlp&Nw%xnl4#X)rrV`=2}bu(E?19RJ43hrczoe>4pK|0AWE znXS3HnTzXR47u6gF^)n!`Eah= zZn#$eR~nP#2W6OejO3ujC-uIa6oA}UFZsQrP zmw3PRTIe$x-+qXYUYMl@BGeyXh$%xE%!QQ1H3l871OrLS^?1f7b`zPwFCZ_=SmI>X z95d8Phm8(usom-v)-H-z@9?Vi+-SaSxJ7fV8hmn4@4tE`NTMNsk2){2^Tr1xbCGw| zEaH;#lrrw362Z`!<0PGfFl%KGGO!$$`}F>!Ck^7$eaMRBS_re|Aj!Q7ZfbvE%_%j zc>InCa$SNS@bE%q46|yH=rWZDWF%)QB2fW%{==9ZALdH(5&aadHYI`gLX%=C7Jqop zW(q?@md@x@K_dB|qb7uojvIl$@+kTjwtnVOmFf9Pd`f*b@3 z3W&HHs`0r$}(+_L8?sdmPnn5gJ(5p`;iUzdRhWn3Np^2hSEnTNK;DhL!$( z={Pm8TcQ3gTj1YY=Kpo+++3|}nWW7;)m)wbr*EU7GVOq-f|5V#|0Rr7`+Il7Ngg$$ zzAcThU6yv!_0JLUmn;18Do zd)x*#BJjPLt7Em5U7*1`t?ti5Bn^#by>Q^r=5!6K>Y8<4!r4*4g$ph`sd`R%#Q5Uq zg(4M>5Jk6f>%X!>aN+Xijg3m&}QBWJj=u17SNF}+>ZZ~i( zG0x3hDxs%sG*#}c-*F!08dX%q7q_@9WfJ4FS+Pc9lispuM?KU1Q^tnxUb(2q9IQe9e&u;*+CXF-;wLnA1x4g8e(zit4?CDE{Is>~D(F{Kqn(1++2K zaQv^6S^pWPChZr5|Ar}cH)Fk*0^!0^s9V|jl3L}&qF`-uNE#A_$~G$~e0Ha@ag@=P zH);fN4_`>bz9=YEs7NT{J}#0SUW}CnF!uSW4K|M%f#Vt9kC$uoK!pR`r#Y3XC%x8< zLMl5V*<~8QX6?^bSHqQH7}42tKO&6ko_c5zcEDW?ggp#tolo~bf)CVc>BhRIMRDh9 z;fB~QjJW7Z&3){>u}VN9xCz)`oGX&vU}CpFj%8 zGiu2v7hW5VUs-a8gs~qvr2#uOJy$F*x$@L?YRu&ZpNXlW0hOQUAW$?*wNWInjB4PC zrgEmG$qqNhN7bb5E``{c48BysjgaofZ#PsjXSEEARy!6^V`6&5kF`nX(}a_z3tS*} zvTY&(eltInlC9ZRVbE6*2N*+!!}A|A=X!pKcVGo5jk9Zz^>k# zPE&S4ca$2t1r05U2!!^x34xcug~`zjKPb0Qm-}U9$u{NL8bXujiwqK}TYkzO;1_uk z178tQlF$v4!%&CBm6HWA{9T<(LUY3&sUt+@o?gz{yeFi82c~cv%vZ3#p!E7%llw;^ zDCS^i=U}h#_ovIh&}gUjufu|6n-|732DHtzMQu=a2xh1%2o)+3vB(vXo?^sPu6%ab zk4FX(L!Ob>OqNC0{Z9i3$S=XMk^zCeeTHHJ2(Shjs{(8;N7+}t7K}ogy}mHaAQ|Y7 zyVLs{6AcggWo9oUZufC#YtdX~0U-P(f}6CRb8iV(*he2SgJEuk%L2Ao|6I^45lxD;s8>|O@uZ$4whb>Ez`EuI7TZo)ytu5-+?SZ~~OECcXC z3U-$5S&n){CkhH4{DombKJRs9urwgs*bpko0|`FK1Ny3WOqF6i7G)SlM^eJvV8VAs=)I!j>tXyE zv+Iai=sX}7&corA0;YM3c=hY4eBQCxIm*KjT?Cj^5;_cbt*+-(6zyYI%#{(;w7icqk z(HUt5g81!x1#F}-21`0_uUE4ihUNgI+MTIUg=yR&&AHNo_{1Jkn&Qi~14z=`Yr}sh z8eA5E3GZK``9gz%N&Lf^k-U=n|JmKKsk$hQBQOm%K2#6^|Ma6neDN6w3x5U+i$KlG z8U+bLuoiNQlg&{WV?8k7`ZFKcZ_ zk760D!#dkwlFqJAubBuH&>;C82P;ZixKer@d2KxBSsKg|2sG$e&pVy4W`Jj?L(SZ1 zhEQ7DKvvkcQZJRWE56Jgxb-q!lAlBf9rv@^LR0Z_6BFSYbkEG*zJeEcuzgOCfIxl8 z!-xOzTjCM#J_`2NT4H89E@+U@3xgUU#XC@p31tPX2|US7t&KUtQO*24C_qgB1q0_5 zZpoXrJ9Tn0y339P3OPb`8J1;d9#JV~o z8>>k@?vxXOtC)l`!z`~B0R9G1&SA+t_%oN}g_{`-1|WNK`?Z7~nKd5!$E$%qCJpF= z$~PQqdb6^f2x`8Dw(Uf z3}weWTa8Y&0IBBuIE2r44ob>otOp|#uc|S}qKzPj{R(DmCz@C|dDfglbs?#ihx0}d zpq9D%(j^lE!z7=d_EWpUeai}QSy)n0Lt^m;LIwcYO4?4Amwg`U3h5Wn_?W(Zt#yhs zP!A${8!iw+FeS&*^HFfj-u0Z6@u6O9h$vlY;KI4Q3faawm47Xh8Ks^XjqfL)N=G9Lw+BuCdCOLq{k^6hgmU4CTo4Lh8=$de%j_$T>m1aVbs@-A{ zbl4`WUC%1A;0LDvb@J@K)ZzCL7YoWx_SL@PaW+1WOM%CnZJu72neKp(*-bMrm` z+SMYMwUF@!9u<~aP7ut7r;W$!FYWi&( z8EVYen;^#p35mD!xw#b^*`~%4(XxE3w?F(YF8b3qP#}ecYw#F9;}?=szQX@e_|9g{ zU22}RyT7kSZ!cT%{cE+1Bf^Rr)-P7J7j^_>Ru@$z{wi}cnzVI+g7byvYRLG&QDR2g ze!7B@H0-a;Uo?ul*)s~LM3q$gk|FelZMp5I%oGH2CL2AuC-$J(URU)}dU&P9%G2hi z?7!41Ptq`*HEcq_@pV;%1)x{X30IClsfSDY=+3}e7CiRpEUOYSY`E&<{+V0lo^)m; z=Qkk1qJdLNFi6c~Gl0DQuHcE*$pAq8=w22}6S|NfY;giG#QE7M?Ap{Y8ii4>;zu>9 zJF>F6h;l5lvtSGvLYOfXmx-+N{rXz}m^3Z+R1Kq4tRFaLLSMjz(cp2c&F^4&q?h4& zqRnQ;^D|lY?2aSzql7r8aPVYDhY!k@#B}u5oYO(F*(Fq=#7H@!!zmQOH?M8l0=-g( zjws52Tol>xN`lz%GZ>7fka&UBI*MFuW{-t`5IbMrjIH=|cZ(q_PYf@7SuIZymXB|>IY39N9*!KX!KDofmZT~HL&g(~-94y!&4N#28gY zCQy)N`e@?`3A5~b>1ZaZ;_3LGdXAJo0$&Q@F}Zl_TaIag!Kv2byP$0q+9xwA!9Gof^avevooEvm#d0Fenyr4eT4wt`ZKd5;`Kd>!Oq;3; z2=yxow8iDy)$}9MMsnA{Muh&7$c3$ym*9!s_XfR`_Oq6m2@@(gK?sd9y}2@Z#&>8z z{UU!;4fGiJxlpAGNdPY(YIW#(y|?VS9b&jkDMZas8z86mIiqAT@6#2c?!JR@Wxn2i zM_lpZJQdUU!+l#&qE4`*MOiC8%i7mlc^YXQR-p7p3cQ<|x}3Q>o5RmwuB371Vy4m~ z=a@8p6#<8#RhC$XoBOklOt$YT>jwuXzBOeox$MehsGb&qZlyun-U}jfS2}oeP=Wr@ zcg=(v?BM*))e*TG9zQ)1-s++}Or^?Vdn!-56`o<6qt1mH7^e(_$QgxQH=Ed*=ITN# z1NxO?hldUc6S`w&67Cm7+UTEjIIa(IK zi>=8lqD&F<^EdG+s*ExAbPi*?W3FCBS?ltt*Wth^)JcjVXR{$blCAJ#6J9mDML$fH1%`2; z1#-h+q}`wAw1kLzu5OLKgE0a5;RA41i5z{`wjzYg9~3gW+*;v!`b(SB6E}#9@jkM|Btn|3X6O1vV@TY4=#mE@F0ad1b26LcXtR@xVuxh1_ycM+NFT;<2ENJ_pS!uTKCf|1O_5Iw za=$Vvw6UNb7Xu}t5NuC?n@^9NetOxK&ITP5Sp!w-(_I^>HmSdy*@^i6PJch>I;f+Wy`tHly zV#H&k5A2T3`qGNBRZiq{rncT?B6R*?5^P8_DGmw^{ZG( zU<>bo`=nj?3bVAhhINI!%^{Y9CSxOt-UI2=V|USuGcj}-(Z>-}uF8$7m|KFyy z=0AALh@c=Kp#Hp>{d4vXLl+lQC(r*K!KGkm`PUrq|G>Oh{i_j{Y?W*+i@qyaSvY>4 zC1+nJ!xF+06i!O+U$8@h6Uw%s9GMwE|4|1Ywq$+<*sq`3H$ce0k?@W89^}JMx6qo0s(Ur_Hrb_kIP&tP-uOeoJrw|YHdA|+piLv&P7wXmq5&(wv>b5 zcB*3|@+>|(*v8e>Tp>(y+?-tYNKF4HGu-2nU6y;Y5}GVKrTw6yDhh%e0*3V^S4L*Ddy&wOeCv1Gj^0Q^?9ptR@Jo_Xm>z@Djpmt3)ktSrstK?p2x&o)?T8@ z+lnVpLun50*@`?a_E~s0IkuH0z9n2n!H4MJkS?wPL~d(Pd;2`jk)=g7BUWwe+9*iH&U-vQXGl=IT|%`}?=IUKe>97>EuQ5&;JQfX zGB@*odwzrNCes66CMK#sJ(TNJvz5cRm%Fm)R-}fsR%-R4u3;Z%d3B%F`;pwbyVaRh z{KT?bGLo}BEwOHo|DF=AJ0sn4Z4|!r*>IU$s4+x_A#t*TxYq#50I2_q#VeQaF@@^Fuzm^y^EstDz1g!+9|cqfV_IK~9bfY9;%~ z)L+mX!-Q_fN{F+5DLllqou7!*BQ-l`+QtOe7OssZ0vt!(Yh|$UrcC|GL~iPx2uFU; zj=44{lf-IgV>a>_ZnKnvI z^{T^aN@v&k-a<9rQf)bL(asQW4Hsql>DpF>9ct= znuXygVcB}M!lXkOiGqy*jWiAo4j>vjfTl4Qpe!o=h&loN5 z#kn2O{7xmS-MQI>>JUQ{`x|SPDZOjvk1QEG0BcVz=AVB5Z6h2mQ)zPp2?2ri=S};6 zhCu0m5taQt?&`|VDggXqUW^K8Gi9>ANxcp)0Hu&95%Cx`NRIM9;Kn&IxHv`UmDZ=p;$T{%cc;MqY7@D--gsj|#4DxmQvFcgDGlLRh zJ|qNVtKX`lzxeswQQOg1lX#{445{TtiK266G1gZ8%9M|w-j~M)F||21t3cKLEUdZO zqfjE)2}NB^otyA=9TNZzxEFPx8~=&~s&z6!&ve@oQ)+fA9h;JdAtT9r1do_n3{6bA znDmc_RVctjWa7_A&!xZx)z}?__l+A zuv3B*Qf%6Q49Iz%y!e975V8C#w`t8rn*(Y4 zBKHTZnvhv#cC|iIj9H}#wM0REw=Rw9z(=}+jBR3x;a;@SG z1cbu>tF4)snpxVJGD`jnfFS(Gx?BwH{tZ3wRa*ZOg8_WTWkt=@Wck!#nxcX<(nypa zq9vtqmWt1e+<(S$WE%3+2d%D;zd0C_U&YV$BQp9xIM-=>*H4=z zRxVij*cGP8?wp6BZl#`}V63}xY+zv1dFBqgS1hv-1fvArwQ*dG8Nb~c&7j9Xq@mMA*4 z6j}roUfj>K${#VBv&FF|8O|aCM*?mGVcOf{`Ob?1XEJZy0E3kz(K@vhe1hr;qA^Zf zV+NR>UT{DAzZ*t9Mt{*E<&Vh}_KW1aqvV~nUedoXu+r#~_IXC=5~q%RDP+c!(EJKQ zE$TG%Lx}Y^*yhQ3xH2N{`n^INYFI=(`TK9UO3=jQ%K0Bt<$(F0SP+Wa*c%zz{9huX z%Bs?=0;aAwLoFMvsEPt+bW@{Jpblowmj>XGLJuW#0plGjpKXCJKZLRQmotZ%_sZTri!TdRd;}G`qe!8zPPG ziI&Y+;4xZRL&S4LQ`r!jjrAJfKBTm2|nJ^I-#y}@#Y^LkC9YMEpjcUe|n zGVoIW(&N%t)U}hqpTMX_S8`k)qgnymdDIA%%#&Pft=fE0k+WW|iCk3$DnVG>4bW7V zZK9bgzT{iH3Td@yGquB46UO8rgG9==&Bw z8$HvjYg^_budP9iRbNCA9kAz(%D0&V?6>ZkM-w^(e`}1@6{l0L=!64!*=xjLtr;Ck zT4`C``Qx)4t(Pmt-ndb>o5E@$NOJa)9HG=LXMJZ`+f=^h!WDO<13?m)d&I42pU;&? zeE{vmIS37s62yRE`m%^$op_g;1X!U?DLa2dta7L7+m%xjX!+(3im!l~~t@eCT63L))x{C4ktbbc;;$nQjW zh5Nm{y_JyC90XHO)6w}9gN->mkG7D}FaN_X5IKp`l5wdm8WTSllQA7Sl0;;(XK^s$rsJ)k1y@~?)51$ z>L7$PhUhD&a!;b~F#9qSb1cH1YFEme3cG^*TW&Vdgy*aqn|3RH_6H#r!i(vt8NO!D z9t;dFb4Rs>s+f@2{jP%J;x_jRYRsjDH*_D;mseXegWzpL0Rk8q(B8W_FwV%}lNVR= z_YHk+RgmH6V5Azrs>N^Qx>`xXm@J$$%Ut5u&J%gDEFjHLf_C}jgD{u+d!25>5|PM! z7xqFcXc+f8=ZA1lc9;aRUwPeP3ssXArhBdjw?YPokD)CO+HvuVyF@iXB!kw&Wtqs9t>!f3{k)4qL4|`@lCyOUOo6B8K=7~vaYnz zd!}gO7^)@vtp1!JoqtVDE;&cgSMa7(Js2WWU}A)MgS6WY?mb-GOzG{_{CUBQv~W0R z09cJ`u4|eIs%mCKDr2J&)wpR{5Mn9Jl^RlCNgQ`YYrx#IvMZN63^UJRj04Y_OcnK0 zQttMNZa}Q#CUT1J@@0&Z2%>e<*-ZL&&xz}B3jGG^^cNACB?IAqJoPda%Q?LAF^AYX z$s1w`ka9p=$13uMydsNje~1*<4P=fMHrR|Ss7|$DmUhE522h=od)DTFH|!oP2@0t% z^D3CG{+2#O$%sc*{}{K($N!0{TFlbP*4@y_RL#`M+0y>+7d$FQN^@UP-#~o541$m! z6_H8x+7PAVgwc2fD}~giq74Gn6ouV1pjv2Q-)esG-G2NXiCJoz%>NCA!p%((^LLS% z(yac2ORIM)&+FOCK{W&jIML0)5&5|qbG=rl?^nR~lIKqvcvF?DV47yeBt6sQ7f)Z+ z!xKM?r_n#(Fbwl5lF4lUPR{Y+Z5x5bU=jRtOv82U@KV4@U%PE$4 zL-e)P)`=XTbdA9?MseqRb*q^MyG=fLue{gMGq1Y5br*jAU(CJ=#DJW;1aG6xc)MC? znr82KH;kC0KtYHljU+xe5h;P4gNdok5{cz~{xK~|jJ0U$n=;a!Gs`SX-K1FxW#qte ze$@W4Bw}CU*!x{ZcB*3R0xNk|vSOqP0(||5gt}sAR4Xkvqwx29JJ7-pxEPmj*d8EV zGtTT0?1e`dby>UA`Er*iQqxDR(v#i?T%+{U{3z*W0?&@#DTQei=sp$1mM3a3tz0==@-s0$SjhoX4 zsqOys-L#6r7LbdJ#EuU%`t0HI)^dkOI{}6WF3PgxcqEQiB(x;p!BQVAttRFa8~!7lsucx9MeqO;L4cBb-rhk;Gkw zv+UC4&$}b!vWXTsr=((0!5~^`EgT2O%}FRxW#L=Db7*G!cmwU#!^F-!e{kGMStVh; ze{aK5LSv8|)D@fynuN$#(m0)&3^6bM?fZpEI3oj^zz5pJh}XZ3V(7h3zPbJJDZY>Y zkWpLQ)J4+H#nj2p&_>zS*}>G#-(55>8++R#1@u}+D@VkCZNHoUn&5U#UevZ+bB-gfp(#TGsy13Os0?H zPMI&JN2qQ+c4M43pL);3NeX3$*?IU_I+`uoCj*(jZHK7vaUl5qQIm4JN^sIL84 zT_ga`S3aayOWH>Fae?RCs8opI7E%>VG}v8}7PksmINoc9lT@({1?@csbfI);$}d2{ zah`GvH=H|#qP966HYACqgTeLkAqo!cVQQ)fxsl5tZP1+uj$R)7q_gT2&yA%v%$iVL z1BQ|riq@tbqNPU44wL+=6lNWvIc}O`Bx(JXMUadslLtOq*@-*Jx7Qvkv8|J*Hm!I4 zPSvMc#gzKMn?GSX*6XG^q`M}qWyO404UHQfI;x4YW(W{Cvq{Bt!BmvWDh4dP3BFgj z%=n9!83R6h-gf7s5>MN9PpQW?cf4f1;J0rlBg2X`~sBJ(yJnsa7VYMyhotH7uu|(tHGC#y6In;Ga~b z+?PQNLV)#H5mVRxbX#}D1w466&OUt+>i5v z4L5?%ux(cdAjQXsEaOE((3??+Ad!=G+C(Ph^nJ98!sHi@LaFb-8W(m~6mAM!iO5II z5r{h1J`5M%_Q{UMxAQJ zg1HXqNn-{4dUb!N%w;r0J*$RQd|#yPR-fp1WxBXwDgHUH+ySuYakpyoeW1IXh zIHP3r4N^t5;F0oh+9mZCcuD-D1k1F;gx-Y8>5w8|@tOjSuIh|mw5J6EFCYGy@!Jl~ zW?*++-Bq;SM0eN^N0|jr4NMKEl9`YF5(e8~+9YrCsnZxQSg2^bi|)|aJ$7E5+v!0i z#G6AnI{D!N|5RsMjCz3Cyy|{)_4>xUWn(y9um7(c1%6Zyzwg0B-w__K^f0@k^-w~+ z70EAmve;glWAN#9`ncU)hcW1;Vq8=yZ$(Ul+t%b_Lu&>~SWXxx{ev#}8EQHPs=uV| zeyZo#@EIC?=BV@=w1KbOR?40=oUDs_imujp)d*#DK9ck(Lgwh&+Arbu$~+^$`}B5`86p%Eq=1%RN$3sm zi2lN;syDV}uG41#o=D#y_2XfI8UMOJTb>z)_9b)WU^y5|x1V_0A4`9_dwG$EaMm#0 z8X9T|iH$Iuw^`rHu}d$@P(>fj-+>c$P^V6mN^Ya6qOBKh(sNE8VCK+aDqBiciC(4Y z5Lxo9;?&Lff_^W-$-qUrIyQGZ*Bf|E^WjgBin2?gpP&1pIyqn2TH;jqOT#OC^}7}R zU4&gi<)V$M2Kz&OdAVkcLQ7Qz?zz+F3(dx0InXtwI^#Q8gOb;cRwv>1O3G2hPl%Rc zQ%lou@PQNoJi0KjDzmkzcM2`?qUkFxx*ZUh3BEv3HkK6Qumh3qV)q$TekAE+{2Y}7 zCv<3f_@iYP0o1W1NV##Z%d9h5B!-oL{nfMt z7QGDw0eew7ha~{Uha0OaODleLcW!*Cs}m#5&H>GY_()_6oOAV8m6CJQ}jPYwQ0v%0hK5z$&1u{e>4#%(1D5E!5uA!^gSe>_WC2oIBt`tl}c?~N$wK8?^4vYX^U=`Cg&*< z8{oF$4BIb|!u-sHDu@$iTaqhJTiQO<(6_j@u_qcdxZSSD)S~V#5&NsEwIE!*l@usg zA#~X#=e5!srqCr*;P*iSS}0suWODkUA;8ctk1{`kqzZCI((;~#tOT(B2z7+{z@88y zWWkR6X~M6sV6RlfYFK@C8W{dKib9TPT(BGNER)UZ^Ibt8u$$OR5h@Od{96zf3LS(8 zEmWFTOe1joHBp^Yk+bhNZcZ+1SVtaqcj6%}nvM$R`}X*B$MemU*JMjv ze6{GIe490+cz|!V7V%RF?HtxTR?3!Asb|B4X-_HQZq+TmlJ4?B3Z`d+<`-6vIs-L$ zZPwe6;~#QM(A!c{LYvqJIQ!BRcf!Pf%?xF|H>Yjt)E)}AA9DuJ%1_c%HDfyG!E(4^ zd{|`5-oa*+f-L)Rp_8r)lHjh(><0{D9QMtV!!TirBP}Kz4moVeQZfZjcehO_bXt^k zKF2fI`I^A(TCqg~8=G(>) z(y_c;gTp6ISLM7-+Tcj2Xu~BeHszVHNg-T4*T}-QZvP})_H6QVL#BX_bW9=~hmS^K z7inyn-ks8ZPP2_6ZHA6CAtn;1oSoKiQnb_zVhFHg{ZS%v8xkZ}jT8Zi(KFL*pf9&{ zGpnDxV&e#Zs_Eq#khw>xyaANsjb;3W96oNP3v^H0UEboRy%h>VWe186&v1onG=?0- zX6tY`BJ!Q>ktMlp%dkIuf=(W-vndKuIseeqMY0HeUH62&pW6sb_PxRV$tb_tBJYl( zg5djggZhbyJj^fx1SmM}BNSnjvc-S;8zFIgdB0NnA6FRs_z&11|8;{ZDP&_RV(MmT z{9ojHwttcbYEE)0R6f~oBvfZssjU!~5s)+~tv(Q5@}m%jKzGU6hJ$YNd5*VtL;o5tSHVt|!ToUs7(&_q;z1VvyKlUcLVf|wSpPx=-?eq_zpO|)os z2GR&)f&xLcpEq{=`c)4v=3*6=2#}Z$Q>CziImn ziE_aqQNjn6`843ym_@4?S_<3{zFHIx{6M&mgGIu=fla1;Xxrp|%~t}`HrU4@e$igK zw-$-5={I;{Tw6q;tqy0Np08cT2AJ;+_YlI|2wcn+SJK$(f`8jqHO^9#?IeAtve1&? zP)$NvsfJfH7%I03jxmeaz(U`9~#kGerZ#Qj?IL+BGn@z1vZl|s7NgYx_2<>^+QsPef`V*E5 zi-iztFD!Fk6j%IRd@wmNE2igz?I!_+>iwo4F!=HCJ(X>8fZIL97bMEbXJ=)2=bepr z29EX-gKw=PiWB%j-0sbk!UMKerJRV~Wj}sMrrV8(B>0GnL>BvOFp1!n9nHb{eED3M z&IG|V%wNE&3NirvIAVt4$R-TYo`RjUXv!^K1n`#lb8NAz)`a)KpGSMkt!}`Ir>2DEA{k^8Nl6_3MG{j@?v^FeA0BNgls%Qv zP~RwOQl%luHLB?~J(aSNq^Gd*BS{I4)lr6890j2WhY$P!-=Xa#G7G4{TpEpXLki~^}q#h^4*-Pu}k9x)oml@v8_=Q1WkN6cIxX6jpH+W(2q zRE42}U{3kx=O8f?d0LjF^2&?FT^eIeQzwI`xEvj#NF_Jf!fN)`*4Q83pT3AvoNBk^ zyNT-R?x_|jASk3&!4CPqEal@xV4mBZez%XMcpf((V+W?miH^x|mr~`Y;6@&gr~q*% zk^0dK)4?WFz(g-@WIfk~@)B!5;T~zfT#x9~pB$eKJKTk&jSI{%$C?f#ZuOr>x+Ma1ix^ zEq%)N0O&ar^O}mXE8=kctU7O(;ms`hi9e*S7-wGLyFx;;I`zi%^t7flK3Iqnc`fE9 zczXxizSBb7x(fJ_VYi)zyw1}2LF_;_{wJq>gsxc;kb z)jXzUXx1!~4b)53q(UlCTA9bTJ6Tz744-JCPO?2kx^ruAb-$w?Tb^t5+!Z}r@1Q@k z?O1JNJpL5w)-Y1jf*|^uE)v{hC&0AgXx3dhJ%(tW^+AENiO-Cg_)dbmadC-Q#`HzG zXF^7wHwDjSkF$Qi@B@BZ8-51OS#4CkYu@}AxkbU_byPgl^%JYav=LPWCZ0U~a`uP9 zjD$fAvJ^YDw1Y{OyW?zxkhkM(JsXh<*rKIhqM!GZ!GO#TjYw|Km2w4F=EOM%$TLyu z)cwleZ^wy|V4Sn1^B(roOC!SL0V8uF2SZsGPgHD_%qcph4{guNvC*JNEHfm)QJiui zTI(7eL#|g%$6*fxieC7-UgZ#sCP!ie10GJZzrSfSEoq1!7v5K~>#B8Y}vj)goe@0qgX^qsM z5XdwrfpDsal4ovULIT(6@ivlR?V35dG!6Hgqf?+QK_@U?HR`eEQ89jATlOQmvhfC( z8xc*Om)L8=gz2LCXl&6cqP;TeoO-IK6#`VMiu#7}NjroX$i&O_bC(SH7G0K#dWPBl zP8;VrokF@aj2l4@UYB-j{R5K)6o}6p-VUf22ZJV*VnLNt+x}dOGWTid0;yx3Kn$L! z*CPxayzGyY9J`p=awVq*YUW}rL zqz83#p1Bns^)4c=C8i?Al}sbC^1BXPC_3V$iLPCInH8#UID|CCu|^`iZTeszC%fbw zNJ=Me_BnHno@Pl-HzGD(iPHVC%xmGh5eh)+z9H{UkMLuqimXKMxY-_c!i3w&%`r~{nKzRN z2j03e;vNmry(4tpZ)yG>jMUSiP4O51*_B(#e89@`YS|qskW!$K#o&xQjDvy)&-j+b zc7v0jNE8kzQ&F|0uciy|$u#uy*&5k6mBwArGbUKW&h}t9b0A-vRUMeK7oWTo4XUMn zi1Xj^ge>=`p&LGT@)R1NHz;>6SedYK2fGR|r4*g6Ed}wwC8js)$Wq$M@HP{QZ44)YsZ)w{7sWJ zm9(~zZx-4H%_^7v&Dg3WJuaijhloviJZ1U!S2JgBbCw}4rLKDJ+cHnNW4WA%!lIx4 zgG%N}v%jGzSC<57tqoeaQ@C<8qfTSj2D70QHBP$>TI06Yd8|ene1(?4uzre)S@lzJ|g3X03q*=XD|5UFSgu?jcv z8paBsthgdq=?9TFd;;If#ANpL73R^A=REa%P>B;h>JNO>KkEw}RV4G|5LWt<B!4*i3^s{9G`-zY@o_;lolMj7_H`lH-PqPZ=on2y{%@e5HshmZ+ zs?F~x^mRiW`DUfmgqaP>~-chR(b;08*eGolX!yeOvGtdtH z@Ocq$QJIFT+w(_5BNyef4}Ed^K4l(N-D1AV^zxdplqco20TH$V!#nLf!&A)DugT$U zoJ)5@=AnP1d%udHd9Q?kuBFY<(WPU|5Hm zl1IU$dz%XQIm~Ljo>fHeJ_?~oufDwtq^K=l@vDH)Z2MTk>tvK;PBW!M(8=;5jXpkC zg|$wmdMz514(vfIa>_^KU7?J&vkig={;|Q6T>PkPUgNM*fhlJIxS-ir+i(kvwuV*8 zHR!qRNARUvH^im>H%rIoN1bim7#A9hQ@rARvQRU=+lbJ}ftbNgs1}fkEoNc*sA%C$ zq=0g3RZUsV6F;w1b!Yp|1YIz(TMB{^!<6K{-FHk+3vnMTM)y~fDp!|avb#vEIE?KoSSRrJOun>vErfL)tiiM+%qn)cY}*QMFu(TvGk2zse@(Lf;f1X#=`D?RHtB6s<2C$i zbd^*H|Id@uDRY~4l3PdoNqFC}!RioxtKAYI;@YeFsZd*9np;x*SVZ5c!OudWwo!1e z_0hsYD9g=#P2iCSKl~;H|HSAAc;BYzCPd%R=#!8QgI%q79t-2HOZ)nG%x4(}Yia|CVSuFdDvgh@O>k&{Rz{H~5Y{>#4eNd#q~)#PVKe%Xw2>vpweD_{sc@ zB?Fp|FC?-pa^jSqHRac8&7UUt8`+5Clm93p^_1N80Vhl46z)*`J6K3RT}Nu24rN#y zGO&9`l`S%oQP=fLJsG~J%z@%+Mx zx2Bm|YKoemC-L*+Jval+$KMA}u8P6Y>aMM&;kfiIprUn9wIi5YU$;*HZ1_9a<662W zar1hc%k$A1TZ6LP!cjG;%~TH^6{79o!wB>5PyX%)0t}LHfggx{Q5^rNZwY zfuO4k9Zv56d%v*vW`~9SodIsCbIj@s^^lKn1j>4?6{GO@R$$(2HgS4fHPyaRfqXTj zisU4XFR!0k%HJy(#@={Dw*BR9@l+E}uA0UBtafXxTzyk?SMNND4rp>bh`Psey7LRH ztYfU^_lLi`o646ITFgRqw)9zPe>W(V7-_1m)%``Av()xjABS-lkyV}4tR1Dv%8?nd zZxHZBGrt#^US+*dyJbZ(Cfop(>EOt2Z7AjJh0AZ7|25#UmrAveRW;*G*%@AVxs&bq z_unaci*IXZ$NrLl`1lWK%l?`dt2&q%x|j;v+u4~K|8LrnD%Ssk79-=I8Vx}1(&|x{ zYA;ih9h!vwC>tRQ2Z^HhGn$`)zqr}j@+1MCWUY39R{zq0Sgjon9cL7q$#UMvxoMyD z<~um3t`>lx+a-k-;9j9^Ph>0E{+9ys1P6icaK#TQx*lKBgJ0YaH(I(D)xXX9Qq>=t z_ad`S@t$DZZ;IuXu&NqM85iNaNqBQ5EbJ`}9l;85n?pT-ZUv?@KJf$>fKK_KQ;$FG7%b(MY3k9ko|hZ;k$y#T^V;M$S-*=jLi{ng)8 z7&(`^L-(So*;NyQjvd3mIa3A@pig4)UWXp^0lI=oq#_w7R-zJ!RGHq0Tgf8AynIGv zqlQEybdF}MZlI>v3lu{K2xS=3%|gemrvSrh$#(#$B-=hvuW2TS?kxw1pw#qrrTfa6 zzPObG*k3SBpBlUlVe`@YC_o<`c>PMb&Pjt3mU#zwtIjY)YiEor(8ZiSswXx_;eyWU z9JxT6qq1pG+u%@aURrC&tfr4?!c>_f6-zf$%rY(_Lncb4 zqsg>qxO&%ND_9O`FW>r@$PUHN<|^5i$DC;WZ4B;&QmDaBzD=^6#Yq=h(6@M(R` zRPPMDNst&ZCU-uqTXPA|RMJfEnFV468XkRR1Dg{5uYnF^r?faQ4)mxG;)6WkGxc;9 zxrj!MbC{bAQr?>0|Dj(A-S@RF@sHy$eEbK5Apc$l%i5cm+Q=E&{@1u{Rc`e!KI;#s zh2S*rBf^J$@(;=k=wxuoDz+itl;Q(LJWJW{cK`&dKM1u{l2Nu{pQT>DR$$*jKP%b} zkp{wEt0-!>)p_5aj-?Y;`@g=uVRT~_<39HEtc=0VUmka!T@K%`8LhK~a96EHqR(ozK*nNZ2G(ATd|! z{ji2#JS?o|C~Qriyh8#x1_>^2dnZx2(|7zJ7RzVY#um8(6v`TpQnF7b^v%gaJUT+~lz?j-P)#^m}(0wWVNCwa)tOz58(epwL0P3`0nr zanjzSBid2aQH&;iVW|0ByOm)bn2FR^5@K7x70HWmRjde7PgQthUO!+-*1R<)2|_+3 zzoMb&nNkgJu5IgmU2H#|;?=FCPZf{;Hkcl<2#0jF=Jr9%A~ot!a(Y1Sv+%2N#@%PZ z&tE6}1Fo@rb2ACBVXc@JEX@N?O$0M|gOi?vRjyG-QIPB@<$wKJt#0;IQAon@l$@Z# z+QXmoph=FsDz1P(#v*@L82dnu1SzkB*I)sE}PxZ4hW#5Ns*a7Vm;9F9xe)% zvu`OT$g0mix?K|}>2NKU%_T2B!aO7uKCP{MxLSnTrqjoXchhxGf(*!DRx zczaHFcBk`b$=J1o7F`|VlOgma?nj#P6)Ew}s77_DJ7 zAUXV4C8S0>e>)y+Mm9!1N?x9-^Ynu;gz$9zBEbN=^+~5Wr>y?ATf=sm>bX~hHJf#@ z?E83Y1>DeHXYF@@oG|qoTXI5aoAcUkiX2_Jtn?wj{UiDcE>XFT=UvrQHFrU0N&)Cn zXXw74Xd}4#45_%f2-nNKC5eQIV6~`IA%1G0n-+!_C|%aoGU}6a*d1WB=2|t0 z%Ru>VTB5Ox3d)r`QyY;mEX0?LV(wpra{mIE3bf_IH3py%eDNl)Vc6D{%IleuD|E$8 zWx^UF|E|E@Y|15;7PC7tcOKZGK-zU-%sspTYZAngB{6XZD`QLR)jmkUS%;B z8*at)ur8?0o`DaY->^BGcZpied%nbRO+1};9P!=>LA_bAtXO!A^X?+Wqwug;w=NLR zv;JZWf8*4YU?`l^sFZm)mrX|B+ zz*cD$=BjWeZ+B`*=a*f$I$c7WOP!f>*K(dam;J0F(B5$p!=<_3wPYD6GEx6wB0hfN zQXy>mh^`X>-jHqee)~F^`%5xECjSc>?Jt%~{;(JaFyH42^U#;o zT;X~He#G7FU?!#*ld#i4BqKbTd8_YuMQ-zz=Bv2aEK}i;u9wVNa?$I%;LP z(uM1qsd_38Y1q3xj#T+$c}d)8_mH$D)P1wQY0+)^#E3T-JIJS*M|O`G3)Y`Cn|_48L5J-b)0xl<8-)I`UgMpiD&SD?b5iJmwsHLC&G zE&x`mXhHLRrJ_qU{NT?^U|qtfgi4h*R>EFvimx}fr-b(#>@mLO+BL6#Yh7I33_N%L ze3LNfe~^X#>$}L%S=um4xz;@ZnidxNmSyRvj=tBZ9-J%`A$66tl0lO+(2 z)xK?U^_CavKxg{|hG9qPldBAx%qxEs2siM#@uvT%%R$es)ozmw_`>pr&{MbnbU(Ag zl($=s{Am$MCSR@Z!#;de+}{0i!5F)?zjw-20(j-l>6nx`F`d+d$M`sEqE|g@t&Rn2k4fM z{zX-^9VsiRO!d=Ue322h!)_8cUpcG{ov+9)ACS9~k5=#LZ{9xJW=`k}0|7CQ_z$G! zjQZ5oz=_mBxtB>UC4)Coca8-vi=d6m9SG zv%x5A0cO>;*o(Cb-gBnV^?%-Vz*{`NCqif$4*A9~j6KNQ^O$FnoR zt?2PA`P38*yrx40L!e#`pdTdUh}bw#8;d0_M~TZL6(-g~@|k)mU7Z@x1*8$mc}AH? zO^Sr_PMfTBn|5E`GH(!Z1cK6|&AF$31sXvSKTn+CxXSCuBYDk)}h}Ro|%0BZ{uUW8^vh`8hqtXj-w#sx zS0=sPW^){ve+t~U8!^_P6`XEeWQ6vxp*@O^6KIFkj0#riE_6&wD>dtcoJwhhpTB3C zov3N%`pnjBrEVoVq2>{Jd(fqXnZ{t0ZszMZW~MfOeNTkI`fxvW<(ZM`U(cMc@iWrJ zQv}R{;bJKDt$<8rY19}h?8DmAG+Jv~0Snjj*&*a!oU4F1juS!$&zptsXMf);o>$Ee z({5H6c?sVZpRZ!>?qmm^okAGa#t+mq$h=s@hwL;L_lZh`0UcSy*(G(0HGmMm#5y~9 zn%hBE-5087nuP`)D3=HY%OQT6uUG*rE=J*YQBy@(4L-O*6B;M(Sgp6o3AuzN9B*I^ z>sc3mbKkC7&0QohJr4=0@0tRY)VI&-yw|bnN56P0_PDtjS4u7;6im&zDZa7=P+Ff+ zMjVY(oL}FSyIO<02e=0M`K+DRGIY?p8HF0oHotr$4>ZA${g(X!FZ)XIBQSBXjV1{evocx9swJfO~qooa4t6G zc|{8=r)JS0xHg+yK}fecaVr>e4*rsJ*>n?7j-j03zxF7lz*gKfXPHQ_D5NcAtPGW0Wdm<25 z2Oij)olhS}xSCE_!o&^{_fWC&zUhlDSr5I7eB*9!x~%zVJ^;J)d`<<)5UybUQw%O? zK!zSa8n1X{bKD&cvrZd^qkhRiXwqc%(Z49=|0C_3qBLo?bzL^UvTfV8@s(}cwr$(h z8cxXvzI+6yD)@9M*t|GKKv@QPD(XIouEy32ot^xEp zwygWbEL4k_uU{Y#HH;%s73-Fr&H$ce0T9*O!_f1e}W-T`S@1e<4)aJh|*S~}(v|n0D zUv9vk4|rRgUL~8hCeL#72$_qyRo z8G3g(GqWzA3FPC~&Yz$ssMYc-Jc%xX#OYHf+(i5QlOZ@M_MgyQHwYq&2UZ9a&if z(-i}7<)?RCf=A<(s^Wx_RObyestph`L?Z}h(Bk;fs!Om}cd6(`o@nNG>IbV>wlVQF z*Vzrz27qYZvZ7;s3`>~WaS!civ$9q_#aVi zCG8xI-9*f6++E$wUH$>KE!TMQL{mrqjHr;ogVupcl7LiDK`TQfrBxMPq)?WjA!mq* z`Qy$iQuhQTY&Eyhr z650e!PB!6xI;=LE`4;njC+W5hv!iM$5fhPaZ!#4dJDyI~%_Wuz3d`@xZr-mbAaW;I zrtNgLu!zklFmFR^k5@I|iq3%Z&}+6k{#NR|OlxzSQiFF}X3WBsOhCuO7>-OrTal7WK$ z2*CVvIfGFC>Va$t<@LVI$VSH7mmg6Z+}Sk8&$`$w55Y%6`7Ty^Md*oY}RD#b5VVq&RZxNB1C z5nUg=x)@;(>GYhw}d@$vdLNZ>4(;7t67W#hBg9Bt*U#`e=ai$)8nm z497UEF{jG{i^I7Ppdp#c;=BarDs`am(r8S)lZK$#9xnq+FdwL-%Jnp| z_S4l+bb97DnvX{6JdK(zMVM<3$6J=l0XT8jTQPyP%qiwA_}U58_3C*Ddm1*Tdtsu3^6ry7 zoQ@|KRGPESP75oac+{#hmw37H9&AJ7r}dIgZg>vJ2gFxJ^;-7H|CI_NCmY0t7dWVH;7=r)S^h7IzfIVvrzUw!T1@9B!$vJJSWHX zfFPqiM~imn^rwTp%S&nm8KOLwhfpHb19n^DY9O^JZ)_o_@%a^k#-1B8%y}8v60b{+ z;|jR4b!h=arxjmZ|=8dY!ZEttRaQ_)@M`>E^YnD$6OnV>;dRQ|?=& z%_Hf&+_86IRevg#JK>w6Uf%^|;O9hHw;?Bl)Xc&UDLC#HaJ{wT{cPfUdL#{0$Qo*s zHJ8UL^U0Y=3Iedt-pH)EMZXw|owrqLU(nwhvYD)r9ZFetZ|)lA+k{x-*HmheTcIi& zNydew_@OsY3x<$>Pl1)>qaKA=>n-UYVId?zXFgr0Y|0ns4rWA1sy5^fw$YHMG2Zc9 zKyDvG>r35_hSpa|3-F8PUMna@RyccalAJdk1ayBYu?+Bzl)V=@H)OSavud`<87=VL z{CP0r`=umN%!^U}QG^6(Pn?4);_IE%sxI=Dj!999pm^}@q=~tth5sH9A}crh>P?w? zgXV8N{3Qu}q>WNiEo{pc7-RrScP8&Ai;ob&#$vp1`&kAZb$XEDo}1}&BQ6p=3#3-FZ|K>0n!*G0TtlY z(fT05*4xHJKqxcI&?+EjHKjw0KOk0&(MpLdX93KrYg|NXJ@UT_E2(<%sGj~vX@Tjs z;-p;>4Q+%bg_(doJDyCxTS$3iK^z5%a6h~9ji9@OJ*UZm`Ov0QOe%3`J0AQ{^_|R*9;(M!UUJ=HOryo89&~M1@vnLO;EBIjRs%R}+qzb%6W=H=qT4;e`oX zaA55Oh!6|@2Zd>zRHVj0`8cttOpqj54PIxt)#{%A>_zHno9od01q>2t)cv`8w$v)c=bxSei z#zJ>FbU8zBVguJ)z+tX;cZQkn_xw<%mx=B-kDVTGwlAI?-?yz?Qzel8m;FkZ-yEwg zwVmIGE1$x^ML2Nh#4rT94C%CWO{_!|;;kr-D7J0N23PA|9wkf!l2cmqZJ0~aGsC)R zeU_Xg`juL+o_RH1X0M4i45p({&}?+|JHNS1=cLrQf}nyJ+Y>KU`-%qFUs(-Tj+GE{ z9CAGb86ryY?-|w7p_JIxQeDhHG~)ho_x0;4_HMhcLu5t8_gywvMpF~hFE~!<&0(_n zD|2+E?&IE8lPHNPDAz3dfD2AT@xYub8uVd&L%gUQEgph`1M|qk#VSOEi;6`y7SLWo zTL}gIQQ;FO7%L^Etb1vFRvNwfyBOsxNxs!&!PQnRK1DJQ#=QC1IMf^FUX?{uE4xvb zc&X-kk$cXaPw$0#u7K`d*&(YYRC{wV`n&;oTGtho?AusU`((SjlM66$^X% z@n+2*Z$LxUnD^OEoo!15$y9jB@XjJJllpnJW*ZaA)d~V`qskxhPtQ4~{)P`gV{_7$ zIH$Wd%0g-IOpp?-Yg{k8n)n;VY4jpK2j()`BSq0AWzE|@ z;zZ7RR={JkWV+foY04CC$ux|cyS1dPe3#JRI@(xN)+Orfqm~pi%30fKl+YryP8KTE-eE|8lBCQcC^^LM0mgYX#Y-7rrEaL zbcQg?7={b>MAIDCawQG^)nZGdITPIS8QEZ%url^j6l_mdi4^c# zElfjgQS%NTHr0nJA#r~G*}@peuU%ZJLWOZpRvha*g&6lj&RO-{?e*~D>OnSM=a$i< zWbJY1GN=v|4o-LCRC*0%kJiQz;e19}&xhEqc>VSfAQmfLBZH2|emnR);>M;T|EyB)EBHUJy(7VlL z%mPc|fHD>AWI=?)MoR%%R*Ic=cf40nwyuDJuAHR8@B3>iK0aiK-LczqJ`VT03%w zQs&X@3o9f9WrVDU9VjK@L)M!kRygA!gbLP9%0yGEe36ClR+LiE!VsqQJsGbY+Iu%O z&SzGfHN626y|lsp>RpyrnPAhKT7o_>LkdrY(;r!SDWdJ-$F2|}3c*`T6J1G=14ONrxZZ=4aMQwZdS7sQ z>>@>7i)Xd~PHC2n$q(lUo8JDyW5hPSpl}D$LV#zPLBPAi6|&{aIGjHurUp_4_is|M*h% zmd!e+Ay!-?oQ&~J0}oe&{K~~Lchf%<|7qvFL*#3n zmhoB=8goGk+jRt?U$5qh)v*AgoA+}Ta5^iXYC~ch#}nXh5pswtrAHS4UOdRh2idH@ zt`n_M9V+CBYExBbXCRa7QY3+e>Bf8a3T`Q@@r_hvqrBFl2W@gO`ZHq(g4w2>qy|TbgshdSL(I-N$>+CX2;cEw zMs9Vq26D|HDrBFBByjgwcg8#x`#}!suYW z0TUoX)hl&e$4tLmw36j=I@ly8;nr>(!tR#9$|edAN1qP&sBTgZd#O}Or33RMf7g*_}(93uK<{PK_kQ4&1yOa>w&+|dqkVz8z#*ByT7KQ z4oFG;7^^rwG)9AcgDZN~NAq0PiQp9@s@RKjR#1_)RK1%yo%VnMLB;a8>QMvLv>GQ& zj$2ggJ|mBz)05_h`D1cOOM!~|nG~LE`*rg| zs`?=Kq4b({{t4Y{U}+E8O2D(fsP)K9j>SDZ?W;1@V7cjD|gIma_en0-GA@~Xk z1RBBSP3(OV4gWPE>P|~d^tO4~uq42m{w;=s*GK7@y5N#?^u!6L7vZ>e>;9eyW;vjm z@el|kuVBO@Y=__V@1v-mt>T0Vf#A$VSGulEoYoG~5S>!&=%G=r~9^@|r( zH#&}adq_Vt5mc8m0j=w;9!;a-Yp!J)AypCoQ_dYKD|N3%`BKZU%(Hy;U7x3gKLmra z)BRDUi&a&l7W%{XDE`-}V|4gRicoYp%;RvTaE#U{)w6a9IVA1l`fv>UW_7pB$i7{- z7R`)^@*zzEXFvcj-G$7{!%&;1M7{&2VX&^>rz6W=PXr8_)Lr8y`E9d|GEpl%5-ZxZ zn!jy7g>)t~e#=8peYvYvpK1&rE~l80p(Lt(Ac4O_L~mT!JG?Rw71X>A+|XGR)RZD} zF~_?$Bj6(+31h;&S&NC$nA}czSLz0ks3s;{=iV>pxK3&0(uRtczl(CX+^OPDB*6q{ zy?AM-mF?2?!XCxIUswl$wJrb7pu=tvhFhYe)Rq7Ku+J>9GS-2`VWP0WV{A9;W0)y*p}WIj~X_ z=L(o->=F;k$hJV=l4^$Mr5O;c@8?~tc7ufaS$$num15Sy%V1~KqOtPcGOlV?twM0u zuDl-6_!|O~@%(Y=W@0e=K%_UZQGw2-6XuEithhppnHv6tPb&)i5Z&Jec=%8|h##VH zG!MLMSeNIBU(`Pe^WL{<&bpnj=Ld32^vF3FMccjvu^5S$Oh>qpgoUTFa2eD_KSe#c zWIiQlt3q?^Kq$r5wC}o(dj~M`Sa-4JWsxj5L?Pd`Yvyzm+^vZ7a75=>lgs~Usv;Ha zfB!zL5SOg`azz%lpG)9XlPbFkr7`j6_z4`i)bwE{lqDCqHfe}_u`f0|QP>?v)EJu> z%jfm#Nm)_&06I@!S@@p5CmK6-h)7>=s@UGT7%0#=o?TF+hsg^=593hauMG?JO zQk{$@XrfhyVod0M$4HQb$LfsDBUTwc8Bwg4?knnd_C}yX0!rgM4T3dAug3H&QbSc+ zkHfDaOD$jAaJ0cEyZX5E}|L%UNjq;cplqZ-y(G0XXZCXwR^cDGT1yAZqy}{ z7I26iYI+~YSu~tP-FQ=>t2_=;P22p&g9vNO$p7>192he#cw6$U!MHYU?hfe>Xwn|3*#So^6=Ec3>!~9IP*H?viHrgAN6DZu7U-Oo=C5ttVo!!F}Cu zn&i4SSoVUj2FD?^Fs{JHbeeb#Htv)Nt0b9<7-BBfII0$uwh=f0{a6&0iSlUGxA`f{%QB7d5q=?U=X>1_y_d~S{`&>ZNzJz0M6Gg!EP zdGHefd~BY+43?d6Jou>qK6HHQzmHaa6E#?!vgMgR2CNCE{VJt`ZY67|gD$#C(laLu z9>l~^F`C)u(>F;`)cYW@xpS`b?SjG z7_w_mOBABpP7oT8;pRG)-?(%1$b0RwBLGpTbMoHOV{p+Ubne{f-7B|&Wm#K#i`Cw4 zp|gtWbqe#qR2)h2oOhl*RBu^8UO7XhA7=d=Zl>9nk%aN`n{@NWo#QOI0ZS~u$04Nd zZUZcH;@n~Ghyx*_r{r1iV;b@n>S1YZ7k>B#1;6c>x6p#|nM1**Fn1VRb4*z8sU7Vx zt86}8*ZO#8)YQl`aS&xtW3`!?D%9cx-@39B-ve;~D?jl7h5w*L$Z+$wpI~Zb!Cpyr zzya~0+=3(8*%MM~NG7Ua&7Qy~&AqQTE{ zYvp(((Mc~xGijVzC!$aZhn%Sy=zfu7G9&!pf?Mxw{ALitNK*I$p)w+d&f4n7WUZeS z?jQlO`3BW~Yg;!o8BP$e`1FAJl+OQTg$Trp2o&T2 zX&@lwUbq%|@V!^|dlwz&RZPV^oC^nO7{bB9q34Hmvt^BC$VKni8p=Au9eNSoFre=p zM8``GAl)e*{oS}F5M!zGtmP#{?=&lcJpur|VyQj+tU>OSy0M34xdTlELKw3gQ{6J` zfz!zm^d7?e^kkxO(_K06N?tv8@I>CFDAgh}LTG3HRl(Ae)^LH@m5JhXhTRv6_l|Q} z2fk)Lf7NUi@4WGWX)nKE(bkS+{8bL{PtX%gYdPi|i-8|BpZS7bK!9NaEaz#fs2@y{ z(mP%>5EO)k6LX>+-(K&sf~@$r`T^9g9tUc z6B?Z04&qf#vQjZl@yli4|Gr5!?xDcA*JapXEipSrs_;j#%Z4UlGldW;VD5}J72bi$ zW54C2hdQs0NA25ihq!jhOx&+zEiyqMG6VNtQ5TGeBuCQ6aO0!L8@qw+3!q82UZ6S4 zR+BYhLDD6NsRoZ_NlfyvLoY<@Hw~0S%97Qb)&0O!`GL?yOCP|~Iq1nsoSsi4EA{n( zAYJ~;m*F*BBW{B1<1=YIkTu<(v7vnN=bNQ)82b&|*1Q#|7wt6Up?dH7{NVIQSrf_U)Sm- zfqOp~_9@|TYgce(m(iU+`Jx;NF?IYF zaZuuen3@pc=+NT>VyBHjriKB|6%%-~R69MeUzC+bj*Beg4N1`*XT;NDdCM*2x#~@v zt}-eWTtnMA+Vir{v!vxw`z7)Vlle*B9U&$U1fBy24y#F8f~2z7VQ(-Xj$M)BovX`& zoufFvLod_dk=? z6zl3(I}l@JC-1+%7Rj>*ef_i}0%Rh=GaXg46tlwHi(e3+k?=f;pbElRc2$Luq=YI< ziy^^-%dj`oBu{G0wS2+jaCTgBWB6xtj+f}}67@ixe}QjzItqh=9!YjgeD56tTj#GH zs2j7gPz^<=V@9PmYueosjvGei0jRo)=a9xc2}guS%Ud6dDz-6tzGQVogaD1+}!Zn6x52{+c)choaj!gGKn-VH7zX6;O^CccFh>w&((a|RDV`#-No;#2^e81Xo%9Ln_ zZfM+i9R2mXL+F#!rgb|JS<60g|D?8!?6!@C-=@{yv`675D&&fO@gGh(wP<(B{3^6R z8kwnltwyOVf+*CvVbXZ)eb}rUmU5LlXq9GH?>9Qk8`wSsh zXiK4A-){w~m;D~?l&j-uGv@<`ou~Q)HSiYgxp03V5hK1t-gVrIrV2wj+9KZ|TdK&_mdfhYo}IAmN6V`tSVo2e83w*y8Fp>j~^x zhXE7YKx%wQN5FRpcJO&_TU<(Jo``m*U}H*WIO5J4ks%2TXL+``XMPySAS{P@_bi_rq+CFB{?`o7V2X~K|^MRh{@~}AlR>gbQZl){2@C=TZ<`Zi%$<@ z+e$s|sw+$zXkGQ@qP9)d2qMz*BnhK4PP$fyI z1O^#1c1r^|A3(cZ1+8a3y^v48FCx)d-=iO~#&Nxf}K$GfF!ujwe=#4D#S!3I9F#B7hnBNe_PPy5Ysvl9Zs2vRd;g$4qx5-UD1 z&1iBY@Ax@Sogwac!xVQ^u?MJO;8>PVM7CiUHs!}7Dy2ynDCXj1yi6YcW!(qNAMERl z0;AVE^3PWp9nA^V1Qx?rg4IXW*(ARWbDDYX@HJS(JLG+$xUbkO(caM{(HyJt(Cj{N zj@Ha8qw3J3N_|vwpNK|bE!wQUEI8vxYsE4k+7v-;T7ly*=Sls0+?kq>!TEbZ!Zl%*=NVA+m*HTcIAX&jUADdehVIUlbD)`nGXKP~$gn3duX*MZ<_cS6J z7I(m!pr2saM(Dh>7)>@p=6mKz9L|4D2ZJLP3R8PT=FHfVO~)9|vQm7)BGGN4&JOgNwwz?)_d zO&OBVH-AO-uQlYv?E;Gh1pueeAk`JQT>a50 z1}x*N(dGeJX~w)k7PiB5bn3$9?y*_#;PFD<)BL#jLS3s&7;wl?$V?>hp0nW z{=FpI{J3f{#zrT{BrBnwe4JH6+(2_1g~IC-%OGuLIck-jxYOen5?R^`b241~yq% z=1hMq{NO~jTKqRUmdb)TBd%f@;4JwWC6fa-N!9d(3QY_vD#mbJjfTjd%GC5k6$g|Q zNC2vkS@JNCs7Nv1JX9MZd3TJ~Rm|rIcN4)V(M@G`G3X#f?l$g)(eSb>Aw24Fv8$#$ zIz!uNzb07i@hqUtv4hb`Wn?@8taF;R{FM6Jj z`~c%zVasI3uj@}oUO2PRZYqgCg0w@V=t`ZXNuCnpzK{}1fU$CIr%NzUgJOu)uk<4- z&C1SSvUgd>=xRYQvitq5f)|`ael@BFb=Mi;`Ic94jz)}$Im-&%T+xWX3=3j-(HjsN zAgkpT8m5H@1qo4b9!pNAaD?p+Jq4_mKko`N{uWTph;p;)@~*DqXq2t9(d1Ujd}A1q zeQthv#970r*?t)iz%?2((sE^$wu(yr&Kly+tVqUAY2KX#UTER=v6gL4E74YF zWu)1I<1kkgrhf3_>Z!Uu0|6?37!jAwcx~JJP1yGgFiac8I&h=;5pTaJjAup6SYFfG zC+qlZw0tYg&rlJMPHYpwT4}aiT5l<2?y!x2kMA*?R`*#8Ht6jk6%Pu=QvxAx}z@l1Ao~{zFmioXy;L)~- z=g<85Xe;0V%f7~;TkAl_Sh{8Z6 zWmLg$wBg5W_3u_Yglwok@^nN*kqm#5s3hCS75U`zi(k#x)RMh;jInbt=vX&b>_9(T zw42oIwN8BPRTkas#!QZWx0;rw^2@N#3;J|%7R49LH9Vt5#}BtN9dM!k@W%NO{#-}* zV5nXEqfW$dH?j0hdU5fzo^Lz-6XzB>tD0$nuGB0zKDVK6PvU&xbPPNOG=J~MWv5q8#2;Wn#L;;g8 zupN?RZ@1{=Z?F81on@Y^+k)%OAa(PD!1kLZ2L4z~NQYbauebS?;llb25tB5nfTYqr zy9{CKm24-DLj=%e?dD!T+qfGIX+7Z(A7l+gC+re@gv%X1E3%>b z3mg`}qj>$#@NMC3iMXGg)W7E1aTJv1=jFHJDdeFNW3#RLw`N&9Q$G6aRO22{j$PjI zr78|Ix7B!9RR3h??$tM>cjP`d9y_q{pm|LUJ>8sv`>|;9k-PgTl&bO_I*AGQ@)z0n zV4{l$t}O;0Nl6!U-$c1CgK;#PoK2OZH~>NT!(n6S~fqSg0uO&1~8uw5fQ5Os)Uct1@>6P8%pOgU!d z#u-owP3TB~m_I1`@@xGaEZd9-beakzZ}>d%CH$0XD`~c&QjzoCh>3x~!bKb7*tQY}bY+B?PL%D70_?zf~Dg-o4=VJnjG7=VWaxMDVIXs#R?+so!>mD^4l{x7?1=A z7QwdXffTQ}mGBMdqyOc6@~m0=Qo-s>mqm!oc!pc0chF>C zG;q!c=OhdHW-1R<%(#h!PiV&Pf>Vq5&=te5vy5WDAzz}*pF^USE+ACQZNtekwo)Qz za!jf$-?bOrDbv%PG&fhf7%gDD7&M5`k46NUn4Q(7aS6*X3i?D|+l-(4mqd$jeP>BOz19mCqqZt-vL)+09}cA0Ho zVcaz%yutmMS-w4*TRDQU?yL}o=8XpGa|b{1jlwg18l7riyKZ-fAmgNAIE0(j|7>JS zmc#r6C!xa0OE5t=KmgYj2i~BzhIiMuPC9YlrWAHAq|AlNe^)Oz0b6&-8zC2Kp&!!E z=v-P)1G0qYA^0V{cWvFv1cvh_`JstX;-uR@UIEv;!PI>Pe&Gs)@=}=E6lr3lAwIUdA15I=3VKjk!OJ_ z9PYvv%(UYNlnWa^h&$zz?67t~8qne7jb>(cGA339Rc-0(YBKkDb@J(MA!cV9rKMSG zvgYC#=(&&7*mH_H*yq}9=a&sIC(JFId$}G$RGWydrFC{JlpVd7GGI5eu(pz{SExKf z+134``R)upWA@(9vu$sc6lw3C#Vy$_fXr?f(=pSbb7b1nn-7%PEgjeJ->h0r z!wMK(DkOZHY7ZdT3z>QULVx6trz|YxK(emEz=qB_sJx=Y5(T-#FxYp$j~H zpBT2;8)v+Lp*7qJCU#L~@5u`1;5a@jfn3}(eilG%(ye?&`+>-h`BAtxk(;VBo?*ChdI4ni#osav2xU-#gXL6O`a|1L zH@UAejw6)R>AuryuQ`?RWs)m5?%=Dzj=6l_kG3D)F?~>mb}Axa`f3_3pAA-``PHTS zBJesMx&AeX0$f#%-To?nhxyOX3`@AUIJ*3;_WXwmrcV7A5KsND==st_0--=nQ;x0x zoC|}%gqI-KLPX!tLXl14EE=3Xrdvcom*E?80H!XsnqoeuVd56i;?{A~_%X5*ok}|`U z`xSE#?c)3d4KB7?)o|vUHcPe7YT33K(qrf8d$EG;WB!@CO4OkIdhtD(gR&g=ZD?3$crUt8Ab*m?1jN!6Z z)nDoxPvjSwEooTfLNZ0PckiYQlRwMP!d+ju!qvIl0)zVwqEf;d63WKUtTNK^;;d;yZ{reZ1}bSQgK7#lS+*WbJ32l zD-So4Nhcm>D3VUh!Q4|=(jYp7;F!7a3HyOvxx*AZHV9`0Hgv4tGUQ6JT*_s{t8>?0 zUa^2Z1kKaMFOpFwfhgjAgdK=Z&md4}?6M$t2OV^CiYAuSp0$`f5?yW41sM~{Z1`gE zYT}3_${o|bp`(a8n>zsq_4~mL=6z~(B+*f7vCYxAuv`S=%qr(zQYH){_;9ZSs={t7 z#dZmaEC3~T89_bAusElij_@MlLDDlo1`cVAE*2O}8XZ70Y)o!VlNn@J)n8tEW z6hrz{Mx!aPX+RZZ5&frzt%UJpj!LrTwiIlOl2$ZHvKNK)4ySr=%DR@9fO-2f5gFdF zRFUj#zMN}#SVks6H%(Uo3@#iju zjyJpWDC?@2R=)}K)d4U9ep?5SQGKh!qO>q^n4{kc7zRJ2X^v^k-7dvFjG}AnBJ()n zSC??*v^=`t!U~`dcf#-&dA^Avc}TC`Ag#&NfS_9U$yM-_^dYG=9kRZf#mQhQ;;=k5 zdFHA_*^wBWuu7HXl}nm-_no%J{H;Lc8*Q%;>2U9>_l0y^1w#p zAcKOQSNL9XNNu?18>}8xB`9~M6qD#_S$67j1Q(2@qR^rid08%VmnjPND{3b4{2Tc0 zopd$k0j{<^dt@ZijmIxg2d~q>AkTtM`QzH63A0cuPt(tWl+gs{Ja44VMz5$R{`R-> z>s!)gCUtP#aM)`th%w)A*k7JE*QP?h5 zpeZ?n33!{$mFE5J3;Db<^q6Q_cpyUJ{|BXJc8h|JU2!ll4rW^!FiJjNwr6;1dV!7D zb*jjG@_O}wmOob36*(iqE0%vPG=wYjJ8Ba=kuGpdnFOCtd=JEs-JPLR-3xmrrO&X9 zc7FL=SRl1~P#xtgxQJ;I_6*@=Q2U>9B6H8jfA>t)hWb{D1PTJ8@YiMV{~O)Ry-dxW z{$874QgjqCb+h&`b~88ouM_1G|MTX*Ur*Awj5AL zy+~7XtXw8~)>{Iym`w1Pr%UUYoJe1vSJ2RS&+4Z=W~P;CI@F^$W96A0&RPU5Rz}$R ztr@Qj;FA?%6a;&Vu9sD`hW<%0X9Ir8+FUJW1y*+{d!cxx9K(#>CmY_H7Vtnm(PkM< z=StaYGY9n9e#Hu$0qH_`By1E>DViA;DxEF*p{(-)gvt*c>PAH{W(xroN9a1)Q-YeW ztEQ95{6@SMX>hY)L;{ZMhTgSZNLb|g0HxdTqu*l=@zn3%TB2qG3x(saRwxJk-(BlX z{>LuUK1p8=Oa&$4*F@s7z1;NVWDSzORR^sxG3GW*FbV`@NT%f~u>LagLdB8v3y1}9 zn@2vCk|^a88M&FsW8=Rc{CTqa1|5W%0OQisVJigB1xH!HRB3>|A;PX!LS}MASDjuJ zoxIe>r+SrI0IB0fSk|DfAWwKHtZ3&re$jc(9?1~7-WkJ@>vUbaQ`^3|PXSIEq?{IC z=%m4$43!f0t~Fyml(}bmu!2M9X+x|){XT$b21BkbkTI0N^#kWCB&qK~)X_(RTHWUh zWLc0zrTUS z-oMuMj1JSW2^a{-GQ|JBerfs-Z|YPzAesnzxMOOx40D=@iim_8I)$vX_}n-jQ$I=+ zpnS`NQh}8@F)Mqw=CJ)4Yl#+%QBODHlOmQac(C3eYpFWV@%7yzS4g0^5QoJ;q_Avg6*D!KUN~ftlvA#5=6mk9yw!W`hjP3j^)$O@7x$Qs; z@OACd0_&ek$pR<@$PzbpKD2D<)4z#Ihp|yhMG8Kj>ck|&*d?O{0{Ry*J#)@%{FSf+ z!e*FF@O73k>3)3mFBQ9F`4skPf`I$rf)%sdLR272TD=qw$l77q*ofd(KxP-Ct%W|4 zz<(!!c4^h?=2}Bi#mQP#hQ1>v`Yj4Qhj(g^BKqq>->8K7x)?ug)n#l7tH^|z7XcWu z09dCY4x_Jm98cpXwwDVy z-_=#!-rRK#zoz!nJTnzt_f$Q5D@gg#a7aa=7+guN7|JI5g^XU1@l4_+$+N0my+d|m zT3(p7;Ha@rwaXKIG5}ls3C7KMOX?-FZ{C4jr;PMt*0&uagbept)K;wJGoJH@>K`_O zugZ2`ps3Wpc1r~u=9iwohW{|we+b0D45Cm)Y66D*(#d{vdyGf)luHEmm=W=pF!W1O)RLN zclooE4)p?f^72v*e+0fmG)A5v?C+bfKBJHFGMQ24EqyCI4;%cLQ2-1)aQZB;ejy55Y} zlXIo~K$JpqN*p}{OFLWkXqJ_U`%JXMYsS<*DpvHa8f~S!-@rqQiAEU(^lh2T)s%7U+M^AZF7@=K;3mooj0WFiF9W#ZaXi>g+W{~ zJ_-p*ZWFCIpUP5@r>6N4UP5LFqByq)Sqd}&?2x1zuT6kLkd&96zR!Ls{)^J!T=t|I ztR^m#XJr>V3&B)eV>E?fv5nq+zGLaZlchno=DD!%Ii7?6SbhV?fv<`$t;wI5Q@h^o z;UPKx4^^^U_bT;kI9uzzf7^fA%$?IS{HvoJ{dJ-JTm1S*mQYBVk_Bf$kNDDRHErFw z*33$e$!}9FuIiwH2_dF-+hd$ATU^RE%QD-F>Pw{?2uI!(i>_2}2}`}ne?NJB`SS@{ z6f6MsdFQg}cNZ?Bis4!rS5Hov_E}vxq2)sNLH1+JUgDw5tI^x!W05hjj{k14D&?l7 zM0BAbN|C|NP5O%WGOiyUh`-p)#v&?1JS)U?CaK-ztK8lDzP6EQ2eZ>O$p1 zIGLuvj2;Mm&#aV?bYY_$n`4$ztc+|!sDVQ{o33#c!v6++iBoG@8b-=#oscoV;(WQm zKSN{EBk^WJy`9aEcBeW5aFXG5>#vKq?Ay z_#)`F9-aiAdO!9p5qP+%FjkX`8mnMPVd-J9NnwKZe0+Ci+GgaD7X=JN-z5mq(M1M` zb`t@=>W>eX1s`fw)f=b{gao$*K-Pv!g7}5LmS6FxQ;&I*6?1E|bD}~ni;BX^%8eLK zWj56WR4d!$?Hjk#qsv=~6LYJ7=JJ|cX8g7j&m~wXP%&N)`_)(LSlg~M@B6)>TLmXyFwTZU z$r?K}Gy_X>VyxAl>PDZM0wJ#3@5x&}gl({ywVTz+g*|f_r`Dzp)zLT@+)xwK|sF0!kGL2Q1@10akb0VXc8P6 zw+4c{y99T4cXxM}U`=p$3-0b3G`MSU4esuTEZP5A-`@YZ`S#7Z>8EG+AP9gU@g6Ipi_R5z>{;A82tGQzxG*h91oMlRa(@^7Q8Y}cAqwPW-Rel0Qj6km01 zru-|wn|Qqb0>`!cJD{c_33}6 z4i^6!eJTt|g1sLb8)K-{iW?6+Gz^I>Qt+tx9rbAJg?4@Hv=`-~DJmflJ1Q$+OW8j>H1rKAGhU9cGG|D+Pe6Yq?={?&l5nU^ymE|E_% z--B|qegqM=Uz27f|DTp?0b z=uRPNPAmHA&>c$1L#d8CtrRdLVe@jxLs+e>pBm~1(!?idjN|8)k#W&SRIh~ zj9SfWkJkB@Gh#n!ffsl`p9Ap!#lE+Mt+9!_n908#NR@QtX8F;0>be@+RCPKbgbRFK z+%&?z!Wh7WTIJdXg!+REeM77}nao_wWXq&8jCdxy`4~a=`oS;ZB58FW?8!KP?0RiH z!)v0&$;|il@fNHLTNQXeG&KGQwH(e_1Mr|CRyh*xtY$wJ4hpa`pkK17w9?gbw{vOA zJBbv+v#c>65UA+gPP>MATC}2lvZC$53l&yENz>BM)Ya5$;!D#9=(|(S8SGGD)hnHd ztj_KZ%Ml%1skb>S!sin$pY&CJGtBa|SqMkjyi{+6uk^SVJYP`0O3$WGpnFhmHA1KZ=DD>;0QH=n^jB@q*?%SVPhLcrKzEUB;ODmw$#%-`*?-mB* zq)-}S*8!^NB|`kuv2^NwEeC3ejBc}?I1wmMYHJ!Mc$Lu_K0_Ng*^JzYotu*atS|(n7Q{`%5pWcy}OTt+<2MGIqUz4SoLQdY<;i~A6%jS zrepL!n4h4Xt+9oz*}r>?Hge0VXufnX!0Gw|`n&>X`kTch@Bj@d_9TWzlAw8Fj#PN* zV*JTSADnW|MiZ{pe2W*_Cm3A(fO?6ik539m<1)1v=#K?qL{GmxPFwaeyw;v4e)zq@ z>%-GRzHSA#yp~x)txn#!Z$bLj;BW}imc*WeM+di-<6SkIYxC6?v0R2?+nXv!S zIm5hQ)q;bMrq`-Gs=y%XlX_jCxH-2oEA^&=BE0v&4HxV*KsM0I&ez5>Bl4$LNwn~1 zNMD-zSWl;*k#rM~)m}M;JkT~H4|5sT6sVrQDOuN0#2%Ta+F$E>7$ST~8OFMH418*H zQL2(i`s6fM{?@G5bQ@LvqFG_90r>TndJ?v#pHj*eS@D>JmaQ*OG94*~QfThfd$VdB zPnbA@uf0)Sq@VSf7n80kqo0&!bOYg(X>^wkX%OGo0S@1UAu<}PkgEq1Sq{9{SayQ1 z1f`#cz8Y$_Li^QQ6l2+VM~OxfK3m~4N3RX%BSJTQ%m_gsw-Be;A!!Xss0kDBl7fs{ z^;CkD?>7r`d$wS@r?8cT_6@kjk%vSi1w>gw-a-k>&(e%sXM9A{bJBSr-5Cy-=oR^P zv2B_Rb9fbqOON?Did7J{(}2NgyW<-#O{F6wskMB7(XWlXOM9@(;N{RK?dM#Lolx?+ zolYaGXPfMPo? zjf<{40_>S2+5VKH#Ni_>`MxnmkZRC+Jb`8~dR|sny$g9tJ(D}{Fm zjO?L#cApC2$nVe5vY!dbNgU7~V*7LiQR~Ms9`Cv5#0>nic*E!!KNq6K#u~#M`0w<@ zU2ar+2ZTgkpUKNsuie8H^cHXx-QVtRhzG9iOjEZGfh!fOk+g)K?bpvThs^TJ71}r3 zhgsq;3u(21O>|~g#Egj%r!R=D$)G1aPKG#A0xwx4205FBb>Zb?bH1o6z5ONij5s11 zFTT?Q`S+LE_-`AJ{(}wv4}0(b+frsZQ~{tbNH94`75vA&;&zs1J`H0Ei#fMEBjtQ< z8w42Cn6s-n`SG~5t7!+Y<$?GaB8yv`g$(b{J_U!y7<)CQA+EE9EAFe#)OA1K*H6_^ zZQ$4Ui*}uEsbQ$d3d9N4eN;QAp!CacN@J_&ZsIP1@L#y5HOf~bg|IC;x5xrTrQ<=+ zHfKWfdTB~M=T=XzPREZ1dSV{Ufd^~)r0a8w{epr7)#hQc98CEdZ##akO}H>4+G;k#`n;2ENk(43mU1(a zQtLV-DyL8lrLO)QbO~e1nZ22@;c4da_KLWo`55(3MSN{zwa4IU=^vaM>5v8%j! ztk<>mW&2qCBL$sb@ACkCSFx{IzV9pyGsS+T@0-)jHFCF{)^01 zB>ZC&$H3db$5qoQ2h6j1Eq0&?og|P@JHXT*yay$22#&`JakabB#!~>3I9tNUzYoaw z6@U2NSt|g>t3YZdYo_SO72m|e-O$z1-CgzvjPn5h?5tCIpIPauO{oyEX{T`b36!N; z3}~@)dGi^~%n*?}x=}ZS73hXt;?p2Q&8ri`z$Xz4egT%-!hw&)o4m0=%4p>re)EBw=?HWH(X-LXa=VpA zJZ3ZHO-cme1F&;zc5s;6Z&Y=CmEc9nIcv#b5cm1POka@%1ks&*=Qj|yvr}1D9ry20VsL=CQSm zod*&uNaw|#c)gdIc_-=t=43!;xtOI_oCQsq$=Ds~yzt;b1{Bcg)O|c36^xZQtRuKf zva8jc*)OwVKygPS1#vfM%Z$WL1a~_a;7HFSfAK*RH6M@XiPG%W2I*wZcwX(tW&GP7-?8* zUTCxQ=GiD=SS zZRJss!BlK%a9UCMJ#n$`6yg^_E=>BYF2<--oMmtb5XI53ws~V|lAW%@JunC(nFfQ{ z`n|N0i16j>UIpU~)PDjU^>~=D*RD%b8ssjWpjNCN|u*8N*6a4{z+rZmB=;NoA{S8iA zrg9N@{xcGOoTfH&gJ6RpPJ_?Ap0hYoWALF=VvmDGiUnBkSeK>~-S!jm(Sk2cg9P;RJ7!dM1t;iA0X z3*hcS19JuByu&qb1c1yE=|ua23rDw7DbyL9RAjcA9ctxBtIi`E=jYc^t2;|vb332j zinHdc22*c(OF5BpUF6IPw`6zs&>}rc<2rwS2^WVWMQRUEIP@o`mk8 zlrZv>lJmjONPT4Od$4DjEq7)aT5&hqQ=h%8r|}kh-`Ceyur9XuO7Gy{bwBf@gd&!3 zB0nr7_AaT=e&OL#9S|VZK^L|r1ARK7nUNWkG>8Ur zqY|*qV2>5tQ?El#aX>vTOqbR$7ffP|#_PEJFPxDrexBrH5Z z0ny6w8p*P)YqAeGRlru@4=5~JMBIUF+-JmRtto5GP#SRG{7q0E}#FT=hnl& z-MIOsuvDLEN5o6#7!W|Yq8#Q-n%5URytB%4i%VmYoHT=|$!JQ++j%p7Br znbJBs$#loT zwFG+`tOly08RtvTsmM=3(SnFMh8eCTgP_4?rZ1Cv11XIdufcK~1B?J#SN*?!wHUF( zVM?ln1;YWn!p6o~FA^u`cYziUjr>rY{_!0gQFp}g{mFhz*HyPI2Rhv$0LehDoRkU% zYq{S_{crq(5*S5>DP1JJ=VVi8-oiGUl~%OOpCv0Vn6-4#JY!^BN|mJv7ebg*OuwlZ zHWfT9PyYrsb%R|Ut>x+>P-CgQ{ zBB;T1paf}*mH@l-=^HngdM7O-pSw`oE76a?&nBQDIW@ZP#6;D~JyO^L3b(X-z43W_ zU52O)bv|cHvEfB2e5ZfRYaWW(2d)rRCBw^Zk5FqTiz)LfpO#fo0d*@+#>w1-T;U_) zm`*HRiF-#zD%~5#R0qdwlrr09Snf%gk(9}BMFA$zR4}m|!!A0_{yX0@=4U-o1x2z7 z8$(<+V}0O%6LchESfEiwxg{O3g20B}>f+VngIleU7!6NvoD@ImG)mT1$NX4ph3o*@ z_L3v0Lg3oD#q$*k$re|c3a!jZ&Js#F{`zHp2H){r`M94eF3AmRmJ6@JWSH$YIJ-dYNT@Py;=sOy_%Cc+Xk=ZVKV`ofD59e43?%)hf4!}#?_Ru0Pp zYGuSEC*OnBwII6$oKY&K`lqbkgC6>39=?Q7ZjtlmlP|)7ib+`^%N939dm%H#eA~lH zG*uhEaQ8M>`EFZE3B!S|RDmZmqBBO+_l3dX8*R5y)*kpgKZ1SxYT8Z+!_LyJ*nMMM zh~!k_>^3lL|EeRgR?}hqQ}=>{{m*T2(|-}LSSh<1ex!jnoFCKICdq1{a`y?EGKx!| z2)Pj|f(AeOo6o@|muK1>&DZ2DmQWGq`X;V}v^9`*BM zZhLxJSb+IzQ_ggLaqn}KhgtX{fOf?#8Slw=0#kJYFGAx^*6wnaVs>+`9#r(JINRey z2+z!L73cHZlec`dXTA>_Bu>Z4*LpL<6yzYo^vFojvRxmlK*q1#n`6K5CpqfBGcS@q zN3a`ZeP&&UBqtiBO#wvA?3>T-vIahIOXfb42C8atsdsT>P*qnC>d*2Xm%E7bH!5+^ zPy}!>6))s#tjNUE>`C}`ZO48;r^Aiz@%n-&w|?Hui)g5+b~`=AfL4{OzVQ*Tg~_un zhUH{zH&Yq}TcNIy^_vULVQ_j%|6Z^98mpgOEY2j+7ha|i|8aftoP3_tva`{oD(mSn zDe$PqCR^|R=PYf!mE_%4sWYk7I_;o7-&BIKmcqI~k?PkXVV`3;?J_S3XEjHi4ld$0 zQMaG2W2bxveCI*?#pYw;;Dj z)@!Rg{|Gi&QS2eT0WUZNT#`2lej_rI9Aud&_Pt10vinakKymbCw!VX*IfFqmr|o{lEFUlpQMLJA4WY+F(ZV__c=1oLj6*3oGSga|=s_sT_tsk}8tU#@BJJ z0$puJ)@X~r`I2qOlOTA1;6>}vP}$;_U99Da&KBFodV74?CjZLAZH9!2nYUKNF;@^9 zT%51Es%VCGHjo61618>WPNmJBsra}yR}g+DC$eeI#pcn3R!Ge}(J4*d_G_)L&dfLy zam$9@)z!_jc5{`9#A+1NZ7C`<(kh3LpigICL0eKzQ(ZA}8#F4diXq=Gr;F9-m-PI_*jq zEojadVe(59(v~dF$0RI_%n6VI?E#?RcapH?5cA{UfZ{I14c72OR^XS3hc`Ga9$-op}m#z>nV=aY6dscrLv|6*A>tnRh;N9Yjo9xc@W zewXb1FJTwQf7E&Zy)(AVkL0rqE*)X!~~Jc?)O+{D(=`BAD`dH#U5-hKCYt%IozB) z;b3khPFoSqnDNH=VR=X5Oc%3387V*k2b+g+#tAVVcFQSAplV-y5-i1C0mllU5rOhf zxDX^vacgMgl(*{2JMoqAYfjM?@kzfW!)U~$$L|Xc@dj9T!*-HUC@Qjau~2fC9%jbt zAlZUs$@u+6al7o|Rc_p-#dL7m1gy1}>{Eo73q`xM92Du&>vz7}f2P}ghE=Fnrfy;f z&EmaohsWUjR)Q+9eR~V%M^3{z)LSk@2zLi8e=U+VtCGv~ZdSQaW5fHd+U+yS=xx1H z>3Y=#iWE~(cegfzOoT0Du*>s|!mt_5$}@rVBB)y(@kqv&dGoCHS}kZv4krT4b&e|$ zu9wOiCic-R?W0(V@CY{KE-j8n*`#7j{?utz8`8=vXZq#g)?d*!a>~yA>`zWi6H(0~ z_(C1lTOCjo55-zr-1#_nrWKJVdPf&CTCET4E`( z-&vv)FGg^;CCsK^3X-_v#_MN(D`?agEQZI)?$Dgt;|^Jnn z@xc;R4q6)<8y%;i;W(?+2k5ISzD7Ct%vlb^GDsXduERxMx+Ast%W4i6u^xa|vV4pd zSw<&II1Qhrb%etL_0X;FRsQxJSv5bEMAq6jQ-6MI#i#|0%(meGE4YVN&o`{ylt#X| zUFoZggkAV-tasPs+-VUPhY&EhO?%-BvdFld+-tr~vP*RY*t zL7p-KoWaPoB#UC70I9GqGYR~MeQTVEjT|?EiIyZ><>q)I2lh~9os(pWzE(OvZMI_r zKoWw4*;KS}s4V2+QSj6e1x8XlJN7U_^|Gj*(Ylvk8|Z;NuStGaFly2EFU+=PY`g?l zj}*c2;7_ggVfucUyaeW%UD1OC(a20tc@%M9Qu>IQLZVKnQiZfl3~@wm3q;Ic?w~AE zbM}!u#aVTNosqzro0%Y!^+5?>b$N&OyJGr)HW7F2utWc=XAA{jBBIhyq zG5DnO`{laa7IZJVb7h_)E_Xkc(|>%Juj%Tdr@PhMtDKBgAe2jtN{>azZ`&keN?4I} z>pyW^M$@76U81Qy{Yi;fq3s}3gyk@!{Bh0~?W94!L#E+}vEfjVBfjc_suu0t9TE@> z*jak7>{WBQ+^nZO&#;GvDQgx~lzRk|DNY|dzrQGH7q5kPTC{YG zo=P<0hK zD*u-|lj5@@8e~VUV3hD#y@DUgxlo13gZQnw8|OSpp}dPllB z@kET>F?3G$?x5SRa|9pcdMCv2B2ta`F}P|BGEWR|`*_@Hc+}EQ%zXJejr*T~=m1OQ zw|Ng=Ct!b<5&gk}yn)NVL6E7WCHEf1UJJDwb($yww880Vif!J*n0WzwC@CSKe~`S8 zpnKXW!bz8_X&ojD}PN#8p=07@wSFkRsHHP!6 ztCwN1<+)7$NA4ueM6M;&+g4myy>s!!aMyG(lP21g8Hxkp+egMb;u>?P3dayAlLKV8 zF&jFsGMTNcP=;KBpb8pVdKIeMBARy~uoZC5N5xgRes19m#jR|Ymx_jpMZK4hqQZ`s z-GIhDU$o(>Ljn_or%*}8mzD~wu$f)NCxWz-46!mvBGSiA3(T_A{oSu0T2qV z*iy(e*a^x;jx9^78`*C3e=>%1$>jzKY*}li^n;-?yNkcB+%+s;Wz(S9<>#HmsVKRgePvZ&C1_DfVPgy;g*@`1FuG9ncnX+dl);QTd{NO< z{~3PGplTkSt~?8P5FK5e(ZOrqmdT!P0a;VxkrR$p*oZ9D9b8eMAP4@Ml-LiWJ%Afk zFa#a*^U>Lp$tKNV@+#gYr}3kKBUoy-YkGz0_LzL z&s3)*YIKM|xp5cqiAZEUnNXYs^JP$&`_=-FUVo35#jG~#04;!lMryY|;3O`wAi7Z@z3{y6EFhY6_r zF-jK4xNNNZo$(Dz7pEK%=Q=wFHF5(x(=*JGu}h=W9B^6(AjKT%B)`z!g|d+I)U zN4D1APTfLwHimYBcGfOV{}AT336@Jd7#8NVRxQ&LXjn0FMLad93 zh>#I4lBp&r;~lnVOZi9&_+4mPtPOCuf~D>R@pVJ zm1%w#mHBAW)aFJfUtU+zBQz%jBUeh1r`T;&4tsrMc84uqbKYHK;r$tTB3|q?NXhOu zJRi5cMCY#IfXfJ zUG?Z0hFChq_d(Ydhng!eYI^cwA1G4@yyQ^B)Ih@~L#sWKt zUl!Z`H^z_uIB|JL3!DFvV*Q`?MYK%MCB(BUgvqIS{jXHHKL&kZxS-xb73B6#~MAt;|p^i#YwiS&o^+FhD zD>uP=E671i6hqPEBLK9{ljx3&SNVs8Er{|&_kx!B(@W~YDJ$pX z(j!P~-C@tF_gGz^XBeaA=BJDrpK+jQ>F z64-xl5z37JM@#^%=L|L;dSH zKFA!3$h4nB-RDJ^Nui=vu$$H~o2^|Pe=!>$eZ7A;;rc3L&3HaC^7|HweX>Jclsi## z&XW`EG%Ed@xIOWTO4Eo8?S9xs#gOOE)h~Oo$Yr)GB{%Nc@q{&60$w~T)~H^DC_)0_ z%j#Ns%WJND7~Q}wdy=`9+r(pMCDeVIn-*A9S|L0fzNp$LWfrrJMNH1$7Lzw%nb6q- z(^XErd4=DPdwMu}S?>J!Y&<2h^iv*`5Y(11!ieuD1ID_R0P|Df+kp#_YW19y@^Z!F zp*$tVtrPO+R7J$RQ8X(=+6p~C^*VK+>Qbl;Ag=&dYtY{v5A|Wg*=Q0dHJBDLdIQuf z#81crpi0`2&R&v~GsTaYu0_3WvESz8hB2*7g5H=G4mD>8%YMtRtQL9qN68ayQlhh- zs*E2ZoEi=NWaZYk|Dzj)qOyj?V@l|#d)%+ZZFV=`=2WF1h$g}XeEQw?*+DXh?`Qny zyW+YGgDI{DO!a;x9MbaN^NO|=jM8|DHdsLlw9mG2&zsz%MBpMEK&6kcFa*5yyhh}Jwp!09W8#)K^(v$ zd{#BMAAi|)<;>>rh3_4uP~d;Vh3|j#pdw-XR|bo>c}MGeK!dZ1&VoWiNB2JVprY87 zCnPlY7Em&bv_TEe-JH9%wZN=wOZMsl66*~t{OO^1f8a}Ss6@hEM`3Eod^j6>c4bO` zxw>0G{%WEOyzlN#pPOhbdddN~6E&1xH_#uma-sNd)Kp_0<3&^J%DH1v@4&* zrLse6a3C$zs4gw|oP!ZOwr;fL#ClXXV{J33($S8^?Iv@t7Wt!c>PPNcaO~$?v@&%z zNg-hYRpV!$!HfVhMK219x`83GAXkvN**90&$@g!6J_`FW$6FDm-LI4=fTE2@0HcWx zeyY@VePCv~-+(-i(lXXEj|onSKE?4B0Xk#vNXx@X9sG;mAu}AQbZSbU2pkz5TI_&{ zW!tYM!{W>t7&ZF_adK^=6yTbrOo0zr9RL*NUEVUi(=G5&pn%&x+vI*4+bb&vYVeW4C2NYd!hz=~WyRDB{F8e?L;Sy&1(GEE=glcVZGtUW$xO0>)8YVS}3> zOMV^*k|G|2NWgepQlMXa(~fHWgt3p)z5kb{u%YO;{kwO#F8tl)Y)U51V%Bzs2G;*) zIa{WZjU5_0l5Yl_wylQv*Ynr7TAAfGmSlLOr5Gtxal!opOvPQz{3OZ2rlU4s`>o|d zItJlAsBhvU@8@FtN3eU+XV}?aG9>wqyI^g#JXs#x%)X}2w=e5&A5wZT!2BMayoM;Rk)w~8NRAu6GvkcA zK(|rHC1k{$4v!v>KDXZ)gTEv8NYO2_%EWG<(0{0W!X*WWb;)sKlz5Y0$Qo{_my)uq zKY%Drhdka@L9&5gziDN$skZ19^?r4}OywzTOkF_JWleHvAhWCa$qX?e01 z7VgAxw()_Eq1lgYIxL-Y1tY`Ouh7%Z#D0m6>^wM7sJV~f_UqYnL(A#IiC77`0+t?1 zi9iyG;Bvcfx*}yNhO=GsJ**yAN*_Cvymx^cU;q{~HF0?2;s=gQ|4UN$0ZbcNE4LEc zUhDiLP^@H@aa$joS19;gYt^><-rgl|-q`_5&2d`=1c#3RC3;q*cK?vrniFB={5ED< zHfO!Ax!PJcZ<=E6a|s>%ig$(X9dd-WlqOTcOg-H(={yCAfy?yDpW##>EpMO2-t#qy zRBxm?mf16Y)4Acnj{vg_fl58^c86~M&b7(uz?+Vh@}8>0WVnddD{YyE@RyAx)kEo) zMOx8by-X6gp$?V5l=A(hF7AE@-cRH-zuZ+0koYIjLS2JrR@n|3h6L>K7*er}MKR?< z-=l3q{I&2;)47a6b;h}kOKy$Q0+RK*nVx}%`0PIw!n4@d^j&kKTh!E>-^!y4_Kfl@ zYoZhDd{(G#F1Ej5`&b`;!R;X%Lue^O5~2e+6ip_*Lj30oHk~sr4D6KLzDwqh%(2|PE($EZrF zronveF_q(Ch;sBhRzF7Qky{3OFsD#KhIQ-@jk?UnY2chm_7y?g!GY_iolTd@918!K@^tcUzeHasOJ$@gGn$?MS*%uP#8TSAsLDtDfU>Ju zYCZMF(sLvHi|ha-Dv8aC?Sc(|l3|M~T1t&tYm8q(U+g1!u z_Af&m8D0TIm3Q!oL4Eij{1KB9t+d^puD`WE!F3TG0-kqAmZssxE;;{c0&F;71!JQD_s0adUc)9kM8rx@9m=2y z)pEpZ1*X6JcT+g-Ddq`=ft|+K!@Oa9^@K1|9BS=qMtZiE=CrR|0OQy0`c<9@`;%K= zG)-N0rsic>?zaAy$@5IxWmk)1S8pMA-sp0l-pfx0_UmlY69=H1!7KC##$y}D?d{e4 zqK4WBjZUzC*#Aa@H{=x5YZ`L61&I0g*lz}T6qXS5cclSB(U%+YR6xyH%U}9~^ETiF zdCA$yd+a5tm^hMqG&0OmX663u<1B7Y5k=8sB3bT1{}>J4+pIf&l4iUWwXNYZL(1dE zq{J*k)DlsC%^)L41ESP?gxOH>P87w~LycjMI1tX+!ZDw|r;UB&h7PsRT4!}4UJp>J zZ7SLTMa;1{nV_Q%7UU35oO`Tb%(c{z?iCV$YdjJ@GFD_5u{Uk2RVaDY^mzQPS~d@) zA};toJKimx^UVVkp|QGB$@h|6?XA~8);$3MI{a2wx`81iLm$nSA)v7)vCWnQG;Avj zFwvmzwb6`Sc8(U98gYj2;uAlQ28Wect%kOVPH>{G%?{lYU3FcKDh~*hdw%3g)9ojf zE?|s;0vy`;+|F_q~v}A53zqG%eFbw|?{~Gf9pLatR5^fySD4slrj;$~l2PTy~hIsB++pRLRJ3 z3pT|>&Z8$tO&hdWGQ{XtCYU8uLw!)Ge3#E#3l>TCtG$omW0mNC3(av2=O7YKd9gsG_KD>-ZU_w|F2kE@a@r|k6nA_r`uvEi~HAb~&4X7ER$fOZD6lN_!ZtMo=8)IawR z7$eAh-)o4AgT}$|NOs8O3@JIky|;|LxPM+hm_Lv6*VF!t!FQOX2bVu$bl?@7W(VgZ zgcwZiPI4{MF`O5mf6ZWNJ2zhNM( zIPmj8_V%~Yc4Z!6G)gl9k|>R9*ch;L4O2{{YeLt0~>wYG>08_!5U1Rnr-;zMCnbMh-B zU>TrPLF4kJ0rdG&LKagZrmfPB6@om%+N@$i4O%ZCbz?+Xf;&|7fzuwMj7qtjv@Y2A zHbKz^Y_3+Qsdt^jJf+Kc>&gkXhi;VskV<^u&RrS8vM`JnY`(CoKtcq0GguXpv>0F6 z_1Q*}*yC|OY0^IY5ydAmH^h{m^A4Rj8TdABtZrB&Gn$(WB)S@AU!i~L8kO*6sN;OO zMS0v~dqKMDBe@1f!ekC*vc%YYpA=*(x!FL@NvLl;H(st!vf7X+^}m74Vm;`#VbZf! z>b2>9)$a6;%_%9QJ1~92wc` zRdydxkKu}M9tnNpTSuvlRE&m+Mn@8Ml;@2lzQhT&$kWZjP1D<^a((epoS%|c)krC$ zVxg={=EN$B5Bh~Z7bSG>%7`7krQUf*08U{4JvTCL-p3gNA$v|zL=|1b`Kh$TlKTD} zyq#Jp<+tX>{*G_&X!;V`ujA{QXgt?X_t4<2?AjUxDaj+6Gnq9(M%GtVxZQ>c9TUqR za4|MAGyM$jwzC(F&h(G|;zWV3v8qY%>7PoTzctwWlg$N81Wio-OX_7KH~U8^2nW?s zCL$68brBl2#PuZ%9JL1-C8{q79JQB}1f=G5V&7ELwCwC~ry|G-v5NH8%G-N^e8dr&|X+rrek#?1Jz>?;Z*A@y3ip4f=z zLg{Ju#B*eqJ=`~4n(cbXlRB?5dR$iAfoIp4%9A(hh&0bOKfln}_&i|({So?@+Q(9` zf^&66v@knFaUfNpM%pHY>?LZN`_9WkoGj5}7M?g>fkFwr(J}s1A)dlT9wjAdbG>OS z@Ut}R=ri>IwGGrRviETpI+Bh55N?4=Cp80}a$FgEE;hVhPgQ`_{vqn8#w2Lchx&vkf%;Wiu(RK zRUh%UV#8ta;1WI|=t27uavO9z!qaCa!Z61vRF3Z(Eh@Ig%M0=A87+Nzu6j#~m5FRb z>!htOWL%Mc+*hAL(ttp`fzQsOq!5*Tw>32?MJOQMs<{r(yP6Ec-`rA}Xd zth9N}lW^{6j`jAJS!CNA{>p$&WNnBJ9UU5)q(g?#1_0@qX+RkuaB$ZyH#SB&+baRS6sVad*{bu zqfmc7Zf9(_#!25|Zaiw-S??3cHVd)o_!V)l-ctu9R31uO=`EZ#k1-x%_Ty@;D-U%g zIs>=9xg@P0M8_2ofLM7OFDI+Isp_+EeSkiPYIlR{r@51U(R~tE_YG0a7H$^VeNr9t z7`f-kSk9fA{V#%I)1mn=F8cX1)4GP7dKw?Ufb#fI;+pL`kwFoTSnBBp#&Qr;Yi5Hg zOIDbGU&BixFc^{;ut55pZ&M08j6;(p8PAr`0&2$@g&rePA>!@#UnScDx5r;oojIPR zIiG2SI;poCxsXH@ohSpkITs{Ic5*IBR3$V&9)!yjNm33gS{p=Ir`AFkn}1Zjsf`ZZ z#gyjx2-YrwDhD_K+osr?DoPf(Jw%4_J1C;`-?UD z_WLo%{;%i;l}sEh46J2cYz$2t|9aa0R8@cklPk4az7?a(3qQ=*dmQ%aPnO@?9J`_``JutV7d(d;qq>G55JyX|i z`exgXULaq$90<1q^9-zY;ru0(e_$geM*@Q>m@PP;0&Y#JY{s>6ff&L6Hpig5sZ8jcAna_p9<1$9Z@}AGPMc) zIcTT&&5#9~_bD$zuB|^JDB5Z|E6Yo6+#}2D>iOYmoBb;Ux8a7E7=1D?RoYy=OJG~8 ziD|m348@3~6fl5GFjWD2Wylgfr^BheFpuWUsTY|!OlN|vC3DUyUuX8#u_2ge8ZRtK zh^(HCFFjpHQ<1m-k2BNcn2!|ejc2b5%ZQVT?Yw4^?sj9|5=d=9MzG4Ftf#i6j8%Ee z66SF>sMQuLtL~*6Lm>#uwGrW-Pz~O8elGWbRjtu;BSUYD8IZvqMKp}$sk>xAaZ3`o z5?0lOl`nYk%QKXaQ^lkGrc~W@(S0WGg9N+P77y66@wd;8y{HJi@yf02uS|3X1PCx{ zdBE=sW7X2#3<;8Ms}dGx{N{8PVhm|r3_pvfzQ;}T3bD==(6rI6mha8*9QxEOx9Qst z4C$e{zQ&b|TcwB-fFw9;&{t;Tmmj130?ie|6waAqfi1mam>2a~p)N0hBzi&vDcT`P%c z7ZuQ8^vEfI%ne|;{tHV(#^IzLe~+2|Fn?nbu2Q$5H@Bb!J-^!3nF(AdVz9GqXdx8igWmr zeCF%v?bkUDBt@2UR1+`x+{&v?$aqxX^j|r=<_#)3Zh!5KKJ2aYy+T%lz#uPne!Hy437qKEeA`~+fr>q zPwJ-$Y*kCESmqD=coMlFNpo68FNYoJFrWOvj6U>M=Hea&6tc**g7WWL1BzKWTc?qh zL6fRpG9r14Rt*pucHue)!ZFuTG{CrGm^8IKh$h&s9qknS@MgZ2>uk4T_YQh^|z3Sif=Q>2Oa@lJ6RCD}L$w zCHd2z%xW_@!q8f7%x})0F6?3p-7HI}9rB^iWmz+Dhmn(IWDEdI1J5@lKhyzuCFWRD zHzm0@myRu8C@sFVeRC63??Rznpepekm5**qX|NTWN_*(b=ZbMjM^l+%xa`%2l0!uI zhG!StILEuG{%$Vm7HD7~-oX_r1S!YuFt`ooZBAv&WDgYV%%)iwyK{04{LuF5a+VBYr_QB5WIol1b26WySr=S?(Q`1Zo%E% zA-D#2cL^@R2_JLrwcdZN?Ds#K`>gt)PoAn#}r(MV(d2hjw5SKeuhh79cU!7 zkzDpGk<3yDCtpXgXMPxbtD5E&L_j~56*>B>Mt^yZOY;JKgH~|k+&P(6$LZL$cj5Mi zZE!^Tox@?cff_mRU^!~Bx`k&XbqUJW{j%NQG&P0bELI9mNzpyQQq50A< zTT+u+aE1_HoK!dnRf1sX^hoyfT-wE@6SjLeB1oL);e$_NOCbuJoLkzn^EoHG>-pHp zEVCbmH1lOfhRNX5nofoQKlW= zUP+D5Ky6?-zS}``Nh(d9ICt%OXT~spTlYSY(J!}f$MI~S46i`-h~}5uDY@q3n)+mr zhSq>_{ji%N?ziyoXk2l+#Ff*vNgH92Jz?R-^;*<~lQsA39(lie7l#ln}+e1A+U+s2~;9iWeP?y2+S$YG)Yt4 zYmZ@9hOZZ-BK5;0Voe=W7W4RwEH{1*XCI7Qzk4onkhKlNYqas+(yirtt(=#q&@?4`& z_3T}{Z@1>JZy2-IlLmIE=Kkmmm?u5$SWY#cV1<2_cJg8pY+IdWo{fk|BS8BLsix1_ zUbU@W+4-G)-A3Q8c-g*^?B1)dzMXcAXxZ8mWXFr2>Iph_3F_e;Te4e4YCc-g>BemN z7C_PGlZN}|jr5d3z2EFGh0=A%)%B2_@-xiC-8#+SA(0shzAs7X=Ljf(6vC3BUyWu6 zegQ!2aW{fZW(Ac3+bU}!yKhKeKE;sP&)qU^C&LdB>zyY!FprK3Fc5DDBH`lsw5L`* zU&k#nW54S1L!Q0NRPKg#nUJ16HkoXY0(HVpr@9l)4eg>Rw$~F;A!}S}AV}F5V2FS5 zu(xmNU7XsPD#Q`ktpn9O+^kDuB1HwATYQO9hiHrCC&6lKHRe8zxz}U~gIk~&YYOnt zUaqk6R&n>J)?Ck{R^n8{>bL$dDEUg^U{arqF5)95=D}gvt(F zxQkkBm%-7DNk1$b-pZB(@_rNjs50~u{YvZ_L_O4UXBmneE1gL=XpTu#y6*uXIN5%+ zfAW0TVi0D+-iEuTzs`w$p{yQXNVy4H?!e6U4qN*<*^(69^<7wVR#%_KJ19-Y`T=Yd z#f05=$jh6@HFBWOsY~}wRLM+D@LY2?o>UbpPcvLOP_gmTtTGPr$W^!trFOI!a%^bD zZ}6lF0z!O^&|os#Xkjr!Ba$m4FBia|My}6?X*H*5VtD7oU(}>12Jp$fS9l&5&oUFn zRMnC3p_P8rt3fBX0Us5EPzcPIryO%%TC0;7x z6M@xW3pB3kW{##8!hZY%$}+0LGf~{ zk`RoavX;s%G5YBl2<$}2wXDC6tz!Vp|Z7$e0rASgX8-v0dP>mPkhIo z$Yk#On{Ey^t{>vBU2R)uTIGcunbdeiN82gNcZi{xi1L_<#Z*-G^%o$6>H1M|CDaK2 zx*?H>HNAt}f^ohc%3V!eL{qSHcf=_k_yUBD{J_w{ZgU*W?5-fTpP?yUOl{kMr~tXT zH>hAeN;n0hQ7%ZKm;0SOIG0ySyZHx#Q2j)1nS$PL35X-6CC^xD$_l5}2l89&Te&Fa zU)X^YJ=^QBpqjMrV5rdRjvk2u4ksCs+ex*s&r>9oNTL#|y(qQo1RtlkJpwBi_4?Sh zp`HGOTiOn6ncuu0RqLSsfwSO$zAgGQ{VH4jO}2zU09vA`U4_BUEz;GXV@tFy*c z>OtzTZiyQei%3o#HqLjx5of?q#PZVuSf~l@=fMl z^UBOV-Bq-+tN^r#PpM+Q#7#BNlH`-@?jGEHWPoNE57Xyt0H4-Ym_b4Vate@q6zK~q zz;KsPf@C4{F}`AONNhK3H5ji<9iu=bZ8o7y8Su)@{a$ktSN~J}8yZVxaG}_m3eF04 zxa5O2)ldvz>2shgWLU99L6||;Z#~YIhUK=jD$`DukWAa~TyITCK^A z7Gk=0v1+@;DzuA%7>W)B;~JH1b>yfH1oYjJnUE{j$y~0%COuSBw&Z{}5)44n26c!> zzPWUQJ?IcFtu&k24wjee<4%qLNA!q(mwvx*ChH&{i^y1)oi=TotqWWRdu4KwAwVSA zB2BiF?e zAc^bP*u>yFU~*LchQV>=$H(}aYfmvk)%hCaEAU20#D42O=_!jf9NA^oGLJW0|Cq3}zZf)x0AUOtb)JKn zsMt){NjOZ+V!=X!JoC&{*#;-j48sMzX>gBo*9FBy^T;Z8i>ibWRoIi;<7v6n9le2k z^z7#^L$PcX5xH-C!FY)>q_-c6b~J)g_~L3EbmW;8BA3^JVWPF|&+gt5(8JS#RjENW z;0ic%e?&3LKs2?F2B$3G4{>_mcKj(n?}8S^E-xc@uA z3|jFm4&Jvpdx(EPkNuBY^4~1Z|NZ8_5Q0{+3IcKH4on0ot$Z>-no5O$fw7cQU^OV{ zLBFVd_H65X+{CnrUf3sr3@0q?ukxaO6H95HoCR50ny16Tn&;%2?Iyo3R81f&xQE9| zRd!2PBpSZLkHV4xCjIlOjJi;Tfi;ur6ixD^ZogyVi#lsJ(0dAIl$pV2NK-xfp%&#}SCSU= z3a_G~HX?X7BCc693bJuZrYrDKJ6wXk-%Y`WNInq1={~uyHeOhsv?|n(dLBD6wf7n6) z&&u_`)%3cVJ@pgIvX5PDqyA7-+WZWgx2~LN@J#C zuR*%FL+lwn5usSgX)N_VQa=D*L z*|g+GK{LCxh%XN0cEOESl-qFlaF!DmHzlVH!8a5?U5G8+V?5(h>w8sO`}*pf;5_He z=~|>@de0FsP~Zlt#_7k^s21qC01yBhNniRJ?`4ajI5a1tjOQx&g0a5gkDWGBPjfEZ zxwHYXL$a$b;9sopC7(f|xPlVr>&GtXg605VEUQg-TgGimd86xSjxaj359#Tm;~Y#+ zQX>XvSE!W543QkvTQ7s80li=h&`auyTm&#Z_Pz>H@E{SO)Jd`kQD3P_@7eK07^Ui` zjuU`Wtg##H6nwxVU_PSVMi?;aVMwmV#J7?U_0-)e$Sw6UXi=}1k#vABMguXE1~E&| z&NYMa512wDW&!~}(7aJoCBQMiAhw26jNL!N;07btLU1u5wuX3RFZ_5-UJp&W)xx2B z#X1|Dsb5Umi6KFP0+Gmm;`{AVEvr9Mm{4WGilNh+=}$b0$b2lU2i-I)yEoJ?so3hk z{%mAeXKsCeZgZvzkX-~_LVTV-V5%~%^%DWrC;mJe{++DD8as4|`0HMe5-F*lmVa)e zVxI6};kxTd zKs#dbQ!U<+>-s1~i`2nqY zZr%P(OL>W{s`t|)8!x^Hu^|JFfEuTvlm|Goa2`qP;1L*$ed2}+PF6qcFR;!BMDmXe zkDlS!5>$*|rDx58H0zi8$W1*s7W*EHDZ{_~gf790EGm(d(0V}{nCTy|_P$b+<`wfX zZ7(=<@YVl~o$TU@d^zB#y^2kXum8(@W=vXJx{Y#^)`OsL7$Fm8b~x(+Mg!hAjFnh! zMw&Qblr@|?4Wi=yQbupoh3ct%jZ0z$M?4(>OF*CO(Xu=Ql$SC=BVMi}A#LM#{MByoMlxCQe;Z-*u0|ZoXjRXP1tL>fD-{Tq4x|JS!iEQI z?Yh)kH??8z^P~e=H#3u-ON)Yp zVYw|EwbrX%HNlL)g!G_D&LAy#!lI@mPA|VlH8{ANWll*AQsrbml zg~KdE4DPy2u0#zlQI!`h4CQIUc7Sk5&vn3qWWsS<1DFFOk-5TxIuyV46QR%+oZ}TZ zDg#T%ti#)HR|=#Lh<)3>o*^AmqCRX^(I0<(L>nlEYTBs15gXNyF$_z0#}UUWr=LEh z|L=J-@)yxld!I8d$p06w?qzI^fmX5x|5tTNNk$G$0i8Ey=A%-$ys(1kbac42HFjPq z%UA6Ne@M!}CckOA;gj3x=y5fZBCF7f7c{H&8kIc8=^%owZu?Tk9qXRhhgt=;3!jbz z$B9n&i`jeR5U`>+)2;UQq~2(YmL^k89u^&P+jgYfiIT1wNaUb&B`uy|Tv`JwC~Izc zQ?^!&(9EB#H*ELUCV@5gQBTT7ZL&+&+c3g{o$U!q3BdtRdB~9;5UGpmk1e8?t%Cix zn}njckv3Uz*AQ4HE!bs33|H@1wmo=QG}E`A1aCyPT4f0vfDXXpHO!=h=pbEC$eWmE z!3ViVL&UmQC6GbOOQx;kj~!X1a!}VXiz&9DW04UEnL>iU9E14;ao@L2KQ{WVL>Ro^tk0CskT9ARVAGj zI$&_Ldt1(q<%FYZq$T{MKfMYA3+pYK*QI1IcS{u9I~FKxUyh6d5>^TtE4DQx&3c?I zKRQE{b{=GrR%)fMT99WMJSSp%0)o5tdLU?a?SB{Jg(Rn2=Jh!qx1|V zAS}HQ;`$z1WqfnN*akz(RJ`|5&w!zLZ8h*EzV6x7651;v|lSq!Mv_Gn|~{XN;NpOzZ`qN(FpZYpqwb4EVO(f zjJA}5YLo#RfL6#?wftW|Blcf21UVb5&{qqW=q=$eG%QODvjakL8`-bU(9dXWya-e2NyXJ^Z;mZq+*50VJ) zVgVY_a(sb?B;gnDI^6?K^Gua>LF=x0w8k(1b6Rj?I_S(!Z_=Xe4F1P#YhhMwcS1v5 zPeWBRO@MV$qUPa%=|# z=orCOszN{2oD$PGbb+?$D^Q-Q4$<@ElQjG$dqt#LXKhrjBT4{#GP6}B^kfa$Y339t zD7$f1OkxgwBRR9oPIVd)mms|n5iJx!Zx~g45!%rHtH7o;Y6I| zMQj)h?kNmma+k6tzo?m19f1Z(u@uM3Cwh_1mVi#gaT9f3$LcebJsq7?Q+UzzhO;4~ z4;E@M%rrMa+34p%xd`rqv7t1CDq{;!VAAZo{tz?rqsB^-Hj2|Cn7LsrpZkeQw`;E}NW2Bpr$lgd(IPte=V zJrnM@l!*2E`Jik(1qp*Uit^W$7-wqkH}F~|P4!ISww7;bruo371hxdFU@xd;eE0#=xz*+%{Ar~h8^;^IYGw@| zN17Bm;q_{J*INP<6_!q1@T$^*`JLJ=>-+xA=OiV$y(OI>3+!jIjrhSX8T(&n*Ad3Q z)>>4*m+k0ukf&{$F0tt$ZMI_chSZhFjpqGsk?A2P$}FJk#~UhO`XO=-u~)B`>&RO{ zW|dP%yvsj?b#~kIJ3jrGXM^$~{Tt4$WXjD!)e0v3RPzH}sO=^17joTnot`usnJn^H z7ADMl1UkHuz?sXI5negALj4)KDV`!eLDSq+>*sG54#kKl$T5m9=uCWmqEFfQw6VTq zQ>T;)>EFIcUcltV8=*-syoh&4C#<37)kaTGPef05*X)xup)B(SA0e43EqS9=-g-_t zJ7G<0^(QEWt_z1Y?Z}9HHmBK9M-fs2bct$y(j=ahz$rXW5-vRdI+Pvig2{LtV%#H# zK@y+qXJv+Qz-pGm`}NGM1**{dwiije#f6*So8C|D0XaX7)UUI^i`}~~s=3A~5wjMA zFhV?7WRF6zGdg(UmLKXG{*YsYqc>7XFlzE5y@^^NK0@E1Db;`>RU2uDCFmK(7o?&t zbGwLY3gJW27Osk4SwGxEQ_0s`{BY{~-`Svm^H;&byB?MM|6@7yFMBr?g&&r)`hd4i zGV7Hh$FR0zX)5O8rV2}JG>fc21pqpF&(E*)*JR#{CNc@J1?{2Sk8*A+MS(g$d&VG< z@p&JxN+XXGC=fSO4xa-zZ+JH8yS$)kqPY=YwzkZl8}}g-{*T3n5h`NPd@b?&*$=^T z0OzV@J2Hqlx;SC!=cxldCbcSum9*QE9ZaIZo157jcO;!wV@g2 zdzgLN?3cSi&m$l2ivm#F8gL!%(as|YZZ5H#dF!NJ^>H{2SARjC6N_!l_@~H!joY)X ztI84XS3K>Ha-xfGz}ownZT|~xMy(*=(RZ6fJXYfAmn~p%vL@#^WH@^#slmYP$&-iu z=OA`6{y`sB#)ZD}o)C?ygT`ojsdKbqA=wY2% zp+>q`M=mu?YFZ~k=Op!%q%`E9lMhTEnL@_{^(u3E=8uUq(m1-v@gByX>P%wY%u9B~ z2*84UKpvde<(zGZYm`&SE%&gcLaYdoiL!qzqNBL}kZ+T8$6NZ1!)>VGJR90ks>z2a znuG(zuC|YtDxyjuWvCmeUjIN$GZto+SDBVWlkv^%L8yhtnCL&kMbu_&t- z7KHMc%_t2GHQN!h_3%f>Llg+J3V8p_X&?SU^Yd>FCh>oZu&mh0zxo@4)c8$2Jyha1u?hh6Lk*f87<=xX{fU^J2bXyfNzP;^+II4^EC~ z*X)_zWp8d-%pwYuQC$sqi1eHZ@_&d`12AdCdBMrxJy=C?O~tk|l}`i9#5bs0aj+6p zu5Cs_8c2W9iS151jc$5=2Y>}?0T)yODyod)tHS<8m#P>?0d{SaNfNE4S%fDHqmtq$ z5|Ck{YrIWSJQ6}(2zb!NxNCV+W|CJmE3uI}9L7`T>xdH*7xc-a@NyLSD>LQ8NtCQV zuzB7X+jM_tSM6(-eL9SA%E>OD(YNAZ1nf0k#oHtnLArwJm<4`$!M~jqi&kue2`q^A zo&WvEY7eeP3igl;7xGoZ1v74SciqW`u8XsmCKPARjlypN`DFMOYl z8Yih93@=S#mjd~x#4OG%qzO`k_|kx-$pUY)9vFT<0?F}@Pz0IeXz`h2^dSk3*C+Qe z*GSGT?!dRjQy%LpA!LpPq5t5@4>fud!X*I9Lq9bBj7;G!tD6NH+!^W?I zsa?{%&s1sW1?@_c-xSTgKiPV=cGmL(g!Q!kYB-b6haE_oFq@+3aMCGjTBN}nwT_fa zQ~9a}Zx=+_jYN{SkM9M)67s6FPWq1#~El`c02yWf>JkBUrCW{g8&8F zT-5lFLO20Zjat}$53wVn?3m!P+eH{Ou7cfu6l$<54g?!Rj94)3t64aUDm?fS)<2^3 zG;UN{mkB}?sWZFO?|qI#rK@ghy*Oc=1`g$@-0W()2uj=675X1n1(7NyDin>7MtuQK5NIJN82V z1r{A*-F(Gd@$o%xd~W>Mz&2_BjOyCG7-=hVmWD>HChU201Rg_od8!pDJZe~2`7y- z6_Q*%2%8LRTyuj1$5?`7WC|O4Bd|HA)egWl3v)Df37u;SMAq(KGp31ZSjl(nx>$9b zIQJdO{tZ>*hXdhzU)A&=+We}z)xzWARlRhLE2uXkv<;6B=Pk;=r7+F+>29oKJ^Z%N z`&`se6ghnW6FFQq(CcF9SL>i)mX}@_1M#Q2^>0}jI@)uaMLmG;@-7|To0pjf0-u+C zxY=43VZDoc0*7JOTxPG#teU}v9zMjJLG;}T8C-s6l-{%!QyuS9?_m(~j=G|$>%U=^B8=qq6Qf0JAQ6%C(dWA$% zJK*@D{nw=JrU%fPWT4re<+;tl=*)~N!iFZ?l#v2`kU*`=*gor+VaZ4IOa~SOBSevL zgY*TLwf08x1*jgq=ma#+*nDI?%BnJe zPf>~7`Ve@>|%l z*f;6(m<$k+nz+0FiYAv?CIM2tOy?t?;|%} z&HyJyENe(1pnNmQET(>vGXq?gEl3_x3wu?Fc@>m9H|v_}QC+l!#?DPtgMr9*eYz&`4d$^9d1O z3}e^QKVL=29LEvzs?r>WkE|EJ)BvnxqLJD6U#^O*K%MO~rDthbly|bw3US=qu9d4z zww9Jsx+MXyge5l_I)yF9KVJbZJVMZ%`LroI%`_8ygGpo84X(${d)u=hjDDuiC#Xg+Xiu6*HD9mzIs&e-v0y8Yu zva1FaT-Ku6RSRSc3m(e>eq{$7di6#n_bAzNZ)Jh}RSSOBs%azV*z_q5t9dCAoL5z` zlDxs3`A;e{93}%&Jk-7}(6=TN?$ zHR}BRpumqY(?*_+oHsE=kNHEUyM;RYBf@}{PYXLm6gpGDgM9Ai!oYJY04dMLDdPlW zyt?_hcYUyh8O`1UguFWv##%tz3;E9~y@>1wJgLK)YvhXR(>J7~p+m%z5%*}V;m2sX zSjHi;x>0-XG`AAZkSn1xIc)y9w@CZO8{OTOo0~y|Ozy}=$CN9Y-Xx3Rky5$2g~T$J%4!75rhvmE_@+TasgsTce|K$6%{f z$HHUe71!a-;7Jxej;-A{rn}5dR#`4z{f_BJqg{`~$w+oiU-ORT$GR)bL)%E)%x-oY zuPqIh>tvnLYmP6A^*pkv-0Jwn9Uqg2BrN^*?N_ce@D)?_-9s4$t{+-t>v_hv;K%w@ z#_M>-x1q=SWY!GbTcWsWkX`L#_P4t(A>fc8RxbGv%=AcIht#jC${i-#zgE8*`GdIan zoB=PDa-IRNBxTz0$1;^h>Vxu+tU0OG&`H&))ruB^jCvv#l8kyfinF7c-CJUOPjC8{ z>XARa$IC8!TXxodp~+EC%w`Osgju|d=f*RCzC^$Z4*&BRGdt#YXySK$VE<2>uz!I4 z{@sL~wwV=2=bdeEwm>8e4xUlOT&v+QS8~m$TeTLE-P6j{OV!h5dy@@h^xLk&It96GHQu2fgR=S$1eiZ|lEoC@J!U0iO*PL( zTcL>Z*xy^RM+W~OnbbvIjhcdcjjkZ)k!3+-e!yM({lr$nN{zdTL4?T!4?)?5RPVe+ zU!9Cy7m{Z_1(^8kaEOC52v83+XxM?$ts9N4yN-d8f54GNnITFS8hHRhU>gtF;p`$F zR($Y#F@k`e_$j^%19FmrBt8h!pQ^BEOy0DI4zjXr`AVb%GAK{4#!c-N!AnRb13!Px zT_xeL-AAsrsS=3y=?7w5z}wq_;L3f_?VvgFxT{(uza`@VDk|}{>_SdY9 zPSjjJ5F@y8;mHPdYAZ9i}^S?dMRmdi#ViFYaFCec#=sj{rTi(7W!ZUkqu@ zY~VJGTc+rjz)GP%SCo4@Wo*a1DRze#A$j{JkW+=5er9}qVsFTOYPE?W)6?YbIfct%a7AmIzUY`LV* zkgS@+G+jdAbJMO~2^rjcUC|=mNHlonMINxB?*JhNK1Z(l>N&UJmQVghlc31^eot)h z7GctUQC*<|pvy9x&0*B$+y7Q&2{3t=?x$t9?^z_a3BDadumy*YWDhf#GbvJwF?OiX zK)au!FO?Nceu-6oD?Ivh71LgNFB(w{R9~zL^{Lv(fX<>T>LP2oU7PN#GjVG)9EL%B zkR8ar+ynCxnr|_8w+MWG0(xDQFoDP@4thUTHM+fk>MvBmU8VO9N46|u!Cg5HJR<#M z@Yg=XqHeP&JK@T<#v^VYa;N?xCny`G*w1sLGs|ZBK|b9@>lg(`aKjkBz58g9uc`WqWvJf$ z=x2(?Hv@DScTNqZJ3NK>U4Y-~u`eGb5+7kU+vwMmY8{|8az@!UJ*11lCA*B+QRw`=MeuO2LFZx3RoF9SpO#_@PE*2a>!q;Hg6X{sfhr(c-6^r^KJwm0QE4k>-ES74>-6Ker7-l zl)7Noe(?2cH6VF=K4@bf^$b^v?;c9XtbKS&i!j(XC6~NjCvz#0!LJ804-jt$f5qX! zK(oyf@9&(vw=Y^qtPR?p*Xf!pmC>SP^H#8$jX)K(r<2__2j!wcDrB0%ss&Fn;H=%p zkYd?W`Xc~L#hakWn#&|hk00~!2Y^3Q+v#jAnr4lMO;mj?7slEJZ?NXkC|J$Tsj9x% zK`olgN1tK$L}Dvq|C$u^!C>^alJ6%qFWpl7;Zpk3&LoeNDCBr6fmq<@_z@_mE8021 z#nFo}4zPGQBK zK8hK{(K*B8PDE^&9|A^xE{{EL^V z2(&i0F$Ov){o7?XD}GaU?koCW_cBzwRzv_{=oSH$q$`jTRT}maDiLG6oq-D)>b^x2 z@2Tw31WOiOI>4Rmm!V9@x~7 z3DR`SP}8XMae2`ay3b4#V;lCWpLEP|6;kq;S1&YtpOwjyRbLxsk474D4NbDe&VISo z|2X9YCd;V*O?F+m=2s`nmC7hSpAQ?_uX30a+m^Q@LIt0Xzi@k%#$wQ z5(8JRV43Xg5=NL^_}iG8b`9X0%ToIrzFRSXm*9D(ez(T@Xu=s%sJ$h7+2m3nof;SE z;4FL5mpqddH*0_;n;|v(4*|Ua)(ZOMaxZ z*!-aOafboAbxaM)rI9Bvxzwr@PH3~2ZwkF0+kI9^>=y=zlOT&ntUrJU1;z6jf7yw@ zuS)HI-0xAbGd6JgkN3NO8lCLW2UX7am6#eftxXA$c2RmprAiAtMdUB= zV?59TESlNPNxpJ_g($aXnYPG{=7!v+CyjL&rjqY!it7_0aNp9=NrcM z;g=UPBJI~dy!LX0e3Q@qa+iVp-}}M;#%d{z+01@@pU)ID_QDHH*lLd?K2$aNf|4*Q z6jD`@GSoT2_VlG$W93yN_fwlsub+JUtd|>5#l66?gFfx7M1f7yX~lRAIqDrgJUl&D z({F-V^%`y!7k4grjw$$1@Zw8huz2mmTaJYXk+)zjw6X5AZAS(eQBIJ*>E1l5f;FE z{$=N@a9KFz8lyr#ixBHB1D)|wHNhHu0+k74ir@tEtxZ?AzX%B2e)6`{(h%+*;9~oA zkuK_t-3ZoO>SB&qB0kgRG_<++|2(7#EM!pIJxt}q9V-6apRxO}mVR|gG8#NVj((Fhzru+* zQ8zbaOg;Siy?^rd(BL=(ut-OkJ+Injc&u{3dXaqiPvucwRch?*nSM z{+yxG9k4Xl?ge60f0ZHfFQB;nsJ1XvdP>QY~orsQhXz0s{75geRP16{eMkAXg<2KpPVg%=@dtp2lcov6DJr~1fgKUsQ${9wal0y(6EgqJ{K@Inr)*Sd?R z2T1^Y7by_6efjilk!Q(UV;+Ay=PhC@ zrFLrXz#f5odi!JV1AnjQ(1U&vFtSEKUo{{GmX!1%=zAt)cC?+EIkr@Lu+dsf+u7M0 z{G*fsfz&SCVgB-Qn2_|S=jS%b{IMy)-<09Ed-yZumzOE_hR37|=c;66#xtZH?iMW! zaJJbvbh5phqE3^4daP1r4Y1k03juM+{}=S5|8%VWLkRrKQwkcMwvmcfUjD0or*$n5 zMSuzmt%qgd0TKjx30%Tk={Y-ZtCtdleIX`bi8Exm`rCrQEGmqp2Unz z<<}XEpF5A~d)~m^H|o2?^>=b7^{$?~#5175&b5P5>!rYPu99@oS-MbJlOLSnb4{Ya z5vZKo5|H5P*QvQT$$I47!4H>LK&|`B7B6vZPy#;k10wFs>OHO8!q2$6SB|=t!=NrF zx15UaQI<0#*zDj^4rB3X8G3A#66c!-8bRg@253~ z8SYOmq?svQs9I}L!G=dGl?+bQSN{Gg*dB!*)Hoj~xeF@9r#1EUq3hEJfFvJSrEVAH?5(b{&ATbvZ`!d59su=-T5Ntm_5H)L`&sQWnrx`rx zt!mQfhy_VR7BPZo0+-GChEl5Q(tGhN(qQ~AKZD}kTi5nKuFNMhr}ih(nK9hz#qNl= zEkrQviBW}lBDy;^&ul|uuk{A9obM9N)5*?!E{y^2OV#LXTOrY@9G+6kw2I2yF>zX( zn76OzI4|D(yMu{3kB4$; zsb(-z^D4=XeVs|lvE(Sq7eBtjnH`g&1=(zoWlWaxZ7|J=HR4J|eqOTgbZ zY`R4p*=CurI4uIJ{t{h#<`{WMmfphs3KghlfI2GEJm-a0m^t4|A~XIgo27QDf2Y`` zm+4z0nu)~EAYQH!f_v0t?v@*Z)pHfD_NiCg|6b+cQ+z3#@2mVH^gkqx$~zlcnHxy~ z-Iahg#{cT#C;nZF7kSA5KWfjg}F|4E>ZGE3JE^ugi(9JPeeSeHYq7Rrkf zEhRhL#L>-=Ju*zt`>u+XW`2-JG5)gn;iyu+O}RGrS!%K6jH+q6B>A{&be6Z#Wnh zl@wHqaF7-rBmk{y0BdWTtd|r{FQKN27~9VYnrgH8xuFF5O6)$>S4m_Lbcwb1B&aCL z7S0%nY>2Lgy%UE2^_no8x&>1JfQAUc=BnLOlWl!fP#i#)_25nj?hxGFVFri5;1Xoe z;O-VY1PFoP4#5LtaDwaLFt`i^_uvwO6JYuN+W%pkZ%x(dHG)|biE7_Ui3 zPW0F#(u&n!Kq#^(j0m8!+Z1=8#SI5v^^1*WP{!ju*6Oy;JMWz?UK)|YYtxL3RpE0d z^}caZ>E4&_WKEme9dUMz3phwwkT#qI^H1o-*ZqRIQ3;ZQD{Xa3=QdHg zKH2zS!riv_8Ed>@^yE#FB51pM#Ji4`O^3pDTBeB`0-4kwUU)x0(g9d)%TOv~ZZIX`*{kY={5(%hP^knh^*i(P2qOxW zU|8p+8cPzswLeuZ zEU!xT`|9drki?y>P8S$B1Qihj@+A`6tm8t}=BYt50mp{l)ilCBB~8xMZuddRDkPEV zTnGk3bz{?Jdr^!1Pj(Xt9Y2b$SL`5UZNsm7Pc$g9P8Y}0dY+*CB)@!dlV0+VFQJE?>1Zv#I{llXLj?%t0L!NsU_+P)ip zagZQo#bs%12~n11dOA?EF{11O$yj5QTlMc_eh}BEZ=x;)6+%US|`+kWejriU}T{^cZIA1s>9vQlQ^A z<%~dg>ybG@_+_Mam9u0?8|vYsW839^T;)bjngEo)J>nIYq{<*=-t5_Xqf8>*xI|&M zuc(URdOcszu)`KXWuIJf|A~xgidTi$IXxf_#<@I1^`Ogtu!d5XzwfAJ3!=u zIDxqf#lphV8zO7DM$lv7fJSJmr{CS1s20r9EaI-z8f$nCbLXAT$^^fj0jEX~Jn8xs zZrs0Xm!u<}QQGSoUi`VqZm5_g2Gig-ztKI5C1>8o+Z$mURVit1+W`|^iA7ccp5q2$ zno@&7ZF|Y5A0HDcj9PW3d#|ve>&_K?bZ6(8?_g+Phuvn*vS|!5KN>1T%_f2VoQG6n zLuk&#ND5636+|G-oVaf3+y%P__2gY#Y*&J3#&-fytfLVgQ-N=8No$9TEF>L5En-yr zO*faGCA5qmDME7iXAlt~cV1n+opuA6CjxbU&P!ic8;)=soBb+(sKW3wSMjCcI%ya1 z;EZA1d93oq%4e#_Z3}BoCWU%yIw<+42p->@s!VW_o@i@zlhIMl{LO5Z*iMfex~Yu%y#=8$rg~|L+h!4EP@{uO3n;+7p=Ks3i zX?e_q^Vr6ejAxk(cI;}c>mQ}2%x>om7D9G3|4}&i-HPHKMadB=bQnJez^=g zc~qnCUTQ+b!XaNk{6N!H4UuN^nBK-H7j=Ndz^D zk1VS3r5=`yoOEagvF>6A8~E*Ku;KS@tg9rPpI7FG+mUx<7ZD6=yALS!o|E-;{K<7V?$W=Skpn_|mlhC#RRul*=X$$iL54_?f3 zOD}9~sIP6OhZp2(Xb>q(xXYO)BKnF)ZWjqr;<*)CjlZSKn+=ZM71ab0IY#m4>U@t$ zhYO$ndq*=CFU!WU{YXTL#mayi7Gcc!ckuXVRLQsdY@#pNT*!gTwMo_EGe z;i4z_d6PxO#h0#h>J z-%7%zIwsYZWiYe_l;)3uNcvpJgJngN-Hscf=^WfXG4Xr*25ZcL>|_C8HJnwHh^BF< z_h4t>lApaggqe41xrXbG)2)Lr$5^30$kC4e?=*_kQ zFCK1DP9Tays!E{1B+hT91j{a`a%7Z&h^!jDz)h3X)gWux8=h)(+CEco+lb@BZjmcLHNW+Zmew&H;QBS2P9=T7{<&{nQH&nlp=36-x#EcVHScs( zkW5LOK}H(=DWAcPD1J^FYh)Wn@m$N9UdUcuSv6TGuY*3>h+z`Qd#`Q@lf(dCd3@w) zXtBFjFts^w!$yZ#y8KrSwyLz!S&%d_9Q@#!&G%kEsr?HcsTapol0olFE$%y7u2uu< zN~s#4%Gg!viC6c$sG*kARzB$Wg}YyMmGjqjzjX`Cs>RCjKCG;ZF7!r@N299f00G(4 z8n~lNh4W*|&@t^2{Pq&x410ts2ae|%=p)XwOpy{mp5n>+8yGn+8}WVM6!{Ud8gbm!_|K#37*MvO8Qgsb4JTpfznd+(tBsNHdIbm!gQ76xL)uR#ta)8d>2vCM@u3O`g86 zwq;Yjh5v{<*(yq>JV#cSK9Nr(ABvP zxsb6b3vbGyMGY7Vhq$% z>ODD6$Og;(vPUxVN)=+bJI>=vbF$Lx6+_DdwD)SM)x7U#oC4WcZQ7>sN%ZqP{E*b> zoZ4CCiGi0O;9{!8^A2*#ICHhdfK#2qgX2I!c8aD@v=h~>GkGRhUdTCw#@{tP7d$k{ zo{aF*1dus;wpxZNJt#!Ew{`@4jUCa-^M!Fi(nDy9*`5oDvK$r~0){qi9DaREzxTn^ zd@58Z^cbyI;~Mf>m8ixFFZ5q(II!hjr7yc=C=Jc*$ZJauFyo)2{LgO=x4*aiw$NmQ)En+U$?1i1`b~ZxEcH`A{On~ zmheY7V+8K*L`iQFss*t-3e@8gaBMKt1&983Jct!y_i z5Ec~|Z{{?=ujUY(81zOCV*N0)RGI_A@)GaSh~y?zAMFDLw2~0g5aMKr4Y(dpoIfKx z|9&iTkQ8Y@4|Yoqg+K%a>CkxIvn(Ik5bX+sW0+PAEj#4cOR!hFO$BW~d zyc~(T)`TBHcSQkR_(~t}n;}W}(TZOODV(Oit*`B_bTEzjXcvYTvJ5}Q=Qn8`#$k6_dikYTF2LYJ9%o#)V<4lUT&a*`rEFl@_B_^rpuRG}Pq*0y-Glc7(CfR|fw4JA z^Z2K%<&ZO}B9rzXkXTg_-z~(Cx2d3quRK;Jl~Oa>VeGjcAtqW&QaAUJ8%T`J2f?Hi zUaz^!ng>M!LO)x?Ei(`J|JE6GwJX*;!xG{DNvGWmj-;K%y!w%At{}A=|GkxNJY=;s z@~@*^u5vL~wf|JaU8?V&99C=Lxvsv;?Rr|h2q`Rv&6N>kEM@F0$oA*+ zKOUr}zPad;rYUW+{gI)S(VNzN=6g6L1OjIgujW-W{3m*PQM zQvh859uY>Z>4DjMzLIoHkN0{WlgA>_3Gyy5b(1|03C;{lI|@ZVePXhWxBA$p;j+ug z1xG~n`WIP7?(D>)7Ic1l3mN+(;<`W$6Ul=aoTY62$fkh~lox7)Cibt!nOlu6D$&Du zRXRWXiY;3VfHS=X*#V|7NP0+Nz_t z*Al3)H|RA0_I7JRNd0v-qoGgv85)S-AU=DHvny81y~w{@T`#ne`NKU&rACSFtNyR) zb|O9=4xw-GqY`Y$nnYq^{GkVZ{&y1#$p`3E&Z!6*3w{VSC~#_9-sk-qyR6T&@CxdbdgE;$U9YXO#*5(!1+#L#Sr_kFBgXCo5|qC#X%h%K^@#-()UCdN;{qs>;Ty2Ibxqw6Ya}XZgO~% zc#OK>)xZe{>;XC*zEG8nz1hah?@+yPW4eaqZV}dNGLOqpfcFR#CXKzuxm&HElx%r; zJvYbnYnV>$E)AL53pI7n>2(G`IRabialNVlB)SiW-@odX zbY|cn*kIERw+hsS*jLLgP#K!9!6pbc&SI~92XA&!WBI&d@Wyf7x7ZCh-*2c)<7Z=X zwi8ieo}m1DD-$cprY7>z}{H33_A~+kM#dz<|m2^!(_lWlX)%lWyL9 zCMy9H+kUT*G({52Jx4gE`WN|y+g|nm(E&r$yZ{p@9PS+Tt28uuD^ zvQjVI--dFlZozo-)BU56VlRY{kEw(hM4KG3N4!;!3Q1@2EyAl6&M+1})01?*F^4|x zwFuKsfvmqPTTr7JQqb^gG4r4I5IT`zl5RS5axQ3Fv|hU+*&(Dnv_6bhm3ot3Iuh0; zo2Form|?j2qMTpt!=m{bMzppJxZ@H~-iwOPP zSf$)q#7NAkIKI(F@%`9ywLG-=YMIl~YMG>Ch+&@H( zq`}Mli+*SsE82qr^zu!!Uk-&{83(r`RM|(?kq6J&&&VNupXn8O`}SN+*kfE*W4ZV?0~xJ%c3v)Dud%Ca;b)5!f~|EXrrIk--#yR!(yBgabQZ9Oh+Ydua!`uqShCWxq8Hj7$(B0N7zo zwTx5lSl-ex5H+@jLUy9yj+dlixJz>zR&N7QQSri-ety*6#%9-GLCJOJs9h7ce^cQ2 z+`bk){((pu5NgFb?A^?tNOb&(i5mST{0$ZDufz0)c|ldT$oC>)U2{{1_Ci=yi06${ zEcBf&0Hkv)tW1KXT;#7;@4RT*h7ImtxRD`8kCMqSF3Hmkt@X1f!vZ}y9j-eIdrzCW z^`ks{NU5SUi3RNz7@2hCl2((q>G{p^WQ4QNm>X+PSn*&o45N8`kcd-MZ}-`W$OX9! z$#FN172OQ;TC=iFP}T(1ktUWEKV16L)7e++p5CzbvaKJ61Q#D)o}aOmFq+f*iA+%2 zBHX(18s%**NuDBp;?%l{D=e=C%TVUSapbOSUKatm$lpMPW{8B(8jUak{wVk79DquXgVmIwU_|I!QDX?p-P6s^$I{c9$J^ige|uUTlGj3l0Dw?q6adqUObMLo?2iu^#ABlU*_I_GAzHO1pF6UIMF%) literal 0 HcmV?d00001 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 @@ + + + + + + + + + + + + + + + +