Skip to content

Omnipod Dash Drift resolution#4499

Open
nl-ruud wants to merge 4 commits intonightscout:dev3from
nl-ruud:issue-4158-dev-fix
Open

Omnipod Dash Drift resolution#4499
nl-ruud wants to merge 4 commits intonightscout:dev3from
nl-ruud:issue-4158-dev-fix

Conversation

@nl-ruud
Copy link
Copy Markdown

@nl-ruud nl-ruud commented Jan 18, 2026

Omnipod Dash basal drift resolution

This pull request introduces a new "basal correction" feature for the Omnipod Dash pump integration. The main goal is to detect under-delivery of basal insulin and automatically trigger a small correction bolus when needed. The implementation tracks delivered basal and bolus pulses, calculates expected vs. actual insulin delivery, and manages correction logic with safety checks and cooldowns.

Recap of the issue

The Omnipod Dash pump exhibits behavior that causes it to deliver less basal insulin than AAPS expects (issue #4158). This is effectively a hardware limitation of the pump.

The Dash uses an internal timer to determine when a basal pulse of 0.05 U is delivered. Once the timer interval elapses, the pulse is delivered. However, this timer is restarted whenever a basal rate change occurs (and possibly also when a bolus or SMB is delivered).

When used in combination with looping, this leads to structural under-delivery of basal insulin, as the algorithm adjusts the basal rate frequently. While the Medtrum pump compensates for this behavior internally, the Dash does not, and neither does the current Dash driver implementation in AAPS.

The issue is most apparent during the night. During daytime operation, SMBs often result in a basal rate of 0, which masks the effect. In observed usage, this results in approximately 10% of the expected Total Daily Dose (TDD) not being delivered over a 24-hour period. Additionally, glucose targets are often not reached overnight, particularly after meals with prolonged glucose impact (e.g. pasta).

Approach

This pull request introduces automatic basal drift compensation by correcting the pump lag with small correction boluses. A correction bolus of 0.05 U is delivered as soon as the detected basal deficit reaches 0.025 U. Several safety constraints are applied. For example, corrections are not delivered during a temporary basal of 0 unless that temporary basal is the direct result of a recently delivered bolus.

With this approach, the deviation between expected and delivered basal insulin is kept within a bounded range of approximately −0.050 U to +0.025 U.

With this fix applied:

  • The TDD reported by AAPS (under Statistics) matches the total amount actually delivered by the pump (Dash menu → Pod managementHistory).
  • Overnight glucose targets are consistently reached.

Without this fix, a clear discrepancy between reported and delivered TDD can be observed.

Main Changes

Basal Correction Feature

  • Added a new custom command, CommandDeliverBasalCorrection, and integrated it into the command queue and handling logic in OmnipodDashPumpPlugin to trigger a basal correction bolus when under-delivery is detected.
  • Implemented the deliverBasalCorrection method to handle the actual delivery of the correction bolus, including safety checks (reservoir level, bolus in progress, cooldowns, etc.).

Basal Delivery Tracking and Drift Calculation

  • Added logic in OmnipodDashPodStateManagerImpl to track basal and bolus pulses separately, compute actual delivered basal, and calculate "drift" (difference between expected and actual basal delivery). This includes new fields in the pod state and methods for integrating expected delivery over time.
  • The needsBasalCorrection method determines when a correction is needed based on drift, cooldowns, and other safety criteria.

Pod State Update and Logging Refactor

  • Refactored pod state update logic to centralize updates in a new updatePodState method, which also updates basal tracking fields and logs basal delivery/drift for debugging. This method is now used in both default and alarm status response handlers and removes code duplication from these methods.

Logging example:

PUMP_BASAL act=8,10U (tot=33,25U bol=25,15U) exp=9,42U err=-1,32U dErr=-0,02U

Legend for the log fields:

Field Description
act Actual basal units delivered (totbol)
tot Total units delivered
bol Bolus units delivered
exp Expected basal units delivered
err Error / cumulative basal drift (act - exp)
dErr Delta of err on this pump status response

Interface and State Enhancements

  • Updated the OmnipodDashPodStateManager interface and its implementation to support new fields and methods for basal correction tracking (lastBasalCorrectionTime, basalCorrectionInProgress, needsBasalCorrection).

These changes together enable the system to automatically detect and correct small basal under-deliveries, improving insulin delivery reliability and safety.

Tests Performed

To verify the fix, we compared the total insulin delivered at 00:00 at the start of a day with the total at 00:00 at the end of the same day.

Before this fix, the difference consistently resulted in a lower number than the TDD reported by AAPS, confirming under-delivery. After applying the fix, this discrepancy no longer occurs: AAPS TDD matches the pump’s delivered total.

@vanelsberg
Copy link
Copy Markdown
Contributor

This PR contains unrelated commits from the dev branche, which I think is undesired?
Not an expert on this: maybe consult with @MilosKozak on how to fix.

@vanelsberg
Copy link
Copy Markdown
Contributor

vanelsberg commented Jan 19, 2026

In addition, it is stated: "It should be reviewed on top of PR #4467"
This makes merging this PR with AAPS-CI impossible. Is there a reason this PR's are separate?
Please consider combining PR#4467 and this PR into one?

@vanelsberg
Copy link
Copy Markdown
Contributor

vanelsberg commented Jan 19, 2026

Nice 👍
Will need to review and test implementation in detail and test to see how this PR does.

@vanelsberg
Copy link
Copy Markdown
Contributor

This PR contains lots of file changes and commits.
To enable proper reviewing, please consider squashing all commits into one?

@nl-ruud nl-ruud force-pushed the issue-4158-dev-fix branch 2 times, most recently from df5aa07 to 189629d Compare January 19, 2026 09:21
@nl-ruud
Copy link
Copy Markdown
Author

nl-ruud commented Jan 19, 2026

@vanelsberg I think I fixed it, there are two commits now in this PR: the first one equals 4467 (quantifying the basal drift) and the second one is resolving the basal drift. does this make sense? I think that also would mean we can close #4467 as the distinction is now just clear in this PR.

@vanelsberg
Copy link
Copy Markdown
Contributor

vanelsberg commented Jan 19, 2026

I think I fixed it, there are two commits now in this PR....

Yes! This looks a lot better, making it easier to follow implementation. Helps reviewing/testing and - eventually - when ok, acceptance for merging with dev.

(When 4467 is no longer of use, I think best to close it to prevent confusion?)

@nl-ruud nl-ruud force-pushed the issue-4158-dev-fix branch 2 times, most recently from 6b01383 to 189629d Compare January 26, 2026 09:14
@vanelsberg
Copy link
Copy Markdown
Contributor

vanelsberg commented Feb 3, 2026

@nl-ruud Proposed way to testing this PR:

To see if AAPS is affected first test this whiteout the PR, preferably for 2 or more days. Then merge this PR and retest with current AAPS (this can be current dev or current 3.4 release).

Test:

  1. Check the pod history on the DASH tab.
    Note "Total insulin delivered" at two points in time for a specific day: At 00:00h and 24:00h; the difference is what the Pod reports as insulin delivered for that day period.

  2. Check AAPS TDD statistics
    For that same day (2), check on what is reported as the sum of insulin (first column at the right of the date).

Validation:
"Total insulin delivered" from (1) should closely match what is reported as TDD. Expect a difference no more than 0.05U

Optional, check the AAPS logfiles, like filtering on the terms below:
grep -E 'PUMP_BASAL|deliverBasalCorrection|calculateBolusPulseIncrease|integrateExpectedDelivery' *

@nl-ruud
Copy link
Copy Markdown
Author

nl-ruud commented Feb 4, 2026

Expect a difference no more than 0.5U

Actually no more than 0.05U. If a bigger difference is perceived then I’d love to see the AAPS logs of that 24 hour period.

Also, the docs currently state “When using SMB, limit the interval to 5 minutes minimum to avoid this issue.”. Even though that does not avoid the issue (as stated there), it may reduce the severity. So reset that setting to default (i believe it’s 3 minutes) before you test if you followed that direction from the docs. Especially if you use a sensor like FSL that updates every minute.

@vanelsberg
Copy link
Copy Markdown
Contributor

vanelsberg commented Feb 5, 2026

Have been testing/reviewing this PR for the past 4 days (PR merged with current 3.4 dev):
Code looks good.

Measuring insulin drift comparing what AAPS reports on statistics/TDD with actual "insulin delivered" as reported on the DASH pod history. Differences measured are in expected range and tolerance of 0.00..0.05U 👍

Note:
Did need to do some approximations on actual insulin delivered as reported by Pod history for running TBR's at 00:00. For example: TBR running from 23:55h to 00:05 of 0.5U/h, adjust insulin delivered at 23:55 with 0.25 units

results

Now testing for 4 days (similar circumstances, identical Profile) for AAPS without PR4499.
Will share final test and code review results.

@xitation
Copy link
Copy Markdown

xitation commented Feb 5, 2026

I've cherry picked the commits into Master (3.4.0.0) and installed updated apk with fix applied tonight.

I saw a drift of about 2U of insulin before patch. Will post back findings tomorrow with patch installed.

image

@xitation
Copy link
Copy Markdown

xitation commented Feb 5, 2026

So far seems to be ok, however it's been beeping when making corrections. I have the Bolus alert / beep enabled only. Annoyed my wife overnight :)

Perhaps could look at using SMB/TBR or one of the other alert beeps for this instead of the Bolus one, I suspect a number of people would leave the Bolus beep enabled.

image

@olorinmaia
Copy link
Copy Markdown
Contributor

olorinmaia commented Feb 5, 2026

Thanks for looking into this issue and providing a promising fix.

I will also assist with testing. Will log TDD for Dash and AAPS calculated like this:
image

I will first run without this PR to see for myself the difference between Dash TDD and AAPS TDD, I haven't had time to check before now.

I will after a day or two apply PR by cherry-picking commits onto my master build and report back with result. My son only got around 15-20TDD, depending on activity, should I expect to see a big total diff without this PR?

@rickvh3
Copy link
Copy Markdown

rickvh3 commented Feb 6, 2026

With the fix, I notice that the loop has become more consistent and shows less unpredictable behavior. I’ve been using this since January 23 (14 days now), and I’m satisfied with the result.

@nl-ruud
Copy link
Copy Markdown
Author

nl-ruud commented Feb 7, 2026

So far seems to be ok, however it's been beeping when making corrections. I have the Bolus alert / beep enabled only. Annoyed my wife overnight :)

If you cherry pick 254dfe3 then the beeps should be gone. Will wait for the others to complete testing and then add a final commit for the PR.

@vanelsberg
Copy link
Copy Markdown
Contributor

vanelsberg commented Feb 7, 2026

Confirming beeps for correction bolus. Noticed this only twice during testing: 1 unexpected beep (ignored it) and unexpected bolus notification for 0.05U on Watch. The last one triggered some questions (forgot all about this when posting test results on TDD deviations).

Currently starting to test DASH without PR4499 under similar circumstances. After 4 days testing I'l post results here.

.... If you cherry pick [254dfe3] then the beeps should be gone.

@nl-ruud You may want to check if your code change not only skips the beep but also the notification?
Once you added this to the PR I'll retest for "no beeps" and no "notification" and post final test results.

@olorinmaia
Copy link
Copy Markdown
Contributor

Posting results from logging total insulin delivery for two days, one without PR and one with PR. No issues so far.

We got all beeps disabled. No issues when they are disabled either.

image

@xitation
Copy link
Copy Markdown

xitation commented Feb 8, 2026

So far seems to be ok, however it's been beeping when making corrections. I have the Bolus alert / beep enabled only. Annoyed my wife overnight :)

If you cherry pick 254dfe3 then the beeps should be gone. Will wait for the others to complete testing and then add a final commit for the PR.

Seems to have resolved beeping, thanks!

In terms of test data here are my results so far:

image

@belevine
Copy link
Copy Markdown

I have tested this PR for several days now on v3.4. Before building with the PR, I observed a drift of 1.5-2.5 units per day. After the PR, I'm getting no drift or 0.05 units (which I suspect may be more due to a challenge in determining the exact Dash delivered amounts at midnight if the only timestamps are 5-10 mins before or after).

I also have manually bolused while testing the PR and found this also was reported correctly. The AAPS statistics included the manual bolus while the Dash history did not.

@nl-ruud
Copy link
Copy Markdown
Author

nl-ruud commented Feb 12, 2026

I just submitted a (final?) update to the PR that addresses two issues that were reported by @xitation, which are:

  • no more beeps
  • reduced the cooldown period from 5 minutes to 2 minutes to avoid drift reset

@vanelsberg
Copy link
Copy Markdown
Contributor

Test without this PR:
image
⚠️Did not trust the high drift values calculated for 08/09 and 09/02: doublecheck my math, but they are correct. Nothing unusual to see in basal rates and.... then I realized I did manual bolus at the time by PEN ("only registering"). To my surprise in AAPS treatments a "register only" bolus is indistinguishable from a normal "meal" type, So I cannot check statistics. No logfiles either as they were already deleted.

As far sa I can remember these manual boluses very likely did amount to 4U. Actual drift values would match expected values?

Test with PR4490:
image
Except for the bolus beeps (which is now fixed) I did not encounter any problems with PR4499. My tests as reported above confirm reported insulin drift for current DASH driver which is effectively fixed with this PR.

Reviewing code for PR4499 with fix I did not see any problems.

@vanelsberg
Copy link
Copy Markdown
Contributor

I just submitted a (final?) update to the PR

Will retest this for a few days.

@olorinmaia
Copy link
Copy Markdown
Contributor

olorinmaia commented Feb 13, 2026

Applied latest commit but so far only 0,05+- difference between pump and AAPS TDD. I think this PR is ready for merge, but will keep monitoring / logging. CC: @MilosKozak

image

@vanelsberg
Copy link
Copy Markdown
Contributor

vanelsberg commented Feb 13, 2026

I think this PR is ready for merge...

100% Agree.

@MilosKozak Ik think this is an important bugfix, affects users especially when on lower TDD (like for kids on DASH)
After merge and testing on dev, we should consider making this a bugfix on 3.4 release?

@xitation
Copy link
Copy Markdown

xitation commented Mar 2, 2026

Hey all,

@nl-ruud rightly pointed out I had forgotten to minus the priming a new pod amount from my values in the previous data posted.

I've fixed this for the data I still have and below is the correct view of my data since cherry picking commit ece3ecc on the 2026/02/27 - 10:30AM.

image

@RSC-RJCgit
Copy link
Copy Markdown

RSC-RJCgit commented Mar 4, 2026

My percent difference ob about 24 units was more like 12% or more, on 1 minute aISF

1st 2 days modified more like 3.5% and 5.5% of 20u tdd
( next 2 days closer to 2.2 %of 19.1 tdd then 15.75 tdd ),much better for 1 minute looping ( really 1 minute bgl, about 2 minute looping set on 1 minute ) than about 15% of 21u pre- fix.

@nl-ruud nl-ruud force-pushed the issue-4158-dev-fix branch 4 times, most recently from be1177d to 6fbf11f Compare March 6, 2026 19:48
@nl-ruud
Copy link
Copy Markdown
Author

nl-ruud commented Mar 6, 2026

To merge this PR into dev, and after discussing with @vanelsberg, I added a semaphore file to enable basal drift compensation. You need an empty file called omnipod_drift_compensation in the extra subfolder of your phone’s AAPS directory for the compensation to be active. Commits are squashed into three parts:

  • Detect basal drift.
  • Automatically compensate basal drift. Logic extended: drift can occur on temp basal cancel, cancel + set, or set without cancel. The last case was previously not considered, which could delay compensation.
  • Make automatic compensation conditional on the semaphore file. This allows merging while enabling broader testing; the guarding can be removed later.

I believe this PR is ready to merge. Please react with 👍 if you agree or 👎 if you have concerns, and share reasoning if you disagree.

Release notes:

  • Basal drift may have (un)intentionally propagated into user profiles; users may need to reassess their profile after updating.
  • Basal deficit compensation is delivered as a 0.05U bolus and will appear as such in the GUI and Watch app.

@xitation
Copy link
Copy Markdown

xitation commented Mar 8, 2026

I know that this PR will help a bunch of people I've had conversations with. Especially in the AutoISF fork/branch community where minor things like this can have a pretty big impact on a system with a lot of power to adjust doses. Also for anyone with little people on low doses this could be a real big issue for them too.

Appreciate you taking the time to put this together @nl-ruud!

From my testing it's been working well for me and I vote it's ready for merge.

@nl-ruud nl-ruud force-pushed the issue-4158-dev-fix branch from 6fbf11f to e7dec05 Compare March 9, 2026 20:46
@nl-ruud
Copy link
Copy Markdown
Author

nl-ruud commented Mar 9, 2026

Rebased onto dev; no conflicts or changes.

@xitation
Copy link
Copy Markdown

Cherry picked the 3 commits into the latest 3.4.1.0 Master and it builds ok.

1st commit name - Quantify the basil drift
git cherry-pick 35189a90b087fa976e6b4f9951e60480bf423f75
2nd commit name - Resolve the basil drift
git cherry-pick 3a19017f8f2d65d27b4efe25e60b91f625ae6ba5
3nd commit name - Added semaphore file for enabling compensation
git cherry-pick 2574f3564ffe549b48c735945424abb97233465

Merging the issue-4158-dev-fix branch into a non Dev Master ended up building a Dev build.

@vanelsberg
Copy link
Copy Markdown
Contributor

vanelsberg commented Mar 10, 2026

For testers:

Merging a PR from Android Studio:
Make sure you are on the latest AAPS dev or release commits. Then from the AS command line run:

  git fetch origin pull/4499/head:dev-PR4499
  git merge dev-PR4499 -m "Merging PR4490"

Merging using AAPS-CI: (preferred)

aaps-ci4499

@vanelsberg
Copy link
Copy Markdown
Contributor

vanelsberg commented Mar 10, 2026

Took some time before I could test (was running Medtrum), my test results on deviation for the past 3 days.
Note that for 10-03-2026 deviation was at 0,10 but was compensated to 0,05 18 mins later (because of a long running 0-TBR. So based on these results I'd say: looks good!

Code review: afaikt no issues
Drift compensation fix: as expected, no issues

Personal evaluation:
This PR fixes an important issue with the DASH driver that has positive impact on users BG regulation while on TBR's
@MilosKozak Recommend merging with dev for broader test coverage.

image

@nl-ruud
Copy link
Copy Markdown
Author

nl-ruud commented Mar 11, 2026

@MilosKozak This PR has been tested over the past couple of months with very positive feedback, everyone agrees it’s ready to merge. The issue it addresses might seem minor at first glance, but the changes make a real difference in practice. It’s now guarded by a semaphore file, allowing broader testing before it becomes the default.

@crimsonkage
Copy link
Copy Markdown

crimsonkage commented Mar 12, 2026

Got another data point that this PR helps.
I helped someone with brittle and their basal is under 10 a day.
Their profile was good, but loop was unstable.
Installed this PR and it Immediately stabilized their loop.
(Except they had their basal too high to compensate for missing basal from bug 10%)

No bugs or issues present.

@BrowserBuildCG
Copy link
Copy Markdown

Working nicely.

@RSC-RJCgit
Copy link
Copy Markdown

Using the newest " basal drift" has smoothed out control heaps.. and using this PR we just got a 100% TIR week for the first time i can remember; (as well .. other factors have helped)

@MilosKozak
Copy link
Copy Markdown
Contributor

PR Review: Omnipod Dash Basal Drift Compensation

Well-motivated PR addressing a real and impactful hardware limitation (~10% TDD under-delivery). The safety guards are reasonable and the semaphore-file gating makes it low-risk for non-opt-in users. Here are my findings:

Strengths

  • Conservative safety design: cooldown timer, drift bounds reset at ±0.10U, skip when TBR=0 (unless recent bolus), skip during activation/suspend/kaput
  • Good refactoring: extracted updatePodState() eliminates duplication between updateFromDefaultStatusResponse and updateFromAlarmStatusResponse
  • Feature-gated behind a semaphore file — off by default, consistent with existing patterns (enable_autotune, etc.)
  • Minimal correction size (0.05U = 1 pulse) — low risk per correction

Concerns

1. integrateExpectedDelivery timezone handling is fragile

The basal program segment boundary calculation manually offsets by podState.timeZoneOffset:

val timeZoneOffset = podState.timeZoneOffset ?: 0
val dayStartLocal = ((startTime + timeZoneOffset) / 86400_000L) * 86400_000L - timeZoneOffset

Meanwhile, BasalProgram.rateAt() uses Calendar.getInstance() (device local time). If the pod timezone and device timezone diverge (which sameTimeZone already tracks as a known issue), the segment boundaries in integrateExpectedDelivery won't match the rates returned by rateAt(). The PR comments show this was already a problem once (commit reverted and re-fixed) — this area warrants extra scrutiny and test coverage.

2. Short overflow risk in pulse arithmetic

basalPulsesDelivered is computed as (total - bolus).toShort(). Kotlin Short wraps at 32767. While typical 3-day pod usage stays well within bounds, bolusPulsesDelivered is incremented cumulatively via calculateBolusPulseIncrease and cast via .toShort() — any intermediate Int arithmetic silently truncated to Short could wrap over many status updates.

3. Correction bolus recorded as BS.Type.NORMAL / BolusType.DEFAULT

The correction bolus is logged as a normal bolus in history. The loop algorithm will correctly account for it in IOB, but there's no way to distinguish basal corrections from user boluses in history/reports. A dedicated bolus type or annotation would improve traceability and debugging.

4. Race condition with bolusDeliveryInProgress

In deliverBasalCorrection(), the shared bolusDeliveryInProgress flag is set, but if a user bolus is requested concurrently via the queue, the interleaving depends on command queue serialization. The deliveryStatus?.bolusDeliveringActive() check helps, but there's a TOCTOU window between the check and the actual bolus command being sent.

5. Drift reset threshold may mask real under-delivery

if (drift >= POD_PULSE_BOLUS_UNITS * 2 || drift <= -POD_PULSE_BOLUS_UNITS * 2)

This resets at ±0.10U, but correction triggers at -0.025U, so the effective correction window is -0.025U to -0.10U. If a single status update shows a jump past -0.10U (e.g., delayed status response), the drift is reset rather than corrected — potentially masking real under-delivery.

6. No unit tests

There are no tests for needsBasalCorrection(), integrateExpectedDelivery(), calculateBolusPulseIncrease(), or the drift calculation logic. These are complex numerical algorithms with edge cases (timezone transitions, pod restarts, concurrent boluses). For code that autonomously delivers insulin, thorough test coverage seems essential.

7. Interface placed in common but only Dash uses it

needsBasalCorrection(), lastBasalCorrectionTime, and basalCorrectionInProgress are added to the OmnipodDashPodStateManager interface in the common module. If Eros isn't planned for this feature, it forces Eros implementations to stub these methods unnecessarily.

Minor nits

  • statusDescription = "BASAL COMPENSATION BOLUS" is all-caps, unlike other command descriptions
  • Inconsistent log precision ("%.3f" vs "%.4f") scattered across drift logging
  • mapIndexed in integrateExpectedDelivery captures index only for logging — could use forEachIndexed + accumulator to avoid intermediate list allocations

Summary

This is a well-designed approach to a real problem. Before merging, I'd recommend:

  1. Add unit tests for the core drift calculation and correction logic
  2. Verify timezone handling with explicit test cases for timezone changes mid-pod
  3. Consider a dedicated bolus type for correction boluses for traceability
  4. Document the ±0.10U reset threshold rationale and whether it should allow larger corrections

@nl-ruud
Copy link
Copy Markdown
Author

nl-ruud commented Mar 18, 2026

@MilosKozak Thanks for your detailed feedback — much appreciated. I’ve reviewed all points carefully. I’ll add testcases (which address points 1 and 6). For the other items, I’ve considered the feedback and decided that the current approach is intentional. I’d appreciate your thoughts on whether this seems reasonable, or if I might be overlooking something.

2. Short overflow risk in pulse arithmetic

I used Short here to stay consistent with totalPulsesDelivered. basalPulsesDelivered and bolusPulsesDelivered are aligned with that type. Even at the maximum value (32767 pulses × 0.05U = 1638.35U), we’re well within pod limits (200U), and both values reset on pod change.

I understand the concern about silent truncation from Int → Short, but since totalPulsesDelivered is already a Short, any overflow would originate there first. From that perspective, keeping these fields as Short is consistent.

Fully eliminating this risk would require changing the underlying type of totalPulsesDelivered, rather than addressing it only here.

3. Correction bolus recorded as BS.Type.NORMAL / BolusType.DEFAULT

I agree with this point. Properly addressing it would require a broader change to the driver internals, so the current implementation is intentional. At the moment, lastBolus carries a BS.Type, but conceptually it should be a BolusType, only converted to BS.Type when communicating back to AAPS.

If you agree with that direction, I’m happy to take it on — but I’d propose handling it in a separate PR to keep this one focused. It would for example also mean a change to the underlying database structure and probably need a data conversion upon upgrade. I will need some help there...

4. Race condition with bolusDeliveryInProgress

I agree this isn’t ideal. I currently see bolusDeliveryInProgress as a workaround. I expect this concern would be resolved as part of addressing point 3, once basal corrections are given their own dedicated BolusType.

5. Drift reset threshold may mask real under-delivery

The primary goal here is to prevent overdelivery at all costs. In uncertain situations, I prefer resetting rather than compensating to avoid potential over-correction. For example, when enabling drift compensation after it has been off for a while, the drift can be large — resetting first prevents overdelivery.

Even if this occasionally means missing a small compensation, the practical impact is negligible.

It’s also worth noting that I haven’t observed this scenario in any real-world test logs. I’m aware the thresholds are set quite tightly, but this was intentional. They can be increased if needed; so far, however, all tests — even with 1-minute loops using AutoISF, where I would have expected to see issues due to high basals and the 2-minute cooldown — have not triggered this scenario.

7. Interface placed in common but only Dash uses it

This file was recently moved to common in preparation for starting work on Omnipod 5 support (#4574). It's even called OmnipodDashPodStateManager.kt - so not that common after all 😀.

Minor nits

My observations:

  • statusDescription follows the ALL-CAPS convention used by other CustomCommands in this codebase (e.g. "SUSPEND DELIVERY" but also “ACCEPT TEMP BASAL" and "BOLUS #.## E").
  • The differing log precision (”%.2f” vs “%.4f”) is intentional — the extra precision helped during debugging to spot small drift effects. I’d be happy to align it for consistency if you feel strongly about it, but my preference would be to keep the extra precision.
  • I used mapIndexed as it felt like the most idiomatic option here. windowed(2) already allocates an intermediate list, and since boundaries is very small, the overhead should be negligible. Alternatives like forEachIndexed + accumulator would add verbosity without much gain — but open to suggestions if you see a cleaner approach.

Summary

Thanks again — this is very helpful feedback. I’ll:
1. Add unit tests for drift calculation and correction logic
2. Update and verify timezone handling with explicit test cases
3. Leave the bolus-type refactor for a follow-up PR
4. Add inline documentation for the ±0.10U reset threshold and rationale

…e for basal drift calculation and correction.
@sonarqubecloud
Copy link
Copy Markdown

@nl-ruud
Copy link
Copy Markdown
Author

nl-ruud commented Mar 20, 2026

@MilosKozak I added unit tests for the drift calculation and correction logic, updated the timezone handling in integrateExpectedDelivery (with accompanying test cases), and documented the 0.10U threshold rationale.

Note: #4574 moved the Dash driver to common. The existing test cases are still under dash, but since common is now the location, I placed the new tests there: testing the new internal methods would otherwise add unnecessary complexity.

@nl-ruud
Copy link
Copy Markdown
Author

nl-ruud commented Mar 29, 2026

For the record: I may have tested the Atlas pods before without realizing it, but I have now explicitly verified that the basal drift is not specific to the SAW pods—it is also present in the Atlas pods.

@nl-ruud
Copy link
Copy Markdown
Author

nl-ruud commented Mar 29, 2026

DST Verification

As there was a DST transition today, I verified the behavior of integrateExpectedDelivery across the change.

Case 1: Phone time (DST jump)

AndroidAPS._2026-03-29_00-00-00_.0:01:52:28.911 [RxCachedThreadScheduler-367] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():276]: integrateExpectedDelivery: period 5.549s
AndroidAPS._2026-03-29_00-00-00_.0:01:52:28.912 [RxCachedThreadScheduler-367] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():336]:   segment 1/1: 5.5489999999999995s @ 0.25U/h = 0,0004U
AndroidAPS._2026-03-29_00-00-00_.0:01:52:28.913 [RxCachedThreadScheduler-367] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():342]:   total integrated delivery: 0,0004U
AndroidAPS._2026-03-29_00-00-00_.0:03:02:23.214 [RxCachedThreadScheduler-367] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():276]: integrateExpectedDelivery: period 594.303s
AndroidAPS._2026-03-29_00-00-00_.0:03:02:23.216 [RxCachedThreadScheduler-367] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():336]:   segment 1/2: 451.08900000000006s @ 0.25U/h = 0,0313U
AndroidAPS._2026-03-29_00-00-00_.0:03:02:23.216 [RxCachedThreadScheduler-367] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():336]:   segment 2/2: 143.214s @ 0.2U/h = 0,0080U
AndroidAPS._2026-03-29_00-00-00_.0:03:02:23.217 [RxCachedThreadScheduler-367] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():342]:   total integrated delivery: 0,0393U

Observation

integrateExpectedDelivery correctly accounts for the DST jump on the phone:

  • Interval 01:52:28.9 → 03:02:23.2 = 594.3s
  • (Expected; incorrect would be ~4194.3s)

It also correctly splits the interval at the basal change at 02:00, applying:

  • 0.25 U/h before
  • 0.2 U/h after

Case 2: Pump time (post-DST sync)

AndroidAPS._2026-03-29_00-00-00_.1:05:24:30.736 [RxCachedThreadScheduler-385] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():276]: integrateExpectedDelivery: period 1198.068s
AndroidAPS._2026-03-29_00-00-00_.1:05:24:30.737 [RxCachedThreadScheduler-385] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():336]:   segment 1/1: 1198.068s @ 0.2U/h = 0,0666U
AndroidAPS._2026-03-29_00-00-00_.1:05:24:30.738 [RxCachedThreadScheduler-385] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():342]:   total integrated delivery: 0,0666U
AndroidAPS._2026-03-29_00-00-00_.1:05:41:13.249 [main] D/PUMPQUEUE: [CommandQueueImplementation.add():209]: Adding: CommandCustomCommand - HANDLE TIME CHANGE
AndroidAPS._2026-03-29_00-00-00_.1:05:41:14.655 [DefaultDispatcher-worker-6] D/PUMPQUEUE: [QueueWorker.doWorkAndLog():140]: performing HANDLE TIME CHANGE
AndroidAPS._2026-03-29_00-00-00_.1:05:41:15.109 [RxCachedThreadScheduler-398] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():276]: integrateExpectedDelivery: period 1004.373s
AndroidAPS._2026-03-29_00-00-00_.1:05:41:15.110 [RxCachedThreadScheduler-398] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():336]:   segment 1/1: 1004.3729999999999s @ 0.2U/h = 0,0558U
AndroidAPS._2026-03-29_00-00-00_.1:05:41:15.111 [RxCachedThreadScheduler-398] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():342]:   total integrated delivery: 0,0558U

