Browse Source

Overcalibrations for Libre

Ivan Valkou 4 years ago
parent
commit
933ebe4bb0

+ 44 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -18,6 +18,7 @@
 		28089E07169488CF6DCC2A31 /* AddCarbsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86FC1CFD647CF34508AF9A3B /* AddCarbsRootView.swift */; };
 		2BE9A6FA20875F6F4F9CD461 /* PumpSettingsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97F14812C1AFED3621165A5 /* PumpSettingsEditorProvider.swift */; };
 		3083261C4B268E353F36CD0B /* AutotuneConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCCCE633F5E98E41B0CD3C /* AutotuneConfigDataFlow.swift */; };
+		320D030F724170A637F06D50 /* CalibrationsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212E8BFE6D66EE65AA26A114 /* CalibrationsProvider.swift */; };
 		33E198D3039045D98C3DC5D4 /* AddCarbsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E7C997E56DAF8D28D09014 /* AddCarbsStateModel.swift */; };
 		3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE0725C9D32E00A708ED /* BaseView.swift */; };
 		3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE0825C9D32F00A708ED /* BaseProvider.swift */; };
@@ -77,6 +78,7 @@
 		385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */; };
 		385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC025F2EA52002D6D5B /* Announcement.swift */; };
 		385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */; };
+		3862CC05273D152B00BF832C /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC04273D152B00BF832C /* CalibrationService.swift */; };
 		386A124C271704DA00DDC61C /* CGMBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 386A124B271704DA00DDC61C /* CGMBLEKit.framework */; };
 		386A124D271704DA00DDC61C /* CGMBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 386A124B271704DA00DDC61C /* CGMBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		386A124F271707F000DDC61C /* DexcomSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 386A124E271707F000DDC61C /* DexcomSource.swift */; };
@@ -219,7 +221,9 @@
 		A33352ED40476125EBAC6EE0 /* CREditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E22146D3DF4853786C78132 /* CREditorDataFlow.swift */; };
 		A6F097A14CAAE0CE0D11BE1B /* AddCarbsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618E62C9757B2F95431B5DC0 /* AddCarbsProvider.swift */; };
 		AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */; };
+		B7C465E9472624D8A2BE2A6A /* CalibrationsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA241FB1663EC96FDBE64C8A /* CalibrationsDataFlow.swift */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
+		BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
@@ -237,6 +241,7 @@
 		E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0127368630002FF094 /* APSAssembly.swift */; };
 		E00EEC0827368630002FF094 /* NetworkAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0227368630002FF094 /* NetworkAssembly.swift */; };
 		E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F58DDD71F0E795464FA3F0 /* TargetsEditorStateModel.swift */; };
+		E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DFCE895C930F784EF11843 /* CalibrationsStateModel.swift */; };
 		E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */; };
 		E4984C5262A90469788754BB /* PreferencesEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8BA8533F56BC55748CA877 /* PreferencesEditorProvider.swift */; };
 		E97285ED9B814CD5253C6658 /* AddCarbsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F48C3AC770D4CCD0EA2B0C2 /* AddCarbsDataFlow.swift */; };
@@ -309,6 +314,7 @@
 		198377E4266C13D2004DE65E /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
 		199732B4271B72DD00129A3F /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		199732B5271B9EE900129A3F /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
+		212E8BFE6D66EE65AA26A114 /* CalibrationsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsProvider.swift; sourceTree = "<group>"; };
 		223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusStateModel.swift; sourceTree = "<group>"; };
 		2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigRootView.swift; sourceTree = "<group>"; };
 		2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -368,6 +374,7 @@
 		385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutStatus.swift; sourceTree = "<group>"; };
 		385CEAC025F2EA52002D6D5B /* Announcement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Announcement.swift; sourceTree = "<group>"; };
 		385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsStorage.swift; sourceTree = "<group>"; };
+		3862CC04273D152B00BF832C /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
 		386A124B271704DA00DDC61C /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		386A124E271707F000DDC61C /* DexcomSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSource.swift; sourceTree = "<group>"; };
 		3870FF4225EC13F40088248F /* BloodGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucose.swift; sourceTree = "<group>"; };
@@ -472,7 +479,9 @@
 		3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorDataFlow.swift; sourceTree = "<group>"; };
 		42369F66CF91F30624C0B3A6 /* BasalProfileEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorProvider.swift; sourceTree = "<group>"; };
 		44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorProvider.swift; sourceTree = "<group>"; };
+		47DFCE895C930F784EF11843 /* CalibrationsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsStateModel.swift; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
+		500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
 		5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetDataFlow.swift; sourceTree = "<group>"; };
 		5C018D1680307A31C9ED7120 /* CGMStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMStateModel.swift; sourceTree = "<group>"; };
@@ -519,6 +528,7 @@
 		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>"; };
 		D97F14812C1AFED3621165A5 /* PumpSettingsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorProvider.swift; sourceTree = "<group>"; };
+		DA241FB1663EC96FDBE64C8A /* CalibrationsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsDataFlow.swift; sourceTree = "<group>"; };
 		E00EEBFD27368630002FF094 /* ServiceAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceAssembly.swift; sourceTree = "<group>"; };
 		E00EEBFE27368630002FF094 /* SecurityAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurityAssembly.swift; sourceTree = "<group>"; };
 		E00EEBFF27368630002FF094 /* StorageAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageAssembly.swift; sourceTree = "<group>"; };
@@ -638,6 +648,7 @@
 				A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */,
 				3811DE0425C9D32E00A708ED /* Base */,
 				C2C98283C436DB934D7E7994 /* Bolus */,
