Browse Source

Refactoring

Jonas Björkert 1 year ago
parent
commit
772272f2ca

+ 4 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -70,6 +70,7 @@
 		DD608A0A2C23593900F91132 /* SMB.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A092C23593900F91132 /* SMB.swift */; };
 		DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */; };
 		DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */; };
+		DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7B0D432D730A320063DCB6 /* CycleHelper.swift */; };
 		DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19832ACDA50C00DBD158 /* Overrides.swift */; };
 		DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19852ACDA59700DBD158 /* BGCheck.swift */; };
 		DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19872ACDA5DA00DBD158 /* Notes.swift */; };
@@ -344,6 +345,7 @@
 		DD608A092C23593900F91132 /* SMB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMB.swift; sourceTree = "<group>"; };
 		DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundAlertManager.swift; sourceTree = "<group>"; };
 		DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusOpenAPS.swift; sourceTree = "<group>"; };
+		DD7B0D432D730A320063DCB6 /* CycleHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CycleHelper.swift; sourceTree = "<group>"; };
 		DD7E19832ACDA50C00DBD158 /* Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Overrides.swift; sourceTree = "<group>"; };
 		DD7E19852ACDA59700DBD158 /* BGCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGCheck.swift; sourceTree = "<group>"; };
 		DD7E19872ACDA5DA00DBD158 /* Notes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notes.swift; sourceTree = "<group>"; };
@@ -1098,6 +1100,7 @@
 		FCC688542489367300A0279D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				DD7B0D432D730A320063DCB6 /* CycleHelper.swift */,
 				DDF6999C2C5AAA4C0058A8D9 /* Views */,
 				FCC6886E2489A53800A0279D /* AppConstants.swift */,
 				FCC6886A24898FD800A0279D /* ObservationToken.swift */,
@@ -1449,6 +1452,7 @@
 				DD4878172C7B75350048F05C /* BolusView.swift in Sources */,
 				DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */,
 				FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */,
+				DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */,
 				DD9ACA0C2D33BB8600415D8A /* CalendarTask.swift in Sources */,
 				DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */,
 				DD0C0C702C4AFFE800DBADDF /* RemoteViewController.swift in Sources */,

+ 9 - 32
LoopFollow/BackgroundRefresh/BT/BLEManager.swift

@@ -220,43 +220,20 @@ extension BLEManager {
     /// The device’s lastSeen time is used (mod cycleDuration) to calculate the effective delay between when the sensor value
     /// becomes available and when the fetch is actually triggered.
     func expectedSensorFetchOffsetString(for device: BLEDevice) -> String? {
-        // Determine the device type using your BackgroundRefreshType matching.
-        guard let matchedType = BackgroundRefreshType.allCases.first(where: { $0.matches(device) }) else {
+        guard
+            let matchedType = BackgroundRefreshType.allCases.first(where: { $0.matches(device) }),
+            let heartBeatInterval = matchedType.heartBeatInterval,
+            let sensorOffset = Storage.shared.sensorScheduleOffset.value
+        else {
             return nil
         }
 
-        // We calculate this for Dexcom and RileyLink devices.
-        if matchedType == .dexcom || matchedType == .rileyLink {
-            // Return nil if the sensor schedule offset hasn't been set.
-            guard let sensorOffset = Storage.shared.sensorScheduleOffset.value else {
-                return nil
-            }
-
-            // Polling delay: use dynamic setting if enabled, otherwise the default.
-            let pollingDelay: TimeInterval = Double(UserDefaultsRepository.bgUpdateDelay.value)
-
-            // T_expected: the time (in seconds) after the sensor reading when the value is available.
-            let expectedOffset = sensorOffset + pollingDelay
+        let pollingDelay: TimeInterval = Double(UserDefaultsRepository.bgUpdateDelay.value)
 
-            // Determine the cycle duration based on the device type.
-            let cycleDuration: TimeInterval = (matchedType == .rileyLink) ? 60 : 300
+        let expectedOffset = sensorOffset + pollingDelay
 
-            // Compute the device’s heartbeat offset within the appropriate cycle.
-            let calendar = Calendar(identifier: .gregorian)
-            let startOfDay = calendar.startOfDay(for: device.lastSeen)
-            let heartbeatOffset = device.lastSeen.timeIntervalSince(startOfDay).truncatingRemainder(dividingBy: cycleDuration)
+        let effectiveDelay = CycleHelper.computeDelay(sensorOffset: expectedOffset, heartbeatLast: device.lastSeen, heartbeatInterval: heartBeatInterval)
 
-            // Calculate effective delay:
-            // If the heartbeat happens after the sensor value is available, delay = heartbeatOffset - expectedOffset.
-            // Otherwise, the fetch will occur on the next cycle:
-            // delay = (heartbeatOffset + cycleDuration) - expectedOffset.
-            let effectiveDelay: TimeInterval = (heartbeatOffset >= expectedOffset)
-            ? (heartbeatOffset - expectedOffset)
-            : (heartbeatOffset + cycleDuration - expectedOffset)
-
-            return "\(Int(effectiveDelay)) sec"
-        }
-        return nil
+        return "\(Int(effectiveDelay)) sec"
     }
 }
-

+ 11 - 0
LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift

@@ -23,6 +23,17 @@ enum BackgroundRefreshType: String, Codable, CaseIterable {
         }
     }
 
