Преглед изворни кода

Merge branch 'dev' of https://github.com/nightscout/Trio-dev into onboarding

polscm32 пре 1 година
родитељ
комит
10d7c1959a
29 измењених фајлова са 899 додато и 1007 уклоњено
  1. 1 1
      Model/Helper/PumpEvent+helper.swift
  2. 148 20
      Trio.xcodeproj/project.pbxproj
  3. 30 172
      Trio.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme
  4. 1 1
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved
  5. 2 1
      Trio/Resources/json/defaults/freeaps/freeaps_settings.json
  6. 1 150
      Trio/Sources/APS/APSManager.swift
  7. 16 5
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  8. 36 258
      Trio/Sources/APS/PluginManager.swift
  9. 141 113
      Trio/Sources/APS/Storage/TDDStorage.swift
  10. 89 11
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  11. 2 1
      Trio/Sources/Models/Preferences.swift
  12. 34 0
      Trio/Sources/Models/TimeInRangeType.swift
  13. 5 0
      Trio/Sources/Models/TrioSettings.swift
  14. 1 2
      Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift
  15. 118 4
      Trio/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift
  16. 125 131
      Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  17. 3 0
      Trio/Sources/Modules/Home/HomeStateModel.swift
  18. 32 23
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  19. 2 1
      Trio/Sources/Modules/Settings/SettingsStateModel.swift
  20. 6 3
      Trio/Sources/Modules/Stat/StatStateModel+Setup/StackedChartSetup.swift
  21. 2 0
      Trio/Sources/Modules/Stat/StatStateModel.swift
  22. 4 2
      Trio/Sources/Modules/Stat/View/StatRootView.swift
  23. 10 6
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift
  24. 14 5
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift
  25. 3 0
      Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift
  26. 69 0
      Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  27. 4 3
      Trio/Sources/Services/Network/TidepoolManager.swift
  28. 0 53
      TrioTests/PluginManagerTests.swift
  29. 0 41
      scripts/copy-plugins.sh

+ 1 - 1
Model/Helper/PumpEvent+helper.swift

@@ -97,7 +97,7 @@ extension NSPredicate {
     static var recentPumpHistory: NSPredicate {
         let date = Date.twentyMinutesAgo
         return NSPredicate(
-            format: "type == %@ AND timestamp <= %@",
+            format: "type == %@ AND timestamp >= %@",
             PumpEventStored.EventType.tempBasal.rawValue,
             date as NSDate
         )

+ 148 - 20
Trio.xcodeproj/project.pbxproj

@@ -205,8 +205,48 @@
 		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
 		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
 		3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */; };
+		3B4BA76A2D8DBD690069D5B8 /* CGMBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */; };
+		3B4BA76B2D8DBD690069D5B8 /* CGMBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA76C2D8DBD690069D5B8 /* CGMBLEKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */; };
+		3B4BA76D2D8DBD690069D5B8 /* CGMBLEKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA76E2D8DBD690069D5B8 /* DanaKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75D2D8DBD690069D5B8 /* DanaKit.framework */; };
+		3B4BA76F2D8DBD690069D5B8 /* DanaKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75D2D8DBD690069D5B8 /* DanaKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7702D8DBD690069D5B8 /* G7SensorKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75E2D8DBD690069D5B8 /* G7SensorKit.framework */; };
+		3B4BA7712D8DBD690069D5B8 /* G7SensorKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75E2D8DBD690069D5B8 /* G7SensorKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7722D8DBD690069D5B8 /* G7SensorKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75F2D8DBD690069D5B8 /* G7SensorKitUI.framework */; };
+		3B4BA7732D8DBD690069D5B8 /* G7SensorKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75F2D8DBD690069D5B8 /* G7SensorKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7742D8DBD690069D5B8 /* LibreTransmitter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7602D8DBD690069D5B8 /* LibreTransmitter.framework */; };
+		3B4BA7752D8DBD690069D5B8 /* LibreTransmitter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7602D8DBD690069D5B8 /* LibreTransmitter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7762D8DBD690069D5B8 /* LibreTransmitterUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7612D8DBD690069D5B8 /* LibreTransmitterUI.framework */; };
+		3B4BA7772D8DBD690069D5B8 /* LibreTransmitterUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7612D8DBD690069D5B8 /* LibreTransmitterUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7782D8DBD690069D5B8 /* MinimedKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7622D8DBD690069D5B8 /* MinimedKit.framework */; };
+		3B4BA7792D8DBD690069D5B8 /* MinimedKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7622D8DBD690069D5B8 /* MinimedKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA77A2D8DBD690069D5B8 /* MinimedKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7632D8DBD690069D5B8 /* MinimedKitUI.framework */; };
+		3B4BA77B2D8DBD690069D5B8 /* MinimedKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7632D8DBD690069D5B8 /* MinimedKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA77C2D8DBD690069D5B8 /* OmniBLE.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7642D8DBD690069D5B8 /* OmniBLE.framework */; };
+		3B4BA77D2D8DBD690069D5B8 /* OmniBLE.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7642D8DBD690069D5B8 /* OmniBLE.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA77E2D8DBD690069D5B8 /* OmniKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7652D8DBD690069D5B8 /* OmniKit.framework */; };
+		3B4BA77F2D8DBD690069D5B8 /* OmniKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7652D8DBD690069D5B8 /* OmniKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7802D8DBD690069D5B8 /* OmniKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7662D8DBD690069D5B8 /* OmniKitUI.framework */; };
+		3B4BA7812D8DBD690069D5B8 /* OmniKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7662D8DBD690069D5B8 /* OmniKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7822D8DBD690069D5B8 /* RileyLinkBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7672D8DBD690069D5B8 /* RileyLinkBLEKit.framework */; };
+		3B4BA7832D8DBD690069D5B8 /* RileyLinkBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7672D8DBD690069D5B8 /* RileyLinkBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7842D8DBD690069D5B8 /* RileyLinkKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7682D8DBD690069D5B8 /* RileyLinkKit.framework */; };
+		3B4BA7852D8DBD690069D5B8 /* RileyLinkKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7682D8DBD690069D5B8 /* RileyLinkKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7862D8DBD690069D5B8 /* RileyLinkKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7692D8DBD690069D5B8 /* RileyLinkKitUI.framework */; };
+		3B4BA7872D8DBD690069D5B8 /* RileyLinkKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7692D8DBD690069D5B8 /* RileyLinkKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA78A2D8DC0EC0069D5B8 /* ShareClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE398D1A297D69A900DF218F /* ShareClient.framework */; };
+		3B4BA78B2D8DC0EC0069D5B8 /* ShareClient.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE398D1A297D69A900DF218F /* ShareClient.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA78C2D8DC0EC0069D5B8 /* ShareClientUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE79502D29980E4D00FA576E /* ShareClientUI.framework */; };
+		3B4BA78D2D8DC0EC0069D5B8 /* ShareClientUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE79502D29980E4D00FA576E /* ShareClientUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA78E2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */; };
+		3B4BA78F2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7902D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */; };
+		3B4BA7912D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
 		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
+		3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687B2D8DDD4600899469 /* SlideButton */; };
+		3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687E2D8DDD8800899469 /* CryptoSwift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
 		491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */; };
@@ -590,6 +630,7 @@
 		DDE1796F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1794F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift */; };
 		DDE179702C910127003CDDB7 /* OverrideStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179502C910127003CDDB7 /* OverrideStored+CoreDataClass.swift */; };
 		DDE179712C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179512C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift */; };
+		DDEBB05C2D89E9050032305D /* TimeInRangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */; };
 		DDF847DD2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847DC2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift */; };
 		DDF847DF2C5C28780049BB3B /* LiveActivitySettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847DE2C5C28780049BB3B /* LiveActivitySettingsProvider.swift */; };
 		DDF847E12C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847E02C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift */; };
@@ -677,10 +718,29 @@
 			dstSubfolderSpec = 10;
 			files = (
 				CE51DD1D2A01970900F163F7 /* ConnectIQ 2.xcframework in Embed Frameworks */,
+				3B4BA78F2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Embed Frameworks */,
+				3B4BA7732D8DBD690069D5B8 /* G7SensorKitUI.framework in Embed Frameworks */,
+				3B4BA76D2D8DBD690069D5B8 /* CGMBLEKitUI.framework in Embed Frameworks */,
+				3B4BA76B2D8DBD690069D5B8 /* CGMBLEKit.framework in Embed Frameworks */,
+				3B4BA7792D8DBD690069D5B8 /* MinimedKit.framework in Embed Frameworks */,
 				CE95BF5C2BA770C300DC3DE3 /* LoopKit.framework in Embed Frameworks */,
+				3B4BA7712D8DBD690069D5B8 /* G7SensorKit.framework in Embed Frameworks */,
 				CEB434FE28B90B8C00B70274 /* SwiftCharts in Embed Frameworks */,
+				3B4BA7812D8DBD690069D5B8 /* OmniKitUI.framework in Embed Frameworks */,
+				3B4BA76F2D8DBD690069D5B8 /* DanaKit.framework in Embed Frameworks */,
+				3B4BA77D2D8DBD690069D5B8 /* OmniBLE.framework in Embed Frameworks */,
+				3B4BA77F2D8DBD690069D5B8 /* OmniKit.framework in Embed Frameworks */,
+				3B4BA7852D8DBD690069D5B8 /* RileyLinkKit.framework in Embed Frameworks */,
+				3B4BA7752D8DBD690069D5B8 /* LibreTransmitter.framework in Embed Frameworks */,
+				3B4BA7772D8DBD690069D5B8 /* LibreTransmitterUI.framework in Embed Frameworks */,
+				3B4BA77B2D8DBD690069D5B8 /* MinimedKitUI.framework in Embed Frameworks */,
+				3B4BA7832D8DBD690069D5B8 /* RileyLinkBLEKit.framework in Embed Frameworks */,
 				CE95BF642BA771BE00DC3DE3 /* LoopTestingKit.framework in Embed Frameworks */,
 				CE95BF622BA7715900DC3DE3 /* MockKitUI.framework in Embed Frameworks */,
+				3B4BA78D2D8DC0EC0069D5B8 /* ShareClientUI.framework in Embed Frameworks */,
+				3B4BA7872D8DBD690069D5B8 /* RileyLinkKitUI.framework in Embed Frameworks */,
+				3B4BA78B2D8DC0EC0069D5B8 /* ShareClient.framework in Embed Frameworks */,
+				3B4BA7912D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Embed Frameworks */,
 				CE95BF602BA7715800DC3DE3 /* MockKit.framework in Embed Frameworks */,
 				CE95BF5E2BA770C300DC3DE3 /* LoopKitUI.framework in Embed Frameworks */,
 			);
@@ -936,6 +996,23 @@
 		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
 		3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeStateModel+CGM.swift"; sourceTree = "<group>"; };
+		3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA75D2D8DBD690069D5B8 /* DanaKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DanaKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA75E2D8DBD690069D5B8 /* G7SensorKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G7SensorKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA75F2D8DBD690069D5B8 /* G7SensorKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G7SensorKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7602D8DBD690069D5B8 /* LibreTransmitter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LibreTransmitter.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7612D8DBD690069D5B8 /* LibreTransmitterUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LibreTransmitterUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7622D8DBD690069D5B8 /* MinimedKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MinimedKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7632D8DBD690069D5B8 /* MinimedKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MinimedKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7642D8DBD690069D5B8 /* OmniBLE.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniBLE.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7652D8DBD690069D5B8 /* OmniKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7662D8DBD690069D5B8 /* OmniKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7672D8DBD690069D5B8 /* RileyLinkBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7682D8DBD690069D5B8 /* RileyLinkKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7692D8DBD690069D5B8 /* RileyLinkKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TidepoolServiceKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TidepoolServiceKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoadingView.swift; sourceTree = "<group>"; };
 		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
@@ -1329,6 +1406,7 @@
 		DDE1794F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrefDetermination+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179502C910127003CDDB7 /* OverrideStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179512C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
+		DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInRangeType.swift; sourceTree = "<group>"; };
 		DDF847DC2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsDataFlow.swift; sourceTree = "<group>"; };
 		DDF847DE2C5C28780049BB3B /* LiveActivitySettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsProvider.swift; sourceTree = "<group>"; };
 		DDF847E02C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsStateModel.swift; sourceTree = "<group>"; };
@@ -1377,17 +1455,38 @@
 			buildActionMask = 2147483647;
 			files = (
 				CE95BF632BA771BE00DC3DE3 /* LoopTestingKit.framework in Frameworks */,
