Просмотр исходного кода

Merge pull request #67 from avouspierre/alpha

Update with tide pool + calibration + CGM home screen
bjornoleh 2 лет назад
Родитель
Сommit
d529658aed
41 измененных файлов с 1453 добавлено и 139 удалено
  1. 3 0
      .gitmodules
  2. 8 0
      BuildDetails.plist
  3. 68 0
      FreeAPS.xcodeproj/project.pbxproj
  4. 65 58
      FreeAPS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  5. 22 8
      FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme
  6. 3 0
      FreeAPS.xcworkspace/contents.xcworkspacedata
  7. 14 5
      FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved
  8. 0 4
      FreeAPS/Resources/Info.plist
  9. 119 0
      FreeAPS/Sources/APS/CGM/Calibrations/CalibrationService.swift
  10. 8 1
      FreeAPS/Sources/APS/CGM/PluginSource.swift
  11. 35 4
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  12. 2 0
      FreeAPS/Sources/APS/PluginManager.swift
  13. 1 0
      FreeAPS/Sources/Assemblies/APSAssembly.swift
  14. 1 0
      FreeAPS/Sources/Assemblies/NetworkAssembly.swift
  15. 13 0
      FreeAPS/Sources/Models/BloodGlucose.swift
  16. 23 0
      FreeAPS/Sources/Models/CarbsEntry.swift
  17. 27 0
      FreeAPS/Sources/Models/PumpHistoryEvent.swift
  18. 19 0
      FreeAPS/Sources/Modules/CGM/CGMStateModel.swift
  19. 20 0
      FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift
  20. 13 0
      FreeAPS/Sources/Modules/Calibrations/CalibrationsDataFlow.swift
  21. 3 0
      FreeAPS/Sources/Modules/Calibrations/CalibrationsProvider.swift
  22. 73 0
      FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift
  23. 60 0
      FreeAPS/Sources/Modules/Calibrations/View/CalibrationsChart.swift
  24. 109 0
      FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift
  25. 13 0
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  26. 7 1
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  27. 5 13
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  28. 43 29
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  29. 4 11
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  30. 7 0
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  31. 3 1
      FreeAPS/Sources/Modules/Settings/SettingsProvider.swift
  32. 27 0
      FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift
  33. 27 0
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  34. 45 0
      FreeAPS/Sources/Modules/Settings/View/TidePoolConfigView.swift
  35. 7 1
      FreeAPS/Sources/Router/Screen.swift
  36. 426 0
      FreeAPS/Sources/Services/Network/TidepoolManager.swift
  37. 55 0
      FreeAPSTests/CalibrationsTests.swift
  38. 71 0
      FreeAPSTests/PluginManagerTests.swift
  39. 2 2
      README.md
  40. 1 0
      TidepoolService
  41. 1 1
      scripts/swiftformat.sh

+ 3 - 0
.gitmodules

@@ -34,3 +34,6 @@
 	path = LibreTransmitter
 	url = https://github.com/LoopKit/LibreTransmitter.git
 	branch = main
+[submodule "TidepoolService"]
+	path = TidepoolService
+	url = https://github.com/LoopKit/TidepoolService.git

+ 8 - 0
BuildDetails.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>TidepoolServiceClientId</key>
+	<string>diy-loop</string>
+</dict>
+</plist>

+ 68 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -277,6 +277,10 @@
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
 		CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */; };
+		CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */; };
+		CE1F6DDB2BAE08B60064EB8D /* TidepoolManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DDA2BAE08B60064EB8D /* TidepoolManager.swift */; };
+		CE1F6DE72BAF1A180064EB8D /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */; };
+		CE1F6DE92BAF37C90064EB8D /* TidePoolConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DE82BAF37C90064EB8D /* TidePoolConfigView.swift */; };
 		CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */; };
 		CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */; };
 		CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */; };
@@ -320,6 +324,13 @@
 		CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */; };
 		CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = CEB434FC28B90B7C00B70274 /* SwiftCharts */; };
 		CEB434FE28B90B8C00B70274 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = CEB434FC28B90B7C00B70274 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+		CEE9A6552BBB418300EB5194 /* CalibrationsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A64F2BBB418300EB5194 /* CalibrationsProvider.swift */; };
+		CEE9A6562BBB418300EB5194 /* CalibrationsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A6512BBB418300EB5194 /* CalibrationsRootView.swift */; };
+		CEE9A6572BBB418300EB5194 /* CalibrationsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A6522BBB418300EB5194 /* CalibrationsChart.swift */; };
+		CEE9A6582BBB418300EB5194 /* CalibrationsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A6532BBB418300EB5194 /* CalibrationsStateModel.swift */; };
+		CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */; };
+		CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */; };
+		CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */; };
 		D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */; };
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
@@ -760,6 +771,10 @@
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusDataFlow.swift; sourceTree = "<group>"; };
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
+		CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManagerTests.swift; sourceTree = "<group>"; };
+		CE1F6DDA2BAE08B60064EB8D /* TidepoolManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolManager.swift; sourceTree = "<group>"; };
+		CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = "<group>"; };
+		CE1F6DE82BAF37C90064EB8D /* TidePoolConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidePoolConfigView.swift; sourceTree = "<group>"; };
 		CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodGlucoseExtensions.swift; sourceTree = "<group>"; };
 		CE398D012977349800DF218F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
 		CE398D17297C9EE800DF218F /* G7SensorKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G7SensorKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -805,6 +820,13 @@
 		CEC751D329D88257006E9D24 /* OmniKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEC751D529D88262006E9D24 /* MinimedKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MinimedKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEC751D729D88262006E9D24 /* MinimedKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MinimedKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		CEE9A64F2BBB418300EB5194 /* CalibrationsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationsProvider.swift; sourceTree = "<group>"; };
+		CEE9A6512BBB418300EB5194 /* CalibrationsRootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationsRootView.swift; sourceTree = "<group>"; };
+		CEE9A6522BBB418300EB5194 /* CalibrationsChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationsChart.swift; sourceTree = "<group>"; };
+		CEE9A6532BBB418300EB5194 /* CalibrationsStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationsStateModel.swift; sourceTree = "<group>"; };
+		CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationsDataFlow.swift; sourceTree = "<group>"; };
+		CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
+		CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationsTests.swift; sourceTree = "<group>"; };
 		CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalStateModel.swift; sourceTree = "<group>"; };
 		D0BDC6993C1087310EDFC428 /* CREditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorRootView.swift; sourceTree = "<group>"; };
 		D295A3F870E826BE371C0BB5 /* AutotuneConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigStateModel.swift; sourceTree = "<group>"; };
@@ -1072,6 +1094,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
+				CEE9A64D2BBB411C00EB5194 /* Calibrations */,
 				190EBCC229FF134900BA767D /* StatConfig */,
 				CE94597C29E9E1CD0047C9C6 /* WatchConfig */,
 				19F95FF129F10F9C00314DDC /* Stat */,
@@ -1200,6 +1223,7 @@
 			isa = PBXGroup;
 			children = (
 				3811DE3C25C9D4A100A708ED /* SettingsRootView.swift */,
+				CE1F6DE82BAF37C90064EB8D /* TidePoolConfigView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -1239,6 +1263,7 @@
 				3811DE9725C9D88300A708ED /* NightscoutManager.swift */,
 				38FE826925CC82DB001FF17A /* NetworkService.swift */,
 				38FE826C25CC8461001FF17A /* NightscoutAPI.swift */,
+				CE1F6DDA2BAE08B60064EB8D /* TidepoolManager.swift */,
 			);
 			path = Network;
 			sourceTree = "<group>";