+    var heartBeatInterval: TimeInterval? {
+        switch self {
+        case .rileyLink:
+            return 60
+        case .dexcom:
+            return 5 * 60
+        case .silentTune, .none:
+            return nil
+        }
+    }
+
     /// Determines if a BLEDevice matches the specific device type
     func matches(_ device: BLEDevice) -> Bool {
         switch self {

+ 19 - 35
LoopFollow/Controllers/Nightscout/BGData.swift

@@ -8,6 +8,7 @@
 
 import Foundation
 import UIKit
+
 extension MainViewController {
     // Dex Share Web Call
     func webLoadDexShare() {
@@ -16,19 +17,19 @@ extension MainViewController {
         let graphHours = 24 * UserDefaultsRepository.downloadDays.value
         let count = graphHours * 12
         dexShare?.fetchData(count) { (err, result) -> () in
-            
+
             if let error = err {
                 LogManager.shared.log(category: .dexcom, message: "Error fetching Dexcom data: \(error.localizedDescription)")
                 self.webLoadNSBGData()
                 return
             }
-            
+
             guard let data = result else {
                 LogManager.shared.log(category: .dexcom, message: "Received nil data from Dexcom")
                 self.webLoadNSBGData()
                 return
             }
-            
+
             // If Dex data is old, load from NS instead
             let latestDate = data[0].date
             let now = dateTimeUtils.getNowTimeIntervalUTC()
@@ -37,7 +38,7 @@ extension MainViewController {
                 self.webLoadNSBGData()
                 return
             }
-            
+
             // Dexcom only returns 24 hrs of data. If we need more, call NS.
             if graphHours > 24 && IsNightscoutEnabled() {
                 self.webLoadNSBGData(dexData: data)
@@ -62,7 +63,7 @@ extension MainViewController {
 
         // Exclude 'cal' entries
         parameters["find[type][$ne]"] = "cal"
-        
+
         NightscoutUtils.executeRequest(eventType: .sgv, parameters: parameters) { (result: Result<[ShareGlucoseData], Error>) in
             switch result {
             case .success(let entriesResponse):
@@ -119,7 +120,7 @@ extension MainViewController {
             }
         }
     }
-    
+
     /// Processes incoming BG data.
     func ProcessDexBGData(data: [ShareGlucoseData], sourceName: String) {
         let graphHours = 24 * UserDefaultsRepository.downloadDays.value
@@ -135,11 +136,8 @@ extension MainViewController {
         // secondsAgo is how old the newest reading is
         let secondsAgo = now - sensorTimestamp
 
-        // Determine the cycle duration based on device type.
-        let cycleDuration: TimeInterval = (Storage.shared.backgroundRefreshType.value == .rileyLink) ? 60 : 300
-
-        // Compute the current sensor schedule offset using the appropriate cycle.
-        let currentOffset = sensorScheduleOffset(for: sensorTimestamp, cycle: cycleDuration)
+        // Compute the current sensor schedule offset
+        let currentOffset = CycleHelper.cycleOffset(for: sensorTimestamp, interval: 5 * 60)
 
         if Storage.shared.sensorScheduleOffset.value != currentOffset {
             Storage.shared.sensorScheduleOffset.value = currentOffset
@@ -214,20 +212,6 @@ extension MainViewController {
         viewUpdateNSBG(sourceName: sourceName)
     }
 
-    /// Computes the sensor schedule offset (in seconds) for a given time interval.
-    /// The offset is the remainder (in seconds) of the time elapsed since midnight (UTC)
-    /// divided by the given cycle length (default 300 seconds).
-    func sensorScheduleOffset(for timeInterval: TimeInterval, cycle: TimeInterval = 300) -> TimeInterval {
-        var calendar = Calendar(identifier: .gregorian)
-        // Use UTC to be consistent with our sensor timestamps.
-        calendar.timeZone = TimeZone(secondsFromGMT: 0)!
-
-        let date = Date(timeIntervalSince1970: timeInterval)
-        let startOfDay = calendar.startOfDay(for: date)
-        let secondsSinceStartOfDay = date.timeIntervalSince(startOfDay)
-        return secondsSinceStartOfDay.truncatingRemainder(dividingBy: cycle)
-    }
-
     func updateServerText(with serverText: String? = nil) {
         if UserDefaultsRepository.showDisplayName.value, let displayName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String {
             self.serverText.text = displayName
@@ -235,7 +219,7 @@ extension MainViewController {
             self.serverText.text = serverText
         }
     }
-    
+
     // NS BG Data Front end updater
     func viewUpdateNSBG(sourceName: String) {
         DispatchQueue.main.async {
@@ -243,28 +227,28 @@ extension MainViewController {
 
             let entries = self.bgData
             if entries.count < 2 { return } // Protect index out of bounds
-            
+
             self.updateBGGraph()
             self.updateStats()
-            
+
             let latestEntryIndex = entries.count - 1
             let latestBG = entries[latestEntryIndex].sgv
             let priorBG = entries[latestEntryIndex - 1].sgv
             let deltaBG = latestBG - priorBG
             let lastBGTime = entries[latestEntryIndex].date
-            
-            let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - lastBGTime) / 60            
+
+            let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - lastBGTime) / 60
             self.updateServerText(with: sourceName)
-            
+
             var snoozerBG = ""
             var snoozerDirection = ""
             var snoozerDelta = ""
-            
+
             // Set BGText with the latest BG value
             self.BGText.text = Localizer.toDisplayUnits(String(latestBG))
             snoozerBG = Localizer.toDisplayUnits(String(latestBG))
             self.setBGTextColor()
-            
+
             // Direction handling
             if let directionBG = entries[latestEntryIndex].direction {
                 self.DirectionText.text = self.bgDirectionGraphic(directionBG)
@@ -275,7 +259,7 @@ extension MainViewController {
                 snoozerDirection = ""
                 self.latestDirectionString = ""
             }
-            
+
             // Delta handling
             if deltaBG < 0 {
                 self.latestDeltaString = Localizer.toDisplayUnits(String(deltaBG))
@@ -298,7 +282,7 @@ extension MainViewController {
                 self.updateBadge(val: latestBG)
             }
             self.BGText.attributedText = attributeString
-            
+
             // Snoozer Display
             guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
             snoozer.BGLabel.text = snoozerBG

+ 52 - 0
LoopFollow/Helpers/CycleHelper.swift

@@ -0,0 +1,52 @@
+//
+//  CycleHelper.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-03-01.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+struct CycleHelper {
+    /// Returns a positive modulus value (always between 0 and modulus).
+    static func positiveModulo(_ value: TimeInterval, modulus: TimeInterval) -> TimeInterval {
+        let remainder = value.truncatingRemainder(dividingBy: modulus)
+        return remainder < 0 ? remainder + modulus : remainder
+    }
+
+    /// Calculates the cycle offset for a given date relative to midnight.
+    /// The offset is the number of seconds into the cycle (i.e., date mod interval).
+    static func cycleOffset(for date: Date, interval: TimeInterval) -> TimeInterval {
+        let calendar = Calendar.current
+        let startOfDay = calendar.startOfDay(for: date)
+        let secondsSinceMidnight = date.timeIntervalSince(startOfDay)
+        return secondsSinceMidnight.truncatingRemainder(dividingBy: interval)
+    }
+
+    /// Same as above, but takes a timestamp (seconds since 1970) instead of a Date.
+    static func cycleOffset(for timestamp: TimeInterval, interval: TimeInterval) -> TimeInterval {
+        let date = Date(timeIntervalSince1970: timestamp)
+        return cycleOffset(for: date, interval: interval)
+    }
+
+    /// Computes the delay experienced when using a heartbeat device to read a sensor value.
+    /// The calculation is based on a sensor reference (Date) and sensor interval.
+    /// All calculations assume midnight as the base reference.
+    static func computeDelay(sensorReference: Date,
+                             sensorInterval: TimeInterval,
+                             heartbeatLast: Date,
+                             heartbeatInterval: TimeInterval) -> TimeInterval {
+        let sensorOffset = cycleOffset(for: sensorReference, interval: sensorInterval)
+        let hbOffset = cycleOffset(for: heartbeatLast, interval: heartbeatInterval)
+        return positiveModulo(hbOffset - sensorOffset, modulus: heartbeatInterval)
+    }
+
+    /// Overloaded version of computeDelay where the sensor cycle offset is already known.
+    static func computeDelay(sensorOffset: TimeInterval,
+                             heartbeatLast: Date,
+                             heartbeatInterval: TimeInterval) -> TimeInterval {
+        let hbOffset = cycleOffset(for: heartbeatLast, interval: heartbeatInterval)
+        return positiveModulo(hbOffset - sensorOffset, modulus: heartbeatInterval)
+    }
+}