From 9a0043b46f723154cdcb9e31a078780d89c3f516 Mon Sep 17 00:00:00 2001 From: Philoul Date: Thu, 6 Aug 2026 22:41:48 +0200 Subject: [PATCH 1/3] Fix SiteLocation Step Trial --- .../mobi/ui/actions/cartridge/FillTubingScreen.kt | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillTubingScreen.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillTubingScreen.kt index 4b9e70d7a572..22b25fb1d413 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillTubingScreen.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillTubingScreen.kt @@ -239,18 +239,7 @@ fun FillTubingScreen( body = { if (isInSiteSelectionMode) { aapsLogger.error(TAG, "In Site Location Wizard Step") - - SiteLocationPicker( - siteType = TE.Type.CANNULA_CHANGE, - bodyType = coreCartridgeActionsModel.bodyType(), - entries = coreCartridgeActionsModel.siteRotationEntries(), - selectedLocation = siteLocation, - selectedArrow = siteArrow, - onLocationSelected = { coreCartridgeActionsModel.updateSiteLocation(it) }, - onArrowSelected = { coreCartridgeActionsModel.updateSiteArrow(it) }, - modifier = Modifier.padding(innerPadding) - ) - + SiteLocationWizardStep(host = coreCartridgeActionsModel) } else if (exitFillTubingState.value != null) { Text( text = resourceHelper.gs(R.string.ca_status_heading), From 242e57e1b9f8daac73bdf9d68ff0b9848806074e Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 7 Aug 2026 23:34:08 +0200 Subject: [PATCH 2/3] Fix SiteLocation Step (Screens) --- .../actions/cartridge/CartridgeWorkflowScreen.kt | 13 +++++++++++-- .../mobi/ui/actions/cartridge/FillTubingScreen.kt | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/CartridgeWorkflowScreen.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/CartridgeWorkflowScreen.kt index e8e011c4b26e..62f6069053d5 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/CartridgeWorkflowScreen.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/CartridgeWorkflowScreen.kt @@ -57,6 +57,13 @@ fun CartridgeWorkflowScreen( sendPumpCommands: (List) -> 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, @@ -92,7 +99,6 @@ fun CartridgeWorkflowScreen( Column( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()) .padding( top = innerPadding.calculateTopPadding(), bottom = innerPadding.calculateBottomPadding(), @@ -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() diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillTubingScreen.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillTubingScreen.kt index 22b25fb1d413..0e2351a9b49a 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillTubingScreen.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillTubingScreen.kt @@ -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 @@ -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, @@ -239,7 +240,17 @@ fun FillTubingScreen( body = { if (isInSiteSelectionMode) { aapsLogger.error(TAG, "In Site Location Wizard Step") - SiteLocationWizardStep(host = coreCartridgeActionsModel) + + SiteLocationPicker( + siteType = TE.Type.CANNULA_CHANGE, + bodyType = coreCartridgeActionsModel.bodyType(), + entries = coreCartridgeActionsModel.siteRotationEntries(), + selectedLocation = siteLocation, + selectedArrow = siteArrow, + onLocationSelected = { coreCartridgeActionsModel.updateSiteLocation(it) }, + onArrowSelected = { coreCartridgeActionsModel.updateSiteArrow(it) }, + modifier = Modifier.padding(innerPadding) + ) } else if (exitFillTubingState.value != null) { Text( text = resourceHelper.gs(R.string.ca_status_heading), From 893bbda757443d51d8703e66b11dbacebc7eabd3 Mon Sep 17 00:00:00 2001 From: Philoul Date: Sat, 8 Aug 2026 00:06:08 +0200 Subject: [PATCH 3/3] Fix SiteLocation Database Record --- .../comm/history/HistoryPostProcessor.kt | 99 ++++++++++++++++++- .../comm/ui/CoreCartridgeActionsModel.kt | 54 +++++----- .../common/keys/TandemLongNonPreferenceKey.kt | 8 +- .../keys/TandemStringNonPreferenceKey.kt | 12 ++- 4 files changed, 143 insertions(+), 30 deletions(-) diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/history/HistoryPostProcessor.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/history/HistoryPostProcessor.kt index 27a9c3aa0b9a..e7222072d71b 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/history/HistoryPostProcessor.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/history/HistoryPostProcessor.kt @@ -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 @@ -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 @@ -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() } @@ -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") @@ -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) + } + } \ No newline at end of file diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/CoreCartridgeActionsModel.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/CoreCartridgeActionsModel.kt index 146d83e98e9a..39abbe9cf096 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/CoreCartridgeActionsModel.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/CoreCartridgeActionsModel.kt @@ -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 @@ -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 = @@ -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 diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/keys/TandemLongNonPreferenceKey.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/keys/TandemLongNonPreferenceKey.kt index b8ddc8b2fdc7..57678044431e 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/keys/TandemLongNonPreferenceKey.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/keys/TandemLongNonPreferenceKey.kt @@ -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) } diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/keys/TandemStringNonPreferenceKey.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/keys/TandemStringNonPreferenceKey.kt index d870b874c09c..088ac28eabac 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/keys/TandemStringNonPreferenceKey.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/keys/TandemStringNonPreferenceKey.kt @@ -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) } \ No newline at end of file