diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index 5c60486659..4ad56cd95d 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -3,9 +3,11 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash import dagger.android.HasAndroidInjector import info.nightscout.androidaps.data.DetailedBolusInfo import info.nightscout.androidaps.data.PumpEnactResult +import info.nightscout.androidaps.events.EventProfileSwitchChanged import info.nightscout.androidaps.interfaces.* import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.LTag +import info.nightscout.androidaps.plugins.bus.RxBusWrapper import info.nightscout.androidaps.plugins.common.ManufacturerType import info.nightscout.androidaps.plugins.general.actions.defs.CustomAction import info.nightscout.androidaps.plugins.general.actions.defs.CustomActionType @@ -15,7 +17,10 @@ import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.ActivationProgress +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.BasalProgram import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.BeepType +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.DeliveryStatus +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.PodConstants import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.ResponseType import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.CommandConfirmed import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager @@ -25,6 +30,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.BolusTy import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.TempBasalRecord import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.OmnipodDashOverviewFragment import info.nightscout.androidaps.plugins.pump.omnipod.dash.util.mapProfileToBasalProgram +import info.nightscout.androidaps.queue.commands.Command import info.nightscout.androidaps.queue.commands.CustomCommand import info.nightscout.androidaps.utils.T import info.nightscout.androidaps.utils.TimeChangeType @@ -48,6 +54,8 @@ class OmnipodDashPumpPlugin @Inject constructor( private val profileFunction: ProfileFunction, private val history: DashHistory, private val pumpSync: PumpSync, + private val rxBus: RxBusWrapper, + injector: HasAndroidInjector, aapsLogger: AAPSLogger, resourceHelper: ResourceHelper, @@ -83,23 +91,7 @@ class OmnipodDashPumpPlugin @Inject constructor( } override fun isConnected(): Boolean { - // NOTE: Using connected state for unconfirmed commands - - // We are faking connection lost on unconfirmed commands. - // During normal execution, the activeCommand is set to null after a command was executed with success or we - // were not able to send that command. - // If we are not sure if the POD received the command or not, then we answer with "success" but keep this - // activeCommand set until we can confirm/deny it. - - // In order to prevent AAPS from sending us other programming commands while the current command was not - // confirmed, we are simulating "connection lost". - // We need to prevent AAPS from sending other commands because they would overwrite the ID of the last - // programming command reported by the POD. And we using that ID to confirm/deny the activeCommand. - - // The effect of answering with 'false' here is that AAPS will call connect() and will not sent any new - // commands. On connect(), we are calling getPodStatus where we are always trying to confirm/deny the - // activeCommand. - return podStateManager.activeCommand == null + return true } override fun isConnecting(): Boolean { @@ -117,12 +109,7 @@ class OmnipodDashPumpPlugin @Inject constructor( } override fun connect(reason: String) { - // See: - // NOTE: Using connected state for unconfirmed commands - if (podStateManager.activeCommand == null) { - return - } - getPumpStatus("unconfirmed command") + // empty on purpose } override fun disconnect(reason: String) { @@ -134,38 +121,81 @@ class OmnipodDashPumpPlugin @Inject constructor( } override fun getPumpStatus(reason: String) { - Observable.concat( - omnipodManager.getStatus(ResponseType.StatusResponseType.DEFAULT_STATUS_RESPONSE), - history.updateFromState(podStateManager).toObservable(), - podStateManager.updateActiveCommand().toObservable(), - ).blockingSubscribeBy( - onNext = { podEvent -> - aapsLogger.debug( - LTag.PUMP, - "Received PodEvent in getPumpStatus: $podEvent" - ) - }, - onError = { throwable -> - aapsLogger.error(LTag.PUMP, "Error in getPumpStatus", throwable) - }, - onComplete = { - aapsLogger.debug("getPumpStatus completed") - } - ) + val throwable = Completable.concat(listOf( + omnipodManager + .getStatus(ResponseType.StatusResponseType.DEFAULT_STATUS_RESPONSE) + .ignoreElements(), + history.updateFromState(podStateManager), + podStateManager.updateActiveCommand() + .map { handleCommandConfirmation(it) } + .ignoreElement(), + )).blockingGet() + if (throwable != null){ + aapsLogger.error(LTag.PUMP, "Error in getPumpStatus", throwable) + } else { + aapsLogger.info(LTag.PUMP, "getPumpStatus executed with success") + + } } override fun setNewBasalProfile(profile: Profile): PumpEnactResult { + val basalProgram = mapProfileToBasalProgram(profile) return executeSimpleProgrammingCommand( - history.createRecord( - commandType = OmnipodCommandType.SET_BASAL_PROFILE - ), - omnipodManager.setBasalProgram(mapProfileToBasalProgram(profile)).ignoreElements() + pre = suspendDeliveryIfActive(), + historyEntry = history.createRecord(commandType = OmnipodCommandType.SET_BASAL_PROFILE), + command = omnipodManager.setBasalProgram(basalProgram).ignoreElements(), + basalProgram = basalProgram, + post = failWhenUnconfirmed(), ).toPumpEnactResult() } - override fun isThisProfileSet(profile: Profile): Boolean = podStateManager.basalProgram?.let { - it == mapProfileToBasalProgram(profile) - } ?: true + private fun failWhenUnconfirmed(): Completable = Completable.defer{ + if (podStateManager.activeCommand != null) { + Completable.error(java.lang.IllegalStateException("Command not confirmed")) + }else { + Completable.complete() + } + } + + private fun suspendDeliveryIfActive(): Completable = Completable.defer { + if (podStateManager.deliveryStatus == DeliveryStatus.SUSPENDED) + Completable.complete() + else + executeSimpleProgrammingCommand( + history.createRecord(OmnipodCommandType.SUSPEND_DELIVERY), + omnipodManager.suspendDelivery() + .filter { podEvent -> podEvent is PodEvent.CommandSent } + .map { + pumpSyncTempBasal( + it, + 0.0, + PodConstants.MAX_POD_LIFETIME.standardMinutes, + PumpSync.TemporaryBasalType.PUMP_SUSPEND + ) + } + .ignoreElements(), + ) + } + + private fun observeDeliverySuspended(): Completable = Completable.defer { + if (podStateManager.deliveryStatus == DeliveryStatus.SUSPENDED) + Completable.complete() + else { + Completable.error(java.lang.IllegalStateException("Expected suspended delivery")) + } + } + + override fun isThisProfileSet(profile: Profile): Boolean { + if (!podStateManager.isActivationCompleted) { + // prevent setBasal requests + return true + } + // TODO: what do we have to answer here if delivery is suspended? + val running = podStateManager.basalProgram + val equal = (mapProfileToBasalProgram(profile) == running) + aapsLogger.info(LTag.PUMP, "isThisProfileSet: $equal") + return equal + } override fun lastDataTime(): Long { return podStateManager.lastUpdatedSystem @@ -270,7 +300,7 @@ class OmnipodDashPumpPlugin @Inject constructor( tempBasalBeeps ) .filter { podEvent -> podEvent is PodEvent.CommandSent } - .map { pumpSyncTempBasal(it, tbrType) } + .map { pumpSyncTempBasal(it, absoluteRate, durationInMinutes.toLong(), tbrType) } .ignoreElements(), pre = observeNoActiveTempBasal() ).toPumpEnactResult() @@ -278,6 +308,8 @@ class OmnipodDashPumpPlugin @Inject constructor( private fun pumpSyncTempBasal( podEvent: PodEvent, + absoluteRate: Double, + durationInMinutes: Long, tbrType: PumpSync.TemporaryBasalType ): Boolean { val activeCommand = podStateManager.activeCommand @@ -289,14 +321,11 @@ class OmnipodDashPumpPlugin @Inject constructor( ) } val historyEntry = history.getById(activeCommand.historyId) - val record = historyEntry.record - if (record == null || !(record is TempBasalRecord)) { - throw IllegalArgumentException("Illegal recording in history: $record. Expected a temp basal") - } + val ret = pumpSync.syncTemporaryBasalWithPumpId( timestamp = historyEntry.createdAt, - rate = record.rate, - duration = T.mins(record.duration.toLong()).msecs(), + rate = absoluteRate, + duration = T.mins(durationInMinutes.toLong()).msecs(), isAbsolute = true, type = tbrType, pumpId = historyEntry.pumpId(), @@ -325,8 +354,9 @@ class OmnipodDashPumpPlugin @Inject constructor( } private fun observeActiveTempBasal(): Completable { + return Completable.defer { - if (podStateManager.tempBasalActive) + if (podStateManager.tempBasalActive || pumpSync.expectedPumpState().temporaryBasal != null) Completable.complete() else Completable.error( @@ -369,36 +399,6 @@ class OmnipodDashPumpPlugin @Inject constructor( .blockingGet() } - private fun handleCommandConfirmation(confirmation: CommandConfirmed) { - val historyEntry = history.getById(confirmation.historyId) - when (historyEntry.commandType) { - OmnipodCommandType.CANCEL_TEMPORARY_BASAL -> - // We can't invalidate this command, - // and this is why it is pumpSync-ed at this point - if (confirmation.success) { - pumpSync.syncStopTemporaryBasalWithPumpId( - historyEntry.createdAt, - historyEntry.pumpId(), - PumpType.OMNIPOD_DASH, - serialNumber() - ) - } - OmnipodCommandType.SET_TEMPORARY_BASAL -> - // This treatment was synced before sending the command - if (!confirmation.success) { - // TODO: the ID here is the temp basal id, not the pumpId!! - pumpSync.invalidateTemporaryBasal(historyEntry.pumpId()) - } - - else -> - aapsLogger.warn( - LTag.PUMP, - "Will not sync confirmed command of type: $historyEntry and " + - "succes: ${confirmation.success}" - ) - } - } - override fun cancelExtendedBolus(): PumpEnactResult { // TODO i18n return PumpEnactResult(injector).success(false).enacted(false) @@ -515,16 +515,35 @@ class OmnipodDashPumpPlugin @Inject constructor( private fun suspendDelivery(): PumpEnactResult { return executeSimpleProgrammingCommand( - history.createRecord(OmnipodCommandType.RESUME_DELIVERY), - omnipodManager.suspendDelivery().ignoreElements() + historyEntry = history.createRecord(OmnipodCommandType.SUSPEND_DELIVERY), + command = omnipodManager.suspendDelivery() + .filter { podEvent -> podEvent is PodEvent.CommandSent } + .map { + pumpSyncTempBasal( + it, + 0.0, + PodConstants.MAX_POD_LIFETIME.standardMinutes, + PumpSync.TemporaryBasalType.PUMP_SUSPEND + ) + } + .ignoreElements(), + pre = observeDeliveryActive(), ).toPumpEnactResult() } + private fun observeDeliveryActive(): Completable = Completable.defer { + if (podStateManager.deliveryStatus != DeliveryStatus.SUSPENDED) + Completable.complete() + else + Completable.error(java.lang.IllegalStateException("Expected active delivery")) + } + private fun resumeDelivery(): PumpEnactResult { return profileFunction.getProfile()?.let { executeSimpleProgrammingCommand( history.createRecord(OmnipodCommandType.RESUME_DELIVERY), - omnipodManager.setBasalProgram(mapProfileToBasalProgram(it)).ignoreElements() + omnipodManager.setBasalProgram(mapProfileToBasalProgram(it)).ignoreElements(), + pre = observeDeliverySuspended(), ).toPumpEnactResult() } ?: PumpEnactResult(injector).success(false).enacted(false).comment("No profile active") // TODO i18n } @@ -578,13 +597,15 @@ class OmnipodDashPumpPlugin @Inject constructor( historyEntry: Single, command: Completable, pre: Completable = Completable.complete(), + basalProgram: BasalProgram? = null, + post: Completable = Completable.complete(), ): Completable { return Completable.concat( listOf( pre, podStateManager.observeNoActiveCommand().ignoreElements(), historyEntry - .flatMap { podStateManager.createActiveCommand(it) } + .flatMap { podStateManager.createActiveCommand(it, basalProgram) } .ignoreElement(), command.doOnError { podStateManager.activeCommand?.sendError = it @@ -593,8 +614,69 @@ class OmnipodDashPumpPlugin @Inject constructor( history.updateFromState(podStateManager), podStateManager.updateActiveCommand() .map { handleCommandConfirmation(it) } - .ignoreElement() + .ignoreElement(), + post, ) ) } + + private fun handleCommandConfirmation(confirmation: CommandConfirmed) { + val command = confirmation.command + val historyEntry = history.getById(command.historyId) + aapsLogger.debug(LTag.PUMPCOMM, "handling command confirmation: $confirmation") + when (historyEntry.commandType) { + OmnipodCommandType.CANCEL_TEMPORARY_BASAL, + OmnipodCommandType.RESUME_DELIVERY -> + // We can't invalidate this command, + // and this is why it is pumpSync-ed at this point + if (confirmation.success) { + pumpSync.syncStopTemporaryBasalWithPumpId( + historyEntry.createdAt, + historyEntry.pumpId(), + PumpType.OMNIPOD_DASH, + serialNumber() + ) + } + + OmnipodCommandType.SET_BASAL_PROFILE -> { + if (confirmation.success) { + podStateManager.basalProgram = command.basalProgram + if (podStateManager.basalProgram == null) { + aapsLogger.warn(LTag.PUMP, "Saving null basal profile") + } + if (!commandQueue.isRunning(Command.CommandType.BASAL_PROFILE)) { + // we are late-confirming this command. before that, we answered with success:false + rxBus.send(EventProfileSwitchChanged()) + } + pumpSync.syncStopTemporaryBasalWithPumpId( + historyEntry.createdAt, + historyEntry.pumpId(), + PumpType.OMNIPOD_DASH, + serialNumber() + ) + } + } + + OmnipodCommandType.SET_TEMPORARY_BASAL -> { + // This treatment was synced before sending the command + aapsLogger.info(LTag.PUMPCOMM, "temporary basal denied. PumpId: ${historyEntry.pumpId()}") + if (!confirmation.success) { + pumpSync.invalidateTemporaryBasal(historyEntry.pumpId()) + } + } + + OmnipodCommandType.SUSPEND_DELIVERY -> { + if (!confirmation.success) { + pumpSync.invalidateTemporaryBasal(historyEntry.pumpId()) + } + } + + else -> + aapsLogger.warn( + LTag.PUMP, + "Will not sync confirmed command of type: $historyEntry and " + + "succes: ${confirmation.success}" + ) + } + } } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/dagger/OmnipodDashHistoryModule.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/dagger/OmnipodDashHistoryModule.kt index 1ac3c8dd3b..92c2368ee9 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/dagger/OmnipodDashHistoryModule.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/dagger/OmnipodDashHistoryModule.kt @@ -4,6 +4,7 @@ import android.content.Context import dagger.Module import dagger.Provides import dagger.Reusable +import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.DashHistory import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.DashHistoryDatabase import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.HistoryRecordDao @@ -28,6 +29,6 @@ class OmnipodDashHistoryModule { @Provides @Singleton - internal fun provideDashHistory(dao: HistoryRecordDao, historyMapper: HistoryMapper) = - DashHistory(dao, historyMapper) + internal fun provideDashHistory(dao: HistoryRecordDao, historyMapper: HistoryMapper, logger: AAPSLogger) = + DashHistory(dao, historyMapper, logger) } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt index 0d44fd4b3f..6f9750be35 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt @@ -172,6 +172,7 @@ class OmnipodDashManagerImpl @Inject constructor( DefaultStatusResponse::class ) }.doOnComplete { + // TODO: remove podStateManager.basalProgram? podStateManager.basalProgram = basalProgram } } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt index 622b3663a3..8af2a9c1fc 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt @@ -103,7 +103,6 @@ class OmnipodDashBleManagerImpl @Inject constructor( ?: Connection(podDevice, aapsLogger, context, podState) connection = conn if (conn.connectionState() is Connected) { - podState.lastConnection = System.currentTimeMillis() if (conn.session == null) { emitter.onNext(PodEvent.EstablishingSession) establishSession(1.toByte()) @@ -116,7 +115,6 @@ class OmnipodDashBleManagerImpl @Inject constructor( } conn.connect() emitter.onNext(PodEvent.BluetoothConnected(podAddress)) - podState.lastConnection = System.currentTimeMillis() emitter.onNext(PodEvent.EstablishingSession) establishSession(1.toByte()) emitter.onNext(PodEvent.Connected) diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/exceptions/CouldNotReadResponse.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/exceptions/CouldNotReadResponse.kt new file mode 100644 index 0000000000..98d81b66cf --- /dev/null +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/exceptions/CouldNotReadResponse.kt @@ -0,0 +1,3 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions + +class CouldNotReadResponse diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BasalProgram.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BasalProgram.kt index b003fba0d1..73b70126d6 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BasalProgram.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BasalProgram.kt @@ -17,7 +17,15 @@ class BasalProgram( fun isZeroBasal() = segments.sumBy(Segment::basalRateInHundredthUnitsPerHour) == 0 - fun rateAt(date: Date): Double = 0.0 // TODO + fun rateAt(date: Date): Double { + val instance = Calendar.getInstance() + instance.time = date + val hourOfDay = instance[Calendar.HOUR_OF_DAY] + val minuteOfHour = instance[Calendar.MINUTE] + val slotIndex = hourOfDay * 2 + minuteOfHour.div(30) + val slot = segments.find { it.startSlotIndex <= slotIndex && slotIndex< it.endSlotIndex } + return (slot?.basalRateInHundredthUnitsPerHour ?: 0).toDouble() / 100 + } class Segment( val startSlotIndex: Short, diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/PodConstants.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/PodConstants.kt new file mode 100644 index 0000000000..f52cc8fb5f --- /dev/null +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/PodConstants.kt @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition + +import org.joda.time.Duration + +class PodConstants { + companion object { + val MAX_POD_LIFETIME = Duration.standardHours(80) + } +} diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/CommandConfirmed.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/CommandConfirmed.kt index d5f57243d8..266c671c33 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/CommandConfirmed.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/CommandConfirmed.kt @@ -1,3 +1,3 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state -class CommandConfirmed(val historyId: String, val success: Boolean) +data class CommandConfirmed(val command: OmnipodDashPodStateManager.ActiveCommand, val success: Boolean) diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManager.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManager.kt index bc5fdf060a..2b9dc3dc7e 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManager.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManager.kt @@ -14,6 +14,13 @@ import io.reactivex.Single import java.io.Serializable import java.util.* +sealed class CommandConfirmationFromState +object CommandSendingFailure : CommandConfirmationFromState() +object CommandSendingNotConfirmed : CommandConfirmationFromState() +object CommandConfirmationDenied : CommandConfirmationFromState() +object CommandConfirmationSuccess : CommandConfirmationFromState() +object NoActiveCommand : CommandConfirmationFromState() + interface OmnipodDashPodStateManager { var activationProgress: ActivationProgress @@ -21,7 +28,6 @@ interface OmnipodDashPodStateManager { val isActivationCompleted: Boolean val isSuspended: Boolean val isPodRunning: Boolean - var lastConnection: Long var bluetoothConnectionState: BluetoothConnectionState val lastUpdatedSystem: Long // System.currentTimeMillis() @@ -67,9 +73,10 @@ interface OmnipodDashPodStateManager { fun updateFromPairing(uniqueId: Id, pairResult: PairResult) fun reset() - fun createActiveCommand(historyId: String): Single + fun createActiveCommand(historyId: String, basalProgram: BasalProgram? = null): Single fun updateActiveCommand(): Maybe fun observeNoActiveCommand(): Observable + fun getCommandConfirmationFromState(): CommandConfirmationFromState data class ActiveCommand( val sequence: Short, @@ -77,6 +84,7 @@ interface OmnipodDashPodStateManager { var sentRealtime: Long = 0, val historyId: String, var sendError: Throwable?, + var basalProgram: BasalProgram? ) // TODO: set created to "now" on boot data class TempBasal(val startTime: Long, val rate: Double, val durationInMinutes: Short) : Serializable diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManagerImpl.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManagerImpl.kt index 9724f7f819..104f4f1cfc 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManagerImpl.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManagerImpl.kt @@ -53,18 +53,11 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( override val isSuspended: Boolean get() = podState.deliveryStatus?.equals(DeliveryStatus.SUSPENDED) - ?: true + ?: false override val isPodRunning: Boolean get() = podState.podStatus?.isRunning() ?: false - override var lastConnection: Long - get() = podState.lastConnection - set(lastConnection) { - podState.lastConnection = lastConnection - store() - } - override val lastUpdatedSystem: Long get() = podState.lastUpdatedSystem @@ -148,7 +141,11 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( get() = podState.tempBasal override val tempBasalActive: Boolean - get() = podState.deliveryStatus in arrayOf(DeliveryStatus.TEMP_BASAL_ACTIVE, DeliveryStatus.BOLUS_AND_TEMP_BASAL_ACTIVE) + get() = podState.deliveryStatus in + arrayOf( + DeliveryStatus.TEMP_BASAL_ACTIVE, + DeliveryStatus.BOLUS_AND_TEMP_BASAL_ACTIVE + ) override var basalProgram: BasalProgram? get() = podState.basalProgram @@ -191,27 +188,29 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( get() = podState.activeCommand @Synchronized - override fun createActiveCommand(historyId: String): Single { - return Single.create { source -> - if (activeCommand == null) { - val command = OmnipodDashPodStateManager.ActiveCommand( - podState.messageSequenceNumber, - createdRealtime = SystemClock.elapsedRealtime(), - historyId = historyId, - sendError = null, - ) - podState.activeCommand = command - source.onSuccess(command) - } else { - source.onError( - java.lang.IllegalStateException( - "Trying to send a command " + - "and the last command was not confirmed" + override fun createActiveCommand(historyId: String, basalProgram: BasalProgram?): + Single { + return Single.create { source -> + if (activeCommand == null) { + val command = OmnipodDashPodStateManager.ActiveCommand( + podState.messageSequenceNumber, + createdRealtime = SystemClock.elapsedRealtime(), + historyId = historyId, + sendError = null, + basalProgram = basalProgram, ) - ) + podState.activeCommand = command + source.onSuccess(command) + } else { + source.onError( + java.lang.IllegalStateException( + "Trying to send a command " + + "and the last command was not confirmed" + ) + ) + } } } - } @Synchronized override fun observeNoActiveCommand(): Observable { @@ -231,34 +230,72 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( @Synchronized override fun updateActiveCommand() = Maybe.create { source -> - podState.activeCommand?.run { - logger.debug( - "Trying to confirm active command with parameters: $activeCommand " + - "lastResponse=$lastStatusResponseReceived " + - "$sequenceNumberOfLastProgrammingCommand $historyId" - ) - - if (sentRealtime < createdRealtime) { // command was not sent, clear it up + val activeCommand = podState.activeCommand + if (activeCommand == null) { + logger.error("No active command to update") + source.onComplete() + return@create + } + val cmdConfirmation = getCommandConfirmationFromState() + logger.info(LTag.PUMPCOMM, "Update active command with confirmation: $cmdConfirmation") + when (cmdConfirmation) { + CommandSendingFailure -> { podState.activeCommand = null source.onError( - this.sendError + activeCommand?.sendError ?: java.lang.IllegalStateException( "Could not send command and sendError is " + "missing" ) ) - } else if (createdRealtime >= lastStatusResponseReceived) - // we did not receive a valid response yet - source.onComplete() - else { - podState.activeCommand = null - if (sequenceNumberOfLastProgrammingCommand == sequence) - source.onSuccess(CommandConfirmed(historyId, true)) - else - source.onSuccess(CommandConfirmed(historyId, false)) } - } ?: source.onComplete() - // no active programming command + + CommandSendingNotConfirmed -> { + // we did not receive a valid response yet + source.onComplete() + } + + CommandConfirmationDenied -> { + podState.activeCommand = null + source.onSuccess(CommandConfirmed(activeCommand, false)) + } + + CommandConfirmationSuccess -> { + podState.activeCommand = null + + source.onSuccess(CommandConfirmed(activeCommand, true)) + } + + NoActiveCommand -> { + source.onComplete() + } + } + } + + @Synchronized + override fun getCommandConfirmationFromState(): CommandConfirmationFromState { + return podState.activeCommand?.run { + logger.debug( + "Getting command state with parameters: $activeCommand " + + "lastResponse=$lastStatusResponseReceived " + + "$sequenceNumberOfLastProgrammingCommand $historyId" + ) + when { + createdRealtime <= podState.lastStatusResponseReceived && + sequence == podState.sequenceNumberOfLastProgrammingCommand -> + CommandConfirmationSuccess + createdRealtime <= podState.lastStatusResponseReceived && + sequence != podState.sequenceNumberOfLastProgrammingCommand -> + CommandConfirmationDenied + // no response received after this point + createdRealtime <= sentRealtime -> + CommandSendingNotConfirmed + createdRealtime > sentRealtime -> + CommandSendingFailure + else -> // this can't happen, see the previous two conditions + NoActiveCommand + } + } ?: NoActiveCommand } override fun increaseEapAkaSequenceNumber(): ByteArray { @@ -386,7 +423,6 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( class PodState : Serializable { var activationProgress: ActivationProgress = ActivationProgress.NOT_STARTED - var lastConnection: Long = 0 var lastUpdatedSystem: Long = 0 var lastStatusResponseReceived: Long = 0 var bluetoothConnectionState: OmnipodDashPodStateManager.BluetoothConnectionState = diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/history/DashHistory.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/history/DashHistory.kt index dea243e814..f1e91651f7 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/history/DashHistory.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/history/DashHistory.kt @@ -1,10 +1,12 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.history import com.github.guepardoapps.kulid.ULID +import info.nightscout.androidaps.logging.AAPSLogger +import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType.SET_BOLUS import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType.SET_TEMPORARY_BASAL -import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.BolusRecord import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.HistoryRecord import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.InitialResult @@ -20,7 +22,8 @@ import javax.inject.Inject class DashHistory @Inject constructor( private val dao: HistoryRecordDao, - private val historyMapper: HistoryMapper + private val historyMapper: HistoryMapper, + private val logger: AAPSLogger ) { private fun markSuccess(id: String): Completable = dao.markResolved( @@ -52,29 +55,29 @@ class DashHistory @Inject constructor( bolusRecord: BolusRecord? = null, resolveResult: ResolvedResult? = null, resolvedAt: Long? = null - ): Single { + ): Single = Single.defer { val id = ULID.random() when { commandType == SET_BOLUS && bolusRecord == null -> - return Single.error(IllegalArgumentException("bolusRecord missing on SET_BOLUS")) + Single.error(IllegalArgumentException("bolusRecord missing on SET_BOLUS")) commandType == SET_TEMPORARY_BASAL && tempBasalRecord == null -> - return Single.error(IllegalArgumentException("tempBasalRecord missing on SET_TEMPORARY_BASAL")) + Single.error(IllegalArgumentException("tempBasalRecord missing on SET_TEMPORARY_BASAL")) + else -> + dao.save( + HistoryRecordEntity( + id = id, + date = date, + createdAt = currentTimeMillis(), + commandType = commandType, + tempBasalRecord = tempBasalRecord, + bolusRecord = bolusRecord, + initialResult = initialResult, + resolvedResult = resolveResult, + resolvedAt = resolvedAt + ) + ).toSingle { id } } - - return dao.save( - HistoryRecordEntity( - id = id, - date = date, - createdAt = currentTimeMillis(), - commandType = commandType, - tempBasalRecord = tempBasalRecord, - bolusRecord = bolusRecord, - initialResult = initialResult, - resolvedResult = resolveResult, - resolvedAt = resolvedAt - ) - ).toSingle { id } } fun getRecords(): Single> = @@ -83,26 +86,23 @@ class DashHistory @Inject constructor( fun getRecordsAfter(time: Long): Single> = dao.allSince(time) fun updateFromState(podState: OmnipodDashPodStateManager) = Completable.defer { - podState.activeCommand?.run { - when { - createdRealtime <= podState.lastStatusResponseReceived && - sequence == podState.sequenceNumberOfLastProgrammingCommand -> - dao.setInitialResult(historyId, InitialResult.SENT) - .andThen(markSuccess(historyId)) - - createdRealtime <= podState.lastStatusResponseReceived && - sequence != podState.sequenceNumberOfLastProgrammingCommand -> - markFailure(historyId) - - // no response received after this point - createdRealtime <= sentRealtime -> - dao.setInitialResult(historyId, InitialResult.SENT) - - createdRealtime > sentRealtime -> - dao.setInitialResult(historyId, InitialResult.FAILURE_SENDING) - - else -> Completable.error(IllegalStateException("This can't happen. Could not update history")) - } - } ?: Completable.complete() // no active programming command + val historyId = podState.activeCommand?.historyId + if (historyId == null) { + logger.error(LTag.PUMP, "HistoryId not found to for updating from state") + return@defer Completable.complete() + } + when (podState.getCommandConfirmationFromState()) { + CommandSendingFailure -> + dao.setInitialResult(historyId, InitialResult.FAILURE_SENDING) + CommandSendingNotConfirmed -> + dao.setInitialResult(historyId, InitialResult.SENT) + CommandConfirmationDenied -> + markFailure(historyId) + CommandConfirmationSuccess -> + dao.setInitialResult(historyId, InitialResult.SENT) + .andThen(markSuccess(historyId)) + NoActiveCommand -> + Completable.complete() + } } } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt index f79180f061..16963b3da8 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt @@ -298,7 +298,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() { */ // base basal rate - podInfoBinding.baseBasalRate.text = if (podStateManager.basalProgram != null) { + podInfoBinding.baseBasalRate.text = if (podStateManager.basalProgram != null && !podStateManager.isSuspended) { resourceHelper.gs( R.string.pump_basebasalrate, omnipodDashPumpPlugin.model() @@ -357,7 +357,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() { private fun updateLastConnection() { if (podStateManager.isUniqueIdSet) { - podInfoBinding.lastConnection.text = readableDuration(podStateManager.lastConnection) + podInfoBinding.lastConnection.text = readableDuration(podStateManager.lastUpdatedSystem) val lastConnectionColor = if (omnipodDashPumpPlugin.isUnreachableAlertTimeoutExceeded(getPumpUnreachableTimeout().millis)) { Color.RED @@ -367,7 +367,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() { podInfoBinding.lastConnection.setTextColor(lastConnectionColor) } else { podInfoBinding.lastConnection.setTextColor(Color.WHITE) - podInfoBinding.lastConnection.text = readableDuration(podStateManager.lastConnection) + podInfoBinding.lastConnection.text = readableDuration(podStateManager.lastUpdatedSystem) } } @@ -518,7 +518,9 @@ class OmnipodDashOverviewFragment : DaggerFragment() { private fun updateSuspendDeliveryButton() { // If the Pod is currently suspended, we show the Resume delivery button instead. - if (isSuspendDeliveryButtonEnabled() && + // TODO: isSuspendDeliveryButtonEnabled doesn't work + val isSuspendDeliveryButtonEnabled = true + if (isSuspendDeliveryButtonEnabled && podStateManager.isPodRunning && (!podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandSuspendDelivery::class.java)) ) {