Procházet zdrojové kódy

Merge branch 'dev' into feat/dev-eversense

Deniz Cengiz před 9 hodinami
rodič
revize
24e2d01a70

+ 4 - 1
.gitmodules

@@ -34,6 +34,9 @@
 [submodule "MedtrumKit"]
 	path = MedtrumKit
 	url = https://github.com/loopandlearn/MedtrumKit
+[submodule "OmnipodKit"]
+	path = OmnipodKit
+	url = https://github.com/loopandlearn/OmnipodKit
 [submodule "EversenseKit"]
 	path = EversenseKit
-	url = https://github.com/loopandlearn/EversenseKit
+	url = https://github.com/loopandlearn/EversenseKit

+ 2 - 2
Config.xcconfig

@@ -18,8 +18,8 @@ BUNDLE_IDENTIFIER = org.nightscout.$(DEVELOPMENT_TEAM).trio
 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 = 0.8.0
-APP_DEV_VERSION = 0.8.0
+APP_VERSION = 0.8.1
+APP_DEV_VERSION = 0.8.1.1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 1 - 1
Gemfile

@@ -1,5 +1,5 @@
 source "https://rubygems.org"
 
-gem "fastlane", "2.233.1"
+gem "fastlane", "2.235.0"
 gem "json", ">=2.19.2"
 gem "addressable", ">=2.9.0"

+ 51 - 46
Gemfile.lock

@@ -8,8 +8,8 @@ GEM
     artifactory (3.0.17)
     atomos (0.1.3)
     aws-eventstream (1.4.0)
-    aws-partitions (1.1206.0)
-    aws-sdk-core (3.241.4)
+    aws-partitions (1.1254.0)
+    aws-sdk-core (3.250.0)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
@@ -17,19 +17,19 @@ GEM
       bigdecimal
       jmespath (~> 1, >= 1.6.1)
       logger
