Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
5c22585
Add a watch-agnostic pipeline for state-changing watch commands
mountrcg Oct 25, 2026
682b088
Serve the Garmin Complication glance and deliver by send result
mountrcg Oct 25, 2026
0335cfb
Add Garmin treatment commands with their safeguard harness
mountrcg Oct 25, 2026
edd7088
Add Garmin adjustment commands
mountrcg Oct 25, 2026
c8ce87d
Let the watchface selection pick the single Garmin command app
mountrcg Oct 27, 2026
212537b
Merge remote-tracking branch 'Tai-public/sync/trio-2026-10-27' into a…
mountrcg Oct 27, 2026
1432b1d
Remove superseded Garmin watch-command implementation
mountrcg Oct 27, 2026
b43e4a7
Merge commit '5c2258502' into alpha
mountrcg Oct 27, 2026
cd0649f
Merge commit '682b0886e' into alpha
mountrcg Oct 27, 2026
8ef61a4
Merge commit '0335cfb09' into alpha
mountrcg Oct 27, 2026
2e9e4dd
Merge commit 'edd7088a9' into alpha
mountrcg Oct 27, 2026
f398f36
Merge commit 'c8ce87db9' into alpha
mountrcg Oct 27, 2026
8ed6485
Migrate Garmin settings decoding to plural datafields
mountrcg Oct 27, 2026
b067372
Restrict Garmin watch config to a single datafield
mountrcg Oct 27, 2026
7ac31f5
Keep TestFlight commit notes when the CI deepen fetch fails
mountrcg Oct 27, 2026
09cb437
Name log exports after Tai and sweep Tai's legacy log files
mountrcg Oct 28, 2026
fd06e1f
Send the command-app ownerMode notice to Complication too
mountrcg Oct 28, 2026
abdfb01
ver + lint
mountrcg Oct 28, 2026
138e2cf
version
mountrcg Oct 28, 2026
d6b40e9
Updated submodule MedtrumKit
mountrcg Oct 29, 2026
1fd6be6
alpha-port
mountrcg Oct 30, 2026
c7d00cc
update medt
mountrcg Oct 30, 2026
6b55b25
Updated submodule MedtrumKit
mountrcg Nov 5, 2026
645f757
ver
mountrcg Nov 5, 2026
cb21621
sub medtrum
mountrcg Nov 7, 2026
83d2a8b
Merge remote-tracking branch 'myTai/sync/trio-2026-11-05' into alpha
mountrcg Nov 7, 2026
d27e2df
fix pbx
mountrcg Nov 7, 2026
d0b62df
Stop the watch from sending a 0 U bolus after the phone reconnects
jwoglom Nov 7, 2026
9a5cac0
Expire watch carbs/bolus entries after 2 minutes
jwoglom Nov 7, 2026
ce86feb
Merge pull request #1751 from jwoglom-osaid/fix/watch-bolus-zero-amount
jwoglom Nov 11, 2026
ade3a11
CI: Bump APP_DEV_VERSION to 1.0.2.67 [skip ci]
github-actions[bot] Nov 11, 2026
1b91221
Updated submodule MedtrumKit
mountrcg Nov 11, 2026
67110b9
Remove localization strings for CFBundleName and CFBundleDisplayName
LiroyvH Nov 11, 2026
a876a48
Merge pull request #1767 from LiroyvH/dev
dnzxy Nov 11, 2026
233c31a
CI: Bump APP_DEV_VERSION to 1.0.2.68 [skip ci]
github-actions[bot] Nov 11, 2026
d1dda09
Alert when Tidepool login is rejected (#1706)
dnzxy Nov 11, 2026
6246f33
CI: Bump APP_DEV_VERSION to 1.0.2.69 [skip ci]
github-actions[bot] Nov 11, 2026
204c43b
Improve automated TestFlight notes for browser build (#1695)
MikePlante1 Nov 12, 2026
1197b51
CI: Bump APP_DEV_VERSION to 1.0.2.70 [skip ci]
github-actions[bot] Nov 12, 2026
86a9b5e
Fix "Sponsor this project" link
lolzivkovic Nov 13, 2026
2c58c64
Merge pull request #1772 from lolzivkovic/fix/funding
MikePlante1 Nov 13, 2026
3f6b238
CI: Bump APP_DEV_VERSION to 1.0.2.71 [skip ci]
github-actions[bot] Nov 13, 2026
9c78b2a
sync(trio): merge upstream/dev at 3f6b238b4
mountrcg Nov 14, 2026
1776681
Merge remote-tracking branch 'myTai/sync/trio-2026-11-14' into alpha
mountrcg Nov 14, 2026
f6933f9
Updated submodule MedtrumKit
mountrcg Nov 14, 2026
cd4165e
Test build: pin LoopKit to sync/trio-next-dev-through-e6b922d7 and Me…
mountrcg Nov 14, 2026
e8cc63e
Test build: remove Omnipod, MiniMed, Dana and RileyLink from the app …
mountrcg Nov 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[submodule "LoopKit"]
path = LoopKit
url = https://github.com/loopandlearn/LoopKit.git
branch = trio-next-dev
url = https://github.com/mountrcg/LoopKit.git
branch = sync/trio-next-dev-through-e6b922d7
[submodule "CGMBLEKit"]
path = CGMBLEKit
url = https://github.com/loopandlearn/CGMBLEKit.git
Expand Down Expand Up @@ -36,8 +36,8 @@
branch = trio-next-dev
[submodule "MedtrumKit"]
path = MedtrumKit
url = https://github.com/loopandlearn/MedtrumKit
branch = trio-next-dev
url = https://github.com/mountrcg/MedtrumKit.git
branch = feat/next-dev-connection-status-activation-ux
[submodule "OmnipodKit"]
path = OmnipodKit
url = https://github.com/loopandlearn/OmnipodKit
Expand Down
4 changes: 2 additions & 2 deletions Config.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ DEVELOPER_TEAM = ##TEAM_ID##
// Typically this is not modified unless you want to create a separate (unique) app using your ID
// It must include $(DEVELOPMENT_TEAM)
// Tai uses .tai at the end instead of .trio
BUNDLE_IDENTIFIER = org.nightscout.$(DEVELOPMENT_TEAM).tai
BUNDLE_IDENTIFIER = org.nightscout.$(DEVELOPMENT_TEAM).trio

// Danger zone - do not modify these unless you know what you are doing

Expand All @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group

// The developers set the version numbers, please leave them alone
APP_VERSION = 1.0.2
APP_DEV_VERSION = 1.0.2.66
APP_DEV_VERSION = 1.0.2.71
APP_BUILD_NUMBER = 1
COPYRIGHT_NOTICE = autoISF 3.01

Expand Down
2 changes: 1 addition & 1 deletion LibreLoop
2 changes: 1 addition & 1 deletion LoopKit
Submodule LoopKit updated 42 files
+1 −2 Documentation/PumpManagerDoseReporting.md
+25 −1 LoopKit.xcodeproj/project.pbxproj
+20 −11 LoopKit/DeviceManager/CGMManager.swift
+5 −4 LoopKit/DeviceManager/DeviceManager.swift
+19 −0 LoopKit/DeviceManager/DeviceManagerPlugin.swift
+31 −27 LoopKit/DeviceManager/PumpManager.swift
+207 −0 LoopKit/DoseMetadataStore.swift
+7 −4 LoopKit/InsulinKit/CachedInsulinDeliveryObject+CoreDataClass.swift
+2 −1 LoopKit/InsulinKit/CachedInsulinDeliveryObject+CoreDataProperties.swift
+27 −10 LoopKit/InsulinKit/DoseEntry.swift
+9 −6 LoopKit/InsulinKit/HKQuantitySample+InsulinKit.swift
+3 −4 LoopKit/InsulinKit/InsulinMath.swift
+4 −2 LoopKit/InsulinKit/PumpEvent+CoreDataClass.swift
+6 −3 LoopKit/InsulinKit/PumpEvent+CoreDataProperties.swift
+1 −1 LoopKit/Persistence/Model.xcdatamodeld/.xccurrentversion
+205 −0 LoopKit/Persistence/Model.xcdatamodeld/Modelv9.xcdatamodel/contents
+8 −0 LoopKit/Service/Remote/RemoteActionDelegate.swift
+25 −51 LoopKit/WeakSynchronizedDelegate.swift
+38 −1 LoopKitHostedTests/CoreDataMigrationTests.swift
+5 −5 LoopKitHostedTests/InsulinDeliveryStoreTests.swift
+23 −0 LoopKitTests/DisplayGlucosePreferenceTests.swift
+64 −4 LoopKitTests/DoseEntryTests.swift
+67 −0 LoopKitTests/DoseMetadataStoreTests.swift
+66 −24 LoopKitTests/DoseStoreTests.swift
+365 −332 LoopKitTests/InsulinMathTests.swift
+1 −1 LoopKitTests/NewPumpEventTests.swift
+3 −3 LoopKitTests/Persistence/CachedInsulinDeliveryObjectTests.swift
+2 −2 LoopKitTests/PumpManagerStatusTests.swift
+66 −59 LoopKitTests/WeakSynchronizedDelegateTests.swift
+12 −2 LoopKitUI/LoopUIPlugin.swift
+3 −0 LoopKitUI/Resources/Localizable.xcstrings
+1 −0 LoopKitUI/ViewModels/DisplayGlucosePreference.swift
+8 −1 LoopKitUI/Views/CheckmarkListItem.swift
+2 −0 LoopKitUI/Views/GuardrailConstraintedQuantityView.swift
+1 −1 LoopTestingKit/DateRelativeBasalEntry.swift
+1 −1 LoopTestingKit/DateRelativeBolusEntry.swift
+13 −28 MockKit/MockCGMManager.swift
+19 −34 MockKit/MockPumpManager.swift
+15 −15 MockKit/UnfinalizedDose.swift
+15 −15 MockKitTests/UnfinalizedDoseTests.swift
+3 −3 MockKitUI/ViewModel/MockPumpManagerSettingsViewModel.swift
+1 −1 MockKitUI/Views/MockPumpManagerSettingsView.swift
2 changes: 1 addition & 1 deletion SyaiKit
3 changes: 2 additions & 1 deletion Testflight/WhatToTest.en-CA.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
based on Trio dev 1.0.0.5
based on Trio dev 1.0.2.71
do M1 medtrum with old Loopkit
13 changes: 2 additions & 11 deletions Trio Watch App Extension/Views/AcknowledgementPendingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,8 @@ struct AcknowledgementPendingView: View {
.navigationBarBackButtonHidden(true)
.toolbar(.hidden)
.background(trioBackgroundColor)
.onChange(of: state.showCommsAnimation) { oldValue, newValue in
if newValue && !oldValue {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
// If after 5 seconds there is still no acknowledgement banner, return to root
if !state.showAcknowledgmentBanner {
// Navigate back to the root
navigationPath.removeLast(navigationPath.count)
}
}
}
}
// WatchState times out a missing answer by showing a banner (beginAwaitingAcknowledgment), so every
// request ends here. A timer in this view never started: the flag is already true when the view appears.
.onChange(of: state.showAcknowledgmentBanner) { _, newValue in
if !newValue {
// Navigate back to the root when acknowledgment banner disappears
Expand Down
15 changes: 3 additions & 12 deletions Trio Watch App Extension/Views/BolusConfirmationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,7 @@ struct BolusConfirmationView: View {
Spacer()

Button("Cancel") {
if state.carbsAmount > 0 {
state.carbsAmount = 0 // reset carbs in state
}
bolusAmount = 0 // reset bolus in state
confirmationProgress = 0 // reset auth progress
state.clearEntry()
navigationPath.removeLast(navigationPath.count)
}
.buttonStyle(.bordered)
Expand All @@ -80,13 +76,8 @@ struct BolusConfirmationView: View {
WKInterfaceDevice.current().play(.success)

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if state.carbsAmount > 0 {
state.sendCarbsRequest(state.carbsAmount, Date())
state.carbsAmount = 0 // reset carbs in state
}
state.sendBolusRequest(Decimal(bolusAmount))
bolusAmount = 0 // reset bolus in state
confirmationProgress = 0 // reset auth progress
// Sends carbs and bolus, or refuses an entry older than 2 min; either way the entry is cleared
state.sendConfirmedEntry(bolusAmount: bolusAmount)
navigationPath.append(NavigationDestinations.acknowledgmentPending)
}
} else if newValue > 0 {
Expand Down
6 changes: 4 additions & 2 deletions Trio Watch App Extension/Views/BolusInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,7 @@ struct BolusInputView: View {

Button(actionButtonLabel) {
if isCarbsOnly {
state.sendCarbsRequest(state.carbsAmount)
state.carbsAmount = 0 // reset carbs in state
state.sendCarbsEntry(state.carbsAmount)
navigationPath.append(NavigationDestinations.acknowledgmentPending)
} else {
state.bolusAmount = min(bolusAmount, effectiveBolusLimit)
Expand Down Expand Up @@ -156,7 +155,10 @@ struct BolusInputView: View {
.clipShape(Circle())
}
}
// The entry's 2 min clock restarts whenever the amount changes, including a recommendation landing
.onChange(of: bolusAmount) { state.markEntryEdited() }
.onAppear {
state.markEntryEdited()
// Set initial bolus amount to recommended value
// Only do this if user has not updated amount previously, e.g., when navigating to next and then back to this view
if bolusAmount == 0 {
Expand Down
5 changes: 4 additions & 1 deletion Trio Watch App Extension/Views/CarbsInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ struct CarbsInputView: View {
state.carbsAmount = Int(min(carbsAmount, effectiveCarbsLimit))
navigationPath.append(NavigationDestinations.bolusInput)
} else {
state.sendCarbsRequest(Int(min(carbsAmount, effectiveCarbsLimit)))
state.sendCarbsEntry(Int(min(carbsAmount, effectiveCarbsLimit)))
navigationPath.append(NavigationDestinations.acknowledgmentPending)
}
}
Expand All @@ -103,6 +103,9 @@ struct CarbsInputView: View {
.disabled(!(carbsAmount > 0.0) || carbsAmount > effectiveCarbsLimit)
}
.background(trioBackgroundColor)
// The entry's 2 min clock restarts whenever the user touches it
.onAppear { state.markEntryEdited() }
.onChange(of: carbsAmount) { state.markEntryEdited() }
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Image("premeal")
Expand Down
10 changes: 10 additions & 0 deletions Trio Watch App Extension/Views/TrioMainWatchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI
import WatchKit

struct TrioMainWatchView: View {
@Environment(\.scenePhase) private var scenePhase
@State private var state = WatchState()

// misc
Expand Down Expand Up @@ -233,6 +234,15 @@ struct TrioMainWatchView: View {
if newPath.isEmpty {
// Reset conditional view navigation when returning to root view
continueToBolus = false
// Back on the main screen, so no entry is in progress
state.clearEntry()
}
}
.onChange(of: scenePhase) { _, newPhase in
// An entry left for more than 2 min (wrist down, app in the background) is
// cleared and the input screens are closed, so a stale amount can't be confirmed
if newPhase == .active, state.expireStaleEntry() {
navigationPath = NavigationPath()
}
}
}
Expand Down
164 changes: 88 additions & 76 deletions Trio Watch App Extension/WatchState+Requests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,138 +4,150 @@ import WatchConnectivity
// MARK: - Send Data to Phone

extension WatchState {
/// Sends the entry confirmed with the crown: its carbs, if any, then the bolus.
/// Refuses an entry older than `entryLifetime` and clears it instead.
func sendConfirmedEntry(bolusAmount amount: Double, at now: Date = Date()) {
guard refuseIfExpired(at: now) == false else { return }

let carbs = carbsAmount
clearEntry()
if carbs > 0 {
sendCarbsRequest(carbs, now)
}
sendBolusRequest(Decimal(amount))
}

/// Sends a carbs-only entry ("Log Carbs"). Refuses an entry older than `entryLifetime` and clears it instead.
func sendCarbsEntry(_ amount: Int, at now: Date = Date()) {
guard refuseIfExpired(at: now) == false else { return }

clearEntry()
sendCarbsRequest(amount, now)
}

private func refuseIfExpired(at now: Date) -> Bool {
guard !isEntryFresh(at: now) else { return false }
WatchLogger.shared.warning(.treatments, "Entry older than \(Int(Self.entryLifetime / 60)) min not sent")
clearEntry()
handleRequestNotSent(Self.entryExpiredMessage)
return true
}

/// Sends a bolus insulin request to the paired iPhone
/// - Parameters:
/// - amount: The insulin amount to be delivered
func sendBolusRequest(_ amount: Decimal) {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.bolus, "Bolus request not sent, phone unreachable")
guard amount > 0 else {
WatchLogger.shared.warning(.bolus, "Bolus of \(amount) U not sent, amount must be above 0")
handleRequestNotSent(Self.invalidBolusAmountMessage)
return
}

WatchLogger.shared.info(.bolus, "Bolus of \(amount) U requested")

let message: [String: Any] = [
WatchMessageKeys.bolus: amount
]
guard let session = reachableSession(.bolus, notSentLog: "Bolus request not sent, phone unreachable") else { return }

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.bolus, "Sending bolus request failed", error: error)
}
WatchLogger.shared.info(.bolus, "Bolus of \(amount) U requested")

// Display pending communication animation
showCommsAnimation = true
send([WatchMessageKeys.bolus: amount], on: session, .bolus, failureLog: "Sending bolus request failed")
}

/// Sends a carbohydrate entry request to the paired iPhone
/// - Parameters:
/// - amount: The amount of carbs in grams
/// - date: The timestamp for the carb entry (defaults to current time)
func sendCarbsRequest(_ amount: Int, _ date: Date = Date()) {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.treatments, "Carbs request not sent, phone unreachable")
return
}
guard let session = reachableSession(.treatments, notSentLog: "Carbs request not sent, phone unreachable") else { return }

WatchLogger.shared.info(.treatments, "Carbs of \(amount) g requested for \(date)")

let message: [String: Any] = [
WatchMessageKeys.carbs: amount,
WatchMessageKeys.date: date.timeIntervalSince1970
]

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.treatments, "Sending carbs request failed", error: error)
}

// Display pending communication animation
showCommsAnimation = true
send(message, on: session, .treatments, failureLog: "Sending carbs request failed")
}

/// Sends a request to cancel the current override preset to the paired iPhone
func sendCancelOverrideRequest() {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.treatments, "Override cancel not sent, phone unreachable")
return
}
guard let session = reachableSession(.treatments, notSentLog: "Override cancel not sent, phone unreachable")
else { return }

WatchLogger.shared.info(.treatments, "Override cancel requested")

let message: [String: Any] = [
WatchMessageKeys.cancelOverride: true
]

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.treatments, "Sending override cancel failed", error: error)
}

