comboctl-main: Support extended/multiwave bolus in Pump.deliverBolus()

Signed-off-by: Carlos Rafael Giani <crg7475@mailbox.org>
This commit is contained in:
Carlos Rafael Giani 2023-01-01 22:29:18 +01:00
parent 83dce8bee3
commit 4bb163a633

View file

@ -1,6 +1,7 @@
package info.nightscout.comboctl.main
import info.nightscout.comboctl.base.ApplicationLayer
import info.nightscout.comboctl.base.ApplicationLayer.CMDDeliverBolusType
import info.nightscout.comboctl.base.ApplicationLayer.CMDHistoryEventDetail
import info.nightscout.comboctl.base.BasicProgressStage
import info.nightscout.comboctl.base.BluetoothAddress
@ -101,11 +102,11 @@ object RTCommandProgressStage {
*
* The amounts are given in 0.1 IU units. For example, "57" means 5.7 IU.
*
* @property deliveredAmount How many units have been delivered so far.
* @property deliveredImmediateAmount How many units have been delivered so far.
* This is always <= totalAmount.
* @property totalAmount Total amount of bolus units.
* @property totalImmediateAmount Total amount of bolus units.
*/
data class DeliveringBolus(val deliveredAmount: Int, val totalAmount: Int) : ProgressStage("deliveringBolus")
data class DeliveringBolus(val deliveredImmediateAmount: Int, val totalImmediateAmount: Int) : ProgressStage("deliveringBolus")
/**
* TDD fetching history stage.
@ -297,7 +298,7 @@ class Pump(
BasicProgressStage.Finished,
is BasicProgressStage.Aborted -> 1.0
is RTCommandProgressStage.DeliveringBolus ->
stage.deliveredAmount.toDouble() / stage.totalAmount.toDouble()
stage.deliveredImmediateAmount.toDouble() / stage.totalImmediateAmount.toDouble()
else -> 0.0
}
}
@ -369,8 +370,11 @@ class Pump(
val force100Percent: Boolean
) : CommandDescription()
class DeliveringBolusCommandDesc(
val bolusAmount: Int,
val bolusReason: StandardBolusReason
val totalBolusAmount: Int,
val immediateBolusAmount: Int,
val durationInMinutes: Int,
val standardBolusReason: StandardBolusReason,
val bolusType: ApplicationLayer.CMDDeliverBolusType
) : CommandDescription()
/**
@ -410,27 +414,27 @@ class Pump(
/**
* Exception thrown when the bolus delivery was cancelled.
*
* @param deliveredAmount Bolus amount that was delivered before the bolus was cancelled. In 0.1 IU units.
* @param totalAmount Total bolus amount that was supposed to be delivered. In 0.1 IU units.
* @param deliveredImmediateAmount Bolus amount that was delivered before the bolus was cancelled. In 0.1 IU units.
* @param totalImmediateAmount Total bolus amount that was supposed to be delivered. In 0.1 IU units.
*/
class BolusCancelledByUserException(val deliveredAmount: Int, totalAmount: Int) :
class BolusCancelledByUserException(val deliveredImmediateAmount: Int, totalImmediateAmount: Int) :
BolusDeliveryException(
totalAmount,
"Bolus cancelled (delivered amount: ${deliveredAmount.toStringWithDecimal(1)} IU " +
"total programmed amount: ${totalAmount.toStringWithDecimal(1)} IU"
totalImmediateAmount,
"Bolus cancelled (delivered amount: ${deliveredImmediateAmount.toStringWithDecimal(1)} IU " +
"total programmed amount: ${totalImmediateAmount.toStringWithDecimal(1)} IU"
)
/**
* Exception thrown when the bolus delivery was aborted due to an error.
*
* @param deliveredAmount Bolus amount that was delivered before the bolus was aborted. In 0.1 IU units.
* @param totalAmount Total bolus amount that was supposed to be delivered.
* @param deliveredImmediateAmount Bolus amount that was delivered before the bolus was aborted. In 0.1 IU units.
* @param totalImmediateAmount Total bolus amount that was supposed to be delivered.
*/
class BolusAbortedDueToErrorException(deliveredAmount: Int, totalAmount: Int) :
class BolusAbortedDueToErrorException(deliveredImmediateAmount: Int, totalImmediateAmount: Int) :
BolusDeliveryException(
totalAmount,
"Bolus aborted due to an error (delivered amount: ${deliveredAmount.toStringWithDecimal(1)} IU " +
"total programmed amount: ${totalAmount.toStringWithDecimal(1)} IU"
totalImmediateAmount,
"Bolus aborted due to an error (delivered amount: ${deliveredImmediateAmount.toStringWithDecimal(1)} IU " +
"total programmed amount: ${totalImmediateAmount.toStringWithDecimal(1)} IU"
)
/**
@ -1409,51 +1413,94 @@ class Pump(
val bolusDeliveryProgressFlow = bolusDeliveryProgressReporter.progressFlow
/**
* Instructs the pump to deliver the specified bolus amount.
* Instructs the pump to deliver a standard bolus with the specified amount.
*
* This function only delivers a standard bolus, no multi-wave / extended ones.
* It is currently not known how to command the Combo to deliver those types.
* This is equivalent to calling the full [deliverBolus] function with the bolus
* type set to [ApplicationLayer.CMDDeliverBolusType.STANDARD_BOLUS], a total
* bolus amount that is set to [bolusAmount], and the immediate amount and
* duration both set to 0.
*
* The function suspends until the bolus was fully delivered or an error occurred.
* In the latter case, an exception is thrown. During the delivery, the current
* status is periodically retrieved from the pump. [bolusStatusUpdateIntervalInMs]
* controls the status update interval. At each update, the bolus state is checked
* (that is, whether it is delivering, or it is done, or an error occurred etc.)
* The bolus amount that was delivered by that point is communicated via the
* [bolusDeliveryProgressFlow].
*
* To cancel the bolus, simply cancel the coroutine that is suspended by this function.
*
* Prior to the delivery, the number of units available in the reservoir is checked
* by looking at [statusFlow]. If there aren't enough IU in the reservoir, this
* function throws [InsufficientInsulinAvailableException].
*
* After the delivery, this function looks at the Combo's bolus history delta. That
* delta is expected to contain exactly one entry - the bolus that was just delivered.
* The details in that history delta entry are then emitted as
* [Event.StandardBolusInfused] via [onEvent].
* If there is no entry, [BolusNotDeliveredException] is thrown. If more than one
* bolus entry is detected, [UnaccountedBolusDetectedException] is thrown (this
* second case is not expected to ever happen, but is possible in theory). The
* history delta is looked at even if an exception is thrown (unless it is one
* of the exceptions that were just mentioned). This is because if there is an
* error _during_ a bolus delivery, then some insulin might have still be
* delivered, and there will be a [Event.StandardBolusInfused] history entry,
* probably just not with the insulin amount that was originally planned.
* It is still important to report that (partial) delivery, which is done
* via [onEvent] just as described above.
*
* Once that is completed, this function calls [updateStatus] to make sure the
* contents of [statusFlow] are up-to-date. A bolus delivery will at least
* change the value of [Status.availableUnitsInReservoir] (unless perhaps it
* is a very small bolus like 0.1 IU, since that value is given in whole IU units).
*
* @param bolusAmount Bolus amount to deliver. Note that this is given
* in 0.1 IU units, so for example, "57" means 5.7 IU. Valid range
* is 0.0 IU to 25.0 IU (that is, integer values 0-250).
* @param bolusAmount Amount of insulin units the standard bolus shall deliver.
* @param bolusReason Reason for this standard bolus.
* @param bolusStatusUpdateIntervalInMs Interval between status updates,
* in milliseconds. Must be at least 1
*/
suspend fun deliverBolus(
bolusAmount: Int,
bolusReason: StandardBolusReason,
bolusStatusUpdateIntervalInMs: Long = 250
) = deliverBolus(
totalBolusAmount = bolusAmount,
immediateBolusAmount = 0,
durationInMinutes = 0,
standardBolusReason = bolusReason,
bolusType = ApplicationLayer.CMDDeliverBolusType.STANDARD_BOLUS,
bolusStatusUpdateIntervalInMs = bolusStatusUpdateIntervalInMs
)
/**
* Instructs the pump to deliver a bolus.
*
* The function suspends until the immediate portion of the bolus was fully delivered,
* or when an error occurred. In the latter case, an exception is thrown.
*
* The bolus delivey is split in two parts: the immediate portion and the extended
* portion. The immediate portion is infused right away, as fast as the Combo is able
* to do so. The extended portion is delivered over the specified [durationInMinutes],
* and behaves much like a TBR.
*
* During the delivery of the immediate portion, the current status is periodically
* retrieved from the pump. [bolusStatusUpdateIntervalInMs] controls the status update
* interval. At each update, the bolus state is checked (that is, whether it is delivering,
* or whethr it is done, or an error occurred etc.) The bolus amount that was delivered
* by that point is communicated via the [bolusDeliveryProgressFlow].
*
* To cancel the immediate delivery of the bolus, simply cancel the coroutine that is
* suspended by this function.
*
* IMPORTANT: The extended portion _cannot_ be canceled that way; there is no way of
* doing that other than for the Combo to be stopped and started again.
*
* Prior to the delivery, the number of units available in the reservoir is checked
* by looking at [statusFlow] and compared against [totalBolusAmount]. If there aren't
* enough IU in the reservoir, this function throws [InsufficientInsulinAvailableException].
*
* After the delivery, this function looks at the Combo's bolus history delta. That
* delta is expected to contain exactly one entry - the bolus that was just delivered
* or started (depending on the bolus type). The details in that history delta entry
* are then emitted via [onEvent] as [Event.StandardBolusInfused] for a standard bolus.
* Extended boluses generate [Event.ExtendedBolusStarted]. Likewise, multiwave boluses
* generate [Event.MultiwaveBolusStarted].
*
* If there is no entry, [BolusNotDeliveredException] is thrown. If a standard bolus
* was delivered, and more than one bolus entry is detected, [UnaccountedBolusDetectedException]
* is thrown. The history delta is looked at even if an exception is thrown (unless
* it is one of the exceptions that were just mentioned). This is because if there is
* an error _during_ an immediate delivery, then some insulin might have still been
* delivered, and there will be a corresponding history entry, probably just not with
* the insulin amount that was originally planned. It is still important to report
* that (partial) delivery, which is done with [onEvent] just as described above.
*
* Once that is completed, this function calls [updateStatus] to make sure the contents
* of [statusFlow] are up-to-date. A bolus delivery will at least change the value of
* [Status.availableUnitsInReservoir] (unless perhaps it is a very small bolus like
* 0.1 IU, since the reservoir level is given inwhole IU units).
*
* @param totalBolusAmount Total bolus amount to deliver (that is, the sum of the immediate
* and extended portions). Note that this is given in 0.1 IU units, so for example,
* "57" means 5.7 IU. Valid range is 0.0 IU to 25.0 IU (that is, integer values 0-250).
* @param immediateBolusAmount The amount to deliver immediately. This is only used
* [bolusType] is [CMDDeliverBolusType.MULTIWAVE_BOLUS], and is ignored otherwise.
* When delivering a multiwave bolus, this value must be >= 1 and < [totalBolusAmount].
* @param durationInMinutes The duration of the extended bolus or the extended portion
* of the multiwave bolus. If [bolusType] is set to [CMDDeliverBolusType.STANDARD_BOLUS],
* this value is ignored. Otherwise, it must be at least 15. Maximum possible value
* is 720 (= 12 hours).
* @param standardBolusReason Reason for the standard bolus. If [bolusType] is not
* [CMDDeliverBolusType.STANDARD_BOLUS], this value is ignored.
* @param bolusType Type of the bolus.
* @param bolusStatusUpdateIntervalInMs Interval between status updates,
* in milliseconds. Must be at least 1.
* @throws BolusNotDeliveredException if the pump did not deliver the bolus.
* This typically happens because the pump is currently stopped.
* @throws BolusCancelledByUserException when the bolus was cancelled by the user.
@ -1463,28 +1510,77 @@ class Pump(
* more than one bolus is reported in the Combo's bolus history delta.
* @throws InsufficientInsulinAvailableException if the reservoir does not
* have enough IUs left for this bolus.
* @throws IllegalArgumentException if [bolusAmount] is not in the 0-250 range,
* or if [bolusStatusUpdateIntervalInMs] is less than 1.
* @throws IllegalArgumentException if [totalBolusAmount] is not in the 0-250 range, or
* if [bolusStatusUpdateIntervalInMs] is less than 1, or if [immediateBolusAmount] exceeds
* [totalBolusAmount] when delivering a multiwave bolus, or if [durationInMinutes] is <15
* when [bolusType] is set to anything other than [CMDDeliverBolusType.STANDARD_BOLUS].
* @throws IllegalStateException if the current state is not
* [State.ReadyForCommands].
* @throws AlertScreenException if alerts occurs during this call, and they
* aren't a W6 warning (those are handled by this function).
*/
suspend fun deliverBolus(bolusAmount: Int, bolusReason: StandardBolusReason, bolusStatusUpdateIntervalInMs: Long = 250) = executeCommand(
suspend fun deliverBolus(
totalBolusAmount: Int,
immediateBolusAmount: Int,
durationInMinutes: Int,
standardBolusReason: StandardBolusReason,
bolusType: ApplicationLayer.CMDDeliverBolusType,
bolusStatusUpdateIntervalInMs: Long = 250
) = executeCommand(
// Instruct executeCommand() to not set the mode on its own.
// This function itself switches manually between the
// command and remote terminal modes.
pumpMode = null,
isIdempotent = false,
description = DeliveringBolusCommandDesc(bolusAmount, bolusReason)
description = DeliveringBolusCommandDesc(
totalBolusAmount,
immediateBolusAmount,
durationInMinutes,
standardBolusReason,
bolusType
)
) {
require((bolusAmount > 0) && (bolusAmount <= 250)) {
"Invalid bolus amount $bolusAmount (${bolusAmount.toStringWithDecimal(1)} IU)"
require((totalBolusAmount > 0) && (totalBolusAmount <= 250)) {
"Invalid bolus amount $totalBolusAmount (${totalBolusAmount.toStringWithDecimal(1)} IU)"
}
require(bolusStatusUpdateIntervalInMs >= 1) {
"Invalid bolus status update interval $bolusStatusUpdateIntervalInMs"
}
when (bolusType) {
CMDDeliverBolusType.STANDARD_BOLUS -> Unit
CMDDeliverBolusType.EXTENDED_BOLUS ->
require(
(durationInMinutes >= 15) &&
(durationInMinutes <= 720) &&
((durationInMinutes % 15) == 0)
) {
"extended bolus duration must be in the 15-720 range and an integer multiple of 15; " +
"actual duration: $durationInMinutes"
}
CMDDeliverBolusType.MULTIWAVE_BOLUS -> {
require(
(durationInMinutes >= 15) &&
(durationInMinutes <= 720) &&
((durationInMinutes % 15) == 0)
) {
"multiwave bolus duration must be in the 15-720 range and an integer multiple of 15; " +
"actual duration: $durationInMinutes"
}
require(immediateBolusAmount >= 1) {
"immediate bolus portion of multiwave bolus must be at least 0.1 IU; actual" +
"amount: ${immediateBolusAmount.toStringWithDecimal(1)}"
}
require(immediateBolusAmount < totalBolusAmount) {
"immediate bolus duration must be < total bolus amount; actual immediate/total " +
"amount: ${immediateBolusAmount.toStringWithDecimal(1)}" +
" / ${totalBolusAmount.toStringWithDecimal(1)}"
}
}
}
// Check that there's enough insulin in the reservoir.
statusFlow.value?.let { status ->
// Round the bolus amount. The reservoir fill level is given in whole IUs
@ -1495,25 +1591,30 @@ class Pump(
// IU units, we'd truncate the 0.3 IU from the bolus, and the check
// would think that it's OK, because the reservoir has 1 IU. If we instead
// round up, any fractional IU will be taken into account correctly.
val roundedBolusIU = (bolusAmount + 9) / 10
val roundedBolusIU = (totalBolusAmount + 9) / 10
logger(LogLevel.DEBUG) {
"Checking if there is enough insulin in reservoir; reservoir fill level: " +
"${status.availableUnitsInReservoir} IU; bolus amount: ${bolusAmount.toStringWithDecimal(1)} IU" +
"${status.availableUnitsInReservoir} IU; bolus amount: ${totalBolusAmount.toStringWithDecimal(1)} IU" +
"(rounded: $roundedBolusIU IU)"
}
if (status.availableUnitsInReservoir < roundedBolusIU)
throw InsufficientInsulinAvailableException(bolusAmount, status.availableUnitsInReservoir)
throw InsufficientInsulinAvailableException(totalBolusAmount, status.availableUnitsInReservoir)
} ?: throw IllegalStateException("Cannot deliver bolus without a known pump status")
// Switch to COMMAND mode for the actual bolus delivery
// and for tracking the bolus progress below.
pumpIO.switchMode(PumpIO.Mode.COMMAND)
logger(LogLevel.DEBUG) { "Beginning bolus delivery of ${bolusAmount.toStringWithDecimal(1)} IU" }
val didDeliver = pumpIO.deliverCMDStandardBolus(bolusAmount)
logger(LogLevel.DEBUG) { "Beginning bolus delivery of ${totalBolusAmount.toStringWithDecimal(1)} IU" }
val didDeliver = pumpIO.deliverCMDStandardBolus(
totalBolusAmount,
immediateBolusAmount,
durationInMinutes,
bolusType
)
if (!didDeliver) {
logger(LogLevel.ERROR) { "Bolus delivery did not commence" }
throw BolusNotDeliveredException(bolusAmount)
throw BolusNotDeliveredException(totalBolusAmount)
}
bolusDeliveryProgressReporter.reset(Unit)
@ -1522,47 +1623,59 @@ class Pump(
var bolusFinishedCompletely = false
// The Combo does not send bolus progress information on its own. Instead,
// we have to regularly poll the current bolus status. Do that in this loop.
// The bolusStatusUpdateIntervalInMs value controls how often we poll.
try {
while (true) {
delay(bolusStatusUpdateIntervalInMs)
// The Combo does not send immediate bolus delivery information on its own.
// Instead, we have to regularly poll the current bolus status. Do that in
// this loop. The bolusStatusUpdateIntervalInMs value controls how often we
// poll. Only do this for standard and multiwave boluses, since the extended
// bolus has no immediate delivery portion.
val status = pumpIO.getCMDCurrentBolusDeliveryStatus()
if (bolusType != CMDDeliverBolusType.EXTENDED_BOLUS) {
val expectedImmediateAmount = if (bolusType == CMDDeliverBolusType.STANDARD_BOLUS)
totalBolusAmount
else
immediateBolusAmount
logger(LogLevel.VERBOSE) { "Got current bolus delivery status: $status" }
while (true) {
delay(bolusStatusUpdateIntervalInMs)
val deliveredAmount = when (status.deliveryState) {
ApplicationLayer.CMDBolusDeliveryState.DELIVERING -> bolusAmount - status.remainingAmount
ApplicationLayer.CMDBolusDeliveryState.DELIVERED -> bolusAmount
ApplicationLayer.CMDBolusDeliveryState.CANCELLED_BY_USER -> {
logger(LogLevel.DEBUG) { "Bolus cancelled by user" }
throw BolusCancelledByUserException(
deliveredAmount = bolusAmount - status.remainingAmount,
totalAmount = bolusAmount
)
val status = pumpIO.getCMDCurrentBolusDeliveryStatus()
logger(LogLevel.VERBOSE) { "Got current bolus delivery status: $status" }
val deliveredAmount = when (status.deliveryState) {
ApplicationLayer.CMDBolusDeliveryState.DELIVERING -> expectedImmediateAmount - status.remainingAmount
ApplicationLayer.CMDBolusDeliveryState.DELIVERED -> expectedImmediateAmount
ApplicationLayer.CMDBolusDeliveryState.CANCELLED_BY_USER -> {
logger(LogLevel.DEBUG) { "Bolus cancelled by user" }
throw BolusCancelledByUserException(
deliveredImmediateAmount = expectedImmediateAmount - status.remainingAmount,
totalImmediateAmount = expectedImmediateAmount
)
}
ApplicationLayer.CMDBolusDeliveryState.ABORTED_DUE_TO_ERROR -> {
logger(LogLevel.ERROR) { "Bolus aborted due to a delivery error" }
throw BolusAbortedDueToErrorException(
deliveredImmediateAmount = expectedImmediateAmount - status.remainingAmount,
totalImmediateAmount = expectedImmediateAmount
)
}
else -> continue
}
ApplicationLayer.CMDBolusDeliveryState.ABORTED_DUE_TO_ERROR -> {
logger(LogLevel.ERROR) { "Bolus aborted due to a delivery error" }
throw BolusAbortedDueToErrorException(
deliveredAmount = bolusAmount - status.remainingAmount,
totalAmount = bolusAmount
)
}
else -> continue
}
bolusDeliveryProgressReporter.setCurrentProgressStage(
RTCommandProgressStage.DeliveringBolus(
deliveredAmount = deliveredAmount,
totalAmount = bolusAmount
bolusDeliveryProgressReporter.setCurrentProgressStage(
RTCommandProgressStage.DeliveringBolus(
deliveredImmediateAmount = deliveredAmount,
totalImmediateAmount = expectedImmediateAmount
)
)
)
if (deliveredAmount >= bolusAmount) {
bolusDeliveryProgressReporter.setCurrentProgressStage(BasicProgressStage.Finished)
break
if (deliveredAmount >= expectedImmediateAmount) {
bolusDeliveryProgressReporter.setCurrentProgressStage(BasicProgressStage.Finished)
break
}
}
}
@ -1612,37 +1725,68 @@ class Pump(
if (historyDelta.isEmpty()) {
if (bolusFinishedCompletely) {
logger(LogLevel.ERROR) { "Bolus delivery did not actually occur" }
throw BolusNotDeliveredException(bolusAmount)
throw BolusNotDeliveredException(totalBolusAmount)
}
} else {
var numStandardBolusInfusedEntries = 0
var numRelevantBolusEntries = 0
var unexpectedBolusEntriesDetected = false
scanHistoryDeltaForBolusToEmit(
historyDelta,
reasonForLastStandardBolusInfusion = bolusReason
reasonForLastStandardBolusInfusion = standardBolusReason
) { entry ->
when (val detail = entry.detail) {
is CMDHistoryEventDetail.StandardBolusInfused -> {
numStandardBolusInfusedEntries++
if (numStandardBolusInfusedEntries > 1)
unexpectedBolusEntriesDetected = true
}
when (bolusType) {
CMDDeliverBolusType.STANDARD_BOLUS ->
when (val detail = entry.detail) {
is CMDHistoryEventDetail.StandardBolusInfused -> {
numRelevantBolusEntries++
if (numRelevantBolusEntries > 1)
unexpectedBolusEntriesDetected = true
}
// We ignore this. It always accompanies StandardBolusInfused.
is CMDHistoryEventDetail.StandardBolusRequested ->
Unit
// We ignore this. It always accompanies StandardBolusInfused.
is CMDHistoryEventDetail.StandardBolusRequested ->
Unit
else -> {
if (detail.isBolusDetail)
unexpectedBolusEntriesDetected = true
}
else -> {
if (detail.isBolusDetail)
unexpectedBolusEntriesDetected = true
}
}
CMDDeliverBolusType.EXTENDED_BOLUS ->
when (val detail = entry.detail) {
is CMDHistoryEventDetail.ExtendedBolusStarted -> {
numRelevantBolusEntries++
if (numRelevantBolusEntries > 1)
unexpectedBolusEntriesDetected = true
}
else -> {
if (detail.isBolusDetail)
unexpectedBolusEntriesDetected = true
}
}
CMDDeliverBolusType.MULTIWAVE_BOLUS ->
when (val detail = entry.detail) {
is CMDHistoryEventDetail.MultiwaveBolusStarted -> {
numRelevantBolusEntries++
if (numRelevantBolusEntries > 1)
unexpectedBolusEntriesDetected = true
}
else -> {
if (detail.isBolusDetail)
unexpectedBolusEntriesDetected = true
}
}
}
}
if (bolusFinishedCompletely) {
if (numStandardBolusInfusedEntries == 0) {
logger(LogLevel.ERROR) { "History delta did not contain an entry about bolus infusion" }
throw BolusNotDeliveredException(bolusAmount)
if (numRelevantBolusEntries == 0) {
logger(LogLevel.ERROR) { "History delta did not contain an entry about bolus delivery" }
throw BolusNotDeliveredException(totalBolusAmount)
} else if (unexpectedBolusEntriesDetected) {
logger(LogLevel.ERROR) { "History delta contained unexpected additional bolus entries" }
throw UnaccountedBolusDetectedException()