-    aws-sdk-kms (1.121.0)
-      aws-sdk-core (~> 3, >= 3.241.4)
+    aws-sdk-kms (1.128.0)
+      aws-sdk-core (~> 3, >= 3.248.0)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.211.0)
-      aws-sdk-core (~> 3, >= 3.241.3)
+    aws-sdk-s3 (1.224.0)
+      aws-sdk-core (~> 3, >= 3.248.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
     aws-sigv4 (1.12.1)
       aws-eventstream (~> 1, >= 1.0.2)
     babosa (1.0.4)
-    base64 (0.2.0)
+    base64 (0.3.0)
     benchmark (0.5.0)
-    bigdecimal (4.0.1)
+    bigdecimal (4.1.2)
     claide (1.1.0)
     colored (1.2)
     colored2 (3.1.2)
@@ -71,17 +71,17 @@ GEM
     faraday-retry (1.0.4)
     faraday_middleware (1.2.1)
       faraday (~> 1.0)
-    fastimage (2.4.0)
-    fastlane (2.233.1)
-      CFPropertyList (>= 2.3, < 4.0.0)
-      abbrev (~> 0.1.2)
+    fastimage (2.4.1)
+    fastlane (2.235.0)
+      CFPropertyList (>= 2.3, < 5.0.0)
+      abbrev (~> 0.1)
       addressable (>= 2.8, < 3.0.0)
       artifactory (~> 3.0)
       aws-sdk-s3 (~> 1.197)
       babosa (>= 1.0.3, < 2.0.0)
-      base64 (~> 0.2.0)
+      base64 (~> 0.2)
       benchmark (>= 0.1.0)
-      bundler (>= 1.17.3, < 5.0.0)
+      bundler (>= 2.4.0, < 5.0.0)
       colored (~> 1.2)
       commander (~> 4.6)
       csv (~> 3.3)
@@ -96,18 +96,18 @@ GEM
       gh_inspector (>= 1.1.2, < 2.0.0)
       google-apis-androidpublisher_v3 (~> 0.3)
       google-apis-playcustomapp_v1 (~> 0.1)
-      google-cloud-env (>= 1.6.0, <= 2.1.1)
+      google-cloud-env (>= 1.6.0, < 2.3.0)
       google-cloud-storage (~> 1.31)
       highline (~> 2.0)
       http-cookie (~> 1.0.5)
       json (< 3.0.0)
-      jwt (>= 2.1.0, < 3)
+      jwt (>= 2.1.0, < 4)
       logger (>= 1.6, < 2.0)
       mini_magick (>= 4.9.4, < 5.0.0)
       multipart-post (>= 2.0.0, < 3.0.0)
-      mutex_m (~> 0.3.0)
+      mutex_m (~> 0.3)
       naturally (~> 2.2)
-      nkf (~> 0.2.0)
+      nkf (~> 0.2)
       optparse (>= 0.1.1, < 1.0.0)
       ostruct (>= 0.1.0)
       plist (>= 3.1.0, < 4.0.0)
@@ -124,39 +124,44 @@ GEM
       xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
     fastlane-sirp (1.1.0)
     gh_inspector (1.1.3)
-    google-apis-androidpublisher_v3 (0.54.0)
-      google-apis-core (>= 0.11.0, < 2.a)
-    google-apis-core (0.11.3)
+    google-apis-androidpublisher_v3 (0.101.0)
+      google-apis-core (>= 0.15.0, < 2.a)
+    google-apis-core (0.18.0)
       addressable (~> 2.5, >= 2.5.1)
-      googleauth (>= 0.16.2, < 2.a)
-      httpclient (>= 2.8.1, < 3.a)
+      googleauth (~> 1.9)
+      httpclient (>= 2.8.3, < 3.a)
       mini_mime (~> 1.0)
+      mutex_m
       representable (~> 3.0)
       retriable (>= 2.0, < 4.a)
-      rexml
-    google-apis-iamcredentials_v1 (0.17.0)
-      google-apis-core (>= 0.11.0, < 2.a)
-    google-apis-playcustomapp_v1 (0.13.0)
-      google-apis-core (>= 0.11.0, < 2.a)
-    google-apis-storage_v1 (0.31.0)
-      google-apis-core (>= 0.11.0, < 2.a)
+    google-apis-iamcredentials_v1 (0.27.0)
+      google-apis-core (>= 0.15.0, < 2.a)
+    google-apis-playcustomapp_v1 (0.17.0)
+      google-apis-core (>= 0.15.0, < 2.a)
+    google-apis-storage_v1 (0.62.0)
+      google-apis-core (>= 0.15.0, < 2.a)
     google-cloud-core (1.8.0)
       google-cloud-env (>= 1.0, < 3.a)
       google-cloud-errors (~> 1.0)
-    google-cloud-env (1.6.0)
-      faraday (>= 0.17.3, < 3.0)
-    google-cloud-errors (1.5.0)
-    google-cloud-storage (1.47.0)
+    google-cloud-env (2.2.2)
+      base64 (~> 0.2)
+      faraday (>= 1.0, < 3.a)
+    google-cloud-errors (1.6.0)
+    google-cloud-storage (1.60.0)
       addressable (~> 2.8)
       digest-crc (~> 0.4)
-      google-apis-iamcredentials_v1 (~> 0.1)
-      google-apis-storage_v1 (~> 0.31.0)
+      google-apis-core (>= 0.18, < 2)
+      google-apis-iamcredentials_v1 (~> 0.18)
+      google-apis-storage_v1 (>= 0.42)
       google-cloud-core (~> 1.6)
-      googleauth (>= 0.16.2, < 2.a)
+      googleauth (~> 1.9)
       mini_mime (~> 1.0)
-    googleauth (1.8.1)
-      faraday (>= 0.17.3, < 3.a)
-      jwt (>= 1.4, < 3.0)
+    google-logging-utils (0.2.0)
+    googleauth (1.16.2)
+      faraday (>= 1.0, < 3.a)
+      google-cloud-env (~> 2.2)
+      google-logging-utils (~> 0.1)
+      jwt (>= 1.4, < 4.0)
       multi_json (~> 1.11)
       os (>= 0.9, < 2.0)
       signet (>= 0.16, < 2.a)
@@ -166,13 +171,13 @@ GEM
     httpclient (2.9.0)
       mutex_m
     jmespath (1.6.2)
-    json (2.19.4)
-    jwt (2.10.3)
+    json (2.19.7)
+    jwt (3.2.0)
       base64
     logger (1.7.0)
     mini_magick (4.13.2)
     mini_mime (1.1.5)
-    multi_json (1.19.1)
+    multi_json (1.21.1)
     multipart-post (2.4.1)
     mutex_m (0.3.0)
     nanaimo (0.4.0)
@@ -183,12 +188,12 @@ GEM
     ostruct (0.6.3)
     plist (3.7.2)
     public_suffix (7.0.5)
-    rake (13.3.1)
+    rake (13.4.2)
     representable (3.2.0)
       declarative (< 0.1.0)
       trailblazer-option (>= 0.1.1, < 0.2.0)
       uber (< 0.2.0)
-    retriable (3.1.2)
+    retriable (3.8.0)
     rexml (3.4.4)
     rouge (3.28.0)
     ruby2_keywords (0.0.5)
@@ -231,7 +236,7 @@ PLATFORMS
 
 DEPENDENCIES
   addressable (>= 2.9.0)
-  fastlane (= 2.233.1)
+  fastlane (= 2.235.0)
   json (>= 2.19.2)
 
 BUNDLED WITH

+ 1 - 0
OmnipodKit

@@ -0,0 +1 @@
+Subproject commit adf210d20bcfc05f8119e8665d8f5904db828e03

+ 7 - 2
PRIVACY_POLICY.md

@@ -73,6 +73,11 @@ The following information is included in the telemetry payload:
   on/off, Live Activity enabled, calendar integration enabled
 - A rolling 7-day count of how often the app was cold-launched
 - The commit SHAs of pinned submodules (e.g. LoopKit, OmniBLE)
+- The device's system locale (e.g. "en_US") — used to help Trio
+  developers understand which languages to prioritize for translation
+- The device's time zone identifier (e.g. "America/New_York") — used
+  to help Trio developers understand which regions of the world Trio
+  is being used in
 
 The payload sends once every 24 hours while the app is running, plus
 once after a new build is installed. Sending failures simply retry on
@@ -85,7 +90,7 @@ the next launch or scheduler tick — there is no continued retry.
 - Your Nightscout URL or API token
 - Your Tidepool email, password, or session token
 - Remote-command secrets or APNS keys
-- Time zone or location
+- GPS coordinates or any precise location data
 - App logs — log sharing remains a separate, user-initiated flow under Settings
 
 ### Debug Symbols (dSYMs)
@@ -177,4 +182,4 @@ trio.diy.diabetes@gmail.com.
 
 ## Last Updated
 
-May 14, 2025
+May 28, 2026

+ 6 - 0
Trio.xcodeproj/project.pbxproj

@@ -352,6 +352,8 @@
 		41740E936552456AAC0EDAC3 /* SettingsSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */; };
 		A33352ED40476125EBAC6EE0 /* CarbRatioEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E22146D3DF4853786C78132 /* CarbRatioEditorDataFlow.swift */; };
 		AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */; };
