From f1181853ff9647a154c5872ab3ad5731276fce36 Mon Sep 17 00:00:00 2001 From: Haaris Edhi Date: Mon, 31 Aug 2026 11:52:54 -0500 Subject: [PATCH 1/5] more connect flow work --- build.gradle.kts | 1 + gradle/libs.versions.toml | 2 +- .../comm/TandemPumpCommunicationManager.kt | 38 +++++++- .../common/comm/ui/TandemUICommunication.kt | 4 + .../common/comm/ui/TandemUIDataStore.kt | 7 ++ .../connector/TandemPumpConnectionManager.kt | 6 +- .../pump/tandem/mobi/TandemMobiPumpPlugin.kt | 20 ++--- .../ui/actions/cartridge/FillCannulaScreen.kt | 90 +++++++++++++++---- .../mobi/ui/overview/MobiComposeContent.kt | 30 +++++++ 9 files changed, 166 insertions(+), 32 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8c22c368524f..47a7560d7b1c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,6 +34,7 @@ val kotlinMetadataVersion = libs.versions.kotlin.get() allprojects { repositories { + mavenLocal() google() mavenCentral() maven("https://jitpack.io") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d4594c9638f4..fa6cce13639d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -196,7 +196,7 @@ androidx-glance-appwidget = { group = "androidx.glance", name = "glance-appwidge com-jakewharton-timber = { group = "com.jakewharton.timber", name = "timber", version="5.0.1" } com-github-weliem-blessed-android = { group = "com.github.weliem", name = "blessed-android", version="2.4.0" } io-github-vanpra-compose-dialogs-datetime = { group = "io.github.vanpra.compose-material-dialogs", name = "datetime", version="0.9.0" } -com-github-jwoglom-pumpx2-android = { group = "com.github.jwoglom.pumpX2", name = "pumpx2-android", version="v1.9.1" } +com-github-jwoglom-pumpx2-android = { group = "com.github.jwoglom.pumpX2", name = "pumpx2-android", version="v1.9.0-tandem-2" } androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt index d1612c130846..893eede4d755 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt @@ -62,6 +62,12 @@ class TandemPumpCommunicationManager( lateinit var peripheral: BluetoothPeripheral @Volatile var connected = false @Volatile var errorConnecting = false + + // True while the *host* (command queue / user) has intentionally disconnected, e.g. the + // idle-disconnect after waitForDisconnectionInSeconds(). Used to veto pumpX2's internal + // auto-reconnect for planned teardowns: resurrecting the link creates a zombie connection + // that occupies the pump's single connection slot and blocks the next real connectToPump(). + @Volatile var plannedDisconnect = false var commandRequestModeRunning: Boolean = false get() { return inFlightRequests.isNotEmpty() } @@ -94,6 +100,7 @@ class TandemPumpCommunicationManager( val COMMAND_TIMEOUT = 5 * 1000 // 5s (in ms) timeout for receiving pump command response val HANDSHAKE_TIMEOUT = 30 * 1000L // 30s (in ms) timeout for handshake (pairing) connecting flow val CONNECT_TIMEOUT = 60 * 1000L // 60s (in ms) timeout for complete connecting flow + val CONNECT_TOTAL_TIMEOUT = 180 * 1000L // 180s (in ms) hard cap for the whole connect call } @@ -106,6 +113,7 @@ class TandemPumpCommunicationManager( } connected = false + plannedDisconnect = false val connectStartTime = System.currentTimeMillis() operationMode = OperationMode.ConnectionMode bluetoothHandler!!.startScan() @@ -130,6 +138,15 @@ class TandemPumpCommunicationManager( aapsLogger.error(TAG, "Connection timeout after ${CONNECT_TIMEOUT / 1000}s, forcing disconnect") errorConnecting = true forceDisconnect(onDisconnect = false, tandemError = null) + } else if (System.currentTimeMillis() - connectStartTime > CONNECT_TOTAL_TIMEOUT) { + // Hard cap independent of handshakingStartTime: when the link drops BEFORE + // service discovery completes, handshakingStartTime stays 0 and neither of + // the timeouts above can fire - this loop then blocked forever holding + // inConnectMode=true, silently swallowing every future connect attempt. + aapsLogger.error(TAG, "Connect total deadline (${CONNECT_TOTAL_TIMEOUT / 1000}s) exceeded - aborting connect") + handshakingStartTime = 0L + errorConnecting = true + forceDisconnect(onDisconnect = false, tandemError = null) } } @@ -155,6 +172,10 @@ class TandemPumpCommunicationManager( aapsLogger.info(TAG, "disconnect()") + // Mark as host-initiated BEFORE tearing down, so the pumpX2 disconnect callback knows + // not to auto-reconnect (see onPumpDisconnected). + plannedDisconnect = true + // Fully tear down the BLE handler: cancel any live peripheral // connection, stop the central, and null the pumpx2 singleton so the // next connect() builds a fresh handler. blessed's close() alone does @@ -481,9 +502,18 @@ class TandemPumpCommunicationManager( override fun onPumpDisconnected(peripheral: BluetoothPeripheral?, status: HciStatus?): Boolean { aapsLogger.error(TAG, "Pump Disconnected: $status") + // Veto pumpX2's auto-reconnect when the queue/host intentionally disconnected: + // the command queue owns the connection lifecycle, and an autonomous reconnect here + // creates a zombie link that blocks the next real connectToPump(). Genuine RF drops + // still return true and keep the 250ms auto-reconnect. + val shouldReconnect = !plannedDisconnect + if (plannedDisconnect) { + aapsLogger.info(TAG, "Planned (host-initiated) disconnect - suppressing pumpX2 auto-reconnect") + } forceDisconnect(onDisconnect = true, hciStatus = status) - return super.onPumpDisconnected(peripheral, status) + plannedDisconnect = false + return shouldReconnect && super.onPumpDisconnected(peripheral, status) } fun forceDisconnect(onDisconnect: Boolean, hciStatus: HciStatus? = null, tandemError: TandemError? = null) { @@ -494,7 +524,11 @@ class TandemPumpCommunicationManager( tandemError = tandemError) pumpUtil.driverStatus = PumpDriverState.Disconnected rxBus.send(EventPumpFragmentValuesChanged(PumpUpdateFragmentType.PumpStatus)) - tandemConnectionFixer.startConnectionFix() + if (!plannedDisconnect) { + // The ConnectionFixer recovers from *unplanned* failures; during a planned + // teardown it would just re-connect the link we were asked to close. + tandemConnectionFixer.startConnectionFix() + } } diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUICommunication.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUICommunication.kt index 0914a670dced..a29078312e03 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUICommunication.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUICommunication.kt @@ -139,6 +139,10 @@ class TandemUICommunication @Inject constructor ( dataStore.pumpRunningState.value = runningState pumpStatus.pumpRunningState = runningState + // Ground truth of actual delivery state - command ACKs (e.g. ResumePumping) + // can report success while the pump has not actually resumed. + dataStore.mirrorBasalStatus.value = message.basalStatusIcon + pumpStatus.pumpStatusMirror = HomeScreenMirrorDto() pumpStatus.pumpStatusMirror!!.parse(message.cargo) } diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUIDataStore.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUIDataStore.kt index 21a2b36335af..42d47760843f 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUIDataStore.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUIDataStore.kt @@ -13,6 +13,7 @@ import app.aaps.pump.tandem.common.database.data.dto.TandemQualifyingEventDto import com.jwoglom.pumpx2.pump.TandemError import com.jwoglom.pumpx2.pump.messages.Message import com.jwoglom.pumpx2.pump.messages.response.currentStatus.HistoryLogStatusResponse +import com.jwoglom.pumpx2.pump.messages.response.currentStatus.HomeScreenMirrorResponse import com.jwoglom.pumpx2.pump.messages.response.currentStatus.InsulinStatusResponse import com.jwoglom.pumpx2.pump.messages.response.currentStatus.TimeSinceResetResponse import com.jwoglom.pumpx2.pump.messages.models.NotificationBundle @@ -82,6 +83,12 @@ class TandemUIDataStore : TandemUiState, TandemUiStateWriter { val fillCannulaState = MutableLiveData() val loadStatus = MutableLiveData() val insulinStatus = MutableLiveData() + val mirrorBasalStatus = MutableLiveData() + + // Mirror-derived ground truth for whether the pump is ACTUALLY delivering basal. + // pumpRunningState above is set optimistically when commands are ACKed - observed + // 2026-08-25: ResumePumpingResponse returned status=0 while HomeScreenMirror kept + // reporting basalStatusIcon=SUSPEND. Workflows must verify against THIS value. // Per-visit completion set; cleared on CartridgeActions exit. val completedCartridgeActions = MutableLiveData>(emptySet()) diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnectionManager.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnectionManager.kt index b45b5b60fa2f..178e6ee8debb 100755 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnectionManager.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnectionManager.kt @@ -56,7 +56,11 @@ class TandemPumpConnectionManager @Inject constructor( aapsLogger.info(LTag.PUMPCOMM, "connect To Pump") if (inConnectMode) { - return false; + // Another attempt holds the lock (its blocking connect loop is still waiting). + // This used to return silently, which made repeated connect calls no-ops + // invisible in the logs. + aapsLogger.warn(LTag.PUMPCOMM, "connectToPump skipped: another connect attempt is in progress") + return false } if (this.tandemConnector.isConnected()) { diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/TandemMobiPumpPlugin.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/TandemMobiPumpPlugin.kt index 97a7e2a81d7f..262b3235eb44 100755 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/TandemMobiPumpPlugin.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/TandemMobiPumpPlugin.kt @@ -674,20 +674,16 @@ class TandemMobiPumpPlugin @Inject constructor( //aapsLogger.debug(LTag.PUMP, logPrefix + "DUB isConnecting [status=$status]") - val unreachable = (error!=null && error==PumpErrorType.PumpUnreachable) + // Limited override - PumpUnreachable only. It is LATCHED by onPumpCriticalError on + // transient link failures (e.g. INSUFFICIENT_AUTHORIZATION / + // CONNECTION_UPDATE_FAILED while separating from the pump) and is never cleared, + // so letting it drive isConnecting() left the command queue stuck in its + // "connecting" branch even after a successful reconnect. Any other latched error + // keeps the legacy behavior. + val unreachable = (error != null && error == PumpErrorType.PumpUnreachable) if (displayConnectionMessages) aapsLogger.debug(LTag.PUMP, logPrefix + "DUB isConnecting [status=$status]") - return status == PumpDriverState.Connecting || unreachable - - // return !isServiceSet || tandemService?.isInitialized != true - - // if (!driverInitialized) - // return false - // - // val driverStatus = tandemUtil.driverStatus - // if (displayConnectionMessages) aapsLogger.debug(LTag.PUMP, "isConnecting - " + driverStatus.name) - // - // return driverStatus == PumpDriverState.Connecting + return status == PumpDriverState.Connecting && !unreachable } // override fun connect(reason: String) { diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillCannulaScreen.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillCannulaScreen.kt index 1f106c751b9a..842eb18c31ac 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillCannulaScreen.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillCannulaScreen.kt @@ -69,6 +69,7 @@ import com.jwoglom.pumpx2.pump.messages.request.currentStatus.HomeScreenMirrorRe import com.jwoglom.pumpx2.pump.messages.request.currentStatus.LoadStatusRequest import com.jwoglom.pumpx2.pump.messages.request.currentStatus.TimeSinceResetRequest import com.jwoglom.pumpx2.pump.messages.response.controlStream.FillCannulaStateStreamResponse +import com.jwoglom.pumpx2.pump.messages.response.currentStatus.HomeScreenMirrorResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -122,7 +123,10 @@ fun FillCannulaScreen( LaunchedEffect(Unit) { aapsLogger.info(TAG, "Initial alert/alarm poll on FillCannulaScreen") - sendPumpCommands(listOf(AlertStatusRequest(), AlarmStatusRequest())) + // Mirror FIRST so pumpSuspended reflects reality before the buttons enable. The + // cached fillCannulaState is intentionally KEPT: re-entering this screen after a + // completed fill should still show the "filled" state. + sendPumpCommands(listOf(HomeScreenMirrorRequest(), AlertStatusRequest(), AlarmStatusRequest())) } LaunchedEffect(intervalOf(10)) { @@ -132,6 +136,33 @@ fun FillCannulaScreen( val pumpRunningState = ds.pumpRunningState.observeAsState() val fillCannulaState = ds.fillCannulaState.observeAsState() + val mirrorBasalStatus = ds.mirrorBasalStatus.observeAsState() + + // Delivery state shown by the buttons - newest source wins: + // 1. commands on this screen set it optimistically (instant feedback), + // 2. HomeScreenMirror samples are ground truth and correct it, but only outside the + // short grace window right after a command (a mirror captured before the command + // took effect must not flip the UI back). + var uiSuspended by remember { mutableStateOf(null) } + var uiSetAtMs by remember { mutableStateOf(0L) } + val mirrorIcon = mirrorBasalStatus.value + + LaunchedEffect(mirrorIcon) { + val m = mirrorIcon ?: return@LaunchedEffect + val inGrace = uiSuspended != null && System.currentTimeMillis() - uiSetAtMs < MIRROR_GRACE_MS + if (!inGrace) { + uiSuspended = m == HomeScreenMirrorResponse.BasalStatusIcon.SUSPEND + uiSetAtMs = System.currentTimeMillis() + } + } + + val pumpSuspended = uiSuspended == true + val deliveryKnown = uiSuspended != null + + fun setDeliverySuspended(v: Boolean) { + uiSuspended = v + uiSetAtMs = System.currentTimeMillis() + } val notificationBundle = ds.notificationBundle.observeAsState() val notifications: List = notificationBundle.value?.get()?.toList() ?: emptyList() @@ -144,6 +175,7 @@ fun FillCannulaScreen( if (isInActiveMode) { showCancelDialog = true } else { + aapsLogger.info(TAG, "FC-NAV: requestCancelOrBack -> navigateBack (not active mode)") navigateBack() } } @@ -177,6 +209,7 @@ fun FillCannulaScreen( confirmButton = { TextButton(onClick = { showSuspendDialog = false + setDeliverySuspended(true) isSuspending = true sendPumpCommand(SuspendPumpingRequest()) refreshScope.launch { @@ -208,21 +241,41 @@ fun FillCannulaScreen( TextButton(onClick = { showResumeDialog = false isResuming = true - sendPumpCommand(ResumePumpingRequest()) refreshScope.launch { - repeat(5) { - if (pumpRunningState.value == PumpRunningState.Running) { - return@repeat + // Verified resume: ResumePumpingResponse can ACK success while the + // pump stays suspended (observed 2026-08-25). HomeScreenMirror's + // basalStatusIcon is the ground truth - BUT note the loop may be + // running a zero-temp, in which case the icon is ZERO_TEMP_RATE, + // not BASAL. Anything != SUSPEND means delivering. + var resumed = false + repeat(3) { + if (resumed) return@repeat + sendPumpCommand(ResumePumpingRequest()) + repeat(6) { + withContext(Dispatchers.IO) { Thread.sleep(1000) } + sendPumpCommand(HomeScreenMirrorRequest()) + val icon = mirrorBasalStatus.value + if (icon != null && icon != HomeScreenMirrorResponse.BasalStatusIcon.SUSPEND) { + resumed = true + return@repeat + } } - withContext(Dispatchers.IO) { Thread.sleep(1000) } - sendPumpCommand(HomeScreenMirrorRequest()) } isResuming = false - ds.completedCartridgeActions.value = - (ds.completedCartridgeActions.value ?: emptySet()) + - CompletedCartridgeAction.FILL_CANNULA - ds.loadStatus.value = null - navigateBack() + if (resumed) { + ds.completedCartridgeActions.value = + (ds.completedCartridgeActions.value ?: emptySet()) + + CompletedCartridgeAction.FILL_CANNULA + ds.loadStatus.value = null + aapsLogger.info(TAG, "FC-NAV: resume VERIFIED -> navigateBack") + setDeliverySuspended(false) + navigateBack() + } else { + aapsLogger.error(TAG, "FC-NAV: resume NOT verified after retries - showing exit warning") + // Delivery did not resume - keep the screen open with the + // exit warning so this cannot silently pass. + showExitWithoutResumeDialog = true + } } }) { Text(resourceHelper.gs(R.string.ca_btn_resume_insulin)) } }, @@ -339,7 +392,7 @@ fun FillCannulaScreen( text = resourceHelper.gs(R.string.fc_filling_state, fillCannulaState.value?.stateId), style = MaterialTheme.typography.bodyMedium ) - } else if (pumpRunningState.value == PumpRunningState.Suspended) { + } else if (pumpSuspended) { Text( text = resourceHelper.gs(R.string.ca_before_you_start_heading), style = MaterialTheme.typography.titleMedium, @@ -443,7 +496,7 @@ fun FillCannulaScreen( PrimaryActionButton( text = resourceHelper.gs(R.string.ca_btn_resume_insulin), onClick = { showResumeDialog = true }, - enabled = pumpRunningState.value == PumpRunningState.Suspended, + enabled = deliveryKnown && pumpSuspended, loading = isResuming, modifier = Modifier.weight(1f) ) @@ -455,7 +508,7 @@ fun FillCannulaScreen( ) } } else { - if (pumpRunningState.value != PumpRunningState.Suspended) { + if (deliveryKnown && !pumpSuspended) { SecondaryActionButton( text = resourceHelper.gs(R.string.ca_btn_suspend_insulin), onClick = { showSuspendDialog = true }, @@ -480,7 +533,7 @@ fun FillCannulaScreen( } }, modifier = Modifier.weight(1f), - enabled = pumpRunningState.value == PumpRunningState.Suspended && + enabled = deliveryKnown && pumpSuspended && cannulaFillAmount != null && allowedCannulaFillAmount(cannulaFillAmount) && !hasActiveNotifications @@ -491,6 +544,11 @@ fun FillCannulaScreen( ) } +// Grace window (ms) after a user suspend/resume during which mirror samples do not +// override the optimistic UI state - a sample captured before the command took effect is +// stale and would flip the screen back. +private const val MIRROR_GRACE_MS = 8000L + val fillCannulaScreenCommands = listOf( HomeScreenMirrorRequest(), TimeSinceResetRequest(), diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/overview/MobiComposeContent.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/overview/MobiComposeContent.kt index 1b151391cc40..e215aac7f5a7 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/overview/MobiComposeContent.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/overview/MobiComposeContent.kt @@ -256,6 +256,36 @@ class MobiComposeContent( } + // Release the UI-workflow comm locks whenever navigation LEAVES the Actions/Data + // screens. Those screens latch preventConnect + preventQueueExecution on entry + // (refreshMainAppData START_ACTIONS / START_DATA) and the only other release is the + // ACTIONS-tab toolbar back-arrow - so leaving via the system back gesture, a bottom + // tab switch or any future route stalled the command queue in "busy" indefinitely. + val actionsScreens = setOf( + MobiScreen.ACTIONS, + MobiScreen.ACTIONS_CARTRIDGE_ACTIONS, + MobiScreen.ACTIONS_CHANGE_CARTRIDGE, + MobiScreen.ACTIONS_FILL_TUBING, + MobiScreen.ACTIONS_FILL_CANNULA, + MobiScreen.ACTIONS_PUMP_INFO, + MobiScreen.ACTIONS_DEBUG_COMMANDS, + MobiScreen.ACTIONS_SITE_REMINDER + ) + val dataScreens = setOf(MobiScreen.DATA, MobiScreen.DATA_NOTIFICATIONS, MobiScreen.DATA_EVENTS, MobiScreen.DATA_HISTORY) + var lastScreen by remember { mutableStateOf(null) } + LaunchedEffect(currentScreen) { + val leftActions = lastScreen in actionsScreens && currentScreen !in actionsScreens + val leftData = lastScreen in dataScreens && currentScreen !in dataScreens + if (leftActions || leftData) { + aapsLogger.info(LTag.PUMP, "Leaving ${lastScreen} -> ${currentScreen}: releasing UI workflow comm locks") + tandemUiController.disposeTandemUiCommunication( + TandemUiController.AdditionalConfigurationScreens.Data.takeIf { leftData } + ?: TandemUiController.AdditionalConfigurationScreens.Actions + ) + } + lastScreen = currentScreen + } + val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { val observer = LifecycleEventObserver { _, event -> From 71c1d2a9ff196f7b6c71fbb758c5105cd51122d1 Mon Sep 17 00:00:00 2001 From: Haaris Edhi Date: Mon, 31 Aug 2026 15:23:55 -0500 Subject: [PATCH 2/5] more connect flow work --- .../common/comm/TandemPumpCommunicationManager.kt | 11 +++++++++++ .../common/concurrency/PumpAvailabilitySync.kt | 12 ++++++++++++ .../driver/connector/TandemPumpConnectionManager.kt | 10 ++++++++++ .../common/driver/connector/TandemPumpConnector.kt | 10 ++++++++++ 4 files changed, 43 insertions(+) diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt index 893eede4d755..e26052c326a9 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt @@ -404,6 +404,13 @@ class TandemPumpCommunicationManager( this.connected = true this.operationMode = OperationMode.StandardOperation + // This handler runs on EVERY (re)connect, including pumpx2's internal auto-reconnect + // after an RF drop — a path that bypasses connect(). Publish the connection fact here + // so pumpConnectedFlow can never go stale-false while the link is demonstrably up. + // (Stale false made PumpAvailabilitySync hold Unknown and fast-fail all delivery ops.) + pumpStatus.pumpConnectedFlow.value = true + dataStore.postPumpConnected(true) + } else if (message is PumpVersionResponse) { dataStore.postPumpVersionResponse(message) } @@ -523,6 +530,10 @@ class TandemPumpCommunicationManager( hciStatus = hciStatus, tandemError = tandemError) pumpUtil.driverStatus = PumpDriverState.Disconnected + // Unplanned drops must drop the connection flow too: PumpAvailabilitySync maps + // (connected=false, running) to Unknown, which is the conservative gate state until + // the link (and a status read) come back. + publishDisconnectedState() rxBus.send(EventPumpFragmentValuesChanged(PumpUpdateFragmentType.PumpStatus)) if (!plannedDisconnect) { // The ConnectionFixer recovers from *unplanned* failures; during a planned diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/concurrency/PumpAvailabilitySync.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/concurrency/PumpAvailabilitySync.kt index ee634cb8dfd9..68a15c4dd05d 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/concurrency/PumpAvailabilitySync.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/concurrency/PumpAvailabilitySync.kt @@ -55,6 +55,18 @@ class PumpAvailabilitySync @Inject constructor( private fun recompute(connected: Boolean, running: PumpRunningState) { when { + // Watchdog: status reads report the pump running, yet the connection flow says + // otherwise. This signature means pumpConnectedFlow is stale (an auto-reconnect + // path bypassed connect()). getPumpStatus() self-heals this within one status + // cycle; if this error persists, a new connection path is skipping flow updates. + !connected && running == PumpRunningState.Running -> { + logger.error( + LTag.PUMP, + "PumpAvailabilitySync: divergence — pump reports Running but pumpConnectedFlow=false; " + + "delivery gate is blocking ops (self-heals on next successful status read)" + ) + availability.markUnknown("not connected (divergence: status reads report Running)") + } !connected -> availability.markUnknown("not connected") running == PumpRunningState.Suspended -> availability.markDisabled("pump suspended") running == PumpRunningState.Running -> availability.markEnabled("pump running") diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnectionManager.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnectionManager.kt index 178e6ee8debb..ce1cfe7e54cb 100755 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnectionManager.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnectionManager.kt @@ -17,6 +17,7 @@ import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.keys.interfaces.Preferences import app.aaps.pump.common.defs.BolusData import app.aaps.pump.tandem.common.driver.TandemPumpStatus +import app.aaps.pump.tandem.common.driver.tandemDataStore import app.aaps.pump.common.defs.PumpDriverMode import app.aaps.pump.common.defs.PumpUpdateFragmentType import app.aaps.pump.common.defs.TempBasalPair @@ -64,6 +65,15 @@ class TandemPumpConnectionManager @Inject constructor( } if (this.tandemConnector.isConnected()) { + // The pumpx2 layer can be connected (auto-reconnect) while pumpConnectedFlow is + // still false from an earlier drop — a stale flow makes PumpAvailabilitySync hold + // Unknown and every delivery op fast-fail. Repair instead of reporting a "fixed" + // connection that the availability gate still treats as disconnected. + if (!tandemPumpStatus.pumpConnectedFlow.value) { + aapsLogger.warn(TAG, "connectToPump: link is up but pumpConnectedFlow is stale (false) — republishing connected state") + tandemPumpStatus.pumpConnectedFlow.value = true + tandemDataStore.postPumpConnected(true) + } return true } diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnector.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnector.kt index cc9db3325a06..7beb518b42fc 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnector.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnector.kt @@ -1281,6 +1281,16 @@ class TandemPumpConnector @Inject constructor(var tandemPumpStatus: TandemPumpSt tandemPumpStatus.pumpRunningState = runningState // Mirror to UI LiveData so the UI reflects loop-observed state. tandemDataStore.postPumpRunningState(runningState) + + // A successful status read proves the link is up. If pumpConnectedFlow is still + // false (stale after a drop whose auto-reconnect bypassed connect()), repair it + // here so the availability gate resolves within one status cycle instead of + // blocking delivery (TBR/bolus) for hours. + if (!tandemPumpStatus.pumpConnectedFlow.value) { + aapsLogger.error(LTag.PUMP, "getPumpStatus: status read succeeded but pumpConnectedFlow is false — repairing stale connection state") + tandemPumpStatus.pumpConnectedFlow.value = true + tandemDataStore.postPumpConnected(true) + } rxBus.send(EventPumpFragmentValuesChanged(PumpUpdateFragmentType.PumpStatus)) return DataCommandResponse( From cfebe183fb257f1d88621156a440d6f1f9d78031 Mon Sep 17 00:00:00 2001 From: Haaris Edhi Date: Mon, 31 Aug 2026 23:54:22 -0500 Subject: [PATCH 3/5] final flags and states --- .../common/comm/TandemPumpCommunicationManager.kt | 10 ++++++++++ .../common/driver/connector/TandemPumpConnector.kt | 13 +++++++++++++ .../mobi/ui/actions/cartridge/FillCannulaScreen.kt | 14 +++++++++++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt index e26052c326a9..f153505fb2ed 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt @@ -411,6 +411,16 @@ class TandemPumpCommunicationManager( pumpStatus.pumpConnectedFlow.value = true dataStore.postPumpConnected(true) + // A successful (re)handshake also invalidates any error state recorded against the + // previous link. errorDescription is tandem-owned; the PumpUtil error latch is + // core-frozen, but its only consumers are latch-tolerant (isConnecting gate) or + // self-recover via the ~5-min history refresh. + if (pumpStatus.errorDescription != null) { + pumpStatus.errorDescription = null + rxBus.send(EventPumpFragmentValuesChanged(PumpUpdateFragmentType.PumpStatus)) + aapsLogger.info(TAG, "Cleared pump error state after successful reconnect") + } + } else if (message is PumpVersionResponse) { dataStore.postPumpVersionResponse(message) } diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnector.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnector.kt index 7beb518b42fc..9c6fcf85c1a5 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnector.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnector.kt @@ -18,6 +18,7 @@ import app.aaps.pump.common.data.PumpTimeDifferenceDto import app.aaps.pump.common.defs.BolusData import app.aaps.pump.common.defs.BolusType import app.aaps.pump.common.defs.PumpConfigurationTypeInterface +import app.aaps.pump.common.defs.PumpDriverState import app.aaps.pump.common.defs.PumpRunningState import app.aaps.pump.common.defs.PumpUpdateFragmentType import app.aaps.pump.common.defs.TempBasalPair @@ -1291,6 +1292,18 @@ class TandemPumpConnector @Inject constructor(var tandemPumpStatus: TandemPumpSt tandemPumpStatus.pumpConnectedFlow.value = true tandemDataStore.postPumpConnected(true) } + + // A successful status read likewise invalidates error state recorded against the + // previous link, and recovers a driverStatus latched at ErrorCommunicatingWithPump + // by a mid-link failure (no reconnect occurred, so TimeSinceReset never fired). + // The PumpUtil errorType latch itself is core-frozen; its consumers are latch-tolerant. + if (tandemPumpStatus.errorDescription != null) { + tandemPumpStatus.errorDescription = null + } + if (tandemPumpUtil.driverStatus == PumpDriverState.ErrorCommunicatingWithPump) { + tandemPumpUtil.resetDriverStatusToConnected() + aapsLogger.error(LTag.PUMP, "getPumpStatus: driverStatus was latched at ErrorCommunicatingWithPump despite a successful status read — resetting to Connected") + } rxBus.send(EventPumpFragmentValuesChanged(PumpUpdateFragmentType.PumpStatus)) return DataCommandResponse( diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillCannulaScreen.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillCannulaScreen.kt index 842eb18c31ac..72dafe4f0d05 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillCannulaScreen.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/actions/cartridge/FillCannulaScreen.kt @@ -45,7 +45,7 @@ import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.ui.compose.AapsSpacing import app.aaps.core.ui.compose.pump.StepProgressIndicator -import app.aaps.pump.common.defs.PumpRunningState + import app.aaps.pump.common.test.ResourceHelperTest import app.aaps.pump.tandem.R import app.aaps.pump.tandem.common.comm.ui.CoreCartridgeActionsModel @@ -134,7 +134,6 @@ fun FillCannulaScreen( sendPumpCommands(listOf(AlertStatusRequest(), AlarmStatusRequest())) } - val pumpRunningState = ds.pumpRunningState.observeAsState() val fillCannulaState = ds.fillCannulaState.observeAsState() val mirrorBasalStatus = ds.mirrorBasalStatus.observeAsState() @@ -213,14 +212,23 @@ fun FillCannulaScreen( isSuspending = true sendPumpCommand(SuspendPumpingRequest()) refreshScope.launch { + // Mirror-confirmed suspend - same ground truth as the resume path + // (basalStatusIcon=SUSPEND), not the ACK-derived pumpRunningState: both + // are mirror-fed today, but the raw icon cannot silently regress to + // ACK-trust if that derivation changes. + var suspendedConfirmed = false repeat(5) { - if (pumpRunningState.value == PumpRunningState.Suspended) { + if (mirrorBasalStatus.value == HomeScreenMirrorResponse.BasalStatusIcon.SUSPEND) { + suspendedConfirmed = true return@repeat } withContext(Dispatchers.IO) { Thread.sleep(1000) } sendPumpCommand(HomeScreenMirrorRequest()) } isSuspending = false + if (!suspendedConfirmed) { + aapsLogger.error(TAG, "FC-NAV: suspend NOT confirmed by mirror within 5s - optimistic UI state stands until next mirror sample") + } } }) { Text(resourceHelper.gs(R.string.ca_btn_suspend_insulin)) } }, From d2357642d7422e5a89566118d1739e0e0c01bd56 Mon Sep 17 00:00:00 2001 From: Haaris Edhi Date: Sun, 6 Sep 2026 08:59:39 -0500 Subject: [PATCH 4/5] latches --- .../comm/TandemPumpCommunicationManager.kt | 12 +++-- .../common/comm/history/HistoryRetriever.kt | 41 ++++++++++++++++ .../common/comm/ui/TandemUICommunication.kt | 14 ++++-- .../tandem/common/driver/TandemPumpStatus.kt | 49 ++++++++++++++++++- .../pump/tandem/mobi/TandemMobiPumpPlugin.kt | 18 ++++++- .../pump/tandem/mobi/ui/TandemUiController.kt | 10 +++- 6 files changed, 132 insertions(+), 12 deletions(-) diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt index f153505fb2ed..765e742b2eca 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/TandemPumpCommunicationManager.kt @@ -74,10 +74,14 @@ class TandemPumpCommunicationManager( var dataStore: TandemUiStateWriter = tandemDataStore var communicationListener : CommunicationListener? = null - set(value) { if (value==null) - operationMode=OperationMode.StandardOperation - else - operationMode=OperationMode.ExternalListenerOperation + set(value) { + // NOTE: `field = value` must run UNCONDITIONALLY. It previously sat inside the else + // branch, so passing null switched operationMode back to StandardOperation but left + // the stale listener attached - responses then routed to a dead listener instance + // (observed 2026-08-31 22:56: a HistoryLogStatusResponse was silently dropped and the + // history download wedged the CommandExecutor for its full timeout). + operationMode = if (value==null) OperationMode.StandardOperation + else OperationMode.ExternalListenerOperation field = value } diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/history/HistoryRetriever.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/history/HistoryRetriever.kt index 9d5d66244ff1..3f054eb5db10 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/history/HistoryRetriever.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/history/HistoryRetriever.kt @@ -139,17 +139,40 @@ class HistoryRetriever @Inject constructor( downloadRunning = true startDataRetrieval() + // Progress watchdog: every message the per-download TandemUICommunication receives counts + // as progress. A stalled stream (RF drop mid-chunk, or a misrouted response that a stale + // listener instance dropped - both observed 2026-08-31/09-01) previously wedged this loop, + // and with it the whole CommandExecutor, for the full 60-min timeout. Un-wedge after + // WATCHDOG_TIMEOUT_MS of no messages instead. + var lastMessageCount = -1 + var lastProgressMs = System.currentTimeMillis() + while (downloadRunning) { aapsLogger.debug("${historyPrefix}download running") pumpUtil.sleepSeconds(5) + if (communication.messageCount != lastMessageCount) { + lastMessageCount = communication.messageCount + lastProgressMs = System.currentTimeMillis() + } else if (System.currentTimeMillis() - lastProgressMs > WATCHDOG_TIMEOUT_MS) { + aapsLogger.error(TAG, "${historyPrefix}No messages received for ${WATCHDOG_TIMEOUT_MS / 1000}s (messages=$lastMessageCount) - aborting history download.") + downloadRunning = false + this.communication.tandemPumpCommunicationManager = null + currentRequest = null + setSemaphore() + endProgress() + return false + } + if (timeoutTime < System.currentTimeMillis()) { if (communication.messageCount==0) { aapsLogger.error(TAG, "[History] Timeout reached while trying to read history, with no messages read.") + this.communication.tandemPumpCommunicationManager = null return false } else { aapsLogger.error(TAG, "[History] Timeout reached while trying to read history, with ${communication.messageCount} messages read.") downloadRunning = false + this.communication.tandemPumpCommunicationManager = null return false } } @@ -203,9 +226,27 @@ class HistoryRetriever @Inject constructor( downloadRunning = true startDataRetrieval() + // Same progress watchdog as downloadHistory(): this loop previously had NO timeout at + // all, so a stalled silent download wedged the CommandExecutor indefinitely. + var lastMessageCount = -1 + var lastProgressMs = System.currentTimeMillis() + while(downloadRunning) { aapsLogger.debug("${historyPrefix}download running") pumpUtil.sleepSeconds(5) + + if (communication.messageCount != lastMessageCount) { + lastMessageCount = communication.messageCount + lastProgressMs = System.currentTimeMillis() + } else if (System.currentTimeMillis() - lastProgressMs > WATCHDOG_TIMEOUT_MS) { + aapsLogger.error(TAG, "${historyPrefix}Silent download: no messages received for ${WATCHDOG_TIMEOUT_MS / 1000}s (messages=$lastMessageCount) - aborting.") + downloadRunning = false + this.communication.tandemPumpCommunicationManager = null + currentRequest = null + setSemaphore() + endProgress() + return mutableListOf() + } } this.communication.tandemPumpCommunicationManager = null diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUICommunication.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUICommunication.kt index a29078312e03..60fb00f06665 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUICommunication.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/ui/TandemUICommunication.kt @@ -86,9 +86,11 @@ class TandemUICommunication @Inject constructor ( return false } - if (!tandemPumpCommunicationManager!!.isListenerEnabled()) { - tandemPumpCommunicationManager!!.communicationListener = this - } + // Always (re)bind: the listener from a previous download cycle (or a UI screen) may + // still be attached, and stale instances drop history responses (receivedLogStatus etc. + // never invoked). Rebinding is idempotent and guarantees the active instance routes + // this download's responses into the shared HistoryRetriever. + tandemPumpCommunicationManager!!.communicationListener = this if (!tandemPumpCommunicationManager!!.isPumpFullyConnected()) { aapsLogger.warn(TAG, "Command ${request::javaClass.name} couldn't be executed because pump is not fully connected yet.") @@ -229,16 +231,22 @@ class TandemUICommunication @Inject constructor ( if (::historyRetriever.isInitialized) { historyRetriever.receivedStatus(message) + } else { + aapsLogger.error(TAG, "HistoryLogStatusResponse dropped: historyRetriever not initialized on this TandemUICommunication instance") } } is HistoryLogResponse -> { if (::historyRetriever.isInitialized) { historyRetriever.receivedLogResponse(message) + } else { + aapsLogger.error(TAG, "HistoryLogResponse dropped: historyRetriever not initialized on this TandemUICommunication instance") } } is HistoryLogStreamResponse -> { if (::historyRetriever.isInitialized) { historyRetriever.receivedLogStreamResponse(message) + } else { + aapsLogger.error(TAG, "HistoryLogStreamResponse dropped: historyRetriever not initialized on this TandemUICommunication instance") } } is ResumePumpingResponse -> { diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/TandemPumpStatus.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/TandemPumpStatus.kt index a71a24293a96..0ee7926deed3 100755 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/TandemPumpStatus.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/TandemPumpStatus.kt @@ -3,6 +3,8 @@ package app.aaps.pump.tandem.common.driver import androidx.compose.runtime.compositionLocalOf import app.aaps.core.data.pump.defs.PumpDescription import app.aaps.core.data.pump.defs.PumpType +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.profile.Profile import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.pump.tandem.common.data.defs.TandemPumpApiVersion @@ -45,9 +47,21 @@ var LocalTandemDataStore = compositionLocalOf { tandemUiDataStore } @Singleton class TandemPumpStatus @Inject constructor(val sp: SP, val rxBus: RxBus, - val preferences: Preferences + val preferences: Preferences, + private val aapsLogger: AAPSLogger ) : PumpStatus(PumpType.TANDEM_MOBI_BT) { + companion object { + + // Safety backstop: the UI workflow latch (preventQueueExecution / preventConnect) is + // released on navigation-leave, tab dispose and lifecycle events. If every release path + // is missed, this latch locked the Loop out overnight (2026-09-06: ~5.4h, no SMBs, + // PumpActivityFab stuck on, scheduled status refresh suppressed) because the latch + // carried no timestamp and its release is wired only to UI navigation/lifecycle events. + // Auto-release latches older than this TTL. + private const val UI_WORKFLOW_LATCH_TTL_MS = 30 * 60_000L + } + lateinit var pumpDescription: PumpDescription var errorDescription: String? = null @@ -95,7 +109,40 @@ class TandemPumpStatus @Inject constructor(val sp: SP, var bolusStep: Double = 0.1 // ?? + // UI workflow latch: latched by TandemUiController.refreshMainAppData(START_ACTIONS/START_DATA) + // when the Actions/Data screens start, released on navigation-leave / tab dispose / + // lifecycle events. preventQueueExecutionSinceMs records when it was latched so a missed + // release can be detected (and bounded by isQueueExecutionPrevented()). + // NOTE: preventConnect is deliberately NOT part of this latch — it belongs to the + // cartridge-change workflow only (setCartridgeChangeMode), per the b14f1abb52 narrowing. + var preventQueueExecutionSinceMs = 0L + private set + var preventQueueExecution = false + set(value) { + if (value && !field) preventQueueExecutionSinceMs = System.currentTimeMillis() + if (!value) preventQueueExecutionSinceMs = 0L + field = value + } + + /** + * Latch state with TTL backstop. Returns true while the UI workflow latch is active; a latch + * older than [UI_WORKFLOW_LATCH_TTL_MS] is auto-released so a missed unlatch cannot lock the + * Loop out indefinitely. The caller (TandemMobiPumpPlugin.isBusy) clears any latched + * preventConnect it observes alongside a stale release. + */ + fun isQueueExecutionPrevented(): Boolean { + if (!preventQueueExecution) return false + val sinceMs = preventQueueExecutionSinceMs + if (sinceMs > 0L && System.currentTimeMillis() - sinceMs > UI_WORKFLOW_LATCH_TTL_MS) { + aapsLogger.error( + LTag.PUMP, + "UI workflow latch stale (${(System.currentTimeMillis() - sinceMs) / 60000} min) - auto-releasing preventQueueExecution (paired preventConnect must also be cleared by caller)" + ) + preventQueueExecution = false + } + return preventQueueExecution + } // Tandem specific var pumpStatusMirror: HomeScreenMirrorDto? = null diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/TandemMobiPumpPlugin.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/TandemMobiPumpPlugin.kt index 262b3235eb44..8004f07d4cfe 100755 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/TandemMobiPumpPlugin.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/TandemMobiPumpPlugin.kt @@ -743,11 +743,25 @@ class TandemMobiPumpPlugin @Inject constructor( // - tandemDispatcher.isBusy(): an op is queued or in flight on PumpOpQueue's single dispatcher. // - tandemPumpUtil.preventConnect: cartridge-change workflow is in progress and owns the // pump comm channel. AAPS auto-reconnect must stand down for its duration. - // Browsing Actions / Data does NOT set preventConnect — those sends are serialized by + // Browsing Actions / Data does NOT set preventConnect (restored b14f1abb52 narrowing: + // preventConnect belongs to the cartridge-change workflow only, via + // setCartridgeChangeMode) — those sends are serialized by // the queue at USER_INITIATED priority and AAPS Loop can safely interleave. // PumpAvailability is intentionally NOT folded in here — mutating ops handle availability // via fast-fail at dispatch, not by stalling AAPS's command queue on isBusy(). - val isBusy = tandemDispatcher.isBusy() || tandemPumpUtil.preventConnect || pumpStatus.preventQueueExecution + // UI-workflow latch watchdog: START_ACTIONS/START_DATA latch preventQueueExecution; + // releases happen on navigation-leave, tab dispose and lifecycle events. If every release + // path is missed, the latch locks the Loop out indefinitely (2026-09-06: ~5.4h overnight, + // no SMBs, PumpActivityFab stuck on) - isQueueExecutionPrevented() auto-releases stale + // latches here. A concurrently latched preventConnect (cartridge-change mode) is cleared + // with it: a >TTL-old latch means every release path was already missed. + val queuePreventedBefore = pumpStatus.preventQueueExecution + val queuePrevented = pumpStatus.isQueueExecutionPrevented() + if (queuePreventedBefore && !queuePrevented) { + aapsLogger.error(LTag.PUMP, "Clearing paired preventConnect after stale UI workflow latch auto-release") + tandemPumpUtil.preventConnect = false + } + val isBusy = tandemDispatcher.isBusy() || tandemPumpUtil.preventConnect || queuePrevented if (displayConnectionMessages) aapsLogger.debug(LTag.PUMP, "isBusy: $isBusy") return isBusy } diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/TandemUiController.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/TandemUiController.kt index 3b593b3c4bfc..a89250394e10 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/TandemUiController.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/TandemUiController.kt @@ -185,12 +185,18 @@ class TandemUiController @Inject constructor( } } RefreshData.START_ACTIONS -> { - tandemPumpUtil.preventConnect = true + aapsLogger.info(TAG, "UI workflow latch: preventQueueExecution = true (Actions screen started)") + // NOTE: preventConnect is intentionally NOT latched here. It belongs to the + // cartridge-change workflow only (setCartridgeChangeMode) per b14f1abb52 — + // latching it on screen entry re-introduces the overnight lockout: the + // MobiComposeContent lifecycle reset clears preventQueueExecution but NOT + // preventConnect, leaving isBusy() stuck true (2026-09-06, ~5.4h, no SMBs). tandemPumpStatus.preventQueueExecution = true ds.reminderDateTime.value = tandemPumpStatus.tandemSiteReminder } RefreshData.START_DATA -> { - tandemPumpUtil.preventConnect = true + aapsLogger.info(TAG, "UI workflow latch: preventQueueExecution = true (Data screen started)") + // See START_ACTIONS: preventConnect must not be latched on screen entry. tandemPumpStatus.preventQueueExecution = true } } From aebfad2bc77617588d82664019b6a9e828f3c9ec Mon Sep 17 00:00:00 2001 From: Haaris Edhi Date: Fri, 11 Sep 2026 09:13:56 -0500 Subject: [PATCH 5/5] restore build files --- build.gradle.kts | 1 - gradle/libs.versions.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 47a7560d7b1c..8c22c368524f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,6 @@ val kotlinMetadataVersion = libs.versions.kotlin.get() allprojects { repositories { - mavenLocal() google() mavenCentral() maven("https://jitpack.io") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e27652f6d7b1..3237e4620024 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -196,7 +196,7 @@ androidx-glance-appwidget = { group = "androidx.glance", name = "glance-appwidge com-jakewharton-timber = { group = "com.jakewharton.timber", name = "timber", version="5.0.1" } com-github-weliem-blessed-android = { group = "com.github.weliem", name = "blessed-android", version="2.4.0" } io-github-vanpra-compose-dialogs-datetime = { group = "io.github.vanpra.compose-material-dialogs", name = "datetime", version="0.9.0" } -com-github-jwoglom-pumpx2-android = { group = "com.github.jwoglom.pumpX2", name = "pumpx2-android", version="v1.9.0-tandem-2" } +com-github-jwoglom-pumpx2-android = { group = "com.github.jwoglom.pumpX2", name = "pumpx2-android", version="v1.9.1" } androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }