Quellcode durchsuchen

add calibration of Libre CGM

Add calibration functions allowing to transform a BG to a corrected BG with a linear function.
Add interface to manage calibrations points
add calibration to calculate a new value of BG
add notification to remove calibration when change CGM or sensors

This functionality is activated only for Libre CGM even could be use for all CGM.

(cherry picked from commit 6bdd3a21729b992ebbdc6709e3f62738659ac8ff)
Pierre L vor 2 Jahren
Ursprung
Commit
491ddf29c4

+ 52 - 6
FreeAPS.xcodeproj/project.pbxproj

@@ -248,8 +248,6 @@
 		69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA5E04A2761F6EEA6568E1 /* CREditorStateModel.swift */; };
 		69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA5E04A2761F6EEA6568E1 /* CREditorStateModel.swift */; };
 		6B1F539F9FF75646D1606066 /* SnoozeDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A708CDB546692C2230B385 /* SnoozeDataFlow.swift */; };
 		6B1F539F9FF75646D1606066 /* SnoozeDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A708CDB546692C2230B385 /* SnoozeDataFlow.swift */; };
 		6B9625766B697D1C98E455A2 /* PumpSettingsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72778B68C3004F71F6E79BDC /* PumpSettingsEditorStateModel.swift */; };
 		6B9625766B697D1C98E455A2 /* PumpSettingsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72778B68C3004F71F6E79BDC /* PumpSettingsEditorStateModel.swift */; };
-		6BCF84DD2B16843A003AD46E /* LiveActitiyShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */; };
-		6BCF84DE2B16843A003AD46E /* LiveActitiyShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */; };
 		6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAE81192B118804DCD23034 /* SnoozeProvider.swift */; };
 		6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAE81192B118804DCD23034 /* SnoozeProvider.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
@@ -273,8 +271,6 @@
 		B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = B958F1B62BA0711600484851 /* MKRingProgressView */; };
 		B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = B958F1B62BA0711600484851 /* MKRingProgressView */; };
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
-		BD188BEC2B1B805B00B183BF /* WidgetBobble.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */; };
-		BD188BED2B1B805B00B183BF /* WidgetBobble.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
@@ -328,6 +324,13 @@
 		CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */; };
 		CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */; };
 		CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = CEB434FC28B90B7C00B70274 /* SwiftCharts */; };
 		CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = CEB434FC28B90B7C00B70274 /* SwiftCharts */; };
 		CEB434FE28B90B8C00B70274 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = CEB434FC28B90B7C00B70274 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		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 */; };
 		D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */; };
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
@@ -763,8 +766,6 @@
 		B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMDataFlow.swift; sourceTree = "<group>"; };
 		B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMDataFlow.swift; sourceTree = "<group>"; };
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
-		BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBobble.swift; sourceTree = "<group>"; };
-		BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
@@ -819,6 +820,13 @@
 		CEC751D329D88257006E9D24 /* OmniKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		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; };
 		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; };
 		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>"; };
 		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>"; };
 		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>"; };
 		D295A3F870E826BE371C0BB5 /* AutotuneConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigStateModel.swift; sourceTree = "<group>"; };
@@ -1086,6 +1094,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				CEE9A64D2BBB411C00EB5194 /* Calibrations */,
 				190EBCC229FF134900BA767D /* StatConfig */,
 				190EBCC229FF134900BA767D /* StatConfig */,
 				CE94597C29E9E1CD0047C9C6 /* WatchConfig */,
 				CE94597C29E9E1CD0047C9C6 /* WatchConfig */,
 				19F95FF129F10F9C00314DDC /* Stat */,
 				19F95FF129F10F9C00314DDC /* Stat */,
@@ -1418,6 +1427,7 @@
 		3856933F270B57A00002C50D /* CGM */ = {
 		3856933F270B57A00002C50D /* CGM */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				CEE9A65A2BBB41AD00EB5194 /* Calibrations */,
 				F816825F28DB441800054060 /* BluetoothTransmitter.swift */,
 				F816825F28DB441800054060 /* BluetoothTransmitter.swift */,
 				F816825D28DB441200054060 /* HeartBeatManager.swift */,
 				F816825D28DB441200054060 /* HeartBeatManager.swift */,
 				38569346270B5DFB0002C50D /* AppGroupSource.swift */,
 				38569346270B5DFB0002C50D /* AppGroupSource.swift */,
@@ -1758,6 +1768,7 @@
 				38FCF3F125E9028E0078B0D1 /* Info.plist */,
 				38FCF3F125E9028E0078B0D1 /* Info.plist */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
 				CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
+				CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
 			);
 			);
 			path = FreeAPSTests;
 			path = FreeAPSTests;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2022,6 +2033,34 @@
 			path = Bluetooth;
 			path = Bluetooth;
 			sourceTree = "<group>";
 			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 */ = {
 		D533BF261CDC1C3F871E7BFD /* NightscoutConfig */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -2449,6 +2488,7 @@
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
+				CEE9A6552BBB418300EB5194 /* CalibrationsProvider.swift in Sources */,
 				19F95FF529F10FCF00314DDC /* StatProvider.swift in Sources */,
 				19F95FF529F10FCF00314DDC /* StatProvider.swift in Sources */,
 				38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */,
 				38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */,
 				19B0EF2128F6D66200069496 /* Statistics.swift in Sources */,
 				19B0EF2128F6D66200069496 /* Statistics.swift in Sources */,
@@ -2469,14 +2509,17 @@
 				382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */,
 				382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */,
 				19D466A529AA2BD4004D5F33 /* FPUConfigProvider.swift in Sources */,
 				19D466A529AA2BD4004D5F33 /* FPUConfigProvider.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
+				CEE9A6572BBB418300EB5194 /* CalibrationsChart.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				38192E04261B82FA0094D973 /* ReachabilityManager.swift in Sources */,
 				38192E04261B82FA0094D973 /* ReachabilityManager.swift in Sources */,
 				38E44539274E411700EC9A94 /* Disk+UIImage.swift in Sources */,
 				38E44539274E411700EC9A94 /* Disk+UIImage.swift in Sources */,
 				388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */,
 				388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */,
 				38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */,
 				38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */,
 				38569348270B5DFB0002C50D /* GlucoseSource.swift in Sources */,
 				38569348270B5DFB0002C50D /* GlucoseSource.swift in Sources */,
+				CEE9A6582BBB418300EB5194 /* CalibrationsStateModel.swift in Sources */,
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
+				CEE9A6562BBB418300EB5194 /* CalibrationsRootView.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
 				CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */,
 				CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */,
 				38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */,
 				38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */,
@@ -2525,6 +2568,7 @@
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
 				3871F39F25ED895A0013ECB5 /* Decimal+Extensions.swift in Sources */,
 				3871F39F25ED895A0013ECB5 /* Decimal+Extensions.swift in Sources */,
+				CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
 				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
 				38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */,
 				38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */,
@@ -2636,6 +2680,7 @@
 				69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */,
 				69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */,
 				38E44538274E411700EC9A94 /* Disk+[Data].swift in Sources */,
 				38E44538274E411700EC9A94 /* Disk+[Data].swift in Sources */,
 				98641AF4F92123DA668AB931 /* CREditorRootView.swift in Sources */,
 				98641AF4F92123DA668AB931 /* CREditorRootView.swift in Sources */,
+				CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */,
 				38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */,
 				38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
@@ -2743,6 +2788,7 @@
 			isa = PBXSourcesBuildPhase;
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
 			files = (
 			files = (
+				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
 				CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */,
 				CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
 			);
 			);

+ 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))
         dispatchPrecondition(condition: .onQueue(processQueue))
         // TODO: Events in APS ?
         // TODO: Events in APS ?
         // currently only display in log the date of the event
         // 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? {
     func startDateToFilterNewData(for _: CGMManager) -> Date? {
@@ -125,6 +131,7 @@ extension PluginSource: CGMManagerDelegate {
     }
     }
 
 
     func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
     func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
