Forráskód Böngészése

Merge branch 'loopandlearn:Main' into bolus_line

Jonas Björkert 3 éve
szülő
commit
d15079b37e

+ 288 - 52
BuildLoopFollow.sh

@@ -1,14 +1,222 @@
 # !/bin/bash
+
+BUILD_DIR=~/Downloads/BuildLoopFollow
+OVERRIDE_FILE=LoopFollowConfigOverride.xcconfig
+OVERRIDE_FULLPATH="${BUILD_DIR}/${OVERRIDE_FILE}"
+DEV_TEAM_SETTING_NAME="LF_DEVELOPMENT_TEAM"
+
+## Unmodified code from Loop build_functions.sh for constistancy - BEGIN
 RED='\033[0;31m'
 GREEN='\033[0;32m'
 PURPLE='\033[0;35m'
 BOLD='\033[1m'
 NC='\033[0m'
-REPO=https://github.com/jonfawcett/LoopFollow
 
-clear
-echo -e "\n\n--------------------------------\n\nWelcome to Loop Follow. This script will assist you in downloading and building the app. Before you begin, please ensure that you have Xcode installed and your phone is plugged into your computer\n\n--------------------------------\n\n"
+function exit_message() {
+    section_divider
+    echo -e "\nShell Script Completed\n"
+    echo -e " * You may close the terminal window now if you want"
+    echo -e "   or"
+    echo -e " * You can press the up arrow ⬆️  on the keyboard"
+    echo -e "    and return to repeat script from beginning.\n\n"
+    exit 0
+}
+
+function section_separator() {
+    clear
+    echo -e "--------------------------------\n"
+}
+
+function section_divider() {
+    echo -e "--------------------------------\n"
+}
+
+function return_when_ready() {
+    echo -e "${RED}${BOLD}Return when ready to continue${NC}"
+    read -p "" dummy
+}
+
+function cancel_entry() {
+    echo -e "\n${RED}${BOLD}User canceled${NC}\n"
+    exit_message
+}
+
+function invalid_entry() {
+    echo -e "\n${RED}${BOLD}User canceled by entering an invalid option${NC}\n"
+    exit_message
+}
+
+function choose_or_cancel() {
+    echo -e "\nType a number from the list below and return to proceed."
+    echo -e "${RED}${BOLD}  To cancel, any entry not in list also works${NC}"
+    echo -e "\n--------------------------------\n"
+}
+
+function how_to_find_your_id() {
+    echo -e "Your Apple Developer ID is the 10-character Team ID"
+    echo -e "  found on the Membership page after logging into your account at:"
+    echo -e "   https://developer.apple.com/account/#!/membership\n"
+    echo -e "It may be necessary to click on the Membership Details icon"
+}
+
+function clone_download_error_check() {
+    # indicate that a clone was created
+    CLONE_OBTAINED=1
+    echo -e "--------------------------------\n"
+    echo -e "🛑 Check for successful Download\n"
+    echo -e "   Please scroll up and look for the word ${BOLD}error${NC} in the window above."
+    echo -e "   OR use the Find command for terminal, hold down CMD key and tap F,"
+    echo -e "      then type error (in new row, top of terminal) and hit return"
+    echo -e "      Be sure to click in terminal again if you use CMD-F"
+    echo -e "   If there are no errors listed, code has successfully downloaded, Continue."
+    echo -e "   If you see the word error in the download, Cancel and resolve the problem."
+    choose_or_cancel
+}
+## Unmodified code from Loop build_functions.sh for constistancy - END
+
+## Modified code from Loop build_functions.sh that should be backward compatible - BEGIN
+function report_persistent_config_override() {
+    echo -e "The file used by Xcode to sign your app is found at:"
+    echo -e "   ${OVERRIDE_FULLPATH}"
+    echo -e "   The line containing the team id is shown next:"
+    grep "^$DEV_TEAM_SETTING_NAME" ${OVERRIDE_FULLPATH}
+    echo -e "\nIf the line has your Apple Developer ID"
+    echo -e "   your target(s) will be automatically signed"
+    echo -e "WARNING: Any line that starts with // is ignored\n"
+    echo -e "  If ID is not OK:"
+    echo -e "    Edit the ${OVERRIDE_FILE} before hitting return"
+    echo -e "     step 1: open finder, navigate to "${BUILD_DIR#*Users/*/}""
+    echo -e "     step 2: locate and double click on "${OVERRIDE_FILE}""
+    echo -e "             this will open that file in Xcode"
+    echo -e "     step 3: edit in Xcode and save file\n"
+    echo -e "  If ID is OK, hit return"
+    return_when_ready
+}
+## Modified code from Loop build_functions.sh that should be backward compatible - END
+
+set_development_team() {
+    team_id="$1"
+    echo "$DEV_TEAM_SETTING_NAME = $team_id" >> ${OVERRIDE_FULLPATH}
+}
+
+function check_config_override_existence_offer_to_configure() {
+    section_separator
+
+    # Automatic signing functionality:
+    # 1) Use existing LoopFollow team
+    # 2) Copy team from Loop
+    # 3) Copy team from latest provisioning profile
+    # 4) Enter team manually with option to skip
+    if [ -f ${OVERRIDE_FULLPATH} ] && grep -q "^$DEV_TEAM_SETTING_NAME" ${OVERRIDE_FULLPATH}; then
+        how_to_find_your_id
+        report_persistent_config_override
+    else
+        if [ -f "../../BuildLoop/LoopConfigOverride.xcconfig" ] && grep -q '^LOOP_DEVELOPMENT_TEAM' "../../BuildLoop/LoopConfigOverride.xcconfig"; then
+            echo -e "Using existing LOOP_DEVELOPMENT_TEAM setting\n"
+            DEVELOPMENT_TEAM=$(grep '^LOOP_DEVELOPMENT_TEAM' "../../BuildLoop/LoopConfigOverride.xcconfig" | awk '{print $3}')
+            set_development_team "$DEVELOPMENT_TEAM"
+            how_to_find_your_id
+            report_persistent_config_override
+        else
+            PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
+
+            if [ -d "${PROFILES_DIR}" ]; then
+                latest_file=$(find "${PROFILES_DIR}" -type f -name "*.mobileprovision" -print0 | xargs -0 ls -t | head -n1)
+                if [ -n "$latest_file" ]; then
+                    # Decode the .mobileprovision file using the security command
+                    decoded_xml=$(security cms -D -i "$latest_file")
+
+                    # Extract the Team ID from the XML
+                    DEVELOPMENT_TEAM=$(echo "$decoded_xml" | awk -F'[<>]' '/<key>TeamIdentifier<\/key>/ { getline; getline; print $3 }')
+                fi
+            fi
+
+            if [ -n "$DEVELOPMENT_TEAM" ]; then
+                echo -e "Using TeamIdentifier from the latest provisioning profile\n"
+                set_development_team "$DEVELOPMENT_TEAM"
+                how_to_find_your_id
+                report_persistent_config_override
+            else
+                echo -e "Choose 1 to Sign Automatically or "
+                echo -e "       2 to Sign Manually (later in Xcode)"
+                echo -e "\nIf you choose Sign Automatically, script guides you"
+                echo -e "  to create a permanent signing file"
+                echo -e "  containing your Apple Developer ID"
+                choose_or_cancel
+                options=("Sign Automatically" "Sign Manually" "Cancel")
+                select opt in "${options[@]}"
+                do
+                    case $opt in
+                        "Sign Automatically")
+                            create_persistent_config_override
+                            break
+                            ;;
+                        "Sign Manually")
+                            break
+                            ;;
+                        "Cancel")
+                            cancel_entry
+                            ;;
+                        *) # Invalid option
+                            invalid_entry
+                            ;;
+                    esac
+                done
+            fi            
+        fi
+    fi
+}
+
+function create_persistent_config_override() {
+    section_separator
+    echo -e "The Apple Developer page will open when you hit return\n"
+    how_to_find_your_id
+    echo -e "That page will be opened for you."
+    echo -e "  Once you get your ID, you will enter it in this terminal window"
+    return_when_ready
+    #
+    open "https://developer.apple.com/account/#!/membership"
+    echo "Please click in terminal window and enter your Apple Developer Team ID (10 characters) or press Enter to skip:"
+    while true; do
+        read DEVELOPMENT_TEAM
+        if [ -z "$DEVELOPMENT_TEAM" ]; then
+            echo -e "You can manually sign target(s) in Xcode"
+            break
+        elif [ ${#DEVELOPMENT_TEAM} -eq 10 ]; then
+            set_development_team "$DEVELOPMENT_TEAM"
+            break
+        else
+            echo "Invalid Team ID. Please enter a valid 10-character Team ID or press Enter to skip."
+        fi
+    done
+    #
+    section_separator
+}
+
+# Check if a argument is provided for the repo
+if [ "$#" -ge 1 ]; then
+  REPO="$1"
+else
+  REPO="https://github.com/jonfawcett/LoopFollow"
+fi
+
+# Check if a second argument is provided for the branch
+if [ "$#" -ge 2 ]; then
+  CUSTOM_BRANCH="$2"
+fi
+
+if [ "$#" -ge 1 ]; then
+  echo "[DEBUG] Repo URL: $REPO"
+    if [ -n "$CUSTOM_BRANCH" ]; then
+        echo "[DEBUG] Custom Branch: $CUSTOM_BRANCH"
+    fi
+    return_when_ready
+fi
+
+section_separator
+echo -e "Welcome to Loop Follow.\nThis script will assist you in downloading and building the app.\nBefore you begin, please ensure that you have Xcode installed and your phone is plugged into your computer\n"
 echo -e "Type 1 and hit enter to begin.\nType 2 and hit enter to cancel."
+choose_or_cancel
 options=("Continue" "Cancel")
 select opt in "${options[@]}"
 do
@@ -17,62 +225,101 @@ do
             break
             ;;
         "Cancel")
-            echo -e "\n${RED}User cancelled!${NC}";
-            exit 0
-            break
+            cancel_entry
             ;;
         *)
