浏览代码

Merge pull request #276 from loopandlearn/bg-voice-alerts-refinement

Implement customizable BG speech alerts based on user-defined conditions
Marion Barker 2 年之前
父节点
当前提交
ad546ff869

+ 128 - 9
LoopFollow/Controllers/Alarms.swift

@@ -861,7 +861,119 @@ extension MainViewController {
         }
         
     }
+
+    func evaluateSpeakConditions(currentValue: Int, previousValue: Int) {
+        if !UserDefaultsRepository.speakBG.value {
+            return
+        }
+        
+        let always = UserDefaultsRepository.speakBGAlways.value
+        let lowThreshold = UserDefaultsRepository.speakLowBGLimit.value
+        let fastDropDelta = UserDefaultsRepository.speakFastDropDelta.value
+        let highThreshold = UserDefaultsRepository.speakHighBGLimit.value
+        let speakLowBG = UserDefaultsRepository.speakLowBG.value
+        let speakProactiveLowBG = UserDefaultsRepository.speakProactiveLowBG.value
+        let speakHighBG = UserDefaultsRepository.speakHighBG.value
+        
+        // Speak always
+        if always {
+            speakBG(currentValue: currentValue, previousValue: previousValue)
+            print("Speaking because 'Always' is enabled.")
+            return
+        }
+        
+        // Speak if low or last value was low
+        if speakLowBG {
+            if currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) {
+                speakBG(currentValue: currentValue, previousValue: previousValue)
+                print("Speaking because of 'Low' condition.")
+                return
+            }
+        }
+        
+        // Speak predictive low if...
+        // * low or last value was low
+        // * next predictive value is low
+        // * fast drop occurs below high
+        if speakProactiveLowBG {
+            let predictiveTrigger = !predictionData.isEmpty && Float(predictionData.first!.sgv) <= lowThreshold
+            
+            if predictiveTrigger ||
+                currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) ||
+                ((currentValue <= Int(highThreshold) && (previousValue - currentValue) >= Int(fastDropDelta))) {
+                speakBG(currentValue: currentValue, previousValue: previousValue)
+                print("Speaking because of 'Proactive Low' condition. Predictive trigger: \(predictiveTrigger)")
+                return
+            }
+        }
+        
+        //Speak if high or if last value was high
+        if speakHighBG {
+            if currentValue >= Int(highThreshold) || previousValue >= Int(highThreshold) {
+                speakBG(currentValue: currentValue, previousValue: previousValue)
+                print("Speaking because of 'High' condition.")
+                return
+            }
+        }
+        
+        print("No condition met for speaking.")
+    }
     