+		B6E925132EB3932A0076D719 /* OmnipodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6E925122EB3932A0076D719 /* OmnipodKit.framework */; };
+		B6E925142EB3932A0076D719 /* OmnipodKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B6E925122EB3932A0076D719 /* OmnipodKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		B7C465E9472624D8A2BE2A6A /* (null) in Sources */ = {isa = PBXBuildFile; };
 		B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = B958F1B62BA0711600484851 /* MKRingProgressView */; };
 		BA00D96F7B2FF169A06FB530 /* CGMSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */; };
@@ -845,6 +847,7 @@
 				3B4BA78B2D8DC0EC0069D5B8 /* ShareClient.framework in Embed Frameworks */,
 				3B4BA7912D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Embed Frameworks */,
 				CE95BF602BA7715800DC3DE3 /* MockKit.framework in Embed Frameworks */,
+				B6E925142EB3932A0076D719 /* OmnipodKit.framework in Embed Frameworks */,
 				CE95BF5E2BA770C300DC3DE3 /* LoopKitUI.framework in Embed Frameworks */,
 			);
 			name = "Embed Frameworks";
@@ -1222,6 +1225,7 @@
 		AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorStateModel.swift; sourceTree = "<group>"; };
 		AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigDataFlow.swift; sourceTree = "<group>"; };
 		B5822B15939E719628E9FF7C /* SnoozeRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeRootView.swift; sourceTree = "<group>"; };
+		B6E925122EB3932A0076D719 /* OmnipodKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmnipodKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		B9B5C0607505A38F256BF99A /* CGMSettingsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMSettingsDataFlow.swift; sourceTree = "<group>"; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressOverlay.swift; sourceTree = "<group>"; };
@@ -1670,6 +1674,7 @@
 				38E87401274F77E400975559 /* CoreNFC.framework in Frameworks */,
 				3B4BA78A2D8DC0EC0069D5B8 /* ShareClient.framework in Frameworks */,
 				3B4BA77E2D8DBD690069D5B8 /* OmniKit.framework in Frameworks */,
