Skip to content

Commit 4b91b56

Browse files
committed
Add Trio smoothed BG display option
- Adds Display Smoothed BG toggle in Settings → Advanced (off by default). - Pulls smoothed values from Nightscout devicestatus openaps.enacted.bg / openaps.suggested.bg and stores an in-memory history bounded by the graph's day range. - Renders smoothed values as a light-grey line on the main BG chart; CGM dots stay colorful, but the dot-connecting line is hidden while smoothing is on. Display Dots / Display Lines toggles are disabled in Graph Settings during this mode. - Chart-tap popup shows the smoothed value (✨ … ✨) above the raw CGM value when a match is available within tolerance of the dot's timestamp. - All work (fetches, parsing, polling) is gated on the toggle so users who don't enable it incur no extra overhead. - Add the option for users to display the smoothed value in the information display table as well.
1 parent 0d91b95 commit 4b91b56

12 files changed

Lines changed: 375 additions & 26 deletions

File tree

LoopFollow.xcodeproj/project.pbxproj

Lines changed: 16 additions & 12 deletions
Large diffs are not rendered by default.

LoopFollow/Controllers/Graphs.swift

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ enum GraphDataIndex: Int {
2727
case smb = 16
2828
case tempTarget = 17
2929
case predictionCone = 18
30+
case smoothedBg = 19
3031
}
3132