+    struct AnnouncementTexts {
+        var stable: String
+        var increase: String
+        var decrease: String
+        var currentBGIs: String
+        
+        static func forLanguage(_ language: String) -> AnnouncementTexts {
+            switch language {
+            case "it":
+                return AnnouncementTexts(
+                    stable: "ed è stabile",
+                    increase: "ed è salita di",
+                    decrease: "ed è scesa di",
+                    currentBGIs: "Glicemia attuale è"
+                )
+            case "sk":
+                return AnnouncementTexts(
+                    stable: "a je stabilná",
+                    increase: "a stúpla o",
+                    decrease: "a klesla o",
+                    currentBGIs: "Aktuálna glykémia je"
+                )
+            case "sv":
+                return AnnouncementTexts(
+                    stable: "och det är stabilt",
+                    increase: "och det har ökat med",
+                    decrease: "och det har minskat med",
+                    currentBGIs: "Blodsockret är"
+                )
+            case "en": fallthrough
+            default:
+                return AnnouncementTexts(
+                    stable: "and it is stable",
+                    increase: "and it is up",
+                    decrease: "and it is down",
+                    currentBGIs: "Glucose is"
+                )
+            }
+        }
+    }
+    
+    struct LanguageVoiceMapping {
+        static let voiceLanguageMap: [String: String] = [
+            "en": "en-US",
+            "it": "it-IT",
+            "sk": "sk-SK",
+            "sv": "sv-SE"
+        ]
+        
+        static func voiceLanguageCode(forAppLanguage appLanguage: String) -> String {
+            return voiceLanguageMap[appLanguage, default: "en-US"]
+        }
+    }
+
+
     // Speaks the current blood glucose value and the change from the previous value.
     // Repeated calls to the function within 30 seconds are prevented.
     func speakBG(currentValue: Int, previousValue: Int) {
@@ -880,21 +992,28 @@ extension MainViewController {
         self.lastSpeechTime = currentTime
 
         let bloodGlucoseDifference = currentValue - previousValue
-        let negligibleThreshold = 3
-        let differenceText: String
 
+        let preferredLanguage = UserDefaultsRepository.speakLanguage.value
+        let voiceLanguageCode = LanguageVoiceMapping.voiceLanguageCode(forAppLanguage: preferredLanguage)
+
+        let texts = AnnouncementTexts.forLanguage(preferredLanguage)
+        
+        let negligibleThreshold = 3
+        let absoluteDifference = bgUnits.toDisplayUnits(String(abs(bloodGlucoseDifference)))
+        let announcementText: String
+        
         if abs(bloodGlucoseDifference) <= negligibleThreshold {
-            differenceText = "stable"
+            announcementText = "\(texts.currentBGIs) \(bgUnits.toDisplayUnits(String(currentValue))) \(texts.stable)"
         } else {
-            let direction = bloodGlucoseDifference < 0 ? "down" : "up"
-            let absoluteDifference = bgUnits.toDisplayUnits(String(abs(bloodGlucoseDifference)))
-            differenceText = "\(direction) \(absoluteDifference)"
+            let directionText = bloodGlucoseDifference < 0 ? texts.decrease : texts.increase
+            announcementText = "\(texts.currentBGIs) \(bgUnits.toDisplayUnits(String(currentValue))) \(directionText) \(absoluteDifference)"
         }
-
-        let announcementText = "Current BG is \(bgUnits.toDisplayUnits(String(currentValue))), and it is \(differenceText)"
+        
         let speechUtterance = AVSpeechUtterance(string: announcementText)
-        speechUtterance.voice = AVSpeechSynthesisVoice(language: "en-US")
+        speechUtterance.voice = AVSpeechSynthesisVoice(language: voiceLanguageCode)
+        
         speechSynthesizer.speak(speechUtterance)
+
     }
     
     func isOnPhoneCall() -> Bool {

+ 1 - 3
LoopFollow/Controllers/Nightscout/BGData.swift

@@ -177,9 +177,7 @@ extension MainViewController {
                 let timerVal = 310 - secondsAgo
                 print("##### started 5:10 bg timer: \(timerVal)")
                 self.updateBadge(val: data[0].sgv)
-                if UserDefaultsRepository.speakBG.value {
-                    self.speakBG(currentValue: data[0].sgv, previousValue: data[1].sgv)
-                }
+                self.evaluateSpeakConditions(currentValue: data[0].sgv, previousValue: data[1].sgv)
             }
         }
         

+ 182 - 16
LoopFollow/ViewControllers/GeneralSettingsViewController.swift

@@ -136,21 +136,7 @@ class GeneralSettingsViewController: FormViewController {
                 guard let value = row.value else { return }
                 UserDefaultsRepository.screenlockSwitchState.value = value
             }
-        
-        
-            
-        
-        <<< SwitchRow("speakBG"){ row in
-            row.title = "Speak BG"
-            row.value = UserDefaultsRepository.speakBG.value
-        }.onChange { [weak self] row in
-                    guard let value = row.value else { return }
-                    UserDefaultsRepository.speakBG.value = value
-
-                    // Post a "toggleSpeakBG" notification whenever the "Speak BG" switch value changes in the General Settings. This allows the SceneDelegate to update the Home Screen Quick Action appearance and ensures consistency between the switch and the Quick Action.
-                    NotificationCenter.default.post(name: Notification.Name("toggleSpeakBG"), object: nil)
-        }
-        
+       
        <<< SwitchRow("showDisplayName") { row in
            row.title = "Show Display Name"
            row.value = UserDefaultsRepository.showDisplayName.value
@@ -163,14 +149,194 @@ class GeneralSettingsViewController: FormViewController {
                appState.generalSettingsChanges |= GeneralSettingsChangeEnum.showDisplayNameChange.rawValue
            }
        }
+        
+       +++ Section("Speak BG Settings")
+       <<< SwitchRow("speakBG") { row in
+           row.title = "Speak BG"
+           row.value = UserDefaultsRepository.speakBG.value
+       }.onChange { [weak self] row in
+           guard let value = row.value else { return }
+           UserDefaultsRepository.speakBG.value = value
+           self?.updateSpeakBGSettingsVisibility()
+       }
+       
+       <<< PushRow<String>("speakLanguage") { row in
+           row.title = "Speaking Language"
+           row.options = ["en", "it", "sk", "sv"]
+           row.value = UserDefaultsRepository.speakLanguage.value
+           row.displayValueFor = { value in
+               switch value {
+               case "en": return "English"
+               case "it": return "Italian"
+               case "sk": return "Slovak"
+               case "sv": return "Swedish"
+               default: return "Unknown"
+               }
+           }
+           row.hidden = Condition.function(["speakBG"], { form in
+               let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG")
+               return !(speakBGRow.value ?? false)
+           })
+       }.onChange { row in
+           guard let value = row.value else { return }
+           UserDefaultsRepository.speakLanguage.value = value
+       }
+
+       <<< SwitchRow("speakBGAlways") { row in
+           row.title = "Always"
+           row.value = UserDefaultsRepository.speakBGAlways.value
+       }.onChange { [weak self] row in
+           guard let value = row.value else { return }
+           UserDefaultsRepository.speakBGAlways.value = value
+           self?.updateSpeakBGSettingsVisibility()
+       }
+       
+       <<< SwitchRow("speakLowBG") { row in
+           row.title = "Low"
+           row.value = UserDefaultsRepository.speakLowBG.value
+       }.onChange { [weak self] row in
+           self?.handleLowProactiveLowToggle(row: row, opposingRowTag: "speakProactiveLowBG")
+       }
+       
+       <<< SwitchRow("speakProactiveLowBG") { row in
+           row.title = "Proactive Low"
+           row.value = UserDefaultsRepository.speakProactiveLowBG.value
+       }.onChange { [weak self] row in
+           self?.handleLowProactiveLowToggle(row: row, opposingRowTag: "speakLowBG")
+       }
+       
+       <<< StepperRow("speakLowBGLimit") { row in
+           row.title = "Low BG Limit"
+           row.cell.stepper.stepValue = 1
+           row.cell.stepper.minimumValue = 40
+           row.cell.stepper.maximumValue = 108
+           row.value = Double(UserDefaultsRepository.speakLowBGLimit.value)
+           row.displayValueFor = { value in
+               guard let value = value else { return nil }
+               return bgUnits.toDisplayUnits(String(value))
+           }
+           // Visibility depends on either 'speakLowBG' or 'speakProactiveLowBG' being true
+           row.hidden = Condition.function(["speakLowBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in
+               let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG")
+               let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways")
+               let speakLowBGRow: SwitchRow! = form.rowBy(tag: "speakLowBG")
+               let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG")
+               return !(speakLowBGRow.value ?? false) && !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false)
+           })
+       }.onChange { [weak self] row in
+           guard let value = row.value else { return }
+           UserDefaultsRepository.speakLowBGLimit.value = Float(value)
+       }
+
+       <<< StepperRow("speakFastDropDelta") { row in
+           row.title = "Fast Drop Delta"
+           row.cell.stepper.stepValue = 1
+           row.cell.stepper.minimumValue = 3
+           row.cell.stepper.maximumValue = 20
+           row.value = Double(UserDefaultsRepository.speakFastDropDelta.value)
+           row.displayValueFor = { value in
+               guard let value = value else { return nil }
+               return bgUnits.toDisplayUnits(String(value))
+           }
+           // Visibility depends on 'speakProactiveLowBG' being true
+           row.hidden = Condition.function(["speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in
+               let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG")
+               let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways")
+               let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG")
+               return !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false)
+           })
+       }.onChange { [weak self] row in
+           guard let value = row.value else { return }
+           UserDefaultsRepository.speakFastDropDelta.value = Float(value)
+       }
+       
+       <<< SwitchRow("speakHighBG") { row in
+           row.title = "High"
+           row.value = UserDefaultsRepository.speakHighBG.value
+       }.onChange { row in
+           guard let value = row.value else { return }
+           UserDefaultsRepository.speakHighBG.value = value
+       }
+       
+       <<< StepperRow("speakHighBGLimit") { row in
+           row.title = "High BG Limit"
+           row.cell.stepper.stepValue = 1
+           row.cell.stepper.minimumValue = 140
+           row.cell.stepper.maximumValue = 300
+           row.value = Double(UserDefaultsRepository.speakHighBGLimit.value)
+           row.displayValueFor = { value in
+               guard let value = value else { return nil }
+               return bgUnits.toDisplayUnits(String(value))
+           }
+           // Visibility depends on 'speakHighBG' or 'speakProactiveLowBG' being true
+           row.hidden = Condition.function(["speakHighBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in
+               let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG")
+               let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways")
+               let speakHighBGRow: SwitchRow! = form.rowBy(tag: "speakHighBG")
+               let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG")
+               return !(speakHighBGRow.value ?? false) && !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false)
+           })
+       }.onChange { [weak self] row in
+           guard let value = row.value else { return }
+           UserDefaultsRepository.speakHighBGLimit.value = Float(value)
+       }
 
        +++ ButtonRow() {
           $0.title = "DONE"
        }.onCellSelection { (row, arg)  in
           self.dismiss(animated:true, completion: nil)
        }
