diff --git a/xdrip.xcodeproj/project.pbxproj b/xdrip.xcodeproj/project.pbxproj index 77459b540..29159aced 100644 --- a/xdrip.xcodeproj/project.pbxproj +++ b/xdrip.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 470CE1FC246802EB00D5CB74 /* BluetoothPeripheralsView.strings in Resources */ = {isa = PBXBuildFile; fileRef = 470CE1FE246802EB00D5CB74 /* BluetoothPeripheralsView.strings */; }; 470F021326DD515300C5D626 /* SettingsViewSensorCountdownSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470F021226DD515300C5D626 /* SettingsViewSensorCountdownSettingsViewModel.swift */; }; 47150A4027F6211C00DB2994 /* SettingsViewTreatmentsSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47150A3F27F6211C00DB2994 /* SettingsViewTreatmentsSettingsViewModel.swift */; }; + 472196EB2868BC5C007B2908 /* ConstantsCalibrationAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472196EA2868BC5C007B2908 /* ConstantsCalibrationAssistant.swift */; }; 4749EB9B25B36E010072DF8B /* LibreNFC.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4749EB9D25B36E010072DF8B /* LibreNFC.strings */; }; 47503382247420A200D2260B /* BluetoothPeripheralView.strings in Resources */ = {isa = PBXBuildFile; fileRef = 47503384247420A200D2260B /* BluetoothPeripheralView.strings */; }; 4752B400263570DA0081D551 /* ConstantsStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4752B3FF263570DA0081D551 /* ConstantsStatistics.swift */; }; @@ -711,6 +712,7 @@ 470CE1FF246802F400D5CB74 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BluetoothPeripheralsView.strings; sourceTree = ""; }; 470F021226DD515300C5D626 /* SettingsViewSensorCountdownSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewSensorCountdownSettingsViewModel.swift; sourceTree = ""; }; 47150A3F27F6211C00DB2994 /* SettingsViewTreatmentsSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTreatmentsSettingsViewModel.swift; sourceTree = ""; }; + 472196EA2868BC5C007B2908 /* ConstantsCalibrationAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsCalibrationAssistant.swift; sourceTree = ""; }; 4749EB9C25B36E010072DF8B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/LibreNFC.strings; sourceTree = ""; }; 47503383247420A200D2260B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BluetoothPeripheralView.strings; sourceTree = ""; }; 4752B3FF263570DA0081D551 /* ConstantsStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsStatistics.swift; sourceTree = ""; }; @@ -2760,6 +2762,7 @@ F8AF36142455C6F700B5977B /* ConstantsTrace.swift */, F8E3A2AA23DA520B00E5E98A /* ConstantsWatch.swift */, 4752B3FF263570DA0081D551 /* ConstantsStatistics.swift */, + 472196EA2868BC5C007B2908 /* ConstantsCalibrationAssistant.swift */, ); name = Constants; path = xdrip/Constants; @@ -3760,6 +3763,7 @@ F85FB769255DE14600D1C39E /* ConstantsLibreSmoothing.swift in Sources */, F8F1671B272B3E4F001AA3D8 /* DexcomBackfillStream.swift in Sources */, F8DF766023E38FC100063910 /* BLEPeripheral+CoreDataClass.swift in Sources */, + 472196EB2868BC5C007B2908 /* ConstantsCalibrationAssistant.swift in Sources */, F80ED2EE236F68F90005C035 /* SettingsViewM5StackWiFiSettingsViewModel.swift in Sources */, F8FDD6CB2553385000625B49 /* Array.swift in Sources */, F8A389C823203E3E0010F405 /* ConstantsM5Stack.swift in Sources */, @@ -4879,7 +4883,7 @@ CODE_SIGN_ENTITLEMENTS = "$(XDRIP_ENTITLEMENTS_DEBUG)"; CODE_SIGN_IDENTITY = "$(XDRIP_CODE_SIGN_IDENTITY_DEBUG)"; CODE_SIGN_STYLE = "$(XDRIP_CODE_SIGN_STYLE)"; - DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = 92C77A2942; INFOPLIST_FILE = "$(SRCROOT)/xdrip/Supporting Files/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/xdrip/Constants/ConstantsCalibrationAssistant.swift b/xdrip/Constants/ConstantsCalibrationAssistant.swift new file mode 100644 index 000000000..4fca59ba4 --- /dev/null +++ b/xdrip/Constants/ConstantsCalibrationAssistant.swift @@ -0,0 +1,72 @@ +// +// ConstantsCalibrationAssistant.swift +// xdrip +// +// Created by Paul Plant on 26/6/22. +// Copyright © 2022 Johan Degraeve. All rights reserved. +// + +import Foundation + +/// constants used by the calibration assistant +enum ConstantsCalibrationAssistant { + + /// the number of minutes of readings that we should use for the calibration assistant calculations + static let minutesToUseForCalculations: Double = 20 + + // Delta + /// the value over which we will consider that the delta change is significant to display as a concern to the user + static let deltaResultLimit: Double = 90 + + /// the weighting that will be applied to the delta change value to push the result up + static let deltaMultiplier: Double = 30 + + + // Standard Deviation + /// the value over which we will consider that the variation in change is significant to display as a concern to the user + static let stdDeviationResultLimit: Double = 60 + + /// the weighting that will be applied to the standard deviation value to push the result up + static let stdDeviationMultiplier: Double = 50 + + + // Very high BG levels + /// the upper higher BG level at which the user should never calibrate. A bigger multiplier will be used for values over this amount + static let higherBgUpperLimit: Double = 160 + + /// the weighting that will be applied to very high BG values to push the result up + static let higherBgUpperMultiplier: Double = 15 + + + // Higher BG levels + /// the higher BG level at which the user should be careful when calibrating. A smaller multiplier will be applied to values over this amount + static let higherBgRecommendedLimit: Double = 130 + + /// the weighting that will be applied to moderately high BG values to push the result up + static let higherBgRecommendedMultiplier: Double = 10 + + + // Lower BG levels + /// the lower BG level at which the user should be careful when calibrating. A smaller multiplier will be applied to values below this amount + static let lowerBgRecommendedLimit: Double = 90 + + /// the weight that will be applied to moderately low BG values to push the result up + static let lowerBgRecommendedMultiplier: Double = 10 + + + // Very low BG levels + /// the lower BG level at which the user should never calibrate. A bigger multiplier will be used for values under this amount + static let lowerBgLowerLimit: Double = 75 + + /// the weighting that will be applied to very low BG values to push the result up + static let lowerBgLowerMultiplier: Double = 15 + + + // Limits + /// the limit below which the calibration result will be considered as OK + static let okToCalibrateLimit: Double = 130 + + /// the limit below which (and above okToCalibrateLimit) when the calibration result will be considered as "Not Ideal" and the user will be warned to be careful and calibrate later + static let notIdealToCalibrateLimit: Double = 200 + +} diff --git a/xdrip/Extensions/UserDefaults.swift b/xdrip/Extensions/UserDefaults.swift index 9abbed0d0..d36101425 100644 --- a/xdrip/Extensions/UserDefaults.swift +++ b/xdrip/Extensions/UserDefaults.swift @@ -50,6 +50,8 @@ extension UserDefaults { case miniChartHoursToShow = "miniChartHoursToShow" /// should the screen/chart be allowed to rotate? case allowScreenRotation = "allowScreenRotation" + /// should the calibration assistant be enabled? + case showVisualCalibrationAssistant = "showVisualCalibrationAssistant" /// should the clock view be shown when the screen is locked? case showClockWhenScreenIsLocked = "showClockWhenScreenIsLocked" /// show the objectives and make them display on the graph? Or just hide it all because it's too complicated to waste time with? @@ -341,6 +343,9 @@ extension UserDefaults { case loopDelayValueInMinutes = "loopDelayValueInMinutes" + /// Default value is false. If true then the calibration assistant will show the results to the user + case showCalibrationAssistantResults = "showCalibrationAssistantResults" + /// used for Libre data parsing - only for Libre 1 or Libre 2 read via transmitter, ie full NFC block case previousRawLibreValues = "previousRawLibreValues" @@ -852,6 +857,17 @@ extension UserDefaults { } } + /// should the calibration assistant be enabled? + @objc dynamic var showVisualCalibrationAssistant: Bool { + // default value for bool in userdefaults is false, as default we want the calibration assistant to be enabled + get { + return !bool(forKey: Key.showVisualCalibrationAssistant.rawValue) + } + set { + set(!newValue, forKey: Key.showVisualCalibrationAssistant.rawValue) + } + } + // MARK: Treatments Settings @@ -1724,6 +1740,16 @@ extension UserDefaults { set(newValue, forKey: Key.suppressLoopShare.rawValue) } } + + /// if true, then the calibration assistant will show all calculations and values. Set to false by default + var showCalibrationAssistantResults: Bool { + get { + return bool(forKey: Key.showCalibrationAssistantResults.rawValue) + } + set { + set(newValue, forKey: Key.showCalibrationAssistantResults.rawValue) + } + } /// used for Libre data parsing - for processing in LibreDataParser which is only in case of reading with NFC (ie bubble etc) var previousRawLibreValues: [Double] { diff --git a/xdrip/Storyboards/Base.lproj/Main.storyboard b/xdrip/Storyboards/Base.lproj/Main.storyboard index 9335be34e..e46f68dda 100644 --- a/xdrip/Storyboards/Base.lproj/Main.storyboard +++ b/xdrip/Storyboards/Base.lproj/Main.storyboard @@ -397,32 +397,32 @@ - + - + - + - + - + diff --git a/xdrip/Storyboards/en.lproj/CalibrationRequest.strings b/xdrip/Storyboards/en.lproj/CalibrationRequest.strings index 55fbd6673..406b6a6a1 100644 --- a/xdrip/Storyboards/en.lproj/CalibrationRequest.strings +++ b/xdrip/Storyboards/en.lproj/CalibrationRequest.strings @@ -2,3 +2,17 @@ "calibration_title" = "Calibration"; "calibration_notification_title" = "Calibration"; "calibration_notification_body" = "Click the Notification to Calibrate"; + +// Calibration Assistant strings +"calibrateButtonTitle" = "Calibrate"; +"calibrateAnywayButtonTitle" = "Calibrate Anyway!"; +"okToCalibrate" = "Conditions are OK to calibrate"; +"waitToCalibrate" = "You may calibrate, but it would be better to wait"; +"doNotCalibrate" = "You should not calibrate now. Wait for a better time"; +"bgValuesRising" = "BG values have been rising"; +"bgValuesDropping" = "BG values have been dropping"; +"bgValuesNotStable" = "BG values are not stable enough"; +"bgValueTooHigh" = "Current BG value is too high"; +"bgValuesSlightlyHigh" = "Current BG value is slightly high"; +"bgValueTooLow" = "Current BG value is too low"; +"bgValuesSlightlyLow" = "Current BG value is slightly low"; diff --git a/xdrip/Storyboards/en.lproj/SettingsViews.strings b/xdrip/Storyboards/en.lproj/SettingsViews.strings index bb33157ab..ed9cbbde0 100644 --- a/xdrip/Storyboards/en.lproj/SettingsViews.strings +++ b/xdrip/Storyboards/en.lproj/SettingsViews.strings @@ -16,6 +16,7 @@ "settingsviews_IntervalMessage" = "Minimum interval between two notifications (mins)"; "settingsviews_allowScreenRotation" = "Allow Chart Rotation?"; "settingsviews_showMiniChart" = "Show the Mini-Chart?"; +"settingsviews_showVisualCalibrationAssistant" = "Show Calibration Assistant?"; "settingsviews_showClockWhenScreenIsLocked" = "Show Clock when Locked?"; "settingsviews_urgentHighValue" = "Urgent High Value:"; "settingsviews_highValue" = "High Value:"; @@ -132,3 +133,4 @@ "expanatoryTextSelectTime" = "As of what time should the value apply"; "expanatoryTextSelectValue" = "Delay in minutes, applied to readings shared with Loop"; "warningLoopDelayAlreadyExists" = "There is already a loopDelay for this time."; +"showCalibrationAssistantResults" = "Calibration Assistant Debug"; diff --git a/xdrip/Storyboards/es.lproj/CalibrationRequest.strings b/xdrip/Storyboards/es.lproj/CalibrationRequest.strings index c9bd89a72..77793547d 100644 --- a/xdrip/Storyboards/es.lproj/CalibrationRequest.strings +++ b/xdrip/Storyboards/es.lproj/CalibrationRequest.strings @@ -2,3 +2,17 @@ "calibration_title" = "Calibración"; "calibration_notification_title" = "Calibración"; "calibration_notification_body" = "Haz click en la notificación para calibrar"; + +// Calibration Assistant strings +"calibrateButtonTitle" = "Calibrar"; +"calibrateAnywayButtonTitle" = "¡Calibrar Igualmente!"; +"okToCalibrate" = "Las condiciones son aptas para calibrar"; +"waitToCalibrate" = "Se puede calibrar ahora, pero sería mejor esperar"; +"doNotCalibrate" = "No se debe calibrar ahora mismo"; +"bgValuesRising" = "Los valores de glucosa han estado subiendo"; +"bgValuesDropping" = "Los valores de glucosa han estado bajando"; +"bgValuesNotStable" = "Los valores de glucosa son poco estables"; +"bgValueTooHigh" = "La glucosa actual es demasiado alto"; +"bgValuesSlightlyHigh" = "La glucosa actual es ligeramente alto"; +"bgValueTooLow" = "La glucosa actual es demasiado bajo"; +"bgValuesSlightlyLow" = "La glucosa actual es ligeramente bajo"; diff --git a/xdrip/Storyboards/es.lproj/SettingsViews.strings b/xdrip/Storyboards/es.lproj/SettingsViews.strings index 4ca73bd6f..dc52a904f 100644 --- a/xdrip/Storyboards/es.lproj/SettingsViews.strings +++ b/xdrip/Storyboards/es.lproj/SettingsViews.strings @@ -141,3 +141,6 @@ "settingsviews_housekeeperRetentionPeriod" = "Periodo de Retención (días):"; "settingsviews_housekeeperExportAllData" = "Exportar Todo"; "settingsviews_housekeeperRetentionPeriodMessage" = "¿Durante cuántos días debemos guardar los datos? (Min 90, Max 365)\n\n(Recomendado: 90 días)"; + +"settingsviews_showVisualCalibrationAssistant" = "Asistente de Calibración?"; +"showCalibrationAssistantResults" = "Calibration Assistant Debug"; diff --git a/xdrip/Texts/TextsCalibration.swift b/xdrip/Texts/TextsCalibration.swift index 5d4b6a3b5..69a4005ba 100644 --- a/xdrip/Texts/TextsCalibration.swift +++ b/xdrip/Texts/TextsCalibration.swift @@ -16,6 +16,55 @@ enum Texts_Calibrations { return NSLocalizedString("enter_calibration_value", tableName: filename, bundle: Bundle.main, value: "Enter Calibration Value", comment: "When calibration alert goess off, user clicks the notification, app opens, dialog pops up, this is the text in the dialog") }() + static let calibrateButtonTitle:String = { + return NSLocalizedString("calibrateButtonTitle", tableName: filename, bundle: Bundle.main, value: "Calibrate", comment: "the calibrate button title") + }() + + static let calibrateAnywayButtonTitle:String = { + return NSLocalizedString("calibrateAnywayButtonTitle", tableName: filename, bundle: Bundle.main, value: "Calibrate Anyway!", comment: "the calibrate anyway button title") + }() + + static let okToCalibrate:String = { + return NSLocalizedString("okToCalibrate", tableName: filename, bundle: Bundle.main, value: "Conditions are OK to calibrate", comment: "a message to inform that the BG values are OK to calibrate") + }() + + static let waitToCalibrate:String = { + return NSLocalizedString("waitToCalibrate", tableName: filename, bundle: Bundle.main, value: "You may calibrate, but it would be better to wait", comment: "a message to inform that the user whould wait before calibrate") + }() + + static let doNotCalibrate:String = { + return NSLocalizedString("doNotCalibrate", tableName: filename, bundle: Bundle.main, value: "You should not calibrate now. Wait for a better time", comment: "a message to inform that the user should not calibrate") + }() + + static let bgValuesRising:String = { + return NSLocalizedString("bgValuesRising", tableName: filename, bundle: Bundle.main, value: "BG values have been rising", comment: "a message to inform that the BG values have been rising too much to calibrate") + }() + + static let bgValuesDropping:String = { + return NSLocalizedString("bgValuesDropping", tableName: filename, bundle: Bundle.main, value: "BG values have been dropping", comment: "a message to inform that the BG values have been dropping too much to calibrate") + }() + + static let bgValuesNotStable:String = { + return NSLocalizedString("bgValuesNotStable", tableName: filename, bundle: Bundle.main, value: "BG values are not stable enough", comment: "a message to inform that the BG values are not stable enough to calibrate") + }() + + static let bgValueTooHigh:String = { + return NSLocalizedString("bgValueTooHigh", tableName: filename, bundle: Bundle.main, value: "Current BG value is too high", comment: "a message to inform that the current BG value is too high to calibrate") + }() + + static let bgValuesSlightlyHigh:String = { + return NSLocalizedString("bgValuesSlightlyHigh", tableName: filename, bundle: Bundle.main, value: "Current BG value is slightly high", comment: "a message to inform that the current BG value is slightly high to calibrate") + }() + + static let bgValueTooLow:String = { + return NSLocalizedString("bgValueTooLow", tableName: filename, bundle: Bundle.main, value: "Current BG value is too low", comment: "a message to inform that the current BG value is too low to calibrate") + }() + + static let bgValuesSlightlyLow:String = { + return NSLocalizedString("bgValuesSlightlyLow", tableName: filename, bundle: Bundle.main, value: "Current BG value is slightly low", comment: "a message to inform that the current BG value is slightly low to calibrate") + }() + + } diff --git a/xdrip/Texts/TextsSettingsView.swift b/xdrip/Texts/TextsSettingsView.swift index 32cef958f..16bea31b8 100644 --- a/xdrip/Texts/TextsSettingsView.swift +++ b/xdrip/Texts/TextsSettingsView.swift @@ -95,6 +95,10 @@ class Texts_SettingsView { static let showMiniChart: String = { return NSLocalizedString("settingsviews_showMiniChart", tableName: filename, bundle: Bundle.main, value: "Show the Mini-Chart?", comment: "home screen settings, should the mini-chart be shown?") }() + + static let showVisualCalibrationAssistant: String = { + return NSLocalizedString("settingsviews_showVisualCalibrationAssistant", tableName: filename, bundle: Bundle.main, value: "Show Calibration Assistant?", comment: "home screen settings, should the visual calibration assistant be enabled?") + }() static let labelUseObjectives: String = { return NSLocalizedString("settingsviews_useobjectives", tableName: filename, bundle: Bundle.main, value: "Show Objectives in Graph?", comment: "home screen settings, use objectives in graph") @@ -573,6 +577,10 @@ class Texts_SettingsView { return NSLocalizedString("warningLoopDelayAlreadyExists", tableName: filename, bundle: Bundle.main, value: "There is already a loopDelay for this time.", comment: "When user creates new loopdelay, with a timestamp that already exists - this is the warning text") }() + static let showCalibrationAssistantResults: String = { + return NSLocalizedString("showCalibrationAssistantResults", tableName: filename, bundle: Bundle.main, value: "Calibration Assistant Debug", comment: "When enabled, the results from the calibration assistant will be shown") + }() + static let nsLog: String = { return NSLocalizedString("nslog", tableName: filename, bundle: Bundle.main, value: "NSLog", comment: "deloper settings, row title for NSLog - with NSLog enabled, a developer can view log information as explained here https://github.com/JohanDegraeve/xdripswift/wiki/NSLog") }() diff --git a/xdrip/View Controllers/Root View Controller/RootViewController.swift b/xdrip/View Controllers/Root View Controller/RootViewController.swift index 471d63e46..bb6919868 100644 --- a/xdrip/View Controllers/Root View Controller/RootViewController.swift +++ b/xdrip/View Controllers/Root View Controller/RootViewController.swift @@ -523,6 +523,9 @@ final class RootViewController: UIViewController { // update statistics related outlets updateStatistics(animatePieChart: true, overrideApplicationState: true) + // update the visual clues to indicate if calibration is feasible + updateCalibrationAssistantStatus(calibrationAssistantResult: calculateCalibrationAssistantResults().calibrationAssistantResult) + } override func viewDidAppear(_ animated: Bool) { @@ -691,6 +694,9 @@ final class RootViewController: UIViewController { // update statistics related outlets self.updateStatistics(animatePieChart: true, overrideApplicationState: true) + // update the visual clues to indicate if calibration is feasible + self.updateCalibrationAssistantStatus(calibrationAssistantResult: self.calculateCalibrationAssistantResults().calibrationAssistantResult) + // create badge counter self.createBgReadingNotificationAndSetAppBadge(overrideShowReadingInNotification: true) @@ -734,6 +740,9 @@ final class RootViewController: UIViewController { // showing or hiding the mini-chart UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.showMiniChart.rawValue, options: .new, context: nil) + // has the visual calibration assistant been enabled or disabled? + UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.showVisualCalibrationAssistant.rawValue, options: .new, context: nil) + // see if the user has changed the statistic days to use UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.daysToUseStatistics.rawValue, options: .new, context: nil) @@ -1276,6 +1285,9 @@ final class RootViewController: UIViewController { // update sensor countdown graphic updateSensorCountdown() + // update visual calibration assistant status + updateCalibrationAssistantStatus(calibrationAssistantResult: calculateCalibrationAssistantResults().calibrationAssistantResult) + } nightScoutUploadManager?.uploadLatestBgReadings(lastConnectionStatusChangeTimeStamp: lastConnectionStatusChangeTimeStamp()) @@ -1340,9 +1352,6 @@ final class RootViewController: UIViewController { } - default: - break - } } @@ -1419,6 +1428,11 @@ final class RootViewController: UIViewController { // redraw mini-chart updateMiniChart() + + case UserDefaults.Key.showVisualCalibrationAssistant: + + // refresh the visual calibration assistant status + updateCalibrationAssistantStatus(calibrationAssistantResult: calculateCalibrationAssistantResults().calibrationAssistantResult) case UserDefaults.Key.daysToUseStatistics: @@ -1662,14 +1676,19 @@ final class RootViewController: UIViewController { // assign deviceName, needed in the closure when creating alert. As closures can create strong references (to bluetoothTransmitter in this case), I'm fetching the deviceName here let deviceName = bluetoothTransmitter.deviceName - let alert = UIAlertController(title: Texts_Calibrations.enterCalibrationValue, message: nil, keyboardType: UserDefaults.standard.bloodGlucoseUnitIsMgDl ? .numberPad:.decimalPad, text: nil, placeHolder: "...", actionTitle: nil, cancelTitle: nil, actionHandler: { - (text:String) in + // run the calibration assistant function and store the returned result and messages. + let calibrationAssistantResultAndAlertMessage = calculateCalibrationAssistantResults() + + let alert = UIAlertController(title: Texts_Calibrations.enterCalibrationValue, message: calibrationAssistantResultAndAlertMessage.calibrationAssistantAlertMessage, keyboardType: UserDefaults.standard.bloodGlucoseUnitIsMgDl ? .numberPad:.decimalPad, text: nil, placeHolder: "...", actionTitle: calibrationAssistantResultAndAlertMessage.calibrationAssistantResult <= ConstantsCalibrationAssistant.okToCalibrateLimit ? Texts_Calibrations.calibrateButtonTitle : Texts_Calibrations.calibrateAnywayButtonTitle, cancelTitle: nil, actionHandler: { (text:String) in guard let valueAsDouble = text.toDouble() else { self.present(UIAlertController(title: Texts_Common.warning, message: Texts_Common.invalidValue, actionHandler: nil), animated: true, completion: nil) return } + // store the calibration assistant trace message + trace("calibration : %{public}@", log: self.log, category: ConstantsLog.categoryRootView, type: .info, calibrationAssistantResultAndAlertMessage.calibrationAssistantTraceMessage) + // store the calibration value entered by the user into the log trace("calibration : value %{public}@ entered by user", log: self.log, category: ConstantsLog.categoryRootView, type: .info, text.description) @@ -2351,7 +2370,7 @@ final class RootViewController: UIViewController { calibrateToolbarButtonOutlet.enable() } else { sensorToolbarButtonOutlet.disable() - calibrateToolbarButtonOutlet.disable() +// calibrateToolbarButtonOutlet.disable() } } @@ -3040,6 +3059,269 @@ final class RootViewController: UIViewController { } + + /// Calibration Assistant calculation function. This will run every time a new glucose reading is processed in master mode or if the calibration toolbar button is pressed. + /// It will use the BG readings over the last "x" minutes (as defined in ConstantsCalibrationAssistant) and runs several checks/calculations. The value derived from each check will then by multiplied by a weighting factor and all results added to give a final confidence score + /// The calculations take into account the standard deviation and overall glucose delta together with checking if the current BG is within a value considering acceptable for calibrating (generally 100-130 mg/dl). Anything outside of this value is penalised and adds to the total score making it less likely to recommend calibrating. + /// If the score is slightly high, the user is warned that it is not ideal to calibrate and to please wait + /// If the score is very high, the user is warned not to calibrate + /// - parameters : + /// - enabled : when true this will force the screen to lock + /// - showClock : when false, this will enable a simple screen lock without changing the UI - useful for keeping the screen open on your desk + /// - returns: + /// - calibrationAssistantResult. Returns the calculated confidence score as a double + /// - calibrationAssistantAlertMessage. The main formatted message that will be displayed in the UI Alert + /// - calibrationAssistantTraceMessage. A compact string holding all the data about what the Calibration Assistant recommended to the user and the reasons why + private func calculateCalibrationAssistantResults() -> (calibrationAssistantResult: Double, calibrationAssistantAlertMessage: String, calibrationAssistantTraceMessage: String) { + + // private vars to return + var calibrationAssistantResult: Double = 0 + var calibrationAssistantAlertMessage: String = "" + var calibrationAssistantTraceMessage: String = "" + + // BG values to use in calculations + var actualBgValue: Double = 0 + var glucoseValues: [Double] = [] + + // calculated values + var stdDeviationValue: Double = 0 + var deltaValue: Double = 0 + var higherBgUpperValue: Double = 0 + var higherBgRecommendedValue: Double = 0 + var lowerBgRecommendedValue: Double = 0 + var lowerBgLowerValue: Double = 0 + + // results after applying multipliers to the calculated values + var stdDeviationResult: Double = 0 + var deltaResult: Double = 0 + var higherBgUpperResult: Double = 0 + var higherBgRecommendedResult: Double = 0 + var lowerBgRecommendedResult: Double = 0 + var lowerBgLowerResult: Double = 0 + var isFirstValue: Bool = true + + + // make sure that the necessary objects are initialised and readings are available. + if let bgReadingsAccessor = bgReadingsAccessor { + + // get the last "x" minutes of BG readings from coredata where "x" is defined in ConstantsCalibrationAssistant + let bgReadings = bgReadingsAccessor.getLatestBgReadings(limit: nil, fromDate: Date(timeIntervalSinceNow: -ConstantsCalibrationAssistant.minutesToUseForCalculations * 60), forSensor: nil, ignoreRawData: true, ignoreCalculatedValue: false) + + // if we successfully got BG readings, then pull out the calculated value and add it to a simple glucoseValues array + if bgReadings.count > 0 { + + isFirstValue = true + + for reading in bgReadings { + + let calculatedValue = reading.calculatedValue + + // only append the BG values if they are not zero or out of range. This is just to avoid strange errors in the calculations + if (calculatedValue != 0.0) && (calculatedValue >= ConstantsGlucoseChart.absoluteMinimumChartValueInMgdl) && (calculatedValue <= 450) { + + // set the actual BG value to the first BG reading in the returned array (which will be the last reading received) + if isFirstValue { + actualBgValue = calculatedValue + } + + glucoseValues.append(calculatedValue) + + isFirstValue = false + + } + } + } + + // assuming that there are glucose values stored in the array, we can start calculating + if glucoseValues.count > 0 { + + // calculate standard deviation + var sum: Double = 0 + + let averageGlucoseValue = Double(glucoseValues.reduce(0, +)) / Double(glucoseValues.count) + + for glucoseValue in glucoseValues { + sum += (glucoseValue - averageGlucoseValue) * (glucoseValue - averageGlucoseValue) + } + + stdDeviationValue = sqrt(sum / Double(glucoseValues.count)) + stdDeviationResult = stdDeviationValue * ConstantsCalibrationAssistant.stdDeviationMultiplier + + + // calculate delta change between the first and last BG value in the array + deltaValue = (glucoseValues.first ?? 0) - (glucoseValues.last ?? 0) + deltaResult = abs(deltaValue) * ConstantsCalibrationAssistant.deltaMultiplier + + + // calculate upper outer limit bg result + if actualBgValue > ConstantsCalibrationAssistant.higherBgUpperLimit { + higherBgUpperValue = ConstantsCalibrationAssistant.higherBgUpperLimit - actualBgValue + higherBgUpperResult = abs(higherBgUpperValue) * ConstantsCalibrationAssistant.higherBgUpperMultiplier + } + + + // calculate upper recommended limit bg result + if actualBgValue > ConstantsCalibrationAssistant.higherBgRecommendedLimit { + higherBgRecommendedValue = ConstantsCalibrationAssistant.higherBgRecommendedLimit - actualBgValue + higherBgRecommendedResult = abs(higherBgRecommendedValue) * ConstantsCalibrationAssistant.higherBgRecommendedMultiplier + } + + + // calculate lower recommended limit bg result + if actualBgValue < ConstantsCalibrationAssistant.lowerBgRecommendedLimit { + lowerBgRecommendedValue = ConstantsCalibrationAssistant.lowerBgRecommendedLimit - actualBgValue + lowerBgRecommendedResult = abs(lowerBgRecommendedValue) * ConstantsCalibrationAssistant.lowerBgRecommendedMultiplier + } + + // calculate lower outer limit bg result + if actualBgValue < ConstantsCalibrationAssistant.lowerBgLowerLimit { + lowerBgLowerValue = ConstantsCalibrationAssistant.lowerBgLowerLimit - actualBgValue + lowerBgLowerResult = abs(lowerBgLowerValue) * ConstantsCalibrationAssistant.lowerBgLowerMultiplier + } + + + // calculate the final result + calibrationAssistantResult = stdDeviationResult + deltaResult + higherBgUpperResult + higherBgRecommendedResult + lowerBgRecommendedResult + lowerBgLowerResult + + + // let's start to construct the alert message that should be shown to the user + if calibrationAssistantResult <= ConstantsCalibrationAssistant.okToCalibrateLimit { + calibrationAssistantAlertMessage += "\n✅ " + Texts_Calibrations.okToCalibrate + } else if calibrationAssistantResult < ConstantsCalibrationAssistant.notIdealToCalibrateLimit { + calibrationAssistantAlertMessage += "\n⚠️ " + Texts_Calibrations.waitToCalibrate + } else { + calibrationAssistantAlertMessage += "\n⛔️ " + Texts_Calibrations.doNotCalibrate + } + + + // if the result is over the ok limit, then we must give further explanations to the user. + if calibrationAssistantResult > ConstantsCalibrationAssistant.okToCalibrateLimit { + + if deltaResult > ConstantsCalibrationAssistant.deltaResultLimit { + + // check if the delta is positive (rising) or negative (dropping) + if deltaValue > 0 { + calibrationAssistantAlertMessage += "\n\n📈 " + Texts_Calibrations.bgValuesRising + } else { + calibrationAssistantAlertMessage += "\n\n📉 " + Texts_Calibrations.bgValuesDropping + } + + } + + if stdDeviationResult > ConstantsCalibrationAssistant.stdDeviationResultLimit { + calibrationAssistantAlertMessage += "\n\n↕️ " + Texts_Calibrations.bgValuesNotStable + } + + if actualBgValue > ConstantsCalibrationAssistant.higherBgUpperLimit { + calibrationAssistantAlertMessage += "\n\n⏫ " + Texts_Calibrations.bgValueTooHigh + } else if actualBgValue > ConstantsCalibrationAssistant.higherBgRecommendedLimit { + calibrationAssistantAlertMessage += "\n\n⬆️ " + Texts_Calibrations.bgValuesSlightlyHigh + } else if actualBgValue < ConstantsCalibrationAssistant.lowerBgLowerLimit { + calibrationAssistantAlertMessage += "\n\n⏬ " + Texts_Calibrations.bgValueTooLow + } else if actualBgValue < ConstantsCalibrationAssistant.lowerBgRecommendedLimit { + calibrationAssistantAlertMessage += "\n\n⬇️ " + Texts_Calibrations.bgValuesSlightlyLow + } + + } + + + // if the user has enabled this in developer settings, then append the calculations values and results to the message. This will probably cause the alert message to be too big, but is easily scrolled and not meant for general use. + if UserDefaults.standard.showCalibrationAssistantResults { + + calibrationAssistantAlertMessage += "\n\n*** DEBUG DATA ***" + + calibrationAssistantAlertMessage += "\nDelta: " + deltaValue.bgValuetoString(mgdl: true) + " * " + Int(ConstantsCalibrationAssistant.deltaMultiplier).description + " = " + Int(deltaResult).description + + calibrationAssistantAlertMessage += "\nStd Dev: " + stdDeviationValue.round(toDecimalPlaces: 1).description + " * " + Int(ConstantsCalibrationAssistant.stdDeviationMultiplier).description + " = " + Int(stdDeviationResult).description + + if higherBgUpperResult > 0 { + calibrationAssistantAlertMessage += "\nVery High BG [>" + Int(ConstantsCalibrationAssistant.higherBgUpperLimit).description + "]: " + higherBgUpperValue.round(toDecimalPlaces: 1).description + " * " + Int(ConstantsCalibrationAssistant.higherBgUpperMultiplier).description + " = " + Int(higherBgUpperResult).description + } + + if higherBgRecommendedResult > 0 { + calibrationAssistantAlertMessage += "\nHigh BG [>" + Int(ConstantsCalibrationAssistant.higherBgRecommendedLimit).description + "]: " + higherBgRecommendedValue.round(toDecimalPlaces: 1).description + " * " + Int(ConstantsCalibrationAssistant.higherBgRecommendedMultiplier).description + " = " + Int(higherBgRecommendedResult).description + } + + if lowerBgRecommendedResult > 0 { + calibrationAssistantAlertMessage += "\nLow BG [<" + Int(ConstantsCalibrationAssistant.lowerBgRecommendedLimit).description + "]: " + lowerBgRecommendedValue.round(toDecimalPlaces: 1).description + " * " + Int(ConstantsCalibrationAssistant.lowerBgRecommendedMultiplier).description + " = " + Int(lowerBgRecommendedResult).description + } + + if lowerBgLowerResult > 0 { + calibrationAssistantAlertMessage += "\nVery Low BG [<" + Int(ConstantsCalibrationAssistant.lowerBgLowerLimit).description + "]: " + lowerBgLowerValue.round(toDecimalPlaces: 1).description + " * " + Int(ConstantsCalibrationAssistant.lowerBgLowerMultiplier).description + " = " + Int(lowerBgLowerResult).description + } + + calibrationAssistantAlertMessage += "\nLimits: [<=" + Int(ConstantsCalibrationAssistant.okToCalibrateLimit).description + " / <" + Int(ConstantsCalibrationAssistant.notIdealToCalibrateLimit).description + "]" + + calibrationAssistantAlertMessage += "\nRESULT: " + Int(calibrationAssistantResult).description + " over " + Int(ConstantsCalibrationAssistant.minutesToUseForCalculations).description + " mins" + + } + + + // let's create the trace file message that will be logged if the user decides to calibrate. We can shorten it to fit on one line. + if calibrationAssistantResult > ConstantsCalibrationAssistant.notIdealToCalibrateLimit { + calibrationAssistantTraceMessage += "user warned not to calibrate." + } else if calibrationAssistantResult > ConstantsCalibrationAssistant.okToCalibrateLimit { + calibrationAssistantTraceMessage += "user advised to wait before calibrating." + } else { + calibrationAssistantTraceMessage += "user informed OK to calibrate." + } + + calibrationAssistantTraceMessage += " Actual BG in mg/dl: " + Int(actualBgValue).description + + calibrationAssistantTraceMessage += "; delta: " + Int(deltaValue).description + "*" + Int(ConstantsCalibrationAssistant.deltaMultiplier).description + "=" + Int(deltaResult).description + + calibrationAssistantTraceMessage += "; stdDev: " + stdDeviationValue.round(toDecimalPlaces: 1).description + "*" + Int(ConstantsCalibrationAssistant.stdDeviationMultiplier).description + "=" + Int(stdDeviationResult).description + + calibrationAssistantTraceMessage += "; veryHighBG [>" + Int(ConstantsCalibrationAssistant.higherBgUpperLimit).description + "]:" + higherBgUpperValue.round(toDecimalPlaces: 1).description + "*" + Int(ConstantsCalibrationAssistant.higherBgUpperMultiplier).description + "=" + Int(higherBgUpperResult).description + + calibrationAssistantTraceMessage += "; highBG [>" + Int(ConstantsCalibrationAssistant.higherBgRecommendedLimit).description + "]:" + higherBgRecommendedValue.round(toDecimalPlaces: 1).description + "*" + Int(ConstantsCalibrationAssistant.higherBgRecommendedMultiplier).description + "=" + Int(higherBgRecommendedResult).description + + calibrationAssistantTraceMessage += "; lowBG [<" + Int(ConstantsCalibrationAssistant.lowerBgRecommendedLimit).description + "]:" + lowerBgRecommendedValue.round(toDecimalPlaces: 1).description + "*" + Int(ConstantsCalibrationAssistant.lowerBgRecommendedMultiplier).description + "=" + Int(lowerBgRecommendedResult).description + + calibrationAssistantTraceMessage += "; veryLowBG [<" + Int(ConstantsCalibrationAssistant.lowerBgLowerLimit).description + "]:" + lowerBgLowerValue.round(toDecimalPlaces: 1).description + "*" + Int(ConstantsCalibrationAssistant.lowerBgLowerMultiplier).description + "=" + Int(lowerBgLowerResult).description + + calibrationAssistantTraceMessage += "; limits: [<=" + Int(ConstantsCalibrationAssistant.okToCalibrateLimit).description + " / <" + Int(ConstantsCalibrationAssistant.notIdealToCalibrateLimit).description + "]" + + calibrationAssistantTraceMessage += "; RESULT: " + Int(calibrationAssistantResult).description + " over " + Int(ConstantsCalibrationAssistant.minutesToUseForCalculations).description + " mins" + + } + + } + + return (calibrationAssistantResult, calibrationAssistantAlertMessage, calibrationAssistantTraceMessage) + + } + + + private func updateCalibrationAssistantStatus(calibrationAssistantResult: Double) { + + // if the user is using the Visual Calibration Assistant, then change the colour of the toolbar icon to indicate calibration suitability + if UserDefaults.standard.showVisualCalibrationAssistant { + + if calibrationAssistantResult < ConstantsCalibrationAssistant.okToCalibrateLimit { + + calibrateToolbarButtonOutlet.tintColor = nil + + } else if calibrationAssistantResult < ConstantsCalibrationAssistant.notIdealToCalibrateLimit { + + calibrateToolbarButtonOutlet.tintColor = UIColor.systemYellow + + } else { + + calibrateToolbarButtonOutlet.tintColor = UIColor.systemOrange + + } + + } else { + + // set it back to standard just in case + calibrateToolbarButtonOutlet.tintColor = nil + + } + + + } + } diff --git a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift index 1c92c1786..dd456f6ab 100644 --- a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift +++ b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift @@ -22,6 +22,9 @@ fileprivate enum Setting:Int, CaseIterable { /// Default value 0, if used then recommended value is multiple of 5 (eg 5 ot 10) case loopDelay = 5 + /// show the Calibration Assistant results + case showCalibrationAssistantResults = 6 + } struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol { @@ -62,6 +65,9 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol { case .loopDelay: return Texts_SettingsView.loopDelaysScreenTitle + case .showCalibrationAssistantResults: + return Texts_SettingsView.showCalibrationAssistantResults + } } @@ -71,7 +77,7 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol { switch setting { - case .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .suppressLoopShare: + case .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .suppressLoopShare, .showCalibrationAssistantResults: return UITableViewCell.AccessoryType.none case .loopDelay: @@ -104,6 +110,9 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol { case .loopDelay: return nil + case .showCalibrationAssistantResults: + return nil + } } @@ -157,6 +166,14 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol { case .loopDelay: return nil + case .showCalibrationAssistantResults: + return UISwitch(isOn: UserDefaults.standard.showCalibrationAssistantResults, action: { + (isOn:Bool) in + + UserDefaults.standard.showCalibrationAssistantResults = isOn + + }) + } } @@ -171,7 +188,7 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol { switch setting { - case .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .suppressLoopShare: + case .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .suppressLoopShare, .showCalibrationAssistantResults: return .nothing case .loopDelay: diff --git a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewHomeScreenSettingsViewModel.swift b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewHomeScreenSettingsViewModel.swift index 616eca9f3..32b8f0ac8 100644 --- a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewHomeScreenSettingsViewModel.swift +++ b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewHomeScreenSettingsViewModel.swift @@ -19,26 +19,29 @@ fileprivate enum Setting:Int, CaseIterable { // show a fixed scale mini-chart under the main scrollable chart? case showMiniChart = 2 + // should the calibration assistant be enabled? + case showVisualCalibrationAssistant = 3 + //urgent high value - case urgentHighMarkValue = 3 + case urgentHighMarkValue = 4 //high value - case highMarkValue = 4 + case highMarkValue = 5 //low value - case lowMarkValue = 5 + case lowMarkValue = 6 //urgent low value - case urgentLowMarkValue = 6 + case urgentLowMarkValue = 7 //use objectives in graph? - case useObjectives = 7 + case useObjectives = 8 //show target line? - case showTarget = 8 + case showTarget = 9 //target value - case targetMarkValue = 9 + case targetMarkValue = 10 } @@ -60,6 +63,9 @@ struct SettingsViewHomeScreenSettingsViewModel:SettingsViewModelProtocol { case .showMiniChart: return UISwitch(isOn: UserDefaults.standard.showMiniChart, action: {(isOn:Bool) in UserDefaults.standard.showMiniChart = isOn}) + case .showVisualCalibrationAssistant: + return UISwitch(isOn: UserDefaults.standard.showVisualCalibrationAssistant, action: {(isOn:Bool) in UserDefaults.standard.showVisualCalibrationAssistant = isOn}) + case .useObjectives: return UISwitch(isOn: UserDefaults.standard.useObjectives, action: {(isOn:Bool) in UserDefaults.standard.useObjectives = isOn}) @@ -133,6 +139,15 @@ struct SettingsViewHomeScreenSettingsViewModel:SettingsViewModelProtocol { } }) + case .showVisualCalibrationAssistant: + return SettingsSelectedRowAction.callFunction(function: { + if UserDefaults.standard.showVisualCalibrationAssistant { + UserDefaults.standard.showVisualCalibrationAssistant = false + } else { + UserDefaults.standard.showVisualCalibrationAssistant = true + } + }) + case .useObjectives: return SettingsSelectedRowAction.callFunction(function: { if UserDefaults.standard.useObjectives { @@ -198,6 +213,9 @@ struct SettingsViewHomeScreenSettingsViewModel:SettingsViewModelProtocol { case .showMiniChart: return Texts_SettingsView.showMiniChart + case .showVisualCalibrationAssistant: + return Texts_SettingsView.showVisualCalibrationAssistant + case .useObjectives: return Texts_SettingsView.labelUseObjectives @@ -217,7 +235,7 @@ struct SettingsViewHomeScreenSettingsViewModel:SettingsViewModelProtocol { case .urgentHighMarkValue, .highMarkValue, .lowMarkValue, .urgentLowMarkValue, .targetMarkValue: return UITableViewCell.AccessoryType.disclosureIndicator - case .allowScreenRotation, .showClockWhenScreenIsLocked, .showMiniChart, .useObjectives, .showTarget: + case .allowScreenRotation, .showClockWhenScreenIsLocked, .showMiniChart, .useObjectives, .showTarget, .showVisualCalibrationAssistant: return UITableViewCell.AccessoryType.none } @@ -243,7 +261,7 @@ struct SettingsViewHomeScreenSettingsViewModel:SettingsViewModelProtocol { case .targetMarkValue: return UserDefaults.standard.targetMarkValueInUserChosenUnit.bgValuetoString(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) - case .allowScreenRotation, .showClockWhenScreenIsLocked, .showMiniChart, .useObjectives, .showTarget: + case .allowScreenRotation, .showClockWhenScreenIsLocked, .showMiniChart, .useObjectives, .showTarget, .showVisualCalibrationAssistant: return nil }