diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 9a11022d6..d06c4b829 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -425,6 +425,9 @@ FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCFEEC9D2486E68E00402A7F /* WebKit.framework */; }; FCFEECA02488157B00402A7F /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEEC9F2488157B00402A7F /* Chart.swift */; }; FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEECA1248857A600402A7F /* SettingsViewController.swift */; }; + DD50C10A2F60A00000000001 /* SocketIO in Frameworks */ = {isa = PBXBuildFile; productRef = DD50C10A2F60A00000000003 /* SocketIO */; }; + DD50C10A2F60B00000000002 /* NightscoutSocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */; }; + DD50C10A2F60B00000000004 /* NightscoutSocketDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -574,6 +577,8 @@ DD493AE42ACF2383009A6922 /* Treatments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Treatments.swift; sourceTree = ""; }; DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = ""; }; DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = ""; }; + DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSocketManager.swift; sourceTree = ""; }; + DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSocketDataHandler.swift; sourceTree = ""; }; DD4A407D2E6AFEE6007B318B /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeOfDay.swift; sourceTree = ""; }; DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmConfiguration.swift; sourceTree = ""; }; @@ -911,6 +916,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DD50C10A2F60A00000000001 /* SocketIO in Frameworks */, FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */, 3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */, ); @@ -1155,6 +1161,8 @@ DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */, DD0C0C612C4175FD00DBADDF /* NSProfile.swift */, DD5334222C60ED3600062F9D /* IAge.swift */, + DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */, + DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */, ); path = Nightscout; sourceTree = ""; @@ -1783,6 +1791,7 @@ ); name = LoopFollow; packageProductDependencies = ( + DD50C10A2F60A00000000003 /* SocketIO */, ); productName = LoopFollow; productReference = FC9788142485969B00A7906C /* Loop Follow.app */; @@ -1820,6 +1829,7 @@ ); mainGroup = FC97880B2485969B00A7906C; packageReferences = ( + DD50C10A2F60A00000000002 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */, ); productRefGroup = FC9788152485969B00A7906C /* Products */; projectDirPath = ""; @@ -2190,6 +2200,8 @@ DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */, 374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */, DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */, + DD50C10A2F60B00000000002 /* NightscoutSocketManager.swift in Sources */, + DD50C10A2F60B00000000004 /* NightscoutSocketDataHandler.swift in Sources */, DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */, FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */, DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */, @@ -2802,6 +2814,25 @@ versionGroupType = wrapper.xcdatamodel; }; /* End XCVersionGroup section */ + +/* Begin XCRemoteSwiftPackageReference section */ + DD50C10A2F60A00000000002 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/socketio/socket.io-client-swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 16.1.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + DD50C10A2F60A00000000003 /* SocketIO */ = { + isa = XCSwiftPackageProductDependency; + package = DD50C10A2F60A00000000002 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */; + productName = SocketIO; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = FC97880C2485969B00A7906C /* Project object */; } diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index d97aba24c..f7f1eda52 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -176,6 +176,10 @@ extension MainViewController { TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(3)) } + if NightscoutSocketManager.shared.connectionState == .authenticated { + delayToSchedule = max(delayToSchedule * 3, 60) + } + TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date().addingTimeInterval(delayToSchedule)) // Evaluate speak conditions if there is a previous value. diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index ff2b13a78..fce835b54 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -213,37 +213,28 @@ extension MainViewController { let secondsAgo = now - (Observable.shared.alertLastLoopTime.value ?? 0) DispatchQueue.main.async { + var interval: Double if secondsAgo >= (20 * 60) { - TaskScheduler.shared.rescheduleTask( - id: .deviceStatus, - to: Date().addingTimeInterval(5 * 60) - ) - + interval = 5 * 60 } else if secondsAgo >= (10 * 60) { - TaskScheduler.shared.rescheduleTask( - id: .deviceStatus, - to: Date().addingTimeInterval(60) - ) - + interval = 60 } else if secondsAgo >= (7 * 60) { - TaskScheduler.shared.rescheduleTask( - id: .deviceStatus, - to: Date().addingTimeInterval(30) - ) - + interval = 30 } else if secondsAgo >= (5 * 60) { - TaskScheduler.shared.rescheduleTask( - id: .deviceStatus, - to: Date().addingTimeInterval(10) - ) + interval = 10 } else { - let interval = (310 - secondsAgo) - TaskScheduler.shared.rescheduleTask( - id: .deviceStatus, - to: Date().addingTimeInterval(interval) - ) + interval = 310 - secondsAgo TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(3)) } + + if NightscoutSocketManager.shared.connectionState == .authenticated { + interval = max(interval * 3, 60) + } + + TaskScheduler.shared.rescheduleTask( + id: .deviceStatus, + to: Date().addingTimeInterval(interval) + ) } evaluateNotLooping() diff --git a/LoopFollow/Controllers/Nightscout/NightscoutSocketDataHandler.swift b/LoopFollow/Controllers/Nightscout/NightscoutSocketDataHandler.swift new file mode 100644 index 000000000..054491c68 --- /dev/null +++ b/LoopFollow/Controllers/Nightscout/NightscoutSocketDataHandler.swift @@ -0,0 +1,54 @@ +// LoopFollow +// NightscoutSocketDataHandler.swift + +import Foundation + +extension MainViewController { + func setupNightscoutSocket() { + NightscoutSocketManager.shared.onDataUpdate = { [weak self] data in + self?.handleSocketDataUpdate(data) + } + NightscoutSocketManager.shared.connectIfNeeded() + } + + func handleSocketDataUpdate(_ data: [String: Any]) { + let isDelta = data["delta"] as? Bool ?? false + + if !isDelta { + // Full data on initial connect — trigger all fetches + LogManager.shared.log(category: .websocket, message: "Full data received, triggering all fetches") + TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date()) + TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date()) + TaskScheduler.shared.rescheduleTask(id: .treatments, to: Date()) + TaskScheduler.shared.rescheduleTask(id: .profile, to: Date()) + return + } + + // Selective: only fetch data types present in the delta + var triggered: [String] = [] + + if data["sgvs"] != nil || data["mbgs"] != nil { + TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date()) + triggered.append("BG") + } + + if data["devicestatus"] != nil { + TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date()) + triggered.append("DeviceStatus") + } + + if data["treatments"] != nil { + TaskScheduler.shared.rescheduleTask(id: .treatments, to: Date()) + triggered.append("Treatments") + } + + if data["profiles"] != nil { + TaskScheduler.shared.rescheduleTask(id: .profile, to: Date()) + triggered.append("Profile") + } + + if !triggered.isEmpty { + LogManager.shared.log(category: .websocket, message: "Delta triggered: \(triggered.joined(separator: ", "))", isDebug: true) + } + } +} diff --git a/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift new file mode 100644 index 000000000..2831d7ef3 --- /dev/null +++ b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift @@ -0,0 +1,187 @@ +// LoopFollow +// NightscoutSocketManager.swift + +import Foundation +import SocketIO + +class NightscoutSocketManager { + static let shared = NightscoutSocketManager() + + enum ConnectionState: String { + case disconnected + case connecting + case connected + case authenticated + case error + } + + private(set) var connectionState: ConnectionState = .disconnected { + didSet { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .nightscoutSocketStateChanged, object: nil) + } + } + } + + private var manager: SocketManager? + private var socket: SocketIOClient? + private var currentURL: String = "" + private var currentToken: String = "" + + var onDataUpdate: (([String: Any]) -> Void)? + + private init() {} + + // MARK: - Public API + + func connectIfNeeded() { + guard Storage.shared.webSocketEnabled.value else { + disconnect() + return + } + + let url = Storage.shared.url.value + let token = Storage.shared.token.value + + guard !url.isEmpty else { + disconnect() + return + } + + // Already connected to the same URL + if connectionState == .authenticated || connectionState == .connecting || connectionState == .connected { + if url == currentURL, token == currentToken { + return + } + // URL or token changed, reconnect + disconnect() + } + + currentURL = url + currentToken = token + connect() + } + + func reconnectIfNeeded() { + guard Storage.shared.webSocketEnabled.value, !currentURL.isEmpty else { return } + + if connectionState == .disconnected || connectionState == .error { + connect() + } + } + + func disconnect() { + socket?.removeAllHandlers() + socket?.disconnect() + manager?.disconnect() + manager = nil + socket = nil + connectionState = .disconnected + currentURL = "" + currentToken = "" + } + + // MARK: - Private + + private func connect() { + guard let url = URL(string: currentURL) else { + LogManager.shared.log(category: .websocket, message: "Invalid Nightscout URL for WebSocket") + connectionState = .error + return + } + + connectionState = .connecting + + var config: SocketIOClientConfiguration = [ + .log(false), + .compress, + .forceWebsockets(false), + .reconnects(true), + .reconnectWait(5), + .reconnectWaitMax(30), + ] + + if !currentToken.isEmpty { + config.insert(.connectParams(["token": currentToken])) + } + + manager = SocketManager(socketURL: url, config: config) + + guard let mgr = manager else { return } + socket = mgr.defaultSocket + + setupEventHandlers() + socket?.connect() + + LogManager.shared.log(category: .websocket, message: "Connecting to Nightscout WebSocket at \(currentURL)") + } + + private func setupEventHandlers() { + guard let socket = socket else { return } + + socket.on(clientEvent: .connect) { [weak self] _, _ in + guard let self = self else { return } + LogManager.shared.log(category: .websocket, message: "Socket connected, authorizing...") + self.connectionState = .connected + self.authorize() + } + + socket.on(clientEvent: .disconnect) { [weak self] data, _ in + guard let self = self else { return } + let reason = (data.first as? String) ?? "unknown" + LogManager.shared.log(category: .websocket, message: "Socket disconnected: \(reason)") + self.connectionState = .disconnected + // Immediately restore normal polling intervals + NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) + } + + socket.on(clientEvent: .reconnect) { _, _ in + LogManager.shared.log(category: .websocket, message: "Socket reconnecting...") + } + + socket.on(clientEvent: .error) { [weak self] data, _ in + let errorMsg = (data.first as? String) ?? "unknown error" + LogManager.shared.log(category: .websocket, message: "Socket error: \(errorMsg)") + self?.connectionState = .error + } + + socket.on("connected") { [weak self] _, _ in + guard let self = self else { return } + LogManager.shared.log(category: .websocket, message: "Authorized and receiving data") + self.connectionState = .authenticated + } + + socket.on("dataUpdate") { [weak self] data, _ in + guard let self = self, + let payload = data.first as? [String: Any] + else { return } + + LogManager.shared.log(category: .websocket, message: "Received dataUpdate (delta: \(payload["delta"] as? Bool ?? false))", isDebug: true) + + DispatchQueue.main.async { + self.onDataUpdate?(payload) + } + } + } + + private func authorize() { + var authPayload: [String: Any] = [ + "client": "LoopFollow", + "history": 1, + ] + + // Nightscout's authorization.resolve() expects: + // - "token" field for JWT tokens (verified via verifyJWT) + // - "secret" field for access tokens (checked via doesAccessTokenExist) + // LoopFollow uses access tokens (e.g. "readable-xxxx"), so pass as "secret". + if !currentToken.isEmpty { + authPayload["secret"] = currentToken + } + + socket?.emit("authorize", authPayload) + } +} + +extension Notification.Name { + static let nightscoutSocketStateChanged = Notification.Name("nightscoutSocketStateChanged") +} diff --git a/LoopFollow/Log/LogManager.swift b/LoopFollow/Log/LogManager.swift index 22403cd0f..c31747edf 100644 --- a/LoopFollow/Log/LogManager.swift +++ b/LoopFollow/Log/LogManager.swift @@ -29,6 +29,7 @@ class LogManager { case calendar = "Calendar" case deviceStatus = "Device Status" case remote = "Remote" + case websocket = "WebSocket" } init() { diff --git a/LoopFollow/Nightscout/NightscoutSettingsView.swift b/LoopFollow/Nightscout/NightscoutSettingsView.swift index 7c08e756f..187bd8176 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsView.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsView.swift @@ -12,6 +12,7 @@ struct NightscoutSettingsView: View { urlSection tokenSection statusSection + webSocketSection importSection } .onDisappear { @@ -56,6 +57,59 @@ struct NightscoutSettingsView: View { } } + @State private var showWebSocketInfo = false + + private var webSocketSection: some View { + Section(header: webSocketSectionHeader) { + Toggle("Enable WebSocket", isOn: $viewModel.webSocketEnabled) + if viewModel.webSocketEnabled { + HStack { + Text("Status") + Spacer() + Text(viewModel.webSocketStatus) + .foregroundColor(viewModel.webSocketStatusColor) + } + } + } + .sheet(isPresented: $showWebSocketInfo) { + NavigationStack { + ScrollView { + Text(""" + When enabled, LoopFollow maintains a live connection to your Nightscout server using WebSocket. This allows data updates (new glucose readings, treatments, device status) to arrive within seconds instead of waiting for the next polling cycle. + + Polling continues at reduced frequency as a safety net. If the WebSocket connection drops, normal polling resumes immediately. + + This feature may affect battery usage. On WiFi, impact is minimal. On cellular, the persistent connection may prevent the radio from entering idle mode. + """) + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } + .navigationTitle("Real-time Updates") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { showWebSocketInfo = false } + } + } + } + .presentationDetents([.medium]) + .presentationDragIndicator(.visible) + } + } + + private var webSocketSectionHeader: some View { + HStack(spacing: 4) { + Text("Real-time Updates") + Button { + showWebSocketInfo = true + } label: { + Image(systemName: "info.circle") + .foregroundStyle(Color.accentColor) + } + .buttonStyle(.plain) + } + } + private var importSection: some View { Section(header: Text("Import Settings")) { NavigationLink(destination: ImportExportSettingsView()) { diff --git a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index 299a73a7d..07cdccf7f 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -3,6 +3,7 @@ import Combine import Foundation +import SwiftUI protocol NightscoutSettingsViewModelDelegate: AnyObject { func nightscoutSettingsDidFinish() @@ -34,6 +35,29 @@ class NightscoutSettingsViewModel: ObservableObject { @Published var nightscoutStatus: String = "Checking..." + @Published var webSocketEnabled: Bool = Storage.shared.webSocketEnabled.value { + didSet { + Storage.shared.webSocketEnabled.value = webSocketEnabled + if webSocketEnabled { + NightscoutSocketManager.shared.connectIfNeeded() + } else { + NightscoutSocketManager.shared.disconnect() + triggerRefresh() + } + } + } + + @Published var webSocketStatus: String = "Disconnected" + + var webSocketStatusColor: Color { + switch NightscoutSocketManager.shared.connectionState { + case .authenticated: return .green + case .connecting, .connected: return .orange + case .disconnected: return .secondary + case .error: return .red + } + } + private var cancellables = Set() private var checkStatusSubject = PassthroughSubject() private var checkStatusWorkItem: DispatchWorkItem? @@ -44,6 +68,7 @@ class NightscoutSettingsViewModel: ObservableObject { setupDebounce() checkNightscoutStatus() + observeWebSocketState() } private func setupDebounce() { @@ -123,6 +148,7 @@ class NightscoutSettingsViewModel: ObservableObject { case .emptyAddress: nightscoutStatus = "Address Empty" } + NightscoutSocketManager.shared.disconnect() } else { let authStatus: String if Storage.shared.nsAdminAuth.value { @@ -142,4 +168,28 @@ class NightscoutSettingsViewModel: ObservableObject { func dismiss() { delegate?.nightscoutSettingsDidFinish() } + + private func triggerRefresh() { + NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) + } + + private func observeWebSocketState() { + updateWebSocketStatus() + NotificationCenter.default.publisher(for: .nightscoutSocketStateChanged) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateWebSocketStatus() + } + .store(in: &cancellables) + } + + private func updateWebSocketStatus() { + switch NightscoutSocketManager.shared.connectionState { + case .disconnected: webSocketStatus = "Disconnected" + case .connecting: webSocketStatus = "Connecting..." + case .connected: webSocketStatus = "Connected" + case .authenticated: webSocketStatus = "Connected" + case .error: webSocketStatus = "Error" + } + } } diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 11997da35..24dc245df 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -192,6 +192,7 @@ class Storage { var device = StorageValue(key: "device", defaultValue: "") var nsWriteAuth = StorageValue(key: "nsWriteAuth", defaultValue: false) var nsAdminAuth = StorageValue(key: "nsAdminAuth", defaultValue: false) + var webSocketEnabled = StorageValue(key: "webSocketEnabled", defaultValue: false) var migrationStep = StorageValue(key: "migrationStep", defaultValue: 0) diff --git a/LoopFollow/Task/ProfileTask.swift b/LoopFollow/Task/ProfileTask.swift index 72de336fb..6b4358ca7 100644 --- a/LoopFollow/Task/ProfileTask.swift +++ b/LoopFollow/Task/ProfileTask.swift @@ -21,6 +21,10 @@ extension MainViewController { webLoadNSProfile() - TaskScheduler.shared.rescheduleTask(id: .profile, to: Date().addingTimeInterval(10 * 60)) + var interval: TimeInterval = 10 * 60 + if NightscoutSocketManager.shared.connectionState == .authenticated { + interval = 30 * 60 + } + TaskScheduler.shared.rescheduleTask(id: .profile, to: Date().addingTimeInterval(interval)) } } diff --git a/LoopFollow/Task/TreatmentsTask.swift b/LoopFollow/Task/TreatmentsTask.swift index d3a0c620e..4e7aa0bc9 100644 --- a/LoopFollow/Task/TreatmentsTask.swift +++ b/LoopFollow/Task/TreatmentsTask.swift @@ -21,7 +21,11 @@ extension MainViewController { WebLoadNSTreatments() - TaskScheduler.shared.rescheduleTask(id: .treatments, to: Date().addingTimeInterval(2 * 60)) + var interval: TimeInterval = 2 * 60 + if NightscoutSocketManager.shared.connectionState == .authenticated { + interval = 10 * 60 + } + TaskScheduler.shared.rescheduleTask(id: .treatments, to: Date().addingTimeInterval(interval)) TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(3)) } } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index ac1f19a24..1b733ae30 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -206,6 +206,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele showHideNSDetails() scheduleAllTasks() + setupNightscoutSocket() // Set up refreshScrollView for BGText refreshScrollView = UIScrollView() @@ -882,6 +883,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele MinAgoText.text = "Refreshing" Observable.shared.minAgoText.value = "Refreshing" scheduleAllTasks() + NightscoutSocketManager.shared.connectIfNeeded() currentCage = nil currentSage = nil @@ -1081,6 +1083,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } TaskScheduler.shared.checkTasksNow() + NightscoutSocketManager.shared.reconnectIfNeeded() checkAndNotifyVersionStatus() checkAppExpirationStatus()