+            invalid_entry
+            ;;        
     esac
 done
 
-clear
+if [ -z "$CUSTOM_BRANCH" ]; then
+    section_separator
+    echo -e "Please select which version of Loop Follow you would like to download and build.\nDev branch has the latest features but may contain more bugs.\n\nType the number 1 or 2 and hit enter to select the branch.\nType 3 and hit enter to cancel.\n"
+    choose_or_cancel
+    options=("Main Branch" "Dev Branch" "Cancel")
+    select opt in "${options[@]}"
+    do
+        case $opt in
+            "Main Branch")
+                FOLDERNAME=LoopFollow-Main
+                BRANCH=Main
+                break
+                ;;
+            "Dev Branch")
+                FOLDERNAME=LoopFollow-Dev
+                BRANCH=dev
+                break
+                ;;
+            "Cancel")
+                cancel_entry
+                ;;
+            *)
+                invalid_entry
+                ;;        
+        esac
+    done
+else
+  BRANCH="$CUSTOM_BRANCH"
+  FOLDERNAME="LoopFollow-$BRANCH"
+fi
 
-echo -e "Please select which version of Loop Follow you would like to download and build. Dev branch has the latest features but may contain more bugs.\n\nType the number 1 or 2 and hit enter to select the branch.\nType 3 and hit enter to cancel.\n\n"
-options=("Main Branch" "Dev Branch" "Cancel")
+section_separator
+echo -e "Would you like to delete prior downloads of Loop Follow before proceeding?\n"
+echo -e "Type 1 and hit enter to delete.\nType 2 and hit enter to continue without deleting"
+choose_or_cancel
+options=("Delete old downloads" "Do not delete old downloads" "Cancel")
 select opt in "${options[@]}"
 do
     case $opt in
-        "Main Branch")
-            FOLDERNAME=LoopFollow-Main
-            BRANCH=Main
-            
+        "Delete old downloads")
+            # Delete all folders below ~/Downloads/BuildLoopFollow but preserve the permanent config file.
+            find ~/Downloads/BuildLoopFollow -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
             break
             ;;
-        "Dev Branch")
-            FOLDERNAME=LoopFollow-Dev
-            BRANCH=dev
+        "Do not delete old downloads")
             break
             ;;
         "Cancel")
-            echo -e "\n${RED}User cancelled!${NC}";
-            exit 0
-            break
+            cancel_entry
             ;;
-        *) 
+        *)
+            invalid_entry
+            ;; 
     esac
 done
 
-clear
-echo -e "Would you like to delete prior downloads of Loop Follow before proceeding?\n\n"
-echo -e "Type 1 and hit enter to delete.\nType 2 and hit enter to continue without deleting.\n\n"
-options=("Delete old downloads" "Do not delete old downloads")
+section_separator
+echo -e "The code will now begin downloading.\nThe files will be saved in your Downloads folder.\n"
+echo -e "Type 1 and hit enter to begin downloading.\nType 2 and hit enter to cancel.\n"
+choose_or_cancel
+options=("Continue" "Cancel")
 select opt in "${options[@]}"
 do
     case $opt in
-        "Delete old downloads")
-            rm -rf ~/Downloads/BuildLoopFollow/*
+        "Continue")
             break
             ;;
-        "Do not delete old downloads")
-            break
+        "Cancel")
+            cancel_entry
             ;;
         *)
+            invalid_entry
+            ;; 
     esac
 done
 
-clear
-echo -e "The code will now begin downloading. The files will be saved in your Downloads folder and Xcode will automatically open when the download is complete.\n\n"
-echo -e "Type 1 and hit enter to begin downloading.\nType 2 and hit enter to cancel.\n\n"
+section_separator
+LOOP_BUILD=$(date +'%y%m%d-%H%M')
+LOOP_DIR=~/Downloads/BuildLoopFollow/$FOLDERNAME-$LOOP_BUILD
+mkdir -p $LOOP_DIR
+cd $LOOP_DIR
+
+echo -e "Downloading Loop Follow to your Downloads folder."
+pwd
+echo -e
+git clone --branch=$BRANCH --recurse-submodules $REPO
+clone_download_error_check
 options=("Continue" "Cancel")
 select opt in "${options[@]}"
 do
@@ -81,29 +328,19 @@ do
             break
             ;;
         "Cancel")
-            echo -e "\n${RED}User cancelled!${NC}";
-            exit 0
-            break
+            cancel_entry
             ;;
         *)
+            invalid_entry
+            ;;
     esac
 done
 
-clear
-
-LOOP_BUILD=$(date +'%y%m%d-%H%M')
-LOOP_DIR=~/Downloads/BuildLoopFollow/$FOLDERNAME-$LOOP_BUILD
-mkdir ~/Downloads/BuildLoopFollow/
-mkdir $LOOP_DIR
-cd $LOOP_DIR
-pwd
-clear
-echo -e "\n\n Downloading Loop Follow to your Downloads folder.\n--------------------------------\n"
-git clone --branch=$BRANCH --recurse-submodules $REPO
-
-echo -e "--------------------------------\n\nIf there are no errors listed above, code has successfully downloaded.\n"
-echo -e "Type 1 and hit enter to open Xcode. You may close the terminal after Xcode opens\n\n"
+check_config_override_existence_offer_to_configure
 
+section_separator
+echo -e "Type 1 and hit enter to open Xcode. You may close the terminal after Xcode opens"
+choose_or_cancel
 options=("Continue" "Cancel")
 select opt in "${options[@]}"
 do
@@ -112,15 +349,14 @@ do
             break
             ;;
         "Cancel")
-            echo -e "\n${RED}User cancelled!${NC}";
-            exit 0
-            break
+            cancel_entry
             ;;
         *)
+            invalid_entry
+            ;;
     esac
 done
 
-
 cd LoopFollow
 Echo Open xcode
 xed ./LoopFollow.xcworkspace

+ 2 - 0
Config.xcconfig

@@ -1,3 +1,5 @@
+#include? "../../LoopFollowConfigOverride.xcconfig"
+
 //
 //  Config.xcconfig
 //  nsapple

+ 6 - 2
LoopFollow.xcodeproj/project.pbxproj

@@ -8,6 +8,7 @@
 
 /* Begin PBXBuildFile section */
 		3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; };
+		DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */; };
 		DD152D3B24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD152D3A24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift */; };
 		DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */; };
 		DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979324C0D380002C9752 /* UIViewExtension.swift */; };