+				B6E925132EB3932A0076D719 /* OmnipodKit.framework in Frameworks */,
 				3811DE1025C9D37700A708ED /* Swinject in Frameworks */,
 				3B4BA78E2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Frameworks */,
 				B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */,
@@ -2308,6 +2313,7 @@
 		3818AA48274C267000843DB3 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				B6E925122EB3932A0076D719 /* OmnipodKit.framework */,
 				3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */,
 				3E84DA3F2F48D96000033608 /* EversenseKit.framework */,
 				3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */,

+ 3 - 0
Trio.xcworkspace/contents.xcworkspacedata

@@ -8,6 +8,9 @@
       location = "group:MinimedKit/MinimedKit.xcodeproj">
    </FileRef>
    <FileRef
+      location = "group:OmnipodKit/OmnipodKit.xcodeproj">
+   </FileRef>
+   <FileRef
       location = "group:LibreTransmitter/LibreTransmitter.xcodeproj">
    </FileRef>
    <FileRef

+ 10 - 3
Trio/Sources/APS/APSManager.swift

@@ -583,11 +583,18 @@ final class BaseAPSManager: APSManager, Injectable {
         do {
             try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
             debug(.apsManager, "Bolus succeeded")
-            if !isSMB {
-                try await determineBasalSync()
-            }
             bolusProgress.send(0)
             callback?(true, String(localized: "Bolus enacted successfully.", comment: "Success message for enacting a bolus"))
+            if !isSMB {
+                do {
+                    try await determineBasalSync()
+                } catch {
+                    warning(
+                        .apsManager,
+                        "determineBasalSync after manual bolus failed: \(error.localizedDescription)"
+                    )
+                }
+            }
         } catch {
             warning(.apsManager, "Bolus failed with error: \(error)")
             processError(APSError.pumpError(error))

+ 59 - 1
Trio/Sources/APS/DeviceDataManager.swift

@@ -10,6 +10,7 @@ import MinimedKit
 import MockKit
 import OmniBLE
 import OmniKit
+import OmnipodKit
 import ShareClient
 import SwiftDate
 import Swinject
@@ -39,6 +40,7 @@ private let staticPumpManagers: [PumpManagerUI.Type] = [
     MinimedPumpManager.self,
     OmnipodPumpManager.self,
     OmniBLEPumpManager.self,
+    OmniPumpManager.self,
     DanaKitPumpManager.self,
     MedtrumPumpManager.self,
     MockPumpManager.self
@@ -48,6 +50,7 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
     MinimedPumpManager.pluginIdentifier: MinimedPumpManager.self,
     OmnipodPumpManager.pluginIdentifier: OmnipodPumpManager.self,
     OmniBLEPumpManager.pluginIdentifier: OmniBLEPumpManager.self,
+    OmniPumpManager.pluginIdentifier: OmniPumpManager.self,
     DanaKitPumpManager.pluginIdentifier: DanaKitPumpManager.self,
     MedtrumPumpManager.pluginIdentifier: MedtrumPumpManager.self,
     MockPumpManager.pluginIdentifier: MockPumpManager.self
@@ -142,6 +145,13 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                         pumpActivatedAtDate.send(medtrumPump.state.patchActivatedAt)
                     }
                 }
+                if let omni = pumpManager as? OmniPumpManager {
+                    guard let endTime = omni.state.podState?.expiresAt else {
+                        pumpExpiresAtDate.send(nil)
+                        return
+                    }
+                    pumpExpiresAtDate.send(endTime)
+                }
                 if let simulatorPump = pumpManager as? MockPumpManager {
                     pumpDisplayState.value = PumpDisplayState(name: simulatorPump.localizedTitle, image: simulatorPump.smallImage)
                     pumpName.send(simulatorPump.localizedTitle)
@@ -292,6 +302,10 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         self.recommendsLoop.send()
     }
 
+    public func pumpManagerTypeByIdentifier(_ identifier: String) -> PumpManagerUI.Type? {
+        staticPumpManagersByIdentifier[identifier]
+    }
+
     private func pumpManagerFromRawValue(_ rawValue: [String: Any]) -> PumpManagerUI? {
         guard let rawState = rawValue["state"] as? PumpManager.RawStateValue,
               let Manager = pumpManagerTypeFromRawValue(rawValue)
@@ -307,7 +321,18 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
             return nil
         }
 
-        return staticPumpManagersByIdentifier[managerIdentifier]
+        if let pumpManager = pumpManagerTypeByIdentifier(managerIdentifier) {
+            return pumpManager
+        }
+
+        /// The pumpManager was not found for managerIdentifier. If this was for an "Omnipod" (OmniKit) or
+        /// "Omnipod-DASH" (OmniBLE), have the universal "Omni" pumpManager (OmnipodKit) handle instead.
+        let OmniStr = "Omni"
+        if managerIdentifier.hasPrefix(OmniStr) {
+            return pumpManagerTypeByIdentifier(OmniStr)
+        }
+
+        return nil
     }
 
     // MARK: - GlucoseSource