+				3B4BA7722D8DBD690069D5B8 /* G7SensorKitUI.framework in Frameworks */,
+				3B4BA7742D8DBD690069D5B8 /* LibreTransmitter.framework in Frameworks */,
+				3B4BA78C2D8DC0EC0069D5B8 /* ShareClientUI.framework in Frameworks */,
+				3B4BA77A2D8DBD690069D5B8 /* MinimedKitUI.framework in Frameworks */,
+				3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */,
+				3B4BA7782D8DBD690069D5B8 /* MinimedKit.framework in Frameworks */,
+				3B4BA7762D8DBD690069D5B8 /* LibreTransmitterUI.framework in Frameworks */,
+				3B4BA7902D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Frameworks */,
+				3B4BA76A2D8DBD690069D5B8 /* CGMBLEKit.framework in Frameworks */,
+				3B4BA77C2D8DBD690069D5B8 /* OmniBLE.framework in Frameworks */,
 				38E87403274F78C000975559 /* libswiftCoreNFC.tbd in Frameworks */,
 				38E87401274F77E400975559 /* CoreNFC.framework in Frameworks */,
+				3B4BA78A2D8DC0EC0069D5B8 /* ShareClient.framework in Frameworks */,
+				3B4BA77E2D8DBD690069D5B8 /* OmniKit.framework in Frameworks */,
 				CE51DD1C2A01970900F163F7 /* ConnectIQ 2.xcframework in Frameworks */,
 				3811DE1025C9D37700A708ED /* Swinject in Frameworks */,
+				3B4BA78E2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Frameworks */,
 				B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */,
+				3B4BA7702D8DBD690069D5B8 /* G7SensorKit.framework in Frameworks */,
+				3B4BA76C2D8DBD690069D5B8 /* CGMBLEKitUI.framework in Frameworks */,
 				CE95BF5B2BA770C300DC3DE3 /* LoopKit.framework in Frameworks */,
 				38B17B6625DD90E0005CAE3D /* SwiftDate in Frameworks */,
 				3833B46D26012030003021B3 /* Algorithms in Frameworks */,
+				3B4BA7822D8DBD690069D5B8 /* RileyLinkBLEKit.framework in Frameworks */,
+				3B4BA76E2D8DBD690069D5B8 /* DanaKit.framework in Frameworks */,
+				3B4BA7862D8DBD690069D5B8 /* RileyLinkKitUI.framework in Frameworks */,
 				CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */,
 				CE95BF5F2BA7715800DC3DE3 /* MockKit.framework in Frameworks */,
+				3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */,
 				38DF1789276FC8C400B3528F /* SwiftMessages in Frameworks */,
+				3B4BA7802D8DBD690069D5B8 /* OmniKitUI.framework in Frameworks */,
+				3B4BA7842D8DBD690069D5B8 /* RileyLinkKit.framework in Frameworks */,
 				CE95BF612BA7715900DC3DE3 /* MockKitUI.framework in Frameworks */,
 				E0CC2C5C275B9F0F00A7BC71 /* HealthKit.framework in Frameworks */,
 				CE95BF5D2BA770C300DC3DE3 /* LoopKitUI.framework in Frameworks */,
@@ -1961,6 +2060,23 @@
 		3818AA48274C267000843DB3 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */,
+				3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */,
+				3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */,
+				3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */,
+				3B4BA75D2D8DBD690069D5B8 /* DanaKit.framework */,
+				3B4BA75E2D8DBD690069D5B8 /* G7SensorKit.framework */,
+				3B4BA75F2D8DBD690069D5B8 /* G7SensorKitUI.framework */,
+				3B4BA7602D8DBD690069D5B8 /* LibreTransmitter.framework */,
+				3B4BA7612D8DBD690069D5B8 /* LibreTransmitterUI.framework */,
+				3B4BA7622D8DBD690069D5B8 /* MinimedKit.framework */,
+				3B4BA7632D8DBD690069D5B8 /* MinimedKitUI.framework */,
+				3B4BA7642D8DBD690069D5B8 /* OmniBLE.framework */,
+				3B4BA7652D8DBD690069D5B8 /* OmniKit.framework */,
+				3B4BA7662D8DBD690069D5B8 /* OmniKitUI.framework */,
+				3B4BA7672D8DBD690069D5B8 /* RileyLinkBLEKit.framework */,
+				3B4BA7682D8DBD690069D5B8 /* RileyLinkKit.framework */,
+				3B4BA7692D8DBD690069D5B8 /* RileyLinkKitUI.framework */,
 				CE95BF492BA5CED700DC3DE3 /* LoopKit.framework */,
 				CE95BF4A2BA5CED700DC3DE3 /* LoopKitUI.framework */,
 				CE51DD1B2A01970800F163F7 /* ConnectIQ 2.xcframework */,
@@ -2120,6 +2236,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */,
 				3B2F77852D7E52ED005ED9FA /* TDD.swift */,
 				DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */,
 				DD3078692D42F94000DE0490 /* GarminDevice.swift */,
@@ -3411,7 +3528,6 @@
 				3821ECD025DC703C00BC42AD /* Embed Frameworks */,
 				38E8753D27554D5900975559 /* Embed Watch Content */,
 				6B1A8D122B14D88E00E76752 /* Embed Foundation Extensions */,
-				CE95BF582BA5F8F300DC3DE3 /* Install plugins */,
 				DD88C8DF2C4D583900F2D558 /* Run Script: Capture Build Details */,
 			);
 			buildRules = (
@@ -3428,6 +3544,8 @@
 				38DF1788276FC8C400B3528F /* SwiftMessages */,
 				CEB434FC28B90B7C00B70274 /* SwiftCharts */,
 				B958F1B62BA0711600484851 /* MKRingProgressView */,
+				3BD9687B2D8DDD4600899469 /* SlideButton */,
+				3BD9687E2D8DDD8800899469 /* CryptoSwift */,
 			);
 			productName = Trio;
 			productReference = 388E595825AD948C0019842D /* Trio.app */;
@@ -3601,6 +3719,8 @@
 				38DF1787276FC8C300B3528F /* XCRemoteSwiftPackageReference "SwiftMessages" */,
 				CEB434FB28B90B7C00B70274 /* XCRemoteSwiftPackageReference "SwiftCharts" */,
 				B958F1B52BA0711600484851 /* XCRemoteSwiftPackageReference "MKRingProgressView" */,
+				3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */,
+				3BD9687D2D8DDD8800899469 /* XCRemoteSwiftPackageReference "CryptoSwift" */,
 			);
 			productRefGroup = 388E595925AD948C0019842D /* Products */;
 			projectDirPath = "";
@@ -3695,25 +3815,6 @@
 			shellPath = /bin/sh;
 			shellScript = "source \"${SRCROOT}\"/scripts/swiftformat.sh\n\n";
 		};
-		CE95BF582BA5F8F300DC3DE3 /* Install plugins */ = {
-			isa = PBXShellScriptBuildPhase;
-			alwaysOutOfDate = 1;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			inputFileListPaths = (
-			);
-			inputPaths = (
-			);
-			name = "Install plugins";
-			outputFileListPaths = (
-			);
-			outputPaths = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-			shellPath = /bin/sh;
-			shellScript = "\"${SRCROOT}/Scripts/copy-plugins.sh\"\n";
-		};
 		DD88C8DF2C4D583900F2D558 /* Run Script: Capture Build Details */ = {
 			isa = PBXShellScriptBuildPhase;
 			alwaysOutOfDate = 1;
@@ -4132,6 +4233,7 @@
 				58A3D5442C96DE11003F90FC /* TempTargetStored+Helper.swift in Sources */,
 				DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */,
 				BD47FD172D88AAF50043966B /* OnboardingStepViews.swift in Sources */,
+				DDEBB05C2D89E9050032305D /* TimeInRangeType.swift in Sources */,
 				DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */,
 				BD47FDDB2D8B659B0043966B /* BasalProfileStepView.swift in Sources */,
 				F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */,
@@ -5007,6 +5109,22 @@
 				minimumVersion = 9.0.0;
 			};
 		};
+		3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/no-comment/SlideButton";
+			requirement = {
+				branch = main;
+				kind = branch;
+			};
+		};
+		3BD9687D2D8DDD8800899469 /* XCRemoteSwiftPackageReference "CryptoSwift" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 1.8.2;
+			};
+		};
 		B958F1B52BA0711600484851 /* XCRemoteSwiftPackageReference "MKRingProgressView" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "https://github.com/maxkonovalov/MKRingProgressView.git";
@@ -5046,6 +5164,16 @@
 			package = 38DF1787276FC8C300B3528F /* XCRemoteSwiftPackageReference "SwiftMessages" */;
 			productName = SwiftMessages;
 		};
+		3BD9687B2D8DDD4600899469 /* SlideButton */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */;
+			productName = SlideButton;
+		};
+		3BD9687E2D8DDD8800899469 /* CryptoSwift */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 3BD9687D2D8DDD8800899469 /* XCRemoteSwiftPackageReference "CryptoSwift" */;
+			productName = CryptoSwift;
+		};
 		B958F1B62BA0711600484851 /* MKRingProgressView */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = B958F1B52BA0711600484851 /* XCRemoteSwiftPackageReference "MKRingProgressView" */;

+ 30 - 172
Trio.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme

@@ -98,166 +98,54 @@
             buildForAnalyzing = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "C1E34B5A29C7AD01009A50A5"
-               BuildableName = "MinimedKitPlugin.loopplugin"
-               BlueprintName = "MinimedKitPlugin"
-               ReferencedContainer = "container:MinimedKit/MinimedKit.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "C124021629C7D93D00B32844"
-               BuildableName = "OmniKitPlugin.loopplugin"
-               BlueprintName = "OmniKitPlugin"
-               ReferencedContainer = "container:OmniKit/OmniKit.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "B4D40D3D23A428BC00D7ECB5"
-               BuildableName = "CGMBLEKitG5Plugin.loopplugin"
-               BlueprintName = "CGMBLEKitG5Plugin"
-               ReferencedContainer = "container:CGMBLEKit/CGMBLEKit.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "B4D40D2D23A3E91800D7ECB5"
-               BuildableName = "CGMBLEKitG6Plugin.loopplugin"
-               BlueprintName = "CGMBLEKitG6Plugin"
-               ReferencedContainer = "container:CGMBLEKit/CGMBLEKit.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "B40BF25D23ABD47400A43CEE"
-               BuildableName = "ShareClientPlugin.loopplugin"
-               BlueprintName = "ShareClientPlugin"
-               ReferencedContainer = "container:dexcom-share-client-swift/ShareClient.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "3E6007862D0C5D0C00B186D1"
-               BuildableName = "DanaKitPlugin.loopplugin"
-               BlueprintName = "DanaKitPlugin"
-               ReferencedContainer = "container:DanaKit/DanaKit.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "C17F511C291EACCD00555EB5"
-               BuildableName = "G7SensorPlugin.loopplugin"
-               BlueprintName = "G7SensorPlugin"
-               ReferencedContainer = "container:G7SensorKit/G7SensorKit.xcodeproj">
+               BlueprintIdentifier = "388E595725AD948C0019842D"
+               BuildableName = "Trio.app"
+               BlueprintName = "Trio"
+               ReferencedContainer = "container:Trio.xcodeproj">
             </BuildableReference>
          </BuildActionEntry>
          <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
+            buildForTesting = "NO"
+            buildForRunning = "NO"
+            buildForProfiling = "NO"
+            buildForArchiving = "NO"
+            buildForAnalyzing = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "C1BDBAE72A4397E200A787D1"
-               BuildableName = "LibreDemoPlugin.loopplugin"
-               BlueprintName = "LibreDemoPlugin"
-               ReferencedContainer = "container:LibreTransmitter/LibreTransmitter.xcodeproj">
+               BlueprintIdentifier = "38FCF3EC25E9028E0078B0D1"
+               BuildableName = "TrioTests.xctest"
+               BlueprintName = "TrioTests"
+               ReferencedContainer = "container:Trio.xcodeproj">
             </BuildableReference>
          </BuildActionEntry>
          <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
+            buildForTesting = "NO"
+            buildForRunning = "NO"
+            buildForProfiling = "NO"
+            buildForArchiving = "NO"
+            buildForAnalyzing = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "B40BF25D23ABD47400A43CEE"
-               BuildableName = "LibreTransmitterPlugin.loopplugin"
-               BlueprintName = "LibreTransmitterPlugin"
-               ReferencedContainer = "container:LibreTransmitter/LibreTransmitter.xcodeproj">
+               BlueprintIdentifier = "43D8FDD41C728FDF0073BE78"
+               BuildableName = "LoopKitTests.xctest"
+               BlueprintName = "LoopKitTests"
+               ReferencedContainer = "container:LoopKit/LoopKit.xcodeproj">
             </BuildableReference>
          </BuildActionEntry>
          <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
