diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f90bc20f5740..5acad5ee1ba5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -253,7 +253,7 @@ jetbrains-compose-material-icons-extended = { module = "org.jetbrains.compose.ma 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.2" } +com-github-jwoglom-pumpx2-android = { group = "com.github.jwoglom.pumpX2", name = "pumpx2-android", version="v1.9.3" } 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 d5d89fc507cb..2c5f79d4389c 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,16 +62,26 @@ 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() } 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 } @@ -94,6 +104,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 +117,13 @@ class TandemPumpCommunicationManager( } connected = false + val wasPlannedDisconnect = plannedDisconnect + plannedDisconnect = false + aapsLogger.info( + TAG, + "connect(): plannedDisconnect $wasPlannedDisconnect -> false, preventReconnect=${pumpUtil.preventReconnect}, " + + "preventQueueExecution=${pumpStatus.preventQueueExecution}" + ) val connectStartTime = System.currentTimeMillis() operationMode = OperationMode.ConnectionMode bluetoothHandler!!.startScan() @@ -130,6 +148,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 +182,11 @@ 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 + aapsLogger.info(TAG, "disconnect(): plannedDisconnect=true (preventReconnect=${pumpUtil.preventReconnect} before teardown)") + // 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 @@ -167,6 +199,11 @@ class TandemPumpCommunicationManager( publishDisconnectedState() + aapsLogger.info( + TAG, + "disconnect(): teardown complete, connected=$connected, preventReconnect=${pumpUtil.preventReconnect}, " + + "plannedDisconnect=$plannedDisconnect" + ) return connected } @@ -383,6 +420,41 @@ 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) + val preventReconnectBefore = pumpUtil.preventReconnect + aapsLogger.info( + TAG, + "Handshake complete: pumpConnectedFlow=true [preventReconnect=$preventReconnectBefore, " + + "preventQueueExecution=${pumpStatus.preventQueueExecution}, errorType=${pumpUtil.errorType}]" + ) + // tryToReconnectToPump() tears the link down through forceResetBluetoothHandler(), which + // sets preventReconnect=true. Once the handshake succeeds the link is demonstrably back, + // so that "do not reconnect" latch must not outlive it - otherwise isBusy() and + // isInPreventConnectMode() stay true while connected, the queue logs "busy" forever and + // the scheduled status refresh is suppressed (observed 2026-10-26 16:43:50). + if (preventReconnectBefore) { + pumpUtil.preventReconnect = false + aapsLogger.info(TAG, "Handshake complete: cleared preventReconnect latch (link is up again)") + } + + // A successful (re)handshake also invalidates any error state recorded against the + // previous link, so a latched error cannot outlive the connection it described. + if (pumpUtil.errorType != null || pumpUtil.errorDetails.isNotEmpty()) { + val latchedType = pumpUtil.errorType + val clearedDetails = pumpUtil.errorDetails + pumpUtil.clearErrorDetails() + rxBus.send(EventPumpFragmentValuesChanged(PumpUpdateFragmentType.PumpStatus)) + aapsLogger.info( + TAG, + "Cleared error details after successful reconnect (core errorType latch left as-is: $latchedType) details='${clearedDetails.take(100)}'" + ) + } + } else if (message is PumpVersionResponse) { dataStore.postPumpVersionResponse(message) } @@ -488,20 +560,35 @@ 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 + aapsLogger.info(TAG, "onPumpDisconnected: plannedDisconnect=$plannedDisconnect -> pumpX2 reconnect=$shouldReconnect") 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) { aapsLogger.error(TAG, "forceDisconnect: onDisconnect=${onDisconnect}" + - ", hciStatus=${hciStatus}, tandemError=${tandemError}") + ", hciStatus=${hciStatus}, tandemError=${tandemError}, plannedDisconnect=$plannedDisconnect") this.pumpStatus.disconnectData = DisconnectDataDto(onDisconnect = onDisconnect, 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)) - 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/history/HistoryRetriever.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/history/HistoryRetriever.kt index fbac72ca0e5b..8e85a0f0ef8a 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,51 @@ class HistoryRetriever( 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() + var firstMessageAtMs = -1L + var loopCount = 0 + aapsLogger.info(TAG, "${historyPrefix}history watchdog: ${WATCHDOG_TIMEOUT_MS / 1000}s without messages aborts, overall timeout ${(timeoutTime - startTime) / 60_000}min") + while (downloadRunning) { aapsLogger.debug("${historyPrefix}download running") pumpUtil.sleepSeconds(5) + loopCount++ + + if (communication.messageCount != lastMessageCount) { + if (firstMessageAtMs < 0L) { + firstMessageAtMs = System.currentTimeMillis() + aapsLogger.info(TAG, "${historyPrefix}first message after ${firstMessageAtMs - startTime}ms (count=${communication.messageCount})") + } + lastMessageCount = communication.messageCount + lastProgressMs = System.currentTimeMillis() + if (loopCount % 12 == 0) { + aapsLogger.info(TAG, "${historyPrefix}progress: messages=$lastMessageCount, elapsed=${System.currentTimeMillis() - startTime}ms") + } + } 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 +237,38 @@ class HistoryRetriever( 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() + var firstMessageAtMs = -1L + var loopCount = 0 + aapsLogger.info(TAG, "${historyPrefix}silent download watchdog: ${WATCHDOG_TIMEOUT_MS / 1000}s without messages aborts") + while(downloadRunning) { aapsLogger.debug("${historyPrefix}download running") pumpUtil.sleepSeconds(5) + loopCount++ + + if (communication.messageCount != lastMessageCount) { + if (firstMessageAtMs < 0L) { + firstMessageAtMs = System.currentTimeMillis() + aapsLogger.info(TAG, "${historyPrefix}silent download first message after ${firstMessageAtMs - startTime}ms (count=${communication.messageCount})") + } + lastMessageCount = communication.messageCount + lastProgressMs = System.currentTimeMillis() + if (loopCount % 12 == 0) { + aapsLogger.info(TAG, "${historyPrefix}silent download progress: messages=$lastMessageCount, elapsed=${System.currentTimeMillis() - startTime}ms") + } + } 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/maint/TandemPairingManager.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/maint/TandemPairingManager.kt index c76357eb094b..7353cd127a1e 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/maint/TandemPairingManager.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/comm/maint/TandemPairingManager.kt @@ -138,8 +138,17 @@ class TandemPairingManager constructor( fun stopBluetoothHandler() { aapsLogger.info(TAG, "TANDEM-PAIR-DBG: stopBluetoothHandler pairing") - tandemPumpUtil.forceResetBluetoothHandler(bluetoothHandler) + val handler = bluetoothHandler + if (handler == null) { + // Nothing owned by the pairing manager. Calling forceReset here would still set + // preventReconnect=true and reset the pumpx2 singleton - and it ran AFTER the wizard's + // onFinish cleared that flag, so the flag stayed latched until an app restart. The main + // TandemPumpCommunicationManager may also own a handler by now; do not touch it. + aapsLogger.info(TAG, "TANDEM-PAIR-DBG: stopBluetoothHandler: no handler owned by pairing manager - nothing to tear down") + return + } bluetoothHandler = null + tandemPumpUtil.forceResetBluetoothHandler(handler) } // Pair Status 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 c89d79f99059..e9a7981c33ad 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 @@ -87,8 +87,14 @@ class TandemUICommunication( 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. + val previousListener = tandemPumpCommunicationManager!!.communicationListener + tandemPumpCommunicationManager!!.communicationListener = this + if (previousListener != null && previousListener !== this) { + aapsLogger.info(TAG, "Rebound communicationListener: previous=${previousListener.javaClass.simpleName} -> new instance (previous listener was still attached)") } if (!tandemPumpCommunicationManager!!.isPumpFullyConnected()) { @@ -140,6 +146,10 @@ class TandemUICommunication( 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) } @@ -226,16 +236,22 @@ class TandemUICommunication( 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/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/TandemPumpStatus.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/TandemPumpStatus.kt index e4d04c0427e7..34a7eb15f10f 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 @@ -48,9 +50,21 @@ var LocalTandemDataStore = compositionLocalOf { tandemUiDataStore } @Inject class TandemPumpStatus(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 // tandem pump firmware @@ -97,7 +111,43 @@ class TandemPumpStatus(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) { + aapsLogger.info(LTag.PUMP, "preventQueueExecution -> $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/common/driver/connector/TandemPumpConnectionManager.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/driver/connector/TandemPumpConnectionManager.kt index c2454bc39f06..0b0ca2c84b20 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.TempBasalPair import app.aaps.pump.common.driver.connector.commands.data.CustomCommandTypeInterface import app.aaps.pump.tandem.common.driver.connector.def.TandemCustomCommand @@ -51,10 +52,29 @@ class TandemPumpConnectionManager( 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()) { + aapsLogger.info( + LTag.PUMPCOMM, + "connectToPump: already connected [preventReconnect=${tandemPumpUtil.preventReconnect}, " + + "preventQueueExecution=${tandemPumpStatus.preventQueueExecution}, " + + "pumpConnectedFlow=${tandemPumpStatus.pumpConnectedFlow.value}, driverStatus=${pumpUtil.driverStatus}]" + ) + // 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 b76286e5c320..0985bc0aaa24 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 @@ -234,6 +235,11 @@ class TandemPumpConnector(var tandemPumpStatus: TandemPumpStatus, aapsLogger.info(TAG, "disconnectFromPump") val c = getCommunicationManager() + aapsLogger.info( + TAG, + "disconnectFromPump: start [manager=${c != null}, driverStatus=${tandemPumpUtil.driverStatus}, " + + "preventReconnect=${tandemPumpUtil.preventReconnect}]" + ) if (c == null) { aapsLogger.warn(TAG, "disconnectFromPump: nothing to do, communication manager already destroyed") return false @@ -243,6 +249,10 @@ class TandemPumpConnector(var tandemPumpStatus: TandemPumpStatus, // Without this, a subsequent wizard-driven re-pair reuses the torn-down instance. tandemPumpCommunicationManager = null btAddressUsed = null + aapsLogger.info( + TAG, + "disconnectFromPump: done, result=$result, preventReconnect=${tandemPumpUtil.preventReconnect}" + ) return result } @@ -1275,6 +1285,35 @@ class TandemPumpConnector(var tandemPumpStatus: TandemPumpStatus, 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) + } + + // 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 (tandemPumpUtil.errorType != null || tandemPumpUtil.errorDetails.isNotEmpty()) { + val latchedType = tandemPumpUtil.errorType + val clearedDetails = tandemPumpUtil.errorDetails + tandemPumpUtil.clearErrorDetails() + aapsLogger.warn( + LTag.PUMP, + "getPumpStatus success: cleared error details='${clearedDetails.take(100)}' " + + "(core errorType latch left as-is: $latchedType)" + ) + } + 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/common/service/TandemService.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/service/TandemService.kt index 477218f5a060..6b22dfef33cc 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/service/TandemService.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/service/TandemService.kt @@ -57,6 +57,7 @@ class TandemService : MetroService() { fun isMobiConfigurationStillValid(config: PumpConfigurationDto): Boolean { if (tandemPumpUtil.preventReconnect) { + aapsLogger.warn(LTag.PUMPCOMM, "isMobiConfigurationStillValid: false because preventReconnect=true (set by a previous teardown and not yet cleared)") return false } @@ -229,6 +230,9 @@ class TandemService : MetroService() { storedBondStatus != 100 || tandemPumpUtil.preventReconnect ) { + if (tandemPumpUtil.preventReconnect) { + aapsLogger.warn(LTag.PUMPCOMM, "prepareConfiguration: rejecting configuration because preventReconnect=true") + } configurationValid = false tandemPumpUtil.setError(errorType = PumpErrorType.ConfiguredPumpNotFound, @@ -240,7 +244,7 @@ class TandemService : MetroService() { pumpAddress = storedAddress configurationValid = true - tandemPumpUtil.clearError() + tandemPumpUtil.clearErrorDetails() // Record the configuration actually applied to AAPS. currentConfiguration = PumpConfigurationDto( @@ -311,10 +315,17 @@ class TandemService : MetroService() { fun disconnectFromPump() { - if (this.tandemPumpConnectionManager.isConnected()) { - val status = this.tandemPumpConnectionManager.disconnectFromPump() - aapsLogger.info(LTag.PUMP, "SRV: disconnectFromPump: connected=$status") - } + // Always route to the connector. Gating this on isConnected() made a force-disconnect a + // no-op while a connect attempt was running (driverStatus=Connecting), so the in-flight + // connect kept the pumpx2 handler singleton and the pairing manager could never own the + // BLE link - the wizard then failed with "already connecting" and 30s pairing timeouts. + aapsLogger.info( + LTag.PUMP, + "SRV: disconnectFromPump requested [isConnected=${this.tandemPumpConnectionManager.isConnected()}, " + + "driverStatus=${tandemPumpUtil.driverStatus}]" + ) + val status = this.tandemPumpConnectionManager.disconnectFromPump() + aapsLogger.info(LTag.PUMP, "SRV: disconnectFromPump: connected=$status") } diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/util/TandemPumpUtil.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/util/TandemPumpUtil.kt index 85ecf611e476..1a0115ecd96b 100755 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/util/TandemPumpUtil.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/common/util/TandemPumpUtil.kt @@ -148,7 +148,9 @@ class TandemPumpUtil( * field — it does not tear down the live BLE state. */ fun forceResetBluetoothHandler(handler: TandemBluetoothHandler?) { + val wasPreventReconnect = preventReconnect preventReconnect = true + aapsLogger.info(LTag.PUMPBTCOMM, "forceResetBluetoothHandler: preventReconnect $wasPreventReconnect -> true (handler=${handler != null})") try { handler?.let { h -> // 1. Neutralize the handler's internal android.os.Handler BEFORE cancelling @@ -244,6 +246,22 @@ class TandemPumpUtil( } + /** + * Mobi-only replacement for [PumpUtil.clearError]. + * + * The shared `clearError()` sets `errorType = null`, and that setter does `pumpErrorType!!`, + * so it throws NPE whenever an error is latched (it broke the reconnect the wizard triggers + * after pairing). `pump/common` is out of scope here, so clear only the details and leave the + * core `errorType` latch in place. Its only Tandem consumer is + * `TandemMobiPumpPlugin.isConnecting()`, which already ignores a latched PumpUnreachable. + */ + fun clearErrorDetails() { + if (errorDetails.isNotEmpty()) { + errorDetails = "" + } + } + + fun clearAllPairingData() { aapsLogger.info(LTag.PUMPCOMM, "TandemPumpUtil: Clearing all pairing data for re-pairing") @@ -267,7 +285,7 @@ class TandemPumpUtil( // Reset pump status tandemPumpStatus.serialNumber = 0L //tandemPumpStatus.errorDescription = "" - clearError() + clearErrorDetails() aapsLogger.info(LTag.PUMPCOMM, "TandemPumpUtil: All pairing data cleared successfully") } 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 4c9d7a3df208..8c982e308c62 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 @@ -622,20 +622,16 @@ class TandemMobiPumpPlugin( //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) { @@ -690,8 +686,29 @@ class TandemMobiPumpPlugin( return true } + private var lastBusyLogged: Boolean? = null + private var lastBusyLogMs = 0L + override fun isBusy(): Boolean { - val isBusy = tandemPumpUtil.preventReconnect || 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) - isQueueExecutionPrevented() auto-releases stale latches here. + // preventConnect no longer exists in dev_4; preventReconnect is the remaining guard. + val queuePrevented = pumpStatus.isQueueExecutionPrevented() + val isBusy = tandemPumpUtil.preventReconnect || queuePrevented + val now = System.currentTimeMillis() + if (lastBusyLogged != isBusy || now - lastBusyLogMs > 60_000L) { + val latchAgeMs = if (pumpStatus.preventQueueExecution) now - pumpStatus.preventQueueExecutionSinceMs else -1L + aapsLogger.info( + LTag.PUMP, + "isBusy=$isBusy [preventReconnect=${tandemPumpUtil.preventReconnect}, " + + "preventQueueExecution=${pumpStatus.preventQueueExecution}, latchAgeMs=$latchAgeMs, " + + "driverStatus=${tandemPumpUtil.driverStatus}]" + ) + lastBusyLogged = isBusy + lastBusyLogMs = now + } if (displayConnectionMessages) aapsLogger.debug(LTag.PUMP, "isBusy: $isBusy") return isBusy } @@ -1624,7 +1641,18 @@ class TandemMobiPumpPlugin( override fun isInPreventConnectMode(): Boolean { - return tandemPumpUtil.preventReconnect || pumpStatus.preventQueueExecution + val prevented = tandemPumpUtil.preventReconnect || pumpStatus.preventQueueExecution + if (prevented) { + val now = System.currentTimeMillis() + val latchAgeMs = if (pumpStatus.preventQueueExecution) now - pumpStatus.preventQueueExecutionSinceMs else -1L + aapsLogger.warn( + LTag.PUMP, + "isInPreventConnectMode=true [preventReconnect=${tandemPumpUtil.preventReconnect}, " + + "preventQueueExecution=${pumpStatus.preventQueueExecution}, latchAgeMs=$latchAgeMs] " + + "- scheduled status refresh suppressed" + ) + } + return prevented } 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 c2ac798b596e..a7f2a51354b3 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 @@ -69,6 +69,10 @@ class TandemUiController( } fun disposeTandemUiCommunication(disposeType: AdditionalConfigurationScreens) { + aapsLogger.info( + LTag.PUMP, + "disposeTandemUiCommunication($disposeType): preventQueueExecution before=${tandemPumpStatus.preventQueueExecution}" + ) if (disposeType==AdditionalConfigurationScreens.Actions) { aapsLogger.info(LTag.PUMP, "Actions window was closed. Sending event to refresh.") @@ -99,6 +103,7 @@ class TandemUiController( this.tandemUICommunication.tandemPumpCommunicationManager = null //aapsLogger.error(LTag.PUMP, "QQ Prevent Queue Execution reset on Mobi (exiting Actions/Data)") tandemPumpStatus.preventQueueExecution = false + aapsLogger.info(LTag.PUMP, "disposeTandemUiCommunication($disposeType): done, preventQueueExecution=false") } enum class AdditionalConfigurationScreens { @@ -181,10 +186,12 @@ class TandemUiController( } } RefreshData.START_ACTIONS -> { + aapsLogger.info(TAG, "UI workflow latch: preventQueueExecution = true (Actions screen started)") tandemPumpStatus.preventQueueExecution = true ds.reminderDateTime.value = tandemPumpStatus.tandemSiteReminder } RefreshData.START_DATA -> { + aapsLogger.info(TAG, "UI workflow latch: preventQueueExecution = true (Data screen started)") tandemPumpStatus.preventQueueExecution = true } } 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 b5ba5e5143c2..eed22386d69c 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 @@ -248,12 +248,49 @@ class MobiComposeContent( MobiOverviewEvent.OpenNotification -> currentScreen = MobiScreen.DATA_NOTIFICATIONS MobiOverviewEvent.StartActions -> currentScreen = MobiScreen.ACTIONS MobiOverviewEvent.StartData -> currentScreen = MobiScreen.DATA - MobiOverviewEvent.StartPairing -> currentScreen = MobiScreen.PAIR_WIZARD + MobiOverviewEvent.StartPairing -> { + aapsLogger.info(LTag.PUMP, "Received StartPairing event -> switching to PAIR_WIZARD (was $currentScreen)") + currentScreen = MobiScreen.PAIR_WIZARD + } } } } + // 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 " + + "(leftActions=$leftActions, leftData=$leftData, preventQueueExecution=${tandemPumpStatus.preventQueueExecution})" + ) + tandemUiController.disposeTandemUiCommunication( + TandemUiController.AdditionalConfigurationScreens.Data.takeIf { leftData } + ?: TandemUiController.AdditionalConfigurationScreens.Actions + ) + } + lastScreen = currentScreen + } + val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { val observer = LifecycleEventObserver { _, _ -> @@ -465,12 +502,17 @@ class MobiComposeContent( } MobiScreen.PAIR_WIZARD -> { + aapsLogger.info(LTag.PUMP, "PAIR_WIZARD screen shown: starting pairing workflow") tandemPumpStatus.lastPairingStatus = TandemPairingStatus.Unknown tandemPumpStatus.preventQueueExecution = true KeepScreenOnEffect() TandemPairWizardHost( blePreCheck = blePreCheck, onFinish = { + aapsLogger.info( + LTag.PUMP, + "Pairing finish: clearing preventReconnect=${tandemPumpUtil.preventReconnect} -> false, preventQueueExecution -> false" + ) tandemPumpStatus.preventQueueExecution = false tandemPumpUtil.preventReconnect = false if (tandemPumpStatus.lastPairingStatus==TandemPairingStatus.PairingSuccess) { diff --git a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/overview/MobiOverviewViewModel.kt b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/overview/MobiOverviewViewModel.kt index 4d18e57a0e17..603a1396da10 100644 --- a/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/overview/MobiOverviewViewModel.kt +++ b/pump/tandem/src/main/kotlin/app/aaps/pump/tandem/mobi/ui/overview/MobiOverviewViewModel.kt @@ -115,6 +115,7 @@ class MobiOverviewViewModel( get() = pairButtonEnabledFlow.value set(value) { if (pairButtonEnabledFlow.value != value) { + aapsLogger.info(LTag.PUMP, "pairButtonEnabled -> $value") pairButtonEnabledFlow.value = value } } @@ -236,14 +237,22 @@ class MobiOverviewViewModel( setPairButtonStatus() }.launchIn(ioScope) + // The observers above only run when a preference CHANGES. Without this call the Pair + // Pump button stays hidden (initial value false) for the whole session after a start. + setPairButtonStatus() } private fun setPairButtonStatus() { + aapsLogger.info( + LTag.PUMP, + "setPairButtonStatus: useSharedConnection=$useSharedConnection, hidePumpPairButton=$hidePumpPairButton" + ) if (useSharedConnection) { pairButtonEnabled = false } else { pairButtonEnabled = !hidePumpPairButton } + aapsLogger.info(LTag.PUMP, "setPairButtonStatus: pairButtonEnabled=$pairButtonEnabled") } @@ -265,7 +274,9 @@ class MobiOverviewViewModel( } fun onPairingClick() { - _events.tryEmit(MobiOverviewEvent.StartPairing) + aapsLogger.info(LTag.PUMP, "onPairingClick: emitting StartPairing (pairButtonEnabled=$pairButtonEnabled)") + val emitted = _events.tryEmit(MobiOverviewEvent.StartPairing) + aapsLogger.info(LTag.PUMP, "onPairingClick: StartPairing emitted=$emitted (buffer=${_events.subscriptionCount.value} subscribers)") } @@ -387,7 +398,12 @@ class MobiOverviewViewModel( primaryActions = buildPrimaryActions(pumpRunningState = pumpRunningState, buttonsEnabledLocal = false, statusBanner = communicationStatus.statusBanner()), - //managementActions = managementActions + // The pair button must be reachable exactly when the pump state is Unknown - + // that is the disconnected / not-yet-paired case this branch exists for. + managementActions = buildManagementActions( + statusBanner = communicationStatus.statusBanner(), + pairButtonEnabledLocal = pairButtonEnabledLocal + ) ) } // else { @@ -620,8 +636,21 @@ class MobiOverviewViewModel( // } } + private var lastPairButtonLogged: Boolean? = null + private fun buildManagementActions(statusBanner: StatusBanner?, pairButtonEnabledLocal: Boolean): List { + if (lastPairButtonLogged != pairButtonEnabledLocal) { + val buttonText = if (pairButtonEnabledLocal) "showing Pair Pump button" else "Pair Pump button HIDDEN" + aapsLogger.info( + LTag.PUMP, + "Overview management actions: pairButtonEnabled=$pairButtonEnabledLocal " + + "(useSharedConnection=$useSharedConnection, hidePumpPairButton=$hidePumpPairButton, " + + "statusBanner=${statusBanner != null}) -> $buttonText" + ) + lastPairButtonLogged = pairButtonEnabledLocal + } + if (managementActionsEnabled==null || managementActionsEnabled.isEmpty()) { managementActionsEnabled = listOf( ScreenAction(