| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- /// 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
- import LoopKitUI
- // MARK: - Glucose simulator
- final class GlucoseSimulatorSource: GlucoseSource {
- var cgmManager: CGMManagerUI?
- var glucoseManager: FetchGlucoseManager?
- 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(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
- guard canGenerateNewValues else {
- return Just([]).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).eraseToAnyPublisher()
- }
- func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
- fetch(nil)
- }
- }
- // 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
- let startup = Date()
- 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: currentGlucose,
- direction: BloodGlucose.Direction(rawValue: trandsStepDirection),
- date: Decimal(Int(date.timeIntervalSince1970) * 1000),
- dateString: date,
- unfiltered: Decimal(currentGlucose),
- filtered: nil,
- noise: nil,
- glucose: currentGlucose,
- type: nil,
- activationDate: startup,
- sessionStartDate: startup,
- transmitterID: "SIMULATOR"
- )
- 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: Int(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, 2.0].randomElement()!)
- trendStepsLeft -= 1
- if trendStepsLeft == 0 {
- generateNewTrend()
- }
- }
- func sourceInfo() -> [String: Any]? {
- [GlucoseSourceKey.description.rawValue: "Glucose simulator"]
- }
- }
|