// Display pending communication animation
showCommsAnimation = true
send([WatchMessageKeys.cancelOverride: true], on: session, .treatments, failureLog: "Sending override cancel failed")
}

/// Sends a request to activate an override preset to the paired iPhone
/// - Parameter presetName: The name of the override preset to activate
func sendActivateOverrideRequest(presetName: String) {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.treatments, "Override request not sent, phone unreachable")
return
}
guard let session = reachableSession(.treatments, notSentLog: "Override request not sent, phone unreachable")
else { return }

WatchLogger.shared.info(.treatments, "Override preset \(presetName) requested")

let message: [String: Any] = [
WatchMessageKeys.activateOverride: presetName
]

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.treatments, "Sending override request failed", error: error)
}

// Display pending communication animation
showCommsAnimation = true
send(
[WatchMessageKeys.activateOverride: presetName],
on: session,
.treatments,
failureLog: "Sending override request failed"
)
}

/// Sends a request to cancel the current temporary target to the paired iPhone
func sendCancelTempTargetRequest() {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.treatments, "Temp target cancel not sent, phone unreachable")
return
}
guard let session = reachableSession(.treatments, notSentLog: "Temp target cancel not sent, phone unreachable")
else { return }

WatchLogger.shared.info(.treatments, "Temp target cancel requested")

