From 19d13e18e57d8d91a25b3c45b73f6141187784ad Mon Sep 17 00:00:00 2001 From: Kot Date: Wed, 3 Jun 2026 10:30:42 +0800 Subject: [PATCH 01/23] using VirtualPumpPlugin, pumpType = CELLNOVO, extendedBolus there is a bug in the initial display. --- .../kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt | 3 ++- .../core/interfaces/pump/defs/PumpDescriptionExtension.kt | 1 + ui/src/main/kotlin/app/aaps/ui/dialogs/ExtendedBolusDialog.kt | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt index 94c2ba0ce922..e2d8701bbf1a 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt @@ -34,7 +34,7 @@ class PumpDescription { var isPatchPump = false var maxResorvoirReading = 50 var useHardwareLink = false - + var extendedBolusMinAmount = 0.0 fun resetSettings() { isBolusCapable = true bolusStep = 0.1 @@ -64,6 +64,7 @@ class PumpDescription { needsManualTDDLoad = true hasCustomUnreachableAlertCheck = false useHardwareLink = false + extendedBolusMinAmount = extendedBolusStep } companion object { diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpDescriptionExtension.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpDescriptionExtension.kt index 9ef7c6eb8e2c..3344beb84091 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpDescriptionExtension.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpDescriptionExtension.kt @@ -41,6 +41,7 @@ fun PumpDescription.fillFor(pumpType: PumpType): PumpDescription { isPatchPump = pumpType.isPatchPump() maxResorvoirReading = pumpType.maxReservoirReading() useHardwareLink = pumpType.useHardwareLink + pumpType.extendedBolusSettings()?.minDose?.let { extendedBolusMinAmount = it } return this } \ No newline at end of file diff --git a/ui/src/main/kotlin/app/aaps/ui/dialogs/ExtendedBolusDialog.kt b/ui/src/main/kotlin/app/aaps/ui/dialogs/ExtendedBolusDialog.kt index b710b5355786..ed4057dda91d 100644 --- a/ui/src/main/kotlin/app/aaps/ui/dialogs/ExtendedBolusDialog.kt +++ b/ui/src/main/kotlin/app/aaps/ui/dialogs/ExtendedBolusDialog.kt @@ -30,6 +30,7 @@ import java.text.DecimalFormat import java.util.LinkedList import javax.inject.Inject import kotlin.math.abs +import kotlin.math.max class ExtendedBolusDialog : DialogFragmentWithDate() { @@ -70,9 +71,10 @@ class ExtendedBolusDialog : DialogFragmentWithDate() { val maxInsulin = constraintChecker.getMaxExtendedBolusAllowed().value() val extendedStep = pumpDescription.extendedBolusStep + val minInsulin = pumpDescription.extendedBolusMinAmount binding.insulin.setParams( savedInstanceState?.getDouble("insulin") - ?: extendedStep, extendedStep, maxInsulin, extendedStep, DecimalFormat("0.00"), false, binding.okcancel.ok + ?: minInsulin, minInsulin, maxInsulin, extendedStep, DecimalFormat("0.00"), false, binding.okcancel.ok ) val extendedDurationStep = pumpDescription.extendedBolusDurationStep From 3af4fbdb1d41d4a79d7886125bf19005292e71e4 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 4 Jun 2026 13:57:51 +0200 Subject: [PATCH 02/23] 3.4.2.3-dev --- buildSrc/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index cb97c9f8f4b2..cb6a550f7ec1 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget object Versions { // On change edit aaps-ci.yml - const val appVersion = "3.4.2.3" + const val appVersion = "3.4.2.3-dev" const val versionCode = 1500 const val compileSdk = 36 From 2e1640b4aa47c3c5909980df0aadbd74db83da80 Mon Sep 17 00:00:00 2001 From: vanelsberg Date: Thu, 11 Jun 2026 14:11:57 +0200 Subject: [PATCH 03/23] Cleanup Workflow AAPS CI: keep_runs=20, keep_threshold=5. Uses cleanup-workflow-runs.yml --- .github/workflows/aaps-ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/aaps-ci.yml b/.github/workflows/aaps-ci.yml index bc8071a315ad..d4838b3c0b45 100644 --- a/.github/workflows/aaps-ci.yml +++ b/.github/workflows/aaps-ci.yml @@ -354,3 +354,15 @@ jobs: upload_to_gdrive "aaps-wear-${VERSION}${VERSION_SUFFIX}.apk" "aaps-wear-${VERSION}${VERSION_SUFFIX}.apk" echo "🎉 APKs successfully uploaded to Google Drive!" + + # Configuration: Threshold-based cleanup + # Clean up only occurs when total runs exceed: keep_runs + keep_threshold (e.g., 20 + 5 = 25) + cleanup: + name: Cleanup Old Workflow Runs + needs: build + if: always() + uses: ./.github/workflows/cleanup-workflow-runs.yml + with: + workflow_name: 'AAPS CI' + keep_runs: 20 + keep_threshold: 5 \ No newline at end of file From 4a7f89d1c2981712cef379d39b5a79c95da99ec7 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 23 Jun 2026 18:22:10 +0200 Subject: [PATCH 04/23] Equil: do not drop commands after connect --- .../kotlin/app/aaps/pump/equil/ble/EquilBLE.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt index adbdb6779cdc..becbd3098915 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt @@ -277,10 +277,18 @@ class EquilBLE @Inject constructor( if (isConnected && baseCmd.isPairStep()) { ready() } else if (isConnected) { - preCmd?.let { preCmd -> - baseCmd.runCode = preCmd.runCode - baseCmd.runPwd = preCmd.runPwd + val prevCmd = preCmd + if (prevCmd != null) { + baseCmd.runCode = prevCmd.runCode + baseCmd.runPwd = prevCmd.runPwd nextCmd2() + } else { + // The GATT link was opened by the queue's connect() phase, which leaves no prior + // command context (baseCmd/preCmd are null, so the connect-time ready() was a no-op). + // Send this command as the first one on the open connection instead of silently + // dropping it - otherwise the pump receives nothing and idle-disconnects (status 19), + // surfacing as a bolus/command timeout. See issue #4910. + ready() } } else { findEquil(mac) From 39a9d2f50bdfb5d2f19b22f773af24436b209192 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 6 Jul 2026 22:50:37 +0200 Subject: [PATCH 05/23] Equil: max basal fix, tbr result fix --- .../app/aaps/pump/equil/EquilPumpPlugin.kt | 42 ++++++++++++++----- .../app/aaps/pump/equil/ble/EquilBLE.kt | 8 ++-- .../aaps/pump/equil/manager/EquilManager.kt | 2 +- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt index 0cd468ce2a0f..43ab3b1c5200 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt @@ -61,6 +61,7 @@ import app.aaps.pump.equil.manager.command.PumpEvent import app.aaps.pump.equil.manager.customCommands.CmdModeAndHistoryGet import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign +import kotlin.math.max import org.joda.time.DateTime import org.joda.time.Duration import javax.inject.Inject @@ -146,20 +147,30 @@ class EquilPumpPlugin @Inject constructor( else ToastUtils.infoToast(context, rh.gs(R.string.equil_error)) } }) - } else if (event.isChanged(DoubleKey.SafetyMaxBolus.key)) { - val profile = pumpSync.expectedPumpState().profile ?: return@subscribe - commandQueue.customCommand( - CmdSettingSet(constraintsChecker.getMaxBolusAllowed().value(), constraintsChecker.getMaxBasalAllowed(profile).value(), aapsLogger, preferences, equilManager), - object : Callback() { - override fun run() { - if (result.success) ToastUtils.infoToast(context, rh.gs(R.string.equil_pump_updated)) - else ToastUtils.infoToast(context, rh.gs(R.string.equil_error)) - } - }) + } else if (event.isChanged(DoubleKey.SafetyMaxBolus.key) || event.isChanged(DoubleKey.ApsMaxBasal.key)) { + resendPumpSettings() } }, fabricPrivacy::logException) } + // Re-program the pod's bolus/basal thresholds. The pod enforces the basal threshold (see CmdSettingSet) + // as a hard limit; if it goes stale-low, temp basals in the gap are silently rejected (the pump drops + // BLE with status 19). Use the STABLE max(ApsMaxBasal, maxDailyBasal) for basal — the same ceiling + // OpenAPS caps temp basals to — not the time-of-day-dependent getMaxBasalAllowed. Kept in sync on + // max-bolus / max-basal pref changes and on profile set (maxDailyBasal is profile-dependent). + private fun resendPumpSettings() { + val profile = pumpSync.expectedPumpState().profile ?: return + val maxBasal = max(preferences.get(DoubleKey.ApsMaxBasal), profile.getMaxDailyBasal()) + commandQueue.customCommand( + CmdSettingSet(constraintsChecker.getMaxBolusAllowed().value(), maxBasal, aapsLogger, preferences, equilManager), + object : Callback() { + override fun run() { + if (result.success) ToastUtils.infoToast(context, rh.gs(R.string.equil_pump_updated)) + else ToastUtils.infoToast(context, rh.gs(R.string.equil_error)) + } + }) + } + var tempActivationProgress = ActivationProgress.NONE var indexEquilReadStatus = 5 @@ -210,6 +221,17 @@ class EquilPumpPlugin @Inject constructor( val mode = equilManager.equilState?.runMode if (mode === RunMode.RUN || mode === RunMode.SUSPEND) { val basalSchedule = BasalSchedule.mapProfileToBasalSchedule(profile) + // Raise the pod's max-basal threshold BEFORE programming the schedule, so the pod accepts both + // the base schedule and later temp basals. Use the STABLE max(ApsMaxBasal, maxDailyBasal) — the + // same ceiling OpenAPS caps temp basals to — NOT getMaxBasalAllowed, which is time-of-day + // dependent (4x current basal) and can dip below the profile's own peak, wrongly starving the + // threshold. Sent directly (we're already inside a queued command — don't re-queue), gated on + // success (don't program the schedule on a stale threshold), and paced by EQUIL_BLE_NEXT_CMD + // like every other chained-command site so the second BLE write isn't dropped. + val maxBasal = max(preferences.get(DoubleKey.ApsMaxBasal), profile.getMaxDailyBasal()) + val settingResult = equilManager.executeCmd(CmdSettingSet(constraintsChecker.getMaxBolusAllowed().value(), maxBasal, aapsLogger, preferences, equilManager)) + if (!settingResult.success) return settingResult + SystemClock.sleep(EquilConst.EQUIL_BLE_NEXT_CMD) val pumpEnactResult = equilManager.executeCmd(CmdBasalSet(basalSchedule, profile, aapsLogger, preferences, equilManager)) if (pumpEnactResult.success) equilManager.equilState?.basalSchedule = basalSchedule return pumpEnactResult diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt index becbd3098915..2143ecf13823 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt @@ -240,9 +240,11 @@ class EquilBLE @Inject constructor( } fun closeBleAuto() { - handler.postDelayed({ - disconnect() - }, EquilConst.EQUIL_BLE_NEXT_CMD) + // Tear down immediately. The AAPS command queue owns the connection lifecycle: after the last + // command it holds the link for waitForDisconnectionInSeconds() (5 s) for reuse and only then + // calls Pump.disconnect() -> here. No extra driver-side linger is needed, and an immediate + // teardown avoids the mid-command race that a deferred, cancellable timer would introduce. + disconnect() } var autoScan = true diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt index a2c55a7e69be..d2e64f634e33 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt @@ -185,7 +185,7 @@ class EquilManager @Inject constructor( updateHistory(equilHistoryRecord, command.resolvedResult) loadEquilHistory() result.success = command.cmdSuccess - result.enacted(true) + result.enacted(command.cmdSuccess) } catch (ex: Exception) { ex.printStackTrace() result.success(false).enacted(false).comment(ex.message ?: "Exception") From 52056ebeb40d4854ee119827ace7717e69bae7a8 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 20 Jul 2026 10:08:37 +0200 Subject: [PATCH 06/23] Profile sync fix --- .../extensions/ProfileSwitchExtension.kt | 2 +- .../ProfileSwitchExtensionKtTest.kt | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/ProfileSwitchExtension.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/ProfileSwitchExtension.kt index 96dddb93756b..29bc867675fc 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/ProfileSwitchExtension.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/ProfileSwitchExtension.kt @@ -33,7 +33,7 @@ fun NSProfileSwitch.toProfileSwitch(activePlugin: ActivePlugin, dateUtil: DateUt profileName = originalProfileName ?: profile, timeshift = timeShift ?: 0, percentage = percentage ?: 100, - duration = duration ?: 0L, + duration = originalDuration ?: duration ?: 0L, iCfg = profileSealed.iCfg, ids = IDs(nightscoutId = identifier, pumpId = pumpId, pumpType = PumpType.fromString(pumpType), pumpSerial = pumpSerial, endId = endId) ) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/ProfileSwitchExtensionKtTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/ProfileSwitchExtensionKtTest.kt index 7b79035bb34f..f8f84a5fb83c 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/ProfileSwitchExtensionKtTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/ProfileSwitchExtensionKtTest.kt @@ -5,6 +5,7 @@ import app.aaps.core.data.model.IDs import app.aaps.core.data.model.PS import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.interfaces.insulin.Insulin +import app.aaps.core.nssdk.localmodel.treatment.EventType import app.aaps.core.nssdk.localmodel.treatment.NSProfileSwitch import app.aaps.core.nssdk.mapper.convertToRemoteAndBack import app.aaps.plugins.sync.extensions.contentEqualsTo @@ -83,4 +84,45 @@ internal class ProfileSwitchExtensionKtTest : TestBaseWithProfile() { assertThat(profileSwitch.contentEqualsTo(profileSwitch2)).isTrue() assertThat(profileSwitch.ids.contentEqualsTo(profileSwitch2.ids)).isTrue() } + + private fun nsProfileSwitch(duration: Long?, originalDuration: Long?) = NSProfileSwitch( + date = 10000, + identifier = "nightscoutId", + utcOffset = 0, + isValid = true, + eventType = EventType.PROFILE_SWITCH, + pumpId = 11000, + endId = null, + pumpType = PumpType.DANA_I.name, + pumpSerial = "bbbb", + profileJson = validProfile.toPureNsJson(dateUtil), + profile = "SomeProfile", + originalProfileName = "SomeProfile", + timeShift = 0, + percentage = 100, + duration = duration, + originalDuration = originalDuration + ) + + /** + * Documents uploaded before 9b90eea774 carry a duration inflated by 60000 and no + * durationInMilliseconds, so TreatmentMapper inflates it by 60000 a second time. + * originalDuration is written in milliseconds by every AAPS version and must win. + */ + @Test + fun inflatedLegacyDurationIsIgnoredInFavourOfOriginalDuration() { + val nineHours = 9 * 3600000L + val parsed = nsProfileSwitch(duration = nineHours * 60000 * 60000, originalDuration = nineHours) + .toProfileSwitch(activePlugin, dateUtil)!! + assertThat(parsed.duration).isEqualTo(nineHours) + } + + /** A writer that supplies no originalDuration must still fall back to the mapped duration. */ + @Test + fun durationIsUsedWhenOriginalDurationIsMissing() { + val nineHours = 9 * 3600000L + val parsed = nsProfileSwitch(duration = nineHours, originalDuration = null) + .toProfileSwitch(activePlugin, dateUtil)!! + assertThat(parsed.duration).isEqualTo(nineHours) + } } From aa82551f45a352bd98a51fccd7009e396380138b Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 22 Jul 2026 13:53:30 +0200 Subject: [PATCH 07/23] 3.4.2.4 --- buildSrc/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index cb6a550f7ec1..5811c19e2e54 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget object Versions { // On change edit aaps-ci.yml - const val appVersion = "3.4.2.3-dev" + const val appVersion = "3.4.2.4" const val versionCode = 1500 const val compileSdk = 36 From 40f96332e9b4ea08e391d8aa60fb102d9d075df4 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 22 Jul 2026 14:22:52 +0200 Subject: [PATCH 08/23] aaps-ci --- .github/workflows/aaps-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/aaps-ci.yml b/.github/workflows/aaps-ci.yml index d4838b3c0b45..fd4ce3e76ed0 100644 --- a/.github/workflows/aaps-ci.yml +++ b/.github/workflows/aaps-ci.yml @@ -6,10 +6,11 @@ on: tagName: description: 'Select AAPS Version' required: true - default: '3.4.2.3' + default: '3.4.2.4' type: choice options: # ── 3.4.x (JDK 21) ────────────────────────────── + - 3.4.2.4 - 3.4.2.3 - 3.4.2.2 - 3.4.2.1 From f93982bfa13bc708c29cd5462a3f49357fd37311 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 22 Jul 2026 14:47:23 +0200 Subject: [PATCH 09/23] 3.4.2.4-dev --- buildSrc/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 5811c19e2e54..9c3eeed1d2a0 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget object Versions { // On change edit aaps-ci.yml - const val appVersion = "3.4.2.4" + const val appVersion = "3.4.2.4-dev" const val versionCode = 1500 const val compileSdk = 36 From 27ffd44250685902c172c5abb86dfec1a795ca71 Mon Sep 17 00:00:00 2001 From: Angus Date: Thu, 23 Jul 2026 22:17:23 +0800 Subject: [PATCH 10/23] fix CI Cleanup permissions --- .github/workflows/aaps-ci.yml | 2 ++ .github/workflows/branch-ci.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/aaps-ci.yml b/.github/workflows/aaps-ci.yml index fd4ce3e76ed0..e20eb7f9d237 100644 --- a/.github/workflows/aaps-ci.yml +++ b/.github/workflows/aaps-ci.yml @@ -362,6 +362,8 @@ jobs: name: Cleanup Old Workflow Runs needs: build if: always() + permissions: + actions: write uses: ./.github/workflows/cleanup-workflow-runs.yml with: workflow_name: 'AAPS CI' diff --git a/.github/workflows/branch-ci.yml b/.github/workflows/branch-ci.yml index 8e731fbbe8c3..1e63e59e65f0 100644 --- a/.github/workflows/branch-ci.yml +++ b/.github/workflows/branch-ci.yml @@ -302,6 +302,8 @@ jobs: name: Cleanup Old Workflow Runs needs: build if: always() + permissions: + actions: write uses: ./.github/workflows/cleanup-workflow-runs.yml with: workflow_name: 'Branch CI' From a4c85abf6a5c8fdc8fdcd410b2ad566a7f9badac Mon Sep 17 00:00:00 2001 From: Angus Date: Thu, 23 Jul 2026 22:17:23 +0800 Subject: [PATCH 11/23] fix CI Cleanup permissions --- .github/workflows/aaps-ci.yml | 2 ++ .github/workflows/branch-ci.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/aaps-ci.yml b/.github/workflows/aaps-ci.yml index fd4ce3e76ed0..e20eb7f9d237 100644 --- a/.github/workflows/aaps-ci.yml +++ b/.github/workflows/aaps-ci.yml @@ -362,6 +362,8 @@ jobs: name: Cleanup Old Workflow Runs needs: build if: always() + permissions: + actions: write uses: ./.github/workflows/cleanup-workflow-runs.yml with: workflow_name: 'AAPS CI' diff --git a/.github/workflows/branch-ci.yml b/.github/workflows/branch-ci.yml index 8e731fbbe8c3..1e63e59e65f0 100644 --- a/.github/workflows/branch-ci.yml +++ b/.github/workflows/branch-ci.yml @@ -302,6 +302,8 @@ jobs: name: Cleanup Old Workflow Runs needs: build if: always() + permissions: + actions: write uses: ./.github/workflows/cleanup-workflow-runs.yml with: workflow_name: 'Branch CI' From 9516bb9b29b5d650837072630dc503861accd860 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 30 Jul 2026 21:37:39 +0200 Subject: [PATCH 12/23] Equil: fix race conditions --- .../app/aaps/pump/equil/ble/EquilBLE.kt | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt index 2143ecf13823..fc0c980cbe2e 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt @@ -123,6 +123,11 @@ class EquilBLE @Inject constructor( isConnected = true equilManager.equilState?.bluetoothConnectionState = BluetoothConnectionState.CONNECTED handler.removeMessages(TIME_OUT_CONNECT_WHAT) + synchronized(notifyLock) { + // New link: notifications not yet enabled. Block command dispatch until onDescriptorWrite. + notificationEnabled = false + pendingCmd = null + } bluetoothGatt?.discoverServices() updateCmdStatus(ResolvedResult.FAILURE) // rxBus.send(new EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED)); @@ -175,7 +180,14 @@ class EquilBLE @Inject constructor( aapsLogger.debug(LTag.PUMPBTCOMM, "onDescriptorWrite received: $status") if (status == BluetoothGatt.GATT_SUCCESS) { aapsLogger.debug(LTag.PUMPBTCOMM, "onDescriptorWrite: Wrote GATT Descriptor successfully.") - ready() + synchronized(notifyLock) { + notificationEnabled = true + // Flush a command that writeCmd deferred while notifications were coming up. + if (pendingCmd != null) { + pendingCmd = null + ready() + } + } } } } @@ -236,6 +248,10 @@ class EquilBLE @Inject constructor( bluetoothGatt = null baseCmd = null preCmd = null + synchronized(notifyLock) { + notificationEnabled = false + pendingCmd = null + } rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED)) } @@ -267,6 +283,21 @@ class EquilBLE @Inject constructor( private var baseCmd: BaseCmd? = null private var preCmd: BaseCmd? = null + + // Notification-readiness gate for the current GATT connection. Android allows only ONE outstanding + // GATT operation at a time. When the queue's connect() phase opens the link, `isConnected` flips + // true at onConnectionStateChange(CONNECTED) - BEFORE onServicesDiscovered runs openNotification() + // (the notify-descriptor write). If a command's writeCmd then writes its first characteristic packet + // in that window, it collides with the pending descriptor write: writeDescriptor() returns false + // (log: "openNotification: false"), notifications never enable, the pump's replies never arrive, and + // the command idle-times-out after ~9 s -> "Pump connection failure / manually check delivered + // insulin" (bolus, tempBasal, and profile/CmdSettingSet all hit this via different writeCmd branches). + // Fix: never send on a connected link until onDescriptorWrite confirms notifications are enabled; + // hold the command in `pendingCmd` and let onDescriptorWrite flush it. See #4910 (and its follow-up). + private val notifyLock = Any() + @Volatile private var notificationEnabled = false + private var pendingCmd: BaseCmd? = null + fun writeCmd(baseCmd: BaseCmd) { aapsLogger.debug(LTag.PUMPCOMM, "writeCmd {}", baseCmd) this.baseCmd = baseCmd @@ -276,6 +307,18 @@ class EquilBLE @Inject constructor( else -> equilManager?.equilState?.address ?: error("Unknown MAC address") } autoScan = baseCmd is CmdRunningModeGet || baseCmd is CmdInsulinGet + if (isConnected) { + synchronized(notifyLock) { + if (!notificationEnabled) { + // Fresh link, notifications not enabled yet: defer ALL send paths (pair step, + // continuation, or first command) so the characteristic write does not collide with + // the openNotification() descriptor write. onDescriptorWrite flushes pendingCmd. + pendingCmd = baseCmd + preCmd = baseCmd + return + } + } + } if (isConnected && baseCmd.isPairStep()) { ready() } else if (isConnected) { @@ -285,11 +328,8 @@ class EquilBLE @Inject constructor( baseCmd.runPwd = prevCmd.runPwd nextCmd2() } else { - // The GATT link was opened by the queue's connect() phase, which leaves no prior - // command context (baseCmd/preCmd are null, so the connect-time ready() was a no-op). - // Send this command as the first one on the open connection instead of silently - // dropping it - otherwise the pump receives nothing and idle-disconnects (status 19), - // surfacing as a bolus/command timeout. See issue #4910. + // GATT link opened by the queue's connect() phase, notifications already up: send this + // command as the first one on the open link (else the pump idle-disconnects, status 19). ready() } } else { From 515489a7f8c45934eb8e21f60a1a20be31107a52 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 30 Jul 2026 21:39:45 +0200 Subject: [PATCH 13/23] 3.4.2.5 --- buildSrc/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 9c3eeed1d2a0..9cbea8e5b358 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget object Versions { // On change edit aaps-ci.yml - const val appVersion = "3.4.2.4-dev" + const val appVersion = "3.4.2.5" const val versionCode = 1500 const val compileSdk = 36 From 1896162c2f2c752cb04943e91eefdda266e7a4eb Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 30 Jul 2026 21:40:59 +0200 Subject: [PATCH 14/23] Add version to CI --- .github/workflows/aaps-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/aaps-ci.yml b/.github/workflows/aaps-ci.yml index e20eb7f9d237..5a06151d1c2e 100644 --- a/.github/workflows/aaps-ci.yml +++ b/.github/workflows/aaps-ci.yml @@ -10,6 +10,7 @@ on: type: choice options: # ── 3.4.x (JDK 21) ────────────────────────────── + - 3.4.2.5 - 3.4.2.4 - 3.4.2.3 - 3.4.2.2 From af65f8b66f570e863935cd8b827df9834acb57c5 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 30 Jul 2026 22:13:15 +0200 Subject: [PATCH 15/23] update default ci --- .github/workflows/aaps-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/aaps-ci.yml b/.github/workflows/aaps-ci.yml index 5a06151d1c2e..616db6632cef 100644 --- a/.github/workflows/aaps-ci.yml +++ b/.github/workflows/aaps-ci.yml @@ -6,7 +6,7 @@ on: tagName: description: 'Select AAPS Version' required: true - default: '3.4.2.4' + default: '3.4.2.5' type: choice options: # ── 3.4.x (JDK 21) ────────────────────────────── From 39b229e581871523fcee58da5c8775c93f77fb67 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 30 Jul 2026 22:18:34 +0200 Subject: [PATCH 16/23] 3.4.2.5-dev --- .github/workflows/aaps-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/aaps-ci.yml b/.github/workflows/aaps-ci.yml index 616db6632cef..aaf083a097f7 100644 --- a/.github/workflows/aaps-ci.yml +++ b/.github/workflows/aaps-ci.yml @@ -6,7 +6,7 @@ on: tagName: description: 'Select AAPS Version' required: true - default: '3.4.2.5' + default: '3.4.2.5-dev' type: choice options: # ── 3.4.x (JDK 21) ────────────────────────────── From c28448df7215c43e34f3be288c4b094607b218b7 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 31 Jul 2026 09:21:52 +0200 Subject: [PATCH 17/23] Fix ci --- .circleci/config.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 371e685527e7..199eb125f44e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,6 +42,28 @@ jobs: emulator -avd citest -delay-adb -verbose -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim background: true + - run: + name: Wait for emulator boot + command: | + export ANDROID_SDK_ROOT=/usr/lib/android-sdk + export ANDROID_HOME=/usr/lib/android-sdk + # Boot gate: the emulator above is launched in the background, so without this the test task + # can start before the device is online and fail with the misleading + # "com.android.builder.testing.api.DeviceException: No online devices found". + # Block until adb sees the device AND the system has fully booted, with a hard timeout so a + # stuck emulator fails loudly HERE (clear message) instead of later. + adb wait-for-device + timeout 420 bash -c 'until [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d "\r")" = "1" ]; do sleep 2; done' + adb shell getprop sys.boot_completed + # Settle, dismiss keyguard and disable animations so ComposeMainActivity launches cleanly: + # a heavy UI launch on an unsettled emulator can ANR system_server and drop the device offline + # mid-run (the failure mode this hardening targets). + adb shell input keyevent 82 || true + adb shell settings put global window_animation_scale 0 || true + adb shell settings put global transition_animation_scale 0 || true + adb shell settings put global animator_duration_scale 0 || true + adb devices + - run: name: Run connectedFullDebugAndroidTest command: | From 8456c9dc502e6b0e4380b363bab721e47d6dd624 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 31 Jul 2026 09:21:52 +0200 Subject: [PATCH 18/23] Fix ci --- .circleci/config.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 371e685527e7..199eb125f44e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,6 +42,28 @@ jobs: emulator -avd citest -delay-adb -verbose -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim background: true + - run: + name: Wait for emulator boot + command: | + export ANDROID_SDK_ROOT=/usr/lib/android-sdk + export ANDROID_HOME=/usr/lib/android-sdk + # Boot gate: the emulator above is launched in the background, so without this the test task + # can start before the device is online and fail with the misleading + # "com.android.builder.testing.api.DeviceException: No online devices found". + # Block until adb sees the device AND the system has fully booted, with a hard timeout so a + # stuck emulator fails loudly HERE (clear message) instead of later. + adb wait-for-device + timeout 420 bash -c 'until [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d "\r")" = "1" ]; do sleep 2; done' + adb shell getprop sys.boot_completed + # Settle, dismiss keyguard and disable animations so ComposeMainActivity launches cleanly: + # a heavy UI launch on an unsettled emulator can ANR system_server and drop the device offline + # mid-run (the failure mode this hardening targets). + adb shell input keyevent 82 || true + adb shell settings put global window_animation_scale 0 || true + adb shell settings put global transition_animation_scale 0 || true + adb shell settings put global animator_duration_scale 0 || true + adb devices + - run: name: Run connectedFullDebugAndroidTest command: | From c0ca46a799191ded825bc66b623687aa3e9d1f5a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 1 Aug 2026 14:19:20 +0200 Subject: [PATCH 19/23] Equil: SCAN_MODE_LOW_LATENCY --- .../app/aaps/pump/equil/ble/EquilBLE.kt | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt index fc0c980cbe2e..642a23552822 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt @@ -240,6 +240,7 @@ class EquilBLE @Inject constructor( isConnected = false connecting = false startTrue = false + connectInitiated = false autoScan = false equilManager?.equilState?.bluetoothConnectionState = BluetoothConnectionState.DISCONNECTED aapsLogger.debug(LTag.PUMPBTCOMM, "Closing GATT connection") @@ -420,12 +421,18 @@ class EquilBLE @Inject constructor( } } private var startTrue = false + + // One-shot guard: set true when onScanResult fires the connect for the current scan session, re-armed at + // each startScan(). Prevents the rapid LOW_LATENCY result stream from opening multiple GATT clients. + private var connectInitiated = false + private fun startScan() { macAddress = equilManager?.equilState?.address aapsLogger.debug(LTag.PUMPBTCOMM, "startScan====$startTrue====$macAddress===") if (macAddress.isNullOrEmpty()) return if (startTrue) return startTrue = true + connectInitiated = false connecting = true equilManager?.equilState?.bluetoothConnectionState = BluetoothConnectionState.CONNECTING if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED) { @@ -469,6 +476,10 @@ class EquilBLE @Inject constructor( private fun buildScanSettings(): ScanSettings { val builder = ScanSettings.Builder() + // Command connects are latency-sensitive (a bolus/temp-basal is waiting on discovery). The default + // SCAN_MODE_LOW_POWER duty-cycles the radio and can take tens of seconds to surface a bonded pump on + // some phones, long enough for the command to time out. Use LOW_LATENCY so the pump is found in ~1 s. + builder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) builder.setReportDelay(0) return builder.build() } @@ -476,20 +487,25 @@ class EquilBLE @Inject constructor( private var scanCallback: ScanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { super.onScanResult(callbackType, result) - val name: String? = result.device.name - if (name?.isNotEmpty() == true) { - try { + // The scan is filtered by MAC address (buildScanFilters), so every result IS the target pump. + // Do NOT gate on result.device.name: it is frequently null until the OS caches the device name, + // which silently drops valid matches and stalls discovery for tens of seconds. Guard with a + // one-shot flag so the rapid LOW_LATENCY result stream opens only a single GATT client. + if (connectInitiated) return + connectInitiated = true + try { + result.scanRecord?.bytes?.let { bytes -> bleHandler.post { - equilManager?.decodeData(result.scanRecord!!.bytes, autoScan) + equilManager?.decodeData(bytes, autoScan) } - stopScan() - if (autoScan) { - updateCmdStatus(ResolvedResult.CONNECT_ERROR) - connectEquil(result.device) - } - } catch (e: Exception) { - e.printStackTrace() } + stopScan() + if (autoScan) { + updateCmdStatus(ResolvedResult.CONNECT_ERROR) + connectEquil(result.device) + } + } catch (e: Exception) { + aapsLogger.error(LTag.PUMPBTCOMM, "onScanResult error", e) } } } From d762bdf355301f4b5752c717ce2e5568b11b8e0d Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 1 Aug 2026 16:28:10 +0200 Subject: [PATCH 20/23] 3.4.2.5-dev --- buildSrc/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 9cbea8e5b358..35307bdb6a93 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget object Versions { // On change edit aaps-ci.yml - const val appVersion = "3.4.2.5" + const val appVersion = "3.4.2.5-dev" const val versionCode = 1500 const val compileSdk = 36 From 1dd66e0481f4a95132fa7e098f0756ba6b8737e9 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 1 Aug 2026 18:48:13 +0200 Subject: [PATCH 21/23] Equil: direct connect without scan --- .../app/aaps/pump/equil/EquilPumpPlugin.kt | 27 +++++----- .../app/aaps/pump/equil/ble/EquilBLE.kt | 51 ++++++++++++++----- .../aaps/pump/equil/manager/EquilManager.kt | 13 +++++ 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt index 43ab3b1c5200..854aa23d8e94 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt @@ -116,20 +116,19 @@ class EquilPumpPlugin @Inject constructor( .toObservable(EventEquilAlarm::class.java) .observeOn(aapsSchedulers.io) .subscribe({ eventEquilError -> - commandQueue.performing()?.let { - if (it.commandType == Command.CommandType.BOLUS) { - aapsLogger.info( - LTag.PUMPCOMM, - "eventEquilError.tips====${eventEquilError.tips}" - ) - rxBus.send(EventDismissNotification(Notification.EQUIL_ALARM)) - equilManager.showNotification( - Notification.EQUIL_ALARM, - eventEquilError.tips, - Notification.URGENT, app.aaps.core.ui.R.raw.alarm - ) - stopBolusDelivering() - } + aapsLogger.info(LTag.PUMPCOMM, "eventEquilError.tips====${eventEquilError.tips}") + // Always surface the pump alarm - it is no longer gated on a bolus being in + // progress (alarms now come from the GATT history read on every connection, not + // just from an advertisement scan caught mid-bolus). See #5040. + rxBus.send(EventDismissNotification(Notification.EQUIL_ALARM)) + equilManager.showNotification( + Notification.EQUIL_ALARM, + eventEquilError.tips, + Notification.URGENT, app.aaps.core.ui.R.raw.alarm + ) + // But only halt bolus tracking if a bolus is actually delivering. + if (commandQueue.performing()?.commandType == Command.CommandType.BOLUS) { + stopBolusDelivering() } }, fabricPrivacy::logException) diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt index 642a23552822..070d49a748fe 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt @@ -18,7 +18,6 @@ import android.content.Context import android.content.pm.PackageManager import android.os.Handler import android.os.HandlerThread -import android.os.Looper import android.os.Message import android.os.SystemClock import android.text.TextUtils @@ -39,9 +38,7 @@ import app.aaps.pump.equil.manager.Utils import app.aaps.pump.equil.manager.command.BaseCmd import app.aaps.pump.equil.manager.command.CmdDevicesOldGet import app.aaps.pump.equil.manager.command.CmdHistoryGet -import app.aaps.pump.equil.manager.command.CmdInsulinGet import app.aaps.pump.equil.manager.command.CmdPair -import app.aaps.pump.equil.manager.command.CmdRunningModeGet import java.util.UUID import javax.inject.Inject import javax.inject.Singleton @@ -123,6 +120,9 @@ class EquilBLE @Inject constructor( isConnected = true equilManager.equilState?.bluetoothConnectionState = BluetoothConnectionState.CONNECTED handler.removeMessages(TIME_OUT_CONNECT_WHAT) + // Link is up: stop the parallel advert-harvest scan (the pump stops advertising once + // connected anyway). If it already caught an advert it stopped itself in onScanResult. + stopScan() synchronized(notifyLock) { // New link: notifications not yet enabled. Block command dispatch until onDescriptorWrite. notificationEnabled = false @@ -237,6 +237,7 @@ class EquilBLE @Inject constructor( } fun disconnect() { + stopScan() // stop any in-flight advert-harvest scan (hybrid connect) isConnected = false connecting = false startTrue = false @@ -268,16 +269,21 @@ class EquilBLE @Inject constructor( private fun findEquil(mac: String) { if (mac.isEmpty()) return if (isConnected) return - val equilDevice: BluetoothDevice? = bluetoothAdapter?.getRemoteDevice(mac) - if (autoScan) startScan() - else connectEquil(equilDevice) + // Known pump: connect straight to the MAC (autoConnect), no scan. See connect() / #5040. + // Mirror connect()'s state handling so isConnecting() reflects the in-flight direct connect. + connecting = true + equilManager?.equilState?.bluetoothConnectionState = BluetoothConnectionState.CONNECTING + connectEquil(bluetoothAdapter?.getRemoteDevice(mac)) } fun connectEquil(device: BluetoothDevice?) { handler.postDelayed({ if (device != null) { aapsLogger.debug(LTag.PUMPCOMM, "connectEquil======") - bluetoothGatt = device.connectGatt(context, false, mGattCallback, BluetoothDevice.TRANSPORT_LE) + // autoConnect = true: the Android stack completes the link as soon as the (known/bonded) + // pump is in range, with no app-level scan. This replaces flaky scan discovery, which took + // 60-90 s on many phones and caused command timeouts / "no insulin delivered" (#5040). + bluetoothGatt = device.connectGatt(context, true, mGattCallback, BluetoothDevice.TRANSPORT_LE) } }, 500) } @@ -303,11 +309,10 @@ class EquilBLE @Inject constructor( aapsLogger.debug(LTag.PUMPCOMM, "writeCmd {}", baseCmd) this.baseCmd = baseCmd val mac: String = when (baseCmd) { - is CmdPair -> baseCmd.address + is CmdPair -> baseCmd.address is CmdDevicesOldGet -> baseCmd.address - else -> equilManager?.equilState?.address ?: error("Unknown MAC address") + else -> equilManager?.equilState?.address ?: error("Unknown MAC address") } - autoScan = baseCmd is CmdRunningModeGet || baseCmd is CmdInsulinGet if (isConnected) { synchronized(notifyLock) { if (!notificationEnabled) { @@ -349,7 +354,7 @@ class EquilBLE @Inject constructor( preCmd = baseCmd } else { aapsLogger.debug(LTag.PUMPCOMM, "readHistory error") - synchronized(baseCmd) { (baseCmd as Object).notifyAll() } + synchronized(baseCmd) { (baseCmd as Any).notifyAll() } } } @@ -382,6 +387,7 @@ class EquilBLE @Inject constructor( } private var dataList: List = ArrayList() + @Synchronized fun decode(buffer: ByteArray) { val str = Utils.bytesToHex(buffer) @@ -408,7 +414,7 @@ class EquilBLE @Inject constructor( override fun handleMessage(msg: Message) { super.handleMessage(msg) when (msg.what) { - TIME_OUT_WHAT -> stopScan() + TIME_OUT_WHAT -> stopScan() TIME_OUT_CONNECT_WHAT -> { stopScan() @@ -458,9 +464,26 @@ class EquilBLE @Inject constructor( if (connecting || isConnected) { return } - autoScan = true baseCmd = null - startScan() + macAddress = equilManager?.equilState?.address + val device = macAddress?.takeIf { it.isNotEmpty() }?.let { bluetoothAdapter?.getRemoteDevice(it) } + if (device != null) { + // Known/bonded pump: connect straight to its MAC (see connectEquil, autoConnect=true) instead + // of scanning-to-connect. Scan-to-connect was the #5040 bottleneck (60-90 s on many phones). + connecting = true + equilManager?.equilState?.bluetoothConnectionState = BluetoothConnectionState.CONNECTING + connectEquil(device) + // Hybrid: run a best-effort advertisement harvest IN PARALLEL. It does NOT gate the connection + // (autoConnect above owns that), but the advert carries data the GATT path can't get: the pump's + // current history index (needed so loadEquilHistory reads new records), battery/reservoir, and the + // live alarm state. autoScan=false so onScanResult only decodes the advert - it does not open a + // second GATT client. The scan is stopped on CONNECTED / onScanResult / disconnect. See #5040. + autoScan = false + startScan() + } else { + autoScan = true + startScan() + } } private fun buildScanFilters(): List { diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt index d2e64f634e33..fad501272ab3 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt @@ -718,6 +718,19 @@ class EquilManager @Inject constructor( equilHistoryPump.serialNumber = getSerialNumber() val id = equilHistoryPumpDao.insert(equilHistoryPump) aapsLogger.debug(LTag.PUMPCOMM, "decodeHistory insert id {}", id) + // Surface pump errors/alarms from the GATT history. loadEquilHistory only replays records past + // the stored cursor, so each call here is a fresh event - no extra dedup needed. This is the only + // alarm source now that connections go direct-to-MAC (no scan, so the advertisement decodeData() + // path that used to raise EventEquilAlarm no longer runs). See #5040. + val errorTips = getEquilError(port, type, level) + if (errorTips.isNotEmpty()) { + val alarmRecord = EquilHistoryRecord(EquilHistoryRecord.EventType.EQUIL_ALARM, System.currentTimeMillis(), getSerialNumber()) + alarmRecord.resolvedAt = System.currentTimeMillis() + alarmRecord.resolvedStatus = ResolvedResult.SUCCESS + alarmRecord.note = errorTips + equilHistoryRecordDao.insert(alarmRecord) + rxBus.send(EventEquilAlarm(errorTips)) + } rxBus.send(EventEquilDataChanged()) } From 03ea9f72f5ef45dc8e9d1fef9e5213df6180f9f8 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 1 Aug 2026 21:36:19 +0200 Subject: [PATCH 22/23] Equil: fix prime --- .../app/aaps/pump/equil/ble/EquilBLE.kt | 39 ++++++++++++------- .../aaps/pump/equil/manager/EquilManager.kt | 13 ------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt index 070d49a748fe..b163f53870c7 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt @@ -126,7 +126,7 @@ class EquilBLE @Inject constructor( synchronized(notifyLock) { // New link: notifications not yet enabled. Block command dispatch until onDescriptorWrite. notificationEnabled = false - pendingCmd = null + dispatchedCmd = null } bluetoothGatt?.discoverServices() updateCmdStatus(ResolvedResult.FAILURE) @@ -182,11 +182,9 @@ class EquilBLE @Inject constructor( aapsLogger.debug(LTag.PUMPBTCOMM, "onDescriptorWrite: Wrote GATT Descriptor successfully.") synchronized(notifyLock) { notificationEnabled = true - // Flush a command that writeCmd deferred while notifications were coming up. - if (pendingCmd != null) { - pendingCmd = null - ready() - } + // Notifications live: send the command (queue-opened, deferred, or issued while the + // link was down). Send-once via dispatchedCmd so it can't collide/double with writeCmd. + dispatchCmd() } } } @@ -252,7 +250,7 @@ class EquilBLE @Inject constructor( preCmd = null synchronized(notifyLock) { notificationEnabled = false - pendingCmd = null + dispatchedCmd = null } rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED)) } @@ -300,10 +298,23 @@ class EquilBLE @Inject constructor( // the command idle-times-out after ~9 s -> "Pump connection failure / manually check delivered // insulin" (bolus, tempBasal, and profile/CmdSettingSet all hit this via different writeCmd branches). // Fix: never send on a connected link until onDescriptorWrite confirms notifications are enabled; - // hold the command in `pendingCmd` and let onDescriptorWrite flush it. See #4910 (and its follow-up). + // dispatchedCmd makes that send-once (per link) so writeCmd and onDescriptorWrite can't double-send, and + // the command dispatches after connect regardless of which writeCmd branch opened the link - including a + // command issued while disconnected (the reservoir-change case, where the pump drops BLE between steps). + // Ported from the dev branch. See #4910 / #5040. private val notifyLock = Any() @Volatile private var notificationEnabled = false - private var pendingCmd: BaseCmd? = null + private var dispatchedCmd: BaseCmd? = null + + // Send the current command's first packet exactly once per link, only after notifications are enabled. + // Null-safe (no-op during the pure connect handshake). Caller MUST hold notifyLock. + private fun dispatchCmd() { + val cmd = baseCmd + if (cmd != null && cmd !== dispatchedCmd) { + dispatchedCmd = cmd + ready() + } + } fun writeCmd(baseCmd: BaseCmd) { aapsLogger.debug(LTag.PUMPCOMM, "writeCmd {}", baseCmd) @@ -316,17 +327,15 @@ class EquilBLE @Inject constructor( if (isConnected) { synchronized(notifyLock) { if (!notificationEnabled) { - // Fresh link, notifications not enabled yet: defer ALL send paths (pair step, - // continuation, or first command) so the characteristic write does not collide with - // the openNotification() descriptor write. onDescriptorWrite flushes pendingCmd. - pendingCmd = baseCmd + // Fresh link, notifications not enabled yet: defer ALL send paths. onDescriptorWrite -> + // dispatchCmd() sends the command once notifications are up (no descriptor collision). preCmd = baseCmd return } } } if (isConnected && baseCmd.isPairStep()) { - ready() + synchronized(notifyLock) { dispatchCmd() } } else if (isConnected) { val prevCmd = preCmd if (prevCmd != null) { @@ -336,7 +345,7 @@ class EquilBLE @Inject constructor( } else { // GATT link opened by the queue's connect() phase, notifications already up: send this // command as the first one on the open link (else the pump idle-disconnects, status 19). - ready() + synchronized(notifyLock) { dispatchCmd() } } } else { findEquil(mac) diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt index fad501272ab3..d2e64f634e33 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt @@ -718,19 +718,6 @@ class EquilManager @Inject constructor( equilHistoryPump.serialNumber = getSerialNumber() val id = equilHistoryPumpDao.insert(equilHistoryPump) aapsLogger.debug(LTag.PUMPCOMM, "decodeHistory insert id {}", id) - // Surface pump errors/alarms from the GATT history. loadEquilHistory only replays records past - // the stored cursor, so each call here is a fresh event - no extra dedup needed. This is the only - // alarm source now that connections go direct-to-MAC (no scan, so the advertisement decodeData() - // path that used to raise EventEquilAlarm no longer runs). See #5040. - val errorTips = getEquilError(port, type, level) - if (errorTips.isNotEmpty()) { - val alarmRecord = EquilHistoryRecord(EquilHistoryRecord.EventType.EQUIL_ALARM, System.currentTimeMillis(), getSerialNumber()) - alarmRecord.resolvedAt = System.currentTimeMillis() - alarmRecord.resolvedStatus = ResolvedResult.SUCCESS - alarmRecord.note = errorTips - equilHistoryRecordDao.insert(alarmRecord) - rxBus.send(EventEquilAlarm(errorTips)) - } rxBus.send(EventEquilDataChanged()) } From 335465cecf1287682ad44e9c1a00fe8a9498294a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 2 Aug 2026 12:17:00 +0200 Subject: [PATCH 23/23] 3.4.2.6 --- .github/workflows/aaps-ci.yml | 4 ++-- buildSrc/src/main/kotlin/Versions.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/aaps-ci.yml b/.github/workflows/aaps-ci.yml index aaf083a097f7..9553f52cc785 100644 --- a/.github/workflows/aaps-ci.yml +++ b/.github/workflows/aaps-ci.yml @@ -6,11 +6,11 @@ on: tagName: description: 'Select AAPS Version' required: true - default: '3.4.2.5-dev' + default: '3.4.2.6' type: choice options: # ── 3.4.x (JDK 21) ────────────────────────────── - - 3.4.2.5 + - 3.4.2.6 - 3.4.2.4 - 3.4.2.3 - 3.4.2.2 diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 35307bdb6a93..e977642029ac 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget object Versions { // On change edit aaps-ci.yml - const val appVersion = "3.4.2.5-dev" + const val appVersion = "3.4.2.6" const val versionCode = 1500 const val compileSdk = 36