Skip to content

Heartbeat: pull must-provide from the delegate, not just the app's push - #121

Open
loopkitdev wants to merge 2 commits into
loopandlearn:ble-heartbeatfrom
loopkitdev:heartbeat-delegate-pull
Open

Heartbeat: pull must-provide from the delegate, not just the app's push#121
loopkitdev wants to merge 2 commits into
loopandlearn:ble-heartbeatfrom
loopkitdev:heartbeat-delegate-pull

Conversation

@loopkitdev

Copy link
Copy Markdown

Problem

A Trio field log showed an ~8-hour locked-looping outage on an O5 pod with a network CGM. On every background, OmnipodKit disconnected the pod but never armed the StartDelay heartbeat probe, so nothing woke the app. Root cause: heartbeatEnabled was never set — OmnipodKit only ever received the preference the host pushes (setMustProvideBLEHeartbeat / setBLEHeartbeatRequest), and Trio doesn't push it. OmnipodKit never queried the host's pumpManagerMustProvideBLEHeartbeat delegate.

That delegate is the classic LoopKit pull contract. MinimedKit (the reference RileyLink pump) drives its heartbeat entirely from it (delegate?.pumpManagerMustProvideBLEHeartbeat(self) == true). OmnipodKit was the outlier — pull-blind — so any host that relies on the pull instead of pushing got no pump heartbeat at all.

Fix — the delegate is the single source of truth

  • updateBLEHeartbeatPreference() resolves must-provide via pumpDelegate.call { $0?.pumpManagerMustProvideBLEHeartbeat(self) == true } (mirroring MinimedKit) and drives the BLE heartbeat / RileyLink timer. It is the only place the enable bit is set.
  • setMustProvideBLEHeartbeat and setBLEHeartbeatRequest no longer set enable themselves — they call updateBLEHeartbeatPreference(). setBLEHeartbeatRequest additionally records the CGM reading schedule (new setHeartbeatSchedule) purely for cadence alignment. So push and pull can never disagree — the push no longer decides enable, only when the wake lands.
  • Also called on pod connect, so the heartbeat engages for a host that never pushes (Trio).
  • BLE-side setHeartbeatRequest is split into setHeartbeatEnabled (enable, from the delegate) and setHeartbeatSchedule (cadence, from the push).

Heartbeat still runs only when the host says the CGM can't self-wake (network CGM); a local CGM resolves to false and gets no probe.

Testing

Built + OmniTests on the dev line (dev LoopKit + ble-heartbeat): 189 tests, 0 failures. A [heartbeat] … mustProvideBLEHeartbeat(delegate)=<bool> line is logged on each evaluation, so a Trio run will confirm the delegate returns true for its network CGM.

Builds on #117 (merged).

OmnipodKit only ever received the heartbeat preference (setMustProvideBLEHeartbeat
/ setBLEHeartbeatRequest) pushed in by the host; it never queried the host's
pumpManagerMustProvideBLEHeartbeat delegate. So a host that relies on the classic
LoopKit pull instead of pushing (e.g. Trio) never turned the heartbeat on: the
StartDelay probe was never armed on background, and with a network CGM nothing
woke the app — an 8-hour outage in a Trio field log. MinimedKit, the reference
RileyLink pump, drives its heartbeat entirely from this delegate; OmnipodKit was
the outlier.

Make the delegate the single source of truth for enable/disable:

- updateBLEHeartbeatPreference() resolves must-provide via
  pumpDelegate.call { $0?.pumpManagerMustProvideBLEHeartbeat(self) == true }
  (mirroring MinimedKit) and drives the BLE heartbeat / RileyLink timer.
- setMustProvideBLEHeartbeat and setBLEHeartbeatRequest no longer set the enable
  bit themselves — they call updateBLEHeartbeatPreference(). setBLEHeartbeatRequest
  additionally records the CGM reading SCHEDULE (via setHeartbeatSchedule) purely
  for cadence alignment. So the push and the pull can never hold conflicting enable
  state — the push no longer decides enable, only when the wake lands.
- Also called on pod connect, so the heartbeat engages even for a host that never
  pushes.

The BLE-side setHeartbeatRequest is split into setHeartbeatEnabled (enable, from the
delegate) and setHeartbeatSchedule (cadence, from the push). Heartbeat still only
runs when the host says the CGM can't self-wake (network CGM); a local CGM resolves
to false and gets no probe.

OmniTests: 189 tests, 0 failures.
@marionbarker

marionbarker commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Test Configuration

Set up a few configurations. Record them as they are created. Later come back for data harvesting.

Edited to put separate tests configurations in individual comments.

This comment has configuration for Trio.

Trio test

This test used a Trio instance already under test that was not looping without a modification in either Trio or OmnipodKit.
See #118 (comment) for the test results of applying just OmnipodKit #121 changes. (The Trio only tests were also successful.)

  • ✅ successful looping Omnipod 5, Nightscout as CGM while phone is locked

@marionbarker

Copy link
Copy Markdown
Contributor

Issue with this PR

❌ the build hangs for all versions other than Trio (and yes I confirmed PR 121 does work with Trio both with and without the timing modification mentioned in PR 118.)

  • For iAPS, Loop and Loop next-dev, all hang using PR 121 code and build without errors using ble-heartbeat branch

Narrative