3233
extension GraphDataIndex {
@@ -51,6 +52,7 @@ extension GraphDataIndex {
5152
case .smb: return "SMB"
5253
case .tempTarget: return "Temp Target"
5354
case .predictionCone: return "Prediction Cone"
55+
case .smoothedBg: return "Smoothed BG"
5456
}
5557
}
5658
}
@@ -622,6 +624,20 @@ extension MainViewController {
622624
lineCone.axisDependency = YAxis.AxisDependency.right
623625
data.append(lineCone)
624626

627+
// Dataset 19: Smoothed BG line — light-grey straight segments connecting
628+
// Trio's smoothed values, no dots. Populated only when displaySmoothedBG
629+
// is on. Linear mode (passes through each value Trio reported, no curve
630+
// interpolation).
631+
let lineSmoothedBg = LineChartDataSet(entries: [ChartDataEntry](), label: "")
632+
lineSmoothedBg.setColor(NSUIColor.lightGray)
633+
lineSmoothedBg.lineWidth = 1.5
634+
lineSmoothedBg.drawCirclesEnabled = false
635+
lineSmoothedBg.drawValuesEnabled = false
636+
lineSmoothedBg.highlightEnabled = false
637+
lineSmoothedBg.axisDependency = YAxis.AxisDependency.right
638+
lineSmoothedBg.mode = .linear
639+
data.append(lineSmoothedBg)
640+
625641
data.setValueFont(UIFont.systemFont(ofSize: 12))
626642

627643
// Add marker popups for bolus and carbs
@@ -772,20 +788,17 @@ extension MainViewController {
772788
let dataIndexPrediction = 1
773789
let lineBG = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
774790
let linePrediction = BGChart.lineData!.dataSets[dataIndexPrediction] as! LineChartDataSet
775-
if Storage.shared.showLines.value {
776-
lineBG.lineWidth = 2
777-
linePrediction.lineWidth = 2
778-
} else {
791+
// When smoothing is on, BG renders dots-only (the white smoothing line replaces
792+
// the BG connecting line). The user-controlled toggles only apply when off.
793+
if Storage.shared.displaySmoothedBG.value {
779794
lineBG.lineWidth = 0
780-
linePrediction.lineWidth = 0
781-
}
782-
if Storage.shared.showDots.value {
783795
lineBG.drawCirclesEnabled = true
784-
linePrediction.drawCirclesEnabled = true
785796
} else {
786-
lineBG.drawCirclesEnabled = false
787-
linePrediction.drawCirclesEnabled = false
797+
lineBG.lineWidth = Storage.shared.showLines.value ? 2 : 0
798+
lineBG.drawCirclesEnabled = Storage.shared.showDots.value
788799
}
800+
linePrediction.lineWidth = Storage.shared.showLines.value ? 2 : 0
801+
linePrediction.drawCirclesEnabled = Storage.shared.showDots.value
789802

790803
BGChart.rightAxis.axisMinimum = 0
791804

@@ -833,6 +846,7 @@ extension MainViewController {
833846

834847
topBG = Storage.shared.minBGScale.value
835848
let thresholds = graphRangeThresholds()
849+
let showSmoothed = Storage.shared.displaySmoothedBG.value
836850
for i in 0 ..< entries.count {
837851
// Clamp the plotted y-value to the same bounds the header text uses
838852
// (HIGH/LOW), so the graph stays consistent with the main display.
@@ -841,7 +855,15 @@ extension MainViewController {
841855
if plottedSgv > topBG - maxBGOffset {
842856
topBG = plottedSgv + maxBGOffset
843857
}
844-
let value = ChartDataEntry(x: Double(entries[i].date), y: plottedSgv, data: formatPillText(line1: Localizer.toDisplayUnits(String(entries[i].sgv)), time: entries[i].date))
858+
let cgmLine = Localizer.toDisplayUnits(String(entries[i].sgv))
859+
// When the smoothed BG is available, render it ABOVE the raw CGM line.
860+
let pillText: String
861+
if showSmoothed, let smoothed = smoothedBg(near: entries[i].date) {
862+
pillText = formatPillText(line1: "\(Localizer.toDisplayUnits(String(smoothed)))", time: entries[i].date, line2: cgmLine)
863+
} else {
864+
pillText = formatPillText(line1: cgmLine, time: entries[i].date)
865+
}
866+
let value = ChartDataEntry(x: Double(entries[i].date), y: plottedSgv, data: pillText)
845867
mainChart.append(value)
846868
smallChart.append(value)
847869

@@ -872,13 +894,54 @@ extension MainViewController {
872894
}
873895
}
874896

897+
// When smoothing is on, force the main BG dataset to render dots-only (no
898+
// connecting line) so the smoothing line is the only line drawn through
899+
// the readings. When off, honor the user's Display Lines/Dots toggles.
900+
// The small chart keeps its original line-only style (set at creation in
901+
// createSmallBGGraph) so we don't touch lineBGSmall here.
902+
if showSmoothed {
903+
lineBG.lineWidth = 0
904+
lineBG.drawCirclesEnabled = true
905+
} else {
906+
lineBG.lineWidth = Storage.shared.showLines.value ? 2 : 0
907+
lineBG.drawCirclesEnabled = Storage.shared.showDots.value
908+
}
909+
910+
// Populate the smoothed-BG line dataset on the main chart only.
911+
// We iterate `smoothedBgData` directly so the line always extends to the
912+
// most recent smoothed value Trio has reported. To avoid the line being
913+
// pulled toward bolus / carb dots (Trio writes extra openaps records on
914+
// every treatment-triggered loop run, with timestamps that can land
915+
// between the regular 5-minute cycles), we skip any record that lands
916+
// within 4 minutes of the previously kept one. Regular runs are ~5 min
917+
// apart so they pass through; treatment-triggered runs that fire shortly
918+
// after a regular one are filtered out.
919+
let smoothedIndex = GraphDataIndex.smoothedBg.rawValue
920+
if let mainSmoothed = BGChart.lineData?.dataSets[smoothedIndex] as? LineChartDataSet {
921+
mainSmoothed.removeAll(keepingCapacity: false)
922+
if showSmoothed, !smoothedBgData.isEmpty {
923+
let firstBgTime = entries.first?.date ?? -.infinity
924+
let lastBgTime = entries.last?.date ?? .infinity
925+
let minSpacing: TimeInterval = 240
926+
var lastKeptTime: TimeInterval = -.infinity
927+
for sb in smoothedBgData where sb.time >= firstBgTime && sb.time <= lastBgTime + 150 {
928+
if sb.time - lastKeptTime < minSpacing { continue }
929+
let plotted = min(max(sb.bgMgdl, Double(globalVariables.minDisplayGlucose)), Double(globalVariables.maxDisplayGlucose))
930+
mainSmoothed.append(ChartDataEntry(x: sb.time, y: plotted))
931+
lastKeptTime = sb.time
932+
}
933+
}
934+
}
935+
875936
BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue())
876937
BGChart.setVisibleXRangeMinimum(600)
877938
BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
939+
BGChart.data?.dataSets[smoothedIndex].notifyDataSetChanged()
878940
BGChart.data?.notifyDataChanged()
879941
BGChart.notifyDataSetChanged()
880942
BGChartFull.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue())
881943
BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
944+
BGChartFull.data?.dataSets[smoothedIndex].notifyDataSetChanged()
882945
BGChartFull.data?.notifyDataChanged()
883946
BGChartFull.notifyDataSetChanged()
884947

@@ -1649,6 +1712,17 @@ extension MainViewController {
16491712
lineConeSmall.axisDependency = YAxis.AxisDependency.right
16501713
data.append(lineConeSmall)
16511714

1715+
// Dataset 19: Smoothed BG line on the small graph too.
1716+
let lineSmoothedBgSmall = LineChartDataSet(entries: [ChartDataEntry](), label: "")
1717+
lineSmoothedBgSmall.setColor(NSUIColor.lightGray)
1718+
lineSmoothedBgSmall.lineWidth = 1.0
1719+
lineSmoothedBgSmall.drawCirclesEnabled = false
1720+
lineSmoothedBgSmall.drawValuesEnabled = false
1721+
lineSmoothedBgSmall.highlightEnabled = false
1722+
lineSmoothedBgSmall.axisDependency = YAxis.AxisDependency.right
1723+
lineSmoothedBgSmall.mode = .cubicBezier
1724+
data.append(lineSmoothedBgSmall)
1725+
16521726
BGChartFull.highlightPerDragEnabled = true
16531727
BGChartFull.leftAxis.enabled = false
16541728
BGChartFull.leftAxis.axisMaximum = maxBasal

LoopFollow/Controllers/Nightscout/BGData.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,15 @@ extension MainViewController {
129129

130130
let latestReading = data[0]
131131
let sensorTimestamp = latestReading.date
132+
133+
// If this is a brand-new reading (newer than what we last processed), pull
134+
// devicestatus right away so the smoothed BG for this dot lands on the chart
135+
// alongside the dot itself, not on the next 5-min devicestatus poll.
136+
let previouslyProcessedBgTime = Storage.shared.lastBgReadingTimeSeconds.value ?? 0
137+
if sensorTimestamp > previouslyProcessedBgTime, IsNightscoutEnabled() {
138+
// Tiny buffer so the loop has a moment to write its devicestatus record.
139+
TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date().addingTimeInterval(1))
140+
}
132141
let now = dateTimeUtils.getNowTimeIntervalUTC()
133142
// secondsAgo is how old the newest reading is
134143
let secondsAgo = now - sensorTimestamp

LoopFollow/Controllers/Nightscout/DeviceStatus.swift

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,31 @@ extension MainViewController {
212212
let now = dateTimeUtils.getNowTimeIntervalUTC()
213213
let secondsAgo = now - (Observable.shared.alertLastLoopTime.value ?? 0)
214214

215+
// If the smoothed-BG feature is on and the most recent BG dot doesn't yet
216+
// have a matching smoothed point, poll devicestatus aggressively until it
217+
// does — Trio's loop runs and writes the smoothed BG within seconds of each
218+
// new CGM reading, but occasionally takes longer. Backoff the cadence:
219+
// 3s while the BG is still fresh (<60s), 15s after that up to 5 minutes,
220+
// then give up. Only run when we've seen at least one smoothed point
221+
// (Trio is in use) so Loop accounts don't burn polls.
222+
let latestBgAge: TimeInterval = bgData.last.map { Date().timeIntervalSince1970 - $0.date } ?? .infinity
223+
let needsSmoothedBgRetry: Bool = {
224+
guard Storage.shared.displaySmoothedBG.value,
225+
!smoothedBgData.isEmpty,
226+
let latestBg = bgData.last,
227+
latestBgAge < 300
228+
else { return false }
229+
return smoothedBg(near: latestBg.date) == nil
230+
}()
231+
let smoothedBgRetryDelay: TimeInterval = latestBgAge < 60 ? 3 : 15
232+
215233
DispatchQueue.main.async {
216-
if secondsAgo >= (20 * 60) {
234+
if needsSmoothedBgRetry {
235+
TaskScheduler.shared.rescheduleTask(
236+
id: .deviceStatus,
237+
to: Date().addingTimeInterval(smoothedBgRetryDelay)
238+
)
239+
} else if secondsAgo >= (20 * 60) {
217240
TaskScheduler.shared.rescheduleTask(
218241
id: .deviceStatus,
219242
to: Date().addingTimeInterval(5 * 60)
@@ -251,6 +274,14 @@ extension MainViewController {
251274
// Mark device status as loaded for initial loading state
252275
markDataLoaded("deviceStatus")
253276

277+
// First successful loop run of the session: backfill the smoothed-BG history
278+
// so the popup can show ✨ values for older glucose dots, not just the latest.
279+
// Gated on the feature toggle and a session flag — DeviceStatusOpenAPS may
280+
// have already appended the current point above, so we can't use isEmpty here.
281+
if Storage.shared.displaySmoothedBG.value, !hasFetchedSmoothedBgHistory {
282+
webLoadNSSmoothedBgHistory()
283+
}
284+
254285
if Storage.shared.contactEnabled.value, Storage.shared.contactIOB.value != .off,
255286
Observable.shared.iobText.value != previousIOBText
256287
{

LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ extension MainViewController {
114114
Observable.shared.deviceRecBolus.value = nil
115115
}
116116

117+
// Smoothed BG (Trio applies CGM smoothing and reports the smoothed value here).
118+
// Append to in-memory history so each loop run's smoothed value can be matched to its glucose dot.
119+
// Skip entirely when the feature toggle is off so we don't pay the parse / append cost.
120+
if Storage.shared.displaySmoothedBG.value,
121+
let smoothedBgValue = enactedOrSuggested["bg"] as? Double,
122+
let updatedTime = updatedTime
123+
{
124+
appendSmoothedBgPoint(time: updatedTime, bgMgdl: smoothedBgValue)
125+
infoManager.updateInfoData(type: .smoothedBg, value: Localizer.toDisplayUnits(String(smoothedBgValue)))
126+
}
127+
117128
// Eventual BG
118129
if let eventualBGValue = enactedOrSuggested["eventualBG"] as? Double {
119130
let eventualBGQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: eventualBGValue)

0 commit comments

Comments
 (0)