Skip to content

Commit

Permalink
add calibration of Libre CGM
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
mountrcg committed May 19, 2024
1 parent 259bae1 commit 61aa30c
Show file tree
Hide file tree
Showing 13 changed files with 535 additions and 5 deletions.
52 changes: 52 additions & 0 deletions FreeAPS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,13 @@
CEB434FE28B90B8C00B70274 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = CEB434FC28B90B7C00B70274 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
CECCB4262BDBDCF7006E41C4 /* carbPresetResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = CECCB4252BDBDCF7006E41C4 /* carbPresetResult.swift */; };
CECCB4222BDB85BC006E41C4 /* ListCarbsPresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CECCB4212BDB85BC006E41C4 /* ListCarbsPresetIntent.swift */; };
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 */; };
Expand Down Expand Up @@ -960,6 +967,13 @@
CEC751D729D88262006E9D24 /* MinimedKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MinimedKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CECCB4252BDBDCF7006E41C4 /* carbPresetResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = carbPresetResult.swift; sourceTree = "<group>"; };
CECCB4212BDB85BC006E41C4 /* ListCarbsPresetIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCarbsPresetIntent.swift; sourceTree = "<group>"; };
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>"; };
Expand Down Expand Up @@ -1283,6 +1297,7 @@
F2159A472BA60A0300A0B716 /* ContactTrick */,
195D80B22AF696EE00D25097 /* Dynamic */,
BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */,
CEE9A64D2BBB411C00EB5194 /* Calibrations */,
190EBCC229FF134900BA767D /* StatConfig */,
19F95FF129F10F9C00314DDC /* Stat */,
CE94597C29E9E1CD0047C9C6 /* WatchConfig */,
Expand Down Expand Up @@ -1619,6 +1634,7 @@
3856933F270B57A00002C50D /* CGM */ = {
isa = PBXGroup;
children = (
CEE9A65A2BBB41AD00EB5194 /* Calibrations */,
F816825F28DB441800054060 /* BluetoothTransmitter.swift */,
F816825D28DB441200054060 /* HeartBeatManager.swift */,
38569346270B5DFB0002C50D /* AppGroupSource.swift */,
Expand Down Expand Up @@ -1974,6 +1990,7 @@
38FCF3F125E9028E0078B0D1 /* Info.plist */,
38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
);
path = FreeAPSTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -2395,6 +2412,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 = (
Expand Down Expand Up @@ -2883,6 +2928,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 */,
Expand All @@ -2903,6 +2949,7 @@
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 */,
Expand All @@ -2911,8 +2958,10 @@
CECCB4262BDBDCF7006E41C4 /* carbPresetResult.swift in Sources */,
F2159A542BA6207F00A0B716 /* ContactTrickEntry.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 */,
Expand Down Expand Up @@ -2966,6 +3015,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 */,
49CA5A182BDA385E001F0D3A /* KetoProtectConfRootView.swift in Sources */,
38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
Expand Down Expand Up @@ -3103,6 +3153,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 */,
495068AA2BDFEF1D0048FF3B /* CarbPresetIntentRequest.swift in Sources */,
Expand Down Expand Up @@ -3231,6 +3282,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */,
38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
);
Expand Down
119 changes: 119 additions & 0 deletions FreeAPS/Sources/APS/CGM/Calibrations/CalibrationService.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
9 changes: 8 additions & 1 deletion FreeAPS/Sources/APS/CGM/PluginSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,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? {
Expand All @@ -126,6 +132,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
Expand Down
37 changes: 33 additions & 4 deletions FreeAPS/Sources/APS/FetchGlucoseManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -35,6 +36,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
@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)
Expand Down Expand Up @@ -68,6 +70,10 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {

var glucoseSource: GlucoseSource!

func removeCalibrations() {
calibrationService.removeAllCalibrations()
}

func deleteGlucoseSource() {
cgmManager = nil
updateGlucoseSource(
Expand All @@ -77,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

Expand All @@ -88,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,
Expand Down Expand Up @@ -154,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] = []

Expand Down Expand Up @@ -260,6 +272,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 {
Expand Down
1 change: 1 addition & 0 deletions FreeAPS/Sources/Assemblies/APSAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
}
6 changes: 6 additions & 0 deletions FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,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")) {
Toggle("Create Events in Calendar", isOn: $state.createCalendarEvents)
if state.calendarIDs.isNotEmpty {
Expand Down
Loading

0 comments on commit 61aa30c

Please sign in to comment.