Merge pull request #40 from 0pen-dash/avereha/bolus

avereha/bolus
This commit is contained in:
Andrei Vereha 2021-07-11 12:25:17 +02:00 committed by GitHub
commit 5cc36fc821
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 729 additions and 216 deletions

View file

@ -344,6 +344,22 @@ interface PumpSync {
**/ **/
fun invalidateTemporaryBasal(id: Long): Boolean fun invalidateTemporaryBasal(id: Long): Boolean
/**
* Invalidate of temporary basals that failed to start
* Dash specific, replace by setting duration to zero ????
*
* If exists, isValid is set false
* If db record doesn't exist data is ignored and false returned
*
*
* @param pumpId pumpId of temporary basal
* @param pumpType pump type like PumpType.ACCU_CHEK_COMBO
* @param pumpSerial pump serial number
* @return true if running record is found and invalidated
**/
fun invalidateTemporaryBasalWithPumpId(pumpId: Long, pumpType: PumpType, pumpSerial: String): Boolean
/** /**
* Invalidate of temporary basals that failed to start * Invalidate of temporary basals that failed to start
* MDT specific * MDT specific

View file

@ -330,6 +330,19 @@ class PumpSyncImplementation @Inject constructor(
} }
} }
override fun invalidateTemporaryBasalWithPumpId(pumpId: Long, pumpType: PumpType, pumpSerial: String): Boolean {
repository.runTransactionForResult(InvalidateTemporaryBasalTransactionWithPumpId(pumpId, pumpType.toDbPumpType(),
pumpSerial))
.doOnError { aapsLogger.error(LTag.DATABASE, "Error while invalidating TemporaryBasal", it) }
.blockingGet()
.also { result ->
result.invalidated.forEach {
aapsLogger.debug(LTag.DATABASE, "Invalidated TemporaryBasal $it")
}
return result.invalidated.size > 0
}
}
override fun invalidateTemporaryBasalWithTempId(temporaryId: Long): Boolean { override fun invalidateTemporaryBasalWithTempId(temporaryId: Long): Boolean {
repository.runTransactionForResult(InvalidateTemporaryBasalWithTempIdTransaction(temporaryId)) repository.runTransactionForResult(InvalidateTemporaryBasalWithTempIdTransaction(temporaryId))
.doOnError { aapsLogger.error(LTag.DATABASE, "Error while invalidating TemporaryBasal", it) } .doOnError { aapsLogger.error(LTag.DATABASE, "Error while invalidating TemporaryBasal", it) }

View file

@ -0,0 +1,25 @@
package info.nightscout.androidaps.database.transactions
import info.nightscout.androidaps.database.embedments.InterfaceIDs
import info.nightscout.androidaps.database.entities.TemporaryBasal
class InvalidateTemporaryBasalTransactionWithPumpId(val pumpId: Long, val pumpType: InterfaceIDs.PumpType, val
pumpSerial:
String) :
Transaction<InvalidateTemporaryBasalTransactionWithPumpId.TransactionResult>() {
override fun run(): TransactionResult {
val result = TransactionResult()
val temporaryBasal = database.temporaryBasalDao.findByPumpIds(pumpId, pumpType, pumpSerial)
?: throw IllegalArgumentException("There is no such Temporary Basal with the specified temp ID.")
temporaryBasal.isValid = false
database.temporaryBasalDao.updateExistingEntry(temporaryBasal)
result.invalidated.add(temporaryBasal)
return result
}
class TransactionResult {
val invalidated = mutableListOf<TemporaryBasal>()
}
}

View file

@ -12,11 +12,12 @@ import info.nightscout.androidaps.plugins.bus.RxBusWrapper
import info.nightscout.androidaps.plugins.common.ManufacturerType import info.nightscout.androidaps.plugins.common.ManufacturerType
import info.nightscout.androidaps.plugins.general.actions.defs.CustomAction import info.nightscout.androidaps.plugins.general.actions.defs.CustomAction
import info.nightscout.androidaps.plugins.general.actions.defs.CustomActionType import info.nightscout.androidaps.plugins.general.actions.defs.CustomActionType
import info.nightscout.androidaps.plugins.general.overview.events.EventOverviewBolusProgress
import info.nightscout.androidaps.plugins.pump.common.defs.PumpType import info.nightscout.androidaps.plugins.pump.common.defs.PumpType
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.* 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.OmnipodDashManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.OmnipodDashBleManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.ActivationProgress import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.ActivationProgress
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.BeepType 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.DeliveryStatus
@ -35,16 +36,15 @@ import info.nightscout.androidaps.queue.commands.CustomCommand
import info.nightscout.androidaps.utils.T import info.nightscout.androidaps.utils.T
import info.nightscout.androidaps.utils.TimeChangeType import info.nightscout.androidaps.utils.TimeChangeType
import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.rx.AapsSchedulers
import info.nightscout.androidaps.utils.sharedPreferences.SP import info.nightscout.androidaps.utils.sharedPreferences.SP
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.rxkotlin.plusAssign
import io.reactivex.rxkotlin.subscribeBy
import org.json.JSONObject import org.json.JSONObject
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.math.ceil
import kotlin.random.Random import kotlin.random.Random
@Singleton @Singleton
@ -56,6 +56,9 @@ class OmnipodDashPumpPlugin @Inject constructor(
private val history: DashHistory, private val history: DashHistory,
private val pumpSync: PumpSync, private val pumpSync: PumpSync,
private val rxBus: RxBusWrapper, private val rxBus: RxBusWrapper,
private val aapsSchedulers: AapsSchedulers,
private val bleManager: OmnipodDashBleManager,
// private val disposable: CompositeDisposable = CompositeDisposable(), // private val disposable: CompositeDisposable = CompositeDisposable(),
// private val aapsSchedulers: AapsSchedulers, // private val aapsSchedulers: AapsSchedulers,
@ -64,8 +67,11 @@ class OmnipodDashPumpPlugin @Inject constructor(
resourceHelper: ResourceHelper, resourceHelper: ResourceHelper,
commandQueue: CommandQueueProvider commandQueue: CommandQueueProvider
) : PumpPluginBase(pluginDescription, injector, aapsLogger, resourceHelper, commandQueue), Pump { ) : PumpPluginBase(pluginDescription, injector, aapsLogger, resourceHelper, commandQueue), Pump {
@Volatile var bolusCanceled = false
companion object { companion object {
private const val BOLUS_RETRY_INTERVAL_MS = 2000.toLong()
private const val BOLUS_RETRIES = 5 // numer of retries for cancel/get bolus status
private val pluginDescription = PluginDescription() private val pluginDescription = PluginDescription()
.mainType(PluginType.PUMP) .mainType(PluginType.PUMP)
@ -94,6 +100,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
} }
override fun isConnected(): Boolean { override fun isConnected(): Boolean {
return true return true
} }
@ -124,18 +131,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
} }
override fun getPumpStatus(reason: String) { override fun getPumpStatus(reason: String) {
val throwable = Completable.concat( val throwable = getPodStatus().blockingGet()
listOf(
omnipodManager
.getStatus(ResponseType.StatusResponseType.DEFAULT_STATUS_RESPONSE)
.ignoreElements(),
history.updateFromState(podStateManager),
podStateManager.updateActiveCommand()
.map { handleCommandConfirmation(it) }
.ignoreElement(),
checkPodKaput()
)
).blockingGet()
if (throwable != null) { if (throwable != null) {
aapsLogger.error(LTag.PUMP, "Error in getPumpStatus", throwable) aapsLogger.error(LTag.PUMP, "Error in getPumpStatus", throwable)
} else { } else {
@ -143,26 +139,58 @@ class OmnipodDashPumpPlugin @Inject constructor(
} }
} }
private fun getPodStatus(): Completable = Completable.concat(
listOf(
omnipodManager
.getStatus(ResponseType.StatusResponseType.DEFAULT_STATUS_RESPONSE)
.ignoreElements(),
history.updateFromState(podStateManager),
podStateManager.updateActiveCommand()
.map { handleCommandConfirmation(it) }
.ignoreElement(),
checkPodKaput(),
)
)
private fun checkPodKaput(): Completable = Completable.defer { private fun checkPodKaput(): Completable = Completable.defer {
val tbr = pumpSync.expectedPumpState().temporaryBasal val tbr = pumpSync.expectedPumpState().temporaryBasal
if (podStateManager.isPodKaput && if (podStateManager.isPodKaput) {
(tbr == null || tbr.rate != 0.0) if (tbr == null || tbr.rate != 0.0) {
) { pumpSync.syncTemporaryBasalWithPumpId(
pumpSync.syncTemporaryBasalWithPumpId( timestamp = System.currentTimeMillis(),
timestamp = System.currentTimeMillis(), rate = 0.0,
rate = 0.0, duration = T.mins(PodConstants.MAX_POD_LIFETIME.standardMinutes).msecs(),
duration = T.mins(PodConstants.MAX_POD_LIFETIME.standardMinutes).msecs(), isAbsolute = true,
isAbsolute = true, type = PumpSync.TemporaryBasalType.PUMP_SUSPEND,
type = PumpSync.TemporaryBasalType.PUMP_SUSPEND, pumpId = Random.Default.nextLong(), // we don't use this, just make sure it's unique
pumpId = Random.Default.nextLong(), // we don't use this, just make sure it's unique pumpType = PumpType.OMNIPOD_DASH,
pumpType = PumpType.OMNIPOD_DASH, pumpSerial = serialNumber()
pumpSerial = serialNumber() )
) }
podStateManager.lastBolus?.run {
if (!deliveryComplete) {
val deliveredUnits = markComplete()
deliveryComplete = true
val bolusHistoryEntry = history.getById(historyId)
val sync = pumpSync.syncBolusWithPumpId(
timestamp = bolusHistoryEntry.createdAt,
amount = deliveredUnits,
pumpId = bolusHistoryEntry.pumpId(),
pumpType = PumpType.OMNIPOD_DASH,
pumpSerial = serialNumber(),
type = bolusType
)
aapsLogger.info(LTag.PUMP, "syncBolusWithPumpId on CANCEL_BOLUS returned: $sync")
}
}
} }
Completable.complete() Completable.complete()
} }
override fun setNewBasalProfile(profile: Profile): PumpEnactResult { override fun setNewBasalProfile(profile: Profile): PumpEnactResult {
if (!podStateManager.isActivationCompleted) {
return PumpEnactResult(injector).success(true).enacted(true)
}
val basalProgram = mapProfileToBasalProgram(profile) val basalProgram = mapProfileToBasalProgram(profile)
return executeProgrammingCommand( return executeProgrammingCommand(
pre = suspendDeliveryIfActive(), pre = suspendDeliveryIfActive(),
@ -170,7 +198,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
activeCommandEntry = { historyId -> activeCommandEntry = { historyId ->
podStateManager.createActiveCommand(historyId, basalProgram = basalProgram) podStateManager.createActiveCommand(historyId, basalProgram = basalProgram)
}, },
command = omnipodManager.setBasalProgram(basalProgram).ignoreElements(), command = omnipodManager.setBasalProgram(basalProgram, hasBasalBeepEnabled()).ignoreElements(),
post = failWhenUnconfirmed(), post = failWhenUnconfirmed(),
).toPumpEnactResult() ).toPumpEnactResult()
} }
@ -190,11 +218,10 @@ class OmnipodDashPumpPlugin @Inject constructor(
else else
executeProgrammingCommand( executeProgrammingCommand(
historyEntry = history.createRecord(OmnipodCommandType.SUSPEND_DELIVERY), historyEntry = history.createRecord(OmnipodCommandType.SUSPEND_DELIVERY),
command = omnipodManager.suspendDelivery() command = omnipodManager.suspendDelivery(hasBasalBeepEnabled())
.filter { podEvent -> podEvent is PodEvent.CommandSent } .filter { podEvent -> podEvent.isCommandSent() }
.map { .map {
pumpSyncTempBasal( pumpSyncTempBasal(
it,
0.0, 0.0,
PodConstants.MAX_POD_LIFETIME.standardMinutes, PodConstants.MAX_POD_LIFETIME.standardMinutes,
PumpSync.TemporaryBasalType.PUMP_SUSPEND PumpSync.TemporaryBasalType.PUMP_SUSPEND
@ -205,12 +232,12 @@ class OmnipodDashPumpPlugin @Inject constructor(
) )
} }
/* override fun onStop() { /* override fun onStop() {
super.onStop() super.onStop()
disposable.clear() disposable.clear()
} }
*/ */
private fun observeDeliverySuspended(): Completable = Completable.defer { private fun observeDeliverySuspended(): Completable = Completable.defer {
if (podStateManager.deliveryStatus == DeliveryStatus.SUSPENDED) if (podStateManager.deliveryStatus == DeliveryStatus.SUSPENDED)
@ -240,7 +267,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
get() { get() {
val date = Date() val date = Date()
val ret = podStateManager.basalProgram?.rateAt(date) ?: 0.0 val ret = podStateManager.basalProgram?.rateAt(date) ?: 0.0
aapsLogger.info(LTag.PUMP, "baseBasalRate: %ret at $date}") aapsLogger.info(LTag.PUMP, "baseBasalRate: $ret at $date}")
return if (podStateManager.alarmType != null) { return if (podStateManager.alarmType != null) {
0.0 0.0
} else } else
@ -264,65 +291,211 @@ class OmnipodDashPumpPlugin @Inject constructor(
get() = 0 get() = 0
override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult { override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult {
// TODO update Treatments (?) try {
// TODO bolus progress aapsLogger.info(LTag.PUMP, "Delivering treatment: $detailedBolusInfo")
// TODO report actual delivered amount after Pod Alarm and bolus cancellation val beepsConfigurationKey = if (detailedBolusInfo.bolusType == DetailedBolusInfo.BolusType.SMB)
R.string.key_omnipod_common_smb_beeps_enabled
else
R.string.key_omnipod_common_bolus_beeps_enabled
val bolusBeeps = sp.getBoolean(beepsConfigurationKey, false)
R.string.key_omnipod_common_smb_beeps_enabled
if (detailedBolusInfo.carbs > 0 ||
detailedBolusInfo.insulin == 0.0
) {
// Accept only valid insulin requests
return PumpEnactResult(injector)
.success(false)
.enacted(false)
.bolusDelivered(0.0)
.comment("Invalid input")
}
val requestedBolusAmount = detailedBolusInfo.insulin
if (requestedBolusAmount > reservoirLevel) {
return PumpEnactResult(injector)
.success(false)
.enacted(false)
.bolusDelivered(0.0)
.comment("Not enough insulin in the reservoir")
}
var deliveredBolusAmount = 0.0
return Single.create<PumpEnactResult> { source -> aapsLogger.info(
val bolusBeeps = sp.getBoolean(R.string.key_omnipod_common_bolus_beeps_enabled, false) LTag.PUMP,
"deliverTreatment: requestedBolusAmount=$requestedBolusAmount"
Observable.concat( )
history.createRecord( val ret = executeProgrammingCommand(
pre = observeDeliveryNotCanceled(),
historyEntry = history.createRecord(
commandType = OmnipodCommandType.SET_BOLUS, commandType = OmnipodCommandType.SET_BOLUS,
bolusRecord = BolusRecord( bolusRecord = BolusRecord(
detailedBolusInfo.insulin, requestedBolusAmount,
BolusType.fromBolusInfoBolusType(detailedBolusInfo.bolusType), BolusType.fromBolusInfoBolusType(detailedBolusInfo.bolusType)
), )
).flatMapObservable { recordId -> ),
podStateManager.createActiveCommand(recordId).toObservable() activeCommandEntry = { historyId ->
podStateManager.createActiveCommand(
historyId,
requestedBolus = requestedBolusAmount
)
}, },
omnipodManager.bolus( command = omnipodManager.bolus(
detailedBolusInfo.insulin, detailedBolusInfo.insulin,
bolusBeeps, bolusBeeps,
bolusBeeps bolusBeeps
), ).filter { podEvent -> podEvent.isCommandSent() }
history.updateFromState(podStateManager).toObservable(), .map { pumpSyncBolusStart(requestedBolusAmount, detailedBolusInfo.bolusType) }
podStateManager.updateActiveCommand().toObservable(), .ignoreElements(),
).subscribeBy( post = waitForBolusDeliveryToComplete(BOLUS_RETRIES, requestedBolusAmount, detailedBolusInfo.bolusType)
onNext = { podEvent -> .map {
aapsLogger.debug( deliveredBolusAmount = it
LTag.PUMP, aapsLogger.info(LTag.PUMP, "deliverTreatment: deliveredBolusAmount=$deliveredBolusAmount")
"Received PodEvent in deliverTreatment: $podEvent" }
) .ignoreElement()
}, ).toSingleDefault(
onError = { throwable -> PumpEnactResult(injector).success(true).enacted(true).bolusDelivered(deliveredBolusAmount)
aapsLogger.error(LTag.PUMP, "Error in deliverTreatment", throwable)
source.onSuccess(
PumpEnactResult(injector).success(false).enacted(false).comment(
throwable.toString()
)
)
},
onComplete = {
aapsLogger.debug("deliverTreatment completed")
source.onSuccess(
PumpEnactResult(injector).success(true).enacted(true)
.bolusDelivered(detailedBolusInfo.insulin)
.carbsDelivered(detailedBolusInfo.carbs)
)
}
) )
}.blockingGet() .onErrorReturnItem(
// success if canceled
PumpEnactResult(injector).success(bolusCanceled).enacted(false)
)
.blockingGet()
aapsLogger.info(LTag.PUMP, "deliverTreatment result: $ret")
return ret
} finally {
bolusCanceled = false
}
}
private fun observeDeliveryNotCanceled(): Completable = Completable.defer {
if (bolusCanceled) {
Completable.error(java.lang.IllegalStateException("Bolus canceled"))
} else {
Completable.complete()
}
}
private fun updateBolusProgressDialog(msg: String, percent: Int) {
val progressUpdateEvent = EventOverviewBolusProgress
val percent = percent
progressUpdateEvent.status = msg
progressUpdateEvent.percent = percent
rxBus.send(progressUpdateEvent)
}
private fun waitForBolusDeliveryToComplete(
maxTries: Int,
requestedBolusAmount: Double,
bolusType: DetailedBolusInfo.BolusType
): Single<Double> = Single.defer {
if (bolusCanceled && podStateManager.activeCommand != null) {
var errorGettingStatus: Throwable? = null
for (tries in 1..maxTries) {
errorGettingStatus = getPodStatus().blockingGet()
if (errorGettingStatus != null) {
aapsLogger.debug(LTag.PUMP, "waitForBolusDeliveryToComplete errorGettingStatus=$errorGettingStatus")
Thread.sleep(BOLUS_RETRY_INTERVAL_MS) // retry every 2 sec
continue
}
}
if (errorGettingStatus != null) {
// requestedBolusAmount will be updated later, via pumpSync
// This state is: cancellation requested and getPodStatus failed 5 times.
return@defer Single.just(requestedBolusAmount)
}
}
val estimatedDeliveryTimeSeconds = estimateBolusDeliverySeconds(requestedBolusAmount)
aapsLogger.info(LTag.PUMP, "estimatedDeliveryTimeSeconds: $estimatedDeliveryTimeSeconds")
var waited = 0
while (waited < estimatedDeliveryTimeSeconds && !bolusCanceled) {
waited += 1
Thread.sleep(1000)
if (bolusType == DetailedBolusInfo.BolusType.SMB) {
continue
}
val percent = (waited.toFloat() / estimatedDeliveryTimeSeconds) * 100
updateBolusProgressDialog(resourceHelper.gs(R.string.bolusdelivering, requestedBolusAmount), percent.toInt())
}
for (tryNumber in 1..maxTries) {
updateBolusProgressDialog("Checking delivery status", 100)
val cmd = if (bolusCanceled)
cancelBolus()
else
getPodStatus()
val errorGettingStatus = cmd.blockingGet()
if (errorGettingStatus != null) {
aapsLogger.debug(LTag.PUMP, "waitForBolusDeliveryToComplete errorGettingStatus=$errorGettingStatus")
Thread.sleep(BOLUS_RETRY_INTERVAL_MS) // retry every 3 sec
continue
}
val bolusDeliveringActive = podStateManager.deliveryStatus?.bolusDeliveringActive() ?: false
if (bolusDeliveringActive) {
// delivery not complete yet
val remainingUnits = podStateManager.lastBolus!!.bolusUnitsRemaining
val percent = ((requestedBolusAmount - remainingUnits) / requestedBolusAmount) * 100
updateBolusProgressDialog(
resourceHelper.gs(R.string.bolusdelivering, requestedBolusAmount),
percent.toInt()
)
val sleepSeconds = if (bolusCanceled)
BOLUS_RETRY_INTERVAL_MS
else
estimateBolusDeliverySeconds(remainingUnits)
Thread.sleep(sleepSeconds * 1000.toLong())
} else {
// delivery is complete. If pod is Kaput, we are handling this in getPodStatus
return@defer Single.just(podStateManager.lastBolus!!.deliveredUnits()!!)
}
}
Single.just(requestedBolusAmount) // will be updated later!
}
private fun cancelBolus(): Completable {
val bolusBeeps = sp.getBoolean(R.string.key_omnipod_common_bolus_beeps_enabled, false)
return executeProgrammingCommand(
historyEntry = history.createRecord(commandType = OmnipodCommandType.CANCEL_BOLUS),
command = omnipodManager.stopBolus(bolusBeeps).ignoreElements(),
checkNoActiveCommand = false,
)
}
private fun estimateBolusDeliverySeconds(requestedBolusAmount: Double): Long {
return ceil(requestedBolusAmount / 0.05).toLong() * 2 + 3
}
private fun pumpSyncBolusStart(
requestedBolusAmount: Double,
bolusType: DetailedBolusInfo.BolusType
): Boolean {
val activeCommand = podStateManager.activeCommand
if (activeCommand == null) {
throw IllegalArgumentException(
"No active command or illegal podEvent: " +
"activeCommand=$activeCommand"
)
}
val historyEntry = history.getById(activeCommand.historyId)
val ret = pumpSync.syncBolusWithPumpId(
timestamp = historyEntry.createdAt,
amount = requestedBolusAmount,
type = bolusType,
pumpId = historyEntry.pumpId(),
pumpType = PumpType.OMNIPOD_DASH,
pumpSerial = serialNumber()
)
aapsLogger.debug(LTag.PUMP, "pumpSyncBolusStart: $ret")
return ret
} }
override fun stopBolusDelivering() { override fun stopBolusDelivering() {
// TODO update Treatments (?)
aapsLogger.info(LTag.PUMP, "stopBolusDelivering called") aapsLogger.info(LTag.PUMP, "stopBolusDelivering called")
val ret = executeProgrammingCommand( bolusCanceled = true
historyEntry = history.createRecord(OmnipodCommandType.CANCEL_BOLUS),
command = omnipodManager.stopBolus().ignoreElements()
).toPumpEnactResult()
aapsLogger.info(LTag.PUMP, "stopBolusDelivering finished with result: $ret")
} }
override fun setTempBasalAbsolute( override fun setTempBasalAbsolute(
@ -332,13 +505,15 @@ class OmnipodDashPumpPlugin @Inject constructor(
enforceNew: Boolean, enforceNew: Boolean,
tbrType: PumpSync.TemporaryBasalType tbrType: PumpSync.TemporaryBasalType
): PumpEnactResult { ): PumpEnactResult {
val tempBasalBeeps = sp.getBoolean(R.string.key_omnipod_common_tbr_beeps_enabled, false) val tempBasalBeeps = hasTempBasalBeepEnabled()
aapsLogger.info( aapsLogger.info(
LTag.PUMP, LTag.PUMP,
"setTempBasalAbsolute: $durationInMinutes min :: $absoluteRate U/h :: " + "setTempBasalAbsolute: duration=$durationInMinutes min, rate=$absoluteRate U/h :: " +
"enforce: $enforceNew :: tbrType: $tbrType" "enforce=$enforceNew, tbrType=$tbrType"
) )
return executeProgrammingCommand(
val ret = executeProgrammingCommand(
pre = observeNoActiveTempBasal(true),
historyEntry = history.createRecord( historyEntry = history.createRecord(
commandType = OmnipodCommandType.SET_TEMPORARY_BASAL, commandType = OmnipodCommandType.SET_TEMPORARY_BASAL,
tempBasalRecord = TempBasalRecord(duration = durationInMinutes, rate = absoluteRate) tempBasalRecord = TempBasalRecord(duration = durationInMinutes, rate = absoluteRate)
@ -358,29 +533,31 @@ class OmnipodDashPumpPlugin @Inject constructor(
durationInMinutes.toShort(), durationInMinutes.toShort(),
tempBasalBeeps tempBasalBeeps
) )
.filter { podEvent -> podEvent is PodEvent.CommandSent } .filter { podEvent -> podEvent.isCommandSent() }
.map { pumpSyncTempBasal(it, absoluteRate, durationInMinutes.toLong(), tbrType) } .map { pumpSyncTempBasal(absoluteRate, durationInMinutes.toLong(), tbrType) }
.ignoreElements(), .ignoreElements(),
pre = observeNoActiveTempBasal(true),
).toPumpEnactResult() ).toPumpEnactResult()
if (ret.success && ret.enacted) {
ret.isPercent(false).absolute(absoluteRate).duration(durationInMinutes)
}
aapsLogger.info(LTag.PUMP, "setTempBasalAbsolute: result=$ret")
return ret
} }
private fun pumpSyncTempBasal( private fun pumpSyncTempBasal(
podEvent: PodEvent,
absoluteRate: Double, absoluteRate: Double,
durationInMinutes: Long, durationInMinutes: Long,
tbrType: PumpSync.TemporaryBasalType tbrType: PumpSync.TemporaryBasalType
): Boolean { ): Boolean {
val activeCommand = podStateManager.activeCommand val activeCommand = podStateManager.activeCommand
if (activeCommand == null || podEvent !is PodEvent.CommandSent) { if (activeCommand == null) {
throw IllegalArgumentException( throw IllegalArgumentException(
"No active command or illegal podEvent: " + "No active command: " +
"activeCommand=$activeCommand" + "activeCommand=$activeCommand"
"podEvent=$podEvent"
) )
} }
val historyEntry = history.getById(activeCommand.historyId) val historyEntry = history.getById(activeCommand.historyId)
aapsLogger.debug(LTag.PUMP, "pumpSyncTempBasal: absoluteRate=$absoluteRate, durationInMinutes=$durationInMinutes")
val ret = pumpSync.syncTemporaryBasalWithPumpId( val ret = pumpSync.syncTemporaryBasalWithPumpId(
timestamp = historyEntry.createdAt, timestamp = historyEntry.createdAt,
rate = absoluteRate, rate = absoluteRate,
@ -391,11 +568,11 @@ class OmnipodDashPumpPlugin @Inject constructor(
pumpType = PumpType.OMNIPOD_DASH, pumpType = PumpType.OMNIPOD_DASH,
pumpSerial = serialNumber() pumpSerial = serialNumber()
) )
aapsLogger.debug(LTag.PUMP, "Pump sync temp basal: $ret") aapsLogger.debug(LTag.PUMP, "pumpSyncTempBasal: $ret")
return ret return ret
} }
private fun observeNoActiveTempBasal(): Completable { private fun observeNoActiveTempBasal(enforceNew: Boolean): Completable {
return Completable.defer { return Completable.defer {
when { when {
podStateManager.deliveryStatus !in podStateManager.deliveryStatus !in
@ -417,27 +594,13 @@ class OmnipodDashPumpPlugin @Inject constructor(
aapsLogger.info(LTag.PUMP, "Canceling existing temp basal") aapsLogger.info(LTag.PUMP, "Canceling existing temp basal")
executeProgrammingCommand( executeProgrammingCommand(
historyEntry = history.createRecord(OmnipodCommandType.CANCEL_TEMPORARY_BASAL), historyEntry = history.createRecord(OmnipodCommandType.CANCEL_TEMPORARY_BASAL),
command = omnipodManager.stopTempBasal().ignoreElements() command = omnipodManager.stopTempBasal(hasTempBasalBeepEnabled()).ignoreElements()
) )
} }
} }
} }
} }
private fun observeActiveTempBasal(): Completable {
return Completable.defer {
if (podStateManager.tempBasalActive || pumpSync.expectedPumpState().temporaryBasal != null)
Completable.complete()
else
Completable.error(
java.lang.IllegalStateException(
"There is no active basal to cancel"
)
)
}
}
override fun setTempBasalPercent( override fun setTempBasalPercent(
percent: Int, percent: Int,
durationInMinutes: Int, durationInMinutes: Int,
@ -456,15 +619,29 @@ class OmnipodDashPumpPlugin @Inject constructor(
.comment("Omnipod Dash driver does not support extended boluses") .comment("Omnipod Dash driver does not support extended boluses")
} }
private fun hasTempBasalBeepEnabled(): Boolean {
return sp.getBoolean(R.string.key_omnipod_common_tbr_beeps_enabled, false)
}
private fun hasBasalBeepEnabled(): Boolean {
return sp.getBoolean(R.string.key_omnipod_common_basal_beeps_enabled, false)
}
override fun cancelTempBasal(enforceNew: Boolean): PumpEnactResult { override fun cancelTempBasal(enforceNew: Boolean): PumpEnactResult {
if (!podStateManager.tempBasalActive &&
pumpSync.expectedPumpState().temporaryBasal == null
) {
// nothing to cancel
return PumpEnactResult(injector).success(true).enacted(false)
}
return executeProgrammingCommand( return executeProgrammingCommand(
historyEntry = history.createRecord(OmnipodCommandType.CANCEL_TEMPORARY_BASAL), historyEntry = history.createRecord(OmnipodCommandType.CANCEL_TEMPORARY_BASAL),
command = omnipodManager.stopTempBasal().ignoreElements(), command = omnipodManager.stopTempBasal(hasTempBasalBeepEnabled()).ignoreElements(),
pre = observeActiveTempBasal(),
).toPumpEnactResult() ).toPumpEnactResult()
} }
fun Completable.toPumpEnactResult(): PumpEnactResult { private fun Completable.toPumpEnactResult(): PumpEnactResult {
return this.toSingleDefault(PumpEnactResult(injector).success(true).enacted(true)) return this.toSingleDefault(PumpEnactResult(injector).success(true).enacted(true))
.doOnError { throwable -> .doOnError { throwable ->
aapsLogger.error(LTag.PUMP, "toPumpEnactResult, error executing command: $throwable") aapsLogger.error(LTag.PUMP, "toPumpEnactResult, error executing command: $throwable")
@ -567,11 +744,10 @@ class OmnipodDashPumpPlugin @Inject constructor(
private fun suspendDelivery(): PumpEnactResult { private fun suspendDelivery(): PumpEnactResult {
return executeProgrammingCommand( return executeProgrammingCommand(
historyEntry = history.createRecord(OmnipodCommandType.SUSPEND_DELIVERY), historyEntry = history.createRecord(OmnipodCommandType.SUSPEND_DELIVERY),
command = omnipodManager.suspendDelivery() command = omnipodManager.suspendDelivery(hasBasalBeepEnabled())
.filter { podEvent -> podEvent is PodEvent.CommandSent } .filter { podEvent -> podEvent.isCommandSent() }
.map { .map {
pumpSyncTempBasal( pumpSyncTempBasal(
it,
0.0, 0.0,
PodConstants.MAX_POD_LIFETIME.standardMinutes, PodConstants.MAX_POD_LIFETIME.standardMinutes,
PumpSync.TemporaryBasalType.PUMP_SUSPEND PumpSync.TemporaryBasalType.PUMP_SUSPEND
@ -594,16 +770,20 @@ class OmnipodDashPumpPlugin @Inject constructor(
executeProgrammingCommand( executeProgrammingCommand(
pre = observeDeliverySuspended(), pre = observeDeliverySuspended(),
historyEntry = history.createRecord(OmnipodCommandType.RESUME_DELIVERY), historyEntry = history.createRecord(OmnipodCommandType.RESUME_DELIVERY),
command = omnipodManager.setBasalProgram(mapProfileToBasalProgram(it)).ignoreElements() command = omnipodManager.setBasalProgram(mapProfileToBasalProgram(it), hasBasalBeepEnabled()).ignoreElements()
).toPumpEnactResult() ).toPumpEnactResult()
} ?: PumpEnactResult(injector).success(false).enacted(false).comment("No profile active") // TODO i18n } ?: PumpEnactResult(injector).success(false).enacted(false).comment("No profile active") // TODO i18n
} }
private fun deactivatePod(): PumpEnactResult { private fun deactivatePod(): PumpEnactResult {
return executeProgrammingCommand( val ret = executeProgrammingCommand(
historyEntry = history.createRecord(OmnipodCommandType.DEACTIVATE_POD), historyEntry = history.createRecord(OmnipodCommandType.DEACTIVATE_POD),
command = omnipodManager.deactivatePod().ignoreElements() command = omnipodManager.deactivatePod().ignoreElements()
).toPumpEnactResult() ).toPumpEnactResult()
if (podStateManager.activeCommand != null) {
ret.success(false)
}
return ret
} }
private fun handleTimeChange(): PumpEnactResult { private fun handleTimeChange(): PumpEnactResult {
@ -651,11 +831,15 @@ class OmnipodDashPumpPlugin @Inject constructor(
{ historyId -> podStateManager.createActiveCommand(historyId) }, { historyId -> podStateManager.createActiveCommand(historyId) },
command: Completable, command: Completable,
post: Completable = Completable.complete(), post: Completable = Completable.complete(),
checkNoActiveCommand: Boolean = true
): Completable { ): Completable {
return Completable.concat( return Completable.concat(
listOf( listOf(
pre, pre,
podStateManager.observeNoActiveCommand().ignoreElements(), if (checkNoActiveCommand)
podStateManager.observeNoActiveCommand()
else
Completable.complete(),
historyEntry historyEntry
.flatMap { activeCommandEntry(it) } .flatMap { activeCommandEntry(it) }
.ignoreElement(), .ignoreElement(),
@ -667,6 +851,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
podStateManager.updateActiveCommand() podStateManager.updateActiveCommand()
.map { handleCommandConfirmation(it) } .map { handleCommandConfirmation(it) }
.ignoreElement(), .ignoreElement(),
checkPodKaput(),
post, post,
) )
) )
@ -712,9 +897,13 @@ class OmnipodDashPumpPlugin @Inject constructor(
OmnipodCommandType.SET_TEMPORARY_BASAL -> { OmnipodCommandType.SET_TEMPORARY_BASAL -> {
// This treatment was synced before sending the command // This treatment was synced before sending the command
aapsLogger.info(LTag.PUMPCOMM, "temporary basal denied. PumpId: ${historyEntry.pumpId()}")
if (!confirmation.success) { if (!confirmation.success) {
pumpSync.invalidateTemporaryBasal(historyEntry.pumpId()) aapsLogger.info(LTag.PUMPCOMM, "temporary basal denied. PumpId: ${historyEntry.pumpId()}")
pumpSync.invalidateTemporaryBasalWithPumpId(
historyEntry.pumpId(),
PumpType.OMNIPOD_DASH,
serialNumber()
)
} else { } else {
podStateManager.tempBasal = command.tempBasal podStateManager.tempBasal = command.tempBasal
} }
@ -722,17 +911,68 @@ class OmnipodDashPumpPlugin @Inject constructor(
OmnipodCommandType.SUSPEND_DELIVERY -> { OmnipodCommandType.SUSPEND_DELIVERY -> {
if (!confirmation.success) { if (!confirmation.success) {
pumpSync.invalidateTemporaryBasal(historyEntry.pumpId()) pumpSync.invalidateTemporaryBasalWithPumpId(historyEntry.pumpId(), PumpType.OMNIPOD_DASH, serialNumber())
} else { } else {
podStateManager.tempBasal = null podStateManager.tempBasal = null
} }
} }
OmnipodCommandType.SET_BOLUS -> {
if (confirmation.success) {
if (command.requestedBolus == null) {
aapsLogger.error(LTag.PUMP, "Requested bolus not found: $command")
}
val record = historyEntry.record
if (record !is BolusRecord) {
aapsLogger.error(
LTag.PUMP,
"Expected SET_BOLUS history record to be a BolusRecord, found " +
"$record"
)
}
record as BolusRecord
podStateManager.createLastBolus(
record.amout,
command.historyId,
record.bolusType.toBolusInfoBolusType()
)
} else {
pumpSync.syncBolusWithPumpId(
timestamp = historyEntry.createdAt,
amount = 0.0,
pumpId = historyEntry.pumpId(),
pumpType = PumpType.OMNIPOD_DASH,
pumpSerial = serialNumber(),
type = null // TODO: set the correct bolus type here!!!
)
}
}
OmnipodCommandType.CANCEL_BOLUS -> {
if (confirmation.success) {
podStateManager.lastBolus?.run {
val deliveredUnits = markComplete()
val bolusHistoryEntry = history.getById(historyId)
val sync = pumpSync.syncBolusWithPumpId(
timestamp = bolusHistoryEntry.createdAt,
amount = deliveredUnits, // we just marked this bolus as complete
pumpId = bolusHistoryEntry.pumpId(),
pumpType = PumpType.OMNIPOD_DASH,
pumpSerial = serialNumber(),
type = bolusType
)
aapsLogger.info(LTag.PUMP, "syncBolusWithPumpId on CANCEL_BOLUS returned: $sync")
} ?: aapsLogger.error(LTag.PUMP, "Cancelled bolus that does not exist")
}
}
else -> else ->
aapsLogger.warn( aapsLogger.warn(
LTag.PUMP, LTag.PUMP,
"Will not sync confirmed command of type: $historyEntry and " + "Will not sync confirmed command of type: $historyEntry and " +
"succes: ${confirmation.success}" "success: ${confirmation.success}"
) )
} }
} }

View file

@ -18,19 +18,19 @@ interface OmnipodDashManager {
fun getStatus(type: ResponseType.StatusResponseType): Observable<PodEvent> fun getStatus(type: ResponseType.StatusResponseType): Observable<PodEvent>
fun setBasalProgram(basalProgram: BasalProgram): Observable<PodEvent> fun setBasalProgram(basalProgram: BasalProgram, hasBasalBeepEnabled: Boolean): Observable<PodEvent>
fun suspendDelivery(): Observable<PodEvent> fun suspendDelivery(hasBasalBeepEnabled: Boolean): Observable<PodEvent>
fun setTime(): Observable<PodEvent> fun setTime(): Observable<PodEvent>
fun setTempBasal(rate: Double, durationInMinutes: Short, tempBasalBeeps: Boolean): Observable<PodEvent> fun setTempBasal(rate: Double, durationInMinutes: Short, tempBasalBeeps: Boolean): Observable<PodEvent>
fun stopTempBasal(): Observable<PodEvent> fun stopTempBasal(hasTempBasalBeepEnabled: Boolean): Observable<PodEvent>
fun bolus(units: Double, confirmationBeeps: Boolean, completionBeeps: Boolean): Observable<PodEvent> fun bolus(units: Double, confirmationBeeps: Boolean, completionBeeps: Boolean): Observable<PodEvent>
fun stopBolus(): Observable<PodEvent> fun stopBolus(beep: Boolean): Observable<PodEvent>
fun playBeep(beepType: BeepType): Observable<PodEvent> fun playBeep(beepType: BeepType): Observable<PodEvent>

View file

@ -77,7 +77,8 @@ class OmnipodDashManagerImpl @Inject constructor(
private val observeConnectToPod: Observable<PodEvent> private val observeConnectToPod: Observable<PodEvent>
get() = Observable.defer { get() = Observable.defer {
bleManager.connect() bleManager.connect()
} // TODO add retry .doOnError { throwable -> logger.warn(LTag.PUMPBTCOMM, "observeConnectToPod error=$throwable") }
}
private val observePairNewPod: Observable<PodEvent> private val observePairNewPod: Observable<PodEvent>
get() = Observable.defer { get() = Observable.defer {
@ -156,7 +157,7 @@ class OmnipodDashManagerImpl @Inject constructor(
} }
} }
private fun observeSendProgramBasalCommand(basalProgram: BasalProgram): Observable<PodEvent> { private fun observeSendProgramBasalCommand(basalProgram: BasalProgram, hasBasalBeepEnabled: Boolean): Observable<PodEvent> {
return Observable.defer { return Observable.defer {
val currentTime = Date() val currentTime = Date()
logger.debug(LTag.PUMPCOMM, "Programming basal. currentTime={}, basalProgram={}", currentTime, basalProgram) logger.debug(LTag.PUMPCOMM, "Programming basal. currentTime={}, basalProgram={}", currentTime, basalProgram)
@ -165,7 +166,7 @@ class OmnipodDashManagerImpl @Inject constructor(
.setUniqueId(podStateManager.uniqueId!!.toInt()) .setUniqueId(podStateManager.uniqueId!!.toInt())
.setSequenceNumber(podStateManager.messageSequenceNumber) .setSequenceNumber(podStateManager.messageSequenceNumber)
.setNonce(NONCE) .setNonce(NONCE)
.setProgramReminder(ProgramReminder(atStart = false, atEnd = false, atInterval = 0)) .setProgramReminder(ProgramReminder(atStart = hasBasalBeepEnabled, atEnd = false, atInterval = 0))
.setBasalProgram(basalProgram) .setBasalProgram(basalProgram)
.setCurrentTime(currentTime) .setCurrentTime(currentTime)
.build(), .build(),
@ -218,7 +219,7 @@ class OmnipodDashManagerImpl @Inject constructor(
return Observable.concat( return Observable.concat(
observePodReadyForActivationPart1, observePodReadyForActivationPart1,
observePairNewPod, observePairNewPod,
observeConnectToPod, // FIXME needed after disconnect; observePairNewPod does not connect in that case. observeConnectToPod,
observeActivationPart1Commands(lowReservoirAlertTrigger) observeActivationPart1Commands(lowReservoirAlertTrigger)
).doOnComplete(ActivationProgressUpdater(ActivationProgress.PHASE_1_COMPLETED)) ).doOnComplete(ActivationProgressUpdater(ActivationProgress.PHASE_1_COMPLETED))
// TODO these would be common for any observable returned in a public function in this class // TODO these would be common for any observable returned in a public function in this class
@ -246,6 +247,7 @@ class OmnipodDashManagerImpl @Inject constructor(
) )
} }
if (podStateManager.activationProgress.isBefore(ActivationProgress.PRIMING)) { if (podStateManager.activationProgress.isBefore(ActivationProgress.PRIMING)) {
observables.add(observeConnectToPod) // connection can time out while waiting
observables.add( observables.add(
Observable.defer { Observable.defer {
Observable.timer(podStateManager.firstPrimeBolusVolume!!.toLong(), TimeUnit.SECONDS) Observable.timer(podStateManager.firstPrimeBolusVolume!!.toLong(), TimeUnit.SECONDS)
@ -397,7 +399,7 @@ class OmnipodDashManagerImpl @Inject constructor(
} }
if (podStateManager.activationProgress.isBefore(ActivationProgress.PROGRAMMED_BASAL)) { if (podStateManager.activationProgress.isBefore(ActivationProgress.PROGRAMMED_BASAL)) {
observables.add( observables.add(
observeSendProgramBasalCommand(basalProgram) observeSendProgramBasalCommand(basalProgram, false)
.doOnComplete(ActivationProgressUpdater(ActivationProgress.PROGRAMMED_BASAL)) .doOnComplete(ActivationProgressUpdater(ActivationProgress.PROGRAMMED_BASAL))
) )
} }
@ -417,11 +419,11 @@ class OmnipodDashManagerImpl @Inject constructor(
.subscribeOn(aapsSchedulers.io) .subscribeOn(aapsSchedulers.io)
} }
override fun setBasalProgram(basalProgram: BasalProgram): Observable<PodEvent> { override fun setBasalProgram(basalProgram: BasalProgram, hasBasalBeepEnabled: Boolean): Observable<PodEvent> {
return Observable.concat( return Observable.concat(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendProgramBasalCommand(basalProgram) observeSendProgramBasalCommand(basalProgram, hasBasalBeepEnabled)
) )
// TODO these would be common for any observable returned in a public function in this class // TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor()) .doOnNext(PodEventInterceptor())
@ -429,25 +431,34 @@ class OmnipodDashManagerImpl @Inject constructor(
.subscribeOn(aapsSchedulers.io) .subscribeOn(aapsSchedulers.io)
} }
private fun observeSendStopDeliveryCommand(deliveryType: StopDeliveryCommand.DeliveryType): Observable<PodEvent> { private fun observeSendStopDeliveryCommand(
deliveryType: StopDeliveryCommand.DeliveryType,
beepEnabled: Boolean
): Observable<PodEvent> {
return Observable.defer { return Observable.defer {
val beepType = if (!beepEnabled)
BeepType.SILENT
else
BeepType.LONG_SINGLE_BEEP
bleManager.sendCommand( bleManager.sendCommand(
StopDeliveryCommand.Builder() StopDeliveryCommand.Builder()
.setSequenceNumber(podStateManager.messageSequenceNumber) .setSequenceNumber(podStateManager.messageSequenceNumber)
.setUniqueId(podStateManager.uniqueId!!.toInt()) .setUniqueId(podStateManager.uniqueId!!.toInt())
.setNonce(NONCE) .setNonce(NONCE)
.setDeliveryType(deliveryType) .setDeliveryType(deliveryType)
.setBeepType(beepType)
.build(), .build(),
DefaultStatusResponse::class DefaultStatusResponse::class
) )
} }
} }
override fun suspendDelivery(): Observable<PodEvent> { override fun suspendDelivery(hasBasalBeepEnabled: Boolean): Observable<PodEvent> {
return Observable.concat( return Observable.concat(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.ALL) observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.ALL, hasBasalBeepEnabled)
) )
// TODO these would be common for any observable returned in a public function in this class // TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor()) .doOnNext(PodEventInterceptor())
@ -489,11 +500,11 @@ class OmnipodDashManagerImpl @Inject constructor(
.subscribeOn(aapsSchedulers.io) .subscribeOn(aapsSchedulers.io)
} }
override fun stopTempBasal(): Observable<PodEvent> { override fun stopTempBasal(hasTempBasalBeepEnabled: Boolean): Observable<PodEvent> {
return Observable.concat( return Observable.concat(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.TEMP_BASAL) observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.TEMP_BASAL, hasTempBasalBeepEnabled)
) )
// TODO these would be common for any observable returned in a public function in this class // TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor()) .doOnNext(PodEventInterceptor())
@ -518,11 +529,11 @@ class OmnipodDashManagerImpl @Inject constructor(
.subscribeOn(aapsSchedulers.io) .subscribeOn(aapsSchedulers.io)
} }
override fun stopBolus(): Observable<PodEvent> { override fun stopBolus(beep: Boolean): Observable<PodEvent> {
return Observable.concat( return Observable.concat(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.BOLUS) observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.BOLUS, beep)
) )
// TODO these would be common for any observable returned in a public function in this class // TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor()) .doOnNext(PodEventInterceptor())
@ -615,7 +626,6 @@ class OmnipodDashManagerImpl @Inject constructor(
override fun deactivatePod(): Observable<PodEvent> { override fun deactivatePod(): Observable<PodEvent> {
return Observable.concat( return Observable.concat(
observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendDeactivateCommand observeSendDeactivateCommand
) )
@ -645,6 +655,7 @@ class OmnipodDashManagerImpl @Inject constructor(
} }
is PodEvent.CommandSent -> { is PodEvent.CommandSent -> {
logger.debug(LTag.PUMP, "Command sent: ${event.command.commandType}")
podStateManager.activeCommand?.let { podStateManager.activeCommand?.let {
if (it.sequence == event.command.sequenceNumber) { if (it.sequence == event.command.sequenceNumber) {
it.sentRealtime = SystemClock.elapsedRealtime() it.sentRealtime = SystemClock.elapsedRealtime()

View file

@ -44,6 +44,14 @@ class OmnipodDashBleManagerImpl @Inject constructor(
val session = assertSessionEstablished() val session = assertSessionEstablished()
emitter.onNext(PodEvent.CommandSending(cmd)) emitter.onNext(PodEvent.CommandSending(cmd))
/*
if (Random.nextBoolean()) {
// XXX use this to test "failed to confirm" commands
emitter.onNext(PodEvent.CommandSendNotConfirmed(cmd))
emitter.tryOnError(MessageIOException("XXX random failure to test unconfirmed commands"))
return@create
}
*/
when (session.sendCommand(cmd)) { when (session.sendCommand(cmd)) {
is CommandSendErrorSending -> { is CommandSendErrorSending -> {
emitter.tryOnError(CouldNotSendCommandException()) emitter.tryOnError(CouldNotSendCommandException())
@ -55,7 +63,12 @@ class OmnipodDashBleManagerImpl @Inject constructor(
is CommandSendErrorConfirming -> is CommandSendErrorConfirming ->
emitter.onNext(PodEvent.CommandSendNotConfirmed(cmd)) emitter.onNext(PodEvent.CommandSendNotConfirmed(cmd))
} }
/*
if (Random.nextBoolean()) {
// XXX use this commands confirmed with success
emitter.tryOnError(MessageIOException("XXX random failure to test unconfirmed commands"))
return@create
}*/
when (val readResult = session.readAndAckResponse(responseType)) { when (val readResult = session.readAndAckResponse(responseType)) {
is CommandReceiveSuccess -> is CommandReceiveSuccess ->
emitter.onNext(PodEvent.ResponseReceived(cmd, readResult.result)) emitter.onNext(PodEvent.ResponseReceived(cmd, readResult.result))
@ -84,7 +97,7 @@ class OmnipodDashBleManagerImpl @Inject constructor(
} }
override fun getStatus(): ConnectionState { override fun getStatus(): ConnectionState {
return connection?.let { getStatus() } return connection?.let { it.connectionState() }
?: NotConnected ?: NotConnected
} }
@ -102,18 +115,27 @@ class OmnipodDashBleManagerImpl @Inject constructor(
val conn = connection val conn = connection
?: Connection(podDevice, aapsLogger, context, podState) ?: Connection(podDevice, aapsLogger, context, podState)
connection = conn connection = conn
if (conn.connectionState() is Connected) { if (conn.connectionState() is Connected && conn.session != null) {
if (conn.session == null) { emitter.onNext(PodEvent.AlreadyConnected(podAddress))
emitter.onNext(PodEvent.EstablishingSession)
establishSession(1.toByte())
emitter.onNext(PodEvent.Connected)
} else {
emitter.onNext(PodEvent.AlreadyConnected(podAddress))
}
emitter.onComplete() emitter.onComplete()
return@create return@create
} }
conn.connect()
// two retries
for (i in 1..MAX_NUMBER_OF_CONNECTION_ATTEMPTS) {
try {
// wait i * CONNECTION_TIMEOUT
conn.connect(i)
break
} catch (e: Exception) {
aapsLogger.warn(LTag.PUMPBTCOMM, "connect error=$e")
if (i == MAX_NUMBER_OF_CONNECTION_ATTEMPTS) {
emitter.onError(e)
return@create
}
}
}
emitter.onNext(PodEvent.BluetoothConnected(podAddress)) emitter.onNext(PodEvent.BluetoothConnected(podAddress))
emitter.onNext(PodEvent.EstablishingSession) emitter.onNext(PodEvent.EstablishingSession)
establishSession(1.toByte()) establishSession(1.toByte())
@ -218,7 +240,7 @@ class OmnipodDashBleManagerImpl @Inject constructor(
} }
companion object { companion object {
const val MAX_NUMBER_OF_CONNECTION_ATTEMPTS = 3
const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else. const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else.
} }
} }

View file

@ -55,7 +55,7 @@ class Connection(
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING
gattConnection = podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE) gattConnection = podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE)
// OnDisconnect can be called after this point!!! // OnDisconnect can be called after this point!!!
val state = waitForConnection() val state = waitForConnection(2)
if (state !is Connected) { if (state !is Connected) {
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED
throw FailedToConnectException(podDevice.address) throw FailedToConnectException(podDevice.address)
@ -79,6 +79,7 @@ class Connection(
gattConnection, gattConnection,
bleCommCallbacks bleCommCallbacks
) )
val sendResult = cmdBleIO.hello() val sendResult = cmdBleIO.hello()
if (sendResult !is BleSendSuccess) { if (sendResult !is BleSendSuccess) {
throw FailedToConnectException("Could not send HELLO command to ${podDevice.address}") throw FailedToConnectException("Could not send HELLO command to ${podDevice.address}")
@ -89,7 +90,7 @@ class Connection(
val msgIO = MessageIO(aapsLogger, cmdBleIO, dataBleIO) val msgIO = MessageIO(aapsLogger, cmdBleIO, dataBleIO)
fun connect() { fun connect(timeoutMultiplier: Int) {
if (session != null) { if (session != null) {
disconnect() disconnect()
} }
@ -100,7 +101,7 @@ class Connection(
throw FailedToConnectException("connect() returned false") throw FailedToConnectException("connect() returned false")
} }
if (waitForConnection() !is Connected) { if (waitForConnection(timeoutMultiplier) !is Connected) {
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED
throw FailedToConnectException(podDevice.address) throw FailedToConnectException(podDevice.address)
} }
@ -111,6 +112,8 @@ class Connection(
dataBleIO.characteristic = discovered[CharacteristicType.DATA]!! dataBleIO.characteristic = discovered[CharacteristicType.DATA]!!
cmdBleIO.characteristic = discovered[CharacteristicType.CMD]!! cmdBleIO.characteristic = discovered[CharacteristicType.CMD]!!
// val ret = gattConnection.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
// aapsLogger.info(LTag.PUMPBTCOMM, "requestConnectionPriority: $ret")
cmdBleIO.hello() cmdBleIO.hello()
cmdBleIO.readyToRead() cmdBleIO.readyToRead()
dataBleIO.readyToRead() dataBleIO.readyToRead()
@ -125,9 +128,9 @@ class Connection(
session = null session = null
} }
private fun waitForConnection(): ConnectionState { private fun waitForConnection(timeoutMultiplier: Int): ConnectionState {
try { try {
bleCommCallbacks.waitForConnection(CONNECT_TIMEOUT_MS) bleCommCallbacks.waitForConnection(BASE_CONNECT_TIMEOUT_MS * timeoutMultiplier)
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
// We are still going to check if connection was successful // We are still going to check if connection was successful
aapsLogger.info(LTag.PUMPBTCOMM, "Interrupted while waiting for connection") aapsLogger.info(LTag.PUMPBTCOMM, "Interrupted while waiting for connection")
@ -178,7 +181,6 @@ class Connection(
} }
companion object { companion object {
private const val BASE_CONNECT_TIMEOUT_MS = 10000
private const val CONNECT_TIMEOUT_MS = 12000
} }
} }

View file

@ -65,4 +65,8 @@ sealed class PodEvent {
return "ResponseReceived(command=$command, response=$response)" return "ResponseReceived(command=$command, response=$response)"
} }
} }
fun isCommandSent(): Boolean {
return this is CommandSent || this is CommandSendNotConfirmed
}
} }

View file

@ -11,4 +11,16 @@ enum class DeliveryStatus(override val value: Byte) : HasValue {
BOLUS_AND_BASAL_ACTIVE(0x05.toByte()), BOLUS_AND_BASAL_ACTIVE(0x05.toByte()),
BOLUS_AND_TEMP_BASAL_ACTIVE(0x06.toByte()), BOLUS_AND_TEMP_BASAL_ACTIVE(0x06.toByte()),
UNKNOWN(0xff.toByte()); UNKNOWN(0xff.toByte());
fun bolusDeliveringActive(): Boolean {
return value in arrayOf(BOLUS_AND_BASAL_ACTIVE.value, BOLUS_AND_TEMP_BASAL_ACTIVE.value)
}
fun tempBasalActive(): Boolean {
return value in arrayOf(BOLUS_AND_TEMP_BASAL_ACTIVE.value, TEMP_BASAL_ACTIVE.value)
}
fun suspended(): Boolean {
return value == SUSPENDED.value
}
} }

View file

@ -1,5 +1,6 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state
import info.nightscout.androidaps.data.DetailedBolusInfo
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.PairResult import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.PairResult
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent
@ -8,6 +9,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.DefaultStatusResponse import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.DefaultStatusResponse
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.SetUniqueIdResponse import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.SetUniqueIdResponse
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.VersionResponse import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.VersionResponse
import io.reactivex.Completable
import io.reactivex.Maybe import io.reactivex.Maybe
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
@ -64,6 +66,7 @@ interface OmnipodDashPodStateManager {
val tempBasalActive: Boolean val tempBasalActive: Boolean
var basalProgram: BasalProgram? var basalProgram: BasalProgram?
val activeCommand: ActiveCommand? val activeCommand: ActiveCommand?
val lastBolus: LastBolus?
fun increaseMessageSequenceNumber() fun increaseMessageSequenceNumber()
fun increaseEapAkaSequenceNumber(): ByteArray fun increaseEapAkaSequenceNumber(): ByteArray
@ -75,12 +78,19 @@ interface OmnipodDashPodStateManager {
fun updateFromPairing(uniqueId: Id, pairResult: PairResult) fun updateFromPairing(uniqueId: Id, pairResult: PairResult)
fun reset() fun reset()
fun createActiveCommand(historyId: String, basalProgram: BasalProgram? = null, tempBasal: TempBasal? = null): fun createActiveCommand(
Single<ActiveCommand> historyId: String,
basalProgram: BasalProgram? = null,
tempBasal: TempBasal? = null,
requestedBolus: Double? = null
): Single<ActiveCommand>
fun updateActiveCommand(): Maybe<CommandConfirmed> fun updateActiveCommand(): Maybe<CommandConfirmed>
fun observeNoActiveCommand(): Observable<PodEvent> fun observeNoActiveCommand(): Completable
fun getCommandConfirmationFromState(): CommandConfirmationFromState fun getCommandConfirmationFromState(): CommandConfirmationFromState
fun createLastBolus(requestedUnits: Double, historyId: String, bolusType: DetailedBolusInfo.BolusType)
fun markLastBolusComplete(): LastBolus?
data class ActiveCommand( data class ActiveCommand(
val sequence: Short, val sequence: Short,
val createdRealtime: Long, val createdRealtime: Long,
@ -88,11 +98,36 @@ interface OmnipodDashPodStateManager {
val historyId: String, val historyId: String,
var sendError: Throwable?, var sendError: Throwable?,
var basalProgram: BasalProgram?, var basalProgram: BasalProgram?,
val tempBasal: TempBasal? val tempBasal: TempBasal?,
val requestedBolus: Double?
) )
// TODO: set created to "now" on boot // TODO: set created to "now" on boot
data class TempBasal(val startTime: Long, val rate: Double, val durationInMinutes: Short) : Serializable data class TempBasal(val startTime: Long, val rate: Double, val durationInMinutes: Short) : Serializable
data class LastBolus(
val startTime: Long,
val requestedUnits: Double,
var bolusUnitsRemaining: Double,
var deliveryComplete: Boolean,
val historyId: String,
val bolusType: DetailedBolusInfo.BolusType
) {
fun deliveredUnits(): Double? {
return if (deliveryComplete) {
requestedUnits - bolusUnitsRemaining
} else {
null
}
}
fun markComplete(): Double {
this.deliveryComplete = true
return requestedUnits - bolusUnitsRemaining
}
}
enum class BluetoothConnectionState { enum class BluetoothConnectionState {
CONNECTING, CONNECTED, DISCONNECTED CONNECTING, CONNECTED, DISCONNECTED
} }

View file

@ -2,6 +2,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state
import android.os.SystemClock import android.os.SystemClock
import com.google.gson.Gson import com.google.gson.Gson
import info.nightscout.androidaps.data.DetailedBolusInfo
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.bus.RxBusWrapper import info.nightscout.androidaps.plugins.bus.RxBusWrapper
@ -17,6 +18,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.SetUniqueIdResponse import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.SetUniqueIdResponse
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.VersionResponse import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.VersionResponse
import info.nightscout.androidaps.utils.sharedPreferences.SP import info.nightscout.androidaps.utils.sharedPreferences.SP
import io.reactivex.Completable
import io.reactivex.Maybe import io.reactivex.Maybe
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
@ -151,6 +153,10 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
store() store()
} }
override val lastBolus: OmnipodDashPodStateManager.LastBolus?
@Synchronized
get() = podState.lastBolus
override val tempBasalActive: Boolean override val tempBasalActive: Boolean
get() = !isSuspended && tempBasal?.let { get() = !isSuspended && tempBasal?.let {
it.startTime + it.durationInMinutes * 60 * 1000 > System.currentTimeMillis() it.startTime + it.durationInMinutes * 60 * 1000 > System.currentTimeMillis()
@ -197,11 +203,46 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
override val activeCommand: OmnipodDashPodStateManager.ActiveCommand? override val activeCommand: OmnipodDashPodStateManager.ActiveCommand?
get() = podState.activeCommand get() = podState.activeCommand
@Synchronized
override fun createLastBolus(requestedUnits: Double, historyId: String, bolusType: DetailedBolusInfo.BolusType) {
podState.lastBolus = OmnipodDashPodStateManager.LastBolus(
startTime = System.currentTimeMillis(),
requestedUnits = requestedUnits,
bolusUnitsRemaining = requestedUnits,
deliveryComplete = false, // cancelled, delivered 100% or pod failure
historyId = historyId,
bolusType = bolusType
)
}
@Synchronized
override fun markLastBolusComplete(): OmnipodDashPodStateManager.LastBolus? {
val lastBolus = podState.lastBolus
lastBolus?.run {
this.deliveryComplete = true
}
?: logger.error(LTag.PUMP, "Trying to mark null bolus as complete")
return lastBolus
}
private fun updateLastBolusFromResponse(bolusPulsesRemaining: Short) {
podState.lastBolus?.run {
val remainingUnits = bolusPulsesRemaining.toDouble() * 0.05
this.bolusUnitsRemaining = remainingUnits
if (remainingUnits == 0.0) {
this.deliveryComplete = true
}
}
}
@Synchronized @Synchronized
override fun createActiveCommand( override fun createActiveCommand(
historyId: String, historyId: String,
basalProgram: BasalProgram?, basalProgram: BasalProgram?,
tempBasal: OmnipodDashPodStateManager.TempBasal? tempBasal: OmnipodDashPodStateManager.TempBasal?,
requestedBolus: Double?
): ):
Single<OmnipodDashPodStateManager.ActiveCommand> { Single<OmnipodDashPodStateManager.ActiveCommand> {
return Single.create { source -> return Single.create { source ->
@ -213,6 +254,7 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
sendError = null, sendError = null,
basalProgram = basalProgram, basalProgram = basalProgram,
tempBasal = tempBasal, tempBasal = tempBasal,
requestedBolus = requestedBolus
) )
podState.activeCommand = command podState.activeCommand = command
source.onSuccess(command) source.onSuccess(command)
@ -228,12 +270,13 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
} }
@Synchronized @Synchronized
override fun observeNoActiveCommand(): Observable<PodEvent> { override fun observeNoActiveCommand(): Completable {
return Observable.defer { return Completable.defer {
if (activeCommand == null) { if (activeCommand == null) {
Observable.empty() Completable.complete()
} else { } else {
Observable.error( logger.warn(LTag.PUMP, "Active command already existing: $activeCommand")
Completable.error(
java.lang.IllegalStateException( java.lang.IllegalStateException(
"Trying to send a command " + "Trying to send a command " +
"and the last command was not confirmed" "and the last command was not confirmed"
@ -257,7 +300,7 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
CommandSendingFailure -> { CommandSendingFailure -> {
podState.activeCommand = null podState.activeCommand = null
source.onError( source.onError(
activeCommand?.sendError activeCommand.sendError
?: java.lang.IllegalStateException( ?: java.lang.IllegalStateException(
"Could not send command and sendError is " + "Could not send command and sendError is " +
"missing" "missing"
@ -323,7 +366,7 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
} }
override fun updateFromDefaultStatusResponse(response: DefaultStatusResponse) { override fun updateFromDefaultStatusResponse(response: DefaultStatusResponse) {
logger.debug(LTag.PUMPBTCOMM, "Default status reponse :$response") logger.debug(LTag.PUMPCOMM, "Default status response :$response")
podState.deliveryStatus = response.deliveryStatus podState.deliveryStatus = response.deliveryStatus
podState.podStatus = response.podStatus podState.podStatus = response.podStatus
podState.pulsesDelivered = response.totalPulsesDelivered podState.pulsesDelivered = response.totalPulsesDelivered
@ -336,6 +379,7 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
podState.lastUpdatedSystem = System.currentTimeMillis() podState.lastUpdatedSystem = System.currentTimeMillis()
podState.lastStatusResponseReceived = SystemClock.elapsedRealtime() podState.lastStatusResponseReceived = SystemClock.elapsedRealtime()
updateLastBolusFromResponse(response.bolusPulsesRemaining)
store() store()
rxBus.send(EventOmnipodDashPumpValuesChanged()) rxBus.send(EventOmnipodDashPumpValuesChanged())
@ -384,6 +428,11 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
podState.uniqueId = response.uniqueIdReceivedInCommand podState.uniqueId = response.uniqueIdReceivedInCommand
podState.lastUpdatedSystem = System.currentTimeMillis() podState.lastUpdatedSystem = System.currentTimeMillis()
// TODO: what is considered to be the pod activation time?
// LTK negotiation ?
// setUniqueId?
// compute it from the number of "minutesOnPod"?
podState.activationTime = System.currentTimeMillis()
store() store()
rxBus.send(EventOmnipodDashPumpValuesChanged()) rxBus.send(EventOmnipodDashPumpValuesChanged())
@ -397,6 +446,7 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
podState.deliveryStatus = response.deliveryStatus podState.deliveryStatus = response.deliveryStatus
podState.podStatus = response.podStatus podState.podStatus = response.podStatus
podState.pulsesDelivered = response.totalPulsesDelivered podState.pulsesDelivered = response.totalPulsesDelivered
if (response.reservoirPulsesRemaining < 1023) { if (response.reservoirPulsesRemaining < 1023) {
podState.pulsesRemaining = response.reservoirPulsesRemaining podState.pulsesRemaining = response.reservoirPulsesRemaining
} }
@ -407,6 +457,7 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
podState.lastUpdatedSystem = System.currentTimeMillis() podState.lastUpdatedSystem = System.currentTimeMillis()
podState.lastStatusResponseReceived = SystemClock.elapsedRealtime() podState.lastStatusResponseReceived = SystemClock.elapsedRealtime()
updateLastBolusFromResponse(response.bolusPulsesRemaining)
store() store()
rxBus.send(EventOmnipodDashPumpValuesChanged()) rxBus.send(EventOmnipodDashPumpValuesChanged())
@ -462,6 +513,7 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
var bluetoothAddress: String? = null var bluetoothAddress: String? = null
var ltk: ByteArray? = null var ltk: ByteArray? = null
var eapAkaSequenceNumber: Long = 1 var eapAkaSequenceNumber: Long = 1
var bolusPulsesRemaining: Short = 0
var bleVersion: SoftwareVersion? = null var bleVersion: SoftwareVersion? = null
var firmwareVersion: SoftwareVersion? = null var firmwareVersion: SoftwareVersion? = null
@ -484,5 +536,6 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
var basalProgram: BasalProgram? = null var basalProgram: BasalProgram? = null
var tempBasal: OmnipodDashPodStateManager.TempBasal? = null var tempBasal: OmnipodDashPodStateManager.TempBasal? = null
var activeCommand: OmnipodDashPodStateManager.ActiveCommand? = null var activeCommand: OmnipodDashPodStateManager.ActiveCommand? = null
var lastBolus: OmnipodDashPodStateManager.LastBolus? = null
} }
} }

View file

@ -11,6 +11,13 @@ data class TempBasalRecord(val duration: Int, val rate: Double) : Record()
enum class BolusType { enum class BolusType {
DEFAULT, SMB; DEFAULT, SMB;
fun toBolusInfoBolusType(): DetailedBolusInfo.BolusType {
return when (this) {
DEFAULT -> DetailedBolusInfo.BolusType.NORMAL
SMB -> DetailedBolusInfo.BolusType.SMB
}
}
companion object { companion object {
fun fromBolusInfoBolusType(type: DetailedBolusInfo.BolusType): BolusType { fun fromBolusInfoBolusType(type: DetailedBolusInfo.BolusType): BolusType {
return when (type) { return when (type) {

View file

@ -14,6 +14,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.activati
import info.nightscout.androidaps.plugins.pump.omnipod.dash.R import info.nightscout.androidaps.plugins.pump.omnipod.dash.R
import info.nightscout.androidaps.plugins.pump.omnipod.dash.databinding.OmnipodDashPodManagementBinding import info.nightscout.androidaps.plugins.pump.omnipod.dash.databinding.OmnipodDashPodManagementBinding
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.ActivationProgress import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.ActivationProgress
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.PodStatus
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.activation.DashPodActivationWizardActivity import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.activation.DashPodActivationWizardActivity
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.deactivation.DashPodDeactivationWizardActivity import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.deactivation.DashPodDeactivationWizardActivity
@ -124,7 +125,8 @@ class DashPodManagementActivity : NoSplashAppCompatActivity() {
binding.buttonActivatePod.isEnabled = podStateManager.activationProgress.isBefore(ActivationProgress.COMPLETED) binding.buttonActivatePod.isEnabled = podStateManager.activationProgress.isBefore(ActivationProgress.COMPLETED)
binding.buttonDeactivatePod.isEnabled = binding.buttonDeactivatePod.isEnabled =
podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED) podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED) ||
podStateManager.podStatus == PodStatus.ALARM
if (podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED)) { if (podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED)) {
if (commandQueue.isCustomCommandInQueue(CommandPlayTestBeep::class.java)) { if (commandQueue.isCustomCommandInQueue(CommandPlayTestBeep::class.java)) {

View file

@ -5,6 +5,7 @@ import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.SystemClock
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -23,6 +24,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.Comm
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandResumeDelivery import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandResumeDelivery
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandSilenceAlerts import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandSilenceAlerts
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandSuspendDelivery import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandSuspendDelivery
import info.nightscout.androidaps.plugins.pump.omnipod.dash.BuildConfig
import info.nightscout.androidaps.plugins.pump.omnipod.dash.EventOmnipodDashPumpValuesChanged import info.nightscout.androidaps.plugins.pump.omnipod.dash.EventOmnipodDashPumpValuesChanged
import info.nightscout.androidaps.plugins.pump.omnipod.dash.OmnipodDashPumpPlugin import info.nightscout.androidaps.plugins.pump.omnipod.dash.OmnipodDashPumpPlugin
import info.nightscout.androidaps.plugins.pump.omnipod.dash.R import info.nightscout.androidaps.plugins.pump.omnipod.dash.R
@ -42,6 +44,8 @@ import info.nightscout.androidaps.utils.ui.UIRunnable
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.plusAssign
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.Duration import org.joda.time.Duration
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -232,6 +236,29 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
} }
} }
// Get time on pos from activation time and minutes since activation
private fun getTimeOnPod(): DateTime? {
var timeOnPod: DateTime? = null
val minutesSinceActivation = podStateManager.minutesSinceActivation
val activationTime = podStateManager.activationTime
if ((activationTime != null) and (minutesSinceActivation != null)) {
timeOnPod = DateTime(activationTime!!).plusMinutes(minutesSinceActivation!!.toInt())
}
return timeOnPod
}
// TODO: Consider storing expiry datetime in pod state saving continuesly recalculating to the same value
private fun getExpiryAt(): DateTime? {
var expiresAt: DateTime? = null
val podLifeInHours = podStateManager.podLifeInHours
val minutesSinceActivation = podStateManager.minutesSinceActivation
if ((podLifeInHours != null) and (minutesSinceActivation != null)) {
val expiresInMinutes = (podLifeInHours!! * 60) - minutesSinceActivation!!
expiresAt = DateTime().plusMinutes(expiresInMinutes)
}
return expiresAt
}
private fun updateOmnipodStatus() { private fun updateOmnipodStatus() {
updateLastConnection() updateLastConnection()
updateLastBolus() updateLastBolus()
@ -262,7 +289,16 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
podStateManager.firmwareVersion.toString(), podStateManager.firmwareVersion.toString(),
podStateManager.bluetoothVersion.toString() podStateManager.bluetoothVersion.toString()
) )
podInfoBinding.timeOnPod.text = podStateManager.minutesSinceActivation.toString() + " minutes"
// Update time on Pod
// TODO: For now: derive from podStateManager.minutesSinceActivation
val timeOnPod = getTimeOnPod()
if (timeOnPod == null) {
podInfoBinding.timeOnPod.text = "???"
} else {
podInfoBinding.timeOnPod.text = readableZonedTime(timeOnPod)
}
// TODO // TODO
/* /*
podInfoBinding.timeOnPod.setTextColor(if (podStateManager.timeDeviatesMoreThan(OmnipodConstants.TIME_DEVIATION_THRESHOLD)) { podInfoBinding.timeOnPod.setTextColor(if (podStateManager.timeDeviatesMoreThan(OmnipodConstants.TIME_DEVIATION_THRESHOLD)) {
@ -272,22 +308,28 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
}) })
*/ */
// TODO // TODO: Active command
/* if (podStateManager.activeCommand != null) {
val expiresAt = podStateManager.expiresAt podInfoBinding.podExpiryDate.setTextColor(Color.RED)
podInfoBinding.podExpiryDate.text = "Active command"
} else {
podInfoBinding.podExpiryDate.text = PLACEHOLDER
podInfoBinding.podExpiryDate.setTextColor(Color.WHITE)
}
if (expiresAt == null) { // Update Pod expiry time
val expiresAt = getExpiryAt()
if (expiresAt is Nothing) {
podInfoBinding.podExpiryDate.text = PLACEHOLDER podInfoBinding.podExpiryDate.text = PLACEHOLDER
podInfoBinding.podExpiryDate.setTextColor(Color.WHITE) podInfoBinding.podExpiryDate.setTextColor(Color.WHITE)
} else { } else {
podInfoBinding.podExpiryDate.text = readableZonedTime(expiresAt) podInfoBinding.podExpiryDate.text = readableZonedTime(expiresAt!!)
podInfoBinding.podExpiryDate.setTextColor(if (DateTime.now().isAfter(expiresAt)) { podInfoBinding.podExpiryDate.setTextColor(if (DateTime.now().isAfter(expiresAt)) {
Color.RED Color.RED
} else { } else {
Color.WHITE Color.WHITE
}) })
} }
*/
/* TODO /* TODO
if (podStateManager.isPodFaulted) { if (podStateManager.isPodFaulted) {
@ -367,7 +409,13 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
private fun updateLastConnection() { private fun updateLastConnection() {
if (podStateManager.isUniqueIdSet) { if (podStateManager.isUniqueIdSet) {
podInfoBinding.lastConnection.text = readableDuration(podStateManager.lastUpdatedSystem) podInfoBinding.lastConnection.text = readableDuration(
Duration(
podStateManager.lastUpdatedSystem,
System
.currentTimeMillis()
)
)
val lastConnectionColor = val lastConnectionColor =
if (omnipodDashPumpPlugin.isUnreachableAlertTimeoutExceeded(getPumpUnreachableTimeout().millis)) { if (omnipodDashPumpPlugin.isUnreachableAlertTimeoutExceeded(getPumpUnreachableTimeout().millis)) {
Color.RED Color.RED
@ -377,7 +425,9 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
podInfoBinding.lastConnection.setTextColor(lastConnectionColor) podInfoBinding.lastConnection.setTextColor(lastConnectionColor)
} else { } else {
podInfoBinding.lastConnection.setTextColor(Color.WHITE) podInfoBinding.lastConnection.setTextColor(Color.WHITE)
podInfoBinding.lastConnection.text = readableDuration(podStateManager.lastUpdatedSystem) podInfoBinding.lastConnection.text = readableDuration(
Duration(podStateManager.lastUpdatedSystem, System.currentTimeMillis())
)
} }
} }
@ -400,8 +450,9 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
resourceHelper.gs(R.string.omnipod_common_pod_status_suspended) resourceHelper.gs(R.string.omnipod_common_pod_status_suspended)
} else { } else {
resourceHelper.gs(R.string.omnipod_common_pod_status_running) + resourceHelper.gs(R.string.omnipod_common_pod_status_running) +
podStateManager.deliveryStatus?.let { " " + podStateManager.deliveryStatus.toString() } if (BuildConfig.DEBUG)
// TODO Display deliveryStatus in a nice way podStateManager.deliveryStatus?.let { " " + podStateManager.deliveryStatus.toString() }
else ""
} }
// TODO // TODO
/* /*
@ -416,7 +467,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
} }
val podStatusColor = val podStatusColor =
if (!podStateManager.isActivationCompleted || /* TODO podStateManager.isPodDead || */ podStateManager.isSuspended) { if (!podStateManager.isActivationCompleted || podStateManager.isPodKaput || podStateManager.isSuspended) {
Color.RED Color.RED
} else { } else {
Color.WHITE Color.WHITE
@ -425,27 +476,43 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
} }
private fun updateLastBolus() { private fun updateLastBolus() {
// TODO
/*
if (podStateManager.isActivationCompleted && podStateManager.hasLastBolus()) {
var text = resourceHelper.gs(R.string.omnipod_common_overview_last_bolus_value, omnipodDashPumpPlugin.model().determineCorrectBolusSize(podStateManager.lastBolusAmount), resourceHelper.gs(R.string.insulin_unit_shortname), readableDuration(podStateManager.lastBolusStartTime))
val textColor: Int
if (podStateManager.isLastBolusCertain) { var textColor = Color.WHITE
textColor = Color.WHITE podStateManager.activeCommand?.let {
} else { val requestedBolus = it.requestedBolus
if (requestedBolus != null) {
var text = resourceHelper.gs(
R.string.omnipod_common_overview_last_bolus_value,
omnipodDashPumpPlugin.model().determineCorrectBolusSize(requestedBolus),
resourceHelper.gs(R.string.insulin_unit_shortname),
readableDuration(Duration(it.createdRealtime, SystemClock.elapsedRealtime()))
)
text += " (uncertain) "
textColor = Color.RED textColor = Color.RED
text += " (" + resourceHelper.gs(R.string.omnipod_eros_uncertain) + ")" podInfoBinding.lastBolus.text = text
podInfoBinding.lastBolus.setTextColor(textColor)
return
} }
}
podInfoBinding.lastBolus.setTextColor(textColor)
podStateManager.lastBolus?.let {
// display requested units if delivery is in progress
var bolusSize = it.deliveredUnits()
?: it.requestedUnits
var text = resourceHelper.gs(
R.string.omnipod_common_overview_last_bolus_value,
omnipodDashPumpPlugin.model().determineCorrectBolusSize(bolusSize),
resourceHelper.gs(R.string.insulin_unit_shortname),
readableDuration(Duration(it.startTime, System.currentTimeMillis()))
)
if (!it.deliveryComplete) {
textColor = Color.YELLOW
}
podInfoBinding.lastBolus.text = text podInfoBinding.lastBolus.text = text
podInfoBinding.lastBolus.setTextColor(textColor) podInfoBinding.lastBolus.setTextColor(textColor)
} else {
podInfoBinding.lastBolus.text = PLACEHOLDER
podInfoBinding.lastBolus.setTextColor(Color.WHITE)
} }
*/
} }
private fun updateTempBasal() { private fun updateTempBasal() {
@ -529,9 +596,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
private fun updateSuspendDeliveryButton() { private fun updateSuspendDeliveryButton() {
// If the Pod is currently suspended, we show the Resume delivery button instead. // If the Pod is currently suspended, we show the Resume delivery button instead.
// TODO: isSuspendDeliveryButtonEnabled doesn't work if (isSuspendDeliveryButtonEnabled() &&
val isSuspendDeliveryButtonEnabled = true
if (isSuspendDeliveryButtonEnabled &&
podStateManager.isPodRunning && podStateManager.isPodRunning &&
(!podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandSuspendDelivery::class.java)) (!podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandSuspendDelivery::class.java))
) { ) {
@ -560,7 +625,8 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
} }
private fun isSuspendDeliveryButtonEnabled(): Boolean { private fun isSuspendDeliveryButtonEnabled(): Boolean {
return sp.getBoolean(R.string.omnipod_common_preferences_suspend_delivery_button_enabled, false) R.string.key_omnipod_common_basal_beeps_enabled
return sp.getBoolean(R.string.key_omnipod_common_suspend_delivery_button_enabled, false)
} }
private fun displayErrorDialog(title: String, message: String, withSound: Boolean) { private fun displayErrorDialog(title: String, message: String, withSound: Boolean) {
@ -577,23 +643,28 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
} }
} }
/* private fun getTimeZone(): DateTimeZone {
// TODO: Get timezone as configured/podState
// return getSafe(() -> podState.getTimeZone());
return DateTimeZone.getDefault()
}
private fun readableZonedTime(time: DateTime): String { private fun readableZonedTime(time: DateTime): String {
val timeAsJavaData = time.toLocalDateTime().toDate() val timeAsJavaData = time.toLocalDateTime().toDate()
val timeZone = podStateManager.timeZone.toTimeZone()
val timeZone = getTimeZone().toTimeZone()
if (timeZone == TimeZone.getDefault()) { if (timeZone == TimeZone.getDefault()) {
return dateUtil.dateAndTimeString(timeAsJavaData) return dateUtil.dateAndTimeString(timeAsJavaData.time)
} }
// Get full timezoned time
val isDaylightTime = timeZone.inDaylightTime(timeAsJavaData) val isDaylightTime = timeZone.inDaylightTime(timeAsJavaData)
val locale = resources.configuration.locales.get(0) val locale = resources.configuration.locales.get(0)
val timeZoneDisplayName = timeZone.getDisplayName(isDaylightTime, TimeZone.SHORT, locale) + " " + timeZone.getDisplayName(isDaylightTime, TimeZone.LONG, locale) val timeZoneDisplayName = timeZone.getDisplayName(isDaylightTime, TimeZone.SHORT, locale) + " " + timeZone.getDisplayName(isDaylightTime, TimeZone.LONG, locale)
return resourceHelper.gs(R.string.omnipod_common_time_with_timezone, dateUtil.dateAndTimeString(timeAsJavaData), timeZoneDisplayName) return resourceHelper.gs(R.string.omnipod_common_time_with_timezone, dateUtil.dateAndTimeString(timeAsJavaData.time), timeZoneDisplayName)
} }
*/
private fun readableDuration(dateTime: Long): String { private fun readableDuration(duration: Duration): String {
val duration = Duration(dateTime, System.currentTimeMillis())
val hours = duration.standardHours.toInt() val hours = duration.standardHours.toInt()
val minutes = duration.standardMinutes.toInt() val minutes = duration.standardMinutes.toInt()
val seconds = duration.standardSeconds.toInt() val seconds = duration.standardSeconds.toInt()