+            buildForTesting = "NO"
+            buildForRunning = "NO"
+            buildForProfiling = "NO"
+            buildForArchiving = "NO"
+            buildForAnalyzing = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "C187C196279086A8006E3557"
-               BuildableName = "OmniBLEPlugin.loopplugin"
-               BlueprintName = "OmniBLEPlugin"
+               BlueprintIdentifier = "84752E8A26ED0FFE009FD801"
+               BuildableName = "OmniBLETests.xctest"
+               BlueprintName = "OmniBLETests"
                ReferencedContainer = "container:OmniBLE/OmniBLE.xcodeproj">
             </BuildableReference>
          </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "A94AE4E3235A89B5005CA320"
-               BuildableName = "TidepoolServiceKitPlugin.loopplugin"
-               BlueprintName = "TidepoolServiceKitPlugin"
-               ReferencedContainer = "container:TidepoolService/TidepoolService.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "388E595725AD948C0019842D"
-               BuildableName = "Trio.app"
-               BlueprintName = "Trio"
-               ReferencedContainer = "container:Trio.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
       </BuildActionEntries>
    </BuildAction>
    <TestAction
@@ -270,16 +158,6 @@
             skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "38FCF3EC25E9028E0078B0D1"
-               BuildableName = "TrioTests.xctest"
-               BlueprintName = "TrioTests"
-               ReferencedContainer = "container:Trio.xcodeproj">
-            </BuildableReference>
-         </TestableReference>
-         <TestableReference
-            skipped = "NO">
-            <BuildableReference
-               BuildableIdentifier = "primary"
                BlueprintIdentifier = "43CABDFC1C3506F100005705"
                BuildableName = "CGMBLEKitTests.xctest"
                BlueprintName = "CGMBLEKitTests"
@@ -300,16 +178,6 @@
             skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "43D8FDD41C728FDF0073BE78"
-               BuildableName = "LoopKitTests.xctest"
-               BlueprintName = "LoopKitTests"
-               ReferencedContainer = "container:LoopKit/LoopKit.xcodeproj">
-            </BuildableReference>
-         </TestableReference>
-         <TestableReference
-            skipped = "NO">
-            <BuildableReference
-               BuildableIdentifier = "primary"
                BlueprintIdentifier = "B4CEE2DF257129780093111B"
                BuildableName = "MockKitTests.xctest"
                BlueprintName = "MockKitTests"
@@ -330,16 +198,6 @@
             skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "84752E8A26ED0FFE009FD801"
-               BuildableName = "OmniBLETests.xctest"
-               BlueprintName = "OmniBLETests"
-               ReferencedContainer = "container:OmniBLE/OmniBLE.xcodeproj">
-            </BuildableReference>
-         </TestableReference>
-         <TestableReference
-            skipped = "NO">
-            <BuildableReference
-               BuildableIdentifier = "primary"
                BlueprintIdentifier = "C12ED9C929C7DBA900435701"
                BuildableName = "OmniKitTests.xctest"
                BlueprintName = "OmniKitTests"

