Skip to content

DO NOT MERGE: alpha-next test build (MedtrumKit M2 + new LoopKit API, non-Medtrum pumps removed) - #45

Closed
mountrcg wants to merge 47 commits into
devfrom
alpha-next
Closed

mountrcg wants to merge 47 commits into
devfrom
alpha-next

Conversation

@mountrcg

Copy link
Copy Markdown
Owner

Robert's personal test build. Do not merge; opened as a draft only so the unit-test CI runs.

mountrcg and others added 30 commits October 27, 2026 08:52
Any watch transport can hand a parsed command to WatchCommandPipeline, which
runs it at most once per app and request ID, rejects it outside a 2-minute
freshness window (60 s clock lead), and keeps it revocable while it waits:
a revocation epoch plus a feature gate are re-checked at every side-effect
boundary through WatchCommandContext.finalCheck().

- WatchCommandPipeline, WatchCommandContext, WatchCommandAuthorization,
  WatchCommandResult and WatchCommandRejection
- WatchCommandRequestCache keyed by app and request ID, content-checked so a
  reused request ID is a conflict, never evicting a running request
- in_progress acknowledgment code for duplicates of a running request
- content-free error categories for logs
Watch side of the Complication glance: Connect IQ messages go through
GarminMessageRouter, which answers v1 status requests and triggers a
state refresh; nothing else goes through it yet.

Delivery no longer asks the watch for app status before every send. That
round trip cost the watch a BLE exchange per app per broadcast, and its
completions were unreliable enough that a readiness probe built on them
retried around the clock. Instead:

- every app gets one send per broadcast, unless it already confirmed the
  same state; skips are logged per app
- the send result is the readiness signal: success clears what the app was
  owed, AppNotFound marks it absent without a resend, other failures keep
  the single 2 s resend and then leave the state owed
- owed state goes out with the next broadcast, a reconnect or the next
  message from that app; no timers
- only a lost connection gates sends; readiness never does, so a missed
  characteristics callback cannot block delivery

Also routes Connect IQ calls through GarminConnectIQClient so delivery is
testable without a watch, and fixes the stale 20 s fallback comments
(the delay is 10 s).
The Garmin watch app can log carbs, deliver a bolus, or both as a meal, once
the user opts in under Watch > Garmin > Commands (both switches default off;
turning commands off also turns bolus off). Commands run through the
watch-agnostic pipeline; this adds the Garmin side and the treatment rules:

- v1 command envelope with exact payload validation and RFC 4122 request
  IDs; every command is answered with a targeted ack, malformed ones too
  when they name a request ID
- gates re-read at every boundary: the master switch, the bolus switch and
  max carbs; switching either off revokes pending commands from the
  settings setter itself, so off-then-on before the notification drains
  still revokes
- one insulin command at a time: validation against max bolus, max IOB and
  recent boluses since the command was issued; after a watch bolus reached
  the pump, the next one is refused for the 6-minute window
- final checks run inside the carb save transaction and right before
  APSManager.enactBolus, so nothing is written or issued after a revocation
  or expiry
- meal carbs are stored before the bolus; a refused bolus acks
  partial_failure
- the preset reply carries the switches and limits, which is where the
  Garmin app reads them, so the watch only offers what the phone accepts;
  its preset lists stay empty until adjustment commands land. Switch and
  max carbs changes push it to Complication apps best-effort

Shared files touched:
- AppleWatchManager: adds the partial_failure ack code to the
  acknowledgment codes both watch transports share; no Apple Watch
  behavior change
- CarbsStorage: adds storeVerifiedCarbs. storeCarbs swallows save errors,
  so a watch could be told "Carbs logged" with nothing saved; the new call
  throws on failure and runs the final authorization check inside the
  transaction. Existing callers are unchanged.
The Garmin watch app can activate or cancel override and temp target
presets through the same pipeline and gates as treatment commands:

- presets are matched by exact, case-sensitive, unique name, whether or not
  they run; two presets that differ only in whitespace never let the watch
  start the wrong one
- cancelling with nothing active is an idempotent success
- AdjustmentManager gets guarded entry points whose authorize closure runs
  inside the transaction before any read or write, so a command revoked
  while it waits for the adjustment serializer writes nothing
- the preset reply now lists override and temp target presets next to the
  switches and limits; a successful preset command, a preset edit or a
  command setting change pushes it to every Complication app, and a push
  that cannot be delivered stays owed like a state broadcast

Tests cover the adjustment processor cases, revocation while the preset
list loads and while queued behind a busy Core Data context (real
BaseAdjustmentManager, save counter), router parsing and push rules.
With Trio-Complication selected as watchface it is the command app;
with any other watchface Trio-WatchControls is, while watch commands are
on. Only the command app's commands run, and only it gets the preset list.

