Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions LoopFollow/Controllers/Nightscout/DeviceStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ extension MainViewController {
)

} else if secondsAgo >= (5 * 60) {
self.evaluateRemoteCommandPollingCompletion()
TaskScheduler.shared.rescheduleTask(
id: .deviceStatus,
to: Date().addingTimeInterval(10)
Expand Down
1 change: 1 addition & 0 deletions LoopFollow/Controllers/Nightscout/Treatments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,5 +184,6 @@ extension MainViewController {
}
}
processCage(entries: pumpSiteChange)
evaluateRemoteCommandPollingCompletion()
}
}
1 change: 1 addition & 0 deletions LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ struct LoopAPNSBolusView: View {
TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
self.alertMessage = "Insulin sent successfully!"
self.alertType = .success
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
LogManager.shared.log(
category: .apns,
message: "Insulin sent - Amount: \(insulinAmount.doubleValue(for: .internationalUnit()))U"
Expand Down
1 change: 1 addition & 0 deletions LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ struct LoopAPNSCarbsView: View {
timeFormatter.timeStyle = .short
self.alertMessage = "Carbs sent successfully for \(timeFormatter.string(from: adjustedConsumedDate))!"
self.alertType = .success
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
LogManager.shared.log(
category: .apns,
message: "Carbs sent - Amount: \(carbsAmount.doubleValue(for: .gram()))g, Absorption: \(absorptionTimeString)h, Time: \(adjustedConsumedDate)"
Expand Down
2 changes: 2 additions & 0 deletions LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ class OverridePresetsViewModel: ObservableObject {
self.isActivating = false
self.statusMessage = "\(preset.name) override activated successfully."
self.alertType = .statusSuccess
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
self.showAlert = true
}
} catch {
Expand All @@ -430,6 +431,7 @@ class OverridePresetsViewModel: ObservableObject {
self.isActivating = false
self.statusMessage = "Active override cancelled successfully."
self.alertType = .statusSuccess
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
self.showAlert = true
}
} catch {
Expand Down
4 changes: 4 additions & 0 deletions LoopFollow/Remote/RemoteType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ enum RemoteType: String, Codable {
case trc = "Trio Remote Control"
case loopAPNS = "Loop APNS"
}

extension Notification.Name {
static let remoteCommandSucceeded = Notification.Name("remoteCommandSucceeded")
}
1 change: 1 addition & 0 deletions LoopFollow/Remote/TRC/BolusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ struct BolusView: View {
category: .apns,
message: "sendBolusPushNotification succeeded - Bolus: \(InsulinFormatter.shared.string(bolusAmount)) U"
)
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
bolusAmount = HKQuantity(unit: .internationalUnit(), doubleValue: 0.0)
alertType = .statusSuccess
} else {
Expand Down
1 change: 1 addition & 0 deletions LoopFollow/Remote/TRC/MealView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ struct MealView: View {
category: .apns,
message: "sendMealPushNotification succeeded - Carbs: \(carbs.doubleValue(for: .gram())) g, Protein: \(protein.doubleValue(for: .gram())) g, Fat: \(fat.doubleValue(for: .gram())) g, Bolus: \(bolusAmount.doubleValue(for: .internationalUnit())) U, Scheduled: \(scheduledDate != nil ? formatDate(scheduledDate!) : "now")"
)
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)

// Reset meal values and scheduled data after success
carbs = HKQuantity(unit: .gram(), doubleValue: 0.0)
Expand Down
2 changes: 2 additions & 0 deletions LoopFollow/Remote/TRC/OverrideView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ struct OverrideView: View {
self.statusMessage = "Override command sent successfully."
self.alertType = .statusSuccess
LogManager.shared.log(category: .apns, message: "sendOverridePushNotification succeeded for override: \(override.name)")
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
} else {
self.statusMessage = errorMessage ?? "Failed to send override command."
self.alertType = .statusFailure
Expand All @@ -195,6 +196,7 @@ struct OverrideView: View {
self.statusMessage = "Cancel override command sent successfully."
self.alertType = .statusSuccess
LogManager.shared.log(category: .apns, message: "sendCancelOverridePushNotification succeeded")
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
} else {
self.statusMessage = errorMessage ?? "Failed to send cancel override command."
self.alertType = .statusFailure
Expand Down
2 changes: 2 additions & 0 deletions LoopFollow/Remote/TRC/TempTargetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ struct TempTargetView: View {
self.statusMessage = "Temp target command successfully sent."
self.alertType = .statusSuccess
LogManager.shared.log(category: .apns, message: "sendTempTargetPushNotification succeeded with target: \(newHKTarget), duration: \(duration)")
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
} else {
self.statusMessage = errorMessage ?? "Failed to send temp target command."
self.alertType = .statusFailure
Expand All @@ -275,6 +276,7 @@ struct TempTargetView: View {
self.statusMessage = "Cancel temp target command successfully sent."
self.alertType = .statusSuccess
LogManager.shared.log(category: .apns, message: "sendCancelTempTargetPushNotification succeeded")
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
} else {
self.statusMessage = errorMessage ?? "Failed to send cancel temp target command."
self.alertType = .statusFailure
Expand Down
2 changes: 2 additions & 0 deletions LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class TrioNightscoutRemoteController {
let response: [TreatmentCancelResponse] = try await NightscoutUtils.executePostRequest(eventType: .treatments, body: tempTargetBody)
Observable.shared.tempTarget.value = nil
NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
completion(true)
} catch {
completion(false)
Expand All @@ -42,6 +43,7 @@ class TrioNightscoutRemoteController {
let response: [TreatmentResponse] = try await NightscoutUtils.executePostRequest(eventType: .treatments, body: tempTargetBody)
Observable.shared.tempTarget.value = newTarget
NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
completion(true)
} catch {
completion(false)
Expand Down
105 changes: 105 additions & 0 deletions LoopFollow/ViewControllers/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ func IsNightscoutEnabled() -> Bool {
}

class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate, UIScrollViewDelegate {
private struct RemoteCommandDataSignature {
let carbTimestamp: TimeInterval?
let bolusTimestamp: TimeInterval?
let overrideTimestamp: TimeInterval?
let overrideStateKey: String

func detectsFreshData(comparedTo baseline: RemoteCommandDataSignature) -> Bool {
if let carbTimestamp, carbTimestamp > (baseline.carbTimestamp ?? 0) {
return true
}

if let bolusTimestamp, bolusTimestamp > (baseline.bolusTimestamp ?? 0) {
return true
}

if let overrideTimestamp, overrideTimestamp > (baseline.overrideTimestamp ?? 0) {
return true
}

return overrideStateKey != baseline.overrideStateKey
}
}

var isPresentedAsModal: Bool = false

@IBOutlet var BGText: UILabel!
Expand Down Expand Up @@ -136,6 +159,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
"deviceStatus": false,
]
private var loadingTimeoutTimer: Timer?
private var remoteCommandPollingTimer: Timer?
private var remoteCommandPollingStartedAt: Date?
private var remoteCommandPollingBaseline: RemoteCommandDataSignature?
private let remoteCommandPollingInterval: TimeInterval = 3
private let remoteCommandPollingDuration: TimeInterval = 30

override func viewDidLoad() {
super.viewDidLoad()
Expand Down Expand Up @@ -239,6 +267,13 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
refreshScrollView.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: NSNotification.Name("refresh"), object: nil)

NotificationCenter.default.publisher(for: .remoteCommandSucceeded)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.startRemoteCommandPolling()
}
.store(in: &cancellables)

Observable.shared.bgText.$value
.receive(on: DispatchQueue.main)
.sink { [weak self] newValue in
Expand Down Expand Up @@ -824,6 +859,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
}

deinit {
remoteCommandPollingTimer?.invalidate()
NotificationCenter.default.removeObserver(self, name: NSNotification.Name("refresh"), object: nil)
}

Expand Down Expand Up @@ -864,6 +900,75 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
refreshControl.endRefreshing()
}

private func startRemoteCommandPolling() {
guard IsNightscoutEnabled() else { return }

remoteCommandPollingBaseline = currentRemoteCommandDataSignature()
remoteCommandPollingStartedAt = Date()
remoteCommandPollingTimer?.invalidate()

performRemoteCommandPollingTick()

let timer = Timer.scheduledTimer(withTimeInterval: remoteCommandPollingInterval, repeats: true) { [weak self] _ in
self?.performRemoteCommandPollingTick()
}
timer.tolerance = 0.5
remoteCommandPollingTimer = timer

LogManager.shared.log(category: .general, message: "Started aggressive polling after remote command success")
}

private func stopRemoteCommandPolling(reason: String) {
guard remoteCommandPollingTimer != nil || remoteCommandPollingStartedAt != nil else { return }

remoteCommandPollingTimer?.invalidate()
remoteCommandPollingTimer = nil
remoteCommandPollingStartedAt = nil
remoteCommandPollingBaseline = nil

LogManager.shared.log(category: .general, message: "Stopped aggressive polling: \(reason)")
}

private func performRemoteCommandPollingTick() {
guard let remoteCommandPollingStartedAt else { return }

if Date().timeIntervalSince(remoteCommandPollingStartedAt) >= remoteCommandPollingDuration {
stopRemoteCommandPolling(reason: "timeout reached")
return
}

bgTaskAction()
deviceStatusAction()
treatmentsTaskAction()
}

private func currentRemoteCommandDataSignature() -> RemoteCommandDataSignature {
let latestBolusTimestamp = max(bolusData.last?.date ?? 0, smbData.last?.date ?? 0)
let overrideNote = Observable.shared.override.value ?? ""
let overrideStateKey: String

if currentOverride != 1.0 || !overrideNote.isEmpty {
overrideStateKey = "\(currentOverride)|\(overrideNote)"
} else {
overrideStateKey = ""
}

return RemoteCommandDataSignature(
carbTimestamp: carbData.last?.date,
bolusTimestamp: latestBolusTimestamp > 0 ? latestBolusTimestamp : nil,
overrideTimestamp: overrideGraphData.last?.date,
overrideStateKey: overrideStateKey
)
}

func evaluateRemoteCommandPollingCompletion() {
guard let remoteCommandPollingBaseline else { return }

if currentRemoteCommandDataSignature().detectsFreshData(comparedTo: remoteCommandPollingBaseline) {
stopRemoteCommandPolling(reason: "fresh remote data received")
}
}

// Scroll down BGText when refreshing
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == refreshScrollView {
Expand Down