+ 1 - 1
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
-  "originHash" : "52d77fc35af7fe71614051dee0b291e2a0d38522eac7ae4d37d2442e81c7530c",
+  "originHash" : "b10fee57248e5d754951672d55dd1e425fadd3089d06858aed6f0f5206be7e5c",
   "pins" : [
     {
       "identity" : "cryptoswift",

+ 2 - 1
Trio/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -47,5 +47,6 @@
   "lockScreenView": "simple",
   "useCalendar": false,
   "displayCalendarIOBandCOB": false,
-  "displayCalendarEmojis": false
+  "displayCalendarEmojis": false,
+  "timeInRangeType": "timeInTightRange"
 }

+ 1 - 150
Trio/Sources/APS/APSManager.swift

@@ -117,7 +117,7 @@ final class BaseAPSManager: APSManager, Injectable {
 
     init(resolver: Resolver) {
         injectServices(resolver)
-        openAPS = OpenAPS(storage: storage)
+        openAPS = OpenAPS(storage: storage, tddStorage: tddStorage)
         subscribe()
         lastLoopDateSubject.send(lastLoopDate)
 
@@ -715,10 +715,6 @@ final class BaseAPSManager: APSManager, Injectable {
                 guard self.privateContext.hasChanges else { return }
                 try self.privateContext.save()
                 debug(.apsManager, "Determination enacted. Enacted: \(wasEnacted)")
-
-                Task.detached(priority: .low) {
-                    await self.statistics()
-                }
             }
         } catch {
             debug(
@@ -904,125 +900,6 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    // TODO: - Refactor this whole shit here...
-
-    // Add to statistics.JSON for upload to NS.
-    private func statistics() async {
-        let now = Date()
-        if settingsManager.settings.uploadStats != nil {
-            let hour = Calendar.current.component(.hour, from: now)
-            guard hour > 20 else {
-                return
-            }
-
-            // MARK: - Core Data related
-
-            async let glucoseStats = glucoseForStats()
-            async let lastLoopForStats = lastLoopForStats()
-            async let carbTotal = carbsForStats()
-            async let preferences = settingsManager.preferences
-
-            let loopStats = await loopStats(oneDayGlucose: Double(rawValue: (await glucoseStats?.oneDayGlucose.readings)!) ?? 0.0)
-
-            // Only save and upload once per day
-            guard (-1 * (await lastLoopForStats ?? .distantPast).timeIntervalSinceNow.hours) > 22 else { return }
-
-            let units = settingsManager.settings.units
-
-            // MARK: - Not Core Data related stuff
-
-            let pref = await preferences
-            var algo_ = "Oref0"
-
-            if pref.sigmoid, pref.enableDynamicCR {
-                algo_ = "Dynamic ISF + CR: Sigmoid"
-            } else if pref.sigmoid, !pref.enableDynamicCR {
-                algo_ = "Dynamic ISF: Sigmoid"
-            } else if pref.useNewFormula, pref.enableDynamicCR {
-                algo_ = "Dynamic ISF + CR: Logarithmic"
-            } else if pref.useNewFormula, !pref.sigmoid,!pref.enableDynamicCR {
-                algo_ = "Dynamic ISF: Logarithmic"
-            }
-            let af = pref.adjustmentFactor
-            let insulin_type = pref.curve
-            let buildDate = BuildDetails.shared.buildDate()
-            let version = Bundle.main.releaseVersionNumber
-            let build = Bundle.main.buildVersionNumber
-
-            var branch = BuildDetails.shared.branchAndSha
-
-            let copyrightNotice_ = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
-            let pump_ = pumpManager?.localizedTitle ?? ""
-            let cgm = settingsManager.settings.cgm
-            let file = OpenAPS.Monitor.statistics
-            var iPa: Decimal = 75
-            if pref.useCustomPeakTime {
-                iPa = pref.insulinPeakTime
-            } else if pref.curve.rawValue == "rapid-acting" {
-                iPa = 65
-            } else if pref.curve.rawValue == "ultra-rapid" {
-                iPa = 50
-            }
-
-            // Insulin placeholder
-            let insulin = Ins(
-                TDD: 0,
-                bolus: 0,
-                temp_basal: 0,
-                scheduled_basal: 0,
-                total_average: 0
-            )
-            guard let processedGlucoseStats = await glucoseStats else { return }
-
-            let eA1cDisplayUnit = processedGlucoseStats.eA1cDisplayUnit
-
-            let dailystat = await Statistics(
-                created_at: Date(),
-                iPhone: UIDevice.current.getDeviceId,
-                iOS: UIDevice.current.getOSInfo,
-                Build_Version: version ?? "",
-                Build_Number: build ?? "1",
-                Branch: branch,
-                CopyRightNotice: String(copyrightNotice_.prefix(32)),
-                Build_Date: buildDate ?? Date(),
-                Algorithm: algo_,
-                AdjustmentFactor: af,
-                Pump: pump_,
-                CGM: cgm.rawValue,
-                insulinType: insulin_type.rawValue,
-                peakActivityTime: iPa,
-                Carbs_24h: await carbTotal,
-                GlucoseStorage_Days: Decimal(roundDouble(Double(rawValue: processedGlucoseStats.numberofDays) ?? 0.0, 1)),
-                Statistics: Stats(
-                    Distribution: processedGlucoseStats.TimeInRange,
-                    Glucose: processedGlucoseStats.avg,
-                    EstimatedA1c: processedGlucoseStats.hbs,
-                    Units: Units(Glucose: units.rawValue, EstimatedA1c: eA1cDisplayUnit.rawValue),
-                    LoopCycles: loopStats,
-                    Insulin: insulin,
-                    Variance: processedGlucoseStats.variance
-                )
-            )
-            storage.save(dailystat, as: file)
-
-            await saveStatsToCoreData()
-        }
-    }
-
-    private func saveStatsToCoreData() async {
-        await privateContext.perform {
-            let saveStatsCoreData = StatsData(context: self.privateContext)
-            saveStatsCoreData.lastrun = Date()
-
-            do {
-                guard self.privateContext.hasChanges else { return }
-                try self.privateContext.save()
-            } catch {
-                print(error.localizedDescription)
-            }
-        }
-    }
-
     private func lastLoopForStats() async -> Date? {
         let requestStats = StatsData.fetchRequest() as NSFetchRequest<StatsData>
         let sortStats = NSSortDescriptor(key: "lastrun", ascending: false)
@@ -1039,32 +916,6 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    private func carbsForStats() async -> Decimal {
-        let requestCarbs = CarbEntryStored.fetchRequest() as NSFetchRequest<CarbEntryStored>
-        let daysAgo = Date().addingTimeInterval(-1.days.timeInterval)
-        requestCarbs.predicate = NSPredicate(format: "carbs > 0 AND date > %@", daysAgo as NSDate)
-        requestCarbs.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
-
-        return await privateContext.perform {
-            do {
-                let carbs = try self.privateContext.fetch(requestCarbs)
-                debugPrint(
-                    "APSManager: statistics() -> \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) fetched carbs"
-                )
-
-                return carbs.reduce(0) { sum, meal in
-                    let mealCarbs = Decimal(string: "\(meal.carbs)") ?? Decimal.zero
-                    return sum + mealCarbs
-                }
-            } catch {
-                debugPrint(
-                    "APSManager: statistics() -> \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) error while fetching carbs"
-                )
-                return 0
-            }
-        }
-    }
-
     private func loopStats(oneDayGlucose: Double) async -> LoopCycles {
         let requestLSR = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
         requestLSR.predicate = NSPredicate(

+ 16 - 5
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -8,13 +8,15 @@ final class OpenAPS {
     private let processQueue = DispatchQueue(label: "OpenAPS.processQueue", qos: .utility)
 
     private let storage: FileStorage
+    private let tddStorage: TDDStorage
 
     let context = CoreDataStack.shared.newTaskContext()
 
     let jsonConverter = JSONConverter()
 
-    init(storage: FileStorage) {
+    init(storage: FileStorage, tddStorage: TDDStorage) {
         self.storage = storage
+        self.tddStorage = tddStorage
     }
 
     static let dateFormatter: ISO8601DateFormatter = {
@@ -284,7 +286,8 @@ final class OpenAPS {
         async let basalAsync = loadFileFromStorageAsync(name: Settings.basalProfile)
         async let autosenseAsync = loadFileFromStorageAsync(name: Settings.autosense)
         async let reservoirAsync = loadFileFromStorageAsync(name: Monitor.reservoir)
-        async let preferencesAsync = loadFileFromStorageAsync(name: Settings.preferences)
+        async let preferencesAsync = storage.retrieveAsync(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
+        async let hasSufficientTddForDynamic = tddStorage.hasSufficientTDD()
 
         // Await the results of asynchronous tasks
         let (
@@ -296,7 +299,7 @@ final class OpenAPS {
             basalProfile,
             autosens,
             reservoir,
-            preferences
+            hasSufficientTdd
         ) = await (
             try parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
             try carbs,
@@ -306,7 +309,7 @@ final class OpenAPS {
             basalAsync,
             autosenseAsync,
             reservoirAsync,
-            preferencesAsync
+            try hasSufficientTddForDynamic
         )
 
         // Meal calculation
@@ -332,6 +335,14 @@ final class OpenAPS {
             storage.save(iob, as: Monitor.iob)
         }
 
+        var preferences = await preferencesAsync
+
+        if !hasSufficientTdd, preferences.useNewFormula || (preferences.useNewFormula && preferences.sigmoid) {
+            debug(.openAPS, "Insufficient TDD for dynamic formula; disabling for determine basal run.")
+            preferences.useNewFormula = false
+            preferences.sigmoid = false
+        }
+
         // Determine basal
         let orefDetermination = try await determineBasal(
             glucose: glucoseAsJSON,
@@ -348,7 +359,7 @@ final class OpenAPS {
             oref2_variables: oref2_variables
         )
 
-        debug(.openAPS, "Determinated: \(orefDetermination)")
+        debug(.openAPS, "OREF DETERMINATION: \(orefDetermination)")
 
         if var determination = Determination(from: orefDetermination), let deliverAt = determination.deliverAt {
             // set both timestamp and deliverAt to the SAME date; this will be updated for timestamp once it is enacted

+ 36 - 258
Trio/Sources/APS/PluginManager.swift

@@ -1,279 +1,57 @@
+import CGMBLEKit
 import Foundation
+import G7SensorKit
+import G7SensorKitUI
+import LibreTransmitter
+import LibreTransmitterUI
 import LoopKit
 import LoopKitUI
 import Swinject
 
 protocol PluginManager {
-    var availablePumpManagers: [PumpManagerDescriptor] { get }
     var availableCGMManagers: [CGMManagerDescriptor] { get }
-    var availableServices: [ServiceDescriptor] { get }
-    func getPumpManagerTypeByIdentifier(_ identifier: String) -> PumpManagerUI.Type?
     func getCGMManagerTypeByIdentifier(_ identifier: String) -> CGMManagerUI.Type?
-    func getServiceTypeByIdentifier(_ identifier: String) -> ServiceUI.Type?
 }
 
 class BasePluginManager: Injectable, PluginManager {
-    let pluginBundles: [Bundle]
+    struct CgmPluginDescription {
+        let pluginIdentifier: String
+        let localizedTitle: String
+        let manager: CGMManagerUI.Type
+    }
+
+    static let cgms = [
+        CgmPluginDescription(
+            pluginIdentifier: G5CGMManager.pluginIdentifier,
+            localizedTitle: String(localized: "Dexcom G5"),
+            manager: G5CGMManager.self
+        ),
+        CgmPluginDescription(
+            pluginIdentifier: G6CGMManager.pluginIdentifier,
+            localizedTitle: String(localized: "Dexcom G6 / ONE"),
+            manager: G6CGMManager.self
+        ),
+        CgmPluginDescription(
+            pluginIdentifier: G7CGMManager.pluginIdentifier,
+            localizedTitle: String(localized: "Dexcom G7 / ONE+"),
+            manager: G7CGMManager.self
+        ),
+        CgmPluginDescription(
+            pluginIdentifier: LibreTransmitterManagerV3.pluginIdentifier,
+            localizedTitle: String(localized: "FreeStyle Libre"),
+            manager: LibreTransmitterManagerV3.self
+        )
+    ]
 
     init(resolver: Resolver) {
-        let pluginsURL: URL? = Bundle.main.privateFrameworksURL
-        var bundles = [Bundle]()
-
-        if let pluginsURL = pluginsURL {
-            do {
-                for pluginURL in try FileManager.default.contentsOfDirectory(at: pluginsURL, includingPropertiesForKeys: nil)
-                    .filter({ $0.path.hasSuffix(".framework") })
-                {
-                    if let bundle = Bundle(url: pluginURL) {
-                        if let bname = bundle.object(forInfoDictionaryKey: "CFBundleName") as? String {
-                            debug(.deviceManager, "bundle name: \(bname)")
-                        }
-                        if let bcgm = bundle.object(forInfoDictionaryKey: "com.loopkit.Loop.CGMManagerIdentifier") as? String {
-                            debug(.deviceManager, "bundle is CGM: \(bcgm)")
-                        }
-
-                        if bundle.isLoopPlugin {
-                            debug(.deviceManager, "Found loop plugin:\(pluginURL.absoluteString)")
-                            bundles.append(bundle)
-                        }
-                    }
-                }
-            } catch {
-                debug(.deviceManager, "Error loading plugin: \(error)")
-            }
-        }
-        pluginBundles = bundles
         injectServices(resolver)
     }
 
-    func getPumpManagerTypeByIdentifier(_ identifier: String) -> PumpManagerUI.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? PumpManagerUIPlugin {
-                            return plugin.pumpManagerType
-                        } else {
-                            fatalError("PrincipalClass does not conform to PumpManagerUIPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
-    }
-
-    var availablePumpManagers: [PumpManagerDescriptor] {
-        pluginBundles.compactMap({ (bundle) -> PumpManagerDescriptor? in
-            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerDisplayName.rawValue) as? String,
-                  let identifier = bundle
-                  .object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String
-            else {
-                return nil
-            }
-
-            return PumpManagerDescriptor(identifier: identifier, localizedTitle: title)
-        })
-    }
-
-    func getCGMManagerTypeByIdentifier(_ identifier: String) -> CGMManagerUI.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? CGMManagerUIPlugin {
-                            return plugin.cgmManagerType
-                        } else {
-                            fatalError("PrincipalClass does not conform to CGMManagerUIPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
+    func getCGMManagerTypeByIdentifier(_ pluginIdentifier: String) -> CGMManagerUI.Type? {
+        BasePluginManager.cgms.filter({ $0.pluginIdentifier == pluginIdentifier }).first?.manager
     }
 
     var availableCGMManagers: [CGMManagerDescriptor] {
-        pluginBundles.compactMap({ (bundle) -> CGMManagerDescriptor? in
-            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerDisplayName.rawValue) as? String,
-                  let identifier = bundle
-                  .object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String
-            else {
-                return nil
-            }
-
-            return CGMManagerDescriptor(identifier: identifier, localizedTitle: title)
-        })
-    }
-
-    func getServiceTypeByIdentifier(_ identifier: String) -> ServiceUI.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? ServiceUIPlugin {
-                            return plugin.serviceType
-                        } else {
-                            fatalError("PrincipalClass does not conform to ServiceUIPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
-    }
-
-    var availableServices: [ServiceDescriptor] {
-        pluginBundles.compactMap({ (bundle) -> ServiceDescriptor? in
-            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceDisplayName.rawValue) as? String,
-                  let identifier = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String
-            else {
-                return nil
-            }
-
-            return ServiceDescriptor(identifier: identifier, localizedTitle: title)
-        })
-    }
-
-    func getStatefulPluginTypeByIdentifier(_ identifier: String) -> StatefulPluggable.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? StatefulPlugin {
-                            return plugin.pluginType
-                        } else {
-                            fatalError("PrincipalClass does not conform to StatefulPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
+        BasePluginManager.cgms.map { CGMManagerDescriptor(identifier: $0.pluginIdentifier, localizedTitle: $0.localizedTitle) }
     }
-
-    var availableStatefulPluginIdentifiers: [String] {
-        pluginBundles.compactMap({ (bundle) -> String? in
-            bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String
-        })
-    }
-
-    func getOnboardingTypeByIdentifier(_ identifier: String) -> OnboardingUI.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? OnboardingUIPlugin {
-                            return plugin.onboardingType
-                        } else {
-                            fatalError("PrincipalClass does not conform to OnboardingUIPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
-    }
-
-    var availableOnboardingIdentifiers: [String] {
-        pluginBundles.compactMap({ (bundle) -> String? in
-            bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String
-        })
-    }
-
-    func getSupportUITypeByIdentifier(_ identifier: String) -> SupportUI.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.supportIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? SupportUIPlugin {
-                            return type(of: plugin.support)
-                        } else {
-                            fatalError("PrincipalClass does not conform to SupportUIPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
-    }
-}
-
-extension Bundle {
-    var isPumpManagerPlugin: Bool {
-        object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String != nil }
-
-    var isCGMManagerPlugin: Bool {
-        object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String != nil }
-
-    var isStatefulPlugin: Bool {
-        object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String != nil }
-
-    var isServicePlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String != nil }
-    var isOnboardingPlugin: Bool {
-        object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String != nil }
-
-    var isSupportPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.supportIdentifier.rawValue) as? String != nil }
-
-    var isLoopPlugin: Bool {
-        isPumpManagerPlugin || isCGMManagerPlugin || isStatefulPlugin || isServicePlugin || isOnboardingPlugin || isSupportPlugin
-    }
-
-    var isLoopExtension: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.extensionIdentifier.rawValue) as? String != nil }
-
-    var isSimulator: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.pluginIsSimulator.rawValue) as? Bool == true }
 }

+ 141 - 113
Trio/Sources/APS/Storage/TDDStorage.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Foundation
 import LoopKitUI
 import Swinject
@@ -10,6 +11,7 @@ protocol TDDStorage {
     ) async throws
         -> TDDResult
     func storeTDD(_ tddResult: TDDResult) async
+    func hasSufficientTDD() async throws -> Bool
 }
 
 /// Structure containing the results of TDD calculations
@@ -26,12 +28,12 @@ struct TDDResult {
 final class BaseTDDStorage: TDDStorage, Injectable {
     @Injected() private var storage: FileStorage!
 
+    private let privateContext = CoreDataStack.shared.newTaskContext()
+
     init(resolver: Resolver) {
         injectServices(resolver)
     }
 
-    private let privateContext = CoreDataStack.shared.newTaskContext()
-
     /// Main function to calculate TDD from pump history and basal profile
     /// - Parameters:
     ///   - pumpManager: Representation of paired pump's PumpManagerUI
@@ -396,117 +398,117 @@ final class BaseTDDStorage: TDDStorage, Injectable {
         return gaps
     }
 
-//    /// Finds gaps between tempBasal events where scheduled basal ran, excluding suspend-resume periods
-//    /// - Parameters:
-//    ///   - tempBasalEvents: Array of pump history events of type tempBasal
-//    ///   - suspendResumePairs: Array of suspend and resume event pairs
-//    /// - Returns: Array of gaps, where each gap has a start and end time
-//    private func findBasalGaps(
-//        in tempBasalEvents: [PumpHistoryEvent],
-//        excluding suspendResumePairs: [(suspend: PumpHistoryEvent, resume: PumpHistoryEvent)]
-//    ) -> [(start: Date, end: Date)] {
-//        guard !tempBasalEvents.isEmpty else {
-//            let startOfDay = Calendar.current.startOfDay(for: Date())
-//            return [(start: startOfDay, end: startOfDay.addingTimeInterval(24 * 60 * 60 - 1))]
-//        }
-//
-//        // Merge temp basal and suspend-resume events into a unified timeline
-//        var timeline = [(start: Date, end: Date, type: EventType)]()
-//
-//        for event in tempBasalEvents {
-//            guard let duration = event.duration else { continue }
-//            let eventEnd = event.timestamp.addingTimeInterval(TimeInterval(duration * 60))
-//            timeline.append((start: event.timestamp, end: eventEnd, type: .tempBasal))
-//        }
-//
-//        for suspendResume in suspendResumePairs {
-//            timeline.append((start: suspendResume.suspend.timestamp, end: suspendResume.resume.timestamp, type: .pumpSuspend))
-//        }
-//
-//        // Sort the timeline by start time
-//        timeline.sort { $0.start < $1.start }
-//
-//        // Process the timeline to calculate gaps
-//        var gaps = [(start: Date, end: Date)]()
-//        var lastEndTime = Calendar.current.startOfDay(for: timeline.first!.start)
-//        let endOfDay = lastEndTime.addingTimeInterval(24 * 60 * 60 - 1)
-//
-//        for interval in timeline {
-//            if interval.type == .pumpSuspend {
-//                // Extend lastEndTime for suspend periods
-//                lastEndTime = max(lastEndTime, interval.end)
-//                continue
-//            }
-//
-//            if interval.start > lastEndTime {
-//                // Add a gap if there is a gap between lastEndTime and interval.start
-//                gaps.append((start: lastEndTime, end: interval.start))
-//            }
-//
-//            // Update lastEndTime to the maximum end time encountered
-//            lastEndTime = max(lastEndTime, interval.end)
-//        }
-//
-//        if lastEndTime < endOfDay {
-//            // Add a final gap if the lastEndTime is before the end of the day
-//            gaps.append((start: lastEndTime, end: endOfDay))
-//        }
-//
-//        return gaps
-//    }
-
-//    /// Calculates scheduled basal insulin delivery during gaps between temporary basals
-//    /// - Parameters:
-//    ///   - gaps: Array of time periods where scheduled basal was active
-//    ///   - profile: Basal profile entries defining rates throughout the day
-//    ///   - roundToSupportedBasalRate: Closure to round rates to pump-supported values
-//    /// - Returns: Total insulin delivered via scheduled basal in units
-//    private func calculateScheduledBasalInsulin(
-//        gaps: [(start: Date, end: Date)],
-//        profile: [BasalProfileEntry],
-//        roundToSupportedBasalRate: @escaping (_ unitsPerHour: Double) -> Double
-//    ) -> Decimal {
-//        // Initialize cached formatter for time string conversion
-//        let timeFormatter: DateFormatter = {
-//            let formatter = DateFormatter()
-//            formatter.dateFormat = "HH:mm:ss"
-//            return formatter
-//        }()
-//
-//        // Pre-calculate profile switch times for efficient lookup
-//        let profileSwitches = profile.map(\.minutes)
-//
-//        return gaps.reduce(into: Decimal(0)) { totalInsulin, gap in
-//            var currentTime = gap.start
-//
-//            while currentTime < gap.end {
-//                // Find applicable basal rate for the current time
-//                guard let rate = findBasalRate(
-//                    for: timeFormatter.string(from: currentTime),
-//                    in: profile
-//                ) else { break }
-//
-//                // Determine when the rate changes (profile switch or gap end)
-//                let nextSwitchTime = getNextBasalRateSwitch(
-//                    after: currentTime,
-//                    switches: profileSwitches,
-//                    calendar: Calendar.current
-//                ) ?? gap.end
-//                let endTime = min(nextSwitchTime, gap.end)
-//                let durationHours = Decimal(endTime.timeIntervalSince(currentTime)) / 3600
-//
-//                let insulin = Decimal(roundToSupportedBasalRate(Double(rate * durationHours)))
-//                totalInsulin += insulin
-//
-//                debug(
-//                    .apsManager,
-//                    "Scheduled Insulin added: \(insulin) U. Duration: \(durationHours) hrs (\(currentTime)-\(endTime))"
-//                )
-//
-//                currentTime = endTime
-//            }
-//        }
-//    }
+    //    /// Finds gaps between tempBasal events where scheduled basal ran, excluding suspend-resume periods
+    //    /// - Parameters:
+    //    ///   - tempBasalEvents: Array of pump history events of type tempBasal
+    //    ///   - suspendResumePairs: Array of suspend and resume event pairs
+    //    /// - Returns: Array of gaps, where each gap has a start and end time
+    //    private func findBasalGaps(
+    //        in tempBasalEvents: [PumpHistoryEvent],
+    //        excluding suspendResumePairs: [(suspend: PumpHistoryEvent, resume: PumpHistoryEvent)]
+    //    ) -> [(start: Date, end: Date)] {
+    //        guard !tempBasalEvents.isEmpty else {
+    //            let startOfDay = Calendar.current.startOfDay(for: Date())
+    //            return [(start: startOfDay, end: startOfDay.addingTimeInterval(24 * 60 * 60 - 1))]
+    //        }
+    //
+    //        // Merge temp basal and suspend-resume events into a unified timeline
+    //        var timeline = [(start: Date, end: Date, type: EventType)]()
+    //
+    //        for event in tempBasalEvents {
+    //            guard let duration = event.duration else { continue }
+    //            let eventEnd = event.timestamp.addingTimeInterval(TimeInterval(duration * 60))
+    //            timeline.append((start: event.timestamp, end: eventEnd, type: .tempBasal))
+    //        }
+    //
+    //        for suspendResume in suspendResumePairs {
+    //            timeline.append((start: suspendResume.suspend.timestamp, end: suspendResume.resume.timestamp, type: .pumpSuspend))
+    //        }
+    //
+    //        // Sort the timeline by start time
+    //        timeline.sort { $0.start < $1.start }
+    //
+    //        // Process the timeline to calculate gaps
+    //        var gaps = [(start: Date, end: Date)]()
+    //        var lastEndTime = Calendar.current.startOfDay(for: timeline.first!.start)
+    //        let endOfDay = lastEndTime.addingTimeInterval(24 * 60 * 60 - 1)
+    //
+    //        for interval in timeline {
+    //            if interval.type == .pumpSuspend {
+    //                // Extend lastEndTime for suspend periods
+    //                lastEndTime = max(lastEndTime, interval.end)
+    //                continue
+    //            }
+    //
+    //            if interval.start > lastEndTime {
+    //                // Add a gap if there is a gap between lastEndTime and interval.start
+    //                gaps.append((start: lastEndTime, end: interval.start))
+    //            }
+    //
+    //            // Update lastEndTime to the maximum end time encountered
+    //            lastEndTime = max(lastEndTime, interval.end)
+    //        }
+    //
+    //        if lastEndTime < endOfDay {
+    //            // Add a final gap if the lastEndTime is before the end of the day
+    //            gaps.append((start: lastEndTime, end: endOfDay))
+    //        }
+    //
+    //        return gaps
+    //    }
+
+    //    /// Calculates scheduled basal insulin delivery during gaps between temporary basals
+    //    /// - Parameters:
+    //    ///   - gaps: Array of time periods where scheduled basal was active
+    //    ///   - profile: Basal profile entries defining rates throughout the day
+    //    ///   - roundToSupportedBasalRate: Closure to round rates to pump-supported values
+    //    /// - Returns: Total insulin delivered via scheduled basal in units
+    //    private func calculateScheduledBasalInsulin(
+    //        gaps: [(start: Date, end: Date)],
+    //        profile: [BasalProfileEntry],
+    //        roundToSupportedBasalRate: @escaping (_ unitsPerHour: Double) -> Double
+    //    ) -> Decimal {
+    //        // Initialize cached formatter for time string conversion
+    //        let timeFormatter: DateFormatter = {
+    //            let formatter = DateFormatter()
+    //            formatter.dateFormat = "HH:mm:ss"
+    //            return formatter
+    //        }()
+    //
+    //        // Pre-calculate profile switch times for efficient lookup
+    //        let profileSwitches = profile.map(\.minutes)
+    //
+    //        return gaps.reduce(into: Decimal(0)) { totalInsulin, gap in
+    //            var currentTime = gap.start
+    //
+    //            while currentTime < gap.end {
+    //                // Find applicable basal rate for the current time
+    //                guard let rate = findBasalRate(
+    //                    for: timeFormatter.string(from: currentTime),
+    //                    in: profile
+    //                ) else { break }
+    //
+    //                // Determine when the rate changes (profile switch or gap end)
+    //                let nextSwitchTime = getNextBasalRateSwitch(
+    //                    after: currentTime,
+    //                    switches: profileSwitches,
+    //                    calendar: Calendar.current
+    //                ) ?? gap.end
+    //                let endTime = min(nextSwitchTime, gap.end)
+    //                let durationHours = Decimal(endTime.timeIntervalSince(currentTime)) / 3600
+    //
+    //                let insulin = Decimal(roundToSupportedBasalRate(Double(rate * durationHours)))
+    //                totalInsulin += insulin
+    //
+    //                debug(
+    //                    .apsManager,
+    //                    "Scheduled Insulin added: \(insulin) U. Duration: \(durationHours) hrs (\(currentTime)-\(endTime))"
+    //                )
+    //
+    //                currentTime = endTime
+    //            }
+    //        }
+    //    }
 
     /// Finds the next basal rate switch time after a given time
     /// - Parameters:
@@ -634,6 +636,32 @@ final class BaseTDDStorage: TDDStorage, Injectable {
             return weightedTDD.truncated(toPlaces: 3)
         }
     }
+
+    /// Checks if there is enough Total Daily Dose (TDD) data collected over the past 7 days.
+    ///
+    /// This function performs a count fetch for TDDStored records in Core Data where:
+    /// - The record's date is within the last 7 days.
+    /// - The total value is greater than 0.
+    ///
+    /// It then checks if at least 85% of the expected data points are present,
+    /// assuming at least 288 expected entries per day (one every 5 minutes).
+    ///
+    /// - Returns: `true` if sufficient TDD data is available, otherwise `false`.
+    /// - Throws: An error if the Core Data count operation fails.
+    func hasSufficientTDD() async throws -> Bool {
+        try await privateContext.perform {
+            let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "TDDStored")
+            fetchRequest.predicate = NSPredicate(
+                format: "date > %@ AND total > 0",
+                Date().addingTimeInterval(-86400 * 7) as NSDate
+            )
+            fetchRequest.resultType = .countResultType
+
+            let count = try self.privateContext.count(for: fetchRequest)
+            let threshold = Int(Double(7 * 288) * 0.85)
+            return count >= threshold
+        }
+    }
 }
 
 /// Extension for rounding Decimal numbers

Разлика између датотеке није приказан због своје велике величине
+ 89 - 11
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 2 - 1
Trio/Sources/Models/Preferences.swift

@@ -298,8 +298,9 @@ extension Preferences: Decodable {
             preferences.sigmoid = sigmoid
         }
 
+        // FIXME: remove this at a later release; hard code it to false for now
         if let enableDynamicCR = try? container.decode(Bool.self, forKey: .enableDynamicCR) {
-            preferences.enableDynamicCR = enableDynamicCR
+            preferences.enableDynamicCR = false
         }
 
         if let useNewFormula = try? container.decode(Bool.self, forKey: .useNewFormula) {

+ 34 - 0
Trio/Sources/Models/TimeInRangeType.swift

@@ -0,0 +1,34 @@
+import Foundation
+
+enum TimeInRangeType: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+    case timeInTightRange
+    case timeInNormoglycemia
+
+    var displayName: String {
+        switch self {
+        case .timeInTightRange:
+            return String(localized: "Time in Tight Range (TITR)", comment: "")
+
+        case .timeInNormoglycemia:
+            return String(localized: "Time in Normoglycemia (TING)", comment: "")
+        }
+    }
+
+    var bottomThreshold: Int {
+        switch self {
+        case .timeInTightRange:
+            return 70
+        case .timeInNormoglycemia:
+            return 63
+        }
+    }
+
+    var topThreshold: Int {
+        switch self {
+        case .timeInNormoglycemia,
+             .timeInTightRange:
+            return 140
+        }
+    }
+}

+ 5 - 0
Trio/Sources/Models/TrioSettings.swift

@@ -69,6 +69,7 @@ struct TrioSettings: JSON, Equatable {
     var useLiveActivity: Bool = false
     var lockScreenView: LockScreenView = .simple
     var bolusShortcut: BolusShortcutLimit = .notAllowed
+    var timeInRangeType: TimeInRangeType = .timeInTightRange
 }
 
 extension TrioSettings: Decodable {
@@ -295,6 +296,10 @@ extension TrioSettings: Decodable {
             settings.bolusShortcut = bolusShortcut
         }
 
+        if let timeInRangeType = try? container.decode(TimeInRangeType.self, forKey: .timeInRangeType) {
+            settings.timeInRangeType = timeInRangeType
+        }
+
         self = settings
     }
 }

+ 1 - 2
Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift

@@ -106,8 +106,7 @@ extension CGMSettings {
                         type: .boolean,
                         label: String(localized: "Smooth Glucose Value"),
                         miniHint: String(localized: "Smooth CGM readings using Savitzky-Golay filtering."),
-                        verboseHint:
-                        VStack(alignment: .leading, spacing: 10) {
+                        verboseHint: VStack(alignment: .leading, spacing: 10) {
                             Text("Default: OFF").bold()
                             Text(
                                 "This filter looks at small groups of nearby readings and fits them to a simple mathematical curve. This process doesn't change the overall pattern of your glucose data but helps smooth out the \"noise\" or irregular fluctuations that could lead to false highs or lows."

+ 118 - 4
Trio/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift

@@ -1,3 +1,5 @@
+import Combine
+import CoreData
 import Observation
 import SwiftUI
 
@@ -5,9 +7,28 @@ extension DynamicSettings {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var settings: SettingsManager!
         @Injected() var storage: FileStorage!
+        @Injected() var tddStorage: TDDStorage!
 
+        // this is an *interim* fix to provide better UI/UX
+        // FIXME: needs to be refactored, once oref-swift lands and dynamicISF becomes swift-bound
+        @Published var dynamicSensitivityType: DynamicSensitivityType = .disabled {
+            didSet {
+                switch dynamicSensitivityType {
+                case .logarithmic:
+                    useNewFormula = true
+                    sigmoid = false
+                case .sigmoid:
+                    useNewFormula = true
+                    sigmoid = true
+                default:
+                    useNewFormula = false
+                    sigmoid = false
+                }
+            }
+        }
+
+        @Published var hasValidTDD: Bool = false
         @Published var useNewFormula: Bool = false
-        @Published var enableDynamicCR: Bool = false
         @Published var sigmoid: Bool = false
         @Published var adjustmentFactor: Decimal = 0.8
         @Published var adjustmentFactorSigmoid: Decimal = 0.5
@@ -15,19 +36,90 @@ extension DynamicSettings {
         @Published var tddAdjBasal: Bool = false
         @Published var threshold_setting: Decimal = 60
 
+        @ObservedObject var pickerSettingsProvider = PickerSettingsProvider.shared
+
         var units: GlucoseUnits = .mgdL
 
+        let context = CoreDataStack.shared.newTaskContext()
+
         override func subscribe() {
             units = settingsManager.settings.units
 
-            subscribePreferencesSetting(\.useNewFormula, on: $useNewFormula) { useNewFormula = $0 }
-            subscribePreferencesSetting(\.enableDynamicCR, on: $enableDynamicCR) { enableDynamicCR = $0 }
-            subscribePreferencesSetting(\.sigmoid, on: $sigmoid) { sigmoid = $0 }
+            /// DynamicISF handling
+            /// Initially, load once from storage and infer `dynamicSensitivityType` based on values of `useNewFormula` (log) and/or `sigmoid`
+            let storedUseNewFormula = settingsManager.preferences.useNewFormula
+            let storedSigmoid = settingsManager.preferences.sigmoid
+            inferDynamicSensitivityType(useNewFormula: storedUseNewFormula, sigmoid: storedSigmoid)
+            /// Subsequently, subscribe to changes from the UI and persist them in the (kept for now) two variables
+            subscribePreferencesSetting(\.useNewFormula, on: $useNewFormula) { _ in }
+            subscribePreferencesSetting(\.sigmoid, on: $sigmoid) { _ in }
+
             subscribePreferencesSetting(\.adjustmentFactor, on: $adjustmentFactor) { adjustmentFactor = $0 }
             subscribePreferencesSetting(\.adjustmentFactorSigmoid, on: $adjustmentFactorSigmoid) { adjustmentFactorSigmoid = $0 }
             subscribePreferencesSetting(\.weightPercentage, on: $weightPercentage) { weightPercentage = $0 }
             subscribePreferencesSetting(\.tddAdjBasal, on: $tddAdjBasal) { tddAdjBasal = $0 }
             subscribePreferencesSetting(\.threshold_setting, on: $threshold_setting) { threshold_setting = $0 }
+
+            Task {
+                do {
+                    let hasValidTDD = try await tddStorage.hasSufficientTDD()
+                    await MainActor.run {
+                        self.hasValidTDD = hasValidTDD
+                    }
+                } catch {
+                    debug(.coreData, "Error when fetching TDD for validity checking: \(error)")
+                    await MainActor.run {
+                        hasValidTDD = false
+                    }
+                }
+            }
+        }
+
+        /// Infers the `dynamicSensitivityType` based on the stored values of `useNewFormula` and `sigmoid`.
+        /// - Logic:
+        ///   - If `useNewFormula` is `true` and `sigmoid` is `false`, sets type to `.logarithmic`.
+        ///   - If both `useNewFormula` and `sigmoid` are `true`, sets type to `.sigmoid`.
+        ///   - Otherwise, sets type to `.disabled`.
+        ///
+        /// This is used at startup to derive the dynamic sensitivity state from persisted values until
+        /// a future refactor makes `dynamicSensitivityType` a first-class stored preference.
+        // FIXME: needs to be refactored, once oref-swift lands and dynamicISF becomes swift-bound
+        private func inferDynamicSensitivityType(useNewFormula: Bool, sigmoid: Bool) {
+            if useNewFormula {
+                dynamicSensitivityType = sigmoid ? .sigmoid : .logarithmic
+            } else {
+                dynamicSensitivityType = .disabled
+            }
+        }
+
+        /// Checks if there is enough Total Daily Dose (TDD) data collected over the past 7 days.
+        ///
+        /// This function performs a count fetch for TDDStored records in Core Data where:
+        /// - The record's date is within the last 7 days.
+        /// - The total value is greater than 0.
+        ///
+        /// It then checks if at least 85% of the expected data points are present,
+        /// assuming at least 288 expected entries per day (one every 5 minutes).
+        ///
+        /// - Returns: `true` if sufficient TDD data is available, otherwise `false`.
+        /// - Throws: An error if the Core Data count operation fails.
+        private func hasSufficientTDD() throws -> Bool {
+            var result = false
+
+            context.performAndWait {
+                let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "TDDStored")
+                fetchRequest.predicate = NSPredicate(
+                    format: "date > %@ AND total > 0",
+                    Date().addingTimeInterval(-86400 * 7) as NSDate
+                )
+                fetchRequest.resultType = .countResultType
+
+                let count = (try? context.count(for: fetchRequest)) ?? 0
+                let threshold = Int(Double(7 * 288) * 0.85)
+                result = count >= threshold
+            }
+
+            return result
         }
     }
 }
@@ -37,3 +129,25 @@ extension DynamicSettings.StateModel: SettingsObserver {
         units = settingsManager.settings.units
     }
 }
+
+extension DynamicSettings {
+    enum DynamicSensitivityType: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+        var id: String { rawValue }
+        case disabled
+        case logarithmic
+        case sigmoid
+
+        var displayName: String {
+            switch self {
+            case .disabled:
+                return String(localized: "Disabled")
+
+            case .logarithmic:
+                return String(localized: "Logarithmic")
+
+            case .sigmoid:
+                return String(localized: "Sigmoid")
+            }
+        }
+    }
+}

+ 125 - 131
Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift

@@ -41,111 +41,104 @@ extension DynamicSettings {
 
         var body: some View {
             List {
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.useNewFormula,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "Activate Dynamic Sensitivity (Dynamic ISF)")
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: String(localized: "Activate Dynamic ISF"),
-                    miniHint: String(
-                        localized: "Dynamically adjust insulin sensitivity using Dynamic Ratio rather than Autosens Ratio."
-                    ),
-                    verboseHint:
-                    VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text(
-                            "Enabling this feature allows Trio to calculate a new Insulin Sensitivity Factor with each loop cycle by considering your current glucose, the weighted total daily dose of insulin, the set adjustment factor, and a few other data points. This helps tailor your insulin response more accurately in real time."
-                        )
-                        Text(
-                            "Dynamic ISF produces a Dynamic Ratio, replacing the Autosens Ratio, determining how much your profile ISF will be adjusted every loop cycle, ensuring it stays within safe limits set by your Autosens Min/Max settings. It provides more precise insulin dosing by responding to changes in insulin needs throughout the day."
-                        )
-                        Text(
-                            "You can influence the adjustments made by Dynamic ISF primarily by adjusting Autosens Max, Autosens Min, and Adjustment Factor. Other settings also influence Dynamic ISF's response, such as Glucose Target, Profile ISF, Peak Insulin Time, and Weighted Average of TDD."
-                        )
-                        Text(
-                            "Warning: Before adjusting these settings, make sure you are fully aware of the impact those changes will have."
-                        )
-                        .bold()
-                    },
-                    headerText: String(localized: "Dynamic Settings")
-                )
-
-                if state.useNewFormula {
-                    SettingInputSection(
-                        decimalValue: $decimalPlaceholder,
-                        booleanValue: $state.enableDynamicCR,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = String(localized: "Activate Dynamic CR (Carb Ratio)")
+                Section(
+                    header: Text("Dynamic Insulin Sensitivity"),
+                    content: {
+                        VStack(alignment: .leading) {
+                            Picker(
+                                selection: $state.dynamicSensitivityType,
+                                label: Text("Dynamic ISF").multilineTextAlignment(.leading)
+                            ) {
+                                ForEach(DynamicSensitivityType.allCases) { selection in
+                                    Text(selection.displayName).tag(selection)
+                                }
                             }
-                        ),
-                        units: state.units,
-                        type: .boolean,
-                        label: String(localized: "Activate Dynamic CR (Carb Ratio)"),
-                        miniHint: String(localized: "Dynamically adjust your Carb Ratio (CR)."),
-                        verboseHint:
+                            .disabled(!state.hasValidTDD)
+                            .padding(.top)
 
-                        VStack(alignment: .leading, spacing: 10) {
-                            Text("Default: OFF").bold()
-                            Text(
-                                "Dynamic CR adjusts your carb ratio based on your Dynamic Ratio, adapting automatically to changes in insulin sensitivity."
-                            )
-                            Text(
-                                "When Dynamic Ratio increases, indicating you need more insulin, the carb ratio value is decreased to make your insulin dosing more effective."
-                            )
-                            Text(
-                                "When Dynamic Ratio decreases, indicating you need less insulin, the carb ratio value is increased to avoid over-delivery."
-                            )
-                        }
-                    )
+                            HStack(alignment: .center) {
+                                let miniHintText = state.hasValidTDD ?
+                                    String(
+                                        localized: "Dynamically adjust insulin sensitivity using Dynamic Ratio rather than Autosens Ratio."
+                                    ) :
+                                    String(
+                                        localized: "Trio has only been actively used and looping for less than seven days. Cannot enable dynamic ISF."
+                                    )
+                                let miniHintTextColorForDisabled: Color = colorScheme == .dark ? .orange :
+                                    .accentColor
+                                let miniHintTextColor: Color = state.hasValidTDD ? .secondary : miniHintTextColorForDisabled
 
-                    SettingInputSection(
-                        decimalValue: $decimalPlaceholder,
-                        booleanValue: $state.sigmoid,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = String(localized: "Use Sigmoid Formula")
-                            }
-                        ),
-                        units: state.units,
-                        type: .boolean,
-                        label: String(localized: "Use Sigmoid Formula"),
-                        miniHint: String(localized: "Adjust insulin sensitivity using a sigmoid-shaped curve."),
-                        verboseHint:
-                        VStack(alignment: .leading, spacing: 10) {
-                            Text("Default: OFF").bold()
-                            Text(
-                                "Turning on the Sigmoid Formula setting alters how your Dynamic Ratio, and thus your New ISF and New Carb Ratio, are calculated using a sigmoid curve rather than the default logarithmic function."
-                            )
-                            Text(
-                                "The curve's steepness is influenced by the Adjustment Factor, while the Autosens Min/Max settings determine the limits of the ratio adjustment, which can also influence the steepness of the sigmoid curve."
-                            )
-                            Text(
-                                "When using the Sigmoid Formula, the weighted Total Daily Dose has a much lower impact on the dynamic adjustments to sensitivity."
-                            )
-                            Text("Careful tuning is essential to avoid overly aggressive insulin changes.")
-                            Text("It is not recommended to set Autosens Max above 150% to maintain safe insulin dosing.")
-                            Text(
-                                "There has been no empirical data analysis to support the use of the Sigmoid Formula for dynamic sensitivity determination."
-                            ).bold()
-                        }
-                    )
+                                Text(miniHintText)
+                                    .font(.footnote)
+                                    .foregroundColor(miniHintTextColor)
+                                    .lineLimit(nil)
 
-                    if !state.sigmoid {
+                                Spacer()
+                                Button(
+                                    action: {
+                                        hintLabel = String(localized: "Time in Range Chart Style")
+                                        selectedVerboseHint =
+                                            AnyView(
+                                                VStack(alignment: .leading, spacing: 10) {
+                                                    Text("Default: Disabled").bold()
+                                                    Text(
+                                                        "Enabling this feature allows Trio to calculate a new Insulin Sensitivity Factor with each loop cycle dynamically. Trio offers two dynamic formulas:"
+                                                    )
+                                                    VStack(alignment: .leading, spacing: 10) {
+                                                        Text("Logarithmic Dynamic ISF").bold()
+                                                        Text(
+                                                            "Enabling this feature allows Trio to calculate a new Insulin Sensitivity Factor with each loop cycle by considering your current glucose, the weighted total daily dose of insulin, the set adjustment factor, and a few other data points. This helps tailor your insulin response more accurately in real time."
+                                                        )
+                                                        Text(
+                                                            "Dynamic ISF produces a Dynamic Ratio, replacing the Autosens Ratio, determining how much your profile ISF will be adjusted every loop cycle, ensuring it stays within safe limits set by your Autosens Min/Max settings. It provides more precise insulin dosing by responding to changes in insulin needs throughout the day."
+                                                        )
+                                                        Text(
+                                                            "You can influence the adjustments made by Dynamic ISF primarily by adjusting Autosens Max, Autosens Min, and Adjustment Factor. Other settings also influence Dynamic ISF's response, such as Glucose Target, Profile ISF, Peak Insulin Time, and Weighted Average of TDD."
+                                                        )
+                                                        Text(
+                                                            "Warning: Before adjusting these settings, make sure you are fully aware of the impact those changes will have."
+                                                        )
+                                                        .bold()
+                                                    }
+
+                                                    VStack(alignment: .leading, spacing: 10) {
+                                                        Text("Sigmoid Dynamic ISF").bold()
+                                                        Text(
+                                                            "Turning on the Sigmoid Formula setting alters how your Dynamic Ratio, and thus your New ISF, are calculated using a sigmoid curve."
+                                                        )
+                                                        Text(
+                                                            "The curve's steepness is influenced by the Adjustment Factor, while the Autosens Min/Max settings determine the limits of the ratio adjustment, which can also influence the steepness of the sigmoid curve."
+                                                        )
+                                                        Text(
+                                                            "When using the Sigmoid Formula, the weighted Total Daily Dose has a much lower impact on the dynamic adjustments to sensitivity."
+                                                        )
+                                                        Text(
+                                                            "Careful tuning is essential to avoid overly aggressive insulin changes."
+                                                        )
+                                                        Text(
+                                                            "It is not recommended to set Autosens Max above 150% to maintain safe insulin dosing."
+                                                        )
+                                                        Text(
+                                                            "There has been no empirical data analysis to support the use of the Sigmoid Formula for dynamic sensitivity determination."
+                                                        ).bold()
+                                                    }
+                                                }
+                                            )
+                                        shouldDisplayHint.toggle()
+                                    },
+                                    label: {
+                                        HStack {
+                                            Image(systemName: "questionmark.circle")
+                                        }
+                                    }
+                                ).buttonStyle(BorderlessButtonStyle())
+                            }.padding(.top)
+                        }.padding(.bottom)
+                    }
+                ).listRowBackground(Color.chart)
+
+                if state.dynamicSensitivityType != .disabled {
+                    if state.dynamicSensitivityType == .logarithmic {
                         SettingInputSection(
                             decimalValue: $state.adjustmentFactor,
                             booleanValue: $booleanPlaceholder,
@@ -176,6 +169,35 @@ extension DynamicSettings {
                                 )
                             }
                         )
+
+                        SettingInputSection(
+                            decimalValue: $state.weightPercentage,
+                            booleanValue: $booleanPlaceholder,
+                            shouldDisplayHint: $shouldDisplayHint,
+                            selectedVerboseHint: Binding(
+                                get: { selectedVerboseHint },
+                                set: {
+                                    selectedVerboseHint = $0.map { AnyView($0) }
+                                    hintLabel = String(localized: "Weighted Average of TDD")
+                                }
+                            ),
+                            units: state.units,
+                            type: .decimal("weightPercentage"),
+                            label: String(localized: "Weighted Average of TDD"),
+                            miniHint: String(localized: "Weight of 24-hr TDD against 10-day TDD."),
+                            verboseHint:
+                            VStack(alignment: .leading, spacing: 10) {
+                                Text("Default: 35%").bold()
+                                Text(
+                                    "This setting adjusts how much weight is given to your recent total daily insulin dose when calculating Dynamic ISF and Dynamic CR."
+                                )
+                                Text(
+                                    "At the default setting, 35% of the calculation is based on the last 24 hours of insulin use, with the remaining 65% considering the last 10 days of data."
+                                )
+                                Text("Setting this to 100% means only the past 24 hours will be used.")
+                                Text("A lower value smooths out these variations for more stability.")
+                            }
+                        )
                     } else {
                         SettingInputSection(
                             decimalValue: $state.adjustmentFactorSigmoid,
@@ -212,35 +234,6 @@ extension DynamicSettings {
                     }
 
                     SettingInputSection(
-                        decimalValue: $state.weightPercentage,
-                        booleanValue: $booleanPlaceholder,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = String(localized: "Weighted Average of TDD")
-                            }
-                        ),
-                        units: state.units,
-                        type: .decimal("weightPercentage"),
-                        label: String(localized: "Weighted Average of TDD"),
-                        miniHint: String(localized: "Weight of 24-hr TDD against 10-day TDD."),
-                        verboseHint:
-                        VStack(alignment: .leading, spacing: 10) {
-                            Text("Default: 35%").bold()
-                            Text(
-                                "This setting adjusts how much weight is given to your recent total daily insulin dose when calculating Dynamic ISF and Dynamic CR."
-                            )
-                            Text(
-                                "At the default setting, 35% of the calculation is based on the last 24 hours of insulin use, with the remaining 65% considering the last 10 days of data."
-                            )
-                            Text("Setting this to 100% means only the past 24 hours will be used.")
-                            Text("A lower value smooths out these variations for more stability.")
-                        }
-                    )
-
-                    SettingInputSection(
                         decimalValue: $decimalPlaceholder,
                         booleanValue: $state.tddAdjBasal,
                         shouldDisplayHint: $shouldDisplayHint,
@@ -265,7 +258,8 @@ extension DynamicSettings {
                             )
                             Text("Autosens Ratio =\n(Weighted Average of TDD) ÷ (10-day Average of TDD)")
                             Text("New Basal Profile =\n(Current Basal Profile) × (Autosens Ratio)")
-                        }
+                        },
+                        headerText: String(localized: "Dynamic-dependent Features")
                     )
 
                     SettingInputSection(

+ 3 - 0
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -63,6 +63,7 @@ extension Home {
         var alarm: GlucoseAlarm?
         var manualTempBasal = false
         var isSmoothingEnabled = false
+        var maxIOB: Decimal = 0.0
         var autosensMax: Decimal = 1.2
         var lowGlucose: Decimal = 70
         var highGlucose: Decimal = 180
@@ -395,6 +396,7 @@ extension Home {
             highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
             lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
             settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
+            maxIOB = settingsManager.preferences.maxIOB
         }
 
         @MainActor private func setupCGMSettings() async {
@@ -670,6 +672,7 @@ extension Home.StateModel:
         highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
         isExerciseModeActive = settingsManager.preferences.exerciseMode
         lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
+        maxIOB = settingsManager.preferences.maxIOB
     }
 
     func pumpSettingsDidChange(_: PumpSettings) {

+ 32 - 23
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -460,32 +460,41 @@ extension Home {
 
                 Spacer()
 
-                HStack {
-                    if state.pumpSuspended {
-                        Text("Pump suspended")
-                            .font(.callout).fontWeight(.bold).fontDesign(.rounded)
-                            .foregroundColor(.loopGray)
-                    } else if let tempBasalString = tempBasalString {
-                        Image(systemName: "drop.circle")
-                            .font(.callout)
-                            .foregroundColor(.insulinTintColor)
-                        if tempBasalString.count > 5 {
-                            Text(tempBasalString)
+                if state.maxIOB == 0.0 {
+                    HStack {
+                        Image(systemName: "exclamationmark.circle.fill")
+                        Text("MaxIOB: 0 U")
+                    }.bold()
+                        .foregroundStyle(Color.red)
+                        .font(.callout)
+                } else {
+                    HStack {
+                        if state.pumpSuspended {
+                            Text("Pump suspended")
                                 .font(.callout).fontWeight(.bold).fontDesign(.rounded)
-                                .lineLimit(1)
-                                .minimumScaleFactor(0.85)
-                                .truncationMode(.tail)
-                                .allowsTightening(true)
+                                .foregroundColor(.loopGray)
+                        } else if let tempBasalString = tempBasalString {
+                            Image(systemName: "drop.circle")
+                                .font(.callout)
+                                .foregroundColor(.insulinTintColor)
+                            if tempBasalString.count > 5 {
+                                Text(tempBasalString)
+                                    .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                                    .lineLimit(1)
+                                    .minimumScaleFactor(0.85)
+                                    .truncationMode(.tail)
+                                    .allowsTightening(true)
+                            } else {
+                                // Short strings can just display normally
+                                Text(tempBasalString).font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                            }
                         } else {
-                            // Short strings can just display normally
-                            Text(tempBasalString).font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                            Image(systemName: "drop.circle")
+                                .font(.callout)
+                                .foregroundColor(.insulinTintColor)
+                            Text("No Data")
+                                .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                         }
-                    } else {
-                        Image(systemName: "drop.circle")
-                            .font(.callout)
-                            .foregroundColor(.insulinTintColor)
-                        Text("No Data")
-                            .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                     }
                 }
             }.padding(.horizontal)

+ 2 - 1
Trio/Sources/Modules/Settings/SettingsStateModel.swift

@@ -1,6 +1,7 @@
 import LoopKit
 import LoopKitUI
 import SwiftUI
+import TidepoolServiceKit
 
 extension Settings {
     final class StateModel: BaseStateModel<Provider> {
@@ -37,7 +38,7 @@ extension Settings {
 
             copyrightNotice = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
 
-            serviceUIType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
+            serviceUIType = TidepoolService.self as? ServiceUI.Type
         }
 
         func logItems() -> [URL] {

+ 6 - 3
Trio/Sources/Modules/Stat/StatStateModel+Setup/StackedChartSetup.swift

@@ -93,9 +93,12 @@ extension Stat.StateModel {
             // Ranges are processed from bottom to top in the stacked chart
             let ranges: [(name: String, condition: (Int) -> Bool)] = [
                 ("<54", { g in g <= 54 }),
-                ("54-70", { g in g > 54 && g < 70 }),
-                ("70-140", { g in g >= 70 && g <= 140 }),
-                ("140-180", { g in g > 140 && g <= 180 }),
+                ("54-\(self.timeInRangeType.bottomThreshold)", { g in g > 54 && g < self.timeInRangeType.bottomThreshold }),
+                (
+                    "\(self.timeInRangeType.bottomThreshold)-\(self.timeInRangeType.topThreshold)",
+                    { g in g >= self.timeInRangeType.bottomThreshold && g <= self.timeInRangeType.topThreshold }
+                ),
+                ("\(self.timeInRangeType.topThreshold)-180", { g in g > self.timeInRangeType.topThreshold && g <= 180 }),
                 ("180-200", { g in g > 180 && g <= 200 }),
                 ("200-220", { g in g > 200 && g <= 220 }),
                 (">220", { g in g > 220 })

+ 2 - 0
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -11,6 +11,7 @@ extension Stat {
         var lowLimit: Decimal = 70
         var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         var units: GlucoseUnits = .mgdL
+        var timeInRangeType: TimeInRangeType = .timeInTightRange
         var useFPUconversion: Bool = false
         var glucoseFromPersistence: [GlucoseStored] = []
         var loopStatRecords: [LoopStatRecord] = []
@@ -85,6 +86,7 @@ extension Stat {
             units = settingsManager.settings.units
             eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
             useFPUconversion = settingsManager.settings.useFPUconversion
+            timeInRangeType = settingsManager.settings.timeInRangeType
         }
 
         func setupGlucoseArray(for interval: StatsTimeIntervalWithToday) {

+ 4 - 2
Trio/Sources/Modules/Stat/View/StatRootView.swift

@@ -130,7 +130,8 @@ extension Stat {
                             highLimit: state.highLimit,
                             lowLimit: state.lowLimit,
                             units: state.units,
-                            glucoseRangeStats: state.glucoseRangeStats
+                            glucoseRangeStats: state.glucoseRangeStats,
+                            timeInRangeType: state.timeInRangeType
                         )
                     }
                 }
@@ -144,7 +145,8 @@ extension Stat {
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
-                        glucose: state.glucoseFromPersistence
+                        glucose: state.glucoseFromPersistence,
+                        timeInRangeType: state.timeInRangeType
                     )
 
                     Divider()

+ 10 - 6
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift

@@ -7,6 +7,7 @@ struct GlucoseDistributionChart: View {
     let lowLimit: Decimal
     let units: GlucoseUnits
     let glucoseRangeStats: [GlucoseRangeStats]
+    let timeInRangeType: TimeInRangeType
 
     var body: some View {
         VStack(alignment: .leading, spacing: 8) {
@@ -25,9 +26,9 @@ struct GlucoseDistributionChart: View {
             }
             .chartForegroundStyleScale([
                 "<54": .purple.opacity(0.7),
-                "54-70": .red.opacity(0.7),
-                "70-140": .green,
-                "140-180": .green.opacity(0.7),
+                "54-\(timeInRangeType.bottomThreshold)": .red.opacity(0.7),
+                "\(timeInRangeType.bottomThreshold)-\(timeInRangeType.topThreshold)": .green,
+                "\(timeInRangeType.topThreshold)-180": .green.opacity(0.7),
                 "180-200": .yellow.opacity(0.7),
                 "200-220": .orange.opacity(0.7),
                 ">220": .orange.opacity(0.8)
@@ -36,12 +37,15 @@ struct GlucoseDistributionChart: View {
                 let legendItems: [(String, Color)] = [
                     ("<\(units == .mgdL ? Decimal(54) : 54.asMmolL)", .purple.opacity(0.7)),
                     (
-                        "\(units == .mgdL ? Decimal(54) : 54.asMmolL)-\(units == .mgdL ? Decimal(70) : 70.asMmolL)",
+                        "\(units == .mgdL ? Decimal(54) : 54.asMmolL)-\(units == .mgdL ? Decimal(timeInRangeType.bottomThreshold) : timeInRangeType.bottomThreshold.asMmolL)",
                         .red.opacity(0.7)
                     ),
-                    ("\(units == .mgdL ? Decimal(70) : 70.asMmolL)-\(units == .mgdL ? Decimal(140) : 140.asMmolL)", .green),
                     (
-                        "\(units == .mgdL ? Decimal(140) : 140.asMmolL)-\(units == .mgdL ? Decimal(180) : 180.asMmolL)",
+                        "\(units == .mgdL ? Decimal(timeInRangeType.bottomThreshold) : timeInRangeType.bottomThreshold.asMmolL)-\(units == .mgdL ? Decimal(timeInRangeType.topThreshold) : timeInRangeType.topThreshold.asMmolL)",
+                        .green
+                    ),
+                    (
+                        "\(units == .mgdL ? Decimal(timeInRangeType.topThreshold) : timeInRangeType.topThreshold.asMmolL)-\(units == .mgdL ? Decimal(180) : 180.asMmolL)",
                         .green.opacity(0.7)
                     ),
                     (

+ 14 - 5
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift

@@ -8,6 +8,7 @@ struct GlucoseSectorChart: View {
     let lowLimit: Decimal
     let units: GlucoseUnits
     let glucose: [GlucoseStored]
+    let timeInRangeType: TimeInRangeType
 
     @State private var selectedCount: Int?
     @State private var selectedRange: GlucoseRange?
@@ -28,8 +29,9 @@ struct GlucoseSectorChart: View {
             let total = Decimal(glucose.count)
             // Count readings between high limit and 250 mg/dL (high)
             let high = glucose.filter { $0.glucose > Int(highLimit) }.count
-            // Count readings between low limit and 140 mg/dL (tight control)
-            let tight = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= 140 }.count
+            // Count readings between low limit (TITR: 70 mg/dL, TING 63 mg/dL) and 140 mg/dL (tight control)
+            let tight = glucose
+                .filter { $0.glucose >= timeInRangeType.bottomThreshold && $0.glucose <= timeInRangeType.topThreshold }.count
             // Count readings between 140 and high limit (normal range)
             let normal = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= Int(highLimit) }.count
             // Count readings between 54 and low limit (low)
@@ -54,7 +56,11 @@ struct GlucoseSectorChart: View {
                 }
 
                 VStack(alignment: .leading, spacing: 5) {
-                    Text("\(formatValue(lowLimit))-\(formatValue(140))").font(.subheadline).foregroundStyle(Color.secondary)
+                    Text(
+                        "\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(Decimal(timeInRangeType.topThreshold)))"
+                    )
+                    .font(.subheadline)
+                    .foregroundStyle(Color.secondary)
                     Text(tightPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
                         .foregroundStyle(Color.green)
                 }
@@ -219,7 +225,8 @@ struct GlucoseSectorChart: View {
             )
 
         case .inRange:
-            let tight = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= 140 }.count
+            let tight = glucose
+                .filter { $0.glucose >= Int(timeInRangeType.bottomThreshold) && $0.glucose <= timeInRangeType.topThreshold }.count
             let glucoseValues = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= Int(highLimit) }
             let glucoseValuesAsInt = glucoseValues.map { Int($0.glucose) }
             let (average, median, standardDeviation) = calculateDetailedStatistics(for: glucoseValuesAsInt)
@@ -233,7 +240,9 @@ struct GlucoseSectorChart: View {
                         formatPercentage(Decimal(glucoseValues.count) / total * 100)
                     ),
                     (
-                        String(localized: "Tight (\(formatValue(lowLimit))-\(formatValue(140)))"),
+                        String(
+                            localized: "\(timeInRangeType == .timeInTightRange ? "TITR" : "TING") (\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(Decimal(timeInRangeType.topThreshold))))"
+                        ),
                         formatPercentage(Decimal(tight) / total * 100)
                     ),
                     (String(localized: "Average"), formatValue(average)),

+ 3 - 0
Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift

@@ -12,6 +12,7 @@ extension UserInterfaceSettings {
         @Published var carbsRequiredThreshold: Decimal = 0
         @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
         @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
+        @Published var timeInRangeType: TimeInRangeType = .timeInTightRange
 
         var units: GlucoseUnits = .mgdL
 
@@ -39,6 +40,8 @@ extension UserInterfaceSettings {
             subscribeSetting(\.glucoseColorScheme, on: $glucoseColorScheme) { glucoseColorScheme = $0 }
 
             subscribeSetting(\.eA1cDisplayUnit, on: $eA1cDisplayUnit) { eA1cDisplayUnit = $0 }
+
+            subscribeSetting(\.timeInRangeType, on: $timeInRangeType) { timeInRangeType = $0 }
         }
     }
 }

+ 69 - 0
Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -419,6 +419,75 @@ extension UserInterfaceSettings {
                     }
                 ).listRowBackground(Color.chart)
 
+                Section {
+                    VStack(alignment: .leading) {
+                        Picker(
+                            selection: $state.timeInRangeType,
+                            label: Text("Time in Range Type").multilineTextAlignment(.leading)
+                        ) {
+                            ForEach(TimeInRangeType.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }.padding(.top)
+
+                        HStack(alignment: .center) {
+                            Text(
+                                "Choose type of time in range to be used for Trio's statistics."
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    hintLabel = String(localized: "Time in Range Type")
+                                    selectedVerboseHint =
+                                        AnyView(
+                                            VStack(
+                                                alignment: .leading,
+                                                spacing: 10
+                                            ) {
+                                                Text(
+                                                    "Choose which type of time in range Trio should adopt for all its statistical charts and displays:"
+                                                )
+                                                VStack(
+                                                    alignment: .leading,
+                                                    spacing: 5
+                                                ) {
+                                                    Text(
+                                                        "Time in Tight Range (TITR):"
+                                                    )
+                                                    .bold()
+                                                    Text(
+                                                        "Uses the fairly established Time in Tight Range definition, which is defined as time between \(state.units == .mgdL ? Decimal(70) : 70.asMmolL) and \(state.units == .mgdL ? Decimal(140) : 140.asMmolL) \(state.units.rawValue)."
+                                                    )
+                                                }
+                                                VStack(
+                                                    alignment: .leading,
+                                                    spacing: 5
+                                                ) {
+                                                    Text(
+                                                        "Time in Normoglycemia (TING):"
+                                                    )
+                                                    .bold()
+                                                    Text(
+                                                        "Uses the very new – first discussed at ATTD 2025 in Amsterdam, NL – Time in Normoglycemia definition, which adopts its range as all values between the normoglycemic minimum threshold (\(state.units == .mgdL ? Decimal(63) : 63.asMmolL) \(state.units.rawValue)) and \(state.units == .mgdL ? Decimal(140) : 140.asMmolL) \(state.units.rawValue)."
+                                                    )
+                                                }
+                                            }
+                                        )
+                                    shouldDisplayHint.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }.listRowBackground(Color.chart)
+
                 SettingInputSection(
                     decimalValue: $state.carbsRequiredThreshold,
                     booleanValue: $state.showCarbsRequiredBadge,

+ 4 - 3
Trio/Sources/Services/Network/TidepoolManager.swift

@@ -5,6 +5,7 @@ import HealthKit
 import LoopKit
 import LoopKitUI
 import Swinject
+import TidepoolServiceKit
 
 protocol TidepoolManager {
     func addTidepoolService(service: Service)
@@ -96,12 +97,12 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
 
     /// Loads the Tidepool service from raw stored data
     private func tidepoolServiceFromRaw(_ rawValue: [String: Any]) -> RemoteDataService? {
-        guard let rawState = rawValue["state"] as? Service.RawStateValue,
-              let serviceType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
+        let serviceType = TidepoolService.self
+        guard let rawState = rawValue["state"] as? Service.RawStateValue
         else { return nil }
 
         if let service = serviceType.init(rawState: rawState) {
-            return service as? RemoteDataService
+            return service as RemoteDataService
         }
         return nil
     }

+ 0 - 53
TrioTests/PluginManagerTests.swift

@@ -24,68 +24,15 @@ import Testing
             let cgmLoopManager = pluginManager.getCGMManagerTypeByIdentifier(cgmLoop.identifier)
             #expect(cgmLoopManager != nil, "Should load valid CGM manager")
         }
-
-        // When trying to load CGM manager with pump identifier
-        if let cgmLoop = cgmLoopManagers.last {
-            let invalidManager = pluginManager.getPumpManagerTypeByIdentifier(cgmLoop.identifier)
-            #expect(invalidManager == nil, "Should not load CGM manager with pump identifier")
-        }
-    }
-
-    @Test("Can load pump managers") func testPumpManagerLoad() {
-        // Given
-        let pumpLoopManagers = pluginManager.availablePumpManagers
-
-        // Then
-        #expect(!pumpLoopManagers.isEmpty, "Should have available pump managers")
-
-        // When loading valid pump manager
-        if let pumpLoop = pumpLoopManagers.first {
-            let pumpLoopManager = pluginManager.getPumpManagerTypeByIdentifier(pumpLoop.identifier)
-            #expect(pumpLoopManager != nil, "Should load valid pump manager")
-        }
-
-        // When trying to load pump manager with CGM identifier
-        if let pumpLoop = pumpLoopManagers.last {
-            let invalidManager = pluginManager.getCGMManagerTypeByIdentifier(pumpLoop.identifier)
-            #expect(invalidManager == nil, "Should not load pump manager with CGM identifier")
-        }
-    }
-
-    @Test("Can load service managers") func testServiceManagerLoad() {
-        // Given
-        let serviceManagers = pluginManager.availableServices
-
-        // Then
-        #expect(!serviceManagers.isEmpty, "Should have available services")
-
-        // When
-        if let serviceLoop = serviceManagers.first {
-            let serviceManager = pluginManager.getServiceTypeByIdentifier(serviceLoop.identifier)
-            #expect(serviceManager != nil, "Should load valid service manager")
-        }
     }
 
     @Test("Available managers have valid descriptors") func testManagerDescriptors() {
         // Given/When
-        let pumpManagers = pluginManager.availablePumpManagers
         let cgmManagers = pluginManager.availableCGMManagers
-        let serviceManagers = pluginManager.availableServices
-
-        // Then
-        for manager in pumpManagers {
-            #expect(!manager.identifier.isEmpty, "Pump manager should have non-empty identifier")
-            #expect(!manager.localizedTitle.isEmpty, "Pump manager should have non-empty title")
-        }
 
         for manager in cgmManagers {
             #expect(!manager.identifier.isEmpty, "CGM manager should have non-empty identifier")
             #expect(!manager.localizedTitle.isEmpty, "CGM manager should have non-empty title")
         }
-
-        for manager in serviceManagers {
-            #expect(!manager.identifier.isEmpty, "Service should have non-empty identifier")
-            #expect(!manager.localizedTitle.isEmpty, "Service should have non-empty title")
-        }
     }
 }

+ 0 - 41
scripts/copy-plugins.sh

@@ -1,41 +0,0 @@
-#!/bin/sh -e
-
-#  copy-plugins.sh
-#  Loop
-#
-#  Copyright © 2019 LoopKit Authors. All rights reserved.
-
-
-shopt -s nullglob
-
-# Copy device plugins
-function copy_plugins {
-    echo "Looking for plugins in $1"
-    for f in "$1"/*.loopplugin; do
-      plugin=$(basename "$f")
-      echo Copying plugin: $plugin to frameworks directory in app
-      plugin_path="$(readlink -f "$f" || echo "$f")"
-      plugin_as_framework_path="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/${plugin%.*}.framework"
-      rsync -va --exclude=Frameworks "$plugin_path/." "${plugin_as_framework_path}"
-      # Rename .plugin to .framework
-      if [ "$EXPANDED_CODE_SIGN_IDENTITY" != "-" ] && [ "$EXPANDED_CODE_SIGN_IDENTITY" != "" ]; then
-        export CODESIGN_ALLOCATE=${DT_TOOLCHAIN_DIR}/usr/bin/codesign_allocate
-        echo "Signing ${plugin} with ${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
-        /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --timestamp=none --preserve-metadata=identifier,entitlements,flags "$plugin_as_framework_path"
-      else
-        echo "Skipping signing, no identity set"
-      fi
-      for framework_path in "${f}"/Frameworks/*.framework; do
-        framework=$(basename "$framework_path")
-        echo "Copying plugin's framework $framework_path to ${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/."
-        cp -avf "$framework_path" "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/."
-        plugin_path="$(readlink -f "$f" || echo "$f")"
-        if [ "$EXPANDED_CODE_SIGN_IDENTITY" != "-" ] && [ "$EXPANDED_CODE_SIGN_IDENTITY" != "" ]; then
-          echo "Signing $framework for $plugin with $EXPANDED_CODE_SIGN_IDENTITY_NAME"
-          /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --timestamp=none --preserve-metadata=identifier,entitlements,flags "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/${framework}"
-        fi
-      done
-    done
-}
-
-copy_plugins "$BUILT_PRODUCTS_DIR"