+				E8176B120B55CE89F1591542 /* Calibrations */,
 				F75CB57ED6971B46F8756083 /* CGM */,
 				0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */,
 				E42231DBF0DBE2B4B92D1B15 /* CREditor */,
@@ -898,10 +909,19 @@
 				386A124E271707F000DDC61C /* DexcomSource.swift */,
 				38569345270B5DFA0002C50D /* GlucoseSource.swift */,
 				38FEF407273B011A00574A46 /* LibreTransmitterSource.swift */,
+				3862CC03273D150600BF832C /* Calibrations */,
 			);
 			path = CGM;
 			sourceTree = "<group>";
 		};
+		3862CC03273D150600BF832C /* Calibrations */ = {
+			isa = PBXGroup;
+			children = (
+				3862CC04273D152B00BF832C /* CalibrationService.swift */,
+			);
+			path = Calibrations;
+			sourceTree = "<group>";
+		};
 		3883582E25EEAFC000E024B2 /* Views */ = {
 			isa = PBXGroup;
 			children = (
@@ -1141,6 +1161,14 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		43952E72FE7AF85715FE020E /* View */ = {
+			isa = PBXGroup;
+			children = (
+				500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		4E8C7B59F8065047ECE20965 /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -1398,6 +1426,17 @@
 			path = PumpSettingsEditor;
 			sourceTree = "<group>";
 		};
+		E8176B120B55CE89F1591542 /* Calibrations */ = {
+			isa = PBXGroup;
+			children = (
+				DA241FB1663EC96FDBE64C8A /* CalibrationsDataFlow.swift */,
+				212E8BFE6D66EE65AA26A114 /* CalibrationsProvider.swift */,
+				47DFCE895C930F784EF11843 /* CalibrationsStateModel.swift */,
+				43952E72FE7AF85715FE020E /* View */,
+			);
+			path = Calibrations;
+			sourceTree = "<group>";
+		};
 		EEC747824D6593B5CD87E195 /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -1680,6 +1719,7 @@
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
 				38BF021D25E7E3AF00579895 /* Reservoir.swift in Sources */,
 				38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */,
+				3862CC05273D152B00BF832C /* CalibrationService.swift in Sources */,
 				3811DEEA25CA063400A708ED /* SyncAccess.swift in Sources */,
 				38BF021F25E7F0DE00579895 /* DeviceDataManager.swift in Sources */,
 				38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
@@ -1786,6 +1826,10 @@
 				6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */,
 				903D18976088B09110BCBE29 /* LibreConfigStateModel.swift in Sources */,
 				9050F378F0063C064D7FFC86 /* LibreConfigRootView.swift in Sources */,
+				B7C465E9472624D8A2BE2A6A /* CalibrationsDataFlow.swift in Sources */,
+				320D030F724170A637F06D50 /* CalibrationsProvider.swift in Sources */,
+				E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */,
+				BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 2 - 2
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -24,8 +24,8 @@
         "repositoryURL": "https://github.com/ivalkou/LibreTransmitterX",
         "state": {
           "branch": null,
-          "revision": "f9591eb04bd812d0fae8f66d8a863e7cbcf2af9b",
-          "version": "1.0.3"
+          "revision": "17ec5eaaa41d65f6fc4659862d88dae23af2f25a",
+          "version": "1.0.4"
         }
       },
       {

+ 5 - 0
FreeAPS/Resources/FreeAPS.entitlements

@@ -2,6 +2,11 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>com.apple.developer.nfc.readersession.formats</key>
+	<array>
+		<string>NDEF</string>
+		<string>TAG</string>
+	</array>
 	<key>com.apple.security.application-groups</key>
 	<array>
 		<string>group.com.T7VZ6LU6H3.aps.JBMgroup</string>

+ 2 - 0
FreeAPS/Resources/Info.plist

@@ -67,6 +67,8 @@
 	</dict>
 	<key>UIApplicationSupportsIndirectInputEvents</key>
 	<true/>
+	<key>NFCReaderUsageDescription</key>
+	<string>NFC is used to scan Libre sensors.</string>
 	<key>UIBackgroundModes</key>
 	<array>
 		<string>bluetooth-central</string>

+ 1 - 0
FreeAPS/Sources/APS/CGM/AppGroupSource.swift

@@ -37,6 +37,7 @@ struct AppGroupSource: GlucoseSource {
                     direction: BloodGlucose.Direction(rawValue: direction),
                     date: Decimal(Int(date.timeIntervalSince1970 * 1000)),
                     dateString: date,
+                    unfiltered: nil,
                     filtered: nil,
                     noise: nil,
                     glucose: glucose,

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

@@ -0,0 +1,96 @@
+import Foundation
+import Swinject
+
+struct Calibration: JSON, Equatable {
+    let x: Double
+    let y: Double
+    var date = Date()
+
+    static let zero = Calibration(x: 0, y: 0)
+}
+
+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: Double) -> Double
+}
+
+final class BaseCalibrationService: CalibrationService, Injectable {
+    @Injected() var storage: FileStorage!
+
+    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) ?? []
+    }
+
+    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 slope
+    }
+
+    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 intercept
+    }
+
+    func calibrate(value: Double) -> 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: Double) -> Double {
+        intercept + slope * x
+    }
+}

