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
29 changes: 21 additions & 8 deletions LoopFollow/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let notificationCenter = UNUserNotificationCenter.current()

private func logRemoteCommandNotificationDetails(userInfo: [AnyHashable: Any]) -> Bool {
let commandStatus = userInfo["command_status"] as? String
let commandType = userInfo["command_type"] as? String

if let commandStatus {
LogManager.shared.log(category: .general, message: "Command status: \(commandStatus)")
}

if let commandType {
LogManager.shared.log(category: .general, message: "Command type: \(commandType)")
}

return commandStatus != nil || commandType != nil
}

func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
LogManager.shared.log(category: .general, message: "App started")
LogManager.shared.cleanupOldLogs()
Expand Down Expand Up @@ -82,14 +97,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Handle silent notification (content-available)
if let contentAvailable = aps["content-available"] as? Int, contentAvailable == 1 {
// This is a silent push, nothing implemented but logging for now

if let commandStatus = userInfo["command_status"] as? String {
LogManager.shared.log(category: .general, message: "Command status: \(commandStatus)")
}

if let commandType = userInfo["command_type"] as? String {
LogManager.shared.log(category: .general, message: "Command type: \(commandType)")
}
_ = logRemoteCommandNotificationDetails(userInfo: userInfo)
}
}

Expand Down Expand Up @@ -199,6 +207,11 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let userInfo = notification.request.content.userInfo
LogManager.shared.log(category: .general, message: "Will present notification: \(userInfo)")

if logRemoteCommandNotificationDetails(userInfo: userInfo) {
NotificationCenter.default.post(name: .remoteCommandSucceeded, object: nil)
LogManager.shared.log(category: .general, message: "Started remote command polling from foreground result notification")
}

// Show the notification even when app is in foreground
completionHandler([.banner, .sound, .badge])
}
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()
}
}
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")
}
118 changes: 118 additions & 0 deletions LoopFollow/ViewControllers/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,39 @@ func IsNightscoutEnabled() -> Bool {
}

class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate, UIScrollViewDelegate {
private struct RemoteCommandDataSignature {
let carbTimestamp: TimeInterval?
let bolusTimestamp: TimeInterval?
let overrideTimestamp: TimeInterval?
let tempTargetTimestamp: TimeInterval?
let overrideStateKey: String
let tempTargetStateKey: 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
}

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

if overrideStateKey != baseline.overrideStateKey {
return true
}

return tempTargetStateKey != baseline.tempTargetStateKey
}
}

var isPresentedAsModal: Bool = false

@IBOutlet var BGText: UILabel!
Expand Down Expand Up @@ -136,6 +169,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 +277,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 +869,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 +910,78 @@ 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 result notification")
}

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 = overrideNote
let tempTargetStateKey: String

if let tempTarget = Observable.shared.tempTarget.value {
tempTargetStateKey = Localizer.formatQuantity(tempTarget)
} else {
tempTargetStateKey = ""
}

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

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