Просмотр исходного кода

Merge branch 'dev' into dash-heartbeat

marionbarker 1 год назад
Родитель
Сommit
f0ef4b651a

+ 1 - 1
LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift

@@ -73,7 +73,7 @@ struct BackgroundRefreshSettingsView: View {
                         .foregroundColor(.secondary)
 
                 case .dexcom:
-                    Text("Requires a Dexcom G6/ONE/G7/ONE+ transmitter within Bluetooth range. Provides updates every 5 minutes and uses less battery than the silent tune method.")
+                    Text("Requires a Dexcom G6/ONE/G7/ONE+ transmitter within Bluetooth range. Provides updates every 5 minutes and uses less battery than the silent tune method. If you have more than one device to choose from, select the one with the smallest expected bg delay.")
                         .font(.footnote)
                         .foregroundColor(.secondary)
 

+ 2 - 16
LoopFollow/Controllers/Alarms.swift

@@ -5,20 +5,12 @@
 //  Created by Jon Fawcett on 6/16/20.
 //  Copyright © 2020 Jon Fawcett. All rights reserved.
 //
-//
-//
-//
-//
-//
-
 
 import Foundation
 import AVFoundation
 import CallKit
 
 extension MainViewController {
-    
-    
     func checkAlarms(bgs: [ShareGlucoseData]) {
         // Don't check or fire alarms within 1 minute of prior alarm
         if checkAlarmTimer.isValid {  return }
@@ -56,7 +48,6 @@ extension MainViewController {
         
         
         let currentBGTime = bgs[bgs.count - 1].date
-        var alarmTriggered = false
         var numLoops = 0
         var playSound = true
         checkQuietHours()
@@ -401,7 +392,6 @@ extension MainViewController {
                     // Get the latest bolus over the small bolus exclusion
                     // Start with 0.0 bolus assuming there isn't one to cause a trigger and only add one if found
                     var lastBolus = 0.0
-                    var lastBolusTime = 0.0
                     var i = 1
                     // check the boluses in reverse order setting it only if the time is after the carb time minus prebolus time.
                     // This will make the loop stop at the most recent bolus that is over the minimum value or continue through all boluses
@@ -409,7 +399,6 @@ extension MainViewController {
                         // Set the bolus if it's after the carb time minus prebolus time
                         if (bolusData[bolusData.count - i].date >= lastCarbTime - Double(UserDefaultsRepository.alertMissedBolusPrebolus.value * 60)) {
                             lastBolus = bolusData[bolusData.count - i].value
-                            lastBolusTime = bolusData[bolusData.count - i].date
                         }
                         i += 1
                     }
@@ -693,6 +682,8 @@ extension MainViewController {
     
     func triggerAlarm(sound: String, snooozedBGReadingTime: TimeInterval?, overrideVolume: Bool, numLoops: Int, snoozeTime: Int = 0, snoozeIncrement: Int = 5, audio: Bool = true)
     {
+        LogManager.shared.log(category: .alarm, message: "Alarm triggered: \(AlarmSound.whichAlarm)")
+
         var audioDuringCall = true
         if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false }
         
@@ -909,13 +900,8 @@ extension MainViewController {
     func checkQuietHours() {
         if UserDefaultsRepository.quietHourStart.value == nil || UserDefaultsRepository.quietHourEnd.value == nil { return }
         
-        var startDateComponents = DateComponents()
-        
         let today = Date()
         let todayCalendar = Calendar.current
-        let month = todayCalendar.component(.month, from: today)
-        let day = todayCalendar.component(.day, from: today)
-        let year = todayCalendar.component(.year, from: today)
         let hour = todayCalendar.component(.hour, from: today)
         let minute = todayCalendar.component(.minute, from: today)
         let todayMinutes = (60 * hour) + minute

+ 26 - 12
LoopFollow/Controllers/BackgroundAlertManager.swift

@@ -11,7 +11,7 @@ import UserNotifications
 
 /// Enum representing different background alert durations.
 enum BackgroundAlertDuration: TimeInterval, CaseIterable {
-    case sixMinutes = 360    // 6 minutes in seconds
+    case sixMinutes = 360 // 6 minutes in seconds
     case twelveMinutes = 720 // 12 minutes in seconds
     case eighteenMinutes = 1080 // 18 minutes in seconds
 }
@@ -34,10 +34,14 @@ class BackgroundAlertManager {
     /// Title prefix for all background refresh notifications.
     private let notificationTitlePrefix = "LoopFollow Background Refresh"
 
+    /// Timestamp of the last scheduled background alert.
+    private var lastScheduleDate: Date?
+
     /// Start scheduling background alerts.
     func startBackgroundAlert() {
         isAlertScheduled = true
-        scheduleBackgroundAlert()
+        // Force execution to bypass throttle when starting
+        scheduleBackgroundAlert(force: true)
     }
 
     /// Stop all scheduled background alerts.
@@ -48,11 +52,21 @@ class BackgroundAlertManager {
     }
 
     /// (Re)schedule all background alerts based on predefined durations.
-    func scheduleBackgroundAlert() {
-        removeDeliveredNotifications()
-
+    /// - Parameter force: When true, the scheduling is executed regardless of throttle constraints.
+    func scheduleBackgroundAlert(force: Bool = false) {
         guard isAlertScheduled, Storage.shared.backgroundRefreshType.value != .none else { return }
 
+        // Throttle execution if not forced: only run once every 10 seconds.
+        if !force {
+            let now = Date()
+            if let lastDate = lastScheduleDate, now.timeIntervalSince(lastDate) < 10 {
+                return
+            }
+            lastScheduleDate = now
+        }
+
+        removeDeliveredNotifications()
+
         let isBluetoothActive = Storage.shared.backgroundRefreshType.value.isBluetooth
         let expectedHeartbeat = BLEManager.shared.expectedHeartbeatInterval()
 
@@ -62,22 +76,22 @@ class BackgroundAlertManager {
                 identifier: BackgroundAlertIdentifier.sixMin.rawValue,
                 timeInterval: BackgroundAlertDuration.sixMinutes.rawValue,
                 body: isBluetoothActive
-                ? "App inactive for 6 minutes. Verify Bluetooth connectivity."
-                : "App inactive for 6 minutes. Open to resume."
+                    ? "App inactive for 6 minutes. Verify Bluetooth connectivity."
+                    : "App inactive for 6 minutes. Open to resume."
             ),
             BackgroundAlert(
                 identifier: BackgroundAlertIdentifier.twelveMin.rawValue,
                 timeInterval: BackgroundAlertDuration.twelveMinutes.rawValue,
                 body: isBluetoothActive
-                ? "App inactive for 12 minutes. Verify Bluetooth connectivity."
-                : "App inactive for 12 minutes. Open to resume."
+                    ? "App inactive for 12 minutes. Verify Bluetooth connectivity."
+                    : "App inactive for 12 minutes. Open to resume."
             ),
             BackgroundAlert(
                 identifier: BackgroundAlertIdentifier.eighteenMin.rawValue,
                 timeInterval: BackgroundAlertDuration.eighteenMinutes.rawValue,
                 body: isBluetoothActive
-                ? "App inactive for 18 minutes. Verify Bluetooth connectivity."
-                : "App inactive for 18 minutes. Open to resume."
+                    ? "App inactive for 18 minutes. Verify Bluetooth connectivity."
+                    : "App inactive for 18 minutes. Open to resume."
             )
         ]
 
@@ -119,7 +133,7 @@ class BackgroundAlertManager {
         UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
     }
 
-    /// Remove all delivered notifications
+    /// Remove all delivered notifications.
     private func removeDeliveredNotifications() {
         let identifiers = BackgroundAlertIdentifier.allCases.map { $0.rawValue }
         UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)

+ 5 - 5
LoopFollow/Controllers/Nightscout/BGData.swift

@@ -19,13 +19,13 @@ extension MainViewController {
         dexShare?.fetchData(count) { (err, result) -> () in
 
             if let error = err {
-                LogManager.shared.log(category: .dexcom, message: "Error fetching Dexcom data: \(error.localizedDescription)")
+                LogManager.shared.log(category: .dexcom, message: "Error fetching Dexcom data: \(error.localizedDescription)", limitIdentifier: "Error fetching Dexcom data")
                 self.webLoadNSBGData()
                 return
             }
 
             guard let data = result else {
-                LogManager.shared.log(category: .dexcom, message: "Received nil data from Dexcom")
+                LogManager.shared.log(category: .dexcom, message: "Received nil data from Dexcom", limitIdentifier: "Received nil data from Dexcom")
                 self.webLoadNSBGData()
                 return
             }
@@ -34,7 +34,7 @@ extension MainViewController {
             let latestDate = data[0].date
             let now = dateTimeUtils.getNowTimeIntervalUTC()
             if (latestDate + 330) < now && IsNightscoutEnabled() {
-                LogManager.shared.log(category: .dexcom, message: "Dexcom data is old, loading from NS instead")
+                LogManager.shared.log(category: .dexcom, message: "Dexcom data is old, loading from NS instead", limitIdentifier: "Dexcom data is old, loading from NS instead")
                 self.webLoadNSBGData()
                 return
             }
@@ -105,7 +105,7 @@ extension MainViewController {
                     self.ProcessDexBGData(data: nsData2, sourceName: sourceName)
                 }
             case .failure(let error):
-                LogManager.shared.log(category: .nightscout, message: "Failed to fetch data: \(error)")
+                LogManager.shared.log(category: .nightscout, message: "Failed to fetch bg data: \(error)", limitIdentifier: "Failed to fetch bg data")
                 DispatchQueue.main.async {
                     TaskScheduler.shared.rescheduleTask(
                         id: .fetchBG,
@@ -126,7 +126,7 @@ extension MainViewController {
         let graphHours = 24 * UserDefaultsRepository.downloadDays.value
 
         guard !data.isEmpty else {
-            LogManager.shared.log(category: .nightscout, message: "No bg data received. Skipping processing.")
+            LogManager.shared.log(category: .nightscout, message: "No bg data received. Skipping processing.", limitIdentifier: "No bg data received. Skipping processing.")
             return
         }
 

+ 3 - 3
LoopFollow/Controllers/Nightscout/DeviceStatus.swift

@@ -30,12 +30,11 @@ extension MainViewController {
     }
 
     private func handleDeviceStatusError() {
-        LogManager.shared.log(category: .deviceStatus, message: "Device status fetch failed!")
+        LogManager.shared.log(category: .deviceStatus, message: "Device status fetch failed!", limitIdentifier: "Device status fetch failed!")
         DispatchQueue.main.async {
             TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date().addingTimeInterval(10))
+            self.evaluateNotLooping()
         }
-
-        evaluateNotLooping()
     }
     
     func evaluateNotLooping() {
@@ -86,6 +85,7 @@ extension MainViewController {
 
         if jsonDeviceStatus.count == 0 {
             LogManager.shared.log(category: .deviceStatus, message: "Device status is empty")
+            TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date().addingTimeInterval(5 * 60))
             return
         }
         

+ 29 - 8
LoopFollow/Log/LogManager.swift

@@ -15,6 +15,9 @@ class LogManager {
     private let logDirectory: URL
     private let dateFormatter: DateFormatter
     private let consoleQueue = DispatchQueue(label: "com.loopfollow.log.console", qos: .background)
+    
+    private let rateLimitQueue = DispatchQueue(label: "com.loopfollow.log.ratelimit")
+    private var lastLoggedTimestamps: [String: Date] = [:]
 
     enum Category: String, CaseIterable {
         case bluetooth = "Bluetooth"
@@ -30,26 +33,46 @@ class LogManager {
     }
 
     init() {
-        // Create log directory in the app's Documents folder
         let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
         logDirectory = documentsDirectory.appendingPathComponent("Logs")
-
-        // Ensure the directory exists
         if !fileManager.fileExists(atPath: logDirectory.path) {
             try? fileManager.createDirectory(at: logDirectory, withIntermediateDirectories: true, attributes: nil)
         }
-
         dateFormatter = DateFormatter()
         dateFormatter.dateFormat = "yyyy-MM-dd"
     }
-
-    func log(category: Category, message: String, isDebug: Bool = false) {
+    
+    /// Logs a message with an optional rate limit.
+    ///
+    /// - Parameters:
+    ///   - category: The log category.
+    ///   - message: The message to log.
+    ///   - isDebug: Indicates if this is a debug log.
+    ///   - limitIdentifier: Optional key to rate-limit similar log messages.
+    ///   - limitInterval: Time interval (in seconds) to wait before logging the same type again.
+    func log(category: Category, message: String, isDebug: Bool = false, limitIdentifier: String? = nil, limitInterval: TimeInterval = 300) {
         let timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
         let logMessage = "[\(timestamp)] [\(category.rawValue)] \(message)"
 
         consoleQueue.async {
             print(logMessage)
         }
+        
+        if let key = limitIdentifier, !Storage.shared.debugLogLevel.value {
+            let shouldLog: Bool = rateLimitQueue.sync {
+                if let lastLogged = lastLoggedTimestamps[key] {
+                    let interval = Date().timeIntervalSince(lastLogged)
+                    if interval < limitInterval {
+                        return false
+                    }
+                }
+                lastLoggedTimestamps[key] = Date()
+                return true
+            }
+            if !shouldLog {
+                return
+            }
+        }
 
         if !isDebug || Storage.shared.debugLogLevel.value {
             let logFileURL = self.currentLogFileURL
@@ -60,7 +83,6 @@ class LogManager {
     func cleanupOldLogs() {
         let today = dateFormatter.string(from: Date())
         let yesterday = dateFormatter.string(from: Calendar.current.date(byAdding: .day, value: -1, to: Date())!)
-
         do {
             let logFiles = try fileManager.contentsOfDirectory(at: logDirectory, includingPropertiesForKeys: nil)
             for logFile in logFiles {
@@ -93,7 +115,6 @@ class LogManager {
         if !fileManager.fileExists(atPath: fileURL.path) {
             fileManager.createFile(atPath: fileURL.path, contents: nil, attributes: nil)
         }
-
         if let fileHandle = try? FileHandle(forWritingTo: fileURL) {
             defer { fileHandle.closeFile() }
             fileHandle.seekToEndOfFile()

+ 9 - 2
LoopFollow/Task/TaskScheduler.swift

@@ -107,7 +107,7 @@ class TaskScheduler {
                 continue
             }
 
-            // Check if we should skip alarmCheck
+            // Check if we should re-schedule alarmCheck till after other tasks are done
             if taskID == .alarmCheck {
                 let shouldSkip = tasksToSkipAlarmCheck.contains {
                     guard let checkTask = tasks[$0] else { return false }
@@ -115,7 +115,14 @@ class TaskScheduler {
                 }
 
                 if shouldSkip {
-                    //LogManager.shared.log(category: .taskScheduler, message: "Skipping alarmCheck because one of the specified tasks is due or set to distant future.")
+                    //LogManager.shared.log(category: .taskScheduler, message: "Skipping alarmCheck because one of the specified tasks is due or set to distant future.", isDebug: true)
+
+                    guard var existingTask = self.tasks[taskID] else {
+                        continue
+                    }
+                    existingTask.nextRun = Date().addingTimeInterval(5)
+                    self.tasks[taskID] = existingTask
+
                     continue
                 }
             }