We have a test phone running the ble-heartbeat method with iAPS with an Omnipod 5 Pod, so I thought I would try it. I ran into a hang using loopkitdev:heartbeat-delegate-pull with iAPS.

Then I tried LoopWorkspace update_dev_to_3.14.4 with the PR and got the same hang.

Finally I repeated an attempt with LoopWorkspace next-dev: I used OmnipodKit next-dev branch with a cherry-pick of the commit for this PR and got the same hang.

iAPS hang screenshot

use iAPS dev branch, point to LoopKit/LoopKit:dev and loopdevkit/OmnipodKit:heartbeat-delegate-pull

iAPS hang with omnipodkit pr 121

Loop dev backport screenshot

use LoopWorkspace update_dev_3.14.4, commit 8af5ca0, use loopdevkit/OmnipodKit:heartbeat-delegate-pull

Loop dev backport hang pr 121

Loop next-dev screenshot

use LoopWorkspace next-dev, commit 495a2db, use loopandlearn/OmnipodKit:next-dev with git cherry-pick 43cf8f0 from heartbeat-delegate-pull

Loop next-dev hang pr 121

updateBLEHeartbeatPreference resolved the must-provide answer with a synchronous
pumpDelegate.call, which does _queue.sync on the delegate's queue. The push entry
points (setMustProvideBLEHeartbeat / setBLEHeartbeatRequest) are invoked by the
host on that same delegate queue (Loop, iAPS), so the sync deadlocked and hung the
app. Trio was unaffected only because it doesn't push — it engages the heartbeat
purely via the on-connect pull, which runs on the BLE queue.

Hop the delegate query onto a private serial queue before calling pumpDelegate.call,
so it can never run on the delegate queue regardless of the caller. state is Locked,
so reading it off-queue is safe.

Field report: loopandlearn#121 — hang on Loop / iAPS / Loop next-dev,
fine on Trio.
@marionbarker

marionbarker commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Test Build

✅ Using the latest version of PR 121, all three builds that were building onto the phone and then hanging yesterday, are successful - no hang. But none of these were tested with a pod.

  • EDITED because the ❌ Loop next-dev version is not opening on the test phone, claims it installed and is running, but main screen is not visible

✅ Confirm the Trio build still works. (This is the Trio build with the 2 commits @dnzxy added yesterday)

  • For this case, since there is an Omnipod 5 Pod connected, observe on Nightscout URL to see if it continues to work while locked = see Signal Loss below
  • ❌ repeated signal loss for Trio and crashes on version with an Omnipod 5 - see Narrative including an ips file
  • Back up one commit for OmnipodKit (only the first commit of this PR) and Trio is ok with Omnipod 5 pod

Build the same Trio version (with both commits from PR 121) onto SE 2nd gen, iOS 18 phone without a pod connected and test with rPi DASH.

  • Pair a dash pod and had to go through a quit/restart to get the insert to finish
  • Got an Empty Value when trying to deactivate the pod
  • Did a quit/restart of rPi and then could deactivate
  • Repeat the Pairing with rPi DASH - get possible bluetooth issues
  • restart Trio and it paired
  • had trouble again with cannula insertion (required rPi and Trio restarts) - got empty value again
  • one more rPi quit and restart and finally goes through
  • log file for this test phone (SE 2nd gen, iOS 18, Eversense CGM, DASH rPi)

Narrative: Signal Loss for Trio test

Test Phone: SE 3rd gen running iOS 26, Omnipod 5 pod, Nightscout as a CGM

There was a signal loss of the pod that occurred around the time of the rebuild for the Trio phone.

  • Confirmed that I have all the correct SHA for Trio
    • rebuild, cycle bluetooth, quit and restart
    • communication restored
    • 11:28 lock phone
  • ❌ observe if it is looping while locked
    • 11:38 cgm 1 m, pump 10 m
    • 11:44 cgm 1 m, pump 16 m
  • unlock phone , Trio was not operating, signal loss again

Try to restore communications:

  • 11:47 cycle bluetooth
  • 11:47 build fresh onto phone
    • Read pod status and comms are restored
  • close the Trio workspace in Xcode, reopen Trio
  • watch phone and URL while unlocked
    • 11:50 cgm 3 m, pump 22 m
    • 11:54 cgm 2 m, pump 26
    • wait 5 more minutes then manually trigger a loop
    • 11:59 cgm 1m, pump 31 m
    • the app had frozen - quit and restart

ips file, SE 3rd gen Trio

there were 2 ips file, post the most recent:

Back up the OmnipodKit by checking out the previous commit, (changed from 02ff824, to 43cf8f0)

  • 12:04 rebuild
  • looping again while locked

Trio log file, SE 3rd gen Trio

20260731_1205_Trio_crashing_log.txt.zip

@marionbarker

Copy link
Copy Markdown
Contributor

Test Build and rPi for Loop dev backport

The issue reported for the Trio build and the rPi DASH in previous comment, is repeated in the Loop dev test below.

Start with update_dev_to_3.14.4, commit 8af5ca0

  • Loop, point to LoopKit dev (new SHA same code)
  • OmnipodKit, point to loopkitdev:heartbeat-delegate-pull (this PR, both commits)

Build onto SE 2nd gen running iOS 18

  • pair rPi DASH pod
  • also have the Empty value
  • had to restart rPi to finish insertion and it hung again
  • Restarted rPi again and it competed

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.

3 participants