@@ -174,6 +175,7 @@
 /* Begin PBXFileReference section */
 		059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = "<group>"; };
 		A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = "<group>"; };
 		DD152D3A24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDisplaySettingsViewController.swift; sourceTree = "<group>"; };
 		DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientExtension.swift; sourceTree = "<group>"; };
 		DDCF979324C0D380002C9752 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = "<group>"; };
@@ -603,6 +605,7 @@
 				FC1BDD2E24A232A3001B652C /* DataStructs.swift */,
 				FCD2A27C24C9D044009F7B7B /* Globals.swift */,
 				FC8589BE252B54F500C8FC73 /* Mobileprovision.swift */,
+				DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */,
 			);
 			path = helpers;
 			sourceTree = "<group>";
@@ -866,6 +869,7 @@
 				FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */,
 				FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */,
 				FC16A97A24996673003D6245 /* NightScout.swift in Sources */,
+				DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */,
 				FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */,
 				FC16A97D24996747003D6245 /* Alarms.swift in Sources */,
 				FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */,
@@ -1046,7 +1050,7 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CODE_SIGN_ENTITLEMENTS = "LoopFollow/Loop Follow.entitlements";
 				CODE_SIGN_STYLE = Automatic;
-				DEVELOPMENT_TEAM = "";
+                DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)";
 				INFOPLIST_FILE = LoopFollow/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
@@ -1068,7 +1072,7 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CODE_SIGN_ENTITLEMENTS = "LoopFollow/Loop Follow.entitlements";
 				CODE_SIGN_STYLE = Automatic;
-				DEVELOPMENT_TEAM = "";
+                DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)";
 				INFOPLIST_FILE = LoopFollow/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",

+ 77 - 0
LoopFollow.xcworkspace/xcshareddata/xcschemes/LoopFollow.xcscheme

@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1430"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "FC9788132485969B00A7906C"
+               BuildableName = "Loop Follow.app"
+               BlueprintName = "LoopFollow"
+               ReferencedContainer = "container:LoopFollow.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "FC9788132485969B00A7906C"
+            BuildableName = "Loop Follow.app"
+            BlueprintName = "LoopFollow"
+            ReferencedContainer = "container:LoopFollow.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "FC9788132485969B00A7906C"
+            BuildableName = "Loop Follow.app"
+            BlueprintName = "LoopFollow"
+            ReferencedContainer = "container:LoopFollow.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 43 - 0
LoopFollow/Application/SceneDelegate.swift

@@ -7,10 +7,12 @@
 //
 
 import UIKit
+import AVFoundation
 
 class SceneDelegate: UIResponder, UIWindowSceneDelegate {
 
     var window: UIWindow?
+    let synthesizer = AVSpeechSynthesizer()
 
     let appStateController = AppStateController()
     
@@ -49,6 +51,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
                vc.appStateController = appStateController
             }
          }
+
+        // Register the SceneDelegate as an observer for the "toggleSpeakBG" notification, which will be triggered when the user toggles the "Speak BG" feature in General Settings. This helps ensure that the Quick Action is updated according to the current setting.
+        NotificationCenter.default.addObserver(self, selector: #selector(handleToggleSpeakBGEvent), name: NSNotification.Name("toggleSpeakBG"), object: nil)
+        updateQuickActions()
     }
 
     func sceneDidDisconnect(_ scene: UIScene) {
@@ -56,6 +62,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
         // This occurs shortly after the scene enters the background, or when its session is discarded.
         // Release any resources associated with this scene that can be re-created the next time the scene connects.
         // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
+
+        NotificationCenter.default.removeObserver(self, name: NSNotification.Name("toggleSpeakBG"), object: nil)
     }
 
     func sceneDidBecomeActive(_ scene: UIScene) {
@@ -82,6 +90,41 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
         (UIApplication.shared.delegate as? AppDelegate)?.saveContext()
     }
 
+    // Update the Home Screen Quick Action for toggling the "Speak BG" feature based on the current setting in UserDefaultsRepository. This function uses UIApplicationShortcutItem to create a 3D touch action for controlling the feature.
+    func updateQuickActions() {
+        let iconName = UserDefaultsRepository.speakBG.value ? "pause.circle.fill" : "play.circle.fill"
+        let iconTemplate = UIApplicationShortcutIcon(systemImageName: iconName)
+
+        let shortcut = UIApplicationShortcutItem(type: Bundle.main.bundleIdentifier! + ".toggleSpeakBG",
+                                                 localizedTitle: "Speak BG",
+                                                 localizedSubtitle: nil,
+                                                 icon: iconTemplate,
+                                                 userInfo: nil)
+        UIApplication.shared.shortcutItems = [shortcut]
+    }
+
 
+    // Handle the UIApplicationShortcutItem when the user taps on the Home Screen Quick Action. This function toggles the "Speak BG" setting in UserDefaultsRepository, speaks the current state (on/off) using AVSpeechSynthesizer, and updates the Quick Action appearance.
+    func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) {
+        if let bundleIdentifier = Bundle.main.bundleIdentifier {
+            let expectedType = bundleIdentifier + ".toggleSpeakBG"
+            if shortcutItem.type == expectedType {
+                UserDefaultsRepository.speakBG.value.toggle()
+                let message = UserDefaultsRepository.speakBG.value ? "BG Speak is now on" : "BG Speak is now off"
+                let utterance = AVSpeechUtterance(string: message)
+                synthesizer.speak(utterance)
+                updateQuickActions()
+            }
+        }
+    }
+
+    // The following method is called when the user taps on the Home Screen Quick Action
+    func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
+        handleShortcutItem(shortcutItem)
+    }
+
+    @objc func handleToggleSpeakBGEvent() {
+        updateQuickActions()
+    }
 }
 

+ 53 - 12
LoopFollow/Controllers/Alarms.swift

@@ -496,9 +496,19 @@ extension MainViewController {
             }
         }
         
-        
-        
-        
+        if UserDefaultsRepository.alertBatteryActive.value && !UserDefaultsRepository.alertBatteryIsSnoozed.value {
+            let currentBatteryLevel = UserDefaultsRepository.deviceBatteryLevel.value
+            let alertAtBatteryLevel = Double(UserDefaultsRepository.alertBatteryLevel.value)
+            
+            if currentBatteryLevel <= alertAtBatteryLevel {
+                AlarmSound.whichAlarm = "Low Battery"
+
+                if UserDefaultsRepository.alertBatteryRepeat.value { numLoops = -1 }
+                triggerAlarm(sound: UserDefaultsRepository.alertBatterySound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertBatterySnoozeHours.value, snoozeIncrement: 1, audio: true)
+                return
+            }
+        }
+
         // still send persistent notification if no alarms trigger and persistent notification is on
         persistentNotification(bgTime: currentBGTime)
         
@@ -757,7 +767,12 @@ extension MainViewController {
 
         }
         