let message: [String: Any] = [
WatchMessageKeys.cancelTempTarget: true
]

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.treatments, "Sending temp target cancel failed", error: error)
}

// Display pending communication animation
showCommsAnimation = true
send(
[WatchMessageKeys.cancelTempTarget: true],
on: session,
.treatments,
failureLog: "Sending temp target cancel failed"
)
}

/// Sends a request to activate a temporary target preset to the paired iPhone
/// - Parameter presetName: The name of the temporary target preset to activate
func sendActivateTempTargetRequest(presetName: String) {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.treatments, "Temp target request not sent, phone unreachable")
return
}
guard let session = reachableSession(.treatments, notSentLog: "Temp target request not sent, phone unreachable")
else { return }

WatchLogger.shared.info(.treatments, "Temp target preset \(presetName) requested")

let message: [String: Any] = [
WatchMessageKeys.activateTempTarget: presetName
]
send(
[WatchMessageKeys.activateTempTarget: presetName],
on: session,
.treatments,
failureLog: "Sending temp target request failed"
)
}

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.treatments, "Sending temp target request failed", error: error)
/// The session when the phone can take a request; otherwise tells the user nothing was sent
private func reachableSession(_ category: WatchLogEntry.Category, notSentLog: String) -> WCSession? {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(category, notSentLog)
handleRequestNotSent(Self.phoneUnreachableMessage)
return nil
}
return session
}

/// Sends a treatment request and waits for the phone's acknowledgment
private func send(
_ message: [String: Any],
on session: WCSession,
_ category: WatchLogEntry.Category,
failureLog: String
) {
beginAwaitingAcknowledgment()

// Display pending communication animation
showCommsAnimation = true
session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(category, failureLog, error: error)
DispatchQueue.main.async {
self.handleMissingAcknowledgment()
}
}
}

/// Sends a request to calculate a bolus recommendation based on the current carbs amount
Expand Down
Loading
Loading