+ 2 - 0
FreeAPS/Sources/APS/CGM/DexcomSource.swift

@@ -47,10 +47,12 @@ extension DexcomSource: TransmitterManagerDelegate {
             let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
 
             return BloodGlucose(
+                _id: glucose.syncIdentifier,
                 sgv: value,
                 direction: .init(trend: glucose.trend),
                 date: Decimal(Int(glucose.readDate.timeIntervalSince1970 * 1000)),
                 dateString: glucose.readDate,
+                unfiltered: nil,
                 filtered: nil,
                 noise: nil,
                 glucose: value,

+ 6 - 0
FreeAPS/Sources/APS/CGM/LibreTransmitterSource.swift

@@ -11,6 +11,7 @@ final class BaseLibreTransmitterSource: LibreTransmitterSource, Injectable {
     private let processQueue = DispatchQueue(label: "BaseLibreTransmitterSource.processQueue")
 
     @Injected() var glucoseStorage: GlucoseStorage!
+    @Injected() var calibrationService: CalibrationService!
 
     private var promise: Future<[BloodGlucose], Error>.Promise?
 
@@ -60,6 +61,7 @@ extension BaseLibreTransmitterSource: LibreTransmitterManagerDelegate {
                         .map { .init(trendType: $0) },
                     date: Decimal(Int(value.startDate.timeIntervalSince1970 * 1000)),
                     dateString: value.startDate,
+                    unfiltered: Decimal(value.unsmoothedGlucose),
                     filtered: nil,
                     noise: nil,
                     glucose: Int(value.glucose),
@@ -74,6 +76,10 @@ extension BaseLibreTransmitterSource: LibreTransmitterManagerDelegate {
             promise?(.failure(error))
         }
     }
+
+    func overcalibration(for _: LibreTransmitterManager) -> ((Double) -> (Double))? {
+        calibrationService.calibrate
+    }
 }
 
 extension BloodGlucose.Direction {

+ 1 - 0
FreeAPS/Sources/APS/OpenAPS/Constants.swift

@@ -84,5 +84,6 @@ extension OpenAPS {
         static let announcements = "freeaps/announcements.json"
         static let announcementsEnacted = "freeaps/announcements_enacted.json"
         static let tempTargetsPresets = "freeaps/temptargets_presets.json"
+        static let calibrations = "freeaps/calibrations.json"
     }
 }

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

@@ -3,11 +3,12 @@ import Swinject
 
 final class APSAssembly: Assembly {
     func assemble(container: Container) {
+        container.register(CalibrationService.self) { r in BaseCalibrationService(resolver: r) }
+        container.register(LibreTransmitterSource.self) { r in BaseLibreTransmitterSource(resolver: r) }
         container.register(DeviceDataManager.self) { r in BaseDeviceDataManager(resolver: r) }
         container.register(APSManager.self) { r in BaseAPSManager(resolver: r) }
         container.register(FetchGlucoseManager.self) { r in BaseFetchGlucoseManager(resolver: r) }
         container.register(FetchTreatmentsManager.self) { r in BaseFetchTreatmentsManager(resolver: r) }
         container.register(FetchAnnouncementsManager.self) { r in BaseFetchAnnouncementsManager(resolver: r) }
-        container.register(LibreTransmitterSource.self) { r in BaseLibreTransmitterSource(resolver: r) }
     }
 }

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

@@ -25,6 +25,7 @@ struct BloodGlucose: JSON, Identifiable, Hashable {
     var direction: Direction?
     let date: Decimal
     let dateString: Date
+    let unfiltered: Decimal?
     let filtered: Decimal?
     let noise: Int?
     var glucose: Int?

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

@@ -34,6 +34,7 @@ extension CGM {
                     Button("Configure Libre Transmitter") {
                         state.showModal(for: .libreConfig)
                     }
+                    Text("Calibrations").navigationLink(to: .calibrations, from: self)
                 }
 
                 Section(header: Text("Other")) {

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

@@ -0,0 +1,5 @@
+enum Calibrations {
+    enum Config {}
+}
+
+protocol CalibrationsProvider {}

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

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

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

@@ -0,0 +1,59 @@
+import SwiftDate
+import SwiftUI
+
+extension Calibrations {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var glucoseStorage: GlucoseStorage!
+        @Injected() var calibrationService: CalibrationService!
+        @Injected() var settingsManager: SettingsManager!
+
+        @Published var slope: Double = 1
+        @Published var intercept: Double = 1
+        @Published var calibration: Decimal = 0
+
+        @Published var calibrationsCount = 0
+
+        var units: GlucoseUnits = .mmolL
+
+        override func subscribe() {
+            slope = calibrationService.slope
+            intercept = calibrationService.intercept
+
+            units = settingsManager.settings.units
+            calibrationsCount = calibrationService.calibrations.count
+        }
+
+        func addCalibration() {
+            defer {
+                hideModal()
+            }
+
+            var glucose = calibration
+            if units == .mmolL {
+                glucose = calibration.asMgdL
+            }
+
+            guard let lastGlucose = glucoseStorage.recent().last,
+                  lastGlucose.dateString.addingTimeInterval(60 * 4.5) > Date(),
+                  let unfiltered = lastGlucose.unfiltered
+            else {
+                warning(.service, "Glucose is invalid for calibration")
+                return
+            }
+
+            let calibration = Calibration(x: Double(unfiltered), y: Double(glucose))
+
+            calibrationService.addCalibration(calibration)
+        }
+
+        func removeLast() {
+            calibrationService.removeLast()
+            hideModal()
+        }
+
+        func removeAll() {
+            calibrationService.removeAllCalibrations()
+            hideModal()
+        }
+    }
+}

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

@@ -0,0 +1,71 @@
+import SwiftUI
+import Swinject
+
+extension Calibrations {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        @State private var isPromtPresented = false
+        @State private var isRemoveAlertPresented = false
+        @State private var removeAlert: Alert?
+
+        private var formatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
+        var body: some View {
+            Form {
+                Section(header: Text("Add calibration")) {
+                    HStack {
+                        Text("Meter glucose")
+                        Spacer()
+                        DecimalTextField("0", value: $state.calibration, formatter: formatter, autofocus: false, cleanInput: true)
+                        Text(state.units.rawValue).foregroundColor(.secondary)
+                    }
+                    Button {
+                        state.addCalibration()
+                    }
+                    label: { Text("Add") }
+                        .disabled(state.calibration <= 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.calibrationsCount == 0)
+
+                    Button {
+                        state.removeAll()
+                    }
+                    label: { Text("Remove All") }
+                        .disabled(state.calibrationsCount == 0)
+                }
+            }
+            .popover(isPresented: $isPromtPresented) {
+                Form {}
+            }
+            .onAppear(perform: configureView)
+            .navigationTitle("Calibrations")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

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

@@ -83,6 +83,8 @@ extension Settings {
                         Group {
                             Text("Target presets")
                                 .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.tempTargetsPresets), from: self)
+                            Text("Calibrations")
+                                .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.calibrations), from: self)
                             Text("Middleware")
                                 .navigationLink(to: .configEditor(file: OpenAPS.Middleware.determineBasal), from: self)
                         }

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

@@ -22,6 +22,7 @@ enum Screen: Identifiable, Hashable {
     case dataTable
     case cgm
     case libreConfig
+    case calibrations
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -69,6 +70,8 @@ extension Screen {
             CGM.RootView(resolver: resolver)
         case .libreConfig:
             LibreConfig.RootView(resolver: resolver)
+        case .calibrations:
+            Calibrations.RootView(resolver: resolver)
         }
     }