Skip to content
Merged
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
2 changes: 1 addition & 1 deletion LoopFollow/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
_ = BLEManager.shared
// Ensure VolumeButtonHandler is initialized so it can receive alarm notifications
_ = VolumeButtonHandler.shared

WatchConnectivityManager.shared.activate()

// Register for remote notifications
Expand Down
2 changes: 1 addition & 1 deletion LoopFollow/Controllers/Nightscout/ProfileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ final class ProfileManager {
}

if let trioPresets = profileData.trioOverrides {
self.trioOverrides = trioPresets.map { entry in
trioOverrides = trioPresets.map { entry in
let targetQuantity = entry.target != nil ? HKQuantity(unit: .milligramsPerDeciliter, doubleValue: entry.target!) : nil
return TrioOverride(
name: entry.name,
Expand Down
3 changes: 3 additions & 0 deletions LoopFollow/LiveActivity/APNsCredentialValidator.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// LoopFollow
// APNsCredentialValidator.swift

import Foundation

enum APNsCredentialValidator {
Expand Down
3 changes: 1 addition & 2 deletions LoopFollow/LiveActivity/LAAppGroupSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ struct WatchOverridePreset: Codable, Identifiable {
var id: String { name }
var name: String
var symbol: String?
var durationSeconds: TimeInterval // 0 = indefinite
var durationSeconds: TimeInterval // 0 = indefinite
}

// MARK: - App Group settings
Expand Down Expand Up @@ -255,7 +255,6 @@ enum LAAppGroupSettings {
return raw.compactMap { LiveActivitySlotOption(rawValue: $0) }
}


// MARK: - Display Name

static func setDisplayName(_ name: String, show: Bool) {
Expand Down
1 change: 0 additions & 1 deletion LoopFollow/Remote/Watch/WatchCommandDispatcher.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// LoopFollow
// WatchCommandDispatcher.swift
// Phone-side handler for Watch remote commands relayed via WCSession.

import Foundation
import HealthKit
Expand Down
18 changes: 10 additions & 8 deletions LoopFollow/WatchComplication/ComplicationEntryBuilder.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// LoopFollow
// ComplicationEntryBuilder.swift
// Philippe Achkar
// 2026-03-25

import ClockKit

Expand All @@ -23,7 +22,6 @@ enum ComplicationID {
// MARK: - Entry builder

enum ComplicationEntryBuilder {

// MARK: - Live template

static func template(
Expand All @@ -36,9 +34,9 @@ enum ComplicationEntryBuilder {
return graphicCircularTemplate(snapshot: snapshot)
case .graphicCorner:
switch identifier {
case ComplicationID.stackCorner: return graphicCornerStackTemplate(snapshot: snapshot)
case ComplicationID.debugCorner: return graphicCornerDebugTemplate(snapshot: snapshot)
default: return graphicCornerGaugeTemplate(snapshot: snapshot)
case ComplicationID.stackCorner: return graphicCornerStackTemplate(snapshot: snapshot)
case ComplicationID.debugCorner: return graphicCornerDebugTemplate(snapshot: snapshot)
default: return graphicCornerGaugeTemplate(snapshot: snapshot)
}
default:
return nil
Expand Down Expand Up @@ -114,6 +112,7 @@ enum ComplicationEntryBuilder {
}

// MARK: - Graphic Circular

// BG (top, colored) + trend arrow (bottom).

private static func graphicCircularTemplate(snapshot: GlucoseSnapshot) -> CLKComplicationTemplate {
Expand All @@ -127,6 +126,7 @@ enum ComplicationEntryBuilder {
}

// MARK: - Graphic Corner — Gauge Text (Complication 1)

// Gauge arc fills from 0 (fresh) to 100% (15 min stale).
// Outer text: BG (colored). Leading text: delta.
// Stale / isNotLooping → "⚠" in yellow, gauge full.
Expand Down Expand Up @@ -167,6 +167,7 @@ enum ComplicationEntryBuilder {
}

// MARK: - Graphic Corner — Stacked Text (Complication 2)

// Outer (top, large): BG value, colored.
// Inner (bottom, small): "→ projected" (falls back to delta if no projection).
// Stale / isNotLooping: outer = "--", inner = "".
Expand Down Expand Up @@ -197,6 +198,7 @@ enum ComplicationEntryBuilder {
}

// MARK: - Graphic Corner — Debug (Complication 3)

// Outer (top): HH:mm of the snapshot's updatedAt — when the CGM reading arrived.
// Inner (bottom): "↺ HH:mm" — when ClockKit last called getCurrentTimelineEntry.
//
Expand All @@ -206,7 +208,7 @@ enum ComplicationEntryBuilder {
// inner stale → reloadTimeline is not being called or ClockKit is ignoring it

private static func graphicCornerDebugTemplate(snapshot: GlucoseSnapshot) -> CLKComplicationTemplate {
let dataTime = WatchFormat.updateTime(snapshot)
let dataTime = WatchFormat.updateTime(snapshot)
let buildTime = WatchFormat.currentTime()

return CLKComplicationTemplateGraphicCornerStackText(
Expand All @@ -220,7 +222,7 @@ enum ComplicationEntryBuilder {
/// snapshot.glucose is always in mg/dL (builder stores canonical mg/dL).
static func thresholdColor(for snapshot: GlucoseSnapshot) -> UIColor {
let t = LAAppGroupSettings.thresholdsMgdl()
if snapshot.glucose < t.low { return .red }
if snapshot.glucose < t.low { return .red }
if snapshot.glucose > t.high { return .orange }
return .green
}
Expand Down
10 changes: 4 additions & 6 deletions LoopFollow/WatchComplication/WatchComplicationProvider.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// LoopFollow
// WatchComplicationProvider.swift
// Philippe Achkar
// 2026-03-10

import ClockKit
import Foundation
Expand All @@ -12,7 +11,6 @@ private let watchLog = OSLog(
)

final class WatchComplicationProvider: NSObject, CLKComplicationDataSource {

// MARK: - Complication Descriptors

func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
Expand All @@ -34,7 +32,7 @@ final class WatchComplicationProvider: NSObject, CLKComplicationDataSource {
identifier: ComplicationID.debugCorner,
displayName: "LoopFollow Debug",
supportedFamilies: [.graphicCorner]
)
),
]
handler(descriptors)
}
Expand All @@ -53,7 +51,7 @@ final class WatchComplicationProvider: NSObject, CLKComplicationDataSource {
// Prefer the file store (persists across launches); fall back to the in-memory
// cache in case the file write hasn't completed or the store is unavailable.
guard let snapshot = GlucoseSnapshotStore.shared.load()
?? WatchSessionReceiver.shared.lastSnapshot
?? WatchSessionReceiver.shared.lastSnapshot
else {
os_log("WatchComplicationProvider: no snapshot available (store and cache both nil)", log: watchLog, type: .error)
handler(nil)
Expand Down Expand Up @@ -94,7 +92,7 @@ final class WatchComplicationProvider: NSObject, CLKComplicationDataSource {
}

func getPrivacyBehavior(
for complication: CLKComplication,
for _: CLKComplication,
withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void
) {
// Glucose is sensitive — hide on locked watch face
Expand Down
58 changes: 28 additions & 30 deletions LoopFollow/WatchComplication/WatchFormat.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
// LoopFollow
// WatchFormat.swift
// Philippe Achkar
// 2026-03-25

import Foundation

/// Formatting helpers for Watch complications and Watch app UI.
/// All glucose values in GlucoseSnapshot are stored in mg/dL; this module
/// converts to mmol/L for display when snapshot.unit == .mmol.
enum WatchFormat {

// MARK: - Glucose

static func glucose(_ s: GlucoseSnapshot) -> String {
Expand Down Expand Up @@ -37,14 +35,14 @@ enum WatchFormat {

static func trendArrow(_ s: GlucoseSnapshot) -> String {
switch s.trend {
case .upFast: return "↑↑"
case .up: return "↑"
case .upSlight: return "↗"
case .flat: return "→"
case .upFast: return "↑↑"
case .up: return "↑"
case .upSlight: return "↗"
case .flat: return "→"
case .downSlight: return "↘"
case .down: return "↓"
case .downFast: return "↓↓"
case .unknown: return "–"
case .down: return "↓"
case .downFast: return "↓↓"
case .unknown: return "–"
}
}

Expand Down Expand Up @@ -159,28 +157,28 @@ enum WatchFormat {

static func slotValue(option: LiveActivitySlotOption, snapshot s: GlucoseSnapshot) -> String {
switch option {
case .none: return ""
case .delta: return delta(s)
case .none: return ""
case .delta: return delta(s)
case .projectedBG: return projected(s)
case .minMax: return minMax(s)
case .iob: return iob(s)
case .cob: return cob(s)
case .recBolus: return recBolus(s)
case .autosens: return autosens(s)
case .tdd: return tdd(s)
case .basal: return basal(s)
case .pump: return pump(s)
case .minMax: return minMax(s)
case .iob: return iob(s)
case .cob: return cob(s)
case .recBolus: return recBolus(s)
case .autosens: return autosens(s)
case .tdd: return tdd(s)
case .basal: return basal(s)
case .pump: return pump(s)
case .pumpBattery: return pumpBattery(s)
case .battery: return battery(s)
case .target: return target(s)
case .isf: return isf(s)
case .carbRatio: return carbRatio(s)
case .sage: return age(insertTime: s.sageInsertTime)
case .cage: return age(insertTime: s.cageInsertTime)
case .iage: return age(insertTime: s.iageInsertTime)
case .carbsToday: return carbsToday(s)
case .override: return override(s)
case .profile: return profileName(s)
case .battery: return battery(s)
case .target: return target(s)
case .isf: return isf(s)
case .carbRatio: return carbRatio(s)
case .sage: return age(insertTime: s.sageInsertTime)
case .cage: return age(insertTime: s.cageInsertTime)
case .iage: return age(insertTime: s.iageInsertTime)
case .carbsToday: return carbsToday(s)
case .override: return override(s)
case .profile: return profileName(s)
}
}

Expand Down
27 changes: 13 additions & 14 deletions LoopFollow/WatchComplication/WatchSessionReceiver.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
// LoopFollow
// WatchSessionReceiver.swift
// Philippe Achkar
// 2026-03-10

import ClockKit
import Foundation
import os.log
import UserNotifications
import WatchConnectivity
import ClockKit
import WatchKit
import os.log

private let watchLog = OSLog(
subsystem: Bundle.main.bundleIdentifier ?? "com.loopfollow.watch",
category: "Watch"
)

final class WatchSessionReceiver: NSObject {

// MARK: - Shared Instance

static let shared = WatchSessionReceiver()
Expand Down Expand Up @@ -47,7 +45,7 @@ final class WatchSessionReceiver: NSObject {

// MARK: - Init

private override init() {
override private init() {
super.init()
}

Expand All @@ -74,7 +72,6 @@ final class WatchSessionReceiver: NSObject {
// MARK: - WCSessionDelegate

extension WatchSessionReceiver: WCSessionDelegate {

func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
Expand Down Expand Up @@ -114,30 +111,30 @@ extension WatchSessionReceiver: WCSessionDelegate {

/// Handles immediate delivery when Watch app is in foreground (sendMessage path).
func session(
_ session: WCSession,
_: WCSession,
didReceiveMessage message: [String: Any]
) {
process(payload: message, source: "sendMessage")
}

/// Handles queued background delivery (transferUserInfo path).
func session(
_ session: WCSession,
_: WCSession,
didReceiveUserInfo userInfo: [String: Any]
) {
process(payload: userInfo, source: "userInfo")
}

func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
func session(_: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
process(payload: applicationContext, source: "applicationContext")
}

// MARK: - Private

private func process(payload: [String: Any], source: String) {
if let _ = payload["watchLoopReturn"] {
let title = payload["title"] as? String ?? "Loop Confirmed ✓"
let body = payload["body"] as? String ?? "Command processed by Loop"
let body = payload["body"] as? String ?? "Command processed by Loop"
let task = pendingConnectivityTask
pendingConnectivityTask = nil
DispatchQueue.main.async {
Expand Down Expand Up @@ -199,7 +196,7 @@ extension WatchSessionReceiver: WCSessionDelegate {
}
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.body = body
content.sound = .default
let request = UNNotificationRequest(
identifier: "loop-return-\(UUID().uuidString)",
Expand Down Expand Up @@ -247,7 +244,9 @@ extension WatchSessionReceiver: WCSessionDelegate {
return
}

for complication in complications { server.reloadTimeline(for: complication) }
for complication in complications {
server.reloadTimeline(for: complication)
}
os_log("WatchSessionReceiver: reloadTimeline called for %d complication(s)", log: watchLog, type: .info, complications.count)
}

Expand Down
Loading
Loading