Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package app.aaps.pump.tandem.common.comm.history
import android.content.Context
import app.aaps.core.data.model.BS
import app.aaps.core.data.model.TE
import app.aaps.core.data.time.T
import app.aaps.core.interfaces.db.PersistenceLayer
import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag
import app.aaps.core.interfaces.notifications.NotificationManager
Expand All @@ -17,6 +19,8 @@ import app.aaps.pump.tandem.common.concurrency.TandemDispatcher
import app.aaps.pump.tandem.common.database.data.DbDataHandler
import app.aaps.pump.tandem.common.driver.TandemPumpStatus
import app.aaps.pump.tandem.common.driver.connector.TandemPumpConnector
import app.aaps.pump.tandem.common.keys.TandemLongNonPreferenceKey
import app.aaps.pump.tandem.common.keys.TandemStringNonPreferenceKey
import app.aaps.pump.tandem.common.util.TandemPumpUtil
import com.jwoglom.pumpx2.pump.messages.response.historyLog.BolusCompletedHistoryLog
import com.jwoglom.pumpx2.pump.messages.response.historyLog.CannulaFilledHistoryLog
Expand All @@ -32,13 +36,25 @@ class HistoryPostProcessor @Inject constructor(
val pumpStatus: TandemPumpStatus,
val aapsLogger: AAPSLogger,
val pumpSync: PumpSync,
val tandemPumpUtil: TandemPumpUtil
val tandemPumpUtil: TandemPumpUtil,
val preferences: Preferences,
val persistenceLayer: PersistenceLayer
) {

var historyPrefix = ""

companion object {
val TAG = LTag.PUMPCOMM

/**
* How long a parked site location stays valid. Long enough to cover a workflow interrupted
* by a lost connection, short enough that a forgotten selection is not attached to the next
* site change days later.
*/
private val PENDING_SITE_VALIDITY = T.hours(6).msecs()

/** Allowance for drift between pump time and phone time when ordering the two. */
private val PENDING_SITE_CLOCK_SKEW = T.hours(1).msecs()
}


Expand All @@ -54,7 +70,26 @@ class HistoryPostProcessor @Inject constructor(
for (historyLog in historyLogs) {

when(historyLog) {
is CannulaFilledHistoryLog,
is CannulaFilledHistoryLog -> {

aapsLogger.info(TAG, "${historyPrefix}PostProcess - NS Cannula Change")

val timestamp = historyLog.pumpTimeSecInstant.toEpochMilli()

runBlocking {
pumpSync.insertTherapyEventIfNewWithTimestamp(
timestamp = timestamp,
type = TE.Type.CANNULA_CHANGE,
note = null,
pumpId = historyLog.sequenceNum,
pumpType = pumpStatus.pumpType,
pumpSerial = pumpStatus.serialNumber.toString()
)
// The cannula fill is the actual site insertion, so this is the event that
// carries the location the user picked earlier in the cartridge workflow.
applyPendingSiteLocation(timestamp)
}
}
is TubingFilledHistoryLog -> {

aapsLogger.info(TAG, "${historyPrefix}PostProcess - NS Cannula Change")
Expand Down Expand Up @@ -107,4 +142,64 @@ class HistoryPostProcessor @Inject constructor(

}

/**
* Attach the site location picked in the cartridge workflow to the CANNULA_CHANGE event just
* inserted at [timestamp].
*
* The selection is parked in preferences by `CoreCartridgeActionsModel`, because when the user
* confirms it the pump has not yet reported the cannula fill and the event does not exist.
* The parked value is dropped once it expires, so a workflow the user abandoned cannot tag an
* unrelated site change later on.
*/
private suspend fun applyPendingSiteLocation(timestamp: Long) {

val selectedAt = preferences.get(TandemLongNonPreferenceKey.PendingSiteSelectedAt)
if (selectedAt == 0L) return

if (System.currentTimeMillis() > selectedAt + PENDING_SITE_VALIDITY) {
aapsLogger.info(TAG, "${historyPrefix}PostProcess - pending site location expired, discarded")
clearPendingSiteLocation()
return
}

// A history backfill can deliver cannula fills that predate the selection. Those describe
// earlier site changes, so leave them alone and keep waiting for the matching one. Pump and
// phone clocks are compared here, hence the tolerance.
if (timestamp < selectedAt - PENDING_SITE_CLOCK_SKEW) {
aapsLogger.info(TAG, "${historyPrefix}PostProcess - cannula fill at $timestamp predates site selection, skipped")
return
}

val location = preferences.get(TandemStringNonPreferenceKey.PendingSiteLocation)
.takeIf { it.isNotEmpty() }
?.let { name -> TE.Location.entries.firstOrNull { it.name == name } }
val arrow = preferences.get(TandemStringNonPreferenceKey.PendingSiteArrow)
.takeIf { it.isNotEmpty() }
?.let { name -> TE.Arrow.entries.firstOrNull { it.name == name } }

if (location == null && arrow == null) {
clearPendingSiteLocation()
return
}

val event = persistenceLayer.getTherapyEventDataFromToTime(timestamp, timestamp)
.firstOrNull { it.type == TE.Type.CANNULA_CHANGE }

if (event == null) {
// Keep the selection parked: a later history pass may still deliver the event.
aapsLogger.warn(TAG, "${historyPrefix}PostProcess - no CANNULA_CHANGE at $timestamp, site location stays pending")
return
}

persistenceLayer.insertOrUpdateTherapyEvent(event.copy(location = location, arrow = arrow))
aapsLogger.info(TAG, "${historyPrefix}PostProcess - site location $location / arrow $arrow attached to CANNULA_CHANGE")
clearPendingSiteLocation()
}

private fun clearPendingSiteLocation() {
preferences.put(TandemStringNonPreferenceKey.PendingSiteLocation, "")
preferences.put(TandemStringNonPreferenceKey.PendingSiteArrow, "")
preferences.put(TandemLongNonPreferenceKey.PendingSiteSelectedAt, 0L)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import app.aaps.core.keys.interfaces.Preferences
import app.aaps.core.ui.compose.siteRotation.BodyType
import app.aaps.core.ui.compose.siteRotation.SiteLocationStepHost
import app.aaps.pump.tandem.common.driver.TandemPumpStatus
import app.aaps.pump.tandem.common.keys.TandemLongNonPreferenceKey
import app.aaps.pump.tandem.common.keys.TandemStringNonPreferenceKey
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -122,18 +124,39 @@ class CoreCartridgeActionsModel @Inject constructor(
// aapsLogger.error(LTag.PUMP, "moveAfterPriming NOT IMPLEMENTED")
// }

/**
* Park the selection until the pump reports the cannula fill.
*
* The CANNULA_CHANGE therapy event is not created here: it is inserted by
* `HistoryPostProcessor` when the pump's `CannulaFilledHistoryLog` is retrieved, which happens
* after this workflow ends. Preferences are used rather than in-memory state so the selection
* survives a process restart while waiting.
*/
override fun completeSiteLocation() {
// Site location is saved after activation completes (patchStartTime not available yet)
//moveStep(PatchStep.ATTACH_PATCH)
// TODO completeSiteLocation
aapsLogger.error(LTag.PUMP, "completeSiteLocation NOT IMPLEMENTED")
val location = _siteLocation.value.takeIf { it != TE.Location.NONE }
val arrow = _siteArrow.value.takeIf { it != TE.Arrow.NONE }
if (location == null && arrow == null) {
clearPendingSiteLocation()
return
}
preferences.put(TandemStringNonPreferenceKey.PendingSiteLocation, location?.name ?: "")
preferences.put(TandemStringNonPreferenceKey.PendingSiteArrow, arrow?.name ?: "")
preferences.put(TandemLongNonPreferenceKey.PendingSiteSelectedAt, System.currentTimeMillis())
aapsLogger.info(LTag.PUMP, "completeSiteLocation: parked location=$location arrow=$arrow")
}


override fun skipSiteLocation() {
aapsLogger.error(LTag.PUMP, "skipSiteLocation")
aapsLogger.info(LTag.PUMP, "skipSiteLocation")
_siteLocation.value = TE.Location.NONE
_siteArrow.value = TE.Arrow.NONE
clearPendingSiteLocation()
}

private fun clearPendingSiteLocation() {
preferences.put(TandemStringNonPreferenceKey.PendingSiteLocation, "")
preferences.put(TandemStringNonPreferenceKey.PendingSiteArrow, "")
preferences.put(TandemLongNonPreferenceKey.PendingSiteSelectedAt, 0L)
}

override fun bodyType(): BodyType =
Expand All @@ -151,27 +174,6 @@ class CoreCartridgeActionsModel @Inject constructor(
}
}

/** Save site location/arrow to the CANNULA_CHANGE therapy event created during activation. */
private fun saveSiteLocationToTherapyEvent(activationTimestamp: Long) {
val location = _siteLocation.value.takeIf { it != TE.Location.NONE }
val arrow = _siteArrow.value.takeIf { it != TE.Arrow.NONE }
aapsLogger.error(LTag.PUMP, "saveSiteLocationToTherapyEvent NOT IMPLEMENTED")
if (location != null || arrow != null) {

// scope.launch {
// try {
// val entries = persistenceLayer.getTherapyEventDataFromToTime(activationTimestamp, activationTimestamp)
// .filter { it.type == TE.Type.CANNULA_CHANGE }
// entries.firstOrNull()?.let { te ->
// persistenceLayer.insertOrUpdateTherapyEvent(te.copy(location = location, arrow = arrow))
// }
// } catch (_: Exception) {
// // location is optional
// }
// }
}
}

// endregion


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ enum class TandemLongNonPreferenceKey(

LastTbrId("tandem_last_tbr_id", defaultValue = 0L),
SiteReminderDateTime(key = "tandem_site_reminder_datetime", defaultValue = 0L),
HistoryResumeUpperSequence(key = "tandem_history_resume_upper_sequence", defaultValue = 0L, exportable = false)
HistoryResumeUpperSequence(key = "tandem_history_resume_upper_sequence", defaultValue = 0L, exportable = false),

/**
* When the user confirmed a site location that is still waiting to be attached to a
* CANNULA_CHANGE event. `0` means nothing is pending.
*/
PendingSiteSelectedAt(key = "tandem_pending_site_selected_at", defaultValue = 0L, exportable = false)

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ enum class TandemStringNonPreferenceKey(
override val defaultValue: String,
override val exportable: Boolean) : StringNonPreferenceKey {

HistorySummaryData("tandem_history_summary", "", true)
HistorySummaryData("tandem_history_summary", "", true),

/**
* Site location picked in the cartridge workflow, parked until the pump reports the matching
* `CannulaFilledHistoryLog`. Holds a [app.aaps.core.data.model.TE.Location] name, empty when none is pending.
*/
PendingSiteLocation("tandem_pending_site_location", "", false),

/**
* Site arrow picked in the cartridge workflow, parked alongside [PendingSiteLocation].
* Holds a [app.aaps.core.data.model.TE.Arrow] name, empty when none is pending.
*/
PendingSiteArrow("tandem_pending_site_arrow", "", false)

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ fun CartridgeWorkflowScreen(
sendPumpCommands: (List<Message>) -> Boolean,
refreshScope: CoroutineScope,
showHeader: Boolean = true,
/**
* Whether the [body] slot scrolls. Keep `true` for text-based steps. Set to `false` when [body]
* hosts a component that sizes itself with `Modifier.weight()` (e.g. the site location picker):
* a scrollable parent measures its children with an unbounded height, which collapses every
* weighted child to 0 px.
*/
scrollableBody: Boolean = true,
aapsLogger: AAPSLogger? = null,
stepIndicator: @Composable () -> Unit = {},
body: @Composable ColumnScope.() -> Unit,
Expand Down Expand Up @@ -92,7 +99,6 @@ fun CartridgeWorkflowScreen(
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(
top = innerPadding.calculateTopPadding(),
bottom = innerPadding.calculateBottomPadding(),
Expand All @@ -113,13 +119,16 @@ fun CartridgeWorkflowScreen(
}
CartridgeNotificationsPanel(resourceHelper = resourceHelper)
}
// Body takes the remaining height, so the action bar stays pinned to the bottom and
// weight-based content in [body] gets a bounded height to measure against.
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.then(if (scrollableBody) Modifier.verticalScroll(rememberScrollState()) else Modifier)
.padding(16.dp),
content = body,
)
Spacer(modifier = Modifier.weight(1f))
Column(
modifier = Modifier
.fillMaxWidth()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag
import app.aaps.core.interfaces.resources.ResourceHelper
import app.aaps.core.ui.compose.siteRotation.SiteLocationPicker
import app.aaps.core.ui.compose.siteRotation.SiteLocationWizardStep
import app.aaps.pump.common.defs.PumpRunningState
import app.aaps.pump.common.test.ResourceHelperTest
import app.aaps.pump.tandem.R
Expand Down Expand Up @@ -225,6 +224,8 @@ fun FillTubingScreen(
onBack = ::requestCancelOrBack,
resourceHelper = resourceHelper,
showHeader = showHeader,
// The site picker sizes itself with Modifier.weight() and needs a bounded height.
scrollableBody = !isInSiteSelectionMode,
stepIndicator = {
WizardStepIndicator(
currentStep = currentStep,
Expand All @@ -250,7 +251,6 @@ fun FillTubingScreen(
onArrowSelected = { coreCartridgeActionsModel.updateSiteArrow(it) },
modifier = Modifier.padding(innerPadding)
)

} else if (exitFillTubingState.value != null) {
Text(
text = resourceHelper.gs(R.string.ca_status_heading),
Expand Down