+        debug(.deviceManager, "DEBUG DID UPDATE STATE")
         processQueue.async {
         processQueue.async {
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
                 self.cgmHasValidSensorSession = status.hasValidSensorSession
                 self.cgmHasValidSensorSession = status.hasValidSensorSession

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

@@ -11,6 +11,7 @@ protocol FetchGlucoseManager: SourceInfoProvider {
     func refreshCGM()
     func refreshCGM()
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
     func deleteGlucoseSource()
     func deleteGlucoseSource()
+    func removeCalibrations()
     var glucoseSource: GlucoseSource! { get }
     var glucoseSource: GlucoseSource! { get }
     var cgmManager: CGMManagerUI? { get }
     var cgmManager: CGMManagerUI? { get }
     var cgmGlucoseSourceType: CGMType? { get set }
     var cgmGlucoseSourceType: CGMType? { get set }
@@ -35,6 +36,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     @Injected() var healthKitManager: HealthKitManager!
     @Injected() var healthKitManager: HealthKitManager!
     @Injected() var deviceDataManager: DeviceDataManager!
     @Injected() var deviceDataManager: DeviceDataManager!
     @Injected() var pluginCGMManager: PluginManager!
     @Injected() var pluginCGMManager: PluginManager!
+    @Injected() var calibrationService: CalibrationService!
 
 
     private var lifetime = Lifetime()
     private var lifetime = Lifetime()
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
@@ -68,6 +70,10 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
 
     var glucoseSource: GlucoseSource!
     var glucoseSource: GlucoseSource!
 
 
+    func removeCalibrations() {
+        calibrationService.removeAllCalibrations()
+    }
+
     func deleteGlucoseSource() {
     func deleteGlucoseSource() {
         cgmManager = nil
         cgmManager = nil
         updateGlucoseSource(
         updateGlucoseSource(
@@ -77,6 +83,11 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     }
     }
 
 
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
     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.cgmGlucoseSourceType = cgmGlucoseSourceType
         self.cgmGlucosePluginId = cgmGlucosePluginId
         self.cgmGlucosePluginId = cgmGlucosePluginId
 
 
@@ -88,12 +99,10 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         if let manager = newManager
         if let manager = newManager
         {
         {
             cgmManager = manager
             cgmManager = manager
+            removeCalibrations()
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
         }
         }
-//        } else if self.cgmGlucoseSourceType == .plugin, self.cgmGlucosePluginId != , self.cgmGlucosePluginId != cgmManager?.pluginIdentifier  {
-//            cgmManager = nil
-//        }
 
 
         switch self.cgmGlucoseSourceType {
         switch self.cgmGlucoseSourceType {
         case nil,
         case nil,
@@ -154,7 +163,10 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     }
     }
 
 
     private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose], glucoseFromHealth: [BloodGlucose] = []) {
     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 filteredByDate: [BloodGlucose] = []
         var filtered: [BloodGlucose] = []
         var filtered: [BloodGlucose] = []
 
 
@@ -266,6 +278,23 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     func sourceInfo() -> [String: Any]? {
     func sourceInfo() -> [String: Any]? {
         glucoseSource.sourceInfo()
         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 {
 extension CGMManager {

+ 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(FetchAnnouncementsManager.self) { r in BaseFetchAnnouncementsManager(resolver: r) }
         container.register(BluetoothStateManager.self) { r in BaseBluetoothStateManager(resolver: r) }
         container.register(BluetoothStateManager.self) { r in BaseBluetoothStateManager(resolver: r) }
         container.register(PluginManager.self) { r in BasePluginManager(resolver: r) }
         container.register(PluginManager.self) { r in BasePluginManager(resolver: r) }
+        container.register(CalibrationService.self) { r in BaseCalibrationService(resolver: r) }
     }
     }
 }
 }

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

@@ -47,6 +47,12 @@ extension CGM {
                             }
                             }
                         }
                         }
                     }
                     }
+                    if state.cgmCurrent.type == .plugin && state.cgmCurrent.id.contains("Libre") {
+                        Section(header: Text("Calibrations")) {
+                            Text("Calibrations").navigationLink(to: .calibrations, from: self)
+                        }
+                    }
+
                     Section(header: Text("Calendar")) {
                     Section(header: Text("Calendar")) {
                         Toggle("Create events in calendar", isOn: $state.createCalendarEvents)
                         Toggle("Create events in calendar", isOn: $state.createCalendarEvents)
                         if state.calendarIDs.isNotEmpty {
                         if state.calendarIDs.isNotEmpty {

+ 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])
+        }
+    }
+}

+ 3 - 0
FreeAPS/Sources/Router/Screen.swift

@@ -30,6 +30,7 @@ enum Screen: Identifiable, Hashable {
     case statistics
     case statistics
     case watch
     case watch
     case statisticsConfig
     case statisticsConfig
+    case calibrations
 
 
     var id: Int { String(reflecting: self).hashValue }
     var id: Int { String(reflecting: self).hashValue }
 }
 }
@@ -93,6 +94,8 @@ extension Screen {
             Stat.RootView(resolver: resolver)
             Stat.RootView(resolver: resolver)
         case .statisticsConfig:
         case .statisticsConfig:
             StatConfig.RootView(resolver: resolver)
             StatConfig.RootView(resolver: resolver)
+        case .calibrations:
+            Calibrations.RootView(resolver: resolver)
         }
         }
     }
     }
 
 

+ 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.
+    }
+}