@@ -554,6 +579,39 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
             }
         }
 
+        if let omni = pumpManager as? OmniPumpManager {
+            let reservoirVal = omni.state.podState?.lastInsulinMeasurements?.reservoirLevel ?? 0xDEAD_BEEF
+            // TODO: find the value Pod.maximumReservoirReading
+            let reservoir = Decimal(reservoirVal) > 50.0 ? 0xDEAD_BEEF : reservoirVal
+
+            storage.save(Decimal(reservoir), as: OpenAPS.Monitor.reservoir)
+            broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
+                $0.pumpReservoirDidChange(Decimal(reservoir))
+            }
+
+            // manual temp basal on
+            if let tempBasal = omni.state.podState?.unfinalizedTempBasal, !tempBasal.isFinished(),
+               !tempBasal.automatic
+            {
+                // the manual basal temp is launch - block every thing
+                debug(.deviceManager, "manual temp basal")
+                manualTempBasal.send(true)
+            } else {
+                // no more manual Temp Basal !
+                manualTempBasal.send(false)
+            }
+
+            guard let endTime = omni.state.podState?.expiresAt else {
+                pumpExpiresAtDate.send(nil)
+                return
+            }
+            pumpExpiresAtDate.send(endTime)
+
+            if let startTime = omni.state.podState?.activatedAt {
+                storage.save(startTime, as: OpenAPS.Monitor.podAge)
+            }
+        }
+
         if let simulatorPump = pumpManager as? MockPumpManager {
             broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
                 $0.pumpReservoirDidChange(Decimal(simulatorPump.state.reservoirUnitsRemaining))

+ 22 - 4
Trio/Sources/Application/AppDelegate.swift

@@ -20,21 +20,39 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(crashReportingEnabled)
         Crashlytics.crashlytics().setCustomValue(Bundle.main.appDevVersion ?? "unknown", forKey: "app_dev_version")
 
-        // Telemetry: record this cold launch into the sliding 7-day window. If
-        // consent is set and the build SHA changed since the last successful
-        // send, fire an immediate ping — the 24h scheduler can't notice a
-        // build update on its own. Then arm the recurring 24h timer.
+        // Telemetry: record this cold launch into the sliding 7-day window,
+        // then drive cadence via three layered triggers — listed below in
+        // priority of reliability:
+        //
+        //   1. SHA-change ping: build updated since last send. Awaited so
+        //      the lastSentAt stamp is fresh before the overdue check.
+        //   2. checkAndSendIfOverdue: covers the regular cold launch on the
+        //      same build when >24h has passed since the last successful
+        //      send. Together with the foreground-transition hook below
+        //      (`applicationWillEnterForeground`), this keeps daily pings
+        //      flowing on iOS.
+        //   3. scheduleRecurring: best-effort fallback for the rare case
+        //      where the app stays foregrounded for a full 24h.
         TelemetryClient.shared.recordColdLaunch()
         Task.detached {
             if TelemetryClient.shared.buildShaChangedSinceLastSend() {
                 await TelemetryClient.shared.maybeSend()
             }
             TelemetryClient.shared.scheduleRecurring()
+            TelemetryClient.shared.checkAndSendIfOverdue()
         }
 
         return true
     }
 