Observation

The interval 05:24:30.7 → 05:41:15.1 = 1004.4s is handled correctly, matching real elapsed time.

Case 3: Profile transition after DST

AndroidAPS._2026-03-29_00-00-00_.1:06:07:26.716 [RxCachedThreadScheduler-396] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():276]: integrateExpectedDelivery: period 475.138s
AndroidAPS._2026-03-29_00-00-00_.1:06:07:26.717 [RxCachedThreadScheduler-396] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():336]:   segment 1/2: 28.423s @ 0.0U/h = 0,0000U
AndroidAPS._2026-03-29_00-00-00_.1:06:07:26.717 [RxCachedThreadScheduler-396] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():336]:   segment 2/2: 446.71500000000003s @ 0.0U/h = 0,0000U
AndroidAPS._2026-03-29_00-00-00_.1:06:07:26.718 [RxCachedThreadScheduler-396] D/PUMP: [OmnipodDashPodStateManagerImpl.integrateExpectedDelivery$common_fullRelease():342]:   total integrated delivery: 0,0000U

Observation

The basal profile change occurs at 06:00, which is correct after the DST adjustment (an incorrect implementation would have shifted this to 07:00.)

Conclusion

  • DST handling is correct for both:
    • Phone time
    • Pump time
  • Basal transitions remain aligned with expectations after the DST change

Limitation

There was no basal/profile transition before the pump timezone update in this dataset, so that specific edge case is not covered here.

@MilosKozak MilosKozak changed the base branch from dev to dev3 April 3, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.