|
@@ -104,6 +104,14 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
/// Track when watchface was last changed to prevent caching stale format data
|
|
/// Track when watchface was last changed to prevent caching stale format data
|
|
|
private var lastWatchfaceChangeTime: Date?
|
|
private var lastWatchfaceChangeTime: Date?
|
|
|
|
|
|
|
|
|
|
+ /// Cache of app installation status to avoid expensive checks before data preparation
|
|
|
|
|
+ /// Key: app UUID string, Value: (isInstalled, lastChecked)
|
|
|
|
|
+ private var appInstallationCache: [String: (isInstalled: Bool, lastChecked: Date)] = [:]
|
|
|
|
|
+ private let appStatusCacheLock = NSLock()
|
|
|
|
|
+
|
|
|
|
|
+ /// How long to trust cached app status (in seconds)
|
|
|
|
|
+ private let appStatusCacheTimeout: TimeInterval = 60 // 1 minute
|
|
|
|
|
+
|
|
|
/// Array of Garmin `IQDevice` objects currently tracked.
|
|
/// Array of Garmin `IQDevice` objects currently tracked.
|
|
|
/// Changing this property triggers re-registration and updates persisted devices.
|
|
/// Changing this property triggers re-registration and updates persisted devices.
|
|
|
private(set) var devices: [IQDevice] = [] {
|
|
private(set) var devices: [IQDevice] = [] {
|
|
@@ -204,6 +212,11 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
// Only send if loop is stale (> 8 minutes)
|
|
// Only send if loop is stale (> 8 minutes)
|
|
|
// Handle infinity case (no loop data available)
|
|
// Handle infinity case (no loop data available)
|
|
|
if loopAge.isFinite, loopAge > 480 { // 8 minutes in seconds
|
|
if loopAge.isFinite, loopAge > 480 { // 8 minutes in seconds
|
|
|
|
|
+ // Skip expensive data preparation if no apps are installed (based on cache)
|
|
|
|
|
+ guard self.areAppsLikelyInstalled() else {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
let loopAgeMinutes = Int(loopAge / 60)
|
|
let loopAgeMinutes = Int(loopAge / 60)
|
|
|
let watchState = try await self.setupGarminWatchState()
|
|
let watchState = try await self.setupGarminWatchState()
|
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
@@ -310,9 +323,12 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
/// Sets up handlers for OrefDetermination and GlucoseStored entity changes in CoreData.
|
|
/// Sets up handlers for OrefDetermination and GlucoseStored entity changes in CoreData.
|
|
|
/// When these change, we re-compute the Garmin watch state and send updates to the watch.
|
|
/// When these change, we re-compute the Garmin watch state and send updates to the watch.
|
|
|
private func registerHandlers() {
|
|
private func registerHandlers() {
|
|
|
- // OrefDetermination - publish to determinationSubject for Combine throttling
|
|
|
|
|
|
|
+ // OrefDetermination - debounce at CoreData level to avoid redundant data preparation
|
|
|
|
|
+ // Multiple determination saves happen within 1-2 seconds during a loop run
|
|
|
|
|
+ // Debouncing here prevents fetching glucose/basals/IOB multiple times for the same loop
|
|
|
coreDataPublisher?
|
|
coreDataPublisher?
|
|
|
.filteredByEntityName("OrefDetermination")
|
|
.filteredByEntityName("OrefDetermination")
|
|
|
|
|
+ .debounce(for: .seconds(2), scheduler: DispatchQueue.main) // Wait 2s after last save before expensive work
|
|
|
.sink { [weak self] _ in
|
|
.sink { [weak self] _ in
|
|
|
guard let self = self else { return }
|
|
guard let self = self else { return }
|
|
|
|
|
|
|
@@ -323,12 +339,17 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
guard !self.devices.isEmpty else { return }
|
|
guard !self.devices.isEmpty else { return }
|
|
|
#endif
|
|
#endif
|
|
|
|
|
|
|
|
|
|
+ // Skip expensive data preparation if no apps are installed (based on cache)
|
|
|
|
|
+ guard self.areAppsLikelyInstalled() else {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
Task {
|
|
Task {
|
|
|
do {
|
|
do {
|
|
|
let watchState = try await self.setupGarminWatchState()
|
|
let watchState = try await self.setupGarminWatchState()
|
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
|
self.currentSendTrigger = "Determination"
|
|
self.currentSendTrigger = "Determination"
|
|
|
- // Publish to subject for throttling - Combine will dedupe
|
|
|
|
|
|
|
+ // Send to subject for additional 2s debouncing before Bluetooth transmission
|
|
|
self.determinationSubject.send(watchStateData)
|
|
self.determinationSubject.send(watchStateData)
|
|
|
} catch {
|
|
} catch {
|
|
|
debug(
|
|
debug(
|
|
@@ -467,9 +488,9 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // MARK: - Unified Watch State Setup
|
|
|
|
|
|
|
+ // MARK: - Watch State Setup
|
|
|
|
|
|
|
|
- /// Builds a unified GarminWatchState array for both Trio and SwissAlpine watchfaces.
|
|
|
|
|
|
|
+ /// Builds a GarminWatchState array for both Trio and SwissAlpine watchfaces.
|
|
|
/// Uses the SwissAlpine numeric format for all data, sent as an array.
|
|
/// Uses the SwissAlpine numeric format for all data, sent as an array.
|
|
|
/// Both watchfaces receive the same data structure with display configuration fields.
|
|
/// Both watchfaces receive the same data structure with display configuration fields.
|
|
|
func setupGarminWatchState() async throws -> [GarminWatchState] {
|
|
func setupGarminWatchState() async throws -> [GarminWatchState] {
|
|
@@ -542,7 +563,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
} else {
|
|
} else {
|
|
|
cobValue = nil
|
|
cobValue = nil
|
|
|
if self.debugWatchState {
|
|
if self.debugWatchState {
|
|
|
- debug(.watchManager, "⌚️ Unified: COB is NaN or infinite, excluding from data")
|
|
|
|
|
|
|
+ debug(.watchManager, "⌚️ COB is NaN or infinite, excluding from data")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -556,7 +577,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
// Invalid ratio - default to 1.0 (no adjustment)
|
|
// Invalid ratio - default to 1.0 (no adjustment)
|
|
|
sensRatioValue = 1.0
|
|
sensRatioValue = 1.0
|
|
|
if self.debugWatchState {
|
|
if self.debugWatchState {
|
|
|
- debug(.watchManager, "⌚️ Unified: SensRatio is NaN or infinite, using default 1.0")
|
|
|
|
|
|
|
+ debug(.watchManager, "⌚️ SensRatio is NaN or infinite, using default 1.0")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
@@ -575,7 +596,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
if self.debugWatchState {
|
|
if self.debugWatchState {
|
|
|
debug(
|
|
debug(
|
|
|
.watchManager,
|
|
.watchManager,
|
|
|
- "⌚️ Unified: ISF out of range (\(insulinSensitivity)), excluding from data"
|
|
|
|
|
|
|
+ "⌚️ ISF out of range (\(insulinSensitivity)), excluding from data"
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -591,7 +612,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
if self.debugWatchState {
|
|
if self.debugWatchState {
|
|
|
debug(
|
|
debug(
|
|
|
.watchManager,
|
|
.watchManager,
|
|
|
- "⌚️ Unified: EventualBG out of range (\(eventualBG)), excluding from data"
|
|
|
|
|
|
|
+ "⌚️ EventualBG out of range (\(eventualBG)), excluding from data"
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -685,7 +706,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
} else {
|
|
} else {
|
|
|
watchState.sgv = nil
|
|
watchState.sgv = nil
|
|
|
if self.debugWatchState {
|
|
if self.debugWatchState {
|
|
|
- debug(.watchManager, "⌚️ Unified: Invalid glucose value (\(glucoseValue)), excluding from data")
|
|
|
|
|
|
|
+ debug(.watchManager, "⌚️ Invalid glucose value (\(glucoseValue)), excluding from data")
|
|
|
}
|
|
}
|
|
|
continue // Skip this invalid glucose entry
|
|
continue // Skip this invalid glucose entry
|
|
|
}
|
|
}
|
|
@@ -702,7 +723,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
} else {
|
|
} else {
|
|
|
watchState.delta = nil
|
|
watchState.delta = nil
|
|
|
if self.debugWatchState {
|
|
if self.debugWatchState {
|
|
|
- debug(.watchManager, "⌚️ Unified: Delta out of range (\(deltaValue)), excluding from data")
|
|
|
|
|
|
|
+ debug(.watchManager, "⌚️ Delta out of range (\(deltaValue)), excluding from data")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
@@ -710,7 +731,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
// This ensures delta is always present in the JSON output
|
|
// This ensures delta is always present in the JSON output
|
|
|
watchState.delta = 0
|
|
watchState.delta = 0
|
|
|
if self.debugWatchState {
|
|
if self.debugWatchState {
|
|
|
- debug(.watchManager, "⌚️ Unified: Only 1 glucose reading available, setting delta to 0")
|
|
|
|
|
|
|
+ debug(.watchManager, "⌚️ Only 1 glucose reading available, setting delta to 0")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -747,7 +768,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // MARK: - Debug Logging Method for Unified Watch State
|
|
|
|
|
|
|
+ // MARK: - Debug Logging Method for Watch State
|
|
|
|
|
|
|
|
private func logWatchState(_ watchState: [GarminWatchState]) {
|
|
private func logWatchState(_ watchState: [GarminWatchState]) {
|
|
|
guard debugWatchState else { return }
|
|
guard debugWatchState else { return }
|
|
@@ -772,11 +793,11 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
|
|
|
|
|
debug(
|
|
debug(
|
|
|
.watchManager,
|
|
.watchManager,
|
|
|
- "📱 Unified (\(watchface.displayName)): Sending \(watchState.count) entries to \(destinations): \(compactJson)"
|
|
|
|
|
|
|
+ "📱 (\(watchface.displayName)): Prepared \(watchState.count) entries for \(destinations): \(compactJson)"
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
} catch {
|
|
} catch {
|
|
|
- debug(.watchManager, "📱 Unified: Sending \(watchState.count) entries (failed to encode for logging)")
|
|
|
|
|
|
|
+ debug(.watchManager, "📱 Prepared \(watchState.count) entries (failed to encode for logging)")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -843,6 +864,12 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
private func registerDevices(_ devices: [IQDevice]) {
|
|
private func registerDevices(_ devices: [IQDevice]) {
|
|
|
// Clear out old references
|
|
// Clear out old references
|
|
|
watchApps.removeAll()
|
|
watchApps.removeAll()
|
|
|
|
|
+
|
|
|
|
|
+ // Clear app installation cache since we're re-registering
|
|
|
|
|
+ appStatusCacheLock.lock()
|
|
|
|
|
+ appInstallationCache.removeAll()
|
|
|
|
|
+ appStatusCacheLock.unlock()
|
|
|
|
|
+ debugGarmin("Garmin: Cleared app installation cache on device registration")
|
|
|
|
|
|
|
|
for device in devices {
|
|
for device in devices {
|
|
|
// Listen for device-level status changes
|
|
// Listen for device-level status changes
|
|
@@ -920,20 +947,22 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
.store(in: &cancellables)
|
|
.store(in: &cancellables)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /// Subscribes to determination updates with 20s throttle (first goes through, rest discarded)
|
|
|
|
|
|
|
+ /// Subscribes to determination updates with 2s debounce (waits for quiet period, then sends latest)
|
|
|
/// Also handles IOB updates since they fire simultaneously with determinations
|
|
/// Also handles IOB updates since they fire simultaneously with determinations
|
|
|
|
|
+ /// Two-stage debouncing: 2s at CoreData level (skip redundant prep) + 2s here (skip redundant sends)
|
|
|
|
|
+ /// Total delay: ~4s from first CoreData save to Bluetooth transmission (faster than old 5s throttle)
|
|
|
private func subscribeToDeterminationThrottle() {
|
|
private func subscribeToDeterminationThrottle() {
|
|
|
determinationSubject
|
|
determinationSubject
|
|
|
- .throttle(for: .seconds(20), scheduler: DispatchQueue.main, latest: false)
|
|
|
|
|
|
|
+ .debounce(for: .seconds(2), scheduler: DispatchQueue.main)
|
|
|
.sink { [weak self] data in
|
|
.sink { [weak self] data in
|
|
|
guard let self = self else { return }
|
|
guard let self = self else { return }
|
|
|
|
|
|
|
|
- // Only cache if no recent watchface change (within last 25 seconds)
|
|
|
|
|
- // This prevents caching stale format data that was in the throttle pipeline
|
|
|
|
|
|
|
+ // Only cache if no recent watchface change (within last 6 seconds)
|
|
|
|
|
+ // This prevents caching stale format data that was in the debounce pipeline
|
|
|
let shouldCache: Bool
|
|
let shouldCache: Bool
|
|
|
if let lastChange = self.lastWatchfaceChangeTime {
|
|
if let lastChange = self.lastWatchfaceChangeTime {
|
|
|
let timeSinceChange = Date().timeIntervalSince(lastChange)
|
|
let timeSinceChange = Date().timeIntervalSince(lastChange)
|
|
|
- shouldCache = timeSinceChange > 25 // Throttle is 20s, add 5s buffer
|
|
|
|
|
|
|
+ shouldCache = timeSinceChange > 6 // 2s CoreData + 2s send debounce + 2s buffer
|
|
|
if !shouldCache {
|
|
if !shouldCache {
|
|
|
debugGarmin(
|
|
debugGarmin(
|
|
|
"[\(self.formatTimeForLog())] Garmin: Not caching - data may be from before watchface change (\(Int(timeSinceChange))s ago)"
|
|
"[\(self.formatTimeForLog())] Garmin: Not caching - data may be from before watchface change (\(Int(timeSinceChange))s ago)"
|
|
@@ -955,7 +984,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- debugGarmin("[\(self.formatTimeForLog())] Garmin: Sending determination/IOB (20s throttle passed)")
|
|
|
|
|
|
|
+ debugGarmin("[\(self.formatTimeForLog())] Garmin: Sending determination/IOB (2s debounce passed)")
|
|
|
self.broadcastStateToWatchApps(jsonObject as Any)
|
|
self.broadcastStateToWatchApps(jsonObject as Any)
|
|
|
}
|
|
}
|
|
|
.store(in: &cancellables)
|
|
.store(in: &cancellables)
|
|
@@ -1001,15 +1030,66 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
connectIQ?.getAppStatus(app) { [weak self] status in
|
|
connectIQ?.getAppStatus(app) { [weak self] status in
|
|
|
- guard status?.isInstalled == true else {
|
|
|
|
|
- self?.debugGarmin("[\(self?.formatTimeForLog() ?? "")] Garmin: App not installed on device: \(app.uuid!)")
|
|
|
|
|
|
|
+ guard let self = self else { return }
|
|
|
|
|
+ let isInstalled = status?.isInstalled == true
|
|
|
|
|
+
|
|
|
|
|
+ // Update cache with current status
|
|
|
|
|
+ if let uuid = app.uuid {
|
|
|
|
|
+ self.updateAppStatusCache(uuid: uuid, isInstalled: isInstalled)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ guard isInstalled else {
|
|
|
|
|
+ self.debugGarmin("[\(self.formatTimeForLog())] Garmin: App not installed on device: \(app.uuid!)")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- debug(.watchManager, "[\(self?.formatTimeForLog() ?? "")] Garmin: Sending watch-state to app \(app.uuid!)")
|
|
|
|
|
- self?.sendMessage(state, to: app)
|
|
|
|
|
|
|
+ debug(.watchManager, "[\(self.formatTimeForLog())] Garmin: Sending watch-state to app \(app.uuid!)")
|
|
|
|
|
+ self.sendMessage(state, to: app)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - App Status Cache Management
|
|
|
|
|
+
|
|
|
|
|
+ /// Updates the installation status cache for a given app UUID
|
|
|
|
|
+ private func updateAppStatusCache(uuid: UUID, isInstalled: Bool) {
|
|
|
|
|
+ appStatusCacheLock.lock()
|
|
|
|
|
+ defer { appStatusCacheLock.unlock() }
|
|
|
|
|
+ appInstallationCache[uuid.uuidString] = (isInstalled, Date())
|
|
|
|
|
+ debugGarmin("[\(formatTimeForLog())] Garmin: Updated app cache - \(uuid) is \(isInstalled ? "installed" : "NOT installed")")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Checks if any Garmin apps are likely installed based on cached status
|
|
|
|
|
+ /// Returns true if cache suggests apps are installed, or if cache is empty/stale (optimistic)
|
|
|
|
|
+ private func areAppsLikelyInstalled() -> Bool {
|
|
|
|
|
+ appStatusCacheLock.lock()
|
|
|
|
|
+ defer { appStatusCacheLock.unlock() }
|
|
|
|
|
+
|
|
|
|
|
+ // If cache is empty, be optimistic and allow data preparation
|
|
|
|
|
+ guard !appInstallationCache.isEmpty else {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let now = Date()
|
|
|
|
|
+
|
|
|
|
|
+ // Check if ANY app is installed (and cache is fresh)
|
|
|
|
|
+ for (_, status) in appInstallationCache {
|
|
|
|
|
+ let cacheAge = now.timeIntervalSince(status.lastChecked)
|
|
|
|
|
+
|
|
|
|
|
+ // If cache is stale, be optimistic
|
|
|
|
|
+ if cacheAge > appStatusCacheTimeout {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // If any app is installed, proceed
|
|
|
|
|
+ if status.isInstalled {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // All apps are confirmed not installed (with fresh cache)
|
|
|
|
|
+ debugGarmin("[\(formatTimeForLog())] Garmin: ⏩ Skipping data preparation - no apps installed (cached)")
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// MARK: - GarminManager Conformance
|
|
// MARK: - GarminManager Conformance
|
|
|
|
|
|
|
@@ -1236,6 +1316,12 @@ extension BaseGarminManager: IQUIOverrideDelegate, IQDeviceEventDelegate, IQAppM
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Use throttled send for status requests to avoid spam
|
|
// Use throttled send for status requests to avoid spam
|
|
|
|
|
+ // Skip if no apps are installed (based on cache)
|
|
|
|
|
+ guard self.areAppsLikelyInstalled() else {
|
|
|
|
|
+ debugGarmin("[\(self.formatTimeForLog())] ⏩ Skipping status request - no apps installed (cached)")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
do {
|
|
do {
|
|
|
let watchState = try await self.setupGarminWatchState()
|
|
let watchState = try await self.setupGarminWatchState()
|
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
@@ -1337,6 +1423,12 @@ extension BaseGarminManager: SettingsObserver {
|
|
|
// Send immediate update for critical changes
|
|
// Send immediate update for critical changes
|
|
|
if needsImmediateUpdate {
|
|
if needsImmediateUpdate {
|
|
|
Task {
|
|
Task {
|
|
|
|
|
+ // Skip if no apps are installed (based on cache)
|
|
|
|
|
+ guard self.areAppsLikelyInstalled() else {
|
|
|
|
|
+ debugGarmin("⏩ Skipping immediate settings update - no apps installed (cached)")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
do {
|
|
do {
|
|
|
// Try to use cached determination data first to avoid CoreData staleness
|
|
// Try to use cached determination data first to avoid CoreData staleness
|
|
|
if let cachedData = self.cachedDeterminationData {
|
|
if let cachedData = self.cachedDeterminationData {
|
|
@@ -1365,6 +1457,12 @@ extension BaseGarminManager: SettingsObserver {
|
|
|
// Send throttled update for data type changes
|
|
// Send throttled update for data type changes
|
|
|
else if needsThrottledUpdate {
|
|
else if needsThrottledUpdate {
|
|
|
Task {
|
|
Task {
|
|
|
|
|
+ // Skip if no apps are installed (based on cache)
|
|
|
|
|
+ guard self.areAppsLikelyInstalled() else {
|
|
|
|
|
+ debugGarmin("⏩ Skipping throttled settings update - no apps installed (cached)")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
do {
|
|
do {
|
|
|
let watchState = try await self.setupGarminWatchState()
|
|
let watchState = try await self.setupGarminWatchState()
|
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|