@@ -1402,6 +1427,7 @@
 		3856933F270B57A00002C50D /* CGM */ = {
 			isa = PBXGroup;
 			children = (
+				CEE9A65A2BBB41AD00EB5194 /* Calibrations */,
 				F816825F28DB441800054060 /* BluetoothTransmitter.swift */,
 				F816825D28DB441200054060 /* HeartBeatManager.swift */,
 				38569346270B5DFB0002C50D /* AppGroupSource.swift */,
@@ -1437,6 +1463,7 @@
 		388E594F25AD948C0019842D = {
 			isa = PBXGroup;
 			children = (
+				CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */,
 				FEFA5C0D299F810B00765C17 /* Core_Data.xcdatamodeld */,
 				38F3783A2613555C009DB701 /* Config.xcconfig */,
 				3818AA42274BBC1100843DB3 /* ConfigOverride.xcconfig */,
@@ -1740,6 +1767,8 @@
 			children = (
 				38FCF3F125E9028E0078B0D1 /* Info.plist */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
+				CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
+				CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
 			);
 			path = FreeAPSTests;
 			sourceTree = "<group>";
@@ -2004,6 +2033,34 @@
 			path = Bluetooth;
 			sourceTree = "<group>";
 		};
+		CEE9A64D2BBB411C00EB5194 /* Calibrations */ = {
+			isa = PBXGroup;
+			children = (
+				CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */,
+				CEE9A64F2BBB418300EB5194 /* CalibrationsProvider.swift */,
+				CEE9A6532BBB418300EB5194 /* CalibrationsStateModel.swift */,
+				CEE9A6502BBB418300EB5194 /* View */,
+			);
+			path = Calibrations;
+			sourceTree = "<group>";
+		};
+		CEE9A6502BBB418300EB5194 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				CEE9A6512BBB418300EB5194 /* CalibrationsRootView.swift */,
+				CEE9A6522BBB418300EB5194 /* CalibrationsChart.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
+		CEE9A65A2BBB41AD00EB5194 /* Calibrations */ = {
+			isa = PBXGroup;
+			children = (
+				CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */,
+			);
+			path = Calibrations;
+			sourceTree = "<group>";
+		};
 		D533BF261CDC1C3F871E7BFD /* NightscoutConfig */ = {
 			isa = PBXGroup;
 			children = (
@@ -2296,6 +2353,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				198377D2266BFFF6004DE65E /* Localizable.strings in Resources */,
+				CE1F6DE72BAF1A180064EB8D /* BuildDetails.plist in Resources */,
 				38DF178D27733E6800B3528F /* snow.sks in Resources */,
 				388E597225AD9CF10019842D /* json in Resources */,
 				38DF178E27733E6800B3528F /* Assets.xcassets in Resources */,
@@ -2430,6 +2488,7 @@
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
+				CEE9A6552BBB418300EB5194 /* CalibrationsProvider.swift in Sources */,
 				19F95FF529F10FCF00314DDC /* StatProvider.swift in Sources */,
 				38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */,
 				19B0EF2128F6D66200069496 /* Statistics.swift in Sources */,
@@ -2450,14 +2509,17 @@
 				382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */,
 				19D466A529AA2BD4004D5F33 /* FPUConfigProvider.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
+				CEE9A6572BBB418300EB5194 /* CalibrationsChart.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				38192E04261B82FA0094D973 /* ReachabilityManager.swift in Sources */,
 				38E44539274E411700EC9A94 /* Disk+UIImage.swift in Sources */,
 				388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */,
 				38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */,
 				38569348270B5DFB0002C50D /* GlucoseSource.swift in Sources */,
+				CEE9A6582BBB418300EB5194 /* CalibrationsStateModel.swift in Sources */,
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
+				CEE9A6562BBB418300EB5194 /* CalibrationsRootView.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
 				CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */,
 				38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */,
@@ -2491,6 +2553,7 @@
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
+				CE1F6DE92BAF37C90064EB8D /* TidePoolConfigView.swift in Sources */,
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
@@ -2505,6 +2568,7 @@
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
 				3871F39F25ED895A0013ECB5 /* Decimal+Extensions.swift in Sources */,
+				CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
 				38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */,
@@ -2561,6 +2625,7 @@
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
+				CE1F6DDB2BAE08B60064EB8D /* TidepoolManager.swift in Sources */,
 				BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */,
 				9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */,
 				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,
@@ -2615,6 +2680,7 @@
 				69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */,
 				38E44538274E411700EC9A94 /* Disk+[Data].swift in Sources */,
 				98641AF4F92123DA668AB931 /* CREditorRootView.swift in Sources */,
+				CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */,
 				38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
@@ -2722,6 +2788,8 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
+				CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

+ 65 - 58
FreeAPS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,61 +1,68 @@
 {
-  "object": {
-    "pins": [
-      {
-        "package": "Alamofire",
-        "repositoryURL": "https://github.com/Alamofire/Alamofire",
-        "state": {
-          "branch": null,
-          "revision": "f96b619bcb2383b43d898402283924b80e2c4bae",
-          "version": "5.4.3"
-        }
-      },
-      {
-        "package": "Disk",
-        "repositoryURL": "https://github.com/saoudrizwan/Disk",
-        "state": {
-          "branch": null,
-          "revision": "b0cb4fdf23e51849cc2460bdc6de795c3bcca99d",
-          "version": "0.6.4"
-        }
-      },
-      {
-        "package": "swift-algorithms",
-        "repositoryURL": "https://github.com/apple/swift-algorithms",
-        "state": {
-          "branch": null,
-          "revision": "2327673b0e9c7e90e6b1826376526ec3627210e4",
-          "version": "0.2.1"
-        }
-      },
-      {
-        "package": "swift-numerics",
-        "repositoryURL": "https://github.com/apple/swift-numerics",
-        "state": {
-          "branch": null,
-          "revision": "6583ac70c326c3ee080c1d42d9ca3361dca816cd",
-          "version": "0.1.0"
-        }
-      },
-      {
-        "package": "SwiftDate",
-        "repositoryURL": "https://github.com/malcommac/SwiftDate",
-        "state": {
-          "branch": null,
-          "revision": "6190d0cefff3013e77ed567e6b074f324e5c5bf5",
-          "version": "6.3.1"
-        }
-      },
-      {
-        "package": "Swinject",
-        "repositoryURL": "https://github.com/Swinject/Swinject",
-        "state": {
-          "branch": null,
-          "revision": "8a76d2c74bafbb455763487cc6a08e91bad1f78b",
-          "version": "2.7.1"
-        }
+  "pins" : [
+    {
+      "identity" : "mkringprogressview",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/maxkonovalov/MKRingProgressView.git",
+      "state" : {
+        "branch" : "master",
+        "revision" : "660888aab1d2ab0ed7eb9eb53caec12af4955fa7"
       }
-    ]
-  },
-  "version": 1
+    },
+    {
+      "identity" : "swift-algorithms",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/apple/swift-algorithms",
+      "state" : {
+        "revision" : "2327673b0e9c7e90e6b1826376526ec3627210e4",
+        "version" : "0.2.1"
+      }
+    },
+    {
+      "identity" : "swift-numerics",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/apple/swift-numerics",
+      "state" : {
+        "revision" : "6583ac70c326c3ee080c1d42d9ca3361dca816cd",
+        "version" : "0.1.0"
+      }
+    },
+    {
+      "identity" : "swiftcharts",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/ivanschuetz/SwiftCharts.git",
+      "state" : {
+        "branch" : "master",
+        "revision" : "c354c1945bb35a1f01b665b22474f6db28cba4a2"
+      }
+    },
+    {
+      "identity" : "swiftdate",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/malcommac/SwiftDate",
+      "state" : {
+        "revision" : "6190d0cefff3013e77ed567e6b074f324e5c5bf5",
+        "version" : "6.3.1"
+      }
+    },
+    {
+      "identity" : "swiftmessages",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/SwiftKickMobile/SwiftMessages",
+      "state" : {
+        "revision" : "62e12e138fc3eedf88c7553dd5d98712aa119f40",
+        "version" : "9.0.9"
+      }
+    },
+    {
+      "identity" : "swinject",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Swinject/Swinject",
+      "state" : {
+        "revision" : "8a76d2c74bafbb455763487cc6a08e91bad1f78b",
+        "version" : "2.7.1"
+      }
+    }
+  ],
+  "version" : 2
 }

+ 22 - 8
FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme

@@ -224,6 +224,20 @@
             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 = "FreeAPS.app"
                BlueprintName = "FreeAPS"
@@ -249,7 +263,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "NO">
+            skipped = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "43CABDFC1C3506F100005705"
@@ -259,7 +273,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "NO">
+            skipped = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "C17F50CD291EAC3800555EB5"
@@ -269,7 +283,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "NO">
+            skipped = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "43D8FDD41C728FDF0073BE78"
@@ -279,7 +293,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "NO">
+            skipped = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "B4CEE2DF257129780093111B"
@@ -289,7 +303,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "NO">
+            skipped = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "C13CC34029C7B73A007F25DE"
@@ -299,7 +313,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "NO">
+            skipped = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "84752E8A26ED0FFE009FD801"
@@ -309,7 +323,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "NO">
+            skipped = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "C12ED9C929C7DBA900435701"
@@ -319,7 +333,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "NO">
+            skipped = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "431CE7761F98564200255374"

+ 3 - 0
FreeAPS.xcworkspace/contents.xcworkspacedata

@@ -31,4 +31,7 @@
    <FileRef
       location = "group:G7SensorKit/G7SensorKit.xcodeproj">
    </FileRef>
+   <FileRef
+      location = "group:TidepoolService/TidepoolService.xcodeproj">
+   </FileRef>
 </Workspace>

+ 14 - 5
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -48,7 +48,7 @@
     {
       "identity" : "swiftcharts",
       "kind" : "remoteSourceControl",
-      "location" : "https://github.com/ivanschuetz/SwiftCharts.git",
+      "location" : "https://github.com/ivanschuetz/SwiftCharts",
       "state" : {
         "branch" : "master",
         "revision" : "c354c1945bb35a1f01b665b22474f6db28cba4a2"
@@ -68,8 +68,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/SwiftKickMobile/SwiftMessages",
       "state" : {
-        "revision" : "b29dd21090b708aa0ae9ecbaf6e2d0487028dc3f",
-        "version" : "9.0.6"
+        "revision" : "62e12e138fc3eedf88c7553dd5d98712aa119f40",
+        "version" : "9.0.9"
       }
     },
     {
@@ -77,8 +77,17 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/Swinject/Swinject",
       "state" : {
-        "revision" : "8bc503e60965298984fb58cf47b71c541449fe2a",
-        "version" : "2.8.3"
+        "revision" : "13d2d7065253eea1e9ce4d9263cf51c783fdf3f0",
+        "version" : "2.8.5"
+      }
+    },
+    {
+      "identity" : "tidepoolkit",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/tidepool-org/TidepoolKit",
+      "state" : {
+        "branch" : "dev",
+        "revision" : "b8185353c0f46a055f6d5681bb3f18ec86a5b974"
       }
     }
   ],

+ 0 - 4
FreeAPS/Resources/Info.plist

@@ -113,10 +113,6 @@
 		<string>UIInterfaceOrientationPortrait</string>
 		<string>UIInterfaceOrientationPortraitUpsideDown</string>
 	</array>
-	<key>NSCalendarsFullAccessUsageDescription</key>
-	<string>To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay</string>
-	<key>LSApplicationCategoryType</key>
-	<string></string>
 	<key>UISupportedInterfaceOrientations~ipad</key>
 	<array>
 		<string>UIInterfaceOrientationPortrait</string>

+ 119 - 0
FreeAPS/Sources/APS/CGM/Calibrations/CalibrationService.swift

@@ -0,0 +1,119 @@
+import Foundation
+import LibreTransmitter
+import Swinject
+
+struct Calibration: JSON, Hashable, Identifiable {
+    let x: Double
+    let y: Double
+    var date = Date()
+
+    static let zero = Calibration(x: 0, y: 0)
+
+    var id = UUID()
+}
+
+protocol CalibrationService {
+    var slope: Double { get }
+    var intercept: Double { get }
+    var calibrations: [Calibration] { get }
+
+    func addCalibration(_ calibration: Calibration)
+    func removeCalibration(_ calibration: Calibration)
+    func removeAllCalibrations()
+    func removeLast()
+
+    func calibrate(value: Int) -> Double
+}
+
+final class BaseCalibrationService: CalibrationService, Injectable {
+    private enum Config {
+        static let minSlope = 0.8
+        static let maxSlope = 1.25
+        static let minIntercept = -100.0
+        static let maxIntercept = 100.0
+        static let maxValue = 500.0
+        static let minValue = 0.0
+    }
+
+    @Injected() var storage: FileStorage!
+    @Injected() var notificationCenter: NotificationCenter!
+    private var lifetime = Lifetime()
+
+    private(set) var calibrations: [Calibration] = [] {
+        didSet {
+            storage.save(calibrations, as: OpenAPS.FreeAPS.calibrations)
+        }
+    }
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+        calibrations = storage.retrieve(OpenAPS.FreeAPS.calibrations, as: [Calibration].self) ?? []
+        subscribe()
+    }
+
+    private func subscribe() {
+//        notificationCenter.publisher(for: .newSensorDetected)
+//            .sink { [weak self] _ in
+//                self?.removeAllCalibrations()
+//            }
+//            .store(in: &lifetime)
+    }
+
+    var slope: Double {
+        guard calibrations.count >= 2 else {
+            return 1
+        }
+
+        let xs = calibrations.map(\.x)
+        let ys = calibrations.map(\.y)
+        let sum1 = average(multiply(xs, ys)) - average(xs) * average(ys)
+        let sum2 = average(multiply(xs, xs)) - pow(average(xs), 2)
+        let slope = sum1 / sum2
+
+        return min(max(slope, Config.minSlope), Config.maxSlope)
+    }
+
+    var intercept: Double {
+        guard calibrations.count >= 1 else {
+            return 0
+        }
+        let xs = calibrations.map(\.x)
+        let ys = calibrations.map(\.y)
+
+        let intercept = average(ys) - slope * average(xs)
+
+        return min(max(intercept, Config.minIntercept), Config.maxIntercept)
+    }
+
+    func calibrate(value: Int) -> Double {
+        linearRegression(value)
+    }
+
+    func addCalibration(_ calibration: Calibration) {
+        calibrations.append(calibration)
+    }
+
+    func removeCalibration(_ calibration: Calibration) {
+        calibrations.removeAll { $0 == calibration }
+    }
+
+    func removeAllCalibrations() {
+        calibrations.removeAll()
+    }
+
+    func removeLast() {
+        calibrations.removeLast()
+    }
+
+    private func average(_ input: [Double]) -> Double {
+        input.reduce(0, +) / Double(input.count)
+    }
+
+    private func multiply(_ a: [Double], _ b: [Double]) -> [Double] {
+        zip(a, b).map(*)
+    }
+
+    private func linearRegression(_ x: Int) -> Double {
+        (intercept + slope * Double(x)).clamped(Config.minValue ... Config.maxValue)
+    }
+}

+ 8 - 1
FreeAPS/Sources/APS/CGM/PluginSource.swift

@@ -102,7 +102,13 @@ extension PluginSource: CGMManagerDelegate {
         dispatchPrecondition(condition: .onQueue(processQueue))
         // TODO: Events in APS ?
         // currently only display in log the date of the event
-        events.forEach { debug(.deviceManager, "events from CGM at \($0.date)") }
+        events.forEach { event in
+            debug(.deviceManager, "events from CGM at \(event.date)")
+
+            if event.type == .sensorStart {
+                self.glucoseManager?.removeCalibrations()
+            }
+        }
     }
 
     func startDateToFilterNewData(for _: CGMManager) -> Date? {
@@ -125,6 +131,7 @@ extension PluginSource: CGMManagerDelegate {
     }
 
     func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
+        debug(.deviceManager, "DEBUG DID UPDATE STATE")
         processQueue.async {
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
                 self.cgmHasValidSensorSession = status.hasValidSensorSession

+ 35 - 4
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -11,6 +11,7 @@ protocol FetchGlucoseManager: SourceInfoProvider {
     func refreshCGM()
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
     func deleteGlucoseSource()
+    func removeCalibrations()
     var glucoseSource: GlucoseSource! { get }
     var cgmManager: CGMManagerUI? { get }
     var cgmGlucoseSourceType: CGMType? { get set }
@@ -29,11 +30,13 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var nightscoutManager: NightscoutManager!
+    @Injected() var tidePoolService: TidePoolManager!
     @Injected() var apsManager: APSManager!
     @Injected() var settingsManager: SettingsManager!
     @Injected() var healthKitManager: HealthKitManager!
     @Injected() var deviceDataManager: DeviceDataManager!
     @Injected() var pluginCGMManager: PluginManager!
+    @Injected() var calibrationService: CalibrationService!
 
     private var lifetime = Lifetime()
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
@@ -67,6 +70,10 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
     var glucoseSource: GlucoseSource!
 
+    func removeCalibrations() {
+        calibrationService.removeAllCalibrations()
+    }
+
     func deleteGlucoseSource() {
         cgmManager = nil
         updateGlucoseSource(
@@ -76,6 +83,11 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     }
 
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
+        // if changed, remove all calibrations
+        if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
+            removeCalibrations()
+        }
+
         self.cgmGlucoseSourceType = cgmGlucoseSourceType
         self.cgmGlucosePluginId = cgmGlucosePluginId
 
@@ -87,12 +99,10 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         if let manager = newManager
         {
             cgmManager = manager
+            removeCalibrations()
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
         }
-//        } else if self.cgmGlucoseSourceType == .plugin, self.cgmGlucosePluginId != , self.cgmGlucosePluginId != cgmManager?.pluginIdentifier  {
-//            cgmManager = nil
-//        }
 
         switch self.cgmGlucoseSourceType {
         case nil,
@@ -153,7 +163,10 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     }
 
     private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose], glucoseFromHealth: [BloodGlucose] = []) {
-        let allGlucose = glucose + glucoseFromHealth
+        // calibration add if required only for sensor
+        let newGlucose = overcalibrate(entries: glucose)
+
+        let allGlucose = newGlucose + glucoseFromHealth
         var filteredByDate: [BloodGlucose] = []
         var filtered: [BloodGlucose] = []
 
@@ -206,6 +219,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         deviceDataManager.heartbeat(date: Date())
 
         nightscoutManager.uploadGlucose()
+        tidePoolService.uploadGlucose(device: cgmManager?.cgmManagerStatus.device)
 
         let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
 
@@ -264,6 +278,23 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     func sourceInfo() -> [String: Any]? {
         glucoseSource.sourceInfo()
     }
+
+    private func overcalibrate(entries: [BloodGlucose]) -> [BloodGlucose] {
+        // overcalibrate
+        var overcalibration: ((Int) -> (Double))?
+        processQueue.sync { overcalibration = calibrationService.calibrate }
+
+        if let overcalibration = overcalibration {
+            return entries.map { entry in
+                var entry = entry
+                entry.glucose = Int(overcalibration(entry.glucose!))
+                entry.sgv = Int(overcalibration(entry.sgv!))
+                return entry
+            }
+        } else {
+            return entries
+        }
+    }
 }
 
 extension CGMManager {

+ 2 - 0
FreeAPS/Sources/APS/PluginManager.swift

@@ -6,8 +6,10 @@ 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 {

+ 1 - 0
FreeAPS/Sources/Assemblies/APSAssembly.swift

@@ -10,5 +10,6 @@ final class APSAssembly: Assembly {
         container.register(FetchAnnouncementsManager.self) { r in BaseFetchAnnouncementsManager(resolver: r) }
         container.register(BluetoothStateManager.self) { r in BaseBluetoothStateManager(resolver: r) }
         container.register(PluginManager.self) { r in BasePluginManager(resolver: r) }
+        container.register(CalibrationService.self) { r in BaseCalibrationService(resolver: r) }
     }
 }

+ 1 - 0
FreeAPS/Sources/Assemblies/NetworkAssembly.swift

@@ -8,5 +8,6 @@ final class NetworkAssembly: Assembly {
         }
 
         container.register(NightscoutManager.self) { r in BaseNightscoutManager(resolver: r) }
+        container.register(TidePoolManager.self) { r in BaseTidePoolManager(resolver: r) }
     }
 }

+ 13 - 0
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -1,4 +1,6 @@
 import Foundation
+import HealthKit
+import LoopKit
 
 struct BloodGlucose: JSON, Identifiable, Hashable {
     enum Direction: String, JSON {
@@ -91,3 +93,14 @@ extension BloodGlucose: SavitzkyGolaySmoothable {
         }
     }
 }
+
+extension BloodGlucose {
+    func convertStoredGlucoseSample(device: HKDevice?) -> StoredGlucoseSample {
+        StoredGlucoseSample(
+            syncIdentifier: id,
+            startDate: dateString.date,
+            quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose!)),
+            device: device
+        )
+    }
+}

+ 23 - 0
FreeAPS/Sources/Models/CarbsEntry.swift

@@ -1,4 +1,5 @@
 import Foundation
+import LoopKit
 
 struct CarbsEntry: JSON, Equatable, Hashable {
     let id: String?
@@ -36,3 +37,25 @@ extension CarbsEntry {
         case fpuID
     }
 }
+
+extension CarbsEntry {
+    func convertSyncCarb(operation: LoopKit.Operation = .create) -> SyncCarbObject {
+        SyncCarbObject(
+            absorptionTime: nil,
+            createdByCurrentApp: true,
+            foodType: nil,
+            grams: Double(carbs),
+            startDate: createdAt,
+            uuid: UUID(uuidString: id!),
+            provenanceIdentifier: enteredBy ?? "",
+            syncIdentifier: id,
+            syncVersion: nil,
+            userCreatedDate: nil,
+            userUpdatedDate: nil,
+            userDeletedDate: nil,
+            operation: operation,
+            addedDate: nil,
+            supercededDate: nil
+        )
+    }
+}

+ 27 - 0
FreeAPS/Sources/Models/PumpHistoryEvent.swift

@@ -1,4 +1,5 @@
 import Foundation
+import LoopKit
 
 struct PumpHistoryEvent: JSON, Equatable {
     let id: String
@@ -82,3 +83,29 @@ extension PumpHistoryEvent {
         case note
     }
 }
+
+extension EventType {
+    func mapEventTypeToPumpEventType() -> PumpEventType? {
+        switch self {
+        case .prime:
+            return PumpEventType.prime
+        case .pumpResume:
+            return PumpEventType.resume
+        case .rewind:
+            return PumpEventType.rewind
+        case .pumpSuspend:
+            return PumpEventType.suspend
+        case .nsBatteryChange,
+             .pumpBattery:
+            return PumpEventType.replaceComponent(componentType: .pump)
+        case .nsInsulinChange:
+            return PumpEventType.replaceComponent(componentType: .reservoir)
+        case .nsSiteChange:
+            return PumpEventType.replaceComponent(componentType: .infusionSet)
+        case .pumpAlarm:
+            return PumpEventType.alarm
+        default:
+            return nil
+        }
+    }
+}

+ 19 - 0
FreeAPS/Sources/Modules/CGM/CGMStateModel.swift

@@ -23,6 +23,8 @@ extension CGM {
         @Injected() var cgmManager: FetchGlucoseManager!
         @Injected() var calendarManager: CalendarManager!
         @Injected() var pluginCGMManager: PluginManager!
+        @Injected() private var broadcaster: Broadcaster!
+        @Injected() var nightscoutManager: NightscoutManager!
 
         @Published var setupCGM: Bool = false
         @Published var cgmCurrent = cgmDefaultName
@@ -33,6 +35,7 @@ extension CGM {
         @Persisted(key: "CalendarManager.currentCalendarID") var storedCalendarID: String? = nil
         @Published var cgmTransmitterDeviceAddress: String? = nil
         @Published var listOfCGM: [cgmName] = []
+        @Published var url: URL?
 
         override func subscribe() {
             // collect the list of CGM available with plugins and CGMType defined manually
@@ -65,6 +68,17 @@ extension CGM {
                 )
             }
 
+            url = nightscoutManager.cgmURL
+            switch url?.absoluteString {
+            case "http://127.0.0.1:1979":
+                url = URL(string: "spikeapp://")!
+            case "http://127.0.0.1:17580":
+                url = URL(string: "diabox://")!
+            //            case CGMType.libreTransmitter.appURL?.absoluteString:
+            //                showModal(for: .libreConfig)
+            default: break
+            }
+
             currentCalendarID = storedCalendarID ?? ""
             calendarIDs = calendarManager.calendarIDs()
             cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
@@ -139,6 +153,11 @@ extension CGM.StateModel: CompletionDelegate {
         settingsManager.settings.uploadGlucose = cgmManager.shouldSyncToRemoteService
 
         // update if required the Glucose source
+        DispatchQueue.main.async {
+            self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                $0.glucoseDidUpdate([])
+            }
+        }
     }
 }
 

+ 20 - 0
FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift

@@ -5,6 +5,7 @@ import Swinject
 extension CGM {
     struct RootView: BaseView {
         let resolver: Resolver
+        let displayClose: Bool
         @StateObject var state = StateModel()
         @State private var setupCGM = false
 
@@ -47,6 +48,24 @@ extension CGM {
                             }
                         }
                     }
+                    if state.cgmCurrent.type == .plugin && state.cgmCurrent.id.contains("Libre") {
+                        Section(header: Text("Calibrations")) {
+                            Text("Calibrations").navigationLink(to: .calibrations, from: self)
+                        }
+                    }
+
+                    if state.cgmCurrent.type == .nightscout {
+                        Section(header: Text("Nightscout")) {
+                            if state.url != nil {
+                                Button(state.url!.absoluteString) {
+                                    UIApplication.shared.open(state.url!, options: [:], completionHandler: nil)
+                                }
+                            } else {
+                                Text("You need to configure Nightscout URL")
+                            }
+                        }
+                    }
+
                     Section(header: Text("Calendar")) {
                         Toggle("Create events in calendar", isOn: $state.createCalendarEvents)
                         if state.calendarIDs.isNotEmpty {
@@ -66,6 +85,7 @@ extension CGM {
                 .onAppear(perform: configureView)
                 .navigationTitle("CGM")
                 .navigationBarTitleDisplayMode(.automatic)
+                .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
                 .sheet(isPresented: $setupCGM) {
                     if let cgmFetchManager = state.cgmManager,
                        let cgmManager = cgmFetchManager.cgmManager,

+ 13 - 0
FreeAPS/Sources/Modules/Calibrations/CalibrationsDataFlow.swift

@@ -0,0 +1,13 @@
+enum Calibrations {
+    enum Config {}
+
+    struct Item: Hashable, Identifiable {
+        let calibration: Calibration
+
+        var id: String {
+            calibration.id.uuidString
+        }
+    }
+}
+
+protocol CalibrationsProvider {}

+ 3 - 0
FreeAPS/Sources/Modules/Calibrations/CalibrationsProvider.swift

@@ -0,0 +1,3 @@
+extension Calibrations {
+    final class Provider: BaseProvider, CalibrationsProvider {}
+}

+ 73 - 0
FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -0,0 +1,73 @@
+import SwiftDate
+import SwiftUI
+
+extension Calibrations {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var glucoseStorage: GlucoseStorage!
+        @Injected() var calibrationService: CalibrationService!
+
+        @Published var slope: Double = 1
+        @Published var intercept: Double = 1
+        @Published var newCalibration: Decimal = 0
+        @Published var calibrations: [Calibration] = []
+        @Published var calibrate: (Int) -> Double = { Double($0) }
+        @Published var items: [Item] = []
+
+        var units: GlucoseUnits = .mmolL
+
+        override func subscribe() {
+            units = settingsManager.settings.units
+            calibrate = calibrationService.calibrate
+            setupCalibrations()
+        }
+
+        private func setupCalibrations() {
+            slope = calibrationService.slope
+            intercept = calibrationService.intercept
+            calibrations = calibrationService.calibrations
+            items = calibrations.map {
+                Item(calibration: $0)
+            }
+        }
+
+        func addCalibration() {
+            defer {
+                UIApplication.shared.endEditing()
+                setupCalibrations()
+            }
+
+            var glucose = newCalibration
+            if units == .mmolL {
+                glucose = newCalibration.asMgdL
+            }
+
+            guard let lastGlucose = glucoseStorage.recent().last,
+                  lastGlucose.dateString.addingTimeInterval(60 * 4.5) > Date(),
+                  let unfiltered = lastGlucose.unfiltered
+            else {
+                info(.service, "Glucose is stale for calibration")
+                return
+            }
+
+            let calibration = Calibration(x: Double(unfiltered), y: Double(glucose))
+
+            calibrationService.addCalibration(calibration)
+        }
+
+        func removeLast() {
+            calibrationService.removeLast()
+            setupCalibrations()
+        }
+
+        func removeAll() {
+            calibrationService.removeAllCalibrations()
+            setupCalibrations()
+        }
+
+        func removeAtIndex(_ index: Int) {
+            let calibration = calibrations[index]
+            calibrationService.removeCalibration(calibration)
+            setupCalibrations()
+        }
+    }
+}

+ 60 - 0
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsChart.swift

@@ -0,0 +1,60 @@
+import SwiftUI
+
+struct CalibrationsChart: View {
+    @EnvironmentObject var state: Calibrations.StateModel
+
+    private var dateFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.timeStyle = .short
+        formatter.dateStyle = .short
+        return formatter
+    }
+
+    private let maxValue = 400.0
+
+    var body: some View {
+        GeometryReader { geo in
+            ZStack(alignment: .top) {
+                Rectangle().fill(Color.secondary)
+                    .frame(height: geo.size.width)
+                Path { path in
+                    let size = geo.size.width
+                    path.move(
+                        to:
+                        CGPoint(
+                            x: 0,
+                            y: size - state.calibrate(0) / maxValue * geo.size.width
+                        )
+                    )
+                    path.addLine(
+                        to: CGPoint(
+                            x: size,
+                            y: size - state.calibrate(Int(maxValue)) / maxValue * geo.size.width
+                        )
+                    )
+                }
+                .stroke(.blue, lineWidth: 2)
+
+                ForEach(state.calibrations, id: \.self) { value in
+                    ZStack {
+                        Circle().fill(.red)
+                            .frame(width: 6, height: 6)
+                            .position(
+                                x: value.x / maxValue * geo.size.width,
+                                y: geo.size.width - (value.y / maxValue * geo.size.width)
+                            )
+                        Text(dateFormatter.string(from: value.date))
+                            .foregroundColor(.white)
+                            .font(.system(size: 10))
+                            .position(
+                                x: value.x / maxValue * geo.size.width,
+                                y: geo.size.width - (value.y / maxValue * geo.size.width) + 10
+                            )
+                    }
+                }
+            }
+            .frame(height: geo.size.width)
+            .clipped()
+        }
+    }
+}

+ 109 - 0
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -0,0 +1,109 @@
+import SwiftUI
+import Swinject
+
+extension Calibrations {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        private var formatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
+        private var dateFormatter: DateFormatter {
+            let formatter = DateFormatter()
+            formatter.timeStyle = .short
+            formatter.dateStyle = .short
+            return formatter
+        }
+
+        var body: some View {
+            GeometryReader { geo in
+                Form {
+                    Section(header: Text("Add calibration")) {
+                        HStack {
+                            Text("Meter glucose")
+                            Spacer()
+                            DecimalTextField(
+                                "0",
+                                value: $state.newCalibration,
+                                formatter: formatter,
+                                autofocus: false,
+                                cleanInput: true
+                            )
+                            Text(state.units.rawValue).foregroundColor(.secondary)
+                        }
+                        Button {
+                            state.addCalibration()
+                        }
+                        label: { Text("Add") }
+                            .disabled(state.newCalibration <= 0)
+                    }
+
+                    Section(header: Text("Info")) {
+                        HStack {
+                            Text("Slope")
+                            Spacer()
+                            Text(formatter.string(from: state.slope as NSNumber)!)
+                        }
+                        HStack {
+                            Text("Intercept")
+                            Spacer()
+                            Text(formatter.string(from: state.intercept as NSNumber)!)
+                        }
+                    }
+
+                    Section(header: Text("Remove")) {
+                        Button {
+                            state.removeLast()
+                        }
+                        label: { Text("Remove Last") }
+                            .disabled(state.calibrations.isEmpty)
+
+                        Button {
+                            state.removeAll()
+                        }
+                        label: { Text("Remove All") }
+                            .disabled(state.calibrations.isEmpty)
+                        List {
+                            ForEach(state.items) { item in
+                                HStack {
+                                    Text(dateFormatter.string(from: item.calibration.date))
+                                    Spacer()
+                                    VStack(alignment: .leading) {
+                                        Text("raw: \(item.calibration.x)")
+                                            .font(.caption2)
+                                            .foregroundColor(.secondary)
+                                        Text("value: \(item.calibration.y)")
+                                            .font(.caption2)
+                                            .foregroundColor(.secondary)
+                                    }
+                                }
+
+                            }.onDelete(perform: delete)
+                        }
+                    }
+
+                    if state.calibrations.isNotEmpty {
+                        Section(header: Text("Chart")) {
+                            CalibrationsChart().environmentObject(state)
+                                .frame(minHeight: geo.size.width)
+                        }
+                    }
+                }
+            }
+            .dynamicTypeSize(...DynamicTypeSize.xxLarge)
+            .onAppear(perform: configureView)
+            .navigationTitle("Calibrations")
+            .navigationBarItems(trailing: EditButton().disabled(state.calibrations.isEmpty))
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+
+        private func delete(at offsets: IndexSet) {
+            state.removeAtIndex(offsets[offsets.startIndex])
+        }
+    }
+}

+ 13 - 0
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -8,6 +8,7 @@ extension DataTable {
         @Injected() var carbsStorage: CarbsStorage!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var healthkitManager: HealthKitManager!
+        @Injected() var tidePoolManager: TidePoolManager!
 
         func pumpHistory() -> [PumpHistoryEvent] {
             pumpHistoryStorage.recent()
@@ -26,6 +27,16 @@ extension DataTable {
         }
 
         func deleteCarbs(_ treatement: Treatment) {
+            // need to start with tidePool because Nightscout delete data
+            // probably to revise the logic
+            // TODO:
+            tidePoolManager.deleteCarbs(
+                at: treatement.date,
+                isFPU: treatement.isFPU,
+                fpuID: treatement.fpuID,
+                syncID: treatement.id
+            )
+
             nightscoutManager.deleteCarbs(
                 at: treatement.date,
                 isFPU: treatement.isFPU,
@@ -35,6 +46,8 @@ extension DataTable {
         }
 
         func deleteInsulin(_ treatement: Treatment) {
+            // delete tidePoolManager before NS - TODO
+            tidePoolManager.deleteInsulin(at: treatement.date)
             nightscoutManager.deleteInsulin(at: treatement.date)
             if let id = treatement.idPumpEvent {
                 healthkitManager.deleteInsulin(syncID: id)

+ 7 - 1
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -44,7 +44,13 @@ extension DataTable {
                                 note: $0.note
                             )
                         } else {
-                            return Treatment(units: units, type: .carbs, date: $0.createdAt, amount: $0.carbs, note: $0.note)
+                            return Treatment(
+                                units: units,
+                                type: .carbs,
+                                date: $0.createdAt,
+                                amount: $0.carbs,
+                                note: $0.note
+                            )
                         }
                     }
 

+ 5 - 13
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -8,7 +8,8 @@ extension Home {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var broadcaster: Broadcaster!
         @Injected() var apsManager: APSManager!
-        @Injected() var nightscoutManager: NightscoutManager!
+        @Injected() var fetchGlucoseManager: FetchGlucoseManager!
+
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         @Published var glucose: [BloodGlucose] = []
@@ -58,6 +59,7 @@ extension Home {
         @Published var displayXgridLines: Bool = false
         @Published var displayYgridLines: Bool = false
         @Published var thresholdLines: Bool = false
+        @Published var cgmAvailable: Bool = false
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
@@ -224,6 +226,7 @@ extension Home {
                     self.glucoseDelta = nil
                 }
                 self.alarm = self.provider.glucoseStorage.alarm
+                cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
             }
         }
 
@@ -350,18 +353,7 @@ extension Home {
         }
 
         func openCGM() {
-            guard var url = nightscoutManager.cgmURL else { return }
-
-            switch url.absoluteString {
-            case "http://127.0.0.1:1979":
-                url = URL(string: "spikeapp://")!
-            case "http://127.0.0.1:17580":
-                url = URL(string: "diabox://")!
-//            case CGMType.libreTransmitter.appURL?.absoluteString:
-//                showModal(for: .libreConfig)
-            default: break
-            }
-            UIApplication.shared.open(url, options: [:], completionHandler: nil)
+            showModal(for: .cgmDirect)
         }
 
         func infoPanelTTPercentage(_ hbt_: Double, _ target: Decimal) -> Decimal {

+ 43 - 29
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -8,6 +8,7 @@ struct CurrentGlucoseView: View {
     @Binding var alarm: GlucoseAlarm?
     @Binding var lowGlucose: Decimal
     @Binding var highGlucose: Decimal
+    @Binding var cgmAvailable: Bool
 
     private var glucoseFormatter: NumberFormatter {
         let formatter = NumberFormatter()
@@ -45,38 +46,51 @@ struct CurrentGlucoseView: View {
     }
 
     var body: some View {
-        VStack(alignment: .center) {
-            HStack {
-                Text(
-                    (recentGlucose?.glucose ?? 100) == 400 ? "HIGH" : recentGlucose?.glucose
-                        .map {
-                            glucoseFormatter
-                                .string(from: Double(units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)! }
-                        ?? "--"
-                )
-                .font(.title).fontWeight(.bold)
-                .foregroundColor(alarm == nil ? colorOfGlucose : .loopRed)
+        if cgmAvailable {
+            VStack(alignment: .center) {
+                HStack {
+                    Text(
+                        (recentGlucose?.glucose ?? 100) == 400 ? "HIGH" : recentGlucose?.glucose
+                            .map {
+                                glucoseFormatter
+                                    .string(from: Double(units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)! }
+                            ?? "--"
+                    )
+                    .font(.title).fontWeight(.bold)
+                    .foregroundColor(alarm == nil ? colorOfGlucose : .loopRed)
 
-                image
-            }
-            HStack {
-                let minutesAgo = -1 * (recentGlucose?.dateString.timeIntervalSinceNow ?? 0) / 60
-                let text = timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
-                Text(
-                    minutesAgo <= 1 ? "< 1 " + NSLocalizedString("min", comment: "Short form for minutes") : (
-                        text + " " +
-                            NSLocalizedString("min", comment: "Short form for minutes") + " "
+                    image
+                }
+                HStack {
+                    let minutesAgo = -1 * (recentGlucose?.dateString.timeIntervalSinceNow ?? 0) / 60
+                    let text = timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
+                    Text(
+                        minutesAgo <= 1 ? "< 1 " + NSLocalizedString("min", comment: "Short form for minutes") : (
+                            text + " " +
+                                NSLocalizedString("min", comment: "Short form for minutes") + " "
+                        )
                     )
-                )
-                .font(.caption2).foregroundColor(.secondary)
+                    .font(.caption2).foregroundColor(.secondary)
 
-                Text(
-                    delta
-                        .map {
-                            deltaFormatter.string(from: Double(units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)!
-                        } ?? "--"
-                )
-                .font(.caption2).foregroundColor(.secondary)
+                    Text(
+                        delta
+                            .map {
+                                deltaFormatter.string(from: Double(units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)!
+                            } ?? "--"
+                    )
+                    .font(.caption2).foregroundColor(.secondary)
+                }.frame(alignment: .top)
+            }
+        } else {
+            VStack(alignment: .center, spacing: 12) {
+                HStack
+                    {
+                        // no cgm defined so display a generic CGM
+                        Image(systemName: "sensor.tag.radiowaves.forward.fill").font(.body).imageScale(.large)
+                    }
+                HStack {
+                    Text("Add CGM").font(.caption).bold()
+                }
             }.frame(alignment: .top)
         }
     }

+ 4 - 11
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -127,23 +127,16 @@ extension Home {
                 units: $state.units,
                 alarm: $state.alarm,
                 lowGlucose: $state.lowGlucose,
-                highGlucose: $state.highGlucose
+                highGlucose: $state.highGlucose,
+                cgmAvailable: $state.cgmAvailable
             )
             .onTapGesture {
-                if state.alarm == nil {
-                    state.openCGM()
-                } else {
-                    state.showModal(for: .snooze)
-                }
+                state.openCGM()
             }
             .onLongPressGesture {
                 let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
                 impactHeavy.impactOccurred()
-                if state.alarm == nil {
-                    state.showModal(for: .snooze)
-                } else {
-                    state.openCGM()
-                }
+                state.showModal(for: .snooze)
             }
         }
 

+ 7 - 0
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -55,6 +55,13 @@ extension NightscoutConfig {
                 }
 
                 Section {
+                    Button("Open Nighstcout") {
+                        UIApplication.shared.open(URL(string: state.url)!, options: [:], completionHandler: nil)
+                    }
+                    .disabled(state.url.isEmpty || state.connecting)
+                }
+
+                Section {
                     Toggle("Upload", isOn: $state.isUploadEnabled)
                     if state.isUploadEnabled {
                         Toggle("Statistics", isOn: $state.uploadStats)

+ 3 - 1
FreeAPS/Sources/Modules/Settings/SettingsProvider.swift

@@ -1,3 +1,5 @@
 extension Settings {
-    final class Provider: BaseProvider, SettingsProvider {}
+    final class Provider: BaseProvider, SettingsProvider {
+        @Injected() var tidePoolManager: TidePoolManager!
+    }
 }

+ 27 - 0
FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift

@@ -1,3 +1,5 @@
+import LoopKit
+import LoopKitUI
 import SwiftUI
 
 extension Settings {
@@ -5,10 +7,14 @@ extension Settings {
         @Injected() private var broadcaster: Broadcaster!
         @Injected() private var fileManager: FileManager!
         @Injected() private var nightscoutManager: NightscoutManager!
+        @Injected() var pluginManager: PluginManager!
+        @Injected() var fetchCgmManager: FetchGlucoseManager!
 
         @Published var closedLoop = false
         @Published var debugOptions = false
         @Published var animatedBackground = false
+        @Published var serviceUIType: ServiceUI.Type?
+        @Published var setupTidePool = false
 
         private(set) var buildNumber = ""
         private(set) var versionNumber = ""
@@ -49,6 +55,8 @@ extension Settings {
             copyrightNotice = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
 
             subscribeSetting(\.animatedBackground, on: $animatedBackground) { animatedBackground = $0 }
+
+            serviceUIType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
         }
 
         func logItems() -> [URL] {
@@ -82,3 +90,22 @@ extension Settings.StateModel: SettingsObserver {
         debugOptions = settings.debugOptions
     }
 }
+
+extension Settings.StateModel: ServiceOnboardingDelegate {
+    func serviceOnboarding(didCreateService service: Service) {
+        debug(.nightscout, "Service with identifier \(service.pluginIdentifier) created")
+        provider.tidePoolManager.addTidePoolService(service: service)
+    }
+
+    func serviceOnboarding(didOnboardService service: Service) {
+        precondition(service.isOnboarded)
+        debug(.nightscout, "Service with identifier \(service.pluginIdentifier) onboarded")
+    }
+}
+
+extension Settings.StateModel: CompletionDelegate {
+    func completionNotifyingDidComplete(_: CompletionNotifying) {
+        setupTidePool = false
+        provider.tidePoolManager.forceUploadData(device: fetchCgmManager.cgmManager?.cgmManagerStatus.device)
+    }
+}

+ 27 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -1,4 +1,6 @@
 import HealthKit
+import LoopKit
+import LoopKitUI
 import SwiftUI
 import Swinject
 
@@ -34,6 +36,11 @@ extension Settings {
 
                 Section {
                     Text("Nightscout").navigationLink(to: .nighscoutConfig, from: self)
+
+                    Text("TidePool")
+                        .onTapGesture {
+                            state.setupTidePool = true
+                        }
                     if HKHealthStore.isHealthDataAvailable() {
                         Text("Apple Health").navigationLink(to: .healthkit, from: self)
                     }
@@ -131,6 +138,26 @@ extension Settings {
             .sheet(isPresented: $showShareSheet) {
                 ShareSheet(activityItems: state.logItems())
             }
+            .sheet(isPresented: $state.setupTidePool) {
+                if let serviceUIType = state.serviceUIType,
+                   let pluginHost = state.provider.tidePoolManager.getTidePoolPluginHost()
+                {
+                    if let serviceUI = state.provider.tidePoolManager.getTidePoolServiceUI() {
+                        TidePoolSettingsView(
+                            serviceUI: serviceUI,
+                            serviceOnBoardDelegate: self.state,
+                            serviceDelegate: self.state
+                        )
+                    } else {
+                        TidePoolSetupView(
+                            serviceUIType: serviceUIType,
+                            pluginHost: pluginHost,
+                            serviceOnBoardDelegate: self.state,
+                            serviceDelegate: self.state
+                        )
+                    }
+                }
+            }
             .onAppear(perform: configureView)
             .navigationTitle("Settings")
             .navigationBarItems(leading: Button("Close", action: state.hideSettingsModal))

+ 45 - 0
FreeAPS/Sources/Modules/Settings/View/TidePoolConfigView.swift

@@ -0,0 +1,45 @@
+import Foundation
+import LoopKit
+import LoopKitUI
+import SwiftUI
+
+struct TidePoolSetupView: UIViewControllerRepresentable {
+    let serviceUIType: ServiceUI.Type
+    let pluginHost: PluginHost
+    let serviceOnBoardDelegate: ServiceOnboardingDelegate
+    let serviceDelegate: CompletionDelegate
+
+    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidePoolSetupView>) -> UIViewController {
+        let result = serviceUIType.setupViewController(
+            colorPalette: .default,
+            pluginHost: pluginHost
+        )
+        switch result {
+        case let .createdAndOnboarded(serviceUI):
+            serviceOnBoardDelegate.serviceOnboarding(didCreateService: serviceUI)
+            serviceOnBoardDelegate.serviceOnboarding(didOnboardService: serviceUI)
+            return UIViewController()
+        case var .userInteractionRequired(setupViewControllerUI):
+            setupViewControllerUI.serviceOnboardingDelegate = serviceOnBoardDelegate
+            setupViewControllerUI.completionDelegate = serviceDelegate
+            return setupViewControllerUI
+        }
+    }
+
+    func updateUIViewController(_: UIViewController, context _: UIViewControllerRepresentableContext<TidePoolSetupView>) {}
+}
+
+struct TidePoolSettingsView: UIViewControllerRepresentable {
+    let serviceUI: ServiceUI
+    let serviceOnBoardDelegate: ServiceOnboardingDelegate
+    let serviceDelegate: CompletionDelegate?
+
+    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidePoolSettingsView>) -> UIViewController {
+        var vc = serviceUI.settingsViewController(colorPalette: .default)
+        vc.completionDelegate = serviceDelegate
+        vc.serviceOnboardingDelegate = serviceOnBoardDelegate
+        return vc
+    }
+
+    func updateUIViewController(_: UIViewController, context _: UIViewControllerRepresentableContext<TidePoolSettingsView>) {}
+}

+ 7 - 1
FreeAPS/Sources/Router/Screen.swift

@@ -21,6 +21,7 @@ enum Screen: Identifiable, Hashable {
     case autotuneConfig
     case dataTable
     case cgm
+    case cgmDirect
     case healthkit
     case notificationsConfig
     case fpuConfig
@@ -30,6 +31,7 @@ enum Screen: Identifiable, Hashable {
     case statistics
     case watch
     case statisticsConfig
+    case calibrations
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -74,7 +76,9 @@ extension Screen {
         case .dataTable:
             DataTable.RootView(resolver: resolver)
         case .cgm:
-            CGM.RootView(resolver: resolver)
+            CGM.RootView(resolver: resolver, displayClose: false)
+        case .cgmDirect:
+            CGM.RootView(resolver: resolver, displayClose: true)
         case .healthkit:
             AppleHealthKit.RootView(resolver: resolver)
         case .notificationsConfig:
@@ -93,6 +97,8 @@ extension Screen {
             Stat.RootView(resolver: resolver)
         case .statisticsConfig:
             StatConfig.RootView(resolver: resolver)
+        case .calibrations:
+            Calibrations.RootView(resolver: resolver)
         }
     }
 

+ 426 - 0
FreeAPS/Sources/Services/Network/TidepoolManager.swift

@@ -0,0 +1,426 @@
+import Combine
+import Foundation
+import HealthKit
+import LoopKit
+import LoopKitUI
+import Swinject
+
+protocol TidePoolManager {
+    func addTidePoolService(service: Service)
+    func getTidePoolServiceUI() -> ServiceUI?
+    func getTidePoolPluginHost() -> PluginHost?
+    func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
+    func deleteInsulin(at date: Date)
+//    func uploadStatus()
+    func uploadGlucose(device: HKDevice?)
+    func forceUploadData(device: HKDevice?)
+//    func uploadStatistics(dailystat: Statistics)
+//    func uploadPreferences(_ preferences: Preferences)
+//    func uploadProfileAndSettings(_: Bool)
+}
+
+final class BaseTidePoolManager: TidePoolManager, Injectable {
+    @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var pluginManager: PluginManager!
+    @Injected() private var glucoseStorage: GlucoseStorage!
+    @Injected() private var carbsStorage: CarbsStorage!
+    @Injected() private var storage: FileStorage!
+    @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
+
+    private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
+    private var tidePoolService: RemoteDataService? {
+        didSet {
+            if let tidePoolService = tidePoolService {
+                rawTidePoolManager = tidePoolService.rawValue
+            } else {
+                rawTidePoolManager = nil
+            }
+        }
+    }
+
+    @PersistedProperty(key: "TidePoolState") var rawTidePoolManager: Service.RawValue?
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+        loadTidePoolManager()
+        subscribe()
+    }
+
+    /// load the TidePool Remote Data Service if available
+    fileprivate func loadTidePoolManager() {
+        if let rawTidePoolManager = rawTidePoolManager {
+            tidePoolService = tidePoolServiceFromRaw(rawTidePoolManager)
+            tidePoolService?.serviceDelegate = self
+            tidePoolService?.stateDelegate = self
+        }
+    }
+
+    /// allows to acces to tidePoolService as a simple ServiceUI
+    func getTidePoolServiceUI() -> ServiceUI? {
+        if let tidePoolService = self.tidePoolService {
+            return tidePoolService as! any ServiceUI as ServiceUI
+        } else {
+            return nil
+        }
+    }
+
+    /// get the pluginHost of TidePool
+    func getTidePoolPluginHost() -> PluginHost? {
+        self as PluginHost
+    }
+
+    func addTidePoolService(service: Service) {
+        tidePoolService = service as! any RemoteDataService as RemoteDataService
+    }
+
+    /// load the TidePool Remote Data Service from raw storage
+    private func tidePoolServiceFromRaw(_ rawValue: [String: Any]) -> RemoteDataService? {
+        guard let rawState = rawValue["state"] as? Service.RawStateValue,
+              let serviceType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
+        else {
+            return nil
+        }
+        if let service = serviceType.init(rawState: rawState) {
+            return service as! any RemoteDataService as RemoteDataService
+        } else { return nil }
+    }
+
+    private func subscribe() {
+        broadcaster.register(PumpHistoryObserver.self, observer: self)
+        broadcaster.register(CarbsObserver.self, observer: self)
+        broadcaster.register(TempTargetsObserver.self, observer: self)
+    }
+
+    func sourceInfo() -> [String: Any]? {
+        nil
+    }
+
+    func uploadCarbs() {
+        let carbs: [CarbsEntry] = carbsStorage.recent()
+
+        guard !carbs.isEmpty, let tidePoolService = self.tidePoolService else { return }
+
+        processQueue.async {
+            carbs.chunks(ofCount: tidePoolService.carbDataLimit ?? 100).forEach { chunk in
+
+                let syncCarb: [SyncCarbObject] = Array(chunk).map {
+                    $0.convertSyncCarb()
+                }
+                tidePoolService.uploadCarbData(created: syncCarb, updated: [], deleted: []) { result in
+                    switch result {
+                    case let .failure(error):
+                        debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
+                    case .success:
+                        debug(.nightscout, "Success synchronizing carbs data:")
+                    }
+                }
+            }
+        }
+    }
+
+    func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID _: String) {
+        guard let tidePoolService = self.tidePoolService else { return }
+
+        processQueue.async {
+            var carbsToDelete: [CarbsEntry] = []
+            let allValues = self.storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
+
+            if let isFPU = isFPU, isFPU {
+                guard let fpuID = fpuID else { return }
+                carbsToDelete = allValues.filter { $0.fpuID == fpuID }.removeDublicates()
+            } else {
+                carbsToDelete = allValues.filter { $0.createdAt == date }.removeDublicates()
+            }
+
+            let syncCarb = carbsToDelete.map { d in
+                d.convertSyncCarb(operation: .delete)
+            }
+
+            tidePoolService.uploadCarbData(created: [], updated: [], deleted: syncCarb) { result in
+                switch result {
+                case let .failure(error):
+                    debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
+                case .success:
+                    debug(.nightscout, "Success synchronizing carbs data:")
+                }
+            }
+        }
+    }
+
+    func deleteInsulin(at d: Date) {
+        let allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
+
+        guard !allValues.isEmpty, let tidePoolService = self.tidePoolService else { return }
+
+        var doseDataToDelete: [DoseEntry] = []
+
+        guard let entry = allValues.first(where: { $0.timestamp == d }) else {
+            return
+        }
+        doseDataToDelete
+            .append(DoseEntry(
+                type: .bolus,
+                startDate: entry.timestamp,
+                value: Double(entry.amount!),
+                unit: .units,
+                syncIdentifier: entry.id
+            ))
+
+        processQueue.async {
+            tidePoolService.uploadDoseData(created: [], deleted: doseDataToDelete) { result in
+                switch result {
+                case let .failure(error):
+                    debug(.nightscout, "Error synchronizing Dose delete data: \(String(describing: error))")
+                case .success:
+                    debug(.nightscout, "Success synchronizing Dose delete data:")
+                }
+            }
+        }
+    }
+
+    func uploadDose() {
+        let events = pumpHistoryStorage.recent()
+        guard !events.isEmpty, let tidePoolService = self.tidePoolService else { return }
+
+        let eventsBasal = events.filter { $0.type == .tempBasal || $0.type == .tempBasalDuration }
+            .sorted { $0.timestamp < $1.timestamp }
+
+        let doseDataBasal: [DoseEntry] = eventsBasal.reduce([]) { result, event in
+            var result = result
+            switch event.type {
+            case .tempBasal:
+                // update the previous tempBasal with endtime = starttime of the last event
+                if let last: DoseEntry = result.popLast() {
+                    let value = max(
+                        0,
+                        Double(event.timestamp.timeIntervalSince1970 - last.startDate.timeIntervalSince1970) / 3600
+                    ) *
+                        (last.scheduledBasalRate?.doubleValue(for: .internationalUnitsPerHour) ?? 0.0)
+                    result.append(DoseEntry(
+                        type: .tempBasal,
+                        startDate: last.startDate,
+                        endDate: event.timestamp,
+                        value: value,
+                        unit: last.unit,
+                        deliveredUnits: value,
+                        syncIdentifier: last.syncIdentifier,
+                        // scheduledBasalRate: last.scheduledBasalRate,
+                        insulinType: last.insulinType,
+                        automatic: last.automatic,
+                        manuallyEntered: last.manuallyEntered
+                    ))
+                }
+                result.append(DoseEntry(
+                    type: .tempBasal,
+                    startDate: event.timestamp,
+                    value: 0.0,
+                    unit: .unitsPerHour,
+                    syncIdentifier: event.id,
+                    scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: Double(event.rate!)),
+                    insulinType: nil,
+                    automatic: true,
+                    manuallyEntered: false,
+                    isMutable: true
+                ))
+            case .tempBasalDuration:
+                if let last: DoseEntry = result.popLast(),
+                   last.type == .tempBasal,
+                   last.startDate == event.timestamp
+                {
+                    let durationMin = event.durationMin ?? 0
+                    // result.append(last)
+                    let value = (Double(durationMin) / 60.0) *
+                        (last.scheduledBasalRate?.doubleValue(for: .internationalUnitsPerHour) ?? 0.0)
+                    result.append(DoseEntry(
+                        type: .tempBasal,
+                        startDate: last.startDate,
+                        endDate: Calendar.current.date(byAdding: .minute, value: durationMin, to: last.startDate) ?? last
+                            .startDate,
+                        value: value,
+                        unit: last.unit,
+                        deliveredUnits: value,
+                        syncIdentifier: last.syncIdentifier,
+                        scheduledBasalRate: last.scheduledBasalRate,
+                        insulinType: last.insulinType,
+                        automatic: last.automatic,
+                        manuallyEntered: last.manuallyEntered
+                    ))
+                }
+            default: break
+            }
+            return result
+        }
+
+        let boluses: [DoseEntry] = events.compactMap { event -> DoseEntry? in
+            switch event.type {
+            case .bolus:
+                return DoseEntry(
+                    type: .bolus,
+                    startDate: event.timestamp,
+                    endDate: event.timestamp,
+                    value: Double(event.amount!),
+                    unit: .units,
+                    deliveredUnits: nil,
+                    syncIdentifier: event.id,
+                    scheduledBasalRate: nil,
+                    insulinType: nil,
+                    automatic: true,
+                    manuallyEntered: false
+                )
+            default: return nil
+            }
+        }
+
+        let pumpEvents: [PersistedPumpEvent] = events.compactMap { event -> PersistedPumpEvent? in
+            if let pumpEventType = event.type.mapEventTypeToPumpEventType() {
+                let dose: DoseEntry? = switch pumpEventType {
+                case .suspend:
+                    DoseEntry(suspendDate: event.timestamp, automatic: true)
+                case .resume:
+                    DoseEntry(resumeDate: event.timestamp, automatic: true)
+                default:
+                    nil
+                }
+
+                return PersistedPumpEvent(
+                    date: event.timestamp,
+                    persistedDate: event.timestamp,
+                    dose: dose,
+                    isUploaded: true,
+                    objectIDURL: URL(string: "x-coredata:///PumpEvent/\(event.id)")!,
+                    raw: event.id.data(using: .utf8),
+                    title: event.note,
+                    type: pumpEventType
+                )
+            } else {
+                return nil
+            }
+        }
+
+        processQueue.async {
+            tidePoolService.uploadDoseData(created: doseDataBasal + boluses, deleted: []) { result in
+                switch result {
+                case let .failure(error):
+                    debug(.nightscout, "Error synchronizing Dose data: \(String(describing: error))")
+                case .success:
+                    debug(.nightscout, "Success synchronizing Dose data:")
+                }
+            }
+
+            tidePoolService.uploadPumpEventData(pumpEvents) { result in
+                switch result {
+                case let .failure(error):
+                    debug(.nightscout, "Error synchronizing Pump Event data: \(String(describing: error))")
+                case .success:
+                    debug(.nightscout, "Success synchronizing Pump Event data:")
+                }
+            }
+        }
+    }
+
+    func uploadGlucose(device: HKDevice?) {
+        let glucose: [BloodGlucose] = glucoseStorage.recent()
+
+        guard !glucose.isEmpty, let tidePoolService = self.tidePoolService else { return }
+
+        let glucoseWithoutCorrectID = glucose.filter { UUID(uuidString: $0._id) != nil }
+
+        processQueue.async {
+            glucoseWithoutCorrectID.chunks(ofCount: tidePoolService.glucoseDataLimit ?? 100)
+                .forEach { chunk in
+                    // all glucose attached with the current device ;-(
+
+                    let chunkStoreGlucose = Array(chunk).map {
+                        $0.convertStoredGlucoseSample(device: device)
+                    }
+                    tidePoolService.uploadGlucoseData(chunkStoreGlucose) { result in
+                        switch result {
+                        case let .failure(error):
+                            debug(.nightscout, "Error synchronizing glucose data: \(String(describing: error))")
+                        // self.uploadFailed(key)
+                        case .success:
+                            debug(.nightscout, "Success synchronizing glucose data:")
+                        }
+                    }
+                }
+        }
+    }
+
+    /// force to uploads all data in TidePool Service
+    func forceUploadData(device: HKDevice?) {
+        uploadDose()
+        uploadCarbs()
+        uploadGlucose(device: device)
+    }
+}
+
+extension BaseTidePoolManager: PumpHistoryObserver {
+    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
+        uploadDose()
+    }
+}
+
+extension BaseTidePoolManager: CarbsObserver {
+    func carbsDidUpdate(_: [CarbsEntry]) {
+        uploadCarbs()
+    }
+}
+
+extension BaseTidePoolManager: TempTargetsObserver {
+    func tempTargetsDidUpdate(_: [TempTarget]) {}
+}
+
+extension BaseTidePoolManager: ServiceDelegate {
+    var hostIdentifier: String {
+        "com.loopkit.Loop" // To check
+    }
+
+    var hostVersion: String {
+        var semanticVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
+
+        while semanticVersion.split(separator: ".").count < 3 {
+            semanticVersion += ".0"
+        }
+
+        semanticVersion += "+\(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String)"
+
+        return semanticVersion
+    }
+
+    func issueAlert(_: LoopKit.Alert) {}
+
+    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
+
+    func enactRemoteOverride(name _: String, durationTime _: TimeInterval?, remoteAddress _: String) async throws {}
+
+    func cancelRemoteOverride() async throws {}
+
+    func deliverRemoteCarbs(
+        amountInGrams _: Double,
+        absorptionTime _: TimeInterval?,
+        foodType _: String?,
+        startDate _: Date?
+    ) async throws {}
+
+    func deliverRemoteBolus(amountInUnits _: Double) async throws {}
+}
+
+extension BaseTidePoolManager: StatefulPluggableDelegate {
+    func pluginDidUpdateState(_: LoopKit.StatefulPluggable) {}
+
+    func pluginWantsDeletion(_: LoopKit.StatefulPluggable) {
+        tidePoolService = nil
+    }
+}
+
+// Service extension for rawValue
+extension Service {
+    typealias RawValue = [String: Any]
+
+    var rawValue: RawValue {
+        [
+            "serviceIdentifier": pluginIdentifier,
+            "state": rawState
+        ]
+    }
+}

+ 55 - 0
FreeAPSTests/CalibrationsTests.swift

@@ -0,0 +1,55 @@
+@testable import FreeAPS
+import Swinject
+import XCTest
+
+class CalibrationsTests: XCTestCase, Injectable {
+    let fileStorage = BaseFileStorage()
+    @Injected() var calibrationService: CalibrationService!
+    let resolver = FreeAPSApp().resolver
+
+    override func setUp() {
+        injectServices(resolver)
+    }
+
+    func testCreateSimpleCalibration() {
+        let calibration = Calibration(x: 100.0, y: 102.0)
+        calibrationService.addCalibration(calibration)
+
+        XCTAssertTrue(calibrationService.calibrations.isNotEmpty)
+
+        XCTAssertTrue(calibrationService.slope == 1)
+
+        XCTAssertTrue(calibrationService.intercept == 2)
+
+        XCTAssertTrue(calibrationService.calibrate(value: 104) == 106)
+    }
+
+    func testCreateMultipleCalibration() {
+        let calibration = Calibration(x: 100.0, y: 120)
+        calibrationService.addCalibration(calibration)
+
+        let calibration2 = Calibration(x: 120.0, y: 130.0)
+        calibrationService.addCalibration(calibration2)
+
+        XCTAssertTrue(calibrationService.slope == 0.8)
+
+        XCTAssertTrue(calibrationService.intercept == 37)
+
+        XCTAssertTrue(calibrationService.calibrate(value: 80) == 101)
+
+        calibrationService.removeLast()
+
+        XCTAssertTrue(calibrationService.calibrations.count == 1)
+
+        calibrationService.removeAllCalibrations()
+        XCTAssertTrue(calibrationService.calibrations.isEmpty)
+    }
+
+    override func setUpWithError() throws {
+        // Put setup code here. This method is called before the invocation of each test method in the class.
+    }
+
+    override func tearDownWithError() throws {
+        // Put teardown code here. This method is called after the invocation of each test method in the class.
+    }
+}

+ 71 - 0
FreeAPSTests/PluginManagerTests.swift

@@ -0,0 +1,71 @@
+@testable import FreeAPS
+import Swinject
+import XCTest
+
+class PluginManagerTests: XCTestCase, Injectable {
+    let fileStorage = BaseFileStorage()
+    @Injected() var pluginManager: PluginManager!
+    let resolver = FreeAPSApp().resolver
+
+    override func setUp() {
+        injectServices(resolver)
+    }
+
+    func testCGMManagerLoad() {
+        let cgmLoopManagers = pluginManager.availableCGMManagers
+        XCTAssertNotNil(cgmLoopManagers)
+        XCTAssertTrue(!cgmLoopManagers.isEmpty)
+        if let cgmLoop = cgmLoopManagers.first {
+            let cgmLoopManager = pluginManager.getCGMManagerTypeByIdentifier(cgmLoop.identifier)
+            XCTAssertNotNil(cgmLoopManager)
+        } else {
+            XCTFail("Not found CGM loop manager")
+        }
+        /// try to load a Pump manager with a CGM identifier
+        if let cgmLoop = cgmLoopManagers.last {
+            let cgmLoopManager = pluginManager.getPumpManagerTypeByIdentifier(cgmLoop.identifier)
+            XCTAssertNil(cgmLoopManager)
+        } else {
+            XCTFail("Not found CGM loop manager")
+        }
+    }
+
+    func testPumpManagerLoad() {
+        let pumpLoopManagers = pluginManager.availablePumpManagers
+        XCTAssertNotNil(pumpLoopManagers)
+        XCTAssertTrue(!pumpLoopManagers.isEmpty)
+        if let pumpLoop = pumpLoopManagers.first {
+            let pumpLoopManager = pluginManager.getPumpManagerTypeByIdentifier(pumpLoop.identifier)
+            XCTAssertNotNil(pumpLoopManager)
+        } else {
+            XCTFail("Not found pump loop manager")
+        }
+        /// try to load a CGM manager with a pump identifier
+        if let pumpLoop = pumpLoopManagers.last {
+            let pumpLoopManager = pluginManager.getCGMManagerTypeByIdentifier(pumpLoop.identifier)
+            XCTAssertNil(pumpLoopManager)
+        } else {
+            XCTFail("Not found pump loop manager")
+        }
+    }
+
+    func testServiceManagerLoad() {
+        let serviceManagers = pluginManager.availableServices
+        XCTAssertNotNil(serviceManagers)
+        XCTAssertTrue(!serviceManagers.isEmpty)
+        if let serviceLoop = serviceManagers.first {
+            let serviceManager = pluginManager.getServiceTypeByIdentifier(serviceLoop.identifier)
+            XCTAssertNotNil(serviceManager)
+        } else {
+            XCTFail("Not found Service loop manager")
+        }
+    }
+
+    override func setUpWithError() throws {
+        // Put setup code here. This method is called before the invocation of each test method in the class.
+    }
+
+    override func tearDownWithError() throws {
+        // Put teardown code here. This method is called after the invocation of each test method in the class.
+    }
+}

+ 2 - 2
README.md

@@ -13,8 +13,8 @@ Open-iAPS continues to leverage a variety of frameworks from the DIY looping com
 
 ## To download this repo:
 
-In Terminal:  
-git clone --branch=main https://github.com/nightscout/Open-iAPS.git  
+In Terminal: 
+git clone --branch=<<branch>> --recurse-submodules https://github.com/nightscout/Open-iAPS.git
 cd Open-iAPS  
 xed .
 

+ 1 - 0
TidepoolService

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

+ 1 - 1
scripts/swiftformat.sh

@@ -97,4 +97,4 @@ trailingClosures \
 --typeattributes same-line \
 --varattributes same-line \
 --wrapcollections before-first \
---exclude Pods,Generated,R.generated.swift,fastlane/swift,Dependencies, LoopKit, LibreTransmitter,G7SensorKit,OmniKit, dexcom-share-client-swift,CGMBLEKit,RileyLinkKit,OmniBLE,MinimedKit
+--exclude Pods,Generated,R.generated.swift,fastlane/swift,Dependencies, LoopKit, LibreTransmitter,G7SensorKit,OmniKit, dexcom-share-client-swift,CGMBLEKit,RileyLinkKit,OmniBLE,MinimedKit,TidepoolService