Forráskód Böngészése

Add Glucose Simulator (#125)

* Added blood glucose simulator

* Update GlucoseSimulatorSource.swift

* Update project.pbxproj

* Update Info.plist
Vasiliy Usov 4 éve
szülő
commit
35a6154d28

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -243,6 +243,7 @@
 		E00EEC0627368630002FF094 /* UIAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0027368630002FF094 /* UIAssembly.swift */; };
 		E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0127368630002FF094 /* APSAssembly.swift */; };
 		E00EEC0827368630002FF094 /* NetworkAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0227368630002FF094 /* NetworkAssembly.swift */; };
+		E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E013D871273AC6FE0014109C /* GlucoseSimulatorSource.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 */; };
@@ -561,6 +562,7 @@
 		E00EEC0027368630002FF094 /* UIAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAssembly.swift; sourceTree = "<group>"; };
 		E00EEC0127368630002FF094 /* APSAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSAssembly.swift; sourceTree = "<group>"; };
 		E00EEC0227368630002FF094 /* NetworkAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkAssembly.swift; sourceTree = "<group>"; };
+		E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSimulatorSource.swift; sourceTree = "<group>"; };
 		E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigProvider.swift; sourceTree = "<group>"; };
 		E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigStateModel.swift; sourceTree = "<group>"; };
 		E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetRootView.swift; sourceTree = "<group>"; };
@@ -935,6 +937,7 @@
 				38569344270B5DFA0002C50D /* CGMType.swift */,
 				386A124E271707F000DDC61C /* DexcomSource.swift */,
 				38569345270B5DFA0002C50D /* GlucoseSource.swift */,
+				E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */,
 				38FEF407273B011A00574A46 /* LibreTransmitterSource.swift */,
 				3862CC03273D150600BF832C /* Calibrations */,
 			);
@@ -1700,6 +1703,7 @@
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
+				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,

+ 5 - 0
FreeAPS/Sources/APS/CGM/CGMType.swift

@@ -7,6 +7,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
     case xdrip
     case dexcomG6
     case dexcomG5
+    case simulator
     case libreTransmitter
 
     var displayName: String {
@@ -19,6 +20,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return "Dexcom G6"
         case .dexcomG5:
             return "Dexcom G5"
+        case .simulator:
+            return "Glucose Simulator"
         case .libreTransmitter:
             return NSLocalizedString("Libre Transmitter", comment: "Libre Transmitter type")
         }
@@ -34,6 +37,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return URL(string: "dexcomg6://")!
         case .dexcomG5:
             return URL(string: "dexcomgcgm://")!
+        case .simulator:
+            return nil
         case .libreTransmitter:
             return URL(string: "freeaps-x://libre-transmitter")!
         }

+ 180 - 0
FreeAPS/Sources/APS/CGM/GlucoseSimulatorSource.swift

@@ -0,0 +1,180 @@
+/// Glucose source - Blood Glucose Simulator
+///
+/// Source publish fake data about glucose's level, creates ascending and descending trends
+///
+/// Enter point of Source is GlucoseSimulatorSource.fetch method. Method is called from FetchGlucoseManager module.
+/// Not more often than a specified period (default - 300 seconds), it returns a Combine-publisher that publishes data on glucose values (global type BloodGlucose). If there is no up-to-date data (or the publication period has not passed yet), then a publisher of type Empty is returned, otherwise it returns a publisher of type Just.
+///
+/// Simulator composition
+/// ===================
+///
+/// class GlucoseSimulatorSource - main class
+/// protocol BloodGlucoseGenerator
+///  - IntelligentGenerator: BloodGlucoseGenerator
+
+// TODO: Every itteration trend make two steps, but must only one
+
+// TODO: Trend's value sticks to max and min Glucose value (in Glucose Generator)
+
+// TODO: Add reaction to insulin
+
+// TODO: Add probability to set trend's target value. Middle values must have more probability, than max and min.
+
+import Combine
+import Foundation
+
+// MARK: - Glucose simulator
+
+final class GlucoseSimulatorSource: GlucoseSource {
+    private enum Config {
+        // min time period to publish data
+        static let workInterval: TimeInterval = 300
+        // default BloodGlucose item at first run
+        // 288 = 1 day * 24 hours * 60 minites * 60 seconds / workInterval
+        static let defaultBGItems = 288
+    }
+
+    @Persisted(key: "GlucoseSimulatorLastGlucose") private var lastGlucose = 100
+
+    @Persisted(key: "GlucoseSimulatorLastFetchDate") private var lastFetchDate: Date! = nil
+
+    init() {
+        if lastFetchDate == nil {
+            var lastDate = Date()
+            for _ in 1 ... Config.defaultBGItems {
+                lastDate = lastDate.addingTimeInterval(-Config.workInterval)
+            }
+            lastFetchDate = lastDate
+        }
+    }
+
+    private lazy var generator: BloodGlucoseGenerator = {
+        IntelligentGenerator(
+            currentGlucose: lastGlucose
+        )
+    }()
+
+    private var canGenerateNewValues: Bool {
+        guard let lastDate = lastFetchDate else { return true }
+        if Calendar.current.dateComponents([.second], from: lastDate, to: Date()).second! >= Int(Config.workInterval) {
+            return true
+        } else {
+            return false
+        }
+    }
+
+    func fetch() -> AnyPublisher<[BloodGlucose], Never> {
+        guard canGenerateNewValues else {
+            return Empty().eraseToAnyPublisher()
+        }
+
+        let glucoses = generator.getBloodGlucoses(
+            startDate: lastFetchDate,
+            finishDate: Date(),
+            withInterval: Config.workInterval
+        )
+
+        if let lastItem = glucoses.last {
+            lastGlucose = lastItem.glucose!
+            lastFetchDate = Date()
+        }
+
+        return Just(glucoses.reversed()).eraseToAnyPublisher()
+    }
+}
+
+// MARK: - Glucose generator
+
+protocol BloodGlucoseGenerator {
+    func getBloodGlucoses(startDate: Date, finishDate: Date, withInterval: TimeInterval) -> [BloodGlucose]
+}
+
+class IntelligentGenerator: BloodGlucoseGenerator {
+    private enum Config {
+        // max and min glucose of trend's target
+        static let maxGlucose = 320
+        static let minGlucose = 45
+    }
+
+    // target glucose of trend
+    @Persisted(key: "GlucoseSimulatorTargetValue") private var trendTargetValue = 100
+    // how many steps left in current trend
+    @Persisted(key: "GlucoseSimulatorTargetSteps") private var trendStepsLeft = 1
+    // direction of last step
+    @Persisted(key: "GlucoseSimulatorDirection") private var trandsStepDirection = BloodGlucose.Direction.flat.rawValue
+    var currentGlucose: Int
+
+    init(currentGlucose: Int) {
+        self.currentGlucose = currentGlucose
+    }
+
+    func getBloodGlucoses(startDate: Date, finishDate: Date, withInterval interval: TimeInterval) -> [BloodGlucose] {
+        var result = [BloodGlucose]()
+
+        var _currentDate = startDate
+        while _currentDate <= finishDate {
+            result.append(getNextBloodGlucose(forDate: _currentDate))
+            _currentDate = _currentDate.addingTimeInterval(interval)
+        }
+
+        return result
+    }
+
+    // get next glucose's value in current trend
+    private func getNextBloodGlucose(forDate date: Date) -> BloodGlucose {
+        let previousGlucose = currentGlucose
+        makeStepInTrend()
+        trandsStepDirection = getDirection(fromGlucose: previousGlucose, toGlucose: currentGlucose).rawValue
+        let glucose = BloodGlucose(
+            _id: UUID().uuidString,
+            sgv: nil,
+            direction: BloodGlucose.Direction(rawValue: trandsStepDirection),
+            date: Decimal(Int(date.timeIntervalSince1970) * 1000),
+            dateString: date,
+            unfiltered: nil,
+            filtered: nil,
+            noise: nil,
+            glucose: currentGlucose,
+            type: nil
+        )
+        return glucose
+    }
+
+    private func setNewRandomTarget() {
+        guard trendTargetValue > 0 else {
+            trendTargetValue = Array(80 ... 110).randomElement()!
+            return
+        }
+        let difference = (Array(-50 ... -20) + Array(20 ... 50)).randomElement()!
+        let _value = trendTargetValue + difference
+        if _value <= Config.minGlucose {
+            trendTargetValue = Config.minGlucose
+        } else if _value >= Config.maxGlucose {
+            trendTargetValue = Config.maxGlucose
+        } else {
+            trendTargetValue = _value
+        }
+    }
+
+    private func setNewRandomSteps() {
+        trendStepsLeft = Array(3 ... 8).randomElement()!
+    }
+
+    private func getDirection(fromGlucose from: Int, toGlucose to: Int) -> BloodGlucose.Direction {
+        BloodGlucose.Direction(trend: to - from)
+    }
+
+    private func generateNewTrend() {
+        setNewRandomTarget()
+        setNewRandomSteps()
+    }
+
+    private func makeStepInTrend() {
+        currentGlucose +=
+            Int(Double((trendTargetValue - currentGlucose) / trendStepsLeft) * [0.3, 0.6, 1, 1.3, 1.6].randomElement()!)
+        trendStepsLeft -= 1
+        if trendStepsLeft == 0 {
+            generateNewTrend()
+        }
+    }
+}

+ 3 - 0
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -18,6 +18,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
     private lazy var appGroupSource = AppGroupSource()
     private lazy var dexcomSource = DexcomSource()
+    private lazy var simulatorSource = GlucoseSimulatorSource()
 
     init(resolver: Resolver) {
         injectServices(resolver)
@@ -36,6 +37,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             glucoseSource = dexcomSource
         case .nightscout:
             glucoseSource = nightscoutManager
+        case .simulator:
+            glucoseSource = simulatorSource
         case .libreTransmitter:
             glucoseSource = libreTransmitter
         }