- Trio-WatchControls is registered, sent state and presets only while it
  is the command app
- ownerMode notice (active, owner) on every change; a missed stand-down
  is repeated when the app next sends anything, a failed activation is
  owed like state
- commands from any other app, including an unregistered command app on
  a selected device, are refused with an ack instead of silence
- a change of command app revokes pending commands
- help text names the command app

Waits for proposed PRs 1-4.
Drop the legacy GarminCommandRouter and WatchCommand processor/cache
stack together with its tests and Xcode project references. The
watch-service pipeline and Garmin command stack merged next replace it.

GarminDevice and GarminWatchState stay: upstream and the new stack's
GarminManager and WatchConfig views still use them.
# Conflicts:
#	Trio.xcodeproj/project.pbxproj
#	Trio/Sources/Assemblies/ServiceAssembly.swift
#	Trio/Sources/Services/WatchManager/AppleWatchManager.swift
# Conflicts:
#	Trio.xcodeproj/project.pbxproj
#	Trio/Sources/Models/WatchMessageKeys.swift
#	Trio/Sources/Services/WatchManager/GarminManager.swift
# Conflicts:
#	Trio.xcodeproj/project.pbxproj
#	Trio/Sources/APS/Storage/CarbsStorage.swift
#	Trio/Sources/Models/GarminWatchSettings.swift
#	Trio/Sources/Models/WatchMessageKeys.swift
#	Trio/Sources/Modules/WatchConfig/View/WatchConfigGarminAppConfigView.swift
#	Trio/Sources/Services/SettingsManager/SettingsManager.swift
#	Trio/Sources/Services/WatchManager/GarminManager.swift
# Conflicts:
#	Trio/Sources/Models/WatchMessageKeys.swift
#	Trio/Sources/Services/Adjustments/AdjustmentManager.swift
# Conflicts:
#	Trio/Sources/Services/WatchManager/GarminManager.swift
Old releases persisted a singular `datafield` key. Decode it as a fallback when the plural `datafields` array is missing or invalid, so existing watch settings keep their selection instead of silently reverting to the default.

Update the watch command fixtures to the plural property and cover the migration: legacy singular, plural priority over legacy, explicit empty selection, invalid values falling back to the default, and an invalid plural falling back to a valid legacy singular.
Replace the plural datafield selection with one configured datafield across
GarminWatchSettings, TrioSettings, the watch config state model and view, and
GarminManager so that registration creates at most one datafield IQApp.

Decoding keeps the plural key working for migration: the first recognized
non-none entry of the stored array wins, an explicitly empty array maps to
none, and otherwise the legacy singular value, the default, or the type
default applies. Malformed or unknown plural values fall back safely.

Tests are updated for the singular model and add coverage for the migration
precedence, including the empty-array and unknown-value cases.
The post-xcodebuild step chained "git fetch --deepen 6 && git log ...", so a
failed fetch dropped the whole commit list from WhatToTest.en-CA.txt. The fetch
recurses into submodules by default and aborts on stale gitlinks from deep
history (MedtrumKit, EversenseKit and CGMBLEKit pins that the configured
upstream no longer serves: "upload-pack: not our ref"), which is exactly what
happened on build 459.

Fetch history without submodule recursion as a best effort and always append
the log afterwards, so the release notes no longer depend on a network-only
step.
The upstream logging rework names its export files "Trio Logs <stamp>.txt"
and, when the new Core Data store first opens, clears leftover plain-text
logs out of Documents/logs. Both carry upstream's own conventions, which do
not fit this fork.

- Export files keep upstream's naming scheme but read "Tai Logs
  2026-10-28 1430.log": Tai's name instead of Trio's, and the .log
  extension in place of .txt. The zip wrapper derives its name from the
  file, so the shared archive follows.
- The legacy sweeper only matched "log.txt", written by the reporter
  upstream retired. Tai's reporter wrote dated "<date>.log" and
  "watch_<date>.log" files instead, so the sweeper collected nothing at all
  on a Tai install and left those files behind for good. It now matches both
  extensions and ages them out after the 8 days of LogRetentionPolicy
  .default rather than a separate 7.
- Zip exports in Documents/logExports were never cleaned up either. The four
  newest are kept and older ones removed, close to the 3 the retired
  reporter held on to.
- Reporting a problem opens an issue on mountrcg/Tai, and the share row
  above it reads "Share Tai Logs".
registerDevices and receivedMessage only checked app.uuid against
GarminWatchControls.appUUID, so Complication never got told it was
active/inactive and had no self-heal path when a notice was dropped -
its glance kept showing stale data as if still active after switching
watchfaces. GarminCommandApp.appUUIDs already unifies both apps
elsewhere in this file; use it here too.