+    /// Foreground-transition entry point for telemetry cadence. Re-evaluates
+    /// the overdue window every time the user brings Trio to the foreground,
+    /// since `scheduleRecurring`'s GCD timer doesn't fire while suspended.
+    /// No-op if a send already landed within the last 24h.
+    func applicationWillEnterForeground(_: UIApplication) {
+        TelemetryClient.shared.checkAndSendIfOverdue()
+    }
+
     func application(
         _: UIApplication,
         didReceiveRemoteNotification userInfo: [AnyHashable: Any],

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 127 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 1 - 0
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -994,6 +994,7 @@ extension Home {
             // PUMP RELATED
             .confirmationDialog("Pump Model", isPresented: $showPumpSelection) {
                 Button("Medtronic") { state.addPump(.minimed) }
+                Button("All Omnipod Types") { state.addPump(.omni) }
                 Button("Omnipod Eros") { state.addPump(.omnipod) }
                 Button("Omnipod DASH") { state.addPump(.omnipodBLE) }
                 Button("Dana(RS/-i)") { state.addPump(.dana) }

+ 1 - 0
Trio/Sources/Modules/PumpConfig/PumpConfigDataFlow.swift

@@ -9,6 +9,7 @@ enum PumpConfig {
         case minimed
         case omnipod
         case omnipodBLE
+        case omni
         case dana
         case medtrum
         case simulator

+ 2 - 0
Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift

@@ -118,6 +118,7 @@ extension PumpConfig {
                                 )
                                 VStack(alignment: .leading) {
                                     Text("• Medtronic")
+                                    Text("• All Omnipod Types")
                                     Text("• Omnipod Eros")
                                     Text("• Omnipod DASH")
                                     Text("• Dana (RS/-i)")
@@ -134,6 +135,7 @@ extension PumpConfig {
                 }
                 .confirmationDialog("Pump Model", isPresented: $showPumpSelection) {
                     Button("Medtronic") { state.addPump(.minimed) }
+                    Button("All Omnipod Types") { state.addPump(.omni) }
                     Button("Omnipod Eros") { state.addPump(.omnipod) }
                     Button("Omnipod DASH") { state.addPump(.omnipodBLE) }
                     Button("Dana(RS/-i)") { state.addPump(.dana) }

+ 10 - 0
Trio/Sources/Modules/PumpConfig/View/PumpSetupView.swift

@@ -9,6 +9,7 @@ import MockKitUI
 import OmniBLE
 import OmniKit
 import OmniKitUI
+import OmnipodKit
 import SwiftUI
 import UIKit
 
@@ -60,6 +61,15 @@ extension PumpConfig {
                     allowDebugFeatures: true,
                     allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
                 )
+            case .omni:
+                setupViewController = OmniPumpManager.setupViewController(
+                    initialSettings: initialSettings,
+                    bluetoothProvider: bluetoothManager,
+                    colorPalette: .default,
+                    allowDebugFeatures: true,
+                    prefersToSkipUserInteraction: false,
+                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
+                )
             case .dana:
                 setupViewController = DanaKitPumpManager.setupViewController(
                     initialSettings: initialSettings,

+ 51 - 4
Trio/Sources/Services/Telemetry/TelemetryClient.swift

@@ -21,6 +21,11 @@ final class TelemetryClient: Injectable {
 
     private static let productionBaseURL: URL? = URL(string: "https://telemetry.triodocs.org")
 
+    // MARK: if you fork Trio and keep telemetry enabled, please change the name here
+
+    // so that we can distinguish forks from mainline Trio builds in our telemetry.
+    private static let telemetryAppName: String = "Trio"
+
     /// Effective base URL: respects the debug override in
     /// `PropertyPersistentFlags.telemetryDebugServerURL`, then falls back to
     /// `productionBaseURL`. Used by both the registration and `/checkin` paths.
@@ -39,6 +44,14 @@ final class TelemetryClient: Injectable {
     private static let dailyInterval: TimeInterval = 24 * 60 * 60
     private static let maxPayloadBytes = 4096
 
+    private static let buildDateFormatter: DateFormatter = {
+        let f = DateFormatter()
+        f.dateFormat = "yyyy-MM-dd"
+        f.locale = Locale(identifier: "en_US_POSIX")
+        f.timeZone = TimeZone(identifier: "UTC")
+        return f
+    }()
+
     // MARK: Injected services
 
     @Injected() private var apsManager: APSManager!
@@ -104,6 +117,12 @@ final class TelemetryClient: Injectable {
     /// Arms (or re-arms) the 24h send timer. Idempotent. Bails out without
     /// scheduling if the user hasn't decided on consent yet or has opted out
     /// — there's nothing for the timer to do.
+    ///
+    /// Best-effort fallback only. GCD timers don't advance while the app is
+    /// suspended, so on iOS this effectively means "fires only if the app
+    /// stays foregrounded for 24h." The reliable cadence driver is
+    /// `checkAndSendIfOverdue()` called on every foreground transition and
+    /// cold launch.
     func scheduleRecurring() {
         guard PropertyPersistentFlags.shared.telemetryConsentDecisionMade == true,
               PropertyPersistentFlags.shared.telemetryEnabled == true
@@ -124,6 +143,31 @@ final class TelemetryClient: Injectable {
         }
     }
 
+    /// If consent is set and we haven't successfully sent within the last 24h
+    /// (or have never sent), fire a send. Called on foreground transitions
+    /// and from the cold-launch path so daily cadence is kept.
+    ///
+    /// Mirrors the pattern used by LoopFollow's `TaskScheduler.checkTasksNow()`:
+    /// wall-clock comparison against `telemetryLastSentAt`, fire-and-forget
+    /// if overdue. Safe to call repeatedly — if a send already fired within
+    /// the window, this is a no-op.
+    func checkAndSendIfOverdue() {
+        guard PropertyPersistentFlags.shared.telemetryConsentDecisionMade == true,
+              PropertyPersistentFlags.shared.telemetryEnabled == true
+        else {
+            return
+        }
+
+        let lastSent = PropertyPersistentFlags.shared.telemetryLastSentAt
+        let overdue: Bool = {
+            guard let lastSent else { return true }
+            return Date().timeIntervalSince(lastSent) >= Self.dailyInterval
+        }()
+        guard overdue else { return }
+
+        Task.detached { await self.maybeSend() }
+    }
+
     /// Single entry point for all sends (scheduler tick, consent-yes, startup
     /// SHA-change). Gated on consent + opt-in. *When* to send is the caller's
     /// decision — startup handles the SHA-change shortcut, the timer handles
@@ -150,6 +194,7 @@ final class TelemetryClient: Injectable {
         var payload: [String: Any] = [:]
 
         if let v = info["CFBundleShortVersionString"] as? String { payload["appVersion"] = v }
+        payload["appName"] = TelemetryClient.telemetryAppName
         // appDevVersion is Trio's 4-component dev counter (e.g. "0.7.0.14") —
         // the most precise build identifier we have. Always emit, even when
         // the Info.plist key is missing, so dashboards can rely on the field.
@@ -157,10 +202,10 @@ final class TelemetryClient: Injectable {
         payload["commitSha"] = bd.trioCommitSHA
         payload["branch"] = bd.trioBranch
 
-        // Date-only prefix of the build-date string. Keeps the field a
-        // low-resolution build identifier, not a precise timestamp.
-        if let raw = bd.buildDateString, raw.count >= 10 {
-            payload["buildDate"] = String(raw.prefix(10))
+        // Date-only (yyyy-MM-dd, UTC) build identifier, parsed from the
+        // "Tue May 26 12:34:56 UTC 2025" form added in BuildDetails.plist.
+        if let date = bd.buildDate() {
+            payload["buildDate"] = Self.buildDateFormatter.string(from: date)
         }
 
         payload["isTestFlight"] = bd.isTestFlightBuild()
@@ -173,6 +218,8 @@ final class TelemetryClient: Injectable {
         payload["device"] = Self.hardwareIdentifier()
         payload["platform"] = Self.detectPlatform()
         payload["osVersion"] = UIDevice.current.systemVersion
+        payload["locale"] = Locale.current.identifier
+        payload["timeZone"] = TimeZone.current.identifier
 
         // Pump model — omitted entirely when no pump is paired.
         if let pump = apsManager?.pumpManager {

+ 1 - 1
scripts/capture-build-details.sh

@@ -16,7 +16,7 @@ fi
 echo "Gathering build details..."
 
 # Capture the current date
-plutil -replace com-trio-build-date -string "$(date -u '+%a %b %e %H:%M:%S UTC %Y')" "${info_plist_path}"
+plutil -replace com-trio-build-date -string "$(LC_ALL=C date -u '+%a %b %e %H:%M:%S UTC %Y')" "${info_plist_path}"
 
 # --- Root repo details ---
 # Retrieve current branch (or tag) and commit SHA.

+ 2 - 1
scripts/swiftformat.sh

@@ -112,4 +112,5 @@ trailingClosures \
   MinimedKit, \
   TidepoolService, \
   DanaKit, \
-  MedtrumKit
+  MedtrumKit, \
+  OmnipodKit