-        
+        if date > UserDefaultsRepository.alertBatterySnoozedTime.value ?? date {
+            UserDefaultsRepository.alertBatterySnoozedTime.setNil(key: "alertBatterySnoozedTime")
+            UserDefaultsRepository.alertBatteryIsSnoozed.value = false
+            alarms.reloadSnoozeTime(key: "alertBatterySnoozedTime", setNil: true)
+            alarms.reloadIsSnoozed(key: "alertBatteryIsSnoozed", value: false)
+        }
       }
     
     func checkQuietHours() {
@@ -847,14 +862,40 @@ extension MainViewController {
         
     }
     
-    
-    func speakBG(sgv: Int) {
-           var speechSynthesizer = AVSpeechSynthesizer()
-           var speechUtterance: AVSpeechUtterance = AVSpeechUtterance(string: "Current BG is " + bgUnits.toDisplayUnits(String(sgv)))
-           speechUtterance.rate = AVSpeechUtteranceMaximumSpeechRate / 2
-           speechUtterance.voice = AVSpeechSynthesisVoice(language: "en-US")
-           speechSynthesizer.speak(speechUtterance)
-       }
+    // 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) {
+        // Get the current time
+        let currentTime = Date()
+
+        // Check if speakBG was called less than 30 seconds ago. If so, prevent repeated announcements and return.
+        // If `lastSpeechTime` is `nil` (i.e., this is the first time `speakBG` is being called), use `Date.distantPast` as the default
+        // value to ensure that the `guard` statement passes and the announcement is made.
+        guard currentTime.timeIntervalSince(lastSpeechTime ?? .distantPast) >= 30 else {
+            print("Repeated calls to speakBG detected!")
+            return
+        }
+
+        // Update the last speech time
+        self.lastSpeechTime = currentTime
+
+        let bloodGlucoseDifference = currentValue - previousValue
+        let negligibleThreshold = 3
+        let differenceText: String
+
+        if abs(bloodGlucoseDifference) <= negligibleThreshold {
+            differenceText = "stable"
+        } else {
+            let direction = bloodGlucoseDifference < 0 ? "down" : "up"
+            let absoluteDifference = bgUnits.toDisplayUnits(String(abs(bloodGlucoseDifference)))
+            differenceText = "\(direction) \(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")
+        speechSynthesizer.speak(speechUtterance)
+    }
     
     func isOnPhoneCall() -> Bool {
         /*

+ 134 - 25
LoopFollow/Controllers/NightScout.swift

@@ -9,10 +9,14 @@
 import Foundation
 import UIKit
 
-
 extension MainViewController {
 
     
+    //NS Carbs Struct
+    struct carbsData: Codable {
+        var carbs: Double?
+    }
+
     //NS Cage Struct
     struct cageData: Codable {
         var created_at: String
@@ -115,27 +119,30 @@ extension MainViewController {
             self.startBGTimer(time: 10)
             return
         }
-        let graphHours = 24 * UserDefaultsRepository.downloadDays.value
-        // Set the count= in the url either to pull day(s) of data or only the last record
-        var points = "1"
-        if !onlyPullLastRecord {
-            points = String(graphHours * 12 + 1)
-        }
         
         // URL processor
         var urlBGDataPath: String = UserDefaultsRepository.url.value + "/api/v1/entries/sgv.json?"
-        if token == "" {
-            urlBGDataPath = urlBGDataPath + "count=" + points
+
+        if onlyPullLastRecord {
+            urlBGDataPath = urlBGDataPath + "count=1"
         } else {
-            urlBGDataPath = urlBGDataPath + "token=" + token + "&count=" + points
+            //Fetch entries for the time period of "downloadDays"
+            let utcISODateFormatter = ISO8601DateFormatter()
+            let date = Calendar.current.date(byAdding: .day, value: -1 * UserDefaultsRepository.downloadDays.value, to: Date())!
+            urlBGDataPath = urlBGDataPath + "count=1000&find[dateString][$gte]=" + utcISODateFormatter.string(from: date)
+        }
+
+        if !token.isEmpty {
+            urlBGDataPath = urlBGDataPath + "&token=" + token
         }
+
         guard let urlBGData = URL(string: urlBGDataPath) else {
             // if we have Dex data, use it
             if !dexData.isEmpty {
                 self.ProcessDexBGData(data: dexData, onlyPullLastRecord: onlyPullLastRecord, sourceName: "Dexcom")
                 return
             }
-            
+
             if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
                 globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
                 //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
@@ -186,8 +193,25 @@ extension MainViewController {
                 
             }
             
+            var entriesResponse: [ShareGlucoseData]?
             let decoder = JSONDecoder()
-            let entriesResponse = try? decoder.decode([ShareGlucoseData].self, from: data)
+            do {
+                entriesResponse = try decoder.decode([ShareGlucoseData].self, from: data)
+            } catch let DecodingError.dataCorrupted(context) {
+                print("Data corrupted: \(context)")
+            } catch let DecodingError.keyNotFound(key, context) {
+                print("Key '\(key)' not found: \(context.debugDescription)")
+                print("codingPath: \(context.codingPath)")
+            } catch let DecodingError.valueNotFound(value, context) {
+                print("Value '\(value)' not found: \(context.debugDescription)")
+                print("codingPath: \(context.codingPath)")
+            } catch let DecodingError.typeMismatch(type, context)  {
+                print("Type '\(type)' mismatch: \(context.debugDescription)")
+                print("codingPath: \(context.codingPath)")
+            } catch {
+                print("Error decoding JSON: \(error)")
+            }
+
             if var nsData = entriesResponse {
                 DispatchQueue.main.async {
                     // transform NS data to look like Dex data
@@ -196,22 +220,39 @@ extension MainViewController {
                         nsData[i].date /= 1000
                         nsData[i].date.round(FloatingPointRoundingRule.toNearestOrEven)
                     }
+                    print(nsData.count)
+
+                    //Avoid duplicate entries messing up the graph, only use one reading per 5 minutes.
+                    let graphHours = 24 * UserDefaultsRepository.downloadDays.value
+                    let points = graphHours * 12 + 1
+                    var nsData2 = [ShareGlucoseData]()
+                    let timestamp = Date().timeIntervalSince1970
+                    for i in 0..<points {
+                        //Starting with "now" and then step 5 minutes back in time
+                        let target = timestamp - Double(i) * 60 * 5
+                        //Find the reading closest to the target, but not too far away
+                        let closest = nsData.filter{ abs($0.date - target) < 3 * 60 }.min { abs($0.date - target) < abs($1.date - target) }
+                        //If a reading is found, add it to the new array
+                        if let item = closest {
+                            nsData2.append(item)
+                        }
+                    }
+                    print(nsData2.count)
                     
                     // merge NS and Dex data if needed; use recent Dex data and older NS data
                     var sourceName = "Nightscout"
                     if !dexData.isEmpty {
                         let oldestDexDate = dexData[dexData.count - 1].date
                         var itemsToRemove = 0
-                        while itemsToRemove < nsData.count && nsData[itemsToRemove].date >= oldestDexDate {
+                        while itemsToRemove < nsData2.count && nsData2[itemsToRemove].date >= oldestDexDate {
                             itemsToRemove += 1
                         }
-                        nsData.removeFirst(itemsToRemove)
-                        nsData = dexData + nsData
+                        nsData2.removeFirst(itemsToRemove)
+                        nsData2 = dexData + nsData2
                         sourceName = "Dexcom"
                     }
-                    
                     // trigger the processor for the data after downloading.
-                    self.ProcessDexBGData(data: nsData, onlyPullLastRecord: onlyPullLastRecord, sourceName: sourceName)
+                    self.ProcessDexBGData(data: nsData2, onlyPullLastRecord: onlyPullLastRecord, sourceName: sourceName)
                     
                 }
             } else {
@@ -271,6 +312,10 @@ extension MainViewController {
                 self.startBGTimer(time: 300 - secondsAgo + Double(UserDefaultsRepository.bgUpdateDelay.value))
                 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)
+                }
             }
         }
         
@@ -281,12 +326,6 @@ extension MainViewController {
             bgData.removeFirst()
             
         } else {
-            if data.count > 0 {
-                self.updateBadge(val: data[data.count - 1].sgv)
-                if UserDefaultsRepository.speakBG.value {
-                    speakBG(sgv: data[data.count - 1].sgv)
-                }
-            }
             return
         }
         
@@ -361,7 +400,6 @@ extension MainViewController {
                 snoozerDelta = "+" + bgUnits.toDisplayUnits(String(deltaBG))
                 self.latestDeltaString = "+" + String(deltaBG)
             }
-            self.updateBadge(val: latestBG)
             
             // Snoozer Display
             guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
@@ -493,6 +531,7 @@ extension MainViewController {
                 if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
                     let upbat = uploader["battery"] as! Double
                     tableData[4].value = String(format:"%.0f", upbat) + "%"
+                    UserDefaultsRepository.deviceBatteryLevel.value = upbat
                 }
             }
         }
@@ -648,6 +687,76 @@ extension MainViewController {
         }
     }
     
+    // NS Carbs Today Web Call
+    func webLoadNSCarbsToday() {
+        if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: Carbs Today") }
+        let urlUser = UserDefaultsRepository.url.value
+
+        let now = Date()
+        let timeZone = TimeZone.current
+
+        var calendar = Calendar(identifier: .gregorian)
+        calendar.timeZone = timeZone
+
+        var dateComponents = calendar.dateComponents(in: timeZone, from: now)
+        dateComponents.hour = 0
+        dateComponents.minute = 0
+        dateComponents.second = 0
+
+        guard let date = dateComponents.date else { fatalError("Invalid date components") }
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
+        dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
+        let utcDateString = dateFormatter.string(from: date)
+
+        var urlString = urlUser + "/api/v1/treatments.json?count=1000&find[eventType]=Carb+Correction&find[timestamp][$gte]=" + utcDateString
+        if token != "" {
+            urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&count=1000&find[eventType]=Carb+Correction&find[timestamp][$gte]=" + utcDateString
+        }
+
+        guard let urlData = URL(string: urlString) else {
+            return
+        }
+        var request = URLRequest(url: urlData)
+        request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+
+        let task = URLSession.shared.dataTask(with: request) { data, response, error in
+            guard error == nil else {
+                return
+            }
+            guard let data = data else {
+                return
+            }
+
+            let decoder = JSONDecoder()
+            let entriesResponse = try? decoder.decode([carbsData].self, from: data)
+            if let entriesResponse = entriesResponse {
+                DispatchQueue.main.async {
+                    self.updateCarbsToday(data: entriesResponse)
+                }
+            } else {
+                return
+            }
+        }
+        task.resume()
+    }
+
+    // NS CarbsToday Response Processor
+    func updateCarbsToday(data: [carbsData]) {
+        self.clearLastInfoData(index: 10)
+        if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: carbs") }
+        if data.count == 0 {
+            return
+        }
+
+        let totalCarbs = data.reduce(0.0) { $0 + ($1.carbs ?? 0.0) }
+        let resultString = String(format: "%.0f", totalCarbs)
+
+        tableData[10].value = resultString
+
+        infoTable.reloadData()
+    }
+
     // NS Cage Web Call
     func webLoadNSCage() {
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: CAGE") }
@@ -1210,7 +1319,7 @@ extension MainViewController {
                 priorDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
                 guard let priorDateString = dateFormatter.date(from: priorStrippedZone) else { continue }
                 let priorDateTimeStamp = priorDateString.timeIntervalSince1970
-                let priorDuration = priorEntry?["duration"] as! Double
+                let priorDuration = priorEntry?["duration"] as? Double ?? 0.0
                 // if difference between time stamps is greater than the duration of the last entry, there is a gap. Give a 15 second leeway on the timestamp
                 if Double( dateTimeStamp - priorDateTimeStamp ) > Double( (priorDuration * 60) + 15 ) {
                     

+ 11 - 0
LoopFollow/Controllers/Timers.swift

@@ -23,6 +23,16 @@ extension MainViewController {
         if !alarmTimer.isValid { startAlarmTimer(time: 30) }
     }
     
+    func invalidateTimers() {
+        bgTimer.invalidate()
+        profileTimer.invalidate()
+        deviceStatusTimer.invalidate()
+        treatmentsTimer.invalidate()
+        cageSageTimer.invalidate()
+        minAgoTimer.invalidate()
+        calendarTimer.invalidate()
+        alarmTimer.invalidate()
+    }
     
     // min Ago Timer
     func startMinAgoTimer(time: TimeInterval) {
@@ -239,6 +249,7 @@ extension MainViewController {
         }
         
         if UserDefaultsRepository.url.value != "" {
+            webLoadNSCarbsToday()
             webLoadNSCage()
             webLoadNSSage()
             startCageSageTimer()

+ 31 - 3
LoopFollow/Extensions/ShareClientExtension.swift

@@ -10,9 +10,37 @@ import Foundation
 import ShareClient
 
 public struct ShareGlucoseData: Codable {
-   var sgv: Int
-   var date: TimeInterval
-   var direction: String?
+    var sgv: Int
+    var date: TimeInterval
+    var direction: String?
+
+    enum CodingKeys: String, CodingKey {
+        case sgv
+        case date
+        case direction
+    }
+
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+
+        if let sgvAsDouble = try? container.decode(Double.self, forKey: .sgv) {
+            sgv = Int(sgvAsDouble.rounded())
+        } else if let sgvAsInt = try? container.decode(Int.self, forKey: .sgv) {
+            sgv = sgvAsInt
+        } else {
+            throw DecodingError.dataCorruptedError(forKey: .sgv, in: container, debugDescription: "Expected to decode an Integer or Double.")
+        }
+
+        // Decode the other properties
+        date = try container.decode(TimeInterval.self, forKey: .date)
+        direction = try container.decodeIfPresent(String.self, forKey: .direction)
+    }
+
+    public init(sgv: Int, date: TimeInterval, direction: String?) {
+        self.sgv = sgv
+        self.date = date
+        self.direction = direction
+    }
 }
 
 private var TrendTable: [String] = [

+ 66 - 1
LoopFollow/ViewControllers/AlarmViewController.swift

@@ -339,7 +339,7 @@ class AlarmViewController: FormViewController {
         
         <<< SegmentedRow<String>("otherAlerts3"){ row in
                 row.title = ""
-                row.options = ["IOB", "COB"]
+                row.options = ["IOB", "COB", "Battery"]
                 if UserDefaultsRepository.url.value == "" {
                     row.hidden = true
                 }
@@ -389,6 +389,7 @@ class AlarmViewController: FormViewController {
         
         buildIOB()
         buildCOB()
+        buildBatteryAlarm()
         
         buildSnoozeAll()
         buildAlarmSettings()
@@ -3178,6 +3179,70 @@ class AlarmViewController: FormViewController {
         }
     }
 
+    func buildBatteryAlarm(){
+        form
+        +++ Section(header: "Battery Alarm", footer: "Activates a notification alert whenever the battery level drops below a user-defined threshold, allowing for proactive device charging and power management.") { row in
+            row.hidden = "$otherAlerts3 != 'Battery'"
+        }
+        <<< SwitchRow("alertBatteryActive"){ row in
+            row.title = "Active"
+            row.value = UserDefaultsRepository.alertBatteryActive.value
+        }.onChange { [weak self] row in
+            guard let value = row.value else { return }
+            UserDefaultsRepository.alertBatteryActive.value = value
+        }
+        <<< StepperRow("alertBatteryLevel") { row in
+            row.title = "Battery Level"
+            row.cell.stepper.stepValue = 5
+            row.cell.stepper.minimumValue = 0
+            row.cell.stepper.maximumValue = 100
+            row.value = Double(UserDefaultsRepository.alertBatteryLevel.value)
+            row.displayValueFor = { value in
+                guard let value = value else { return nil }
+                return "\(Int(value))%"
+            }
+        }.onChange { [weak self] row in
+            guard let value = row.value else { return }
+            UserDefaultsRepository.alertBatteryLevel.value = Int(value)
+        }
+        <<< StepperRow("alertBatterySnoozeHours") { row in
+            row.title = "Snooze Hours"
+            row.cell.stepper.stepValue = 1
+            row.cell.stepper.minimumValue = 1
+            row.cell.stepper.maximumValue = 24
+            row.value = Double(UserDefaultsRepository.alertBatterySnoozeHours.value)
+            row.displayValueFor = { value in
+                guard let value = value else { return nil }
+                return "\(Int(value))"
+            }
+        }.onChange { [weak self] row in
+            guard let value = row.value else { return }
+            UserDefaultsRepository.alertBatterySnoozeHours.value = Int(value)
+        }
+        <<< PickerInputRow<String>("alertBatterySound") { row in
+            row.title = "Sound"
+            row.options = soundFiles
+            row.value = UserDefaultsRepository.alertBatterySound.value
+            row.displayValueFor = { value in
+                guard let value = value else { return nil }
+                return "\(String(value.replacingOccurrences(of: "_", with: " ")))"
+            }
+        }.onChange { [weak self] row in
+            guard let value = row.value else { return }
+            UserDefaultsRepository.alertBatterySound.value = value
+            AlarmSound.setSoundFile(str: value)
+            AlarmSound.stop()
+            AlarmSound.playTest()
+        }
+        <<< SwitchRow("alertBatteryRepeat"){ row in
+            row.title = "Repeat Sound"
+            row.value = UserDefaultsRepository.alertBatteryRepeat.value
+        }.onChange { [weak self] row in
+            guard let value = row.value else { return }
+            UserDefaultsRepository.alertBatteryRepeat.value = value
+        }
+    }
+    
     func buildAlarmSettings() {
            form
             +++ Section(header: "Alarm Settings", footer: "")

+ 17 - 0
LoopFollow/ViewControllers/GeneralSettingsViewController.swift

@@ -22,6 +22,9 @@ class GeneralSettingsViewController: FormViewController {
          overrideUserInterfaceStyle = .dark
       }
       buildGeneralSettings()
+
+      // Register the GeneralSettingsViewController as an observer for the UIApplication.willEnterForegroundNotification, which will be triggered when the app enters the foreground. This helps ensure that the "Speak BG" switch in the General Settings is updated according to the current setting.
+      NotificationCenter.default.addObserver(self, selector: #selector(handleAppWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
    }
    
    private func buildGeneralSettings() {
@@ -143,6 +146,9 @@ class GeneralSettingsViewController: FormViewController {
         }.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)
         }
         
         +++ ButtonRow() {
@@ -152,4 +158,15 @@ class GeneralSettingsViewController: FormViewController {
        }
     }
    
+    // 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 {
+            row.value = UserDefaultsRepository.speakBG.value
+            row.updateCell()
+        }
+    }
+
+    deinit {
+        NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
+    }
 }

+ 55 - 5
LoopFollow/ViewControllers/MainViewController.swift

@@ -13,7 +13,7 @@ import ShareClient
 import UserNotifications
 import Photos
 
-class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate {
+class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate, UIScrollViewDelegate {
     
     @IBOutlet weak var BGText: UILabel!
     @IBOutlet weak var DeltaText: UILabel!
@@ -36,8 +36,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     @IBOutlet weak var serverText: UILabel!
     @IBOutlet weak var statsView: UIView!
     @IBOutlet weak var smallGraphHeightConstraint: NSLayoutConstraint!
-    
-      
+    var refreshScrollView: UIScrollView!
+    var refreshControl: UIRefreshControl!
+
+    let speechSynthesizer = AVSpeechSynthesizer()
+
     // Data Table class
     class infoData {
         public var name: String
@@ -140,6 +143,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     
     var snoozeTabItem: UITabBarItem = UITabBarItem()
     
+    // Stores the time of the last speech announcement to prevent repeated announcements.
+    // This is a temporary safeguard until the issue with multiple calls to speakBG is fixed.
+    var lastSpeechTime: Date?
+
     override func viewDidLoad() {
         super.viewDidLoad()
 
@@ -155,7 +162,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         UserDefaultsRepository.infoNames.value.append("CAGE")
         UserDefaultsRepository.infoNames.value.append("Rec. Bolus")
         UserDefaultsRepository.infoNames.value.append("Pred.")
-        
+        UserDefaultsRepository.infoNames.value.append("Carbs today")
+
         // Reset deprecated settings
         UserDefaultsRepository.debugLog.value = false;
         UserDefaultsRepository.alwaysDownloadAllBG.value = true;
@@ -227,10 +235,49 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         // Load Startup Data
         restartAllTimers()
         
+        // Set up refreshScrollView for BGText
+        refreshScrollView = UIScrollView()
+        refreshScrollView.translatesAutoresizingMaskIntoConstraints = false
+        refreshScrollView.alwaysBounceVertical = true
+        view.addSubview(refreshScrollView)
+        
+        NSLayoutConstraint.activate([
+            refreshScrollView.leadingAnchor.constraint(equalTo: BGText.leadingAnchor),
+            refreshScrollView.trailingAnchor.constraint(equalTo: BGText.trailingAnchor),
+            refreshScrollView.topAnchor.constraint(equalTo: BGText.topAnchor),
+            refreshScrollView.bottomAnchor.constraint(equalTo: BGText.bottomAnchor)
+        ])
+        
+        refreshControl = UIRefreshControl()
+        refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
+        refreshScrollView.addSubview(refreshControl)
+        
+        // Add this line to prevent scrolling in other directions
+        refreshScrollView.alwaysBounceVertical = true
+        
+        refreshScrollView.delegate = self
     }
     
-
+    // Clean all timers and start new ones when refreshing
+    @objc func refresh() {
+        print("Refreshing")
+        MinAgoText.text = "Refreshing"
+        invalidateTimers()
+        restartAllTimers()
+        refreshControl.endRefreshing()
+    }
     
+    // Scroll down BGText when refreshing
+    func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        if scrollView == refreshScrollView {
+            let yOffset = scrollView.contentOffset.y
+            if yOffset < 0 {
+                BGText.transform = CGAffineTransform(translationX: 0, y: -yOffset)
+            } else {
+                BGText.transform = CGAffineTransform.identity
+            }
+        }
+    }
     
     override func viewWillAppear(_ animated: Bool) {
         // set screen lock
@@ -300,6 +347,9 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     private func createDerivedData() {
         
         self.derivedTableData = []
+        while UserDefaultsRepository.infoVisible.value.count < self.tableData.count {
+            UserDefaultsRepository.infoVisible.value.append(true)
+        }
         for i in 0..<self.tableData.count {
             if(UserDefaultsRepository.infoVisible.value[UserDefaultsRepository.infoSort.value[i]]) {
                 self.derivedTableData.append(self.tableData[UserDefaultsRepository.infoSort.value[i]])

+ 83 - 43
LoopFollow/ViewControllers/SettingsViewController.swift

@@ -14,7 +14,8 @@ import EventKitUI
 class SettingsViewController: FormViewController {
 
    var appStateController: AppStateController?
-    
+    var statusLabelRow: LabelRow!
+
     func showHideNSDetails() {
         var isHidden = false
         var isEnabled = true
@@ -70,45 +71,56 @@ class SettingsViewController: FormViewController {
                guard let value = row.value else { return }
                UserDefaultsRepository.showNS.value = value
        }
-        <<< TextRow(){ row in
-            row.title = "URL"
-            row.placeholder = "https://mycgm.herokuapp.com"
-            row.value = UserDefaultsRepository.url.value
-            row.hidden = "$showNS == false"
-        }.cellSetup { (cell, row) in
-            cell.textField.autocorrectionType = .no
-        }.onChange { row in
-            guard let value = row.value else {
-                UserDefaultsRepository.url.value = ""
-                self.showHideNSDetails()
-                return }
-            // check the format of the URL entered by the user and trim away any spaces or "/" at the end
-            var urlNSInput = value.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
-            if urlNSInput.last == "/" {
-                urlNSInput = String(urlNSInput.dropLast())
-            }
-            UserDefaultsRepository.url.value = urlNSInput.lowercased()
-            // set the row value back to the correctly formatted URL so that the user immediately sees how it should have been written
-            row.value = UserDefaultsRepository.url.value
-            self.showHideNSDetails()
-            globalVariables.nsVerifiedAlert = 0
-            }
-        <<< TextRow(){ row in
-            row.title = "NS Token"
-            row.placeholder = "Leave blank if not using tokens"
-            row.value = UserDefaultsRepository.token.value
-            row.hidden = "$showNS == false"
-        }.cellSetup { (cell, row) in
-            cell.textField.autocorrectionType = .no
-        }.onChange { row in
-            if row.value == nil {
-                UserDefaultsRepository.token.value = ""
-            }
-            guard let value = row.value else { return }
-            UserDefaultsRepository.token.value = value
-            globalVariables.nsVerifiedAlert = 0
-        }
-       
+       <<< TextRow() { row in
+           row.title = "URL"
+           row.placeholder = "https://mycgm.herokuapp.com"
+           row.value = UserDefaultsRepository.url.value
+           row.hidden = "$showNS == false"
+       }.cellSetup { (cell, row) in
+           cell.textField.autocorrectionType = .no
+       }.onChange { row in
+           guard let value = row.value else {
+               UserDefaultsRepository.url.value = ""
+               self.showHideNSDetails()
+               return }
+           // check the format of the URL entered by the user and trim away any spaces or "/" at the end
+           var urlNSInput = value.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
+           if urlNSInput.last == "/" {
+               urlNSInput = String(urlNSInput.dropLast())
+           }
+           UserDefaultsRepository.url.value = urlNSInput.lowercased()
+           // set the row value back to the correctly formatted URL so that the user immediately sees how it should have been written
+           row.value = UserDefaultsRepository.url.value
+           self.showHideNSDetails()
+           globalVariables.nsVerifiedAlert = 0
+           
+           // Verify Nightscout URL and token
+           self.checkNightscoutStatus()
+       }
+       <<< TextRow() { row in
+           row.title = "NS Token"
+           row.placeholder = "Leave blank if not using tokens"
+           row.value = UserDefaultsRepository.token.value
+           row.hidden = "$showNS == false"
+       }.cellSetup { (cell, row) in
+           cell.textField.autocorrectionType = .no
+       }.onChange { row in
+           if row.value == nil {
+               UserDefaultsRepository.token.value = ""
+           }
+           guard let value = row.value else { return }
+           UserDefaultsRepository.token.value = value
+           globalVariables.nsVerifiedAlert = 0
+           
+           // Verify Nightscout URL and token
+           self.checkNightscoutStatus()
+       }
+       <<< LabelRow() { row in
+           row.title = "NS Status"
+           row.value = "Checking..."
+           statusLabelRow = row
+           row.hidden = "$showNS == false"
+       }
        <<< SwitchRow("loopUser"){ row in
            row.title = "Download Loop/FreeAPS Data"
            row.tag = "loopUser"
@@ -240,10 +252,38 @@ class SettingsViewController: FormViewController {
             +++ Section(header: "App Expiration", footer: String(expiration.description))
     
         showHideNSDetails()
-      
-    
+       checkNightscoutStatus()
     }
     
+    func updateStatusLabel(error: NightscoutUtils.NightscoutError?) {
+        if let error = error {
+            switch error {
+            case .invalidURL:
+                statusLabelRow.value = "Invalid URL"
+            case .networkError:
+                statusLabelRow.value = "Network Error"
+            case .invalidToken:
+                statusLabelRow.value = "Invalid Token"
+            case .tokenRequired:
+                statusLabelRow.value = "Token Required"
+            case .siteNotFound:
+                statusLabelRow.value = "Site Not Found"
+            case .unknown:
+                statusLabelRow.value = "Unknown Error"
+            case .emptyAddress:
+                statusLabelRow.value = "Address Empty"
+            }
+        } else {
+            statusLabelRow.value = "OK"
+        }
+        statusLabelRow.updateCell()
+    }
     
-
+    func checkNightscoutStatus() {
+        NightscoutUtils.verifyURLAndToken(urlUser: UserDefaultsRepository.url.value, token: UserDefaultsRepository.token.value) { error in
+            DispatchQueue.main.async {
+                self.updateStatusLabel(error: error)
+            }
+        }
+    }
 }

+ 7 - 0
LoopFollow/ViewControllers/SnoozeViewController.swift

@@ -215,6 +215,13 @@ class SnoozeViewController: UIViewController, UNUserNotificationCenterDelegate {
         guard let alarms = self.tabBarController!.viewControllers?[1] as? AlarmViewController else { return }
         alarms.reloadIsSnoozed(key: "alertCOBIsSnoozed", value: true)
         alarms.reloadSnoozeTime(key: "alertCOBSnoozedTime", setNil: false, value: Date().addingTimeInterval(TimeInterval(snoozeForMinuteStepper.value * 60 * 60)))
+       } else if AlarmSound.whichAlarm == "Low Battery" {
+           UserDefaultsRepository.alertBatteryIsSnoozed.value = true
+
+           UserDefaultsRepository.alertBatterySnoozedTime.value = Date().addingTimeInterval(TimeInterval(snoozeForMinuteStepper.value * 60 * 60))
+           guard let alarms = self.tabBarController!.viewControllers?[1] as? AlarmViewController else { return }
+           alarms.reloadIsSnoozed(key: "alertBatteryIsSnoozed", value: true)
+           alarms.reloadSnoozeTime(key: "alertBatterySnoozedTime", setNil: false, value: Date().addingTimeInterval(TimeInterval(snoozeForMinuteStepper.value * 60 * 60)))
        }
     }
     

+ 74 - 0
LoopFollow/helpers/NightscoutUtils.swift

@@ -0,0 +1,74 @@
+//
+//  NightscoutUtils.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2023-04-09.
+//  Copyright © 2023 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+class NightscoutUtils {
+    enum NightscoutError {
+        case emptyAddress
+        case invalidURL
+        case networkError
+        case siteNotFound
+        case invalidToken
+        case tokenRequired
+        case unknown
+    }
+    
+    static func createURLRequest(url: String, token: String?, path: String) -> URLRequest? {
+        var requestURLString = "\(url)\(path)"
+        
+        if let token = token {
+            let encodedToken = token.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? token
+            requestURLString += "?token=\(encodedToken)"
+        }
+        
+        guard let requestURL = URL(string: requestURLString) else {
+            return nil
+        }
+        
+        var request = URLRequest(url: requestURL)
+        request.httpMethod = "GET"
+        return request
+    }
+    
+    static func verifyURLAndToken(urlUser: String, token: String?, completion: @escaping (NightscoutError?) -> Void) {
+        if urlUser.isEmpty {
+            completion(.emptyAddress)
+            return
+        }
+
+        guard let request = createURLRequest(url: urlUser, token: token, path: "/api/v1/status") else {
+            completion(.invalidURL)
+            return
+        }
+        
+        let task = URLSession.shared.dataTask(with: request) { data, response, error in
+            if let httpResponse = response as? HTTPURLResponse {
+                switch httpResponse.statusCode {
+                case 200:
+                    completion(nil)
+                case 401:
+                    if token == nil || token!.isEmpty {
+                        completion(.tokenRequired)
+                    } else {
+                        completion(.invalidToken) // Change this from "unauthorized"
+                    }
+                default:
+                    completion(.unknown)
+                }
+            }  else {
+                if let _ = error {
+                    completion(.siteNotFound)
+                } else {
+                    completion(.networkError)
+                }
+            }
+        }
+        task.resume()
+    }
+}

+ 16 - 6
LoopFollow/repository/UserDefaults.swift

@@ -16,7 +16,7 @@ import Foundation
 import UIKit
 
 class UserDefaultsRepository {
-
+    
     // DisplayValues total
     static let infoDataTotal = UserDefaultsValue<Int>(key: "infoDataTotal", default: 0)
     static let infoNames = UserDefaultsValue<[String]>(key: "infoNames", default: [
@@ -29,9 +29,10 @@ class UserDefaultsRepository {
         "SAGE",
         "CAGE",
         "Rec. Bolus",
-        "Pred."])
-    static let infoSort = UserDefaultsValue<[Int]>(key: "infoSort", default: [0,1,2,3,4,5,6,7,8,9])
-    static let infoVisible = UserDefaultsValue<[Bool]>(key: "infoVisible", default: [true,true,true,true,true,true,true,true,true,true])
+        "Pred.",
+        "Carbs today"])
+    static let infoSort = UserDefaultsValue<[Int]>(key: "infoSort", default: [0,1,2,3,4,5,6,7,8,9,10])
+    static let infoVisible = UserDefaultsValue<[Bool]>(key: "infoVisible", default: [true,true,true,true,true,true,true,true,true,true,true])
     static let hideInfoTable = UserDefaultsValue<Bool>(key: "hideInfoTable", default: false)
     
     // Nightscout Settings
@@ -83,7 +84,7 @@ class UserDefaultsRepository {
         default: UIApplication.shared.isIdleTimerDisabled,
         onChange: { screenlock in
             UIApplication.shared.isIdleTimerDisabled = screenlock
-    })
+        })
     
     // Advanced Settings
     //static let onlyDownloadBG = UserDefaultsValue<Bool>(key: "onlyDownloadBG", default: false)
@@ -97,7 +98,7 @@ class UserDefaultsRepository {
     static let alwaysDownloadAllBG = UserDefaultsValue<Bool>(key: "alwaysDownloadAllBG", default: true)
     static let bgUpdateDelay = UserDefaultsValue<Int>(key: "bgUpdateDelay", default: 10)
     static let downloadDays = UserDefaultsValue<Int>(key: "downloadDays", default: 1)
-
+    
     
     // Watch Calendar Settings
     static let calendarIdentifier = UserDefaultsValue<String>(key: "calendarIdentifier", default: "")
@@ -428,4 +429,13 @@ class UserDefaultsRepository {
     static let alertCOBAutosnooze = UserDefaultsValue<String>(key: "alertCOBAutosnooze", default: "Never")
     static let alertCOBAutosnoozeDay = UserDefaultsValue<Bool>(key: "alertCOBAutosnoozeDay", default: false)
     static let alertCOBAutosnoozeNight = UserDefaultsValue<Bool>(key: "alertCOBAutosnoozeNight", default: false)
+    
+    static let alertBatteryActive = UserDefaultsValue<Bool>(key: "alertBatteryActive", default: false)
+    static let alertBatteryLevel = UserDefaultsValue<Int>(key: "alertBatteryLevel", default: 25)
+    static let alertBatterySound = UserDefaultsValue<String>(key: "alertBatterySound", default: "Machine_Charge")
+    static let alertBatteryRepeat = UserDefaultsValue<Bool>(key: "alertBatteryRepeat", default: true)
+    static let alertBatteryIsSnoozed = UserDefaultsValue<Bool>(key: "alertBatteryIsSnoozed", default: false)
+    static let alertBatterySnoozedTime = UserDefaultsValue<Date?>(key: "alertBatterySnoozedTime", default: nil)
+    static let alertBatterySnoozeHours = UserDefaultsValue<Int>(key: "alertBatterySnoozeHours", default: 1)
+    static var deviceBatteryLevel: UserDefaultsValue<Double> = UserDefaultsValue(key: "deviceBatteryLevel", default: 100.0)
 }

+ 18 - 71
readme.md

@@ -1,4 +1,19 @@
-Call for developers: Because we have switched to Omnipod 5, we no longer are using Loop or Nightscout. I will not be able to do maintenance on the features related to those. If you are interested in assisting to keep this app maintained and work on new features and improvements for Loop and Nightscout functionality, please reach out.
+> **Message from Jon Fawcett:**
+> * Because our family now uses Omnipod 5, I will not be updating this repository. 
+> * I will also no longer provide an option for a TestFlight invitation from me. 
+> * You must build the app yourself.
+
+> **Message from the Loop and Learn Team:**
+> * We will keep the Loop Follow app going; it will stay at this repository. 
+> * Additional Loop Follow documentation is at [Loop and Learn: Loop Follow](https://www.loopandlearn.org/loop-follow/).
+> * If you are having problems with the app:
+>     * Post in the [Loop and Learn Facebook group](https://www.facebook.com/groups/LOOPandLEARN); indicate that your question is related to Loop Follow. 
+>     * If you do not use Facebook - please click on this [link to file an Issue](https://github.com/jonfawcett/LoopFollow/issues) with your problem.
+
+> **Message to Developers**
+> * If you are interested in assisting with this app and want to work on new features and improvements for Loop, iAPS and Nightscout functionality, please reach out. 
+> * Issues and Pull Requests in GitHub are monitored and will get a response. 
+> * Please always direct your PR to the dev branch.
 
 ## Loop Follow 
 ![screenshot](https://user-images.githubusercontent.com/38429455/93782187-436e8880-fbf8-11ea-8709-e2afba692132.png)
@@ -16,74 +31,9 @@ And there are some functions I've always wished for and not found anywhere such
 for those nights when Loop is stuck on high and you open loop with a correction. This lets you set a higher
 low alert for the BG you want to wake up to and close Loop.
 
-### App Store Beta Test Install
-If you would like to be part of our beta testing for the App Store and install that version without needing to build it yourself, please submit the form here: https://customtypeone.com/blogs/news/loop-follow-beta-testing
-
 ### Building Options
 
-### GitHub Browser Build
-
-Loop Follow can be built using a paid Apple Developer Account on any computer using the GitHub Browser Build method as explained (tersely) [here](fastlane/testflight.md) or with more detail in [LoopDocs: GitHub Browser Build: Other Apps](https://loopkit.github.io/loopdocs/gh-actions/gh-other-apps).
-
-If you choose GitHub Browser Build and want to run Loop Follow on your Mac, you need to install the TestFlight app on your Mac. The TestFlight app shows the same set of builds and uses the same installation procedure as shown in LoopDocs for installing apps on a phone from TestFlight; just do it on your Mac.
-
-Note:
-
-If you have a Loop Follow app in your Applications folder from a prior Mac/Xcode build and then install from TestFlight:
-
-* TestFlight does not ask if you want to replace your app - you get a second app, Loop Follow 2.app, in your Applications folder
-* You can then delete the original Loop Follow.app and rename the TestFlight version from Loop Follow 2.app to Loop Follow.app
-* Subsequent installations from TestFlight overwrite the app
-* Your settings are maintained regardless of the app name
-
-### Build using Mac/Xcode
-
-Loop Follow can be built using a Mac computer with Xcode as described below (tersely) or with more detail using the Build-Select script, see [Loop and Learn: Build-Select Script](https://www.loopandlearn.org/build-select) or [LoopDocs: Build Select Script](https://loopkit.github.io/loopdocs/build/step14/#build-select-script) and choose to build Loop Follow.
-
-1. Open Terminal
-2. copy/paste this code into terminal and hit enter:
-```
-/bin/bash -c "$(curl -fsSL https://git.io/JTKEt)"
-```
-3. Follow instructions in terminal
-4. Plugin your phone or ipad, select your signing team, select your phone or ipad or "my mac" (Big Sur or later operating systems), and click play.
-
-Note: Tested this with Ventura and Xcode 14.2, and successfully built to
-
-* My Mac (Mac Catalyst)
-* My Mac (Mac Catalyst, Rosetta)
-* My Mac (Designed for iPad)
-    * I did not test the app on the iPad, but it built with no problem
-
-### Run Loop Follow on Mac
-
-To run Loop Follow on your Mac, you need to move the app to your Applications folder.
-
-After building to your Mac:
-1. Click stop to close the running app
-1. On the left side of Xcode, click on the Folder icon
-    * Click to open the LoopFollow folder list
-    * Click to open the LoopFollow/Products folder
-    * Right click (or Cntl-click) on "Loop Follow.app" and select Show in Finder
-4. Drag the Loop Follow.app icon to your Applications folder in finder (see Note below)
-5. From Mac system settings/notifications, scroll down to Loop Follow and enable notifications with the options you want. For instance, Badge app icon will allow the BG reading to display on the icon.
-
-Note:
-
-If you have a Loop Follow app in your Applications folder from a prior TestFlight installation, then when you drag the Loop (from Finder location) to Applications, you will be asked if you want to Replace or Keep Both. Choose Replace.
-
-** Big Sur **
-Some things do not work correctly yet in Big Sur
-- Background Refresh. Mac apps stay open when minimized, so this is unneeded. Please disable the toggle switch.
-- Disable Keep Screen Active. I haven't tested, but doubt this will do anything.
-- Watch/Carplay calendar selection does not work
-- Alarms will not override the Mac volume or mute.
-
-Build Instruction Video: https://youtu.be/s07QPZ7xycY
-
-Special thanks to Spike-App, NSApple, and Nightguard for helping me figure out how to do a lot of the code for this.
-
-If you want to contribute, the biggest needs today are to make the code cleaner and more efficient, and create the basis for a watch app.
+Please see [Loop and Learn: Loop Follow](https://www.loopandlearn.org/loop-follow/) for all the building options.
 
 ### General feature list
 - scrollable/scalable graph display with BG, basal, bolus, and carb details plus Loop status, Loop Prediction, and the General NS Care portal info.
@@ -97,9 +47,6 @@ If you want to contribute, the biggest needs today are to make the code cleaner
 - calendar entries to use watch complication with BG, arrow, delta, cob, iob and minutes ago (if old reading).
 - background silent audio to keep iOS from killing the app. This is why it can’t go in the App Store for just a simple download.
 
-### Contributing, Building, and Branches
-- New code will be pushed to the Dev branch as soon as it has been added. It might be very rough around the edges. Once it has been thoroughly tested, it will be merged to Master. If you are even remotely adventurous, please build Dev to help test the new features as they are added.
-- If you want to contribute, please PR on Dev unless it is an important bug fix to address in Master
-
 ### Open Source DIY
 - This is a DIY open source project that may or may not function as you expect. You take full responsibility for building and running this app and do so at your own risk.
+