Verified against Trio-Complication's TrioOwnerMode.mc: it already
parses this exact envelope and drives the glance's active/inactive
display, so the Connect IQ side needs no change.
The watch reset bolusAmount, carbsAmount and confirmationProgress every time
the phone became reachable again, which happens on each wrist raise. A user
on the confirm screen who lowered and raised their wrist had their amount
replaced with 0 and confirmed a 0 U bolus. The phone's enactBolus returned
without calling back for amounts of 0 or less, so the watch never got an
answer, and the Processing screen's 5 s fallback never started because
showCommsAnimation was already true when the view appeared.

Watch:
- Reachability changes no longer touch entries in progress; the main view
  still resets them when it appears.
- sendBolusRequest refuses amounts of 0 or less with a visible message.
- Every treatment request shows "iPhone not reachable. Nothing was sent."
  instead of an empty screen when the phone is unreachable.
- WatchState times out a missing answer after 15 s, and on a send error,
  with a message to check Trio on the iPhone (a send error does not prove
  the request was not delivered). The view-level timer is removed.

Phone:
- enactBolus answers every caller with a failure for amounts of 0 or less,
  or amounts that round to 0 on the pump.

Tests:
- Watch: reconnect keeps the bolus entry, 0 U and negative boluses are
  refused, unreachable requests say nothing was sent, the timeout fires and
  is cancelled by an answer, a send error ends processing.
- Phone: enactBolus calls back once with a failure for 0, negative and NaN.
- Bring the watch test target back to compiling: drop assertions on members
  that no longer exist (isBolusCanceled, maxCOB, bolus cancel) and import
  SwiftUI instead of SwiftUICore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An amount entered on the watch stayed on the input and confirm screens for
as long as the app was left open, so an old entry could be confirmed much
later. Entries now expire 2 minutes after their last edit.

- Opening the carbs or bolus input screen, or changing the amount there
  (crown, +/-, a recommendation landing), restarts the entry's clock.
- Confirming with the crown and "Log Carbs" send only an entry edited within
  the last 2 minutes; an older one is cleared and the watch says nothing was
  sent. An entry with no recorded edit is never sent.
- When the app becomes active again, an entry older than 2 minutes is
  cleared and the watch returns to the main screen.
- Returning to the main screen, Cancel and sending end the entry.

Tests cover the 2 minute boundary, refusing expired and unedited entries,
sending fresh ones, carbs-only entries and clearing on return to the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Watch: stop 0 U boluses after a reconnect, time out Processing, expire entries after 2 min
github-actions Bot and others added 17 commits November 11, 2026 04:36
Remove localization strings for CFBundleName and CFBundleDisplayName
* Alert and flag when Tidepool login is rejected

* Drop Tidepool sign-out now TidepoolKit clears it

* Skip Tidepool re-login alert when deleting service
* Include dev version in TestFlight build notes

Builds with a 4-part APP_DEV_VERSION (anything off main) now show it alongside the branch and commit, e.g. "dev 1.0.2.33 abc1234".

* Create TestFlight What to Test note without a manual first entry

fastlane's changelog option only updates What to Test notes that already exist on a build, so apps that never had one entered by hand got no notes. Passing the app's primary locale through localized_build_info makes fastlane create the note.

* Add bundled release notes to TestFlight build notes

Reads BundledReleaseNotes.json from the IPA (written by scripts/capture-release-notes.sh) and appends the newest release's summary, callouts and At A Glance bullets, matching what the in-app sheet shows. Builds without bundled notes keep the branch, version and commit line only.

* Only add release notes to TestFlight notes on main builds

Also fall back to en-US if the primary locale lookup fails, and read the
bundled notes as UTF-8 regardless of the runner's locale.
# Conflicts:
#	.github/FUNDING.yml
#	Trio.xcodeproj/project.pbxproj
#	Trio/Sources/APS/APSManager.swift
#	Trio/Sources/Modules/Home/View/HomeRootView+BottomControls.swift
#	Trio/Sources/Modules/Home/View/MultiUsePanelState.swift
#	TrioTests/CoreDataTests/MultiUsePanelStateTests.swift
#	fastlane/Fastfile
…build

Only Medtrum and the pump simulator stay in the device catalog. The
removed kits have not adopted the async delegate API of the pinned
LoopKit, so they are unlinked from the Trio target, dropped from the
workspace and the Trio scheme's testables, and their test cases trimmed.
@mountrcg

Copy link
Copy Markdown
Owner Author

Aborted per Robert 2026-11-14: staying on MedtrumKit M1 until the ecosystem adopts LoopKit #696/#695 (all native CGM kits also require adoption). Test build no longer needed.

@mountrcg mountrcg closed this Nov 14, 2026
@mountrcg
mountrcg deleted the alpha-next branch November 14, 2026 09:24
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.

6 participants