diff --git a/_docs/icons/ic_medtrum_128.svg b/_docs/icons/ic_medtrum_128.svg new file mode 100644 index 0000000000..98c9b91ac5 --- /dev/null +++ b/_docs/icons/ic_medtrum_128.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle index 87da8f35c3..4514dc5e8b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -213,6 +213,7 @@ dependencies { implementation project(':pump:danar') implementation project(':pump:diaconn') implementation project(':pump:eopatch') + implementation project(':pump:medtrum') implementation project(':insight') implementation project(':pump:medtronic') implementation project(':pump:pump-common') 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 50a4030a9b..7467304b22 100644 --- a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt @@ -54,6 +54,7 @@ import info.nightscout.plugins.sync.xdrip.XdripPlugin import info.nightscout.pump.combo.ComboPlugin import info.nightscout.pump.combov2.ComboV2Plugin import info.nightscout.pump.diaconn.DiaconnG8Plugin +import info.nightscout.pump.medtrum.MedtrumPlugin import info.nightscout.pump.virtual.VirtualPumpPlugin import info.nightscout.rx.bus.RxBus import info.nightscout.rx.events.EventPreferenceChange @@ -122,6 +123,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang @Inject lateinit var wearPlugin: WearPlugin @Inject lateinit var maintenancePlugin: MaintenancePlugin @Inject lateinit var eopatchPumpPlugin: EopatchPumpPlugin + @Inject lateinit var medtrumPlugin: MedtrumPlugin @Inject lateinit var passwordCheck: PasswordCheck @Inject lateinit var nsSettingStatus: NSSettingsStatus @@ -212,6 +214,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang addPreferencesFromResourceIfEnabled(medtronicPumpPlugin, rootKey, config.PUMPDRIVERS) addPreferencesFromResourceIfEnabled(diaconnG8Plugin, rootKey, config.PUMPDRIVERS) addPreferencesFromResourceIfEnabled(eopatchPumpPlugin, rootKey, config.PUMPDRIVERS) + addPreferencesFromResourceIfEnabled(medtrumPlugin, rootKey, config.PUMPDRIVERS) addPreferencesFromResource(R.xml.pref_pump, rootKey, config.PUMPDRIVERS) addPreferencesFromResourceIfEnabled(virtualPumpPlugin, rootKey) addPreferencesFromResourceIfEnabled(insulinOrefFreePeakPlugin, 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 def98a723c..f96fd72374 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt @@ -31,6 +31,7 @@ import info.nightscout.pump.common.di.PumpCommonModule import info.nightscout.pump.dana.di.DanaHistoryModule import info.nightscout.pump.dana.di.DanaModule import info.nightscout.pump.danars.di.DanaRSModule +import info.nightscout.pump.medtrum.di.MedtrumModule import info.nightscout.pump.diaconn.di.DiaconnG8Module import info.nightscout.pump.virtual.di.VirtualPumpModule import info.nightscout.rx.di.RxModule @@ -87,6 +88,7 @@ import javax.inject.Singleton OmnipodErosModule::class, PumpCommonModule::class, RileyLinkModule::class, + MedtrumModule::class, VirtualPumpModule::class ] ) diff --git a/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt b/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt index b30a77bf89..4753c24877 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt @@ -46,6 +46,7 @@ import info.nightscout.plugins.sync.tidepool.TidepoolPlugin import info.nightscout.plugins.sync.xdrip.XdripPlugin import info.nightscout.pump.combo.ComboPlugin import info.nightscout.pump.combov2.ComboV2Plugin +import info.nightscout.pump.medtrum.MedtrumPlugin import info.nightscout.pump.diaconn.DiaconnG8Plugin import info.nightscout.pump.virtual.VirtualPumpPlugin import info.nightscout.sensitivity.SensitivityAAPSPlugin @@ -209,6 +210,12 @@ abstract class PluginsListModule { @IntKey(156) abstract fun bindEopatchPumpPlugin(plugin: EopatchPumpPlugin): PluginBase + @Binds + @PumpDriver + @IntoMap + @IntKey(160) + abstract fun bindMedtrumPlugin(plugin: MedtrumPlugin): PluginBase + @Binds @AllConfigs @IntoMap diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/notifications/Notification.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/notifications/Notification.kt index 066e412c5f..b15459c9b0 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/notifications/Notification.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/notifications/Notification.kt @@ -131,9 +131,12 @@ open class Notification { const val IDENTIFICATION_NOT_SET = 77 const val PERMISSION_BT = 78 const val EOELOW_PATCH_ALERTS = 79 - const val COMBO_PUMP_SUSPENDED = 80 + const val PUMP_SUSPENDED = 80 const val COMBO_UNKNOWN_TBR = 81 const val BLUETOOTH_NOT_ENABLED = 82 + const val PATCH_NOT_ACTIVE = 83 + const val PUMP_SETTINGS_FAILED = 84 + const val PUMP_TIMEZONE_UPDATE_FAILED = 85 const val USER_MESSAGE = 1000 diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/Medtrum.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/Medtrum.kt new file mode 100644 index 0000000000..a574e3a609 --- /dev/null +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/Medtrum.kt @@ -0,0 +1,13 @@ +package info.nightscout.interfaces.pump + +/** + * Functionality supported by Medtrum* pumps only + */ +interface Medtrum { + + fun loadEvents(): PumpEnactResult // events history to build treatments from + fun setUserOptions(): PumpEnactResult // set user settings + fun clearAlarms(): PumpEnactResult // clear alarms + fun deactivate(): PumpEnactResult // deactivate patch + fun updateTime(): PumpEnactResult // update time +} diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/ManufacturerType.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/ManufacturerType.kt index ddecad988c..8ffcfeb6c6 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/ManufacturerType.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/ManufacturerType.kt @@ -2,6 +2,7 @@ package info.nightscout.interfaces.pump.defs enum class ManufacturerType(val description: String) { AAPS("AAPS"), + Medtrum("Medtrum"), Medtronic("Medtronic"), Sooil("SOOIL"), Tandem("Tandem"), diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpCapability.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpCapability.kt index ed9620a818..b2557ff80b 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpCapability.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpCapability.kt @@ -26,6 +26,7 @@ enum class PumpCapability { 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)), + MedtrumCapabilities(arrayOf(Bolus, TempBasal, BasalProfileSet, BasalRate30min, TDD)), // Technically the pump supports ExtendedBolus, but not implemented (yet) BasalRate_Duration15minAllowed, BasalRate_Duration30minAllowed, BasalRate_Duration15and30minAllowed(arrayOf(BasalRate_Duration15minAllowed, BasalRate_Duration30minAllowed)), diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpType.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpType.kt index 30163c9ddf..3bd481629c 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpType.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpType.kt @@ -391,7 +391,29 @@ enum class PumpType { isPatchPump = true, maxReservoirReading = 50, source = Source.EOPatch2 - ); + ), + + //Medtrum Nano Pump + MEDTRUM_NANO( + description = "Medtrum Nano", + manufacturer = ManufacturerType.Medtrum, + model = "Nano", + bolusSize = 0.05, + specialBolusSize = null, + extendedBolusSettings = DoseSettings(0.05, 30, 8 * 60, 0.05, 30.0), + pumpTempBasalType = PumpTempBasalType.Absolute, + tbrSettings = DoseSettings(0.05, 30, 12 * 60, 0.0, 25.0), + specialBasalDurations = PumpCapability.BasalRate_Duration30minAllowed, + baseBasalMinValue = 0.05, + baseBasalMaxValue = 25.0, + baseBasalStep = 0.05, + baseBasalSpecialSteps = null, + pumpCapability = PumpCapability.MedtrumCapabilities, + isPatchPump = true, + maxReservoirReading = 400, + source = Source.Medtrum + ), + MEDTRUM_UNTESTED(description = "Medtrum untested", model = "untested", parent = MEDTRUM_NANO); val description: String var manufacturer: ManufacturerType? = null @@ -458,6 +480,7 @@ enum class PumpType { OmnipodEros, OmnipodDash, EOPatch2, + Medtrum, MDI, VirtualPump, Unknown diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/queue/Command.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/queue/Command.kt index 977613aa81..545630ecc3 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/queue/Command.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/queue/Command.kt @@ -23,11 +23,14 @@ abstract class Command( BASAL_PROFILE, READSTATUS, LOAD_HISTORY, // TDDs and so far only Dana specific - LOAD_EVENTS, // so far only Dana specific + LOAD_EVENTS, LOAD_TDD, SET_USER_SETTINGS, // so far only Dana specific, START_PUMP, STOP_PUMP, + CLEAR_ALARMS, // so far only Medtrum specific + DEACTIVATE, // so far only Medtrum specific + UPDATE_TIME, // so far only Medtrum specific INSIGHT_SET_TBR_OVER_ALARM, // insight only CUSTOM_COMMAND } diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/queue/CommandQueue.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/queue/CommandQueue.kt index eadf4663db..047263c3b2 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/queue/CommandQueue.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/queue/CommandQueue.kt @@ -31,6 +31,9 @@ interface CommandQueue { fun setUserOptions(callback: Callback?): Boolean fun loadTDDs(callback: Callback?): Boolean fun loadEvents(callback: Callback?): Boolean + fun clearAlarms(callback: Callback?): Boolean + fun deactivate(callback: Callback?): Boolean + fun updateTime(callback: Callback?): Boolean fun customCommand(customCommand: CustomCommand, callback: Callback?): Boolean fun isCustomCommandRunning(customCommandType: Class): Boolean fun isCustomCommandInQueue(customCommandType: Class): Boolean diff --git a/core/main/src/main/java/info/nightscout/core/pump/PumpTypeExtension.kt b/core/main/src/main/java/info/nightscout/core/pump/PumpTypeExtension.kt index 7024036f9e..321365cba4 100644 --- a/core/main/src/main/java/info/nightscout/core/pump/PumpTypeExtension.kt +++ b/core/main/src/main/java/info/nightscout/core/pump/PumpTypeExtension.kt @@ -59,6 +59,8 @@ fun PumpType.Companion.fromDbPumpType(pt: InterfaceIDs.PumpType): PumpType = InterfaceIDs.PumpType.USER -> PumpType.USER InterfaceIDs.PumpType.DIACONN_G8 -> PumpType.DIACONN_G8 InterfaceIDs.PumpType.EOPATCH2 -> PumpType.EOFLOW_EOPATCH2 + InterfaceIDs.PumpType.MEDTRUM -> PumpType.MEDTRUM_NANO + InterfaceIDs.PumpType.MEDTRUM_UNTESTED -> PumpType.MEDTRUM_UNTESTED InterfaceIDs.PumpType.CACHE -> PumpType.CACHE } @@ -117,5 +119,7 @@ fun PumpType.toDbPumpType(): InterfaceIDs.PumpType = PumpType.USER -> InterfaceIDs.PumpType.USER PumpType.DIACONN_G8 -> InterfaceIDs.PumpType.DIACONN_G8 PumpType.EOFLOW_EOPATCH2 -> InterfaceIDs.PumpType.EOPATCH2 + PumpType.MEDTRUM_NANO -> InterfaceIDs.PumpType.MEDTRUM + PumpType.MEDTRUM_UNTESTED -> InterfaceIDs.PumpType.MEDTRUM_UNTESTED PumpType.CACHE -> InterfaceIDs.PumpType.CACHE } diff --git a/core/ui/src/main/res/drawable/ic_medtrum_128.xml b/core/ui/src/main/res/drawable/ic_medtrum_128.xml new file mode 100644 index 0000000000..a8e35cd3e0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_medtrum_128.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 075ac674bf..96e341ef5d 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -393,6 +393,9 @@ CARBS %1$d g EXTENDED BOLUS %1$.2f U %2$d min LOAD EVENTS + CLEAR ALARMS + DEACTIVATE + UPDATE TIME LOAD HISTORY %1$d LOAD TDDs SET PROFILE diff --git a/database/entities/src/main/java/info/nightscout/database/entities/embedments/InterfaceIDs.kt b/database/entities/src/main/java/info/nightscout/database/entities/embedments/InterfaceIDs.kt index 5f78da4e8c..0a244aa3c6 100644 --- a/database/entities/src/main/java/info/nightscout/database/entities/embedments/InterfaceIDs.kt +++ b/database/entities/src/main/java/info/nightscout/database/entities/embedments/InterfaceIDs.kt @@ -43,6 +43,8 @@ data class InterfaceIDs( MDI, DIACONN_G8, EOPATCH2, + MEDTRUM, + MEDTRUM_UNTESTED, USER, CACHE; diff --git a/implementation/src/main/java/info/nightscout/implementation/di/CommandQueueModule.kt b/implementation/src/main/java/info/nightscout/implementation/di/CommandQueueModule.kt index e6864c9508..3c83defd4f 100644 --- a/implementation/src/main/java/info/nightscout/implementation/di/CommandQueueModule.kt +++ b/implementation/src/main/java/info/nightscout/implementation/di/CommandQueueModule.kt @@ -14,12 +14,15 @@ import info.nightscout.implementation.queue.commands.CommandTempBasalPercent import info.nightscout.implementation.queue.commands.CommandBolus import info.nightscout.implementation.queue.commands.CommandCancelExtendedBolus import info.nightscout.implementation.queue.commands.CommandCancelTempBasal +import info.nightscout.implementation.queue.commands.CommandClearAlarms import info.nightscout.implementation.queue.commands.CommandCustomCommand +import info.nightscout.implementation.queue.commands.CommandDeactivate import info.nightscout.implementation.queue.commands.CommandExtendedBolus import info.nightscout.implementation.queue.commands.CommandInsightSetTBROverNotification import info.nightscout.implementation.queue.commands.CommandLoadEvents import info.nightscout.implementation.queue.commands.CommandLoadHistory import info.nightscout.implementation.queue.commands.CommandLoadTDDs +import info.nightscout.implementation.queue.commands.CommandUpdateTime @Module @Suppress("unused") @@ -32,6 +35,9 @@ abstract class CommandQueueModule { @ContributesAndroidInjector abstract fun commandExtendedBolusInjector(): CommandExtendedBolus @ContributesAndroidInjector abstract fun commandInsightSetTBROverNotificationInjector(): CommandInsightSetTBROverNotification @ContributesAndroidInjector abstract fun commandLoadEventsInjector(): CommandLoadEvents + @ContributesAndroidInjector abstract fun commandClearAlarmsInjector(): CommandClearAlarms + @ContributesAndroidInjector abstract fun commandDeactivateInjector(): CommandDeactivate + @ContributesAndroidInjector abstract fun commandUpdateTimeInjector(): CommandUpdateTime @ContributesAndroidInjector abstract fun commandLoadHistoryInjector(): CommandLoadHistory @ContributesAndroidInjector abstract fun commandLoadTDDsInjector(): CommandLoadTDDs @ContributesAndroidInjector abstract fun commandReadStatusInjector(): CommandReadStatus diff --git a/implementation/src/main/java/info/nightscout/implementation/pump/DetailedBolusInfoStorageImpl.kt b/implementation/src/main/java/info/nightscout/implementation/pump/DetailedBolusInfoStorageImpl.kt index 24628ece59..d476796218 100644 --- a/implementation/src/main/java/info/nightscout/implementation/pump/DetailedBolusInfoStorageImpl.kt +++ b/implementation/src/main/java/info/nightscout/implementation/pump/DetailedBolusInfoStorageImpl.kt @@ -1,11 +1,15 @@ package info.nightscout.implementation.pump import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import info.nightscout.androidaps.annotations.OpenForTesting +import info.nightscout.implementation.R import info.nightscout.interfaces.pump.DetailedBolusInfo import info.nightscout.interfaces.pump.DetailedBolusInfoStorage import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.shared.sharedPreferences.SP import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper import info.nightscout.shared.utils.T import javax.inject.Inject import javax.inject.Singleton @@ -14,10 +18,12 @@ import kotlin.math.abs @OpenForTesting @Singleton class DetailedBolusInfoStorageImpl @Inject constructor( - val aapsLogger: AAPSLogger + val aapsLogger: AAPSLogger, + val sp: SP, + val rh: ResourceHelper ) : DetailedBolusInfoStorage { - val store = ArrayList() + val store = loadStore() fun DetailedBolusInfo.toJsonString(): String = Gson().toJson(this) @@ -25,6 +31,7 @@ class DetailedBolusInfoStorageImpl @Inject constructor( override fun add(detailedBolusInfo: DetailedBolusInfo) { aapsLogger.debug("Stored bolus info: ${detailedBolusInfo.toJsonString()}") store.add(detailedBolusInfo) + saveStore() } @Synchronized @@ -36,6 +43,7 @@ class DetailedBolusInfoStorageImpl @Inject constructor( if (bolusTime > d.timestamp - T.mins(1).msecs() && bolusTime < d.timestamp + T.mins(1).msecs() && abs(store[i].insulin - bolus) < 0.01) { aapsLogger.debug(LTag.PUMP, "Using & removing bolus info for time $bolusTime: ${store[i]}") store.removeAt(i) + saveStore() return d } } @@ -45,6 +53,7 @@ class DetailedBolusInfoStorageImpl @Inject constructor( if (bolusTime > d.timestamp - T.mins(1).msecs() && bolusTime < d.timestamp + T.mins(1).msecs() && bolus <= store[i].insulin + 0.01) { aapsLogger.debug(LTag.PUMP, "Using TIME-ONLY & removing bolus info for time $bolusTime: ${store[i]}") store.removeAt(i) + saveStore() return d } } @@ -61,4 +70,24 @@ class DetailedBolusInfoStorageImpl @Inject constructor( aapsLogger.debug(LTag.PUMP, "Bolus info not found for time $bolusTime") return null } + + private fun saveStore() { + var lastTwoEntries = store + // Only save last two entries, to avoid too much data in preferences + if (store.size > 2) { + lastTwoEntries = ArrayList(store.subList(store.size - 2, store.size)) + } + val jsonString = Gson().toJson(lastTwoEntries) + sp.putString(rh.gs(R.string.key_bolus_storage), jsonString) + } + + private fun loadStore(): ArrayList { + val jsonString = sp.getString(rh.gs(R.string.key_bolus_storage), "") + return if (jsonString != null && jsonString.isNotEmpty()) { + val type = object : TypeToken>() {}.type + Gson().fromJson(jsonString, type) + } else { + ArrayList() + } + } } \ No newline at end of file diff --git a/implementation/src/main/java/info/nightscout/implementation/queue/CommandQueueImplementation.kt b/implementation/src/main/java/info/nightscout/implementation/queue/CommandQueueImplementation.kt index 894d7aa308..63ebd67901 100644 --- a/implementation/src/main/java/info/nightscout/implementation/queue/CommandQueueImplementation.kt +++ b/implementation/src/main/java/info/nightscout/implementation/queue/CommandQueueImplementation.kt @@ -22,7 +22,9 @@ import info.nightscout.implementation.R import info.nightscout.implementation.queue.commands.CommandBolus import info.nightscout.implementation.queue.commands.CommandCancelExtendedBolus import info.nightscout.implementation.queue.commands.CommandCancelTempBasal +import info.nightscout.implementation.queue.commands.CommandClearAlarms import info.nightscout.implementation.queue.commands.CommandCustomCommand +import info.nightscout.implementation.queue.commands.CommandDeactivate import info.nightscout.implementation.queue.commands.CommandExtendedBolus import info.nightscout.implementation.queue.commands.CommandInsightSetTBROverNotification import info.nightscout.implementation.queue.commands.CommandLoadEvents @@ -36,6 +38,7 @@ import info.nightscout.implementation.queue.commands.CommandStartPump import info.nightscout.implementation.queue.commands.CommandStopPump import info.nightscout.implementation.queue.commands.CommandTempBasalAbsolute import info.nightscout.implementation.queue.commands.CommandTempBasalPercent +import info.nightscout.implementation.queue.commands.CommandUpdateTime import info.nightscout.interfaces.AndroidPermission import info.nightscout.interfaces.Config import info.nightscout.interfaces.constraints.Constraint @@ -535,6 +538,46 @@ class CommandQueueImplementation @Inject constructor( return true } + // returns true if command is queued + override fun clearAlarms(callback: Callback?): Boolean { + if (isRunning(CommandType.CLEAR_ALARMS)) { + callback?.result(executingNowError())?.run() + return false + } + // remove all unfinished + removeAll(CommandType.CLEAR_ALARMS) + // add new command to queue + add(CommandClearAlarms(injector, callback)) + notifyAboutNewCommand() + return true + } + + override fun deactivate(callback: Callback?): Boolean { + if (isRunning(CommandType.DEACTIVATE)) { + callback?.result(executingNowError())?.run() + return false + } + // remove all unfinished + removeAll(CommandType.DEACTIVATE) + // add new command to queue + add(CommandDeactivate(injector, callback)) + notifyAboutNewCommand() + return true + } + + override fun updateTime(callback: Callback?): Boolean { + if (isRunning(CommandType.UPDATE_TIME)) { + callback?.result(executingNowError())?.run() + return false + } + // remove all unfinished + removeAll(CommandType.UPDATE_TIME) + // add new command to queue + add(CommandUpdateTime(injector, callback)) + notifyAboutNewCommand() + return true + } + override fun customCommand(customCommand: CustomCommand, callback: Callback?): Boolean { if (isCustomCommandInQueue(customCommand.javaClass)) { callback?.result(executingNowError())?.run() diff --git a/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandClearAlarms.kt b/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandClearAlarms.kt new file mode 100644 index 0000000000..00e5bb1ff0 --- /dev/null +++ b/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandClearAlarms.kt @@ -0,0 +1,39 @@ +package info.nightscout.implementation.queue.commands + +import dagger.android.HasAndroidInjector +import info.nightscout.interfaces.plugin.ActivePlugin +import info.nightscout.interfaces.pump.Dana +import info.nightscout.interfaces.pump.Diaconn +import info.nightscout.interfaces.pump.Medtrum +import info.nightscout.interfaces.pump.PumpEnactResult +import info.nightscout.interfaces.queue.Callback +import info.nightscout.interfaces.queue.Command +import info.nightscout.rx.logging.LTag +import javax.inject.Inject + +class CommandClearAlarms( + injector: HasAndroidInjector, + callback: Callback? +) : Command(injector, CommandType.CLEAR_ALARMS, callback) { + + @Inject lateinit var activePlugin: ActivePlugin + + override fun execute() { + val pump = activePlugin.activePump + + if (pump is Medtrum) { + val medtrumPump = pump as Medtrum + val r = medtrumPump.clearAlarms() + aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}") + callback?.result(r)?.run() + } + } + + override fun status(): String = rh.gs(info.nightscout.core.ui.R.string.clear_alarms) + + override fun log(): String = "CLEAR ALARMS" + override fun cancel() { + aapsLogger.debug(LTag.PUMPQUEUE, "Result cancel") + callback?.result(PumpEnactResult(injector).success(false).comment(info.nightscout.core.ui.R.string.connectiontimedout))?.run() + } +} diff --git a/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandDeactivate.kt b/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandDeactivate.kt new file mode 100644 index 0000000000..eef55c1ecc --- /dev/null +++ b/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandDeactivate.kt @@ -0,0 +1,39 @@ +package info.nightscout.implementation.queue.commands + +import dagger.android.HasAndroidInjector +import info.nightscout.interfaces.plugin.ActivePlugin +import info.nightscout.interfaces.pump.Dana +import info.nightscout.interfaces.pump.Diaconn +import info.nightscout.interfaces.pump.Medtrum +import info.nightscout.interfaces.pump.PumpEnactResult +import info.nightscout.interfaces.queue.Callback +import info.nightscout.interfaces.queue.Command +import info.nightscout.rx.logging.LTag +import javax.inject.Inject + +class CommandDeactivate( + injector: HasAndroidInjector, + callback: Callback? +) : Command(injector, CommandType.DEACTIVATE, callback) { + + @Inject lateinit var activePlugin: ActivePlugin + + override fun execute() { + val pump = activePlugin.activePump + + if (pump is Medtrum) { + val medtrumPump = pump as Medtrum + val r = medtrumPump.deactivate() + aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}") + callback?.result(r)?.run() + } + } + + override fun status(): String = rh.gs(info.nightscout.core.ui.R.string.deactivate) + + override fun log(): String = "DEACTIVATE" + override fun cancel() { + aapsLogger.debug(LTag.PUMPQUEUE, "Result cancel") + callback?.result(PumpEnactResult(injector).success(false).comment(info.nightscout.core.ui.R.string.connectiontimedout))?.run() + } +} diff --git a/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandLoadEvents.kt b/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandLoadEvents.kt index df6fb48cef..16475adf4f 100644 --- a/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandLoadEvents.kt +++ b/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandLoadEvents.kt @@ -4,6 +4,7 @@ import dagger.android.HasAndroidInjector import info.nightscout.interfaces.plugin.ActivePlugin import info.nightscout.interfaces.pump.Dana import info.nightscout.interfaces.pump.Diaconn +import info.nightscout.interfaces.pump.Medtrum import info.nightscout.interfaces.pump.PumpEnactResult import info.nightscout.interfaces.queue.Callback import info.nightscout.interfaces.queue.Command @@ -32,6 +33,13 @@ class CommandLoadEvents( aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}") callback?.result(r)?.run() } + + if (pump is Medtrum) { + val medtrumPump = pump as Medtrum + val r = medtrumPump.loadEvents() + aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}") + callback?.result(r)?.run() + } } override fun status(): String = rh.gs(info.nightscout.core.ui.R.string.load_events) diff --git a/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandSetUserSettings.kt b/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandSetUserSettings.kt index 471c83c670..cb120cf32a 100644 --- a/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandSetUserSettings.kt +++ b/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandSetUserSettings.kt @@ -4,6 +4,7 @@ import dagger.android.HasAndroidInjector import info.nightscout.interfaces.plugin.ActivePlugin import info.nightscout.interfaces.pump.Dana import info.nightscout.interfaces.pump.Diaconn +import info.nightscout.interfaces.pump.Medtrum import info.nightscout.interfaces.pump.PumpEnactResult import info.nightscout.interfaces.queue.Callback import info.nightscout.interfaces.queue.Command @@ -30,6 +31,12 @@ class CommandSetUserSettings( aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}") callback?.result(r)?.run() } + + if (pump is Medtrum) { + val r = pump.setUserOptions() + aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}") + callback?.result(r)?.run() + } } override fun status(): String = rh.gs(info.nightscout.core.ui.R.string.set_user_settings) diff --git a/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandUpdateTime.kt b/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandUpdateTime.kt new file mode 100644 index 0000000000..a8b5c5654b --- /dev/null +++ b/implementation/src/main/java/info/nightscout/implementation/queue/commands/CommandUpdateTime.kt @@ -0,0 +1,39 @@ +package info.nightscout.implementation.queue.commands + +import dagger.android.HasAndroidInjector +import info.nightscout.interfaces.plugin.ActivePlugin +import info.nightscout.interfaces.pump.Dana +import info.nightscout.interfaces.pump.Diaconn +import info.nightscout.interfaces.pump.Medtrum +import info.nightscout.interfaces.pump.PumpEnactResult +import info.nightscout.interfaces.queue.Callback +import info.nightscout.interfaces.queue.Command +import info.nightscout.rx.logging.LTag +import javax.inject.Inject + +class CommandUpdateTime( + injector: HasAndroidInjector, + callback: Callback? +) : Command(injector, CommandType.UPDATE_TIME, callback) { + + @Inject lateinit var activePlugin: ActivePlugin + + override fun execute() { + val pump = activePlugin.activePump + + if (pump is Medtrum) { + val medtrumPump = pump as Medtrum + val r = medtrumPump.updateTime() + aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}") + callback?.result(r)?.run() + } + } + + override fun status(): String = rh.gs(info.nightscout.core.ui.R.string.update_time) + + override fun log(): String = "UPDATE TIME" + override fun cancel() { + aapsLogger.debug(LTag.PUMPQUEUE, "Result cancel") + callback?.result(PumpEnactResult(injector).success(false).comment(info.nightscout.core.ui.R.string.connectiontimedout))?.run() + } +} diff --git a/implementation/src/main/res/values/strings.xml b/implementation/src/main/res/values/strings.xml index c9d2c3b0ab..2cbfb9810f 100644 --- a/implementation/src/main/res/values/strings.xml +++ b/implementation/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ + key_bolus_storage BG Command is executed right now diff --git a/implementation/src/test/java/info/nightscout/implementation/pump/DetailedBolusInfoStorageTest.kt b/implementation/src/test/java/info/nightscout/implementation/pump/DetailedBolusInfoStorageTest.kt index 3831f62381..91d38733d8 100644 --- a/implementation/src/test/java/info/nightscout/implementation/pump/DetailedBolusInfoStorageTest.kt +++ b/implementation/src/test/java/info/nightscout/implementation/pump/DetailedBolusInfoStorageTest.kt @@ -2,13 +2,19 @@ package info.nightscout.implementation.pump import info.nightscout.androidaps.TestBase import info.nightscout.interfaces.pump.DetailedBolusInfo +import info.nightscout.shared.interfaces.ResourceHelper +import info.nightscout.shared.sharedPreferences.SP import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.mockito.Mock class DetailedBolusInfoStorageTest : TestBase() { + @Mock lateinit var sp: SP + @Mock lateinit var rh: ResourceHelper + private val info1 = DetailedBolusInfo() private val info2 = DetailedBolusInfo() private val info3 = DetailedBolusInfo() @@ -26,7 +32,7 @@ class DetailedBolusInfoStorageTest : TestBase() { @BeforeEach fun prepare() { - detailedBolusInfoStorage = DetailedBolusInfoStorageImpl(aapsLogger) + detailedBolusInfoStorage = DetailedBolusInfoStorageImpl(aapsLogger, sp, rh) } private fun setUp() { diff --git a/implementation/src/test/java/info/nightscout/implementation/queue/CommandQueueImplementationTest.kt b/implementation/src/test/java/info/nightscout/implementation/queue/CommandQueueImplementationTest.kt index aaaa6657ce..75183844ab 100644 --- a/implementation/src/test/java/info/nightscout/implementation/queue/CommandQueueImplementationTest.kt +++ b/implementation/src/test/java/info/nightscout/implementation/queue/CommandQueueImplementationTest.kt @@ -238,6 +238,19 @@ class CommandQueueImplementationTest : TestBaseWithProfile() { // add loadEvents commandQueue.loadEvents(null) Assertions.assertEquals(4, commandQueue.size()) + + // add clearAlarms + commandQueue.clearAlarms(null) + Assertions.assertEquals(5, commandQueue.size()) + + // add deactivate + commandQueue.deactivate(null) + Assertions.assertEquals(6, commandQueue.size()) + + // add updateTime + commandQueue.updateTime(null) + Assertions.assertEquals(7, commandQueue.size()) + commandQueue.clear() commandQueue.tempBasalAbsolute(0.0, 30, true, validProfile, PumpSync.TemporaryBasalType.NORMAL, null) commandQueue.pickup() @@ -354,6 +367,54 @@ class CommandQueueImplementationTest : TestBaseWithProfile() { Assertions.assertEquals(1, commandQueue.size()) } + @Test + fun isClearAlarmsCommandInQueue() { + // given + Assertions.assertEquals(0, commandQueue.size()) + + // when + commandQueue.clearAlarms(null) + + // then + Assertions.assertTrue(commandQueue.isLastScheduled(Command.CommandType.CLEAR_ALARMS)) + Assertions.assertEquals(1, commandQueue.size()) + // next should be ignored + commandQueue.clearAlarms(null) + Assertions.assertEquals(1, commandQueue.size()) + } + + @Test + fun isDeactivateCommandInQueue() { + // given + Assertions.assertEquals(0, commandQueue.size()) + + // when + commandQueue.deactivate(null) + + // then + Assertions.assertTrue(commandQueue.isLastScheduled(Command.CommandType.DEACTIVATE)) + Assertions.assertEquals(1, commandQueue.size()) + // next should be ignored + commandQueue.deactivate(null) + Assertions.assertEquals(1, commandQueue.size()) + } + + @Test + fun isUpdateTimeCommandInQueue() { + // given + Assertions.assertEquals(0, commandQueue.size()) + + // when + commandQueue.updateTime(null) + + // then + Assertions.assertTrue(commandQueue.isLastScheduled(Command.CommandType.UPDATE_TIME)) + Assertions.assertEquals(1, commandQueue.size()) + // next should be ignored + commandQueue.updateTime(null) + Assertions.assertEquals(1, commandQueue.size()) + } + @Test fun isLoadTDDsCommandInQueue() { // given diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/setupwizard/SWDefinition.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/setupwizard/SWDefinition.kt index 27465d04f4..ec5858f591 100644 --- a/plugins/configuration/src/main/java/info/nightscout/configuration/setupwizard/SWDefinition.kt +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/setupwizard/SWDefinition.kt @@ -35,6 +35,7 @@ import info.nightscout.interfaces.plugin.ActivePlugin import info.nightscout.interfaces.plugin.PluginBase import info.nightscout.interfaces.plugin.PluginType import info.nightscout.interfaces.profile.ProfileFunction +import info.nightscout.interfaces.pump.Medtrum import info.nightscout.interfaces.pump.OmnipodDash import info.nightscout.interfaces.pump.OmnipodEros import info.nightscout.interfaces.queue.CommandQueue @@ -319,23 +320,24 @@ class SWDefinition @Inject constructor( .text(R.string.readstatus) .action { commandQueue.readStatus(rh.gs(info.nightscout.core.ui.R.string.clicked_connect_to_pump), null) } .visibility { - // Hide for Omnipod, because as we don't require a Pod to be paired in the setup wizard, + // Hide for Omnipod and Medtrum, because as we don't require a Pod/Patch to be paired in the setup wizard, // Getting the status might not be possible - activePlugin.activePump !is OmnipodEros && activePlugin.activePump !is OmnipodDash + activePlugin.activePump !is OmnipodEros && activePlugin.activePump !is OmnipodDash && activePlugin.activePump !is Medtrum }) .add(SWEventListener(injector, EventPumpStatusChanged::class.java) - .visibility { activePlugin.activePump !is OmnipodEros && activePlugin.activePump !is OmnipodDash }) + .visibility { activePlugin.activePump !is OmnipodEros && activePlugin.activePump !is OmnipodDash && activePlugin.activePump !is Medtrum }) .validator { isPumpInitialized() } private fun isPumpInitialized(): Boolean { val activePump = activePlugin.activePump - // For Omnipod, activating a Pod can be done after setup through the Omnipod fragment - // For the Eros model, consider the pump initialized when a RL has been configured successfully - // For Dash model, consider the pump setup without any extra conditions + // For Omnipod and Medtrum, activating a Pod/Patch can be done after setup through the pump fragment + // For the Eros, consider the pump initialized when a RL has been configured successfully + // For all others, consider the pump setup without any extra conditions return activePump.isInitialized() || (activePump is OmnipodEros && activePump.isRileyLinkReady()) || activePump is OmnipodDash + || activePump is Medtrum } private val screenAps diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt index 7a095ea7da..cbe56f27f5 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt @@ -2226,7 +2226,7 @@ class ComboV2Plugin @Inject constructor ( // only shows up in the Combo fragment. if (newState == DriverState.Suspended) { uiInteraction.addNotification( - Notification.COMBO_PUMP_SUSPENDED, + Notification.PUMP_SUSPENDED, text = rh.gs(R.string.combov2_pump_is_suspended), level = Notification.NORMAL ) diff --git a/pump/medtrum/.gitignore b/pump/medtrum/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/pump/medtrum/.gitignore @@ -0,0 +1 @@ +/build diff --git a/pump/medtrum/build.gradle b/pump/medtrum/build.gradle new file mode 100644 index 0000000000..2f7325883b --- /dev/null +++ b/pump/medtrum/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'kotlin-kapt' + id 'kotlin-allopen' +} + +apply from: "${project.rootDir}/core/main/android_dependencies.gradle" +apply from: "${project.rootDir}/core/main/android_module_dependencies.gradle" +apply from: "${project.rootDir}/core/main/allopen_dependencies.gradle" +apply from: "${project.rootDir}/core/main/test_dependencies.gradle" +apply from: "${project.rootDir}/core/main/jacoco_global.gradle" + +android { + namespace 'info.nightscout.pump.medtrum' + dataBinding { + enabled = true + } +} + +dependencies { + implementation project(':app-wear-shared:shared') + implementation project(':database:entities') + implementation project(':core:libraries') + implementation project(':core:interfaces') + implementation project(':core:main') + implementation project(':core:ui') + implementation project(':core:validators') + implementation project(':pump:pump-common') + implementation project(':core:utils') + + testImplementation project(':core:main') +} diff --git a/pump/medtrum/consumer-rules.pro b/pump/medtrum/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pump/medtrum/proguard-rules.pro b/pump/medtrum/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/pump/medtrum/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 diff --git a/pump/medtrum/src/main/AndroidManifest.xml b/pump/medtrum/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..22b7e1ba50 --- /dev/null +++ b/pump/medtrum/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt new file mode 100644 index 0000000000..ad5e49cbc8 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt @@ -0,0 +1,472 @@ +package info.nightscout.pump.medtrum + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.text.format.DateFormat +import androidx.preference.EditTextPreference +import androidx.preference.PreferenceFragmentCompat +import dagger.android.HasAndroidInjector +import info.nightscout.core.ui.toast.ToastUtils +import info.nightscout.core.utils.fabric.FabricPrivacy +import info.nightscout.interfaces.constraints.Constraint +import info.nightscout.interfaces.constraints.Constraints +import info.nightscout.interfaces.notifications.Notification +import info.nightscout.interfaces.plugin.PluginDescription +import info.nightscout.interfaces.plugin.PluginType +import info.nightscout.interfaces.profile.Profile +import info.nightscout.interfaces.pump.DetailedBolusInfo +import info.nightscout.interfaces.pump.Medtrum +import info.nightscout.interfaces.pump.Pump +import info.nightscout.interfaces.pump.PumpEnactResult +import info.nightscout.interfaces.pump.PumpPluginBase +import info.nightscout.interfaces.pump.PumpSync +import info.nightscout.interfaces.pump.TemporaryBasalStorage +import info.nightscout.interfaces.pump.actions.CustomAction +import info.nightscout.interfaces.pump.actions.CustomActionType +import info.nightscout.interfaces.pump.defs.ManufacturerType +import info.nightscout.interfaces.pump.defs.PumpDescription +import info.nightscout.interfaces.pump.defs.PumpType +import info.nightscout.interfaces.queue.Callback +import info.nightscout.interfaces.queue.CommandQueue +import info.nightscout.interfaces.queue.CustomCommand +import info.nightscout.interfaces.ui.UiInteraction +import info.nightscout.interfaces.utils.DecimalFormatter +import info.nightscout.interfaces.utils.TimeChangeType +import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState +import info.nightscout.pump.medtrum.ui.MedtrumOverviewFragment +import info.nightscout.pump.medtrum.services.MedtrumService +import info.nightscout.rx.AapsSchedulers +import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventAppExit +import info.nightscout.rx.events.EventDismissNotification +import info.nightscout.rx.events.EventOverviewBolusProgress +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import info.nightscout.shared.utils.DateUtil +import info.nightscout.shared.utils.T +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.plusAssign +import org.json.JSONException +import org.json.JSONObject +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.math.abs + +@Singleton class MedtrumPlugin @Inject constructor( + injector: HasAndroidInjector, + aapsLogger: AAPSLogger, + rh: ResourceHelper, + commandQueue: CommandQueue, + private val constraintChecker: Constraints, + private val aapsSchedulers: AapsSchedulers, + private val rxBus: RxBus, + private val context: Context, + private val fabricPrivacy: FabricPrivacy, + private val dateUtil: DateUtil, + private val medtrumPump: MedtrumPump, + private val uiInteraction: UiInteraction, + private val pumpSync: PumpSync, + private val temporaryBasalStorage: TemporaryBasalStorage +) : PumpPluginBase( + PluginDescription() + .mainType(PluginType.PUMP) + .fragmentClass(MedtrumOverviewFragment::class.java.name) + .pluginIcon(info.nightscout.core.ui.R.drawable.ic_medtrum_128) + .pluginName(R.string.medtrum) + .shortName(R.string.medtrum_pump_shortname) + .preferencesId(R.xml.pref_medtrum_pump) + .description(R.string.medtrum_pump_description), injector, aapsLogger, rh, commandQueue +), Pump, Medtrum { + + private val disposable = CompositeDisposable() + private var medtrumService: MedtrumService? = null + + override fun onStart() { + super.onStart() + aapsLogger.debug(LTag.PUMP, "MedtrumPlugin onStart()") + val intent = Intent(context, MedtrumService::class.java) + context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE) + disposable += rxBus + .toObservable(EventAppExit::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ context.unbindService(mConnection) }, fabricPrivacy::logException) + } + + override fun onStop() { + aapsLogger.debug(LTag.PUMP, "MedtrumPlugin onStop()") + context.unbindService(mConnection) + disposable.clear() + super.onStop() + } + + private val mConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName) { + aapsLogger.debug(LTag.PUMP, "Service is disconnected") + medtrumService = null + } + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + aapsLogger.debug(LTag.PUMP, "Service is connected") + val mLocalBinder = service as MedtrumService.LocalBinder + medtrumService = mLocalBinder.serviceInstance + } + } + + fun getService(): MedtrumService? { + return medtrumService + } + + override fun preprocessPreferences(preferenceFragment: PreferenceFragmentCompat) { + super.preprocessPreferences(preferenceFragment) + val serialSetting = preferenceFragment.findPreference(rh.gs(R.string.key_sn_input)) as EditTextPreference? + serialSetting?.isEnabled = !isInitialized() + } + + override fun isInitialized(): Boolean { + return medtrumPump.pumpState > MedtrumPumpState.EJECTED && medtrumPump.pumpState < MedtrumPumpState.STOPPED + } + + override fun isSuspended(): Boolean { + return medtrumPump.pumpState < MedtrumPumpState.ACTIVE || medtrumPump.pumpState > MedtrumPumpState.ACTIVE_ALT + } + + override fun isBusy(): Boolean { + return false + } + + override fun isConnected(): Boolean { + // This is a workaround to prevent AAPS to trigger connects when we have no patch activated + return if (!isInitialized()) true else medtrumService?.isConnected ?: false + } + + override fun isConnecting(): Boolean = medtrumService?.isConnecting ?: false + override fun isHandshakeInProgress(): Boolean = false + + override fun finishHandshaking() { + } + + override fun connect(reason: String) { + if (isInitialized()) { + aapsLogger.debug(LTag.PUMP, "Medtrum connect - reason:$reason") + if (medtrumService != null) { + aapsLogger.debug(LTag.PUMP, "Medtrum connect - Attempt connection!") + val success = medtrumService?.connect(reason) ?: false + if (!success) ToastUtils.errorToast(context, info.nightscout.core.ui.R.string.ble_not_supported_or_not_paired) + } + } + } + + override fun disconnect(reason: String) { + if (isInitialized()) { + aapsLogger.debug(LTag.PUMP, "Medtrum disconnect from: $reason") + medtrumService?.disconnect(reason) + } + } + + override fun stopConnecting() { + if (isInitialized()) { + aapsLogger.debug(LTag.PUMP, "Medtrum stopConnecting") + medtrumService?.stopConnecting() + } + } + + override fun getPumpStatus(reason: String) { + aapsLogger.debug(LTag.PUMP, "Medtrum getPumpStatus - reason:$reason") + if (isInitialized()) { + val connectionOK = medtrumService?.readPumpStatus() ?: false + if (connectionOK == false) { + aapsLogger.error(LTag.PUMP, "Medtrum getPumpStatus failed") + } + } + } + + override fun setNewBasalProfile(profile: Profile): PumpEnactResult { + // New profile will be set when patch is activated + if (!isInitialized()) return PumpEnactResult(injector).success(true).enacted(true) + + return if (medtrumService?.updateBasalsInPump(profile) == true) { + rxBus.send(EventDismissNotification(Notification.FAILED_UPDATE_PROFILE)) + uiInteraction.addNotificationValidFor(Notification.PROFILE_SET_OK, rh.gs(info.nightscout.core.ui.R.string.profile_set_ok), Notification.INFO, 60) + PumpEnactResult(injector).success(true).enacted(true) + } else { + uiInteraction.addNotification(Notification.FAILED_UPDATE_PROFILE, rh.gs(info.nightscout.core.ui.R.string.failed_update_basal_profile), Notification.URGENT) + PumpEnactResult(injector) + } + } + + override fun isThisProfileSet(profile: Profile): Boolean { + if (!isInitialized()) return true + var result = false + val profileBytes = medtrumPump.buildMedtrumProfileArray(profile) + if (profileBytes?.size == medtrumPump.actualBasalProfile.size) { + result = true + for (i in profileBytes.indices) { + if (profileBytes[i] != medtrumPump.actualBasalProfile[i]) { + result = false + break + } + } + } + return result + } + + override fun lastDataTime(): Long = medtrumPump.lastConnection + override val baseBasalRate: Double + get() = medtrumPump.baseBasalRate + + override val reservoirLevel: Double + get() = medtrumPump.reservoir + + override val batteryLevel: Int + get() = 0 // We cannot determine battery level (yet) + + @Synchronized + override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult { + aapsLogger.debug(LTag.PUMP, "deliverTreatment: " + detailedBolusInfo.insulin + "U") + if (!isInitialized()) return PumpEnactResult(injector).success(false).enacted(false) + detailedBolusInfo.insulin = constraintChecker.applyBolusConstraints(Constraint(detailedBolusInfo.insulin)).value() + return if (detailedBolusInfo.insulin > 0 && detailedBolusInfo.carbs == 0.0) { + aapsLogger.debug(LTag.PUMP, "deliverTreatment: Delivering bolus: " + detailedBolusInfo.insulin + "U") + val t = EventOverviewBolusProgress.Treatment(0.0, 0, detailedBolusInfo.bolusType == DetailedBolusInfo.BolusType.SMB, detailedBolusInfo.id) + val connectionOK = medtrumService?.setBolus(detailedBolusInfo, t) ?: false + val result = PumpEnactResult(injector) + result.success = connectionOK && abs(detailedBolusInfo.insulin - t.insulin) < pumpDescription.bolusStep + result.bolusDelivered = t.insulin + if (!result.success) { + // Note: There are no error codes + result.comment = "failed" + } else { + result.comment = "ok" + } + aapsLogger.debug(LTag.PUMP, "deliverTreatment: OK. Success: ${result.success} Asked: ${detailedBolusInfo.insulin} Delivered: ${result.bolusDelivered}") + result + } else { + aapsLogger.debug(LTag.PUMP, "deliverTreatment: Invalid input") + val result = PumpEnactResult(injector) + result.success = false + result.bolusDelivered = 0.0 + result.comment = rh.gs(info.nightscout.core.ui.R.string.invalid_input) + aapsLogger.error("deliverTreatment: Invalid input") + result + } + } + + override fun stopBolusDelivering() { + if (!isInitialized()) return + + aapsLogger.info(LTag.PUMP, "stopBolusDelivering") + medtrumService?.stopBolus() + } + + @Synchronized + override fun setTempBasalAbsolute(absoluteRate: Double, durationInMinutes: Int, profile: Profile, enforceNew: Boolean, tbrType: PumpSync.TemporaryBasalType): PumpEnactResult { + if (!isInitialized()) return PumpEnactResult(injector).success(false).enacted(false) + + aapsLogger.info(LTag.PUMP, "setTempBasalAbsolute - absoluteRate: $absoluteRate, durationInMinutes: $durationInMinutes, enforceNew: $enforceNew") + // round rate to pump rate + val pumpRate = constraintChecker.applyBasalConstraints(Constraint(absoluteRate), profile).value() + temporaryBasalStorage.add(PumpSync.PumpState.TemporaryBasal(dateUtil.now(), T.mins(durationInMinutes.toLong()).msecs(), pumpRate, true, tbrType, 0L, 0L)) + val connectionOK = medtrumService?.setTempBasal(pumpRate, durationInMinutes) ?: false + if (connectionOK + && medtrumPump.tempBasalInProgress + && Math.abs(medtrumPump.tempBasalAbsoluteRate - pumpRate) <= 0.05 + ) { + + return PumpEnactResult(injector).success(true).enacted(true).duration(durationInMinutes).absolute(medtrumPump.tempBasalAbsoluteRate) + .isPercent(false) + .isTempCancel(false) + } else { + aapsLogger.error( + LTag.PUMP, + "setTempBasalAbsolute failed, connectionOK: $connectionOK, tempBasalInProgress: ${medtrumPump.tempBasalInProgress}, tempBasalAbsoluteRate: ${medtrumPump.tempBasalAbsoluteRate}" + ) + return PumpEnactResult(injector).success(false).enacted(false).comment("Medtrum setTempBasalAbsolute failed") + } + } + + 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") + return PumpEnactResult(injector).success(false).enacted(false).comment("Medtrum driver does not support percentage temp basals") + } + + override fun setExtendedBolus(insulin: Double, durationInMinutes: Int): PumpEnactResult { + aapsLogger.info(LTag.PUMP, "setExtendedBolus - insulin: $insulin, durationInMinutes: $durationInMinutes") + return PumpEnactResult(injector).success(false).enacted(false).comment("Medtrum driver does not support extended boluses") + } + + override fun cancelTempBasal(enforceNew: Boolean): PumpEnactResult { + if (!isInitialized()) return PumpEnactResult(injector).success(false).enacted(false) + + aapsLogger.info(LTag.PUMP, "cancelTempBasal - enforceNew: $enforceNew") + val connectionOK = medtrumService?.cancelTempBasal() ?: false + if (connectionOK && !medtrumPump.tempBasalInProgress) { + return PumpEnactResult(injector).success(true).enacted(true).isTempCancel(true) + } else { + aapsLogger.error(LTag.PUMP, "cancelTempBasal failed, connectionOK: $connectionOK, tempBasalInProgress: ${medtrumPump.tempBasalInProgress}") + return PumpEnactResult(injector).success(false).enacted(false).comment("Medtrum cancelTempBasal failed") + } + } + + override fun cancelExtendedBolus(): PumpEnactResult { + return PumpEnactResult(injector) + } + + override fun getJSONStatus(profile: Profile, profileName: String, version: String): JSONObject { + val now = System.currentTimeMillis() + if (medtrumPump.lastConnection + 60 * 60 * 1000L < System.currentTimeMillis()) { + return JSONObject() + } + val pumpJson = JSONObject() + val status = JSONObject() + val extended = JSONObject() + try { + status.put( + "status", if (!isSuspended()) "normal" + else if (isInitialized() && isSuspended()) "suspended" + else "no active patch" + ) + status.put("timestamp", dateUtil.toISOString(medtrumPump.lastConnection)) + if (medtrumPump.lastBolusTime != 0L) { + extended.put("lastBolus", dateUtil.dateAndTimeString(medtrumPump.lastBolusTime)) + extended.put("lastBolusAmount", medtrumPump.lastBolusAmount) + } + val tb = pumpSync.expectedPumpState().temporaryBasal + if (tb != null) { + extended.put("TempBasalAbsoluteRate", tb.convertedToAbsolute(now, profile)) + extended.put("TempBasalStart", dateUtil.dateAndTimeString(tb.timestamp)) + extended.put("TempBasalRemaining", tb.plannedRemainingMinutes) + } + extended.put("BaseBasalRate", baseBasalRate) + try { + extended.put("ActiveProfile", profileName) + } catch (ignored: Exception) { + } + pumpJson.put("status", status) + pumpJson.put("extended", extended) + pumpJson.put("reservoir", medtrumPump.reservoir.toInt()) + pumpJson.put("clock", dateUtil.toISOString(now)) + } catch (e: JSONException) { + aapsLogger.error(LTag.PUMP, "Unhandled exception: $e") + } + return pumpJson + } + + override fun manufacturer(): ManufacturerType { + return ManufacturerType.Medtrum + } + + override fun model(): PumpType { + return medtrumPump.pumpType() + } + + override fun serialNumber(): String { + // Load from SP here, because this value will be get before pump is initialized + return medtrumPump.pumpSNFromSP.toString(radix = 16) + } + + override val pumpDescription: PumpDescription + get() = PumpDescription(medtrumPump.pumpType()) + + override fun shortStatus(veryShort: Boolean): String { + var ret = "" + if (medtrumPump.lastConnection != 0L) { + val agoMillis = System.currentTimeMillis() - medtrumPump.lastConnection + val agoMin = (agoMillis / 60.0 / 1000.0).toInt() + ret += "LastConn: $agoMin minAgo\n" + } + if (medtrumPump.lastBolusTime != 0L) + ret += "LastBolus: ${DecimalFormatter.to2Decimal(medtrumPump.lastBolusAmount)}U @${DateFormat.format("HH:mm", medtrumPump.lastBolusTime)}\n" + + if (medtrumPump.tempBasalInProgress) + ret += "Temp: ${medtrumPump.temporaryBasalToString()}\n" + + ret += "Res: ${DecimalFormatter.to0Decimal(medtrumPump.reservoir)}U\n" + return ret + } + + override val isFakingTempsByExtendedBoluses: Boolean = false + + override fun loadTDDs(): PumpEnactResult { + return PumpEnactResult(injector) // Note: Can implement this if we implement history fully (no priority) + } + + override fun getCustomActions(): List? { + return null + } + + override fun executeCustomAction(customActionType: CustomActionType) { + } + + override fun executeCustomCommand(customCommand: CustomCommand): PumpEnactResult? { + return null + } + + override fun canHandleDST(): Boolean { + return true + } + + override fun timezoneOrDSTChanged(timeChangeType: TimeChangeType) { + medtrumPump.needCheckTimeUpdate = true + if (isInitialized()) { + commandQueue.updateTime(object : Callback() { + override fun run() { + if (this.result.success == false) { + aapsLogger.error(LTag.PUMP, "Medtrum time update failed") + // Only notify here on failure (connection may be failed), service will handle success + medtrumService?.timeUpdateNotification(false) + } + } + }) + } + } + + // Medtrum interface + override fun loadEvents(): PumpEnactResult { + if (!isInitialized()) { + val result = PumpEnactResult(injector).success(false) + result.comment = "pump not initialized" + return result + } + val connectionOK = medtrumService?.loadEvents() ?: false + return PumpEnactResult(injector).success(connectionOK) + } + + override fun setUserOptions(): PumpEnactResult { + if (!isInitialized()) { + val result = PumpEnactResult(injector).success(false) + result.comment = "pump not initialized" + return result + } + val connectionOK = medtrumService?.setUserSettings() ?: false + return PumpEnactResult(injector).success(connectionOK) + } + + override fun clearAlarms(): PumpEnactResult { + if (!isInitialized()) { + val result = PumpEnactResult(injector).success(false) + result.comment = "pump not initialized" + return result + } + val connectionOK = medtrumService?.clearAlarms() ?: false + return PumpEnactResult(injector).success(connectionOK) + } + + override fun deactivate(): PumpEnactResult { + val connectionOK = medtrumService?.deactivatePatch() ?: false + return PumpEnactResult(injector).success(connectionOK) + } + + override fun updateTime(): PumpEnactResult { + if (!isInitialized()) { + val result = PumpEnactResult(injector).success(false) + result.comment = "pump not initialized" + return result + } + val connectionOK = medtrumService?.updateTimeIfNeeded() ?: false + return PumpEnactResult(injector).success(connectionOK) + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPump.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPump.kt new file mode 100644 index 0000000000..fd24934cda --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPump.kt @@ -0,0 +1,539 @@ +package info.nightscout.pump.medtrum + +import android.util.Base64 +import info.nightscout.interfaces.profile.Profile +import info.nightscout.interfaces.pump.PumpSync +import info.nightscout.interfaces.pump.TemporaryBasalStorage +import info.nightscout.interfaces.pump.defs.PumpType +import info.nightscout.pump.medtrum.code.ConnectionState +import info.nightscout.pump.medtrum.comm.enums.AlarmSetting +import info.nightscout.pump.medtrum.comm.enums.AlarmState +import info.nightscout.pump.medtrum.comm.enums.BasalType +import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.rx.events.EventOverviewBolusProgress +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import info.nightscout.shared.sharedPreferences.SP +import info.nightscout.shared.utils.DateUtil +import info.nightscout.shared.utils.T +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.math.round + +@Singleton +class MedtrumPump @Inject constructor( + private val aapsLogger: AAPSLogger, + private val rh: ResourceHelper, + private val sp: SP, + private val dateUtil: DateUtil, + private val pumpSync: PumpSync, + private val temporaryBasalStorage: TemporaryBasalStorage +) { + + companion object { + + const val FAKE_TBR_LENGTH = 4800L + } + + // Connection state flow + private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED) + val connectionStateFlow: StateFlow = _connectionState + var connectionState: ConnectionState + get() = _connectionState.value + set(value) { + _connectionState.value = value + } + + // Pump state flow + private val _pumpState = MutableStateFlow(MedtrumPumpState.NONE) + val pumpStateFlow: StateFlow = _pumpState + var pumpState: MedtrumPumpState + get() = _pumpState.value + set(value) { + _pumpState.value = value + sp.putInt(R.string.key_pump_state, value.state.toInt()) + } + + // Active alarms + private var _activeAlarms: EnumSet = EnumSet.noneOf(AlarmState::class.java) + var activeAlarms: EnumSet + get() = _activeAlarms + set(value) { + _activeAlarms = value + } + + // Prime progress as state flow + private val _primeProgress = MutableStateFlow(0) + val primeProgressFlow: StateFlow = _primeProgress + var primeProgress: Int + get() = _primeProgress.value + set(value) { + _primeProgress.value = value + } + + private var _lastBasalType: MutableStateFlow = MutableStateFlow(BasalType.NONE) + val lastBasalTypeFlow: StateFlow = _lastBasalType + val lastBasalType: BasalType + get() = _lastBasalType.value + + private val _lastBasalRate = MutableStateFlow(0.0) + val lastBasalRateFlow: StateFlow = _lastBasalRate + val lastBasalRate: Double + get() = _lastBasalRate.value + + private val _reservoir = MutableStateFlow(0.0) + val reservoirFlow: StateFlow = _reservoir + var reservoir: Double + get() = _reservoir.value + set(value) { + _reservoir.value = value + } + + var batteryVoltage_A = 0.0 // Not used in UI + private val _batteryVoltage_B = MutableStateFlow(0.0) + val batteryVoltage_BFlow: StateFlow = _batteryVoltage_B + var batteryVoltage_B: Double + get() = _batteryVoltage_B.value + set(value) { + _batteryVoltage_B.value = value + } + + /** Stuff stored in SP */ + private var _patchSessionToken = 0L + var patchSessionToken: Long + get() = _patchSessionToken + set(value) { + _patchSessionToken = value + sp.putLong(R.string.key_session_token, value) + } + + private var _patchId = 0L + var patchId: Long + get() = _patchId + set(value) { + _patchId = value + sp.putLong(R.string.key_patch_id, value) + } + + private var _currentSequenceNumber = 0 + var currentSequenceNumber: Int + get() = _currentSequenceNumber + set(value) { + _currentSequenceNumber = value + sp.putInt(R.string.key_current_sequence_number, value) + } + + private var _syncedSequenceNumber = 0 + var syncedSequenceNumber: Int + get() = _syncedSequenceNumber + set(value) { + _syncedSequenceNumber = value + sp.putInt(R.string.key_synced_sequence_number, value) + } + + private var _actualBasalProfile = byteArrayOf(0) + var actualBasalProfile: ByteArray + get() = _actualBasalProfile + set(value) { + _actualBasalProfile = value + val encodedString = Base64.encodeToString(value, Base64.DEFAULT) + sp.putString(R.string.key_actual_basal_profile, encodedString ?: "") + } + + private var _lastBolusTime = 0L // Time in ms! + var lastBolusTime: Long + get() = _lastBolusTime + set(value) { + _lastBolusTime = value + sp.putLong(R.string.key_last_bolus_time, value) + } + + private var _lastBolusAmount = 0.0 + var lastBolusAmount: Double + get() = _lastBolusAmount + set(value) { + _lastBolusAmount = value + sp.putDouble(R.string.key_last_bolus_amount, value) + } + + private var _lastConnection = 0L // Time in ms! + var lastConnection: Long + get() = _lastConnection + set(value) { + _lastConnection = value + sp.putLong(R.string.key_last_connection, value) + } + + private var _deviceType: Int = 80 // As reported by pump + var deviceType: Int + get() = _deviceType + set(value) { + _deviceType = value + sp.putInt(R.string.key_device_type, value) + } + + private var _swVersion: String = "" // As reported by pump + var swVersion: String + get() = _swVersion + set(value) { + _swVersion = value + sp.putString(R.string.key_sw_version, value) + } + + private var _patchStartTime = 0L // Time in ms! + var patchStartTime: Long + get() = _patchStartTime + set(value) { + _patchStartTime = value + sp.putLong(R.string.key_patch_start_time, value) + } + + private var _pumpTimeZoneOffset = 0 // As reported by pump + var pumpTimeZoneOffset: Int + get() = _pumpTimeZoneOffset + set(value) { + _pumpTimeZoneOffset = value + sp.putInt(R.string.key_pump_time_zone_offset, value) + } + + private var _pumpSN = 0L + val pumpSN: Long + get() = _pumpSN + + val pumpSNFromSP: Long + get() = + try { + sp.getString(R.string.key_sn_input, "0").toLong(radix = 16) + } catch (e: NumberFormatException) { + aapsLogger.debug(LTag.PUMP, "pumpSNFromSP: Invalid input!") + 0L + } + + var needCheckTimeUpdate = false + var lastTimeReceivedFromPump = 0L // Time in ms! + var suspendTime = 0L // Time in ms! + var patchAge = 0L // Time in seconds?! // As reported by pump, not used (yet) + + // bolus status + var bolusingTreatment: EventOverviewBolusProgress.Treatment? = null // actually delivered treatment + var bolusAmountToBeDelivered = 0.0 // amount to be delivered + var bolusProgressLastTimeStamp: Long = 0 // timestamp of last bolus progress message + var bolusStopped = false // bolus stopped by user + var bolusDone = false // Bolus completed or stopped on pump + + // Last basal status update (from pump) + private var _lastBasalSequence = 0 + val lastBasalSequence: Int + get() = _lastBasalSequence + + private var _lastBasalPatchId = 0L + val lastBasalPatchId: Long + get() = _lastBasalPatchId + + private var _lastBasalStartTime = 0L + val lastBasalStartTime: Long + get() = _lastBasalStartTime + + val baseBasalRate: Double + get() = getHourlyBasalFromMedtrumProfileArray(actualBasalProfile, dateUtil.now()) + + // TBR status + val tempBasalInProgress: Boolean + get() = lastBasalType == BasalType.ABSOLUTE_TEMP || lastBasalType == BasalType.RELATIVE_TEMP + val tempBasalAbsoluteRate: Double + get() = if (tempBasalInProgress) lastBasalRate else 0.0 + + // Last stop status update + var lastStopSequence = 0 + var lastStopPatchId = 0L + + // User settings (desired values, to be set on pump) + var desiredPatchExpiration = false + var desiredAlarmSetting = AlarmSetting.LIGHT_VIBRATE_AND_BEEP + var desiredHourlyMaxInsulin: Int = 40 + var desiredDailyMaxInsulin: Int = 180 + + init { + // Load stuff from SP + _patchSessionToken = sp.getLong(R.string.key_session_token, 0L) + _lastConnection = sp.getLong(R.string.key_last_connection, 0L) + _lastBolusTime = sp.getLong(R.string.key_last_bolus_time, 0L) + _lastBolusAmount = sp.getDouble(R.string.key_last_bolus_amount, 0.0) + _currentSequenceNumber = sp.getInt(R.string.key_current_sequence_number, 0) + _patchId = sp.getLong(R.string.key_patch_id, 0L) + _syncedSequenceNumber = sp.getInt(R.string.key_synced_sequence_number, 0) + _pumpState.value = MedtrumPumpState.fromByte(sp.getInt(R.string.key_pump_state, MedtrumPumpState.NONE.state.toInt()).toByte()) + _deviceType = sp.getInt(R.string.key_device_type, 0) + _swVersion = sp.getString(R.string.key_sw_version, "") + _patchStartTime = sp.getLong(R.string.key_patch_start_time, 0L) + _pumpTimeZoneOffset = sp.getInt(R.string.key_pump_time_zone_offset, 0) + + loadActiveAlarms() + + val encodedString = sp.getString(R.string.key_actual_basal_profile, "0") + try { + _actualBasalProfile = Base64.decode(encodedString, Base64.DEFAULT) + } catch (e: Exception) { + aapsLogger.error(LTag.PUMP, "Error decoding basal profile from SP: $encodedString") + } + } + + fun pumpType(): PumpType = + when (deviceType) { + 80, 88 -> PumpType.MEDTRUM_NANO + else -> PumpType.MEDTRUM_UNTESTED + } + + fun loadUserSettingsFromSP() { + desiredPatchExpiration = sp.getBoolean(info.nightscout.pump.medtrum.R.string.key_patch_expiration, false) + val alarmSettingCode = sp.getString(info.nightscout.pump.medtrum.R.string.key_alarm_setting, AlarmSetting.LIGHT_VIBRATE_AND_BEEP.code.toString()).toByte() + desiredAlarmSetting = AlarmSetting.values().firstOrNull { it.code == alarmSettingCode } ?: AlarmSetting.LIGHT_VIBRATE_AND_BEEP + desiredHourlyMaxInsulin = sp.getInt(info.nightscout.pump.medtrum.R.string.key_hourly_max_insulin, 40) + desiredDailyMaxInsulin = sp.getInt(info.nightscout.pump.medtrum.R.string.key_daily_max_insulin, 180) + _pumpSN = pumpSNFromSP + + } + + fun buildMedtrumProfileArray(nsProfile: Profile): ByteArray? { + val list = nsProfile.getBasalValues() + var basals = byteArrayOf() + for (item in list) { + val rate = round(item.value / 0.05).toInt() + val time = item.timeAsSeconds / 60 + if (rate > 0xFFF || time > 0xFFF) { + aapsLogger.error(LTag.PUMP, "buildMedtrumProfileArray: rate or time too large: $rate, $time") + return null + } + basals += ((rate shl 12) + time).toByteArray(3) + aapsLogger.debug(LTag.PUMP, "buildMedtrumProfileArray: value: ${item.value} time: ${item.timeAsSeconds}, converted: $rate, $time") + } + return (list.size).toByteArray(1) + basals + } + + fun getHourlyBasalFromMedtrumProfileArray(basalProfile: ByteArray, timestamp: Long): Double { + val basalCount = basalProfile[0].toInt() + var basal = 0.0 + if (basalProfile.size < 4 || (basalProfile.size - 1) % 3 != 0 || basalCount > 24) { + aapsLogger.debug(LTag.PUMP, "getHourlyBasalFromMedtrumProfileArray: No valid basal profile set") + return basal + } + + val date = GregorianCalendar() + date.timeInMillis = timestamp + val hourOfDayMinutes = date.get(GregorianCalendar.HOUR_OF_DAY) * 60 + date.get(GregorianCalendar.MINUTE) + + for (index in 0 until basalCount) { + val currentIndex = 1 + (index * 3) + val nextIndex = currentIndex + 3 + val rateAndTime = basalProfile.copyOfRange(currentIndex, nextIndex).toInt() + val rate = (rateAndTime shr 12) * 0.05 + val startMinutes = rateAndTime and 0xFFF + + val endMinutes = if (nextIndex < basalProfile.size) { + val nextRateAndTime = basalProfile.copyOfRange(nextIndex, nextIndex + 3).toInt() + nextRateAndTime and 0xFFF + } else { + 24 * 60 + } + + if (hourOfDayMinutes in startMinutes until endMinutes) { + basal = rate + aapsLogger.debug(LTag.PUMP, "getHourlyBasalFromMedtrumProfileArray: basal: $basal") + break + } + // aapsLogger.debug(LTag.PUMP, "getHourlyBasalFromMedtrumProfileArray: rate: $rate, startMinutes: $startMinutes, endMinutes: $endMinutes") + } + return basal + } + + fun handleBolusStatusUpdate(bolusType: Int, bolusCompleted: Boolean, amountDelivered: Double) { + aapsLogger.debug(LTag.PUMP, "handleBolusStatusUpdate: bolusType: $bolusType bolusCompleted: $bolusCompleted amountDelivered: $amountDelivered") + bolusProgressLastTimeStamp = dateUtil.now() + bolusingTreatment?.insulin = amountDelivered + bolusDone = bolusCompleted + } + + fun handleBasalStatusUpdate(basalType: BasalType, basalValue: Double, basalSequence: Int, basalPatchId: Long, basalStartTime: Long) { + handleBasalStatusUpdate(basalType, basalValue, basalSequence, basalPatchId, basalStartTime, dateUtil.now()) + } + + fun handleBasalStatusUpdate(basalType: BasalType, basalRate: Double, basalSequence: Int, basalPatchId: Long, basalStartTime: Long, receivedTime: Long) { + aapsLogger.debug( + LTag.PUMP, + "handleBasalStatusUpdate: basalType: $basalType basalValue: $basalRate basalSequence: $basalSequence basalPatchId: $basalPatchId basalStartTime: $basalStartTime " + "receivedTime: $receivedTime" + ) + @Suppress("UNNECESSARY_SAFE_CALL") // Safe call to allow mocks to return null + val expectedTemporaryBasal = pumpSync.expectedPumpState()?.temporaryBasal + if (basalType.isTempBasal() && expectedTemporaryBasal?.pumpId != basalStartTime) { + // Note: temporaryBasalInfo will be removed from temporaryBasalStorage after this call + val temporaryBasalInfo = temporaryBasalStorage.findTemporaryBasal(basalStartTime, basalRate) + + // If duration is unknown, no way to get it now, set patch lifetime as duration + val duration = temporaryBasalInfo?.duration ?: T.mins(FAKE_TBR_LENGTH).msecs() + val adjustedBasalRate = if (basalType == BasalType.ABSOLUTE_TEMP) { + basalRate + } else { + (basalRate / baseBasalRate) * 100 // calculate the percentage of the original basal rate + } + val newRecord = pumpSync.syncTemporaryBasalWithPumpId( + timestamp = basalStartTime, + rate = adjustedBasalRate, + duration = duration, + isAbsolute = (basalType == BasalType.ABSOLUTE_TEMP), + type = temporaryBasalInfo?.type, + pumpId = basalStartTime, + pumpType = pumpType(), + pumpSerial = pumpSN.toString(radix = 16) + ) + aapsLogger.debug( + LTag.PUMPCOMM, + "handleBasalStatusUpdate: ${if (newRecord) "**NEW** " else ""}EVENT TEMP_START ($basalType) ${dateUtil.dateAndTimeString(basalStartTime)} ($basalStartTime) " + "Rate: $basalRate Duration: ${duration} temporaryBasalInfo: $temporaryBasalInfo, expectedTemporaryBasal: $expectedTemporaryBasal" + ) + } else if (basalType.isSuspendedByPump() && expectedTemporaryBasal?.pumpId != basalStartTime) { + val newRecord = pumpSync.syncTemporaryBasalWithPumpId( + timestamp = basalStartTime, + rate = 0.0, + duration = T.mins(FAKE_TBR_LENGTH).msecs(), + isAbsolute = true, + type = PumpSync.TemporaryBasalType.PUMP_SUSPEND, + pumpId = basalStartTime, + pumpType = pumpType(), + pumpSerial = pumpSN.toString(radix = 16) + ) + aapsLogger.debug( + LTag.PUMPCOMM, + "handleBasalStatusUpdate: ${if (newRecord) "**NEW** " else ""}EVENT TEMP_START ($basalType) ${dateUtil.dateAndTimeString(basalStartTime)} ($basalStartTime) expectedTemporaryBasal: $expectedTemporaryBasal" + ) + } else if (basalType == BasalType.NONE && expectedTemporaryBasal?.rate != basalRate && expectedTemporaryBasal?.duration != T.mins(FAKE_TBR_LENGTH).msecs()) { + // Pump suspended, set fake TBR + setFakeTBR() + } else if (basalType == BasalType.STANDARD) { + if (expectedTemporaryBasal != null) { + // Pump resumed, sync end + val success = pumpSync.syncStopTemporaryBasalWithPumpId( + timestamp = basalStartTime + 250, // Time of normal basal start = time of tbr end + endPumpId = basalStartTime + 250, // +250ms Make sure there is time between start and stop of TBR + pumpType = pumpType(), + pumpSerial = pumpSN.toString(radix = 16) + ) + aapsLogger.debug(LTag.PUMPCOMM, "handleBasalStatusUpdate: EVENT TEMP_END ${dateUtil.dateAndTimeString(basalStartTime)} ($basalStartTime) success: $success") + } + } + + // Update medtrum pump state + _lastBasalType.value = basalType + _lastBasalRate.value = basalRate + _lastBasalSequence = basalSequence + if (basalSequence > currentSequenceNumber) { + currentSequenceNumber = basalSequence + } + _lastBasalPatchId = basalPatchId + if (basalPatchId != patchId) { + aapsLogger.error(LTag.PUMP, "handleBasalStatusUpdate: WTF? PatchId in status update does not match current patchId!") + } + _lastBasalStartTime = basalStartTime + } + + fun handleStopStatusUpdate(stopSequence: Int, stopPatchId: Long) { + aapsLogger.debug(LTag.PUMP, "handleStopStatusUpdate: stopSequence: $stopSequence stopPatchId: $stopPatchId") + lastStopSequence = stopSequence + if (stopSequence > currentSequenceNumber) { + currentSequenceNumber = stopSequence + } + lastStopPatchId = stopPatchId + if (stopPatchId != patchId) { + aapsLogger.error(LTag.PUMP, "handleStopStatusUpdate: WTF? PatchId in status update does not match current patchId!") + } + } + + fun setFakeTBRIfNeeded() { + val expectedTemporaryBasal = pumpSync.expectedPumpState().temporaryBasal + if (expectedTemporaryBasal?.duration != T.mins(FAKE_TBR_LENGTH).msecs()) { + setFakeTBR() + } + } + + private fun setFakeTBR() { + val newRecord = pumpSync.syncTemporaryBasalWithPumpId( + timestamp = dateUtil.now(), + rate = 0.0, + duration = T.mins(FAKE_TBR_LENGTH).msecs(), + isAbsolute = true, + type = PumpSync.TemporaryBasalType.PUMP_SUSPEND, + pumpId = dateUtil.now(), + pumpType = pumpType(), + pumpSerial = pumpSN.toString(radix = 16) + ) + aapsLogger.debug( + LTag.PUMPCOMM, + "handleBasalStatusUpdate: ${if (newRecord) "**NEW** " else ""}EVENT TEMP_START (FAKE)" + ) + } + + fun temporaryBasalToString(): String { + if (!tempBasalInProgress) return "" + return tempBasalAbsoluteRate.toString() + "U/h" + } + + fun addAlarm(alarm: AlarmState) { + activeAlarms.add(alarm) + saveActiveAlarms() + } + + fun removeAlarm(alarm: AlarmState) { + activeAlarms.remove(alarm) + saveActiveAlarms() + } + + fun clearAlarmState() { + activeAlarms.clear() + saveActiveAlarms() + } + + fun alarmStateToString(alarmState: AlarmState): String { + val stringId = when (alarmState) { + AlarmState.NONE -> R.string.alarm_none + AlarmState.PUMP_LOW_BATTERY -> R.string.alarm_pump_low_battery + AlarmState.PUMP_LOW_RESERVOIR -> R.string.alarm_pump_low_reservoir + AlarmState.PUMP_EXPIRES_SOON -> R.string.alarm_pump_expires_soon + AlarmState.LOWBG_SUSPENDED -> R.string.alarm_lowbg_suspended + AlarmState.LOWBG_SUSPENDED2 -> R.string.alarm_lowbg_suspended2 + AlarmState.AUTO_SUSPENDED -> R.string.alarm_auto_suspended + AlarmState.HMAX_SUSPENDED -> R.string.alarm_hmax_suspended + AlarmState.DMAX_SUSPENDED -> R.string.alarm_dmax_suspended + AlarmState.SUSPENDED -> R.string.alarm_suspended + AlarmState.PAUSED -> R.string.alarm_paused + AlarmState.OCCLUSION -> R.string.alarm_occlusion + AlarmState.EXPIRED -> R.string.alarm_expired + AlarmState.RESERVOIR_EMPTY -> R.string.alarm_reservoir_empty + AlarmState.PATCH_FAULT -> R.string.alarm_patch_fault + AlarmState.PATCH_FAULT2 -> R.string.alarm_patch_fault2 + AlarmState.BASE_FAULT -> R.string.alarm_base_fault + AlarmState.BATTERY_OUT -> R.string.alarm_battery_out + AlarmState.NO_CALIBRATION -> R.string.alarm_no_calibration + } + return rh.gs(stringId) + } + + private fun saveActiveAlarms() { + val alarmsStr = activeAlarms.joinToString(separator = ",") { it.name } + sp.putString(R.string.key_active_alarms, alarmsStr) + } + + private fun loadActiveAlarms() { + val alarmsStr = sp.getString(R.string.key_active_alarms, "") + if (alarmsStr.isNullOrEmpty()) { + activeAlarms = EnumSet.noneOf(AlarmState::class.java) + } else { + activeAlarms = alarmsStr.split(",") + .mapNotNull { AlarmState.values().find { alarm -> alarm.name == it } } + .let { EnumSet.copyOf(it) } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/bindingadapters/OnSafeClickListener.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/bindingadapters/OnSafeClickListener.kt new file mode 100644 index 0000000000..4475938960 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/bindingadapters/OnSafeClickListener.kt @@ -0,0 +1,26 @@ +package info.nightscout.pump.medtrum.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 { + // Set duplicate click prevention time + private const val MIN_CLICK_INTERVAL: Long = 1000 + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/bindingadapters/ViewBindingAdapter.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/bindingadapters/ViewBindingAdapter.kt new file mode 100644 index 0000000000..f4b51efe1a --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/bindingadapters/ViewBindingAdapter.kt @@ -0,0 +1,29 @@ +package info.nightscout.pump.medtrum.bindingadapters + +import android.view.View +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.databinding.BindingAdapter +import info.nightscout.pump.medtrum.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)) +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/ConnectionState.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/ConnectionState.kt new file mode 100644 index 0000000000..2b51faf0f0 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/ConnectionState.kt @@ -0,0 +1,8 @@ +package info.nightscout.pump.medtrum.code + +enum class ConnectionState { + CONNECTED, + DISCONNECTED, + CONNECTING, + DISCONNECTING; +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/EventType.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/EventType.kt new file mode 100644 index 0000000000..bd8f902afd --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/EventType.kt @@ -0,0 +1,7 @@ +package info.nightscout.pump.medtrum.code + +enum class EventType { + CHANGE_PATCH_CLICKED, + PROFILE_NOT_SET, + ; +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/PatchStep.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/PatchStep.kt new file mode 100644 index 0000000000..ea0cdf3d28 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/code/PatchStep.kt @@ -0,0 +1,21 @@ +package info.nightscout.pump.medtrum.code + +enum class PatchStep { + START_DEACTIVATION, + DEACTIVATE, + FORCE_DEACTIVATION, + DEACTIVATION_COMPLETE, + PREPARE_PATCH, + PREPARE_PATCH_CONNECT, + PRIME, + PRIMING, + PRIME_COMPLETE, + ATTACH_PATCH, + ACTIVATE, + ACTIVATE_COMPLETE, + RETRY_ACTIVATION, + RETRY_ACTIVATION_CONNECT, + ERROR, + CANCEL, + COMPLETE; +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ManufacturerData.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ManufacturerData.kt new file mode 100644 index 0000000000..098d31d984 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ManufacturerData.kt @@ -0,0 +1,36 @@ +package info.nightscout.pump.medtrum.comm + +import kotlin.experimental.and +import info.nightscout.pump.medtrum.extension.toLong + +class ManufacturerData(private val manufacturerDataBytes: ByteArray) { + private var deviceID: Long = 0 + private var deviceType = 0 + private var version = 0 + + init { + setData(manufacturerDataBytes) + } + + fun setData(inputData: ByteArray) { + var index = 0 + val deviceIDBytes: ByteArray = inputData.copyOfRange(index, index + 4) + deviceID = deviceIDBytes.toLong() + index += 4 + deviceType = (inputData[index] and 0xff.toByte()).toInt() + index += 1 + version = (inputData[index] and 0xff.toByte()).toInt() + } + + fun getDeviceSN(): Long{ + return deviceID + } + + fun getDeviceType(): Int { + return deviceType + } + + fun getVersion(): Int { + return version + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ReadDataPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ReadDataPacket.kt new file mode 100644 index 0000000000..e091cb8f73 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ReadDataPacket.kt @@ -0,0 +1,19 @@ +package info.nightscout.pump.medtrum.comm + +class ReadDataPacket(data: ByteArray) { + + private var totalData = data.copyOfRange(0, data.size - 1) // Strip crc + private var dataSize: Byte = data[0] + + fun addData(newData: ByteArray) { + totalData += newData.copyOfRange(4, newData.size - 1) // Strip header and crc + } + + fun allDataReceived(): Boolean { + return (totalData.size >= dataSize) + } + + fun getData(): ByteArray { + return totalData + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommandPackets.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommandPackets.kt new file mode 100644 index 0000000000..ae0f18e9f4 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommandPackets.kt @@ -0,0 +1,65 @@ +package info.nightscout.pump.medtrum.comm + +class WriteCommandPackets(data: ByteArray, sequenceNumber: Int) { + + private val CRC_8_TABLE: IntArray = intArrayOf(0, 155, 173, 54, 193, 90, 108, 247, 25, 130, 180, 47, 216, 67, 117, 238, 50, 169, 159, 4, 243, 104, 94, 197, 43, 176, 134, 29, 234, 113, 71, 220, 100, 255, 201, 82, 165, 62, 8, 147, 125, 230, 208, 75, 188, 39, 17, 138, 86, 205, 251, 96, 151, 12, 58, 161, 79, 212, 226, 121, 142, 21, 35, 184, 200, 83, 101, 254, 9, 146, 164, 63, 209, 74, 124, 231, 16, 139, 189, 38, 250, 97, 87, 204, 59, 160, 150, 13, 227, 120, 78, 213, 34, 185, 143, 20, 172, 55, 1, 154, 109, 246, 192, 91, 181, 46, 24, 131, 116, 239, 217, 66, 158, 5, 51, 168, 95, 196, 242, 105, 135, 28, 42, 177, 70, 221, 235, 112, 11, 144, 166, 61, 202, 81, 103, 252, 18, 137, 191, 36, 211, 72, 126, 229, 57, 162, 148, 15, 248, 99, 85, 206, 32, 187, 141, 22, 225, 122, 76, 215, 111, 244, 194, 89, 174, 53, 3, 152, 118, 237, 219, 64, 183, 44, 26, 129, 93, 198, 240, 107, 156, 7, 49, 170, 68, 223, 233, 114, 133, 30, 40, 179, 195, 88, 110, 245, 2, 153, 175, 52, 218, 65, 119, 236, 27, 128, 182, 45, 241, 106, 92, 199, 48, 171, 157, 6, 232, 115, 69, 222, 41, 178, 132, 31, 167, 60, 10, 145, 102, 253, 203, 80, 190, 37, 19, 136, 127, 228, 210, 73, 149, 14, 56, 163, 84, 207, 249, 98, 140, 23, 33, 186, 77, 214, 224, 123) + + private val packages = mutableListOf() + private var index = 0 + + init { + // PackageIndex: 0 initially, if there are multiple packets, for the first packet it is set to 0 (not included in CRC calculation but sent in actual header) + var pkgIndex = 0 + var header = byteArrayOf( + (data.size + 4).toByte(), + data[0], + sequenceNumber.toByte(), + pkgIndex.toByte() + ) + + var tmp: ByteArray = header + data.copyOfRange(1, data.size) + var totalCommand: ByteArray = tmp + calcCrc8(tmp, tmp.size).toByte() + + if ((totalCommand.size - header.size) <= 15) { + packages.add(totalCommand + 0.toByte()) + } else { + pkgIndex = 1 + var remainingCommand = totalCommand.copyOfRange(4, totalCommand.size) + + while (remainingCommand.size > 15) { + header[3] = pkgIndex.toByte() + tmp = header + remainingCommand.copyOfRange(0, 15) + packages.add(tmp + calcCrc8(tmp, tmp.size).toByte()) + + remainingCommand = remainingCommand.copyOfRange(15, remainingCommand.size) + pkgIndex = (pkgIndex + 1) % 256 + } + + // Add last package + header[3] = pkgIndex.toByte() + tmp = header + remainingCommand + packages.add(tmp + calcCrc8(tmp, tmp.size).toByte()) + } + } + + fun getNextPacket(): ByteArray? { + var ret: ByteArray? = null + if (index < packages.size) { + ret = packages[index] + index++ + } + return ret + } + + fun allPacketsConsumed(): Boolean { + return !(index < packages.size) + } + + private fun calcCrc8(value: ByteArray, size: Int): Int { + var crc8 = 0 + for (i in 0 until size) { + crc8 = CRC_8_TABLE[(value[i].toInt() and 255) xor (crc8 and 255)].toInt() and 255 + } + return crc8 + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/AlarmSetting.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/AlarmSetting.kt new file mode 100644 index 0000000000..83557b4ff3 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/AlarmSetting.kt @@ -0,0 +1,12 @@ +package info.nightscout.pump.medtrum.comm.enums + +enum class AlarmSetting(val code: Byte) { + LIGHT_VIBRATE_AND_BEEP(0), + LIGHT_AND_VIBRATE(1), + LIGHT_AND_BEEP(2), + LIGHT_ONLY(3), + VIBRATE_AND_BEEP(4), + VIBRATE_ONLY(5), + BEEP_ONLY(6), + NONE(7) +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/AlarmState.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/AlarmState.kt new file mode 100644 index 0000000000..50ae4e30f6 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/AlarmState.kt @@ -0,0 +1,23 @@ +package info.nightscout.pump.medtrum.comm.enums + +enum class AlarmState { + NONE, + PUMP_LOW_BATTERY, // Mapped from error flag 1 + PUMP_LOW_RESERVOIR, // Mapped from error flag 2 + PUMP_EXPIRES_SOON, // Mapped from error flag 3 + LOWBG_SUSPENDED, // Mapped from pump status 64 + LOWBG_SUSPENDED2, // Mapped from pump status 65 + AUTO_SUSPENDED, // Mapped from pump status 66 + HMAX_SUSPENDED, // Mapped from pump status 67 + DMAX_SUSPENDED, // Mapped from pump status 68 + SUSPENDED, // Mapped from pump status 69 + PAUSED, // Mapped from pump status 70 + OCCLUSION, // Mapped from pump status 96 + EXPIRED, // Mapped from pump status 97 + RESERVOIR_EMPTY, // Mapped from pump status 98 + PATCH_FAULT, // Mapped from pump status 99 + PATCH_FAULT2, // Mapped from pump status 100 + BASE_FAULT, // Mapped from pump status 101 + BATTERY_OUT, // Mapped from pump status 102 + NO_CALIBRATION // Mapped from pump status 103 +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/BasalType.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/BasalType.kt new file mode 100644 index 0000000000..f35ff3b4ba --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/BasalType.kt @@ -0,0 +1,54 @@ +package info.nightscout.pump.medtrum.comm.enums + +enum class BasalType { + NONE, + STANDARD, + EXERCISE, + HOLIDAY, + PROGRAM_A, + PROGRAM_B, + ABSOLUTE_TEMP, + RELATIVE_TEMP, + PROGRAM_C, + PROGRAM_D, + SICK, + AUTO, + NEW, + SUSPEND_LOW_GLUCOSE, + SUSPEND_PREDICT_LOW_GLUCOSE, + SUSPEND_AUTO, + SUSPEND_MORE_THAN_MAX_PER_HOUR, + SUSPEND_MORE_THAN_MAX_PER_DAY, + SUSPEND_MANUAL, + SUSPEND_KEY_LOST, + STOP_OCCLUSION, + STOP_EXPIRED, + STOP_EMPTY, + STOP_PATCH_FAULT, + STOP_PATCH_FAULT2, + STOP_BASE_FAULT, + STOP_DISCARD, + STOP_BATTERY_EXHAUSTED, + STOP, + PAUSE_INTERRUPT, + PRIME, + AUTO_MODE_START, + AUTO_MODE_EXIT, + AUTO_MODE_TARGET_100, + AUTO_MODE_TARGET_110, + AUTO_MODE_TARGET_120, + AUTO_MODE_BREAKFAST, + AUTO_MODE_LUNCH, + AUTO_MODE_DINNER, + AUTO_MODE_SNACK, + AUTO_MODE_EXERCISE_START, + AUTO_MODE_EXERCISE_EXIT; + + fun isTempBasal(): Boolean { + return this == ABSOLUTE_TEMP || this == RELATIVE_TEMP + } + + fun isSuspendedByPump(): Boolean { + return this in SUSPEND_LOW_GLUCOSE..STOP + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/BolusType.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/BolusType.kt new file mode 100644 index 0000000000..7bf88c9f53 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/BolusType.kt @@ -0,0 +1,8 @@ +package info.nightscout.pump.medtrum.comm.enums + +enum class BolusType { + NONE, + NORMAL, + EXTENDED, + COMBI; +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/CommandType.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/CommandType.kt new file mode 100644 index 0000000000..035174e815 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/CommandType.kt @@ -0,0 +1,26 @@ +package info.nightscout.pump.medtrum.comm.enums + +enum class CommandType(val code: Byte) { + SYNCHRONIZE(3), + SUBSCRIBE(4), + AUTH_REQ(5), + GET_DEVICE_TYPE(6), + SET_TIME(10), + GET_TIME(11), + SET_TIME_ZONE(12), + PRIME(16), + ACTIVATE(18), + SET_BOLUS(19), + CANCEL_BOLUS(20), + SET_BASAL_PROFILE(21), + SET_TEMP_BASAL(24), + CANCEL_TEMP_BASAL(25), + RESUME_PUMP(29), + POLL_PATCH(30), + STOP_PATCH(31), + READ_BOLUS_STATE(34), + SET_PATCH(35), + SET_BOLUS_MOTOR(36), + GET_RECORD(99), + CLEAR_ALARM(115) +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/MedtrumPumpState.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/MedtrumPumpState.kt new file mode 100644 index 0000000000..b52b9c1793 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/enums/MedtrumPumpState.kt @@ -0,0 +1,34 @@ +package info.nightscout.pump.medtrum.comm.enums + +enum class MedtrumPumpState(val state: Byte) { + NONE(0), + IDLE(1), + FILLED(2), + PRIMING(3), + PRIMED(4), + EJECTING(5), + EJECTED(6), + ACTIVE(32), + ACTIVE_ALT(33), + LOWBG_SUSPENDED(64), + LOWBG_SUSPENDED2(65), + AUTO_SUSPENDED(66), + HMAX_SUSPENDED(67), + DMAX_SUSPENDED(68), + SUSPENDED(69), + PAUSED(70), + OCCLUSION(96), + EXPIRED(97), + RESERVOIR_EMPTY(98), + PATCH_FAULT(99), + PATCH_FAULT2(100), + BASE_FAULT(101), + BATTERY_OUT(102), + NO_CALIBRATION(103), + STOPPED(128.toByte()); + + companion object { + fun fromByte(state: Byte) = values().find { it.state == state } + ?: throw IllegalAccessException("") + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ActivatePacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ActivatePacket.kt new file mode 100644 index 0000000000..838fff9727 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ActivatePacket.kt @@ -0,0 +1,122 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.interfaces.pump.DetailedBolusInfo +import info.nightscout.interfaces.pump.PumpSync +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.comm.enums.CommandType.ACTIVATE +import info.nightscout.pump.medtrum.comm.enums.BasalType +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.extension.toByte +import info.nightscout.interfaces.stats.TddCalculator +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.pump.medtrum.extension.toLong +import info.nightscout.pump.medtrum.util.MedtrumTimeUtil +import javax.inject.Inject +import kotlin.math.round + +class ActivatePacket(injector: HasAndroidInjector, private val basalProfile: ByteArray) : MedtrumPacket(injector) { + + @Inject lateinit var medtrumPump: MedtrumPump + @Inject lateinit var tddCalculator: TddCalculator + @Inject lateinit var pumpSync: PumpSync + + companion object { + + private const val RESP_PATCH_ID_START = 6 + private const val RESP_PATCH_ID_END = RESP_PATCH_ID_START + 4 + private const val RESP_TIME_START = 10 + private const val RESP_TIME_END = RESP_TIME_START + 4 + private const val RESP_BASAL_TYPE_START = 14 + private const val RESP_BASAL_TYPE_END = RESP_BASAL_TYPE_START + 1 + private const val RESP_BASAL_VALUE_START = 15 + private const val RESP_BASAL_VALUE_END = RESP_BASAL_VALUE_START + 2 + private const val RESP_BASAL_SEQUENCE_START = 17 + private const val RESP_BASAL_SEQUENCE_END = RESP_BASAL_SEQUENCE_START + 2 + private const val RESP_BASAL_PATCH_ID_START = 19 + private const val RESP_BASAL_PATCH_ID_END = RESP_BASAL_PATCH_ID_START + 2 + private const val RESP_BASAL_START_TIME_START = 21 + private const val RESP_BASAL_START_TIME_END = RESP_BASAL_START_TIME_START + 4 + } + + init { + opCode = ACTIVATE.code + expectedMinRespLength = RESP_BASAL_START_TIME_END + } + + override fun getRequest(): ByteArray { + /** + * byte 0: opCode + * byte 1: autoSuspendEnable // Value for auto mode, not used for AAPS + * byte 2: autoSuspendTime // Value for auto mode, not used for AAPS + * byte 3: expirationTimer // Expiration timer, 0 = no expiration 1 = 12 hour reminder and expiration after 3 days + * byte 4: alarmSetting // See AlarmSetting + * byte 5: lowSuspend // Value for auto mode, not used for AAPS + * byte 6: predictiveLowSuspend // Value for auto mode, not used for AAPS + * byte 7: predictiveLowSuspendRange // Value for auto mode, not used for AAPS + * byte 8-9: hourlyMaxInsulin // Max hourly dose of insulin, divided by 0.05 + * byte 10-11: daylyMaxSet // Max daily dose of insulin, divided by 0.05 + * byte 12-13: tddToday // Current TDD (of present day), divided by 0.05 + * byte 14: 1 // Always 1 + * bytes 15 - end // Basal profile > see MedtrumPump + */ + + val autoSuspendEnable: Byte = 0 + val autoSuspendTime: Byte = 12 // Not sure why, but pump needs this in order to activate + + val patchExpiration: Byte = medtrumPump.desiredPatchExpiration.toByte() + val alarmSetting: Byte = medtrumPump.desiredAlarmSetting.code + + val lowSuspend: Byte = 0 + val predictiveLowSuspend: Byte = 0 + val predictiveLowSuspendRange: Byte = 30 // Not sure why, but pump needs this in order to activate + + val hourlyMaxInsulin: Int = round(medtrumPump.desiredHourlyMaxInsulin / 0.05).toInt() + val dailyMaxInsulin: Int = round(medtrumPump.desiredDailyMaxInsulin / 0.05).toInt() + val currentTDD: Double = tddCalculator.calculateToday()?.totalAmount?.div(0.05) ?: 0.0 + + return byteArrayOf(opCode) + autoSuspendEnable + autoSuspendTime + patchExpiration + alarmSetting + lowSuspend + predictiveLowSuspend + predictiveLowSuspendRange + hourlyMaxInsulin.toByteArray( + 2 + ) + dailyMaxInsulin.toByteArray(2) + currentTDD.toInt().toByteArray(2) + 1.toByte() + basalProfile + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + val medtrumTimeUtil = MedtrumTimeUtil() + + val patchId = data.copyOfRange(RESP_PATCH_ID_START, RESP_PATCH_ID_END).toLong() + val time = medtrumTimeUtil.convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_TIME_START, RESP_TIME_END).toLong()) + val basalType = enumValues()[data.copyOfRange(RESP_BASAL_TYPE_START, RESP_BASAL_TYPE_END).toInt()] + val basalValue = data.copyOfRange(RESP_BASAL_VALUE_START, RESP_BASAL_VALUE_END).toInt() * 0.05 + val basalSequence = data.copyOfRange(RESP_BASAL_SEQUENCE_START, RESP_BASAL_SEQUENCE_END).toInt() + val basalPatchId = data.copyOfRange(RESP_BASAL_PATCH_ID_START, RESP_BASAL_PATCH_ID_END).toLong() + val basalStartTime = medtrumTimeUtil.convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_BASAL_START_TIME_START, RESP_BASAL_START_TIME_END).toLong()) + + medtrumPump.patchId = patchId + medtrumPump.lastTimeReceivedFromPump = time + medtrumPump.currentSequenceNumber = basalSequence // We are activated, set the new seq nr + medtrumPump.syncedSequenceNumber = basalSequence // We are activated, reset the synced seq nr () + + // Sync canula change + pumpSync.insertTherapyEventIfNewWithTimestamp( + timestamp = System.currentTimeMillis(), + type = DetailedBolusInfo.EventType.CANNULA_CHANGE, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + pumpSync.insertTherapyEventIfNewWithTimestamp( + timestamp = System.currentTimeMillis(), + type = DetailedBolusInfo.EventType.INSULIN_CHANGE, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + + // Update the actual basal profile + medtrumPump.actualBasalProfile = basalProfile + medtrumPump.handleBasalStatusUpdate(basalType, basalValue, basalSequence, basalPatchId, basalStartTime, time) + } + + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/AuthorizePacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/AuthorizePacket.kt new file mode 100644 index 0000000000..fce1d8c869 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/AuthorizePacket.kt @@ -0,0 +1,57 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.comm.enums.CommandType.AUTH_REQ +import info.nightscout.pump.medtrum.encryption.Crypt +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.rx.logging.LTag +import javax.inject.Inject + +class AuthorizePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + @Inject lateinit var medtrumPump: MedtrumPump + + companion object { + + private const val RESP_DEVICE_TYPE_START = 7 + private const val RESP_DEVICE_TYPE_END = RESP_DEVICE_TYPE_START + 1 + private const val RESP_VERSION_X_START = 8 + private const val RESP_VERSION_X_END = RESP_VERSION_X_START + 1 + private const val RESP_VERSION_Y_START = 9 + private const val RESP_VERSION_Y_END = RESP_VERSION_Y_START + 1 + private const val RESP_VERSION_Z_START = 10 + private const val RESP_VERSION_Z_END = RESP_VERSION_Z_START + 1 + } + + init { + opCode = AUTH_REQ.code + expectedMinRespLength = RESP_VERSION_Z_END + } + + override fun getRequest(): ByteArray { + val role = 2 // Fixed to 2 for pump + val key = Crypt().keyGen(medtrumPump.pumpSN) + return byteArrayOf(opCode) + byteArrayOf(role.toByte()) + medtrumPump.patchSessionToken.toByteArray(4) + key.toByteArray(4) + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + val deviceType = data.copyOfRange(RESP_DEVICE_TYPE_START, RESP_DEVICE_TYPE_END).toInt() + val swVersion = "" + data.copyOfRange(RESP_VERSION_X_START, RESP_VERSION_X_END).toInt() + "." + data.copyOfRange(RESP_VERSION_Y_START, RESP_VERSION_Y_END).toInt() + "." + data.copyOfRange( + RESP_VERSION_Z_START, RESP_VERSION_Z_END + ).toInt() + + if (medtrumPump.deviceType != deviceType) { + medtrumPump.deviceType = deviceType + } + if (medtrumPump.swVersion != swVersion) { + medtrumPump.swVersion = swVersion + } + aapsLogger.debug(LTag.PUMPCOMM, "GetDeviceTypeState: deviceType: ${deviceType}, swVersion: ${swVersion}") + } + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/CancelBolusPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/CancelBolusPacket.kt new file mode 100644 index 0000000000..3a2e2cbcca --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/CancelBolusPacket.kt @@ -0,0 +1,20 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.comm.enums.CommandType.CANCEL_BOLUS + +class CancelBolusPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + init { + opCode = CANCEL_BOLUS.code + } + + override fun getRequest(): ByteArray { + // Bolus types: + // 1 = normal + // 2 = Extended + // 3 = Combi + val bolusType: Byte = 1 // Only support for normal bolus for now + return byteArrayOf(opCode) + bolusType + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/CancelTempBasalPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/CancelTempBasalPacket.kt new file mode 100644 index 0000000000..f6b54aaa18 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/CancelTempBasalPacket.kt @@ -0,0 +1,51 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.comm.enums.CommandType.CANCEL_TEMP_BASAL +import info.nightscout.pump.medtrum.comm.enums.BasalType +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.pump.medtrum.extension.toLong +import info.nightscout.pump.medtrum.util.MedtrumTimeUtil +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.utils.DateUtil +import javax.inject.Inject + +class CancelTempBasalPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + @Inject lateinit var medtrumPump: MedtrumPump + @Inject lateinit var dateUtil: DateUtil + + companion object { + + private const val RESP_BASAL_TYPE_START = 6 + private const val RESP_BASAL_TYPE_END = RESP_BASAL_TYPE_START + 1 + private const val RESP_BASAL_RATE_START = RESP_BASAL_TYPE_END + private const val RESP_BASAL_RATE_END = RESP_BASAL_RATE_START + 2 + private const val RESP_BASAL_SEQUENCE_START = RESP_BASAL_RATE_END + private const val RESP_BASAL_SEQUENCE_END = RESP_BASAL_SEQUENCE_START + 2 + private const val RESP_BASAL_PATCH_ID_START = RESP_BASAL_SEQUENCE_END + private const val RESP_BASAL_PATCH_ID_END = RESP_BASAL_PATCH_ID_START + 2 + private const val RESP_BASAL_START_TIME_START = RESP_BASAL_PATCH_ID_END + private const val RESP_BASAL_START_TIME_END = RESP_BASAL_START_TIME_START + 4 + } + + init { + opCode = CANCEL_TEMP_BASAL.code + expectedMinRespLength = RESP_BASAL_START_TIME_END + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + val basalType = enumValues()[data.copyOfRange(RESP_BASAL_TYPE_START, RESP_BASAL_TYPE_END).toInt()] + val basalRate = data.copyOfRange(RESP_BASAL_RATE_START, RESP_BASAL_RATE_END).toInt() * 0.05 + val basalSequence = data.copyOfRange(RESP_BASAL_SEQUENCE_START, RESP_BASAL_SEQUENCE_END).toInt() + val basalPatchId = data.copyOfRange(RESP_BASAL_PATCH_ID_START, RESP_BASAL_PATCH_ID_END).toLong() + val basalStartTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_BASAL_START_TIME_START, RESP_BASAL_START_TIME_END).toLong()) + + medtrumPump.handleBasalStatusUpdate(basalType, basalRate, basalSequence, basalPatchId, basalStartTime) + } + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ClearPumpAlarmPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ClearPumpAlarmPacket.kt new file mode 100644 index 0000000000..15f860fc50 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ClearPumpAlarmPacket.kt @@ -0,0 +1,16 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.comm.enums.CommandType.CLEAR_ALARM +import info.nightscout.pump.medtrum.extension.toByteArray + +class ClearPumpAlarmPacket(injector: HasAndroidInjector, val clearType: Int) : MedtrumPacket(injector) { + + init { + opCode = CLEAR_ALARM.code + } + + override fun getRequest(): ByteArray { + return byteArrayOf(opCode) + clearType.toByteArray(2) + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/GetDeviceTypePacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/GetDeviceTypePacket.kt new file mode 100644 index 0000000000..22de4d50c3 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/GetDeviceTypePacket.kt @@ -0,0 +1,35 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.comm.enums.CommandType.GET_DEVICE_TYPE +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.pump.medtrum.extension.toLong + +class GetDeviceTypePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + var deviceType: Int = 0 + var deviceSN: Long = 0 + + companion object { + + private const val RESP_DEVICE_TYPE_START = 6 + private const val RESP_DEVICE_TYPE_END = RESP_DEVICE_TYPE_START + 1 + private const val RESP_DEVICE_SN_START = 7 + private const val RESP_DEVICE_SN_END = RESP_DEVICE_SN_START + 4 + } + + init { + opCode = GET_DEVICE_TYPE.code + expectedMinRespLength = RESP_DEVICE_SN_END + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + deviceType = data.copyOfRange(RESP_DEVICE_TYPE_START, RESP_DEVICE_TYPE_END).toInt() + deviceSN = data.copyOfRange(RESP_DEVICE_SN_START, RESP_DEVICE_SN_END).toLong() + } + + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/GetRecordPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/GetRecordPacket.kt new file mode 100644 index 0000000000..ad127fed14 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/GetRecordPacket.kt @@ -0,0 +1,353 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.interfaces.pump.DetailedBolusInfoStorage +import info.nightscout.interfaces.pump.PumpSync +import info.nightscout.interfaces.pump.TemporaryBasalStorage +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.comm.enums.CommandType.GET_RECORD +import info.nightscout.pump.medtrum.comm.enums.BasalType +import info.nightscout.pump.medtrum.comm.enums.BolusType +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.extension.toFloat +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.pump.medtrum.extension.toLong +import info.nightscout.pump.medtrum.util.MedtrumTimeUtil +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.utils.DateUtil +import info.nightscout.shared.utils.T +import javax.inject.Inject + +class GetRecordPacket(injector: HasAndroidInjector, private val recordIndex: Int) : MedtrumPacket(injector) { + + @Inject lateinit var medtrumPump: MedtrumPump + @Inject lateinit var pumpSync: PumpSync + @Inject lateinit var temporaryBasalStorage: TemporaryBasalStorage + @Inject lateinit var detailedBolusInfoStorage: DetailedBolusInfoStorage + @Inject lateinit var dateUtil: DateUtil + + companion object { + + private const val RESP_RECORD_HEADER_START = 6 + private const val RESP_RECORD_HEADER_END = RESP_RECORD_HEADER_START + 1 + private const val RESP_RECORD_UNKNOWN_START = RESP_RECORD_HEADER_END + private const val RESP_RECORD_UNKNOWN_END = RESP_RECORD_UNKNOWN_START + 1 + private const val RESP_RECORD_TYPE_START = RESP_RECORD_UNKNOWN_END + private const val RESP_RECORD_TYPE_END = RESP_RECORD_TYPE_START + 1 + private const val RESP_RECORD_UNKNOWN1_START = RESP_RECORD_TYPE_END + private const val RESP_RECORD_UNKNOWN1_END = RESP_RECORD_UNKNOWN1_START + 1 + private const val RESP_RECORD_SERIAL_START = RESP_RECORD_UNKNOWN1_END + private const val RESP_RECORD_SERIAL_END = RESP_RECORD_SERIAL_START + 4 + private const val RESP_RECORD_PATCHID_START = RESP_RECORD_SERIAL_END + private const val RESP_RECORD_PATCHID_END = RESP_RECORD_PATCHID_START + 2 + private const val RESP_RECORD_SEQUENCE_START = RESP_RECORD_PATCHID_END + private const val RESP_RECORD_SEQUENCE_END = RESP_RECORD_SEQUENCE_START + 2 + private const val RESP_RECORD_DATA_START = RESP_RECORD_SEQUENCE_END + + private const val VALID_HEADER = 170 + private const val BOLUS_RECORD = 1 + private const val BOLUS_RECORD_ALT = 65 + private const val BASAL_RECORD = 2 + private const val BASAL_RECORD_ALT = 66 + private const val ALARM_RECORD = 3 + private const val AUTO_RECORD = 4 + private const val TIME_SYNC_RECORD = 5 + private const val AUTO1_RECORD = 6 + private const val AUTO2_RECORD = 7 + private const val AUTO3_RECORD = 8 + private const val TDD_RECORD = 9 + + } + + init { + opCode = GET_RECORD.code + expectedMinRespLength = RESP_RECORD_DATA_START + } + + override fun getRequest(): ByteArray { + return byteArrayOf(opCode) + recordIndex.toByteArray(2) + medtrumPump.patchId.toByteArray(2) + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + val recordHeader = data.copyOfRange(RESP_RECORD_HEADER_START, RESP_RECORD_HEADER_END).toInt() + val recordUnknown = data.copyOfRange(RESP_RECORD_UNKNOWN_START, RESP_RECORD_UNKNOWN_END).toInt() + val recordType = data.copyOfRange(RESP_RECORD_TYPE_START, RESP_RECORD_TYPE_END).toInt() + val recordSerial = data.copyOfRange(RESP_RECORD_SERIAL_START, RESP_RECORD_SERIAL_END).toLong() + val recordPatchId = data.copyOfRange(RESP_RECORD_PATCHID_START, RESP_RECORD_PATCHID_END).toInt() + val recordSequence = data.copyOfRange(RESP_RECORD_SEQUENCE_START, RESP_RECORD_SEQUENCE_END).toInt() + + aapsLogger.debug( + LTag.PUMPCOMM, + "GetRecordPacket HandleResponse: Record header: $recordHeader, unknown: $recordUnknown, type: $recordType, serial: $recordSerial, patchId: $recordPatchId, " + "sequence: $recordSequence" + ) + + medtrumPump.syncedSequenceNumber = recordSequence // Assume sync upwards + + if (recordHeader == VALID_HEADER) { + when (recordType) { + BOLUS_RECORD, BOLUS_RECORD_ALT -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BOLUS_RECORD") + val typeAndWizard = data.copyOfRange(RESP_RECORD_DATA_START, RESP_RECORD_DATA_START + 1).toInt() + val bolusCause = data.copyOfRange(RESP_RECORD_DATA_START + 1, RESP_RECORD_DATA_START + 2).toInt() + val unknown = data.copyOfRange(RESP_RECORD_DATA_START + 2, RESP_RECORD_DATA_START + 4).toInt() + val bolusStartTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_RECORD_DATA_START + 4, RESP_RECORD_DATA_START + 8).toLong()) + val bolusNormalAmount = data.copyOfRange(RESP_RECORD_DATA_START + 8, RESP_RECORD_DATA_START + 10).toInt() * 0.05 + val bolusNormalDelivered = data.copyOfRange(RESP_RECORD_DATA_START + 10, RESP_RECORD_DATA_START + 12).toInt() * 0.05 + val bolusExtendedAmount = data.copyOfRange(RESP_RECORD_DATA_START + 12, RESP_RECORD_DATA_START + 14).toInt() * 0.05 + val bolusExtendedDuration = data.copyOfRange(RESP_RECORD_DATA_START + 14, RESP_RECORD_DATA_START + 16).toLong() * 1000 + val bolusExtendedDelivered = data.copyOfRange(RESP_RECORD_DATA_START + 16, RESP_RECORD_DATA_START + 18).toInt() * 0.05 + val bolusCarb = data.copyOfRange(RESP_RECORD_DATA_START + 18, RESP_RECORD_DATA_START + 20).toInt() + val bolusGlucose = data.copyOfRange(RESP_RECORD_DATA_START + 20, RESP_RECORD_DATA_START + 22).toInt() + val bolusIOB = data.copyOfRange(RESP_RECORD_DATA_START + 22, RESP_RECORD_DATA_START + 24).toInt() + val unkown1 = data.copyOfRange(RESP_RECORD_DATA_START + 24, RESP_RECORD_DATA_START + 26).toInt() + val unkown2 = data.copyOfRange(RESP_RECORD_DATA_START + 26, RESP_RECORD_DATA_START + 28).toInt() + val bolusType = enumValues()[typeAndWizard and 0x0F] + val bolusWizard = (typeAndWizard and 0xF0) != 0 + aapsLogger.debug( + LTag.PUMPCOMM, + "GetRecordPacket HandleResponse: BOLUS_RECORD: typeAndWizard: $typeAndWizard, bolusCause: $bolusCause, unknown: $unknown, bolusStartTime: $bolusStartTime, " + "bolusNormalAmount: $bolusNormalAmount, bolusNormalDelivered: $bolusNormalDelivered, bolusExtendedAmount: $bolusExtendedAmount, bolusExtendedDuration: $bolusExtendedDuration, " + "bolusExtendedDelivered: $bolusExtendedDelivered, bolusCarb: $bolusCarb, bolusGlucose: $bolusGlucose, bolusIOB: $bolusIOB, unkown1: $unkown1, unkown2: $unkown2, " + "bolusType: $bolusType, bolusWizard: $bolusWizard" + ) + + if (bolusType == BolusType.NORMAL) { + val detailedBolusInfo = detailedBolusInfoStorage.findDetailedBolusInfo(bolusStartTime, bolusNormalDelivered) + var newRecord = false + if (detailedBolusInfo != null) { + val syncOk = pumpSync.syncBolusWithTempId( + timestamp = bolusStartTime, + amount = bolusNormalDelivered, + temporaryId = detailedBolusInfo.timestamp, + type = detailedBolusInfo.bolusType, + pumpId = bolusStartTime, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + if (syncOk == false) { + aapsLogger.warn(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BOLUS_RECORD: Failed to sync bolus with tempId: ${detailedBolusInfo.timestamp}") + // detailedInfo can be from another similar record. Reinsert + detailedBolusInfoStorage.add(detailedBolusInfo) + } + } else { + newRecord = pumpSync.syncBolusWithPumpId( + timestamp = bolusStartTime, + amount = bolusNormalDelivered, + type = null, + pumpId = bolusStartTime, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + } + + aapsLogger.debug( + LTag.PUMPCOMM, + "from record: ${if (newRecord) "**NEW** " else ""}EVENT BOLUS ${dateUtil.dateAndTimeString(bolusStartTime)} ($bolusStartTime) Bolus: ${bolusNormalDelivered}U " + ) + if (bolusStartTime > medtrumPump.lastBolusTime) { + medtrumPump.lastBolusTime = bolusStartTime + medtrumPump.lastBolusAmount = bolusNormalDelivered + } + } else if (bolusType == BolusType.EXTENDED) { + val newRecord = pumpSync.syncExtendedBolusWithPumpId( + timestamp = bolusStartTime, + amount = bolusExtendedDelivered, + duration = bolusExtendedDuration, + isEmulatingTB = false, + pumpId = bolusStartTime, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + aapsLogger.debug( + LTag.PUMPCOMM, + "from record: ${if (newRecord) "**NEW** " else ""}EVENT EXTENDED BOLUS ${dateUtil.dateAndTimeString(bolusStartTime)} ($bolusStartTime) Bolus: ${bolusNormalDelivered}U " + ) + } else if (bolusType == BolusType.COMBI) { + // Note, this should never happen, as we don't use combo bolus + val detailedBolusInfo = detailedBolusInfoStorage.findDetailedBolusInfo(bolusStartTime, bolusNormalDelivered) + val newRecord = pumpSync.syncBolusWithPumpId( + timestamp = bolusStartTime, + amount = bolusNormalDelivered, + type = detailedBolusInfo?.bolusType, + pumpId = bolusStartTime, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + pumpSync.syncExtendedBolusWithPumpId( + timestamp = bolusStartTime, + amount = bolusExtendedDelivered, + duration = bolusExtendedDuration, + isEmulatingTB = false, + pumpId = bolusStartTime, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + aapsLogger.error( + LTag.PUMPCOMM, + "from record: ${if (newRecord) "**NEW** " else ""}EVENT COMBI BOLUS ${dateUtil.dateAndTimeString(bolusStartTime)} ($bolusStartTime) Bolus: ${bolusNormalDelivered}U Extended: ${bolusExtendedDelivered} THIS SHOULD NOT HAPPEN!!!" + ) + if (!newRecord && detailedBolusInfo != null) { + // detailedInfo can be from another similar record. Reinsert + detailedBolusInfoStorage.add(detailedBolusInfo) + } + if (bolusStartTime > medtrumPump.lastBolusTime) { + medtrumPump.lastBolusTime = bolusStartTime + medtrumPump.lastBolusAmount = bolusNormalDelivered + } + } else { + aapsLogger.error(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BOLUS_RECORD: Unknown bolus type: $bolusType") + } + + } + + BASAL_RECORD, BASAL_RECORD_ALT -> { + val medtrumTimeUtil = MedtrumTimeUtil() + val basalStartTime = medtrumTimeUtil.convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_RECORD_DATA_START, RESP_RECORD_DATA_START + 4).toLong()) + val basalEndTime = medtrumTimeUtil.convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_RECORD_DATA_START + 4, RESP_RECORD_DATA_START + 8).toLong()) + val basalType = enumValues()[data.copyOfRange(RESP_RECORD_DATA_START + 8, RESP_RECORD_DATA_START + 9).toInt()] + val basalEndReason = data.copyOfRange(RESP_RECORD_DATA_START + 9, RESP_RECORD_DATA_START + 10).toInt() + val basalRate = data.copyOfRange(RESP_RECORD_DATA_START + 10, RESP_RECORD_DATA_START + 12).toInt() * 0.05 + val basalDelivered = data.copyOfRange(RESP_RECORD_DATA_START + 12, RESP_RECORD_DATA_START + 14).toInt() * 0.05 + val basalPercent = data.copyOfRange(RESP_RECORD_DATA_START + 14, RESP_RECORD_DATA_START + 16).toInt() + + aapsLogger.debug( + LTag.PUMPCOMM, + "GetRecordPacket HandleResponse: BASAL_RECORD: Start: $basalStartTime, End: $basalEndTime, Type: $basalType, EndReason: $basalEndReason, Rate: $basalRate, Delivered: $basalDelivered, Percent: $basalPercent" + ) + + when (basalType) { + BasalType.STANDARD -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BASAL_RECORD: Standard basal") + // If we are here it means the basal has ended + } + + BasalType.ABSOLUTE_TEMP, BasalType.RELATIVE_TEMP -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BASAL_RECORD: Absolute temp basal") + var duration = (basalEndTime - basalStartTime) + // Work around for pumpSync not accepting 0 duration. + // sometimes we get 0 duration for very short basal because the pump only reports time in seconds + if (duration < 250) duration = 250 // 250ms to make sure AAPS accepts it + + val newRecord = pumpSync.syncTemporaryBasalWithPumpId( + timestamp = basalStartTime, + rate = if (basalType == BasalType.ABSOLUTE_TEMP) basalRate else basalPercent.toDouble(), + duration = duration, + isAbsolute = (basalType == BasalType.ABSOLUTE_TEMP), + type = PumpSync.TemporaryBasalType.NORMAL, + pumpId = basalStartTime, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + aapsLogger.debug( + LTag.PUMPCOMM, + "handleBasalStatusUpdate from record: ${if (newRecord) "**NEW** " else ""}EVENT TEMP_SYNC: ($basalType) ${dateUtil.dateAndTimeString(basalStartTime)} ($basalStartTime) " + + "Rate: $basalRate Duration: ${duration}" + ) + } + + in BasalType.SUSPEND_LOW_GLUCOSE..BasalType.STOP -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BASAL_RECORD: Suspend basal") + val duration = (basalEndTime - basalStartTime) + val newRecord = pumpSync.syncTemporaryBasalWithPumpId( + timestamp = basalEndTime, + rate = 0.0, + duration = duration, + isAbsolute = true, + type = PumpSync.TemporaryBasalType.PUMP_SUSPEND, + pumpId = basalStartTime, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + aapsLogger.debug( + LTag.PUMPCOMM, + "handleBasalStatusUpdate from record: ${if (newRecord) "**NEW** " else ""}EVENT SUSPEND: ($basalType) ${dateUtil.dateAndTimeString(basalStartTime)} ($basalStartTime) " + + "Rate: $basalRate Duration: ${duration}" + ) + } + + else -> { + aapsLogger.error(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BASAL_RECORD: Unknown basal type: $basalType") + } + } + } + + ALARM_RECORD -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: ALARM_RECORD") + } + + AUTO_RECORD -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: AUTO_RECORD") + } + + TIME_SYNC_RECORD -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: TIME_SYNC_RECORD") + } + + AUTO1_RECORD -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: AUTO1_RECORD") + } + + AUTO2_RECORD -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: AUTO2_RECORD") + } + + AUTO3_RECORD -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: AUTO3_RECORD") + } + + TDD_RECORD -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: TDD_RECORD") + val timestamp = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_RECORD_DATA_START, RESP_RECORD_DATA_START + 4).toLong()) + val timeZoneOffset = data.copyOfRange(RESP_RECORD_DATA_START + 4, RESP_RECORD_DATA_START + 6).toInt() + val tddMins = data.copyOfRange(RESP_RECORD_DATA_START + 6, RESP_RECORD_DATA_START + 8).toInt() + val glucoseRecordTime = data.copyOfRange(RESP_RECORD_DATA_START + 8, RESP_RECORD_DATA_START + 12).toLong() + val tdd = data.copyOfRange(RESP_RECORD_DATA_START + 12, RESP_RECORD_DATA_START + 16).toFloat() + val basalTdd = data.copyOfRange(RESP_RECORD_DATA_START + 16, RESP_RECORD_DATA_START + 20).toFloat() + val glucose = data.copyOfRange(RESP_RECORD_DATA_START + 20, RESP_RECORD_DATA_START + 24).toFloat() + val unknown = data.copyOfRange(RESP_RECORD_DATA_START + 24, RESP_RECORD_DATA_START + 28).toFloat() + val meanSomething = data.copyOfRange(RESP_RECORD_DATA_START + 28, RESP_RECORD_DATA_START + 32).toFloat() + val usedTdd = data.copyOfRange(RESP_RECORD_DATA_START + 32, RESP_RECORD_DATA_START + 36).toFloat() + val usedIBasal = data.copyOfRange(RESP_RECORD_DATA_START + 36, RESP_RECORD_DATA_START + 40).toFloat() + val usedSgBasal = data.copyOfRange(RESP_RECORD_DATA_START + 40, RESP_RECORD_DATA_START + 44).toFloat() + val usedUMax = data.copyOfRange(RESP_RECORD_DATA_START + 44, RESP_RECORD_DATA_START + 48).toFloat() + val newTdd = data.copyOfRange(RESP_RECORD_DATA_START + 48, RESP_RECORD_DATA_START + 52).toFloat() + val newIBasal = data.copyOfRange(RESP_RECORD_DATA_START + 52, RESP_RECORD_DATA_START + 56).toFloat() + val newSgBasal = data.copyOfRange(RESP_RECORD_DATA_START + 56, RESP_RECORD_DATA_START + 60).toFloat() + val newUMax = data.copyOfRange(RESP_RECORD_DATA_START + 60, RESP_RECORD_DATA_START + 64).toFloat() + + aapsLogger.debug( + LTag.PUMPCOMM, "TDD_RECORD: timestamp: $timestamp, timeZoneOffset: $timeZoneOffset, tddMins: $tddMins, glucoseRecordTime: $glucoseRecordTime, tdd: " + + "$tdd, basalTdd: $basalTdd, glucose: $glucose, unknown: $unknown, meanSomething: $meanSomething, usedTdd: $usedTdd, usedIBasal: $usedIBasal, usedSgBasal: " + + "$usedSgBasal, usedUMax: $usedUMax, newTdd: $newTdd, newIBasal: $newIBasal, newSgBasal: $newSgBasal, newUMax: $newUMax" + ) + + val newRecord = pumpSync.createOrUpdateTotalDailyDose( + timestamp = timestamp, + bolusAmount = (tdd - basalTdd).toDouble(), + basalAmount = basalTdd.toDouble(), + totalAmount = tdd.toDouble(), + pumpId = timestamp, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + + aapsLogger.debug( + LTag.PUMPCOMM, + "handleBasalStatusUpdate from record: ${if (newRecord) "**NEW** " else ""}EVENT TDD: ${dateUtil.dateAndTimeString(timestamp)} ($timestamp) " + + "TDD: $tdd, BasalTDD: $basalTdd, BolusTDD: ${tdd - basalTdd}" + ) + + } + + else -> { + aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: Unknown record type: $recordType") + } + } + + } else { + aapsLogger.error(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: Invalid record header") + } + } + + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/GetTimePacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/GetTimePacket.kt new file mode 100644 index 0000000000..45efb5c30b --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/GetTimePacket.kt @@ -0,0 +1,34 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.comm.enums.CommandType.GET_TIME +import info.nightscout.pump.medtrum.extension.toLong +import info.nightscout.pump.medtrum.util.MedtrumTimeUtil +import javax.inject.Inject + +class GetTimePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + @Inject lateinit var medtrumPump: MedtrumPump + + companion object { + + private const val RESP_TIME_START = 6 + private const val RESP_TIME_END = RESP_TIME_START + 4 + } + + init { + opCode = GET_TIME.code + expectedMinRespLength = RESP_TIME_END + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + val time = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_TIME_START, RESP_TIME_END).toLong()) + medtrumPump.lastTimeReceivedFromPump = time + } + + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/MedtrumPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/MedtrumPacket.kt new file mode 100644 index 0000000000..6b175c06df --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/MedtrumPacket.kt @@ -0,0 +1,79 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import javax.inject.Inject + +open class MedtrumPacket(protected var injector: HasAndroidInjector) { + + @Inject lateinit var aapsLogger: AAPSLogger + + var opCode: Byte = 0 + var failed = false + var expectedMinRespLength = RESP_RESULT_END + + companion object { + + const val RESP_OPCODE_START = 1 + const val RESP_OPCODE_END = RESP_OPCODE_START + 1 + const val RESP_RESULT_START = 4 + const val RESP_RESULT_END = RESP_RESULT_START + 2 + + private const val RESP_WAITING = 16384 + } + + init { + // @Suppress("LeakingThis") + injector.androidInjector().inject(this) + } + + open fun getRequest(): ByteArray { + return byteArrayOf(opCode) + } + + /** handles a response from the Medtrum pump, returns true if command was successfull, returns false if command failed or waiting for response */ + open fun handleResponse(data: ByteArray): Boolean { + // Check for broken packets + if (RESP_RESULT_END > data.size) { + failed = true + aapsLogger.debug(LTag.PUMPCOMM, "handleResponse: Unexpected response length, expected: $expectedMinRespLength got: ${data.size}") + return false + } + + val incomingOpCode: Byte = data.copyOfRange(RESP_OPCODE_START, RESP_OPCODE_END).first() + val responseCode = data.copyOfRange(RESP_RESULT_START, RESP_RESULT_END).toInt() + + return when { + incomingOpCode != opCode -> { + failed = true + aapsLogger.error(LTag.PUMPCOMM, "handleResponse: Unexpected command, expected: $opCode got: $incomingOpCode") + false + } + + responseCode == 0 -> { + // Check if length is what is expected from this type of packet + if (expectedMinRespLength > data.size) { + failed = true + aapsLogger.debug(LTag.PUMPCOMM, "handleResponse: Unexpected response length, expected: $expectedMinRespLength got: ${data.size}") + return false + } + aapsLogger.debug(LTag.PUMPCOMM, "handleResponse: Happy command: $opCode response: $responseCode") + true + } + + responseCode == RESP_WAITING -> { + aapsLogger.debug(LTag.PUMPCOMM, "handleResponse: Waiting command: $opCode response: $responseCode") + // Waiting do nothing + false + } + + else -> { + failed = true + aapsLogger.warn(LTag.PUMPCOMM, "handleResponse: Error in command: $opCode response: $responseCode") + false + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/NotificationPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/NotificationPacket.kt new file mode 100644 index 0000000000..a37b5323cd --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/NotificationPacket.kt @@ -0,0 +1,236 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.comm.enums.AlarmState +import info.nightscout.pump.medtrum.comm.enums.BasalType +import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.pump.medtrum.extension.toLong +import info.nightscout.pump.medtrum.util.MedtrumTimeUtil +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import javax.inject.Inject + +class NotificationPacket(val injector: HasAndroidInjector) { + + /** + * This is a bit of a special packet, as it is not a command packet + * but a notification packet. It is sent by the pump to the phone + * when the pump has a notification to send. + * + * Notifications are sent regualary, regardless of the pump state. + * + * There can be multiple messages in one packet, this is noted by the fieldMask. + * + * Byte 1: State (Handle a state change directly? before analyzing further?) + * Byte 2-3: FieldMask (BitMask which tells the fields present in the message) + * Byte 4-end : status data + * + * When multiple fields are in the message, the data is concatenated. + * This kind of message can also come as a response of SynchronizePacket, + * and can be handled here by handleMaskedMessage() as well. + */ + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var medtrumPump: MedtrumPump + + companion object { + + private const val NOTIF_STATE_START = 0 + private const val NOTIF_STATE_END = NOTIF_STATE_START + 1 + + private const val MASK_SUSPEND = 0x01 + private const val MASK_NORMAL_BOLUS = 0x02 + private const val MASK_EXTENDED_BOLUS = 0x04 + private const val MASK_BASAL = 0x08 + + private const val MASK_SETUP = 0x10 + private const val MASK_RESERVOIR = 0x20 + private const val MASK_START_TIME = 0x40 + private const val MASK_BATTERY = 0x80 + + private const val MASK_STORAGE = 0x100 + private const val MASK_ALARM = 0x200 + private const val MASK_AGE = 0x400 + private const val MASK_UNKNOWN_1 = 0x800 + + private const val MASK_UNUSED_CGM = 0x1000 + private const val MASK_UNUSED_COMMAND_CONFIRM = 0x2000 + private const val MASK_UNUSED_AUTO_STATUS = 0x4000 + private const val MASK_UNUSED_LEGACY = 0x8000 + } + + init { + injector.androidInjector().inject(this) + } + + fun handleNotification(notification: ByteArray) { + val state = MedtrumPumpState.fromByte(notification[0]) + aapsLogger.debug(LTag.PUMPCOMM, "Notification state: $state, current state: ${medtrumPump.pumpState}") + + if (state != medtrumPump.pumpState) { + aapsLogger.debug(LTag.PUMPCOMM, "State changed from ${medtrumPump.pumpState} to $state") + medtrumPump.pumpState = state + } + + if (notification.size > NOTIF_STATE_END) { + handleMaskedMessage(notification.copyOfRange(NOTIF_STATE_END, notification.size)) + } + } + + /** + * Handle a message with a field mask, can be used by other packets as well + */ + fun handleMaskedMessage(data: ByteArray) { + val fieldMask = data.copyOfRange(0, 2).toInt() + var offset = 2 + + aapsLogger.debug(LTag.PUMPCOMM, "Message field mask: $fieldMask") + + if (fieldMask and MASK_SUSPEND != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Suspend notification received") + medtrumPump.suspendTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(offset, offset + 4).toLong()) + aapsLogger.debug(LTag.PUMPCOMM, "Suspend time: ${medtrumPump.suspendTime}") + offset += 4 + } + + if (fieldMask and MASK_NORMAL_BOLUS != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Normal bolus notification received") + var bolusData = data.copyOfRange(offset, offset + 1).toInt() + var bolusType = bolusData and 0x7F + val bolusCompleted: Boolean = ((bolusData shr 7) and 0x01) != 0 + var bolusDelivered = data.copyOfRange(offset + 1, offset + 3).toInt() * 0.05 + aapsLogger.debug(LTag.PUMPCOMM, "Bolus type: $bolusType, bolusData: $bolusData bolus completed: $bolusCompleted, bolus delivered: $bolusDelivered") + medtrumPump.handleBolusStatusUpdate(bolusType, bolusCompleted, bolusDelivered) + offset += 3 + } + + if (fieldMask and MASK_EXTENDED_BOLUS != 0) { + aapsLogger.error(LTag.PUMPCOMM, "Extended bolus notification received, extended bolus not supported!") + // TODO Handle error and stop pump if this happens? + offset += 3 + } + + if (fieldMask and MASK_BASAL != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Basal notification received") + val basalType = enumValues()[data.copyOfRange(offset, offset + 1).toInt()] + var basalSequence = data.copyOfRange(offset + 1, offset + 3).toInt() + var basalPatchId = data.copyOfRange(offset + 3, offset + 5).toLong() + var basalStartTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(offset + 5, offset + 9).toLong()) + var basalRateAndDelivery = data.copyOfRange(offset + 9, offset + 12).toInt() + var basalRate = (basalRateAndDelivery and 0xFFF) * 0.05 + var basalDelivery = (basalRateAndDelivery shr 12) * 0.05 + aapsLogger.debug( + LTag.PUMPCOMM, + "Basal type: $basalType, basal sequence: $basalSequence, basal patch id: $basalPatchId, basal time: $basalStartTime, basal rate: $basalRate, basal delivery: $basalDelivery" + ) + // Don't spam with basal updates here, only if the running basal rate has changed, or a new basal is set + if (medtrumPump.lastBasalRate != basalRate || medtrumPump.lastBasalStartTime != basalStartTime) { + medtrumPump.handleBasalStatusUpdate(basalType, basalRate, basalSequence, basalPatchId, basalStartTime) + } + offset += 12 + } + + if (fieldMask and MASK_SETUP != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Setup notification received") + medtrumPump.primeProgress = data.copyOfRange(offset, offset + 1).toInt() + aapsLogger.debug(LTag.PUMPCOMM, "Prime progress: ${medtrumPump.primeProgress}") + offset += 1 + } + + if (fieldMask and MASK_RESERVOIR != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Reservoir notification received") + medtrumPump.reservoir = data.copyOfRange(offset, offset + 2).toInt() * 0.05 + aapsLogger.debug(LTag.PUMPCOMM, "Reservoir: ${medtrumPump.reservoir}") + offset += 2 + } + + if (fieldMask and MASK_START_TIME != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Start time notification received") + val patchStartTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(offset, offset + 4).toLong()) + if (medtrumPump.patchStartTime != patchStartTime) { + aapsLogger.debug(LTag.PUMPCOMM, "Patch start time changed from ${medtrumPump.patchStartTime} to $patchStartTime") + medtrumPump.patchStartTime = patchStartTime + } + aapsLogger.debug(LTag.PUMPCOMM, "Patch start time: ${patchStartTime}") + offset += 4 + } + + if (fieldMask and MASK_BATTERY != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Battery notification received") + var parameter = data.copyOfRange(offset, offset + 3).toInt() + // Precision for voltage A is a guess, voltage B is the important one, threshold: < 2.64 + medtrumPump.batteryVoltage_A = (parameter and 0xFFF) / 512.0 + medtrumPump.batteryVoltage_B = (parameter shr 12) / 512.0 + aapsLogger.debug(LTag.PUMPCOMM, "Battery voltage A: ${medtrumPump.batteryVoltage_A}, battery voltage B: ${medtrumPump.batteryVoltage_B}") + offset += 3 + } + + if (fieldMask and MASK_STORAGE != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Storage notification received") + val sequence = data.copyOfRange(offset, offset + 2).toInt() + if (sequence > medtrumPump.currentSequenceNumber) { + medtrumPump.currentSequenceNumber = sequence + } + val patchId = data.copyOfRange(offset + 2, offset + 4).toLong() + if (patchId != medtrumPump.patchId) { + aapsLogger.warn(LTag.PUMPCOMM, "handleMaskedMessage: We got wrong patch id!") + } + aapsLogger.debug(LTag.PUMPCOMM, "Last known sequence number: ${medtrumPump.currentSequenceNumber}, patch id: ${patchId}") + offset += 4 + } + + if (fieldMask and MASK_ALARM != 0) { + val alarmFlags = data.copyOfRange(offset, offset + 2).toInt() + val alarmParameter = data.copyOfRange(offset + 2, offset + 4).toInt() + aapsLogger.debug(LTag.PUMPCOMM, "Alarm notification received, Alarm flags: $alarmFlags, alarm parameter: $alarmParameter") + + // If no alarm, clear activeAlarm list + if (alarmFlags == 0 && medtrumPump.activeAlarms.isNotEmpty()) { + medtrumPump.clearAlarmState() + } else if (alarmFlags != 0) { + // Check each alarm bit + for (i in 0..3) { // Only the first 3 flags are interesting for us, the rest we will get from the pump state + val alarmState = AlarmState.values()[i] + if ((alarmFlags shr i) and 1 != 0) { + // If the alarm bit is set, add the corresponding alarm to activeAlarms + medtrumPump.addAlarm(alarmState) + } else if (medtrumPump.activeAlarms.contains(alarmState)) { + // If the alarm bit is not set, and the corresponding alarm is in activeAlarms, remove it + medtrumPump.removeAlarm(alarmState) + } + } + } + offset += 4 + } + + if (fieldMask and MASK_AGE != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Age notification received") + medtrumPump.patchAge = data.copyOfRange(offset, offset + 4).toLong() + aapsLogger.debug(LTag.PUMPCOMM, "Patch age: ${medtrumPump.patchAge}") + offset += 4 + } + + if (fieldMask and MASK_UNKNOWN_1 != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Unknown 1 notification received, not handled!") + } + + if (fieldMask and MASK_UNUSED_CGM != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Unused CGM notification received, not handled!") + } + + if (fieldMask and MASK_UNUSED_COMMAND_CONFIRM != 0) { + // This one is a warning, as this happens we need to know about it, and maybe implement + aapsLogger.warn(LTag.PUMPCOMM, "Unused command confirm notification received, not handled!") + } + + if (fieldMask and MASK_UNUSED_AUTO_STATUS != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Unused auto status notification received, not handled!") + } + + if (fieldMask and MASK_UNUSED_LEGACY != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "Unused legacy notification received, not handled!") + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/PollPatchPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/PollPatchPacket.kt new file mode 100644 index 0000000000..e7adaeea2f --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/PollPatchPacket.kt @@ -0,0 +1,11 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.comm.enums.CommandType.POLL_PATCH + +class PollPatchPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + init { + opCode = POLL_PATCH.code + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/PrimePacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/PrimePacket.kt new file mode 100644 index 0000000000..70e0c12500 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/PrimePacket.kt @@ -0,0 +1,11 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.comm.enums.CommandType.PRIME + +class PrimePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + init { + opCode = PRIME.code + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ReadBolusStatePacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ReadBolusStatePacket.kt new file mode 100644 index 0000000000..4672b1d185 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ReadBolusStatePacket.kt @@ -0,0 +1,31 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.comm.enums.CommandType.READ_BOLUS_STATE + +class ReadBolusStatePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + // UNUSED + // Bolus sync is currently done by getting the records and syncing then with AAPS pumpSync there + + var bolusData: ByteArray = byteArrayOf() + + companion object { + + private const val RESP_BOLUS_DATA_START = 6 + } + + init { + opCode = READ_BOLUS_STATE.code + expectedMinRespLength = RESP_BOLUS_DATA_START + 1 + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + // UNUSED + bolusData = data.copyOfRange(RESP_BOLUS_DATA_START, data.size) + } + + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ResumePumpPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ResumePumpPacket.kt new file mode 100644 index 0000000000..aa613b3656 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/ResumePumpPacket.kt @@ -0,0 +1,16 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.comm.enums.CommandType.RESUME_PUMP +import info.nightscout.pump.medtrum.extension.toByteArray + +class ResumePumpPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + init { + opCode = RESUME_PUMP.code + } + + override fun getRequest(): ByteArray { + return byteArrayOf(opCode) + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetBasalProfilePacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetBasalProfilePacket.kt new file mode 100644 index 0000000000..88684d2096 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetBasalProfilePacket.kt @@ -0,0 +1,57 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_BASAL_PROFILE +import info.nightscout.pump.medtrum.comm.enums.BasalType +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.pump.medtrum.extension.toLong +import info.nightscout.pump.medtrum.util.MedtrumTimeUtil +import javax.inject.Inject + +class SetBasalProfilePacket(injector: HasAndroidInjector, private val basalProfile: ByteArray) : MedtrumPacket(injector) { + + @Inject lateinit var medtrumPump: MedtrumPump + + companion object { + + private const val RESP_BASAL_TYPE_START = 6 + private const val RESP_BASAL_TYPE_END = RESP_BASAL_TYPE_START + 1 + private const val RESP_BASAL_VALUE_START = 7 + private const val RESP_BASAL_VALUE_END = RESP_BASAL_VALUE_START + 2 + private const val RESP_BASAL_SEQUENCE_START = 9 + private const val RESP_BASAL_SEQUENCE_END = RESP_BASAL_SEQUENCE_START + 2 + private const val RESP_BASAL_PATCH_ID_START = 11 + private const val RESP_BASAL_PATCH_ID_END = RESP_BASAL_PATCH_ID_START + 2 + private const val RESP_BASAL_START_TIME_START = 13 + private const val RESP_BASAL_START_TIME_END = RESP_BASAL_START_TIME_START + 4 + } + + init { + opCode = SET_BASAL_PROFILE.code + expectedMinRespLength = RESP_BASAL_START_TIME_END + + } + + override fun getRequest(): ByteArray { + val basalType: Byte = 1 // Fixed to normal basal + return byteArrayOf(opCode) + basalType + basalProfile + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + val medtrumTimeUtil = MedtrumTimeUtil() + val basalType = enumValues()[data.copyOfRange(RESP_BASAL_TYPE_START, RESP_BASAL_TYPE_END).toInt()] + val basalValue = data.copyOfRange(RESP_BASAL_VALUE_START, RESP_BASAL_VALUE_END).toInt() * 0.05 + val basalSequence = data.copyOfRange(RESP_BASAL_SEQUENCE_START, RESP_BASAL_SEQUENCE_END).toInt() + val basalPatchId = data.copyOfRange(RESP_BASAL_PATCH_ID_START, RESP_BASAL_PATCH_ID_END).toLong() + val basalStartTime = medtrumTimeUtil.convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_BASAL_START_TIME_START, RESP_BASAL_START_TIME_END).toLong()) + + // Update the actual basal profile + medtrumPump.actualBasalProfile = basalProfile + medtrumPump.handleBasalStatusUpdate(basalType, basalValue, basalSequence, basalPatchId, basalStartTime) + } + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetBolusMotorPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetBolusMotorPacket.kt new file mode 100644 index 0000000000..c41d5fa1a9 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetBolusMotorPacket.kt @@ -0,0 +1,17 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_BOLUS_MOTOR + +class SetBolusMotorPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + // UNUSED in our driver + + init { + opCode = SET_BOLUS_MOTOR.code + } + + override fun getRequest(): ByteArray { + return byteArrayOf(opCode) + 0.toByte() + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetBolusPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetBolusPacket.kt new file mode 100644 index 0000000000..4bfc2f1f33 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetBolusPacket.kt @@ -0,0 +1,23 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_BOLUS +import info.nightscout.pump.medtrum.extension.toByteArray +import kotlin.math.round + +class SetBolusPacket(injector: HasAndroidInjector, private val insulin: Double) : MedtrumPacket(injector) { + + init { + opCode = SET_BOLUS.code + } + + override fun getRequest(): ByteArray { + // Bolus types: + // 1 = normal + // 2 = Extended + // 3 = Combi + val bolusType: Byte = 1 // Only support for normal bolus for now + val bolusAmount: Int = round(insulin / 0.05).toInt() + return byteArrayOf(opCode) + bolusType + bolusAmount.toByteArray(2) + 0.toByte() + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetPatchPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetPatchPacket.kt new file mode 100644 index 0000000000..c9196f5bee --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetPatchPacket.kt @@ -0,0 +1,46 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.comm.enums.AlarmSetting +import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_PATCH +import info.nightscout.pump.medtrum.extension.toByte +import info.nightscout.pump.medtrum.extension.toByteArray +import javax.inject.Inject +import kotlin.math.round + +class SetPatchPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + @Inject lateinit var medtrumPump: MedtrumPump + + init { + opCode = SET_PATCH.code + } + + override fun getRequest(): ByteArray { + /** + * byte 0: opCode + * byte 1: alarmSetting // See AlarmSetting + * byte 2-3: hourlyMaxInsulin // Max hourly dose of insulin, divided by 0.05 + * byte 4-5: dailyMaxSet // Max daily dose of insulin, divided by 0.05 + * byte 6: expirationTimer // Expiration timer, 0 = no expiration 1 = 12 hour reminder and expiration + * byte 7: autoSuspendEnable // Value for auto mode, not used for AAPS + * byte 8: autoSuspendTime // Value for auto mode, not used for AAPS + * byte 9: lowSuspend // Value for auto mode, not used for AAPS + * byte 10: predictiveLowSuspend // Value for auto mode, not used for AAPS + * byte 11: predictiveLowSuspendRange // Value for auto mode, not used for AAPS + */ + + val alarmSetting: AlarmSetting = medtrumPump.desiredAlarmSetting + val hourlyMaxInsulin: Int = round(medtrumPump.desiredHourlyMaxInsulin / 0.05).toInt() + val dailyMaxInsulin: Int = round(medtrumPump.desiredDailyMaxInsulin / 0.05).toInt() + val patchExpiration: Byte = medtrumPump.desiredPatchExpiration.toByte() + val autoSuspendEnable: Byte = 0 + val autoSuspendTime: Byte = 12 // Not sure why, but pump needs this + val lowSuspend: Byte = 0 + val predictiveLowSuspend: Byte = 0 + val predictiveLowSuspendRange: Byte = 30 // Not sure why, but pump needs this + + return byteArrayOf(opCode) + alarmSetting.code + hourlyMaxInsulin.toByteArray(2) + dailyMaxInsulin.toByteArray(2) + patchExpiration + autoSuspendEnable + autoSuspendTime + lowSuspend + predictiveLowSuspend + predictiveLowSuspendRange + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetTempBasalPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetTempBasalPacket.kt new file mode 100644 index 0000000000..8e392d2ded --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetTempBasalPacket.kt @@ -0,0 +1,67 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_TEMP_BASAL +import info.nightscout.pump.medtrum.comm.enums.BasalType +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.pump.medtrum.extension.toLong +import info.nightscout.pump.medtrum.util.MedtrumTimeUtil +import info.nightscout.rx.logging.LTag +import javax.inject.Inject +import kotlin.math.round + +class SetTempBasalPacket(injector: HasAndroidInjector, private val absoluteRate: Double, private val durationInMinutes: Int) : MedtrumPacket(injector) { + + @Inject lateinit var medtrumPump: MedtrumPump + + companion object { + + private const val RESP_BASAL_TYPE_START = 6 + private const val RESP_BASAL_TYPE_END = RESP_BASAL_TYPE_START + 1 + private const val RESP_BASAL_RATE_START = RESP_BASAL_TYPE_END + private const val RESP_BASAL_RATE_END = RESP_BASAL_RATE_START + 2 + private const val RESP_BASAL_SEQUENCE_START = RESP_BASAL_RATE_END + private const val RESP_BASAL_SEQUENCE_END = RESP_BASAL_SEQUENCE_START + 2 + private const val RESP_BASAL_PATCH_ID_START = RESP_BASAL_SEQUENCE_END + private const val RESP_BASAL_PATCH_ID_END = RESP_BASAL_PATCH_ID_START + 2 + private const val RESP_BASAL_START_TIME_START = RESP_BASAL_PATCH_ID_END + private const val RESP_BASAL_START_TIME_END = RESP_BASAL_START_TIME_START + 4 + } + + init { + opCode = SET_TEMP_BASAL.code + expectedMinRespLength = RESP_BASAL_START_TIME_END + } + + override fun getRequest(): ByteArray { + /** + * byte 0: opCode + * byte 1: tempBasalType + * byte 2-3: tempBasalRate + * byte 4-5: tempBasalDuration + */ + val tempBasalType: Byte = 6 // Fixed to temp basal value for now + val tempBasalRate: Int = round(absoluteRate / 0.05).toInt() + val tempBasalDuration: Int = durationInMinutes + return byteArrayOf(opCode) + tempBasalType + tempBasalRate.toByteArray(2) + tempBasalDuration.toByteArray(2) + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + val basalType = enumValues()[data.copyOfRange(RESP_BASAL_TYPE_START, RESP_BASAL_TYPE_END).toInt()] + val basalRate = data.copyOfRange(RESP_BASAL_RATE_START, RESP_BASAL_RATE_END).toInt() * 0.05 + val basalSequence = data.copyOfRange(RESP_BASAL_SEQUENCE_START, RESP_BASAL_SEQUENCE_END).toInt() + val basalPatchId = data.copyOfRange(RESP_BASAL_PATCH_ID_START, RESP_BASAL_PATCH_ID_END).toLong() + + val rawTime = data.copyOfRange(RESP_BASAL_START_TIME_START, RESP_BASAL_START_TIME_END).toLong() + val basalStartTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_BASAL_START_TIME_START, RESP_BASAL_START_TIME_END).toLong()) + aapsLogger.debug(LTag.PUMPCOMM, "Basal status update: type=$basalType, rate=$basalRate, sequence=$basalSequence, patchId=$basalPatchId, startTime=$basalStartTime, rawTime=$rawTime") + + medtrumPump.handleBasalStatusUpdate(basalType, basalRate, basalSequence, basalPatchId, basalStartTime) + } + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetTimePacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetTimePacket.kt new file mode 100644 index 0000000000..a7a2c7d7a8 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetTimePacket.kt @@ -0,0 +1,18 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_TIME +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.util.MedtrumTimeUtil + +class SetTimePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + init { + opCode = SET_TIME.code + } + + override fun getRequest(): ByteArray { + val time = MedtrumTimeUtil().getCurrentTimePumpSeconds() + return byteArrayOf(opCode) + 2.toByte() + time.toByteArray(4) + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetTimeZonePacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetTimeZonePacket.kt new file mode 100644 index 0000000000..129e665cf8 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SetTimeZonePacket.kt @@ -0,0 +1,36 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_TIME_ZONE +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.util.MedtrumTimeUtil +import info.nightscout.shared.utils.DateUtil +import javax.inject.Inject + +class SetTimeZonePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + @Inject lateinit var dateUtil: DateUtil + @Inject lateinit var medtrumPump: MedtrumPump + + var offsetMins: Int = 0 + + init { + opCode = SET_TIME_ZONE.code + } + + override fun getRequest(): ByteArray { + val time = MedtrumTimeUtil().getCurrentTimePumpSeconds() + offsetMins = dateUtil.getTimeZoneOffsetMinutes(dateUtil.now()) + if (offsetMins < 0) offsetMins += 65536 + return byteArrayOf(opCode) + offsetMins.toByteArray(2) + time.toByteArray(4) + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + medtrumPump.pumpTimeZoneOffset = offsetMins + } + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/StopPatchPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/StopPatchPacket.kt new file mode 100644 index 0000000000..dba47867a5 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/StopPatchPacket.kt @@ -0,0 +1,37 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.pump.medtrum.comm.enums.CommandType.STOP_PATCH +import info.nightscout.pump.medtrum.extension.toLong +import javax.inject.Inject + +class StopPatchPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + @Inject lateinit var medtrumPump: MedtrumPump + + companion object { + + private const val RESP_STOP_SEQUENCE_START = 6 + private const val RESP_STOP_SEQUENCE_END = RESP_STOP_SEQUENCE_START + 2 + private const val RESP_STOP_PATCH_ID_START = RESP_STOP_SEQUENCE_END + private const val RESP_STOP_PATCH_ID_END = RESP_STOP_PATCH_ID_START + 2 + } + + init { + opCode = STOP_PATCH.code + expectedMinRespLength = RESP_STOP_PATCH_ID_END + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + val stopSequence = data.copyOfRange(RESP_STOP_SEQUENCE_START, RESP_STOP_SEQUENCE_END).toInt() + val stopPatchId = data.copyOfRange(RESP_STOP_PATCH_ID_START, RESP_STOP_PATCH_ID_END).toLong() + + medtrumPump.handleStopStatusUpdate(stopSequence, stopPatchId) + } + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SubscribePacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SubscribePacket.kt new file mode 100644 index 0000000000..c1615a2013 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SubscribePacket.kt @@ -0,0 +1,16 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.comm.enums.CommandType.SUBSCRIBE +import info.nightscout.pump.medtrum.extension.toByteArray + +class SubscribePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + init { + opCode = SUBSCRIBE.code + } + + override fun getRequest(): ByteArray { + return byteArrayOf(opCode) + 4095.toByteArray(2) + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SynchronizePacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SynchronizePacket.kt new file mode 100644 index 0000000000..4df32dafe7 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/packets/SynchronizePacket.kt @@ -0,0 +1,74 @@ +package info.nightscout.pump.medtrum.comm.packets + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.comm.enums.CommandType.SYNCHRONIZE +import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.rx.logging.LTag +import javax.inject.Inject + +class SynchronizePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) { + + @Inject lateinit var medtrumPump: MedtrumPump + + companion object { + + private const val RESP_STATE_START = 6 + private const val RESP_STATE_END = RESP_STATE_START + 1 + private const val RESP_FIELDS_START = 7 + private const val RESP_FIELDS_END = RESP_FIELDS_START + 2 + private const val RESP_SYNC_DATA_START = 9 + + private const val MASK_SUSPEND = 0x01 + private const val MASK_NORMAL_BOLUS = 0x02 + private const val MASK_EXTENDED_BOLUS = 0x04 + } + + init { + opCode = SYNCHRONIZE.code + expectedMinRespLength = RESP_SYNC_DATA_START + 1 + } + + override fun handleResponse(data: ByteArray): Boolean { + val success = super.handleResponse(data) + if (success) { + var state = MedtrumPumpState.fromByte(data[RESP_STATE_START]) + + aapsLogger.debug(LTag.PUMPCOMM, "SynchronizePacket: state: $state") + if (state != medtrumPump.pumpState) { + aapsLogger.debug(LTag.PUMPCOMM, "State changed from ${medtrumPump.pumpState} to $state") + medtrumPump.pumpState = state + } + + var fieldMask = data.copyOfRange(RESP_FIELDS_START, RESP_FIELDS_END).toInt() + var syncData = data.copyOfRange(RESP_SYNC_DATA_START, data.size) + var offset = 0 + + if (fieldMask != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "SynchronizePacket: fieldMask: $fieldMask") + } + + // Remove bolus fields from fieldMask if fields are present (we sync bolus trough other commands) + if (fieldMask and MASK_SUSPEND != 0) { + offset += 4 // If field is present, skip 4 bytes + } + if (fieldMask and MASK_NORMAL_BOLUS != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "SynchronizePacket: Normal bolus present removing from fieldMask") + fieldMask = fieldMask and MASK_NORMAL_BOLUS.inv() + syncData = syncData.copyOfRange(0, offset) + syncData.copyOfRange(offset + 3, syncData.size) + } + if (fieldMask and MASK_EXTENDED_BOLUS != 0) { + aapsLogger.debug(LTag.PUMPCOMM, "SynchronizePacket: Extended bolus present removing from fieldMask") + fieldMask = fieldMask and MASK_EXTENDED_BOLUS.inv() + syncData = syncData.copyOfRange(0, offset) + syncData.copyOfRange(offset + 3, syncData.size) + } + + // Let the notification packet handle the rest of the sync data + NotificationPacket(injector).handleMaskedMessage(fieldMask.toByteArray(2) + syncData) + } + + return success + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumCommModule.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumCommModule.kt new file mode 100644 index 0000000000..d58d9d2f33 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumCommModule.kt @@ -0,0 +1,57 @@ +package info.nightscout.pump.medtrum.di + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import info.nightscout.pump.medtrum.comm.packets.ActivatePacket +import info.nightscout.pump.medtrum.comm.packets.AuthorizePacket +import info.nightscout.pump.medtrum.comm.packets.CancelBolusPacket +import info.nightscout.pump.medtrum.comm.packets.CancelTempBasalPacket +import info.nightscout.pump.medtrum.comm.packets.ClearPumpAlarmPacket +import info.nightscout.pump.medtrum.comm.packets.GetDeviceTypePacket +import info.nightscout.pump.medtrum.comm.packets.GetRecordPacket +import info.nightscout.pump.medtrum.comm.packets.GetTimePacket +import info.nightscout.pump.medtrum.comm.packets.MedtrumPacket +import info.nightscout.pump.medtrum.comm.packets.NotificationPacket +import info.nightscout.pump.medtrum.comm.packets.PollPatchPacket +import info.nightscout.pump.medtrum.comm.packets.PrimePacket +import info.nightscout.pump.medtrum.comm.packets.ReadBolusStatePacket +import info.nightscout.pump.medtrum.comm.packets.ResumePumpPacket +import info.nightscout.pump.medtrum.comm.packets.SetBasalProfilePacket +import info.nightscout.pump.medtrum.comm.packets.SetBolusMotorPacket +import info.nightscout.pump.medtrum.comm.packets.SetBolusPacket +import info.nightscout.pump.medtrum.comm.packets.SetPatchPacket +import info.nightscout.pump.medtrum.comm.packets.SetTempBasalPacket +import info.nightscout.pump.medtrum.comm.packets.SetTimePacket +import info.nightscout.pump.medtrum.comm.packets.SetTimeZonePacket +import info.nightscout.pump.medtrum.comm.packets.StopPatchPacket +import info.nightscout.pump.medtrum.comm.packets.SubscribePacket +import info.nightscout.pump.medtrum.comm.packets.SynchronizePacket + +@Module +abstract class MedtrumCommModule { + + @ContributesAndroidInjector abstract fun contributesActivatePacket(): ActivatePacket + @ContributesAndroidInjector abstract fun contributesAuthorizePacket(): AuthorizePacket + @ContributesAndroidInjector abstract fun contributesCancelBolusPacket(): CancelBolusPacket + @ContributesAndroidInjector abstract fun contributesCancelTempBasalPacket(): CancelTempBasalPacket + @ContributesAndroidInjector abstract fun contributesClearPumpAlarmPacket(): ClearPumpAlarmPacket + @ContributesAndroidInjector abstract fun contributesGetDeviceTypePacket(): GetDeviceTypePacket + @ContributesAndroidInjector abstract fun contributesGetRecordPacket(): GetRecordPacket + @ContributesAndroidInjector abstract fun contributesGetTimePacket(): GetTimePacket + @ContributesAndroidInjector abstract fun contributesMedtrumPacket(): MedtrumPacket + @ContributesAndroidInjector abstract fun contributesNotificationPacket(): NotificationPacket + @ContributesAndroidInjector abstract fun contributesPollPatchPacket(): PollPatchPacket + @ContributesAndroidInjector abstract fun contributesPrimePacket(): PrimePacket + @ContributesAndroidInjector abstract fun contributesReadBolusStatePacket(): ReadBolusStatePacket + @ContributesAndroidInjector abstract fun contributesResumePumpPacket(): ResumePumpPacket + @ContributesAndroidInjector abstract fun contributesSetBasalProfilePacket(): SetBasalProfilePacket + @ContributesAndroidInjector abstract fun contributesSetBolusMotorPacket(): SetBolusMotorPacket + @ContributesAndroidInjector abstract fun contributesSetBolusPacket(): SetBolusPacket + @ContributesAndroidInjector abstract fun contributesSetPatchPacket(): SetPatchPacket + @ContributesAndroidInjector abstract fun contributesSetTempBasalPacket(): SetTempBasalPacket + @ContributesAndroidInjector abstract fun contributesSetTimePacket(): SetTimePacket + @ContributesAndroidInjector abstract fun contributesSetTimeZonePacket(): SetTimeZonePacket + @ContributesAndroidInjector abstract fun contributesStopPatchPacket(): StopPatchPacket + @ContributesAndroidInjector abstract fun contributesSubscribePacket(): SubscribePacket + @ContributesAndroidInjector abstract fun contributesSynchronizePacket(): SynchronizePacket +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumInjectHelpers.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumInjectHelpers.kt new file mode 100644 index 0000000000..96118f4670 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumInjectHelpers.kt @@ -0,0 +1,12 @@ +package info.nightscout.pump.medtrum.di + +import javax.inject.Qualifier +import javax.inject.Scope + +@Qualifier +annotation class MedtrumPluginQualifier + +@MustBeDocumented +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class FragmentScope diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumModule.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumModule.kt new file mode 100644 index 0000000000..bc5c286b46 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumModule.kt @@ -0,0 +1,121 @@ +package info.nightscout.pump.medtrum.di + +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.pump.medtrum.services.MedtrumService +import info.nightscout.pump.medtrum.ui.MedtrumActivateCompleteFragment +import info.nightscout.pump.medtrum.ui.MedtrumActivateFragment +import info.nightscout.pump.medtrum.ui.MedtrumAttachPatchFragment +import info.nightscout.pump.medtrum.ui.MedtrumDeactivatePatchFragment +import info.nightscout.pump.medtrum.ui.MedtrumDeactivationCompleteFragment +import info.nightscout.pump.medtrum.ui.MedtrumPreparePatchConnectFragment +import info.nightscout.pump.medtrum.ui.MedtrumPreparePatchFragment +import info.nightscout.pump.medtrum.ui.MedtrumPrimeCompleteFragment +import info.nightscout.pump.medtrum.ui.MedtrumPrimeFragment +import info.nightscout.pump.medtrum.ui.MedtrumPrimingFragment +import info.nightscout.pump.medtrum.ui.MedtrumStartDeactivationFragment +import info.nightscout.pump.medtrum.ui.MedtrumActivity +import info.nightscout.pump.medtrum.ui.MedtrumOverviewFragment +import info.nightscout.pump.medtrum.ui.MedtrumRetryActivationConnectFragment +import info.nightscout.pump.medtrum.ui.MedtrumRetryActivationFragment +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumOverviewViewModel +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.pump.medtrum.ui.viewmodel.ViewModelFactory +import info.nightscout.pump.medtrum.ui.viewmodel.ViewModelKey +import javax.inject.Provider + +@Module(includes = [MedtrumCommModule::class]) +@Suppress("unused") +abstract class MedtrumModule { + + companion object { + + @Provides + @MedtrumPluginQualifier + fun providesViewModelFactory(@MedtrumPluginQualifier viewModels: MutableMap, @JvmSuppressWildcards Provider>): ViewModelProvider.Factory { + return ViewModelFactory(viewModels) + } + } + + // VIEW MODELS + @Binds + @IntoMap + @MedtrumPluginQualifier + @ViewModelKey(MedtrumOverviewViewModel::class) + internal abstract fun bindsMedtrumOverviewViewmodel(viewModel: MedtrumOverviewViewModel): ViewModel + + @Binds + @IntoMap + @MedtrumPluginQualifier + @ViewModelKey(MedtrumViewModel::class) + internal abstract fun bindsMedtrumViewModel(viewModel: MedtrumViewModel): ViewModel + + // FRAGMENTS + @ContributesAndroidInjector + abstract fun contributesMedtrumOverviewFragment(): MedtrumOverviewFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesStartDeactivationFragment(): MedtrumStartDeactivationFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesDeactivatePatchFragment(): MedtrumDeactivatePatchFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesDeactivationCompleteFragment(): MedtrumDeactivationCompleteFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesPreparePatchFragment(): MedtrumPreparePatchFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesPreparePatchConnectFragment(): MedtrumPreparePatchConnectFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesRetryActivationFragment(): MedtrumRetryActivationFragment + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesRetryActivationConnectFragment(): MedtrumRetryActivationConnectFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesPrimeFragment(): MedtrumPrimeFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesPrimeCompleteFragment(): MedtrumPrimeCompleteFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesPrimingFragment(): MedtrumPrimingFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesAttachPatchFragment(): MedtrumAttachPatchFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesActivateFragment(): MedtrumActivateFragment + + @FragmentScope + @ContributesAndroidInjector + internal abstract fun contributesActivateCompleteFragment(): MedtrumActivateCompleteFragment + + // ACTIVITIES + @ContributesAndroidInjector + abstract fun contributesMedtrumActivity(): MedtrumActivity + + // SERVICE + @ContributesAndroidInjector + abstract fun contributesMedtrumService(): MedtrumService + +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/encryption/Crypt.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/encryption/Crypt.kt new file mode 100644 index 0000000000..ce5126de48 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/encryption/Crypt.kt @@ -0,0 +1,74 @@ +package info.nightscout.pump.medtrum.encryption + +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.extension.toLong + +class Crypt { + + private val RIJNDEAL_S_BOX: IntArray = intArrayOf(99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22) + private val RIJNDEAL_INVERSE_S_BOX: IntArray = intArrayOf(82, 9, 106, 213, 48, 54, 165, 56, 191, 64, 163, 158, 129, 243, 215, 251, 124, 227, 57, 130, 155, 47, 255, 135, 52, 142, 67, 68, 196, 222, 233, 203, 84, 123, 148, 50, 166, 194, 35, 61, 238, 76, 149, 11, 66, 250, 195, 78, 8, 46, 161, 102, 40, 217, 36, 178, 118, 91, 162, 73, 109, 139, 209, 37, 114, 248, 246, 100, 134, 104, 152, 22, 212, 164, 92, 204, 93, 101, 182, 146, 108, 112, 72, 80, 253, 237, 185, 218, 94, 21, 70, 87, 167, 141, 157, 132, 144, 216, 171, 0, 140, 188, 211, 10, 247, 228, 88, 5, 184, 179, 69, 6, 208, 44, 30, 143, 202, 63, 15, 2, 193, 175, 189, 3, 1, 19, 138, 107, 58, 145, 17, 65, 79, 103, 220, 234, 151, 242, 207, 206, 240, 180, 230, 115, 150, 172, 116, 34, 231, 173, 53, 133, 226, 249, 55, 232, 28, 117, 223, 110, 71, 241, 26, 113, 29, 41, 197, 137, 111, 183, 98, 14, 170, 24, 190, 27, 252, 86, 62, 75, 198, 210, 121, 32, 154, 219, 192, 254, 120, 205, 90, 244, 31, 221, 168, 51, 136, 7, 199, 49, 177, 18, 16, 89, 39, 128, 236, 95, 96, 81, 127, 169, 25, 181, 74, 13, 45, 229, 122, 159, 147, 201, 156, 239, 160, 224, 59, 77, 174, 42, 245, 176, 200, 235, 187, 60, 131, 83, 153, 97, 23, 43, 4, 126, 186, 119, 214, 38, 225, 105, 20, 99, 85, 33, 12, 125) + + val MED_CIPHER: Long = 1344751489 + + fun keyGen(input: Long): Long { + val key = randomGen(randomGen(MED_CIPHER xor input)) + return simpleCrypt(key) + } + + fun generateRandomToken(): Long { + val randomBytes = ByteArray(4) + val random = java.security.SecureRandom() + random.nextBytes(randomBytes) + return randomBytes.toLong() + } + + private fun simpleCrypt(inputData: Long): Long { + var temp = inputData xor MED_CIPHER + for (i in 0 until 32) { + temp = changeByTable(rotatoLeft(temp, 32, 1), RIJNDEAL_S_BOX).toLong() + } + return temp + } + + fun simpleDecrypt(inputData: Long): Long { + var temp = inputData + for (i in 0 until 32) { + temp = rotatoRight(changeByTable(temp, RIJNDEAL_INVERSE_S_BOX), 32, 1).toLong() + } + return temp xor MED_CIPHER + } + + private fun randomGen(input: Long): Long { + val A = 16807 + val Q = 127773 + val R = 2836 + val tmp1 = input / Q + var ret = (input - (tmp1 * Q)) * A - (tmp1 * R) + if (ret < 0) { + ret += 2147483647L + } + return ret + } + + private fun changeByTable(inputData: Long, tableData: IntArray): Long { + val value = inputData.toByteArray(4) + val results = ByteArray(4) + + for (i in value.indices) { + var byte = value[i].toInt() + if (byte < 0) { + byte += 256 + } + results[i] = tableData[byte].toByte() + } + return results.toLong() + } + + private fun rotatoLeft(x: Long, s: Int, n: Int): Long { + return (x shl n) or (x ushr (s - n)) + } + + private fun rotatoRight(x: Long, s: Int, n: Int): Int { + return (x ushr n or (x shl (s - n))).toInt() + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/AppCompatActivityExtension.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/AppCompatActivityExtension.kt new file mode 100644 index 0000000000..fd4c50713c --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/AppCompatActivityExtension.kt @@ -0,0 +1,19 @@ +package info.nightscout.pump.medtrum.extension + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction + +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() +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/BoolExtension.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/BoolExtension.kt new file mode 100644 index 0000000000..35728d68fd --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/BoolExtension.kt @@ -0,0 +1,8 @@ +package info.nightscout.pump.medtrum.extension + +fun Boolean.toByte(): Byte { + return if (this == true) + 0x1 + else + 0x0 +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/ByteArrayExtension.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/ByteArrayExtension.kt new file mode 100644 index 0000000000..4e1d431086 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/ByteArrayExtension.kt @@ -0,0 +1,41 @@ +package info.nightscout.pump.medtrum.extension + +/** Extensions for different types of conversions needed when doing stuff with bytes */ +fun ByteArray.toLong(): Long { + require(this.size <= 8) { + "Array size must be <= 8 for 'toLong' conversion operation" + } + var result = 0L + for (i in this.indices) { + val byte = this[i] + val shifted = (byte.toInt() and 0xFF).toLong() shl 8 * i + result = result or shifted + } + return result +} + +fun ByteArray.toInt(): Int { + require(this.size <= 4) { + "Array size must be <= 4 for 'toInt' conversion operation" + } + var result = 0 + for (i in this.indices) { + val byte = this[i] + val shifted = (byte.toInt() and 0xFF).toInt() shl 8 * i + result = result or shifted + } + return result +} + +fun ByteArray.toFloat(): Float { + require(this.size == 4) { + "Array size must be == 4 for 'toFloat' conversion operation" + } + var asInt = 0 + for (i in this.indices) { + val byte = this[i] + val shifted = (byte.toInt() and 0xFF) shl 8 * i + asInt = asInt or shifted + } + return Float.fromBits(asInt) +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/IntExtension.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/IntExtension.kt new file mode 100644 index 0000000000..8589496e18 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/IntExtension.kt @@ -0,0 +1,10 @@ +package info.nightscout.pump.medtrum.extension + +/** Extensions for different types of conversions needed when doing stuff with bytes */ +fun Int.toByteArray(byteLength: Int): ByteArray { + val bytes = ByteArray(byteLength) + for (i in 0 until byteLength) { + bytes[i] = (this shr (i * 8) and 0xFF).toByte() + } + return bytes +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/LongExtension.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/LongExtension.kt new file mode 100644 index 0000000000..a294670ec1 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/LongExtension.kt @@ -0,0 +1,10 @@ +package info.nightscout.pump.medtrum.extension + +/** Extensions for different types of conversions needed when doing stuff with bytes */ +fun Long.toByteArray(byteLength: Int): ByteArray { + val bytes = ByteArray(byteLength) + for (i in 0 until byteLength) { + bytes[i] = (this shr (i * 8) and 0xFF).toByte() + } + return bytes +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/ViewExtension.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/ViewExtension.kt new file mode 100644 index 0000000000..5cf65baeb6 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/ViewExtension.kt @@ -0,0 +1,34 @@ +package info.nightscout.pump.medtrum.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/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt new file mode 100644 index 0000000000..b4058efd49 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt @@ -0,0 +1,475 @@ +package info.nightscout.pump.medtrum.services + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothGattService +import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothProfile +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings +import android.bluetooth.le.ScanFilter +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.os.SystemClock +import androidx.core.app.ActivityCompat +import info.nightscout.core.ui.toast.ToastUtils +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.pump.medtrum.comm.WriteCommandPackets +import info.nightscout.pump.medtrum.comm.ManufacturerData +import info.nightscout.pump.medtrum.comm.ReadDataPacket +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import java.util.UUID +import java.util.Arrays +import javax.inject.Inject +import javax.inject.Singleton + +interface BLECommCallback { + + fun onBLEConnected() + fun onBLEDisconnected() + fun onNotification(notification: ByteArray) + fun onIndication(indication: ByteArray) + fun onSendMessageError(reason: String) +} + +@Singleton +class BLEComm @Inject internal constructor( + private val aapsLogger: AAPSLogger, + private val context: Context +) { + + companion object { + + private const val WRITE_DELAY_MILLIS: Long = 10 + private const val SERVICE_UUID = "669A9001-0008-968F-E311-6050405558B3" + private const val READ_UUID = "669a9120-0008-968f-e311-6050405558b3" + private const val WRITE_UUID = "669a9101-0008-968f-e311-6050405558b3" + private const val CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb" + + private const val NEEDS_ENABLE_NOTIFICATION = 0x10 + private const val NEEDS_ENABLE_INDICATION = 0x20 + private const val NEEDS_ENABLE = 0x30 + + private const val MANUFACTURER_ID = 18305 + } + + private val handler = + Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) + private val mBluetoothAdapter: BluetoothAdapter? get() = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter + private var mBluetoothGatt: BluetoothGatt? = null + + private var isConnected = false // Only to track internal ble state + private var isConnecting = false // Only to track internal ble state + private var uartWrite: BluetoothGattCharacteristic? = null + private var uartRead: BluetoothGattCharacteristic? = null + + // Read and write buffers + private var mWritePackets: WriteCommandPackets? = null + private var mWriteSequenceNumber: Int = 0 + private var mReadPacket: ReadDataPacket? = null + private val readLock = Any() + + private var mDeviceSN: Long = 0 + private var mCallback: BLECommCallback? = null + private var mDevice: BluetoothDevice? = null + + fun setCallback(callback: BLECommCallback?) { + this.mCallback = callback + } + + /** Connect flow: 1. Start scanning for our device (SN entered in settings) */ + @SuppressLint("MissingPermission") + @Synchronized + fun startScan(): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED + ) { + ToastUtils.errorToast(context, context.getString(info.nightscout.core.ui.R.string.need_connect_permission)) + aapsLogger.error(LTag.PUMPBTCOMM, "missing permissions") + return false + } + aapsLogger.debug(LTag.PUMPBTCOMM, "Start scan!!") + val settings = ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build() + val filters = mutableListOf() + + // Find our Medtrum Device! + filters.add( + ScanFilter.Builder().setDeviceName("MT").build() + ) + mBluetoothAdapter?.bluetoothLeScanner?.startScan(filters, settings, mScanCallback) + return true + } + + @SuppressLint("MissingPermission") + @Synchronized + fun stopScan() { + mBluetoothAdapter?.bluetoothLeScanner?.stopScan(mScanCallback) + } + + @Synchronized + fun connect(from: String, deviceSN: Long): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED + ) { + ToastUtils.errorToast(context, context.getString(info.nightscout.core.ui.R.string.need_connect_permission)) + aapsLogger.error(LTag.PUMPBTCOMM, "missing permission: $from") + return false + } + aapsLogger.debug(LTag.PUMPBTCOMM, "Initializing BLEComm.") + if (mBluetoothAdapter == null) { + aapsLogger.error("Unable to obtain a BluetoothAdapter.") + return false + } + + isConnected = false + isConnecting = true + mWritePackets = null + mReadPacket = null + + if (mDevice != null && mDeviceSN == deviceSN) { + // Skip scanning and directly connect to gatt + aapsLogger.debug(LTag.PUMPBTCOMM, "Skipping scan and directly connecting to gatt") + connectGatt(mDevice!!) + } else { + // Scan for device + aapsLogger.debug(LTag.PUMPBTCOMM, "Scanning for device") + mDeviceSN = deviceSN + startScan() + } + + return true + } + + /** Connect flow: 2. When device is found this is called by onScanResult() */ + @SuppressLint("MissingPermission") + @Synchronized + private fun connectGatt(device: BluetoothDevice) { + // Reset sequence counter + mWriteSequenceNumber = 0 + if (mBluetoothGatt == null) { + mBluetoothGatt = device.connectGatt(context, false, mGattCallback, BluetoothDevice.TRANSPORT_LE) + } else { + // Already connected?, this should not happen force disconnect + aapsLogger.error(LTag.PUMPBTCOMM, "connectGatt, mBluetoothGatt is not null") + disconnect("connectGatt, mBluetoothGatt is not null") + } + } + + @SuppressLint("MissingPermission") + @Synchronized + fun disconnect(from: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED + ) { + aapsLogger.error(LTag.PUMPBTCOMM, "missing permission: $from") + return + } + aapsLogger.debug(LTag.PUMPBTCOMM, "disconnect from: $from") + if (isConnecting) { + isConnecting = false + stopScan() + SystemClock.sleep(100) + } + if (isConnected) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Connected, disconnecting") + mBluetoothGatt?.disconnect() + } else { + aapsLogger.debug(LTag.PUMPBTCOMM, "Not connected, closing gatt") + close() + isConnected = false + mCallback?.onBLEDisconnected() + } + } + + @SuppressLint("MissingPermission") + @Synchronized fun close() { + aapsLogger.debug(LTag.PUMPBTCOMM, "BluetoothAdapter close") + mBluetoothGatt?.close() + SystemClock.sleep(100) + mBluetoothGatt = null + } + + /** Scan callback */ + private val mScanCallback: ScanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + aapsLogger.debug(LTag.PUMPBTCOMM, "OnScanResult!" + result) + super.onScanResult(callbackType, result) + + val manufacturerData = + result.scanRecord?.getManufacturerSpecificData(MANUFACTURER_ID) + ?.let { ManufacturerData(it) } + + aapsLogger.debug(LTag.PUMPBTCOMM, "Found deviceSN: " + manufacturerData?.getDeviceSN()) + + if (manufacturerData?.getDeviceSN() == mDeviceSN) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Found our device! deviceSN: " + manufacturerData.getDeviceSN()) + stopScan() + mDevice = result.device + connectGatt(result.device) + } + } + + override fun onScanFailed(errorCode: Int) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Scan FAILED!") + } + } + + @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") + private val mGattCallback: BluetoothGattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + onConnectionStateChangeSynchronized(gatt, status, newState) // call it synchronized + } + + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + aapsLogger.debug(LTag.PUMPBTCOMM, "onServicesDiscovered") + if (status == BluetoothGatt.GATT_SUCCESS) { + findCharacteristic() + } + } + + override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { + aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicRead data: " + characteristic.value.contentToString() + " UUID: " + characteristic.getUuid().toString() + " status: " + status) + } + + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { + aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicChanged data: " + characteristic.value.contentToString() + " UUID: " + characteristic.getUuid().toString()) + + val value = characteristic.getValue() + if (characteristic.getUuid() == UUID.fromString(READ_UUID)) { + mCallback?.onNotification(value) + } else if (characteristic.getUuid() == UUID.fromString(WRITE_UUID)) { + synchronized(readLock) { + if (mReadPacket == null) { + mReadPacket = ReadDataPacket(value) + } else { + mReadPacket?.addData(value) + } + if (mReadPacket?.allDataReceived() == true) { + mReadPacket?.getData()?.let { mCallback?.onIndication(it) } + mReadPacket = null + } + } + } + } + + override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { + aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicWrite status = " + status) + + if (status == BluetoothGatt.GATT_SUCCESS) { + // Check if we need to finish our command! + mWritePackets?.let { + synchronized(it) { + val value: ByteArray? = mWritePackets?.getNextPacket() + if (value != null) { + writeCharacteristic(uartWriteBTGattChar, value) + } + } + } + } else { + mCallback?.onSendMessageError("onCharacteristicWrite failure") + } + } + + override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) { + super.onDescriptorWrite(gatt, descriptor, status) + aapsLogger.debug(LTag.PUMPBTCOMM, "onDescriptorWrite " + status) + if (status == BluetoothGatt.GATT_SUCCESS) { + readDescriptor(descriptor) + } + } + + /** Connect flow: 5. Notifications enabled read descriptor to verify and start auth process*/ + override fun onDescriptorRead( + gatt: BluetoothGatt, + descriptor: BluetoothGattDescriptor, + status: Int + ) { + super.onDescriptorRead(gatt, descriptor, status) + aapsLogger.debug(LTag.PUMPBTCOMM, "onDescriptorRead status: " + status) + if (status == BluetoothGatt.GATT_SUCCESS) { + checkDescriptor(descriptor) + } + } + } + + @SuppressLint("MissingPermission") + @Synchronized + private fun readDescriptor(descriptor: BluetoothGattDescriptor?) { + aapsLogger.debug(LTag.PUMPBTCOMM, "readDescriptor") + if (mBluetoothAdapter == null || mBluetoothGatt == null || descriptor == null) { + aapsLogger.error("BluetoothAdapter not initialized_ERROR") + isConnecting = false + isConnected = false + return + } + mBluetoothGatt?.readDescriptor(descriptor) + } + + @Suppress("DEPRECATION") + private fun checkDescriptor(descriptor: BluetoothGattDescriptor) { + aapsLogger.debug(LTag.PUMPBTCOMM, "checkDescriptor") + val service = getGattService() + if (mBluetoothAdapter == null || mBluetoothGatt == null || service == null) { + aapsLogger.error("BluetoothAdapter not initialized_ERROR") + isConnecting = false + isConnected = false + return + } + if (descriptor.value.toInt() > 0) { + var notificationEnabled = true + val characteristics = service.characteristics + for (j in 0 until characteristics.size) { + val configDescriptor = + characteristics[j].getDescriptor(UUID.fromString(CONFIG_UUID)) + if (configDescriptor.value == null || configDescriptor.value.toInt() <= 0) { + notificationEnabled = false + } + } + if (notificationEnabled) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Notifications enabled!") + /** Connect flow: 6. Connected */ + mCallback?.onBLEConnected() + } + } + } + + @Suppress("DEPRECATION") + @SuppressLint("MissingPermission") + @Synchronized + private fun setCharacteristicNotification(characteristic: BluetoothGattCharacteristic?, enabled: Boolean) { + aapsLogger.debug(LTag.PUMPBTCOMM, "setCharacteristicNotification") + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + aapsLogger.error("BluetoothAdapter not initialized_ERROR") + isConnecting = false + isConnected = false + return + } + mBluetoothGatt?.setCharacteristicNotification(characteristic, enabled) + characteristic?.getDescriptor(UUID.fromString(CONFIG_UUID))?.let { + if (characteristic.properties and NEEDS_ENABLE_NOTIFICATION > 0) { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + mBluetoothGatt?.writeDescriptor(it) + } else if (characteristic.properties and NEEDS_ENABLE_INDICATION > 0) { + it.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE + mBluetoothGatt?.writeDescriptor(it) + } else { + + } + } + } + + /** Connect flow: 3. When we are connected discover services*/ + @SuppressLint("MissingPermission") + @Synchronized + private fun onConnectionStateChangeSynchronized(gatt: BluetoothGatt, status: Int, newState: Int) { + aapsLogger.debug(LTag.PUMPBTCOMM, "onConnectionStateChange newState: " + newState + " status: " + status) + if (newState == BluetoothProfile.STATE_CONNECTED) { + isConnected = true + isConnecting = false + mBluetoothGatt?.discoverServices() + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + if (isConnecting) { + // When we are disconnected during connecting, we reset the device address to force a new scan + aapsLogger.warn(LTag.PUMPBTCOMM, "Disconnected while connecting! Reset device address") + mDevice = null + // Wait a bit before retrying + SystemClock.sleep(2000) + } + close() + isConnected = false + isConnecting = false + mCallback?.onBLEDisconnected() + aapsLogger.debug(LTag.PUMPBTCOMM, "Device was disconnected " + gatt.device.name) //Device was disconnected + } + } + + @Synchronized + fun sendMessage(message: ByteArray) { + aapsLogger.debug(LTag.PUMPBTCOMM, "sendMessage message = " + Arrays.toString(message)) + if (mWritePackets?.allPacketsConsumed() == false) { + aapsLogger.error(LTag.PUMPBTCOMM, "sendMessage not all packets consumed!! unable to sent message!") + return + } + mWritePackets = WriteCommandPackets(message, mWriteSequenceNumber) + mWriteSequenceNumber = (mWriteSequenceNumber + 1) % 256 + val value: ByteArray? = mWritePackets?.getNextPacket() + if (value != null) { + writeCharacteristic(uartWriteBTGattChar, value) + } else { + aapsLogger.error(LTag.PUMPBTCOMM, "sendMessage error in writePacket!") + mCallback?.onSendMessageError("error in writePacket!") + } + } + + private fun getGattService(): BluetoothGattService? { + aapsLogger.debug(LTag.PUMPBTCOMM, "getGattService") + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + aapsLogger.error("BluetoothAdapter not initialized_ERROR") + isConnecting = false + isConnected = false + return null + } + return mBluetoothGatt?.getService(UUID.fromString(SERVICE_UUID)) + } + + @Suppress("DEPRECATION") + @SuppressLint("MissingPermission") + @Synchronized + private fun writeCharacteristic(characteristic: BluetoothGattCharacteristic, data: ByteArray?) { + handler.postDelayed({ + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + aapsLogger.error("BluetoothAdapter not initialized_ERROR") + isConnecting = false + isConnected = false + } else { + characteristic.value = data + characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + aapsLogger.debug(LTag.PUMPBTCOMM, "writeCharacteristic: ${Arrays.toString(data)}") + val success = mBluetoothGatt?.writeCharacteristic(characteristic) + if (success != true) { + mCallback?.onSendMessageError("Failed to write characteristic") + } + } + }, WRITE_DELAY_MILLIS) + } + + private val uartWriteBTGattChar: BluetoothGattCharacteristic + get() = uartWrite + ?: BluetoothGattCharacteristic(UUID.fromString(WRITE_UUID), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT, 0).also { uartWrite = it } + + /** Connect flow: 4. When services are discovered find characteristics and set notifications*/ + private fun findCharacteristic() { + val gattService = getGattService() ?: return + var uuid: String + val gattCharacteristics = gattService.characteristics + for (i in 0..gattCharacteristics.size - 1) { + val gattCharacteristic = gattCharacteristics.get(i) + // Check whether read or write properties is set, the pump needs us to enable notifications on all characteristics that have these properties + if (gattCharacteristic.properties and NEEDS_ENABLE > 0) { + handler.postDelayed({ + uuid = gattCharacteristic.uuid.toString() + setCharacteristicNotification(gattCharacteristic, true) + if (READ_UUID == uuid) { + uartRead = gattCharacteristic + } + if (WRITE_UUID == uuid) { + uartWrite = gattCharacteristic + } + }, (i * 600).toLong()) + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt new file mode 100644 index 0000000000..c8d793c36c --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt @@ -0,0 +1,980 @@ +package info.nightscout.pump.medtrum.services + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Binder +import android.os.IBinder +import android.os.SystemClock +import dagger.android.DaggerService +import dagger.android.HasAndroidInjector +import info.nightscout.core.utils.fabric.FabricPrivacy +import info.nightscout.interfaces.constraints.Constraints +import info.nightscout.interfaces.notifications.Notification +import info.nightscout.interfaces.plugin.ActivePlugin +import info.nightscout.interfaces.profile.Profile +import info.nightscout.interfaces.profile.ProfileFunction +import info.nightscout.interfaces.pump.DetailedBolusInfo +import info.nightscout.interfaces.pump.DetailedBolusInfoStorage +import info.nightscout.interfaces.pump.BolusProgressData +import info.nightscout.interfaces.pump.PumpSync +import info.nightscout.interfaces.pump.defs.PumpType +import info.nightscout.interfaces.queue.Callback +import info.nightscout.interfaces.queue.CommandQueue +import info.nightscout.interfaces.ui.UiInteraction +import info.nightscout.pump.medtrum.MedtrumPlugin +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.ConnectionState +import info.nightscout.pump.medtrum.comm.enums.AlarmState +import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState +import info.nightscout.pump.medtrum.comm.packets.* +import info.nightscout.pump.medtrum.util.MedtrumTimeUtil +import info.nightscout.rx.AapsSchedulers +import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventAppExit +import info.nightscout.rx.events.EventDismissNotification +import info.nightscout.rx.events.EventOverviewBolusProgress +import info.nightscout.rx.events.EventPreferenceChange +import info.nightscout.rx.events.EventPumpStatusChanged +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import info.nightscout.shared.sharedPreferences.SP +import info.nightscout.shared.utils.DateUtil +import info.nightscout.shared.utils.T +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.round + +class MedtrumService : DaggerService(), BLECommCallback { + + @Inject lateinit var injector: HasAndroidInjector + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var aapsSchedulers: AapsSchedulers + @Inject lateinit var rxBus: RxBus + @Inject lateinit var sp: SP + @Inject lateinit var rh: ResourceHelper + @Inject lateinit var profileFunction: ProfileFunction + @Inject lateinit var commandQueue: CommandQueue + @Inject lateinit var context: Context + @Inject lateinit var medtrumPlugin: MedtrumPlugin + @Inject lateinit var medtrumPump: MedtrumPump + @Inject lateinit var activePlugin: ActivePlugin + @Inject lateinit var constraintChecker: Constraints + @Inject lateinit var uiInteraction: UiInteraction + @Inject lateinit var bleComm: BLEComm + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var pumpSync: PumpSync + @Inject lateinit var detailedBolusInfoStorage: DetailedBolusInfoStorage + @Inject lateinit var dateUtil: DateUtil + + companion object { + + private const val COMMAND_DEFAULT_TIMEOUT_SEC: Long = 60 + private const val COMMAND_SYNC_TIMEOUT_SEC: Long = 120 + private const val COMMAND_CONNECTING_TIMEOUT_SEC: Long = 30 + private const val ALARM_HOURLY_MAX_CLEAR_CODE = 4 + private const val ALARM_DAILY_MAX_CLEAR_CODE = 5 + } + + val timeUtil = MedtrumTimeUtil() + + private val disposable = CompositeDisposable() + private val mBinder: IBinder = LocalBinder() + + private var currentState: State = IdleState() + private var mPacket: MedtrumPacket? = null + + private val scope = CoroutineScope(Dispatchers.Default) + + val isConnected: Boolean + get() = medtrumPump.connectionState == ConnectionState.CONNECTED + val isConnecting: Boolean + get() = medtrumPump.connectionState == ConnectionState.CONNECTING + + override fun onCreate() { + super.onCreate() + bleComm.setCallback(this) + disposable += rxBus + .toObservable(EventAppExit::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ stopSelf() }, fabricPrivacy::logException) + disposable += rxBus + .toObservable(EventPreferenceChange::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ event -> + if (event.isChanged(rh.gs(R.string.key_sn_input))) { + aapsLogger.debug(LTag.PUMPCOMM, "Serial number changed, reporting new pump!") + pumpSync.connectNewPump() + medtrumPump.loadUserSettingsFromSP() + medtrumPump.setFakeTBRIfNeeded() + } + if (event.isChanged(rh.gs(R.string.key_alarm_setting)) + || event.isChanged(rh.gs(R.string.key_patch_expiration)) + || event.isChanged(rh.gs(R.string.key_hourly_max_insulin)) + || event.isChanged(rh.gs(R.string.key_daily_max_insulin)) + ) { + medtrumPump.loadUserSettingsFromSP() + commandQueue.setUserOptions(object : Callback() { + override fun run() { + if (medtrumPlugin.isInitialized() && this.result.success == false) { + uiInteraction.addNotification( + Notification.PUMP_SETTINGS_FAILED, + rh.gs(R.string.pump_setting_failed), + Notification.NORMAL, + ) + } + } + }) + } + }, fabricPrivacy::logException) + scope.launch { + medtrumPump.pumpStateFlow.collect { state -> + handlePumpStateUpdate(state) + } + } + scope.launch { + medtrumPump.connectionStateFlow.collect { state -> + if (medtrumPlugin.isInitialized()) { + when (state) { + ConnectionState.CONNECTED -> rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED)) + ConnectionState.DISCONNECTED -> rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED)) + ConnectionState.CONNECTING -> rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTING)) + ConnectionState.DISCONNECTING -> rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTING)) + } + } + } + } + + medtrumPump.loadUserSettingsFromSP() + } + + override fun onDestroy() { + super.onDestroy() + disposable.clear() + scope.cancel() + } + + fun connect(from: String): Boolean { + aapsLogger.debug(LTag.PUMP, "connect: called from: $from") + if (currentState is IdleState) { + medtrumPump.connectionState = ConnectionState.CONNECTING + return bleComm.connect(from, medtrumPump.pumpSN) + } else { + aapsLogger.error(LTag.PUMPCOMM, "Connect attempt when in non Idle state from: $from") + return false + } + } + + fun startPrime(): Boolean { + return sendPacketAndGetResponse(PrimePacket(injector)) + } + + fun startActivate(): Boolean { + val profile = profileFunction.getProfile()?.let { medtrumPump.buildMedtrumProfileArray(it) } + val packet = profile?.let { ActivatePacket(injector, it) } + return packet?.let { sendPacketAndGetResponse(it) } == true + } + + fun deactivatePatch(): Boolean { + var result = true + if (medtrumPump.tempBasalInProgress) { + result = sendPacketAndGetResponse(CancelTempBasalPacket(injector)) + } + // Make sure we have all events of this patch if possible + loadEvents() + if (result) result = sendPacketAndGetResponse(StopPatchPacket(injector)) + return result + } + + fun stopConnecting() { + bleComm.disconnect("stopConnecting") + } + + fun disconnect(from: String) { + medtrumPump.connectionState = ConnectionState.DISCONNECTING + bleComm.disconnect(from) + } + + fun readPumpStatus() { + rxBus.send(EventPumpStatusChanged(rh.gs(R.string.gettingpumpstatus))) + updateTimeIfNeeded(false) + loadEvents() + } + + fun timeUpdateNotification(updateSuccess: Boolean) { + if (updateSuccess) { + aapsLogger.debug(LTag.PUMPCOMM, "Pump time updated") + uiInteraction.addNotification( + Notification.INSIGHT_DATE_TIME_UPDATED, // :---) + rh.gs(info.nightscout.core.ui.R.string.pump_time_updated), + Notification.INFO, + ) + } else { + aapsLogger.error(LTag.PUMPCOMM, "Failed to update pump time") + uiInteraction.addNotification( + Notification.PUMP_TIMEZONE_UPDATE_FAILED, + rh.gs(R.string.pump_time_update_failed), + Notification.URGENT, + ) + } + } + + fun updateTimeIfNeeded(needLoadHistory: Boolean = true): Boolean { + // Note we only check timeZone here, time is updated each connection attempt if needed, because the pump requires it to be checked + // But we dont check timeZone each time, therefore we do it here (if needed) + var result = true + if (medtrumPump.pumpTimeZoneOffset != dateUtil.getTimeZoneOffsetMinutes(dateUtil.now())) { + result = sendPacketAndGetResponse(SetTimePacket(injector)) + if (result) result = sendPacketAndGetResponse(SetTimeZonePacket(injector)) + timeUpdateNotification(result) + } + // Do this here, because TBR can be cancelled due to time change by connect flow + if (needLoadHistory) { + if (result) result = loadEvents() + } + if (result) medtrumPump.needCheckTimeUpdate = false + return result + } + + fun loadEvents(): Boolean { + rxBus.send(EventPumpStatusChanged(rh.gs(R.string.gettingpumpstatus))) + // Sync records + val result = syncRecords() + if (result) { + aapsLogger.debug(LTag.PUMPCOMM, "Events loaded") + medtrumPump.lastConnection = System.currentTimeMillis() + } else { + aapsLogger.error(LTag.PUMPCOMM, "Failed to load events") + } + return result + } + + fun clearAlarms(): Boolean { + var result = true + if (medtrumPump.pumpState in listOf( + MedtrumPumpState.PAUSED, + MedtrumPumpState.HMAX_SUSPENDED, + MedtrumPumpState.DMAX_SUSPENDED + ) + ) { + when (medtrumPump.pumpState) { + MedtrumPumpState.HMAX_SUSPENDED -> { + result = sendPacketAndGetResponse(ClearPumpAlarmPacket(injector, ALARM_HOURLY_MAX_CLEAR_CODE)) + } + + MedtrumPumpState.DMAX_SUSPENDED -> { + result = sendPacketAndGetResponse(ClearPumpAlarmPacket(injector, ALARM_DAILY_MAX_CLEAR_CODE)) + } + + else -> { + // Nothing to reset + } + } + // Resume suspended pump + if (result) result = sendPacketAndGetResponse(ResumePumpPacket(injector)) + } + return result + } + + fun setUserSettings(): Boolean { + rxBus.send(EventPumpStatusChanged(rh.gs(R.string.settingpumpsettings))) + return sendPacketAndGetResponse(SetPatchPacket(injector)) + } + + fun setBolus(detailedBolusInfo: DetailedBolusInfo, t: EventOverviewBolusProgress.Treatment): Boolean { + if (!isConnected) return false + if (BolusProgressData.stopPressed) return false + val insulin = detailedBolusInfo.insulin + val bolusStart = System.currentTimeMillis() + + medtrumPump.bolusDone = false + medtrumPump.bolusingTreatment = t + medtrumPump.bolusAmountToBeDelivered = insulin + medtrumPump.bolusStopped = false + medtrumPump.bolusProgressLastTimeStamp = bolusStart + + if (insulin > 0 && !medtrumPump.bolusStopped) { + val result = sendPacketAndGetResponse(SetBolusPacket(injector, insulin)) + if (result == false) { + aapsLogger.error(LTag.PUMPCOMM, "Failed to set bolus") + commandQueue.loadEvents(null) // make sure if anything is delivered (which is highly unlikely at this point) we get it + t.insulin = 0.0 + return false + } + } else { + aapsLogger.debug(LTag.PUMPCOMM, "Bolus not set, insulin: $insulin, bolusStopped: ${medtrumPump.bolusStopped}") + t.insulin = 0.0 + return false + } + + detailedBolusInfo.timestamp = bolusStart // Make sure the timestamp is set to the start of the bolus + detailedBolusInfoStorage.add(detailedBolusInfo) // will be picked up on reading history + // Sync the initial bolus + val newRecord = pumpSync.addBolusWithTempId( + timestamp = detailedBolusInfo.timestamp, + amount = detailedBolusInfo.insulin, + temporaryId = detailedBolusInfo.timestamp, + type = detailedBolusInfo.bolusType, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + if (newRecord) { + aapsLogger.debug( + LTag.PUMPCOMM, + "set bolus: **NEW** EVENT BOLUS (tempId) ${dateUtil.dateAndTimeString(detailedBolusInfo.timestamp)} (${detailedBolusInfo.timestamp}) Bolus: ${detailedBolusInfo.insulin}U " + ) + } else { + aapsLogger.error(LTag.PUMPCOMM, "Bolus with tempId ${detailedBolusInfo.timestamp} already exists") + } + + val bolusingEvent = EventOverviewBolusProgress + var communicationLost = false + + while (!medtrumPump.bolusStopped && !medtrumPump.bolusDone && !communicationLost) { + SystemClock.sleep(100) + if (System.currentTimeMillis() - medtrumPump.bolusProgressLastTimeStamp > T.secs(20).msecs()) { + communicationLost = true + aapsLogger.warn(LTag.PUMPCOMM, "Communication stopped") + disconnect("Communication stopped") + } else { + bolusingEvent.t = medtrumPump.bolusingTreatment + bolusingEvent.status = rh.gs(info.nightscout.pump.common.R.string.bolus_delivered_so_far, medtrumPump.bolusingTreatment?.insulin, medtrumPump.bolusAmountToBeDelivered) + bolusingEvent.percent = round((medtrumPump.bolusingTreatment?.insulin?.div(medtrumPump.bolusAmountToBeDelivered) ?: 0.0) * 100).toInt() - 1 + rxBus.send(bolusingEvent) + } + } + + bolusingEvent.percent = 99 + val bolusDurationInMSec = (insulin * 60 * 1000) + val expectedEnd = bolusStart + bolusDurationInMSec + 2000 + while (System.currentTimeMillis() < expectedEnd && !medtrumPump.bolusDone) { + SystemClock.sleep(1000) + } + + bolusingEvent.t = medtrumPump.bolusingTreatment + medtrumPump.bolusingTreatment = null + + if (medtrumPump.bolusStopped && t.insulin == 0.0) { + // In this case we don't get a bolus end event, so need to remove all the stuff added previously + val syncOk = pumpSync.syncBolusWithTempId( + timestamp = bolusStart, + amount = 0.0, + temporaryId = bolusStart, + type = detailedBolusInfo.bolusType, + pumpId = bolusStart, + pumpType = medtrumPump.pumpType(), + pumpSerial = medtrumPump.pumpSN.toString(radix = 16) + ) + aapsLogger.debug( + LTag.PUMPCOMM, + "set bolus: **SYNC** EVENT BOLUS (tempId) ${dateUtil.dateAndTimeString(detailedBolusInfo.timestamp)} (${bolusStart}) Bolus: ${0.0}U SyncOK: $syncOk" + ) + // remove detailed bolus info + detailedBolusInfoStorage.findDetailedBolusInfo(bolusStart, detailedBolusInfo.insulin) + } + + // Do not call update status directly, reconnection may be needed + commandQueue.loadEvents(object : Callback() { + override fun run() { + rxBus.send(EventPumpStatusChanged(rh.gs(R.string.gettingbolusstatus))) + bolusingEvent.percent = 100 + } + }) + return true + } + + fun stopBolus() { + aapsLogger.debug(LTag.PUMPCOMM, "bolusStop >>>>> @ " + if (medtrumPump.bolusingTreatment == null) "" else medtrumPump.bolusingTreatment?.insulin) + if (isConnected) { + var success = sendPacketAndGetResponse(CancelBolusPacket(injector)) + var timeout = System.currentTimeMillis() + T.secs(30).msecs() + while (!success && System.currentTimeMillis() < timeout) { + success = sendPacketAndGetResponse(CancelBolusPacket(injector)) + SystemClock.sleep(200) + } + aapsLogger.debug(LTag.PUMPCOMM, "bolusStop success: $success") + medtrumPump.bolusStopped = true + } else { + medtrumPump.bolusStopped = true + } + } + + fun setTempBasal(absoluteRate: Double, durationInMinutes: Int): Boolean { + var result = true + if (medtrumPump.tempBasalInProgress) { + result = sendPacketAndGetResponse(CancelTempBasalPacket(injector)) + } + if (result) result = sendPacketAndGetResponse(SetTempBasalPacket(injector, absoluteRate, durationInMinutes)) + + // Get history records, this will update the prevoius basals + // Do not call update status directly, reconnection may be needed + commandQueue.loadEvents(object : Callback() { + override fun run() { + rxBus.send(EventPumpStatusChanged(rh.gs(R.string.gettingtempbasalstatus))) + } + }) + + return result + } + + fun cancelTempBasal(): Boolean { + var result = sendPacketAndGetResponse(CancelTempBasalPacket(injector)) + + // Get history records, this will update the prevoius basals + // Do not call update status directly, reconnection may be needed + commandQueue.loadEvents(object : Callback() { + override fun run() { + rxBus.send(EventPumpStatusChanged(rh.gs(R.string.gettingtempbasalstatus))) + } + }) + + return result + } + + fun updateBasalsInPump(profile: Profile): Boolean { + var result = true + // Update basal affects the TBR records (the pump will cancel the TBR, set our basal profile, and resume the TBR in a new record) + // Cancel any TBR in progress + if (medtrumPump.tempBasalInProgress) { + result = sendPacketAndGetResponse(CancelTempBasalPacket(injector)) + } + val packet = medtrumPump.buildMedtrumProfileArray(profile)?.let { SetBasalProfilePacket(injector, it) } + if (result) result = packet?.let { sendPacketAndGetResponse(it) } == true + + // Get history records, this will update the pump state and add changes in TBR to AAPS history + commandQueue.loadEvents(null) + + return result + } + + /** This gets the history records from the pump */ + private fun syncRecords(): Boolean { + aapsLogger.debug(LTag.PUMP, "syncRecords: called!, syncedSequenceNumber: ${medtrumPump.syncedSequenceNumber}, currentSequenceNumber: ${medtrumPump.currentSequenceNumber}") + var result = true + // Note: medtrum app fetches all records when they sync? + if (medtrumPump.syncedSequenceNumber < medtrumPump.currentSequenceNumber) { + for (sequence in (medtrumPump.syncedSequenceNumber + 1)..medtrumPump.currentSequenceNumber) { + result = sendPacketAndGetResponse(GetRecordPacket(injector, sequence), COMMAND_SYNC_TIMEOUT_SEC) + SystemClock.sleep(100) + if (result == false) break + } + } + return result + } + + private fun handlePumpStateUpdate(state: MedtrumPumpState) { + // Map the pump state to an alarm state and add it to the active alarms + val alarmState = when (state) { + MedtrumPumpState.NONE -> AlarmState.NONE + MedtrumPumpState.LOWBG_SUSPENDED -> AlarmState.LOWBG_SUSPENDED + MedtrumPumpState.LOWBG_SUSPENDED2 -> AlarmState.LOWBG_SUSPENDED2 + MedtrumPumpState.AUTO_SUSPENDED -> AlarmState.AUTO_SUSPENDED + MedtrumPumpState.HMAX_SUSPENDED -> AlarmState.HMAX_SUSPENDED + MedtrumPumpState.DMAX_SUSPENDED -> AlarmState.DMAX_SUSPENDED + MedtrumPumpState.SUSPENDED -> AlarmState.SUSPENDED + MedtrumPumpState.PAUSED -> AlarmState.PAUSED + MedtrumPumpState.OCCLUSION -> AlarmState.OCCLUSION + MedtrumPumpState.EXPIRED -> AlarmState.EXPIRED + MedtrumPumpState.RESERVOIR_EMPTY -> AlarmState.RESERVOIR_EMPTY + MedtrumPumpState.PATCH_FAULT -> AlarmState.PATCH_FAULT + MedtrumPumpState.PATCH_FAULT2 -> AlarmState.PATCH_FAULT2 + MedtrumPumpState.BASE_FAULT -> AlarmState.BASE_FAULT + MedtrumPumpState.BATTERY_OUT -> AlarmState.BATTERY_OUT + MedtrumPumpState.NO_CALIBRATION -> AlarmState.NO_CALIBRATION + else -> null + } + if (alarmState != null && alarmState != AlarmState.NONE) { + medtrumPump.addAlarm(alarmState) + pumpSync.insertAnnouncement( + medtrumPump.alarmStateToString(alarmState), + null, + medtrumPump.pumpType(), + medtrumPump.pumpSN.toString(radix = 16) + ) + } + + // Map the pump state to a notification + when (state) { + MedtrumPumpState.NONE, + MedtrumPumpState.STOPPED -> { + rxBus.send(EventDismissNotification(Notification.PUMP_ERROR)) + rxBus.send(EventDismissNotification(Notification.PUMP_SUSPENDED)) + uiInteraction.addNotification( + Notification.PATCH_NOT_ACTIVE, + rh.gs(R.string.patch_not_active), + Notification.URGENT, + ) + medtrumPump.setFakeTBRIfNeeded() + medtrumPump.clearAlarmState() + } + + MedtrumPumpState.IDLE, + MedtrumPumpState.FILLED, + MedtrumPumpState.PRIMING, + MedtrumPumpState.PRIMED, + MedtrumPumpState.EJECTING, + MedtrumPumpState.EJECTED -> { + rxBus.send(EventDismissNotification(Notification.PUMP_ERROR)) + rxBus.send(EventDismissNotification(Notification.PUMP_SUSPENDED)) + medtrumPump.setFakeTBRIfNeeded() + medtrumPump.clearAlarmState() + } + + MedtrumPumpState.ACTIVE, + MedtrumPumpState.ACTIVE_ALT -> { + rxBus.send(EventDismissNotification(Notification.PATCH_NOT_ACTIVE)) + rxBus.send(EventDismissNotification(Notification.PUMP_SUSPENDED)) + medtrumPump.clearAlarmState() + } + + MedtrumPumpState.LOWBG_SUSPENDED, + MedtrumPumpState.LOWBG_SUSPENDED2, + MedtrumPumpState.AUTO_SUSPENDED, + MedtrumPumpState.SUSPENDED, + MedtrumPumpState.PAUSED -> { + uiInteraction.addNotification( + Notification.PUMP_SUSPENDED, + rh.gs(R.string.pump_is_suspended), + Notification.NORMAL, + ) + // Pump will report proper TBR for this + } + + MedtrumPumpState.HMAX_SUSPENDED -> { + uiInteraction.addNotification( + Notification.PUMP_SUSPENDED, + rh.gs(R.string.pump_is_suspended_hour_max), + Notification.NORMAL, + ) + // Pump will report proper TBR for this + } + + MedtrumPumpState.DMAX_SUSPENDED -> { + uiInteraction.addNotification( + Notification.PUMP_SUSPENDED, + rh.gs(R.string.pump_is_suspended_day_max), + Notification.NORMAL, + ) + // Pump will report proper TBR for this + } + + MedtrumPumpState.OCCLUSION, + MedtrumPumpState.EXPIRED, + MedtrumPumpState.RESERVOIR_EMPTY, + MedtrumPumpState.PATCH_FAULT, + MedtrumPumpState.PATCH_FAULT2, + MedtrumPumpState.BASE_FAULT, + MedtrumPumpState.BATTERY_OUT, + MedtrumPumpState.NO_CALIBRATION -> { + rxBus.send(EventDismissNotification(Notification.PATCH_NOT_ACTIVE)) + rxBus.send(EventDismissNotification(Notification.PUMP_SUSPENDED)) + // Pump suspended due to error, show error! + uiInteraction.addNotificationWithSound( + Notification.PUMP_ERROR, + rh.gs(R.string.pump_error, alarmState?.let { medtrumPump.alarmStateToString(it) }), + Notification.URGENT, + info.nightscout.core.ui.R.raw.alarm + ) + medtrumPump.setFakeTBRIfNeeded() + } + } + } + + /** BLECommCallbacks */ + override fun onBLEConnected() { + aapsLogger.debug(LTag.PUMPCOMM, "<<<<< onBLEConnected") + currentState.onConnected() + } + + override fun onBLEDisconnected() { + aapsLogger.debug(LTag.PUMPCOMM, "<<<<< onBLEDisconnected") + currentState.onDisconnected() + } + + override fun onNotification(notification: ByteArray) { + aapsLogger.debug(LTag.PUMPCOMM, "<<<<< onNotification" + notification.contentToString()) + NotificationPacket(injector).handleNotification(notification) + } + + override fun onIndication(indication: ByteArray) { + aapsLogger.debug(LTag.PUMPCOMM, "<<<<< onIndication" + indication.contentToString()) + currentState.onIndication(indication) + } + + override fun onSendMessageError(reason: String) { + aapsLogger.debug(LTag.PUMPCOMM, "<<<<< error during send message $reason") + currentState.onSendMessageError(reason) + } + + /** Service stuff */ + inner class LocalBinder : Binder() { + + val serviceInstance: MedtrumService + get() = this@MedtrumService + } + + override fun onBind(intent: Intent): IBinder { + return mBinder + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + return Service.START_STICKY + } + + /** + * States are used to keep track of the communication and to guide the flow + */ + private fun toState(nextState: State) { + currentState = nextState + currentState.onEnter() + } + + private fun sendPacketAndGetResponse(packet: MedtrumPacket, timeout: Long = COMMAND_DEFAULT_TIMEOUT_SEC): Boolean { + var result = false + if (currentState is ReadyState) { + toState(CommandState()) + mPacket = packet + mPacket?.getRequest()?.let { bleComm.sendMessage(it) } + result = currentState.waitForResponse(timeout) + } else { + aapsLogger.error(LTag.PUMPCOMM, "Send packet attempt when in non Ready state") + } + return result + } + + // State class + private abstract inner class State { + + protected var responseHandled = false + protected var responseSuccess = false + protected var sendRetryCounter = 0 + + open fun onEnter() {} + open fun onIndication(data: ByteArray) { + aapsLogger.warn(LTag.PUMPCOMM, "onIndication: " + this.toString() + "Should not be called here!") + } + + open fun onConnected() { + aapsLogger.debug(LTag.PUMPCOMM, "onConnected") + } + + fun onDisconnected() { + aapsLogger.debug(LTag.PUMPCOMM, "onDisconnected") + medtrumPump.connectionState = ConnectionState.DISCONNECTED + responseHandled = true + responseSuccess = false + toState(IdleState()) + } + + fun waitForResponse(timeout: Long): Boolean { + val startTime = System.currentTimeMillis() + val timeoutMillis = T.secs(timeout).msecs() + while (!responseHandled) { + if (System.currentTimeMillis() - startTime > timeoutMillis) { + // If we haven't received a response in the specified time, assume the command failed + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service State timeout") + // Disconnect to cancel any outstanding commands and go back to ready state + disconnect("Timeout") + toState(IdleState()) + return false + } + SystemClock.sleep(25) + } + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service State responseHandled: $responseHandled responseSuccess: $responseSuccess") + return responseSuccess + } + + fun onSendMessageError(reason: String) { + aapsLogger.warn(LTag.PUMPCOMM, "onSendMessageError: " + this.toString() + "reason: $reason") + // Retry 3 times + if (sendRetryCounter < 3) { + sendRetryCounter++ + mPacket?.getRequest()?.let { bleComm.sendMessage(it) } + } else { + responseHandled = true + responseSuccess = false + disconnect("onSendMessageError") + toState(IdleState()) + } + } + } + + private inner class IdleState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached IdleState") + } + + override fun onConnected() { + super.onConnected() + toState(AuthState()) + } + } + + // State for connect flow + private inner class AuthState : State() { + + val retryCounter = 0 + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached AuthState") + mPacket = AuthorizePacket(injector) + mPacket?.getRequest()?.let { bleComm.sendMessage(it) } + scope.launch { + waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC) + } + } + + override fun onIndication(data: ByteArray) { + if (mPacket?.handleResponse(data) == true) { + // Succes! + responseHandled = true + responseSuccess = true + // Check if we have a supported pump + if (medtrumPump.pumpType() == PumpType.MEDTRUM_UNTESTED) { + // Throw error + aapsLogger.error(LTag.PUMPCOMM, "Unsupported pump type") + uiInteraction.addNotificationWithSound( + Notification.PUMP_ERROR, + rh.gs(R.string.pump_unsupported, medtrumPump.deviceType), + Notification.URGENT, + info.nightscout.core.ui.R.raw.alarm + ) + disconnect("Unsupported pump") + toState(IdleState()) + } else { + toState(GetDeviceTypeState()) + } + } else if (mPacket?.failed == true) { + // Failure + responseHandled = true + responseSuccess = false + disconnect("Failure") + toState(IdleState()) + } + } + } + + // State for connect flow + private inner class GetDeviceTypeState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached GetDeviceTypeState") + mPacket = GetDeviceTypePacket(injector) + mPacket?.getRequest()?.let { bleComm.sendMessage(it) } + scope.launch { + waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC) + } + } + + override fun onIndication(data: ByteArray) { + if (mPacket?.handleResponse(data) == true) { + // Succes! + responseHandled = true + responseSuccess = true + // Place holder, not really used (yet) + val deviceType = (mPacket as GetDeviceTypePacket).deviceType + val deviceSN = (mPacket as GetDeviceTypePacket).deviceSN + aapsLogger.debug(LTag.PUMPCOMM, "GetDeviceTypeState: deviceType: $deviceType deviceSN: $deviceSN") + toState(GetTimeState()) + } else if (mPacket?.failed == true) { + // Failure + responseHandled = true + responseSuccess = false + disconnect("Failure") + toState(IdleState()) + } + } + } + + // State for connect flow + private inner class GetTimeState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached GetTimeState") + mPacket = GetTimePacket(injector) + mPacket?.getRequest()?.let { bleComm.sendMessage(it) } + scope.launch { + waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC) + } + } + + override fun onIndication(data: ByteArray) { + if (mPacket?.handleResponse(data) == true) { + // Succes! + responseHandled = true + responseSuccess = true + val currTime = dateUtil.now() + aapsLogger.debug(LTag.PUMPCOMM, "GetTimeState.onIndication systemTime: $currTime, pumpTime: ${medtrumPump.lastTimeReceivedFromPump}") + if (abs(medtrumPump.lastTimeReceivedFromPump - currTime) <= T.secs(10).msecs()) { // Allow 10 sec deviation + toState(SynchronizeState()) + } else { + aapsLogger.warn(LTag.PUMPCOMM, "GetTimeState.onIndication time difference too big, setting time") + toState(SetTimeState()) + } + } else if (mPacket?.failed == true) { + // Failure + responseHandled = true + responseSuccess = false + disconnect("Failure") + toState(IdleState()) + } + } + } + + // State for connect flow + private inner class SetTimeState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SetTimeState") + mPacket = SetTimePacket(injector) + mPacket?.getRequest()?.let { bleComm.sendMessage(it) } + scope.launch { + waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC) + } + } + + override fun onIndication(data: ByteArray) { + if (mPacket?.handleResponse(data) == true) { + // Succes! + responseHandled = true + responseSuccess = true + toState(SetTimeZoneState()) + } else if (mPacket?.failed == true) { + // Failure + responseHandled = true + responseSuccess = false + disconnect("Failure") + toState(IdleState()) + } + } + } + + // State for connect flow + private inner class SetTimeZoneState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SetTimeZoneState") + mPacket = SetTimeZonePacket(injector) + mPacket?.getRequest()?.let { bleComm.sendMessage(it) } + scope.launch { + waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC) + } + } + + override fun onIndication(data: ByteArray) { + if (mPacket?.handleResponse(data) == true) { + // Succes! + responseHandled = true + responseSuccess = true + medtrumPump.needCheckTimeUpdate = false + timeUpdateNotification(true) + toState(SynchronizeState()) + } else if (mPacket?.failed == true) { + // Failure + responseHandled = true + responseSuccess = false + disconnect("Failure") + toState(IdleState()) + } + } + } + + // State for connect flow + private inner class SynchronizeState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SynchronizeState") + mPacket = SynchronizePacket(injector) + mPacket?.getRequest()?.let { bleComm.sendMessage(it) } + scope.launch { + waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC) + } + } + + override fun onIndication(data: ByteArray) { + if (mPacket?.handleResponse(data) == true) { + // Succes! + responseHandled = true + responseSuccess = true + toState(SubscribeState()) + } else if (mPacket?.failed == true) { + // Failure + responseHandled = true + responseSuccess = false + disconnect("Failure") + toState(IdleState()) + } + } + } + + // State for connect flow + private inner class SubscribeState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SubscribeState") + mPacket = SubscribePacket(injector) + mPacket?.getRequest()?.let { bleComm.sendMessage(it) } + scope.launch { + waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC) + } + } + + override fun onIndication(data: ByteArray) { + if (mPacket?.handleResponse(data) == true) { + // Succes! + responseHandled = true + responseSuccess = true + toState(ReadyState()) + } else if (mPacket?.failed == true) { + // Failure + responseHandled = true + responseSuccess = false + disconnect("Failure") + toState(IdleState()) + } + } + } + + // This state is reached when the patch is ready to receive commands (Activation, Bolus, temp basal and whatever) + private inner class ReadyState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached ReadyState!") + // Now we are fully connected and authenticated and we can start sending commands. Let AAPS know + if (isConnected == false) { + medtrumPump.connectionState = ConnectionState.CONNECTED + } + } + } + + // This state is when a command is send and we wait for a response for that command + private inner class CommandState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached CommandState") + } + + override fun onIndication(data: ByteArray) { + if (mPacket?.handleResponse(data) == true) { + // Succes! + responseHandled = true + responseSuccess = true + toState(ReadyState()) + } else if (mPacket?.failed == true) { + // Failure + responseHandled = true + responseSuccess = false + toState(ReadyState()) + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivateCompleteFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivateCompleteFragment.kt new file mode 100644 index 0000000000..b714622053 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivateCompleteFragment.kt @@ -0,0 +1,47 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.toast.ToastUtils +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumActivateCompleteBinding +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import javax.inject.Inject + +class MedtrumActivateCompleteFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rh: ResourceHelper + + companion object { + + fun newInstance(): MedtrumActivateCompleteFragment = MedtrumActivateCompleteFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_activate_complete + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + setupStep.observe(viewLifecycleOwner) { + when (it) { + MedtrumViewModel.SetupStep.INITIAL, + MedtrumViewModel.SetupStep.PRIMED -> Unit // Nothing to do here, previous state + MedtrumViewModel.SetupStep.ACTIVATED -> btnPositive.visibility = View.VISIBLE + + else -> { + ToastUtils.errorToast(requireContext(), rh.gs(R.string.unexpected_state, it.toString())) + aapsLogger.error(LTag.PUMP, "Unexpected state: $it") + } + } + } + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivateFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivateFragment.kt new file mode 100644 index 0000000000..0654dbfa64 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivateFragment.kt @@ -0,0 +1,64 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.dialogs.OKDialog +import info.nightscout.core.ui.toast.ToastUtils +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumActivateBinding +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import javax.inject.Inject + +class MedtrumActivateFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rh: ResourceHelper + + companion object { + + fun newInstance(): MedtrumActivateFragment = MedtrumActivateFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_activate + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + setupStep.observe(viewLifecycleOwner) { + when (it) { + MedtrumViewModel.SetupStep.INITIAL, + MedtrumViewModel.SetupStep.PRIMED -> Unit // Nothing to do here, previous state + MedtrumViewModel.SetupStep.ACTIVATED -> moveStep(PatchStep.ACTIVATE_COMPLETE) + + MedtrumViewModel.SetupStep.ERROR -> { + moveStep(PatchStep.ERROR) + updateSetupStep(MedtrumViewModel.SetupStep.PRIMED) // Reset setup step + binding.textActivatingPump.text = rh.gs(R.string.activating_error) + binding.btnPositive.visibility = View.VISIBLE + } + + else -> { + ToastUtils.errorToast(requireContext(), "Unexpected state: $it") + aapsLogger.error(LTag.PUMP, "Unexpected state: $it") + } + } + } + startActivate() + } + btnNegative.setOnClickListener { + OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) { + viewModel?.apply { + moveStep(PatchStep.CANCEL) + } + } + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivity.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivity.kt new file mode 100644 index 0000000000..07f96164d8 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumActivity.kt @@ -0,0 +1,111 @@ +package info.nightscout.pump.medtrum.ui + +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.view.MotionEvent +import android.view.WindowManager +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.utils.extensions.safeGetSerializableExtra +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState +import info.nightscout.pump.medtrum.databinding.ActivityMedtrumBinding +import info.nightscout.pump.medtrum.extension.replaceFragmentInActivity +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel + +class MedtrumActivity : MedtrumBaseActivity() { + + override fun getLayoutId(): Int = R.layout.activity_medtrum + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + return super.dispatchTouchEvent(event) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + binding.apply { + viewModel = ViewModelProvider(this@MedtrumActivity, viewModelFactory).get(MedtrumViewModel::class.java) + viewModel?.apply { + processIntent(intent) + + patchStep.observe(this@MedtrumActivity) { + when (it) { + PatchStep.PREPARE_PATCH -> setupViewFragment(MedtrumPreparePatchFragment.newInstance()) + PatchStep.PREPARE_PATCH_CONNECT -> setupViewFragment(MedtrumPreparePatchConnectFragment.newInstance()) + PatchStep.PRIME -> setupViewFragment(MedtrumPrimeFragment.newInstance()) + PatchStep.PRIMING -> setupViewFragment(MedtrumPrimingFragment.newInstance()) + PatchStep.PRIME_COMPLETE -> setupViewFragment(MedtrumPrimeCompleteFragment.newInstance()) + PatchStep.ATTACH_PATCH -> setupViewFragment(MedtrumAttachPatchFragment.newInstance()) + PatchStep.ACTIVATE -> setupViewFragment(MedtrumActivateFragment.newInstance()) + PatchStep.ACTIVATE_COMPLETE -> setupViewFragment(MedtrumActivateCompleteFragment.newInstance()) + PatchStep.CANCEL, + PatchStep.COMPLETE -> this@MedtrumActivity.finish() + PatchStep.ERROR -> Unit // Do nothing, let activity handle this + PatchStep.RETRY_ACTIVATION -> setupViewFragment(MedtrumRetryActivationFragment.newInstance()) + PatchStep.RETRY_ACTIVATION_CONNECT -> setupViewFragment(MedtrumRetryActivationConnectFragment.newInstance()) + PatchStep.START_DEACTIVATION -> setupViewFragment(MedtrumStartDeactivationFragment.newInstance()) + PatchStep.DEACTIVATE -> setupViewFragment(MedtrumDeactivatePatchFragment.newInstance()) + + PatchStep.FORCE_DEACTIVATION -> { + medtrumPump.pumpState = MedtrumPumpState.STOPPED + moveStep(PatchStep.DEACTIVATION_COMPLETE) + } + + PatchStep.DEACTIVATION_COMPLETE -> setupViewFragment(MedtrumDeactivationCompleteFragment.newInstance()) + null -> Unit + } + } + } + } + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + processIntent(intent) + } + + private fun processIntent(intent: Intent?) { + binding.viewModel?.apply { + intent?.run { + val step = intent.safeGetSerializableExtra(EXTRA_START_PATCH_STEP, PatchStep::class.java) + if (step != null) { + initializePatchStep(step) + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + } + + override fun onBackPressed() { + binding.viewModel?.apply { + // Do nothing + } + } + + companion object { + + const val EXTRA_START_PATCH_STEP = "EXTRA_START_PATCH_FRAGMENT_UI" + const val EXTRA_START_FROM_MENU = "EXTRA_START_FROM_MENU" + + @JvmStatic fun createIntentFromMenu(context: Context, patchStep: PatchStep): Intent { + return Intent(context, MedtrumActivity::class.java).apply { + putExtra(EXTRA_START_PATCH_STEP, patchStep) + putExtra(EXTRA_START_FROM_MENU, true) + } + } + + } + + private fun setupViewFragment(baseFragment: MedtrumBaseFragment<*>) { + replaceFragmentInActivity(baseFragment, R.id.framelayout_fragment, false) + } + +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumAttachPatchFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumAttachPatchFragment.kt new file mode 100644 index 0000000000..524f6d8139 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumAttachPatchFragment.kt @@ -0,0 +1,56 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.dialogs.OKDialog +import info.nightscout.core.ui.toast.ToastUtils +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumAttachPatchBinding +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import javax.inject.Inject + +class MedtrumAttachPatchFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rh: ResourceHelper + + companion object { + + fun newInstance(): MedtrumAttachPatchFragment = MedtrumAttachPatchFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_attach_patch + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + aapsLogger.debug(LTag.PUMP, "MedtrumAttachPatchFragment onViewCreated") + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + setupStep.observe(viewLifecycleOwner) { + when (it) { + MedtrumViewModel.SetupStep.INITIAL, + MedtrumViewModel.SetupStep.PRIMED -> Unit // Nothing to do here, previous state + + else -> { + ToastUtils.errorToast(requireContext(), rh.gs(R.string.unexpected_state, it.toString())) + aapsLogger.error(LTag.PUMP, "Unexpected state: $it") + } + } + } + } + btnNegative.setOnClickListener { + OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) { + viewModel?.apply { + moveStep(PatchStep.CANCEL) + } + } + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumBaseActivity.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumBaseActivity.kt new file mode 100644 index 0000000000..110a18513f --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumBaseActivity.kt @@ -0,0 +1,57 @@ +package info.nightscout.pump.medtrum.ui + +import android.content.Intent +import android.os.Bundle +import androidx.activity.result.ActivityResultLauncher +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.lifecycle.ViewModelProvider +import dagger.android.support.DaggerAppCompatActivity +import info.nightscout.core.ui.R +import info.nightscout.pump.medtrum.di.MedtrumPluginQualifier +import info.nightscout.rx.AapsSchedulers +import io.reactivex.rxjava3.disposables.CompositeDisposable +import javax.inject.Inject + +abstract class MedtrumBaseActivity : DaggerAppCompatActivity(), MedtrumBaseNavigator { + @Inject + @MedtrumPluginQualifier + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject lateinit var aapsSchedulers: AapsSchedulers + + protected lateinit var binding: B + + private val compositeDisposable = CompositeDisposable() + + protected lateinit var getResult: ActivityResultLauncher + + @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 back() { + if(supportFragmentManager.backStackEntryCount == 0) { + finish() + } else { + supportFragmentManager.popBackStack() + } + } + + override fun finish(finishAffinity: Boolean) { + if(finishAffinity) { + finishAffinity() + } else { + finish() + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumBaseFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumBaseFragment.kt new file mode 100644 index 0000000000..1add056d09 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumBaseFragment.kt @@ -0,0 +1,66 @@ +package info.nightscout.pump.medtrum.ui + +import android.content.Context +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.pump.medtrum.di.MedtrumPluginQualifier +import io.reactivex.rxjava3.disposables.CompositeDisposable +import javax.inject.Inject + +abstract class MedtrumBaseFragment : DaggerFragment(), MedtrumBaseNavigator { + @Inject + @MedtrumPluginQualifier + lateinit var viewModelFactory: ViewModelProvider.Factory + + protected var baseActivity: MedtrumBaseActivity<*>? = 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 MedtrumBaseActivity<*>) { + 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 back() { + baseActivity?.back() + } + + override fun finish(finishAffinity: Boolean) { + baseActivity?.finish(finishAffinity) + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumBaseNavigator.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumBaseNavigator.kt new file mode 100644 index 0000000000..5e2cb70651 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumBaseNavigator.kt @@ -0,0 +1,7 @@ +package info.nightscout.pump.medtrum.ui + +interface MedtrumBaseNavigator { + fun back() + + fun finish(finishAffinity: Boolean = false) +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumDeactivatePatchFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumDeactivatePatchFragment.kt new file mode 100644 index 0000000000..ec94ef89f1 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumDeactivatePatchFragment.kt @@ -0,0 +1,62 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.dialogs.OKDialog +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumDeactivatePatchBinding +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import javax.inject.Inject + +class MedtrumDeactivatePatchFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rh: ResourceHelper + + companion object { + + fun newInstance(): MedtrumDeactivatePatchFragment = MedtrumDeactivatePatchFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_deactivate_patch + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + aapsLogger.debug(LTag.PUMP, "MedtrumDeactivatePatchFragment onViewCreated") + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + setupStep.observe(viewLifecycleOwner) { + when (it) { + MedtrumViewModel.SetupStep.STOPPED -> { + moveStep(PatchStep.DEACTIVATION_COMPLETE) + } + + MedtrumViewModel.SetupStep.ERROR -> { + moveStep(PatchStep.ERROR) + updateSetupStep(MedtrumViewModel.SetupStep.START_DEACTIVATION) // Reset setup step + binding.textDeactivatingPump.text = rh.gs(R.string.deactivating_error) + binding.btnNegative.visibility = View.VISIBLE + binding.btnPositive.visibility = View.VISIBLE + } + + else -> Unit // Nothing to do here + } + } + deactivatePatch() + } + btnNegative.setOnClickListener { + OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) { + viewModel?.apply { + moveStep(PatchStep.CANCEL) + } + } + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumDeactivationCompleteFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumDeactivationCompleteFragment.kt new file mode 100644 index 0000000000..b0d72d2395 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumDeactivationCompleteFragment.kt @@ -0,0 +1,34 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumDeactivationCompleteBinding +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import javax.inject.Inject + +class MedtrumDeactivationCompleteFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + + companion object { + + fun newInstance(): MedtrumDeactivationCompleteFragment = MedtrumDeactivationCompleteFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_deactivation_complete + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + aapsLogger.debug(LTag.PUMP, "MedtrumStartDeactivationFragment onViewCreated") + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + // Nothing to do here (yet) + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumOverviewFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumOverviewFragment.kt new file mode 100644 index 0000000000..2a3907cbac --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumOverviewFragment.kt @@ -0,0 +1,83 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.toast.ToastUtils +import info.nightscout.interfaces.protection.ProtectionCheck +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumOverviewBinding +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumOverviewViewModel +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.EventType +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState +import info.nightscout.rx.AapsSchedulers +import info.nightscout.rx.logging.AAPSLogger +import io.reactivex.rxjava3.disposables.CompositeDisposable +import javax.inject.Inject + +class MedtrumOverviewFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsSchedulers: AapsSchedulers + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var medtrumPump: MedtrumPump + @Inject lateinit var protectionCheck: ProtectionCheck + + private var disposable: CompositeDisposable = CompositeDisposable() + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_overview + + override fun onDestroy() { + super.onDestroy() + disposable.clear() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.apply { + viewmodel = ViewModelProvider(this@MedtrumOverviewFragment, viewModelFactory).get(MedtrumOverviewViewModel::class.java) + viewmodel?.apply { + eventHandler.observe(viewLifecycleOwner) { evt -> + when (evt.peekContent()) { + EventType.CHANGE_PATCH_CLICKED -> requireContext().apply { + protectionCheck.queryProtection( + requireActivity(), + ProtectionCheck.Protection.PREFERENCES, + { + val nextStep = when { + medtrumPump.pumpState > MedtrumPumpState.EJECTED && medtrumPump.pumpState < MedtrumPumpState.STOPPED -> { + PatchStep.START_DEACTIVATION + } + + medtrumPump.pumpState in listOf(MedtrumPumpState.STOPPED, MedtrumPumpState.NONE) -> { + PatchStep.PREPARE_PATCH + } + + else -> { + PatchStep.RETRY_ACTIVATION + } + } + startActivity(MedtrumActivity.createIntentFromMenu(this, nextStep)) + } + ) + } + + EventType.PROFILE_NOT_SET -> ToastUtils.infoToast(requireContext(), R.string.no_profile_selected) + + else -> Unit + } + } + } + } + } + + override fun onPause() { + super.onPause() + } + + override fun onResume() { + super.onResume() + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPreparePatchConnectFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPreparePatchConnectFragment.kt new file mode 100644 index 0000000000..57e05b2032 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPreparePatchConnectFragment.kt @@ -0,0 +1,59 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.dialogs.OKDialog +import info.nightscout.core.ui.toast.ToastUtils +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumPreparePatchConnectBinding +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import javax.inject.Inject + +class MedtrumPreparePatchConnectFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rh: ResourceHelper + + companion object { + + fun newInstance(): MedtrumPreparePatchConnectFragment = MedtrumPreparePatchConnectFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_prepare_patch_connect + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + aapsLogger.debug(LTag.PUMP, "MedtrumPreparePatchFragment onViewCreated") + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + setupStep.observe(viewLifecycleOwner) { + when (it) { + MedtrumViewModel.SetupStep.INITIAL -> btnPositive.visibility = View.GONE + MedtrumViewModel.SetupStep.FILLED -> btnPositive.visibility = View.VISIBLE + + MedtrumViewModel.SetupStep.ERROR -> { + ToastUtils.errorToast(requireContext(), rh.gs(R.string.unexpected_state, it.toString())) + moveStep(PatchStep.CANCEL) + } + + else -> Unit + } + } + preparePatchConnect() + } + btnNegative.setOnClickListener { + OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) { + viewModel?.apply { + moveStep(PatchStep.CANCEL) + } + } + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPreparePatchFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPreparePatchFragment.kt new file mode 100644 index 0000000000..5e23d96632 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPreparePatchFragment.kt @@ -0,0 +1,35 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumPreparePatchBinding +import info.nightscout.pump.medtrum.ui.MedtrumBaseFragment +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import javax.inject.Inject + +class MedtrumPreparePatchFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + + companion object { + + fun newInstance(): MedtrumPreparePatchFragment = MedtrumPreparePatchFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_prepare_patch + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + aapsLogger.debug(LTag.PUMP, "MedtrumPreparePatchFragment onViewCreated") + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + preparePatch() + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimeCompleteFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimeCompleteFragment.kt new file mode 100644 index 0000000000..75b2bad284 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimeCompleteFragment.kt @@ -0,0 +1,56 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.dialogs.OKDialog +import info.nightscout.core.ui.toast.ToastUtils +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumPrimeCompleteBinding +import info.nightscout.pump.medtrum.ui.MedtrumBaseFragment +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import javax.inject.Inject + +class MedtrumPrimeCompleteFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rh: ResourceHelper + + companion object { + + fun newInstance(): MedtrumPrimeCompleteFragment = MedtrumPrimeCompleteFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_prime_complete + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + setupStep.observe(viewLifecycleOwner) { + when (it) { + MedtrumViewModel.SetupStep.INITIAL, + MedtrumViewModel.SetupStep.PRIMED -> Unit // Nothing to do here, previous state + + else -> { + ToastUtils.errorToast(requireContext(), rh.gs(R.string.unexpected_state, it.toString())) + aapsLogger.error(LTag.PUMP, "Unexpected state: $it") + } + } + } + } + btnNegative.setOnClickListener { + OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) { + viewModel?.apply { + moveStep(PatchStep.CANCEL) + } + } + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimeFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimeFragment.kt new file mode 100644 index 0000000000..3322c8a54f --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimeFragment.kt @@ -0,0 +1,56 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.dialogs.OKDialog +import info.nightscout.core.ui.toast.ToastUtils +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumPrimeBinding +import info.nightscout.pump.medtrum.ui.MedtrumBaseFragment +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import javax.inject.Inject + +class MedtrumPrimeFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rh: ResourceHelper + + companion object { + + fun newInstance(): MedtrumPrimeFragment = MedtrumPrimeFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_prime + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + setupStep.observe(viewLifecycleOwner) { + when (it) { + MedtrumViewModel.SetupStep.INITIAL, + MedtrumViewModel.SetupStep.FILLED -> Unit // Nothing to do here, previous state + + else -> { + ToastUtils.errorToast(requireContext(), rh.gs(R.string.unexpected_state, it.toString())) + aapsLogger.error(LTag.PUMP, "Unexpected state: $it") + } + } + } + } + btnNegative.setOnClickListener { + OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) { + viewModel?.apply { + moveStep(PatchStep.CANCEL) + } + } + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimingFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimingFragment.kt new file mode 100644 index 0000000000..fe1234a5c3 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPrimingFragment.kt @@ -0,0 +1,67 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.dialogs.OKDialog +import info.nightscout.core.ui.toast.ToastUtils +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumPrimingBinding +import info.nightscout.pump.medtrum.ui.MedtrumBaseFragment +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import javax.inject.Inject + +class MedtrumPrimingFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rh: ResourceHelper + + companion object { + + fun newInstance(): MedtrumPrimingFragment = MedtrumPrimingFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_priming + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + setupStep.observe(viewLifecycleOwner) { + when (it) { + MedtrumViewModel.SetupStep.INITIAL, + MedtrumViewModel.SetupStep.FILLED, + MedtrumViewModel.SetupStep.PRIMING -> Unit // Nothing to do here + MedtrumViewModel.SetupStep.PRIMED -> moveStep(PatchStep.PRIME_COMPLETE) + + MedtrumViewModel.SetupStep.ERROR -> { + moveStep(PatchStep.ERROR) + updateSetupStep(MedtrumViewModel.SetupStep.FILLED) // Reset setup step + binding.textWaitForPriming.text = rh.gs(R.string.priming_error) + binding.btnNegative.visibility = View.VISIBLE + binding.btnPositive.visibility = View.VISIBLE + } + + else -> { + ToastUtils.errorToast(requireContext(), rh.gs(R.string.unexpected_state, it.toString())) + aapsLogger.error(LTag.PUMP, "Unexpected state: $it") + } + } + } + startPrime() + } + btnNegative.setOnClickListener { + OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) { + viewModel?.apply { + moveStep(PatchStep.CANCEL) + } + } + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumRetryActivationConnectFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumRetryActivationConnectFragment.kt new file mode 100644 index 0000000000..f1e3c5614a --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumRetryActivationConnectFragment.kt @@ -0,0 +1,60 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.dialogs.OKDialog +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumRetryActivationConnectBinding +import info.nightscout.pump.medtrum.ui.MedtrumBaseFragment +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import javax.inject.Inject + +class MedtrumRetryActivationConnectFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rh: ResourceHelper + + companion object { + + fun newInstance(): MedtrumRetryActivationConnectFragment = MedtrumRetryActivationConnectFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_retry_activation_connect + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + aapsLogger.debug(LTag.PUMP, "MedtrumRetryActivationConnectFragment onViewCreated") + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + + setupStep.observe(viewLifecycleOwner) { + when (it) { + MedtrumViewModel.SetupStep.INITIAL -> Unit // Nothing to do here + MedtrumViewModel.SetupStep.FILLED -> forceMoveStep(PatchStep.PRIME) + MedtrumViewModel.SetupStep.PRIMING -> forceMoveStep(PatchStep.PRIMING) + MedtrumViewModel.SetupStep.PRIMED -> forceMoveStep(PatchStep.PRIME_COMPLETE) + MedtrumViewModel.SetupStep.ACTIVATED -> forceMoveStep(PatchStep.ACTIVATE_COMPLETE) + + else -> { + aapsLogger.error(LTag.PUMP, "Unexpected state: $it") + } + } + } + retryActivationConnect() + } + btnNegative.setOnClickListener { + OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) { + viewModel?.apply { + moveStep(PatchStep.CANCEL) + } + } + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumRetryActivationFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumRetryActivationFragment.kt new file mode 100644 index 0000000000..5e1673685b --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumRetryActivationFragment.kt @@ -0,0 +1,46 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.dialogs.OKDialog +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumRetryActivationBinding +import info.nightscout.pump.medtrum.ui.MedtrumBaseFragment +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import javax.inject.Inject + +class MedtrumRetryActivationFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rh: ResourceHelper + + companion object { + + fun newInstance(): MedtrumRetryActivationFragment = MedtrumRetryActivationFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_retry_activation + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + aapsLogger.debug(LTag.PUMP, "MedtrumRetryActivationFragment onViewCreated") + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + preparePatch() // Use this to make sure we are disconnceted at this stage + } + btnNegative.setOnClickListener { + OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.medtrum_deactivate_pump_confirm)) { + viewModel?.apply { + moveStep(PatchStep.FORCE_DEACTIVATION) + } + } + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumStartDeactivationFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumStartDeactivationFragment.kt new file mode 100644 index 0000000000..94f3bd93d9 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumStartDeactivationFragment.kt @@ -0,0 +1,46 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import info.nightscout.core.ui.dialogs.OKDialog +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.databinding.FragmentMedtrumStartDeactivationBinding +import info.nightscout.pump.medtrum.ui.MedtrumBaseFragment +import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import javax.inject.Inject + +class MedtrumStartDeactivationFragment : MedtrumBaseFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rh: ResourceHelper + + companion object { + + fun newInstance(): MedtrumStartDeactivationFragment = MedtrumStartDeactivationFragment() + } + + override fun getLayoutId(): Int = R.layout.fragment_medtrum_start_deactivation + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + aapsLogger.debug(LTag.PUMP, "MedtrumStartDeactivationFragment onViewCreated") + binding.apply { + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java] + viewModel?.apply { + updateSetupStep(MedtrumViewModel.SetupStep.START_DEACTIVATION) + } + btnPositive.setOnClickListener { + OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.medtrum_deactivate_pump_confirm)) { + viewModel?.apply { + moveStep(PatchStep.DEACTIVATE) + } + } + } + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/event/SingleLiveEvent.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/event/SingleLiveEvent.kt new file mode 100644 index 0000000000..4d09335375 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/event/SingleLiveEvent.kt @@ -0,0 +1,29 @@ +package info.nightscout.pump.medtrum.ui.event + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.annotation.MainThread +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) { 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 + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/event/UIEvent.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/event/UIEvent.kt new file mode 100644 index 0000000000..e1f879799b --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/event/UIEvent.kt @@ -0,0 +1,7 @@ +package info.nightscout.pump.medtrum.ui.event + +open class UIEvent(private val content: T) { + var value: Any? = null + fun peekContent(): T = content +} + diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/BaseViewModel.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/BaseViewModel.kt new file mode 100644 index 0000000000..9b2a3bbbde --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/BaseViewModel.kt @@ -0,0 +1,30 @@ +package info.nightscout.pump.medtrum.ui.viewmodel + +import androidx.lifecycle.ViewModel +import info.nightscout.pump.medtrum.ui.MedtrumBaseNavigator +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable +import java.lang.ref.WeakReference + +abstract class BaseViewModel : 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 back() = navigator?.back() + + fun finish() = navigator?.finish() + + fun Disposable.addTo() = apply { compositeDisposable.add(this) } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumOverviewViewModel.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumOverviewViewModel.kt new file mode 100644 index 0000000000..da742ea91d --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumOverviewViewModel.kt @@ -0,0 +1,189 @@ +package info.nightscout.pump.medtrum.ui.viewmodel + +import androidx.lifecycle.LiveData +import info.nightscout.pump.medtrum.code.EventType +import info.nightscout.pump.medtrum.ui.MedtrumBaseNavigator +import info.nightscout.pump.medtrum.ui.event.SingleLiveEvent +import info.nightscout.pump.medtrum.ui.event.UIEvent +import info.nightscout.interfaces.profile.ProfileFunction +import info.nightscout.interfaces.queue.CommandQueue +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.ConnectionState +import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import info.nightscout.shared.interfaces.ResourceHelper +import info.nightscout.shared.utils.DateUtil +import info.nightscout.shared.utils.T +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import javax.inject.Inject + +class MedtrumOverviewViewModel @Inject constructor( + private val aapsLogger: AAPSLogger, + private val rh: ResourceHelper, + private val profileFunction: ProfileFunction, + private val commandQueue: CommandQueue, + private val dateUtil: DateUtil, + val medtrumPump: MedtrumPump +) : BaseViewModel() { + + private val scope = CoroutineScope(Dispatchers.Default) + + private val _eventHandler = SingleLiveEvent>() + val eventHandler: LiveData> + get() = _eventHandler + + private val _canDoRefresh = SingleLiveEvent() + val canDoRefresh: LiveData + get() = _canDoRefresh + + private val _canDoResetAlarms = SingleLiveEvent() + val canDoResetAlarms: LiveData + get() = _canDoResetAlarms + + private val _bleStatus = SingleLiveEvent() + val bleStatus: LiveData + get() = _bleStatus + + private val _lastConnectionMinAgo = SingleLiveEvent() + val lastConnectionMinAgo: LiveData + get() = _lastConnectionMinAgo + + private val _lastBolus = SingleLiveEvent() + val lastBolus: LiveData + get() = _lastBolus + + private val _activeAlarms = SingleLiveEvent() + val activeAlarms: LiveData + get() = _activeAlarms + + private val _pumpType = SingleLiveEvent() + val pumpType: LiveData + get() = _pumpType + + private val _fwVersion = SingleLiveEvent() + val fwVersion: LiveData + get() = _fwVersion + + private val _patchNo = SingleLiveEvent() + val patchNo: LiveData + get() = _patchNo + + private val _patchExpiry = SingleLiveEvent() + val patchExpiry: LiveData + get() = _patchExpiry + + init { + scope.launch { + medtrumPump.connectionStateFlow.collect { state -> + aapsLogger.debug(LTag.PUMP, "MedtrumViewModel connectionStateFlow: $state") + when (state) { + ConnectionState.CONNECTING -> { + _bleStatus.postValue("{fa-bluetooth-b spin}") + _canDoRefresh.postValue(false) + } + + ConnectionState.CONNECTED -> { + _bleStatus.postValue("{fa-bluetooth}") + _canDoRefresh.postValue(false) + } + + ConnectionState.DISCONNECTED -> { + _bleStatus.postValue("{fa-bluetooth-b}") + if (medtrumPump.pumpState > MedtrumPumpState.EJECTED && medtrumPump.pumpState < MedtrumPumpState.STOPPED) { + _canDoRefresh.postValue(true) + } else { + _canDoRefresh.postValue(false) + } + } + + ConnectionState.DISCONNECTING -> { + _bleStatus.postValue("{fa-bluetooth-b spin}") + _canDoRefresh.postValue(false) + } + } + updateGUI() + } + } + scope.launch { + medtrumPump.pumpStateFlow.collect { state -> + aapsLogger.debug(LTag.PUMP, "MedtrumViewModel pumpStateFlow: $state") + _canDoResetAlarms.postValue( + medtrumPump.pumpState in listOf( + MedtrumPumpState.PAUSED, MedtrumPumpState.HMAX_SUSPENDED, MedtrumPumpState.DMAX_SUSPENDED + ) + ) + + updateGUI() + } + } + // Periodically update gui + scope.launch { + while (true) { + updateGUI() + kotlinx.coroutines.delay(T.mins(1).msecs()) + } + } + } + + override fun onCleared() { + super.onCleared() + scope.cancel() + } + + fun onClickRefresh() { + commandQueue.readStatus(rh.gs(R.string.requested_by_user), null) + } + + fun onClickResetAlarms() { + commandQueue.clearAlarms(null) + } + + fun onClickChangePatch() { + aapsLogger.debug(LTag.PUMP, "ChangePatch Patch clicked!") + val profile = profileFunction.getProfile() + if (profile == null) { + _eventHandler.postValue(UIEvent(EventType.PROFILE_NOT_SET)) + } else { + _eventHandler.postValue(UIEvent(EventType.CHANGE_PATCH_CLICKED)) + } + } + + fun updateGUI() { + // Update less dynamic values + if (medtrumPump.lastConnection != 0L) { + val agoMilliseconds = System.currentTimeMillis() - medtrumPump.lastConnection + val agoMinutes = agoMilliseconds / 1000 / 60 + _lastConnectionMinAgo.postValue(rh.gs(info.nightscout.shared.R.string.minago, agoMinutes)) + } + if (medtrumPump.lastBolusTime != 0L) { + val agoMilliseconds = System.currentTimeMillis() - medtrumPump.lastBolusTime + val agoHours = agoMilliseconds.toDouble() / 60.0 / 60.0 / 1000.0 + if (agoHours < 6) + // max 6h back + _lastBolus.postValue( + dateUtil.timeString(medtrumPump.lastBolusTime) + " " + dateUtil.sinceString(medtrumPump.lastBolusTime, rh) + " " + rh.gs( + info.nightscout.interfaces.R.string.format_insulin_units, medtrumPump.lastBolusAmount + ) + ) + else _lastBolus.postValue("") + } + + val activeAlarmStrings = medtrumPump.activeAlarms.map { medtrumPump.alarmStateToString(it) } + _activeAlarms.postValue(activeAlarmStrings.joinToString("\n")) + _pumpType.postValue(medtrumPump.deviceType.toString()) + _fwVersion.postValue(medtrumPump.swVersion) + _patchNo.postValue(medtrumPump.patchId.toString()) + + if (medtrumPump.desiredPatchExpiration) { + val expiry = medtrumPump.patchStartTime + T.hours(72).msecs() + _patchExpiry.postValue(dateUtil.dateAndTimeString(expiry)) + } else { + _patchExpiry.postValue(rh.gs(R.string.expiry_not_enabled)) + } + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumViewModel.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumViewModel.kt new file mode 100644 index 0000000000..da2245ce21 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/MedtrumViewModel.kt @@ -0,0 +1,327 @@ +package info.nightscout.pump.medtrum.ui.viewmodel + +import android.os.SystemClock +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import info.nightscout.interfaces.queue.Callback +import info.nightscout.interfaces.queue.CommandQueue +import info.nightscout.pump.medtrum.MedtrumPlugin +import info.nightscout.pump.medtrum.MedtrumPump +import info.nightscout.pump.medtrum.R +import info.nightscout.pump.medtrum.code.ConnectionState +import info.nightscout.pump.medtrum.services.MedtrumService +import info.nightscout.pump.medtrum.code.EventType +import info.nightscout.pump.medtrum.code.PatchStep +import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState +import info.nightscout.pump.medtrum.encryption.Crypt +import info.nightscout.pump.medtrum.ui.MedtrumBaseNavigator +import info.nightscout.pump.medtrum.ui.event.SingleLiveEvent +import info.nightscout.pump.medtrum.ui.event.UIEvent +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import javax.inject.Inject + +class MedtrumViewModel @Inject constructor( + private val aapsLogger: AAPSLogger, + private val medtrumPlugin: MedtrumPlugin, + private val commandQueue: CommandQueue, + val medtrumPump: MedtrumPump +) : BaseViewModel() { + + val patchStep = MutableLiveData() + + val medtrumService: MedtrumService? + get() = medtrumPlugin.getService() + + private val scope = CoroutineScope(Dispatchers.Default) + + private val _title = MutableLiveData(R.string.step_prepare_patch) + val title: LiveData + get() = _title + + private val _eventHandler = SingleLiveEvent>() + val eventHandler: LiveData> + get() = _eventHandler + + private var mInitPatchStep: PatchStep? = null + private var connectRetryCounter = 0 + + init { + scope.launch { + medtrumPump.connectionStateFlow.collect { state -> + aapsLogger.debug(LTag.PUMP, "MedtrumViewModel connectionStateFlow: $state") + if (patchStep.value != null) { + when (state) { + ConnectionState.CONNECTED -> { + medtrumPump.lastConnection = System.currentTimeMillis() + } + + ConnectionState.DISCONNECTED -> { + if (patchStep.value in listOf( + PatchStep.PRIME, + PatchStep.PRIMING, + PatchStep.PRIME_COMPLETE, + PatchStep.ATTACH_PATCH, + PatchStep.ACTIVATE + ) + ) { + medtrumService?.connect("Try reconnect from viewModel") + } + if (patchStep.value in listOf(PatchStep.PREPARE_PATCH_CONNECT, PatchStep.RETRY_ACTIVATION_CONNECT)) { + // We are disconnected during prepare patch connect, this means we failed to connect (wrong session token?) + // Retry 3 times, then give up + if (connectRetryCounter < 3) { + connectRetryCounter++ + aapsLogger.info(LTag.PUMP, "preparePatchConnect: retry $connectRetryCounter") + medtrumService?.connect("Try reconnect from viewModel") + } else { + aapsLogger.info(LTag.PUMP, "preparePatchConnect: failed to connect") + updateSetupStep(SetupStep.ERROR) + } + } + } + + ConnectionState.CONNECTING, ConnectionState.DISCONNECTING -> { + } + } + } + } + } + scope.launch { + medtrumPump.pumpStateFlow.collect { state -> + aapsLogger.debug(LTag.PUMP, "MedtrumViewModel pumpStateFlow: $state") + if (patchStep.value != null) { + when (state) { + MedtrumPumpState.NONE, MedtrumPumpState.IDLE -> { + updateSetupStep(SetupStep.INITIAL) + } + + MedtrumPumpState.FILLED -> { + updateSetupStep(SetupStep.FILLED) + } + + MedtrumPumpState.PRIMING -> { + updateSetupStep(SetupStep.PRIMING) + } + + MedtrumPumpState.PRIMED, MedtrumPumpState.EJECTED -> { + updateSetupStep(SetupStep.PRIMED) + } + + MedtrumPumpState.ACTIVE, MedtrumPumpState.ACTIVE_ALT -> { + updateSetupStep(SetupStep.ACTIVATED) + } + + MedtrumPumpState.STOPPED -> { + updateSetupStep(SetupStep.STOPPED) + } + + else -> { + updateSetupStep(SetupStep.ERROR) + } + } + } + } + } + } + + override fun onCleared() { + super.onCleared() + scope.cancel() + } + + fun moveStep(newPatchStep: PatchStep) { + val oldPatchStep = patchStep.value + + if (oldPatchStep != newPatchStep) { + when (newPatchStep) { + PatchStep.CANCEL -> { + if (oldPatchStep !in listOf( + PatchStep.PREPARE_PATCH, + PatchStep.START_DEACTIVATION, + PatchStep.DEACTIVATE, + PatchStep.FORCE_DEACTIVATION, + PatchStep.DEACTIVATION_COMPLETE + ) + ) { + medtrumService?.disconnect("Cancel") + } + } + + PatchStep.COMPLETE -> { + medtrumService?.disconnect("Complete") + } + + PatchStep.START_DEACTIVATION, + PatchStep.DEACTIVATE, + PatchStep.FORCE_DEACTIVATION, + PatchStep.DEACTIVATION_COMPLETE, + PatchStep.PREPARE_PATCH, + PatchStep.RETRY_ACTIVATION, + PatchStep.RETRY_ACTIVATION_CONNECT -> { + // Do nothing, deactivation uses commandQueue to control connection + } + + PatchStep.PREPARE_PATCH_CONNECT -> { + // Make sure we are disconnected, else dont move step + if (medtrumService?.isConnected == true) { + aapsLogger.info(LTag.PUMP, "moveStep: connected, not moving step") + return + } else { + } + } + + else -> { + // Make sure we are connected, else dont move step + if (medtrumService?.isConnected == false) { + aapsLogger.info(LTag.PUMP, "moveStep: not connected, not moving step") + return + } else { + } + } + } + } + + prepareStep(newPatchStep) + + aapsLogger.info(LTag.PUMP, "moveStep: $oldPatchStep -> $newPatchStep") + } + + fun forceMoveStep(newPatchStep: PatchStep) { + val oldPatchStep = patchStep.value + + prepareStep(newPatchStep) + aapsLogger.info(LTag.PUMP, "forceMoveStep: $oldPatchStep -> $newPatchStep") + } + + fun initializePatchStep(step: PatchStep) { + mInitPatchStep = prepareStep(step) + } + + fun preparePatch() { + medtrumService?.disconnect("PreparePatch") + } + + fun preparePatchConnect() { + scope.launch { + if (medtrumService?.isConnected == false) { + aapsLogger.info(LTag.PUMP, "preparePatch: new session") + // New session, generate new session token + medtrumPump.patchSessionToken = Crypt().generateRandomToken() + // Connect to pump + medtrumService?.connect("PreparePatch") + } else { + aapsLogger.error(LTag.PUMP, "preparePatch: Already connected when trying to prepare patch") + // Do nothing, we are already connected + } + } + } + + fun startPrime() { + scope.launch { + if (medtrumPump.pumpState == MedtrumPumpState.PRIMING) { + aapsLogger.info(LTag.PUMP, "startPrime: already priming!") + } else { + if (medtrumService?.startPrime() == true) { + aapsLogger.info(LTag.PUMP, "startPrime: success!") + } else { + aapsLogger.info(LTag.PUMP, "startPrime: failure!") + updateSetupStep(SetupStep.ERROR) + } + } + } + } + + fun startActivate() { + scope.launch { + if (medtrumService?.startActivate() == true) { + aapsLogger.info(LTag.PUMP, "startActivate: success!") + } else { + aapsLogger.info(LTag.PUMP, "startActivate: failure!") + updateSetupStep(SetupStep.ERROR) + } + } + } + + fun deactivatePatch() { + commandQueue.deactivate(object : Callback() { + override fun run() { + if (this.result.success) { + // Do nothing, state change will handle this + } else { + if (medtrumPump.pumpState >= MedtrumPumpState.OCCLUSION && medtrumPump.pumpState <= MedtrumPumpState.NO_CALIBRATION) { + // We are in a fault state, we need to force deactivation + aapsLogger.info(LTag.PUMP, "deactivatePatch: force deactivation") + medtrumService?.disconnect("ForceDeactivation") + SystemClock.sleep(1000) + medtrumPump.pumpState = MedtrumPumpState.STOPPED + } else { + aapsLogger.info(LTag.PUMP, "deactivatePatch: failure!") + updateSetupStep(SetupStep.ERROR) + } + } + } + }) + } + + fun retryActivationConnect() { + scope.launch { + if (medtrumService?.isConnected == false) { + // Reset medtrum pump state, we will pickup pomp state on connect + medtrumPump.pumpState = MedtrumPumpState.NONE + medtrumService?.connect("RetryActivationConnect") + } else { + aapsLogger.error(LTag.PUMP, "retryActivationConnect: Already connected when trying to prepare patch") + updateSetupStep(SetupStep.ERROR) + } + } + } + + private fun prepareStep(newStep: PatchStep): PatchStep { + val stringResId = when (newStep) { + PatchStep.PREPARE_PATCH -> R.string.step_prepare_patch + PatchStep.PREPARE_PATCH_CONNECT -> R.string.step_prepare_patch_connect + PatchStep.PRIME -> R.string.step_prime + PatchStep.PRIMING -> R.string.step_priming + PatchStep.PRIME_COMPLETE -> R.string.step_priming_complete + PatchStep.ATTACH_PATCH -> R.string.step_attach + PatchStep.ACTIVATE -> R.string.step_activate + PatchStep.ACTIVATE_COMPLETE -> R.string.step_activate_complete + PatchStep.START_DEACTIVATION -> R.string.step_deactivate + PatchStep.DEACTIVATE -> R.string.step_deactivating + PatchStep.DEACTIVATION_COMPLETE -> R.string.step_deactivate_complete + PatchStep.RETRY_ACTIVATION, + PatchStep.RETRY_ACTIVATION_CONNECT -> R.string.step_retry_activation + + PatchStep.COMPLETE, + PatchStep.FORCE_DEACTIVATION, + PatchStep.ERROR, + PatchStep.CANCEL -> _title.value + } + + val currentTitle = _title.value + aapsLogger.info(LTag.PUMP, "prepareStep: title before cond: $stringResId") + if (currentTitle != stringResId) { + aapsLogger.info(LTag.PUMP, "prepareStep: title: $stringResId") + _title.postValue(stringResId) + } + + patchStep.postValue(newStep) + + return newStep + } + + enum class SetupStep { INITIAL, FILLED, PRIMING, PRIMED, ACTIVATED, ERROR, START_DEACTIVATION, STOPPED + } + + val setupStep = MutableLiveData() + + fun updateSetupStep(newSetupStep: SetupStep) { + aapsLogger.info(LTag.PUMP, "curSetupStep: ${setupStep.value}, newSetupStep: $newSetupStep") + setupStep.postValue(newSetupStep) + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/ViewModelFactory.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/ViewModelFactory.kt new file mode 100644 index 0000000000..0baf6a7679 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/ViewModelFactory.kt @@ -0,0 +1,40 @@ +package info.nightscout.pump.medtrum.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) +@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 { + @Suppress("UNCHECKED_CAST") + return creator.get() as T + } catch (e: Exception) { + throw IllegalStateException(e) + } + + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/util/MedtrumTimeUtil.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/util/MedtrumTimeUtil.kt new file mode 100644 index 0000000000..ad3910f0ec --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/util/MedtrumTimeUtil.kt @@ -0,0 +1,26 @@ +package info.nightscout.pump.medtrum.util + +import java.time.Duration +import java.time.Instant + +class MedtrumTimeUtil { + + fun getCurrentTimePumpSeconds() : Long { + val startInstant = Instant.parse("2014-01-01T00:00:00Z") + val currentInstant = Instant.now() + return Duration.between(startInstant, currentInstant).seconds + } + + fun getCurrentTimePumpMillis() : Long { + val startInstant = Instant.parse("2014-01-01T00:00:00Z") + val currentInstant = Instant.now() + return Duration.between(startInstant, currentInstant).seconds * 1000 + } + + fun convertPumpTimeToSystemTimeMillis(pumpTime: Long) : Long { + val startInstant = Instant.parse("2014-01-01T00:00:00Z") + val pumpInstant = startInstant.plusSeconds(pumpTime) + val epochInstant = Instant.EPOCH + return Duration.between(epochInstant, pumpInstant).seconds * 1000 + } +} diff --git a/pump/medtrum/src/main/res/drawable/ic_refresh_conn.xml b/pump/medtrum/src/main/res/drawable/ic_refresh_conn.xml new file mode 100644 index 0000000000..851d544e89 --- /dev/null +++ b/pump/medtrum/src/main/res/drawable/ic_refresh_conn.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/pump/medtrum/src/main/res/drawable/ic_silence_alerts.xml b/pump/medtrum/src/main/res/drawable/ic_silence_alerts.xml new file mode 100644 index 0000000000..0688820a25 --- /dev/null +++ b/pump/medtrum/src/main/res/drawable/ic_silence_alerts.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/pump/medtrum/src/main/res/layout/activity_medtrum.xml b/pump/medtrum/src/main/res/layout/activity_medtrum.xml new file mode 100644 index 0000000000..2c1d16aad2 --- /dev/null +++ b/pump/medtrum/src/main/res/layout/activity_medtrum.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pump/medtrum/src/main/res/layout/fragment_medtrum_activate.xml b/pump/medtrum/src/main/res/layout/fragment_medtrum_activate.xml new file mode 100644 index 0000000000..517e8bd56b --- /dev/null +++ b/pump/medtrum/src/main/res/layout/fragment_medtrum_activate.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + +