Skip to content

Commit 9da678d

Browse files
authored
Denote stale glucose as three dashes (#1275)
1 parent ed7bf7f commit 9da678d

File tree

15 files changed

+269
-87
lines changed

15 files changed

+269
-87
lines changed

Common/Models/WatchContext.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ final class WatchContext: RawRepresentable {
2121
var glucose: HKQuantity?
2222
var glucoseTrendRawValue: Int?
2323
var glucoseDate: Date?
24+
var glucoseSyncIdentifier: String?
2425

2526
var predictedGlucose: WatchPredictedGlucose?
2627
var eventualGlucose: HKQuantity? {
@@ -58,6 +59,7 @@ final class WatchContext: RawRepresentable {
5859

5960
glucoseTrendRawValue = rawValue["gt"] as? Int
6061
glucoseDate = rawValue["gd"] as? Date
62+
glucoseSyncIdentifier = rawValue["gs"] as? String
6163
iob = rawValue["iob"] as? Double
6264
reservoir = rawValue["r"] as? Double
6365
reservoirPercentage = rawValue["rp"] as? Double
@@ -95,6 +97,7 @@ final class WatchContext: RawRepresentable {
9597

9698
raw["gt"] = glucoseTrendRawValue
9799
raw["gd"] = glucoseDate
100+
raw["gs"] = glucoseSyncIdentifier
98101
raw["iob"] = iob
99102
raw["ld"] = loopLastRunDate
100103
raw["r"] = reservoir
@@ -117,3 +120,12 @@ extension WatchContext {
117120
}
118121
}
119122
}
123+
124+
extension WatchContext {
125+
var newGlucoseSample: NewGlucoseSample? {
126+
if let quantity = glucose, let date = glucoseDate, let syncIdentifier = glucoseSyncIdentifier {
127+
return NewGlucoseSample(date: date, quantity: quantity, isDisplayOnly: false, syncIdentifier: syncIdentifier, syncVersion: 0)
128+
}
129+
return nil
130+
}
131+
}

Loop Status Extension/StatusViewController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,12 @@ class StatusViewController: UIViewController, NCWidgetProviding {
256256
return
257257
}
258258

259-
if let lastGlucose = glucose.last {
259+
if let lastGlucose = glucose.last, let recencyInterval = defaults.loopSettings?.inputDataRecencyInterval {
260260
self.hudView.glucoseHUD.setGlucoseQuantity(
261261
lastGlucose.quantity.doubleValue(for: unit),
262262
at: lastGlucose.startDate,
263263
unit: unit,
264+
staleGlucoseAge: recencyInterval,
264265
sensor: context.sensor
265266
)
266267
}

Loop.xcodeproj/project.pbxproj

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,8 @@
375375
C17824A61E1AF91F00D9D25C /* BolusRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */; };
376376
C1814B86225E507C008D2D8E /* Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1814B85225E507C008D2D8E /* Sequence.swift */; };
377377
C18C8C511D5A351900E043FB /* NightscoutDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */; };
378+
C19E96DF23D275F8003F79B0 /* LoopCompletionFreshness.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19E96DD23D2733F003F79B0 /* LoopCompletionFreshness.swift */; };
379+
C19E96E023D275FA003F79B0 /* LoopCompletionFreshness.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19E96DD23D2733F003F79B0 /* LoopCompletionFreshness.swift */; };
378380
C1A3EED2235233E1007672E3 /* DerivedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C1A3EED1235233E1007672E3 /* DerivedAssets.xcassets */; };
379381
C1A3EED423523551007672E3 /* DerivedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C1A3EED323523551007672E3 /* DerivedAssets.xcassets */; };
380382
C1A3EED523535FFF007672E3 /* DefaultAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 894F71E11FFEC4D8007D365C /* DefaultAssets.xcassets */; };
@@ -1087,6 +1089,7 @@
10871089
C18A491422FCC22900FDA733 /* build-derived-watch-assets.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "build-derived-watch-assets.sh"; sourceTree = "<group>"; };
10881090
C18A491522FCC22900FDA733 /* copy-plugins.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "copy-plugins.sh"; sourceTree = "<group>"; };
10891091
C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutDataManager.swift; sourceTree = "<group>"; };
1092+
C19E96DD23D2733F003F79B0 /* LoopCompletionFreshness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopCompletionFreshness.swift; sourceTree = "<group>"; };
10901093
C1A3EED1235233E1007672E3 /* DerivedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssets.xcassets; sourceTree = "<group>"; };
10911094
C1A3EED323523551007672E3 /* DerivedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssets.xcassets; sourceTree = "<group>"; };
10921095
C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_dropping_then_rising.json; sourceTree = "<group>"; };
@@ -1517,6 +1520,7 @@
15171520
43C05CB721EBEA54006FB252 /* HKUnit.swift */,
15181521
434FF1E91CF26C29000DB779 /* IdentifiableClass.swift */,
15191522
430B298C2041F56500BA9F93 /* LoopSettings.swift */,
1523+
C19E96DD23D2733F003F79B0 /* LoopCompletionFreshness.swift */,
15201524
430B29892041F54A00BA9F93 /* NSUserDefaults.swift */,
15211525
431E73471FF95A900069B5F7 /* PersistenceController.swift */,
15221526
43D848AF1E7DCBE100DADCBC /* Result.swift */,
@@ -2790,6 +2794,7 @@
27902794
files = (
27912795
43C05CB821EBEA54006FB252 /* HKUnit.swift in Sources */,
27922796
4345E3F421F036FC009E00E5 /* Result.swift in Sources */,
2797+
C19E96E023D275FA003F79B0 /* LoopCompletionFreshness.swift in Sources */,
27932798
43D9002021EB209400AF44BF /* NSTimeInterval.swift in Sources */,
27942799
43C05CA921EB2B26006FB252 /* PersistenceController.swift in Sources */,
27952800
431EA87221EB29150076EC1A /* InsulinModelSettings.swift in Sources */,
@@ -2842,6 +2847,7 @@
28422847
files = (
28432848
43C05CB921EBEA54006FB252 /* HKUnit.swift in Sources */,
28442849
4345E3F521F036FC009E00E5 /* Result.swift in Sources */,
2850+
C19E96DF23D275F8003F79B0 /* LoopCompletionFreshness.swift in Sources */,
28452851
43D9FFFB21EAF3D300AF44BF /* NSTimeInterval.swift in Sources */,
28462852
43C05CA821EB2B26006FB252 /* PersistenceController.swift in Sources */,
28472853
431EA87321EB29160076EC1A /* InsulinModelSettings.swift in Sources */,
@@ -3531,7 +3537,7 @@
35313537
ASSETCATALOG_COMPILER_APPICON_NAME = "$(APPICON_NAME)";
35323538
CODE_SIGN_ENTITLEMENTS = Loop/Loop.entitlements;
35333539
CODE_SIGN_IDENTITY = "iPhone Developer";
3534-
DEVELOPMENT_TEAM = "";
3540+
DEVELOPMENT_TEAM = UY678SP37Q;
35353541
ENABLE_BITCODE = YES;
35363542
INFOPLIST_FILE = Loop/Info.plist;
35373543
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -3550,7 +3556,7 @@
35503556
ASSETCATALOG_COMPILER_APPICON_NAME = "$(APPICON_NAME)";
35513557
CODE_SIGN_ENTITLEMENTS = Loop/Loop.entitlements;
35523558
CODE_SIGN_IDENTITY = "iPhone Developer";
3553-
DEVELOPMENT_TEAM = "";
3559+
DEVELOPMENT_TEAM = UY678SP37Q;
35543560
ENABLE_BITCODE = YES;
35553561
INFOPLIST_FILE = Loop/Info.plist;
35563562
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -3568,7 +3574,7 @@
35683574
CODE_SIGN_ENTITLEMENTS = "WatchApp Extension/WatchApp Extension.entitlements";
35693575
CODE_SIGN_IDENTITY = "iPhone Developer";
35703576
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
3571-
DEVELOPMENT_TEAM = "";
3577+
DEVELOPMENT_TEAM = UY678SP37Q;
35723578
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/Carthage/Build/watchOS";
35733579
INFOPLIST_FILE = "WatchApp Extension/Info.plist";
35743580
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
@@ -3591,7 +3597,7 @@
35913597
CODE_SIGN_ENTITLEMENTS = "WatchApp Extension/WatchApp Extension.entitlements";
35923598
CODE_SIGN_IDENTITY = "iPhone Developer";
35933599
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
3594-
DEVELOPMENT_TEAM = "";
3600+
DEVELOPMENT_TEAM = UY678SP37Q;
35953601
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/Carthage/Build/watchOS";
35963602
INFOPLIST_FILE = "WatchApp Extension/Info.plist";
35973603
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
@@ -3612,7 +3618,7 @@
36123618
ASSETCATALOG_COMPILER_APPICON_NAME = "$(APPICON_NAME)";
36133619
CODE_SIGN_IDENTITY = "iPhone Developer";
36143620
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
3615-
DEVELOPMENT_TEAM = "";
3621+
DEVELOPMENT_TEAM = UY678SP37Q;
36163622
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/Carthage/Build/watchOS";
36173623
IBSC_MODULE = WatchApp_Extension;
36183624
INFOPLIST_FILE = WatchApp/Info.plist;
@@ -3633,7 +3639,7 @@
36333639
ASSETCATALOG_COMPILER_APPICON_NAME = "$(APPICON_NAME)";
36343640
CODE_SIGN_IDENTITY = "iPhone Developer";
36353641
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
3636-
DEVELOPMENT_TEAM = "";
3642+
DEVELOPMENT_TEAM = UY678SP37Q;
36373643
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/Carthage/Build/watchOS";
36383644
IBSC_MODULE = WatchApp_Extension;
36393645
INFOPLIST_FILE = WatchApp/Info.plist;
@@ -3913,7 +3919,7 @@
39133919
CODE_SIGN_IDENTITY = "Apple Development";
39143920
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
39153921
CODE_SIGN_STYLE = Automatic;
3916-
DEVELOPMENT_TEAM = "";
3922+
DEVELOPMENT_TEAM = UY678SP37Q;
39173923
ENABLE_BITCODE = NO;
39183924
INFOPLIST_FILE = "Loop Status Extension/Info.plist";
39193925
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
@@ -3935,7 +3941,7 @@
39353941
CODE_SIGN_IDENTITY = "Apple Development";
39363942
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
39373943
CODE_SIGN_STYLE = Automatic;
3938-
DEVELOPMENT_TEAM = "";
3944+
DEVELOPMENT_TEAM = UY678SP37Q;
39393945
ENABLE_BITCODE = NO;
39403946
INFOPLIST_FILE = "Loop Status Extension/Info.plist";
39413947
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";

Loop/Managers/LoopDataManager.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ extension LoopDataManager {
726726
// Fetch glucose effects as far back as we want to make retroactive analysis
727727
var latestGlucoseDate: Date?
728728
updateGroup.enter()
729-
glucoseStore.getCachedGlucoseSamples(start: Date(timeIntervalSinceNow: -settings.recencyInterval)) { (values) in
729+
glucoseStore.getCachedGlucoseSamples(start: Date(timeIntervalSinceNow: -settings.inputDataRecencyInterval)) { (values) in
730730
latestGlucoseDate = values.last?.startDate
731731
updateGroup.leave()
732732
}
@@ -917,11 +917,11 @@ extension LoopDataManager {
917917
let lastGlucoseDate = glucose.startDate
918918
let now = Date()
919919

920-
guard now.timeIntervalSince(lastGlucoseDate) <= settings.recencyInterval else {
920+
guard now.timeIntervalSince(lastGlucoseDate) <= settings.inputDataRecencyInterval else {
921921
throw LoopError.glucoseTooOld(date: glucose.startDate)
922922
}
923923

924-
guard now.timeIntervalSince(pumpStatusDate) <= settings.recencyInterval else {
924+
guard now.timeIntervalSince(pumpStatusDate) <= settings.inputDataRecencyInterval else {
925925
throw LoopError.pumpDataTooOld(date: pumpStatusDate)
926926
}
927927

@@ -1015,11 +1015,11 @@ extension LoopDataManager {
10151015
let lastGlucoseDate = glucose.startDate
10161016
let now = Date()
10171017

1018-
guard now.timeIntervalSince(lastGlucoseDate) <= settings.recencyInterval else {
1018+
guard now.timeIntervalSince(lastGlucoseDate) <= settings.inputDataRecencyInterval else {
10191019
throw LoopError.glucoseTooOld(date: glucose.startDate)
10201020
}
10211021

1022-
guard now.timeIntervalSince(pumpStatusDate) <= settings.recencyInterval else {
1022+
guard now.timeIntervalSince(pumpStatusDate) <= settings.inputDataRecencyInterval else {
10231023
throw LoopError.pumpDataTooOld(date: pumpStatusDate)
10241024
}
10251025

@@ -1112,7 +1112,7 @@ extension LoopDataManager {
11121112
retrospectiveGlucoseEffect = retrospectiveCorrection.computeEffect(
11131113
startingAt: glucose,
11141114
retrospectiveGlucoseDiscrepanciesSummed: retrospectiveGlucoseDiscrepanciesSummed,
1115-
recencyInterval: settings.recencyInterval,
1115+
recencyInterval: settings.inputDataRecencyInterval,
11161116
insulinSensitivitySchedule: insulinSensitivitySchedule,
11171117
basalRateSchedule: basalRateSchedule,
11181118
glucoseCorrectionRangeSchedule: settings.glucoseTargetRangeSchedule,
@@ -1126,7 +1126,7 @@ extension LoopDataManager {
11261126
return retrospectiveCorrection.computeEffect(
11271127
startingAt: glucose,
11281128
retrospectiveGlucoseDiscrepanciesSummed: retrospectiveGlucoseDiscrepanciesSummed,
1129-
recencyInterval: settings.recencyInterval,
1129+
recencyInterval: settings.inputDataRecencyInterval,
11301130
insulinSensitivitySchedule: insulinSensitivitySchedule,
11311131
basalRateSchedule: basalRateSchedule,
11321132
glucoseCorrectionRangeSchedule: settings.glucoseTargetRangeSchedule,
@@ -1155,12 +1155,12 @@ extension LoopDataManager {
11551155

11561156
let startDate = Date()
11571157

1158-
guard startDate.timeIntervalSince(glucose.startDate) <= settings.recencyInterval else {
1158+
guard startDate.timeIntervalSince(glucose.startDate) <= settings.inputDataRecencyInterval else {
11591159
self.predictedGlucose = nil
11601160
throw LoopError.glucoseTooOld(date: glucose.startDate)
11611161
}
11621162

1163-
guard startDate.timeIntervalSince(pumpStatusDate) <= settings.recencyInterval else {
1163+
guard startDate.timeIntervalSince(pumpStatusDate) <= settings.inputDataRecencyInterval else {
11641164
self.predictedGlucose = nil
11651165
throw LoopError.pumpDataTooOld(date: pumpStatusDate)
11661166
}

Loop/Managers/WatchDataManager.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,18 @@ final class WatchDataManager: NSObject {
211211
if let trend = self.deviceManager.cgmManager?.sensorState?.trendType {
212212
context.glucoseTrendRawValue = trend.rawValue
213213
}
214+
215+
if let glucose = glucose {
216+
updateGroup.enter()
217+
manager.glucoseStore.getCachedGlucoseSamples(start: glucose.startDate) { (samples) in
218+
if let sample = samples.last {
219+
context.glucose = sample.quantity
220+
context.glucoseDate = sample.startDate
221+
context.glucoseSyncIdentifier = sample.syncIdentifier
222+
}
223+
updateGroup.leave()
224+
}
225+
}
214226

215227
updateGroup.enter()
216228
manager.doseStore.insulinOnBoard(at: Date()) { (result) in

Loop/View Controllers/StatusTableViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ final class StatusTableViewController: ChartsTableViewController {
487487
hudView.glucoseHUD.setGlucoseQuantity(glucose.quantity.doubleValue(for: unit),
488488
at: glucose.startDate,
489489
unit: unit,
490+
staleGlucoseAge: self.deviceManager.loopManager.settings.inputDataRecencyInterval,
490491
sensor: self.deviceManager.sensorState
491492
)
492493
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// LoopCompletionFreshness.swift
3+
// Loop
4+
//
5+
// Created by Pete Schwamb on 1/17/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public enum LoopCompletionFreshness {
12+
case fresh
13+
case aging
14+
case stale
15+
case unknown
16+
17+
public var maxAge: TimeInterval? {
18+
switch self {
19+
case .fresh:
20+
return TimeInterval(minutes: 6)
21+
case .aging:
22+
return TimeInterval(minutes: 16)
23+
case .stale:
24+
return TimeInterval(hours: 12)
25+
case .unknown:
26+
return nil
27+
}
28+
}
29+
30+
public init(age: TimeInterval?) {
31+
guard let age = age else {
32+
self = .unknown
33+
return
34+
}
35+
36+
switch age {
37+
case let t where t <= LoopCompletionFreshness.fresh.maxAge!:
38+
self = .fresh
39+
case let t where t <= LoopCompletionFreshness.aging.maxAge!:
40+
self = .aging
41+
case let t where t <= LoopCompletionFreshness.stale.maxAge!:
42+
self = .stale
43+
default:
44+
self = .unknown
45+
}
46+
}
47+
48+
public init(lastCompletion: Date?, at date: Date = Date()) {
49+
guard let lastCompletion = lastCompletion else {
50+
self = .unknown
51+
return
52+
}
53+
54+
self = LoopCompletionFreshness(age: date.timeIntervalSince(lastCompletion))
55+
}
56+
57+
}

LoopCore/LoopSettings.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@ public struct LoopSettings: Equatable {
3636
/// The interval over which to aggregate changes in glucose for retrospective correction
3737
public let retrospectiveCorrectionGroupingInterval = TimeInterval(minutes: 30)
3838

39-
/// The amount of time since a given date that data should be considered valid
40-
public let recencyInterval = TimeInterval(minutes: 15)
39+
/// The amount of time since a given date that input data should be considered valid
40+
public let inputDataRecencyInterval = TimeInterval(minutes: 15)
41+
42+
/// Loop completion aging category limits
43+
public let completionFreshLimit = TimeInterval(minutes: 6)
44+
public let completionAgingLimit = TimeInterval(minutes: 16)
45+
public let completionStaleLimit = TimeInterval(hours: 12)
4146

4247
public let batteryReplacementDetectionThreshold = 0.5
4348

LoopUI/Views/GlucoseHUDView.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,27 +91,27 @@ public final class GlucoseHUDView: BaseHUDView {
9191
}
9292
}
9393

94-
public func setGlucoseQuantity(_ glucoseQuantity: Double, at glucoseStartDate: Date, unit: HKUnit, sensor: SensorDisplayable?) {
94+
public func setGlucoseQuantity(_ glucoseQuantity: Double, at glucoseStartDate: Date, unit: HKUnit, staleGlucoseAge: TimeInterval, sensor: SensorDisplayable?) {
9595
var accessibilityStrings = [String]()
9696

9797
let time = timeFormatter.string(from: glucoseStartDate)
9898
caption?.text = time
9999

100-
let sensorDataCurrent = glucoseStartDate.timeIntervalSinceNow > TimeInterval(minutes: -15)
100+
let glucoseValueCurrent = glucoseStartDate.timeIntervalSinceNow > -staleGlucoseAge
101101

102102
let numberFormatter = NumberFormatter.glucoseFormatter(for: unit)
103103
if let valueString = numberFormatter.string(from: glucoseQuantity) {
104-
if sensorDataCurrent {
104+
if glucoseValueCurrent {
105105
glucoseLabel.text = valueString
106106
} else {
107-
glucoseLabel.text = "-"
107+
glucoseLabel.text = "---"
108108
}
109109
accessibilityStrings.append(String(format: LocalizedString("%1$@ at %2$@", comment: "Accessbility format value describing glucose: (1: glucose number)(2: glucose time)"), valueString, time))
110110
}
111111

112112
var unitStrings = [unit.localizedShortUnitString]
113113

114-
if let trend = sensor?.trendType, sensorDataCurrent {
114+
if let trend = sensor?.trendType, glucoseValueCurrent {
115115
unitStrings.append(trend.symbol)
116116
accessibilityStrings.append(trend.localizedDescription)
117117
}

0 commit comments

Comments
 (0)