+       
+       // Call to update initial visibility based on current settings
+       updateSpeakBGSettingsVisibility()
     }
-   
+    
+    func updateSpeakBGSettingsVisibility() {
+        let alwaysOn = UserDefaultsRepository.speakBGAlways.value
+        let speakBGOn = UserDefaultsRepository.speakBG.value
+        
+        // Determine visibility for "Always", "Low", "Proactive Low", and "High" based on "Speak BG" and "Always"
+        let shouldHideAlways = !speakBGOn
+        let shouldHideSettings = alwaysOn || !speakBGOn
+        
+        form.rowBy(tag: "speakBGAlways")?.hidden = Condition(booleanLiteral: shouldHideAlways)
+        form.rowBy(tag: "speakBGAlways")?.evaluateHidden()
+        
+        ["speakLowBG", "speakProactiveLowBG", "speakHighBG"].forEach { tag in
+            if let row = form.rowBy(tag: tag) {
+                row.hidden = Condition(booleanLiteral: shouldHideSettings)
+                row.evaluateHidden()
+                row.updateCell()
+            }
+        }
+    }
+    
+    func handleLowProactiveLowToggle(row: BaseRow, opposingRowTag: String) {
+        guard let switchRow = row as? SwitchRow, let value = switchRow.value else { return }
+        
+        // Update the UserDefaults value for the current row.
+        if row.tag == "speakLowBG" {
+            UserDefaultsRepository.speakLowBG.value = value
+        } else if row.tag == "speakProactiveLowBG" {
+            UserDefaultsRepository.speakProactiveLowBG.value = value
+        }
+        
+        // If the current switch is being turned ON, turn the opposing switch OFF.
+        if value {
+            if let opposingRow = form.rowBy(tag: opposingRowTag) as? SwitchRow {
+                opposingRow.value = false
+                opposingRow.updateCell()
+                
+                // Update the UserDefaults value for the opposing row.
+                if opposingRowTag == "speakLowBG" {
+                    UserDefaultsRepository.speakLowBG.value = false
+                } else if opposingRowTag == "speakProactiveLowBG" {
+                    UserDefaultsRepository.speakProactiveLowBG.value = false
+                }
+            }
+        }
+    }
+        
     // Update the "Speak BG" SwitchRow value in the General Settings when the app enters the foreground. This ensures that the switch reflects the current setting in UserDefaultsRepository, even if it was changed using the Home Screen Quick Action while the app was in the background.
     @objc func handleAppWillEnterForeground() {
         if let row = self.form.rowBy(tag: "speakBG") as? SwitchRow {

+ 1 - 0
LoopFollow/ViewControllers/MainViewController.swift

@@ -277,6 +277,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         restartAllTimers()
         currentCage = nil
         currentSage = nil
+        lastSpeechTime = nil
         refreshControl.endRefreshing()
     }
     

+ 8 - 0
LoopFollow/repository/UserDefaults.swift

@@ -76,6 +76,14 @@ class UserDefaultsRepository {
     static let useIFCC = UserDefaultsValue<Bool>(key: "useIFCC", default: false)
     static let showSmallGraph = UserDefaultsValue<Bool>(key: "showSmallGraph", default: true)
     static let speakBG = UserDefaultsValue<Bool>(key: "speakBG", default: false)
+    static let speakBGAlways = UserDefaultsValue<Bool>(key: "speakBGAlways", default: true)
+    static let speakLowBG = UserDefaultsValue<Bool>(key: "speakLowBG", default: false)
+    static let speakProactiveLowBG = UserDefaultsValue<Bool>(key: "speakProactiveLowBG", default: false)
+    static let speakFastDropDelta = UserDefaultsValue<Float>(key: "speakFastDropDelta", default: 10.0)
+    static let speakLowBGLimit = UserDefaultsValue<Float>(key: "speakLowBGLimit", default: 72.0)
+    static let speakHighBGLimit = UserDefaultsValue<Float>(key: "speakHighBGLimit", default: 180.0)
+    static let speakHighBG = UserDefaultsValue<Bool>(key: "speakHighBG", default: false)
+    static let speakLanguage = UserDefaultsValue<String>(key: "speakLanguage", default: "en")
     static let showDisplayName = UserDefaultsValue<Bool>(key: "showDisplayName", default: false)
     static let backgroundRefreshFrequency = UserDefaultsValue<Double>(key: "backgroundRefreshFrequency", default: 1)
     static let backgroundRefresh = UserDefaultsValue<Bool>(key: "backgroundRefresh", default: true)