Просмотр исходного кода

Merge pull request #944 from mountrcg/garminRebuild

Refactor Garmin: simplify manager, add documentation, and improve simulator testing
Deniz Cengiz 3 месяцев назад
Родитель
Сommit
97c76f7339

+ 2 - 2
Trio.xcodeproj/project.pbxproj

@@ -267,8 +267,8 @@
 		491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */; };
 		491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */; };
 		491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */; };
-		4984D1D42EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4984D1D32EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift */; };
 		49239B432EEA27AD00469145 /* TempTargetCalculations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49239B422EEA27AD00469145 /* TempTargetCalculations.swift */; };
+		4984D1D42EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4984D1D32EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift */; };
 		49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
@@ -1096,8 +1096,8 @@
 		491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
-		4984D1D32EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigGarminAppConfigView.swift; sourceTree = "<group>"; };
 		49239B422EEA27AD00469145 /* TempTargetCalculations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetCalculations.swift; sourceTree = "<group>"; };
+		4984D1D32EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigGarminAppConfigView.swift; sourceTree = "<group>"; };
 		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.swift"; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };

+ 2 - 2
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -51,8 +51,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/garmin/connectiq-companion-app-sdk-ios",
       "state" : {
-        "revision" : "00594907c84884a9430c6a33825940c2769f261a",
-        "version" : "1.7.0"
+        "revision" : "f0d29ff691d700a132d86205ed9bb091e336c2f7",
+        "version" : "1.8.0"
       }
     },
     {

+ 6 - 1
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -10037,7 +10037,6 @@
       }
     },
     "%lld h" : {
-      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -261724,6 +261723,12 @@
     "Watch/Carplay Widget Style" : {
 
     },
+    "Watchface Selection" : {
+
+    },
+    "Watchface Settings" : {
+
+    },
     "We recommend reviewing them carefully — Trio will guide you step-by-step." : {
       "localizations" : {
         "bg" : {

+ 3 - 1
Trio/Sources/Models/GarminWatchSettings.swift

@@ -99,7 +99,9 @@ enum GarminDatafield: String, JSON, CaseIterable, Identifiable, Codable, Hashabl
     var datafieldUUID: UUID? {
         switch self {
         case .trio:
-            return UUID(uuidString: "71cf0982-ca41-42a5-8441-ea81d36056c3")
+            // return UUID(uuidString: "71cf0982-ca41-42a5-8441-ea81d36056c3")  // local build
+            // return UUID(uuidString: "f07f4ef9-108b-4397-95c9-217b5173412e")  // ConnectIQ test build
+            return UUID(uuidString: "3d9b6528-8c84-459a-bbab-989b5f001ebd") // ConnectIQ live build
         case .swissalpine:
             // return UUID(uuidString: "7A2268F6-3381-4474-81BD-0A3E7F458CB7") // ConnectIQ test build
             return UUID(uuidString: "dec5292a-74b0-41bc-8e45-cd93f1d5e137") // ConnectIQ live build

+ 21 - 4
Trio/Sources/Models/GarminWatchState.swift

@@ -1,3 +1,9 @@
+//
+//  GarminWatchState.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 25.01.25.
+//
 import Foundation
 import SwiftUI
 
@@ -7,9 +13,14 @@ import SwiftUI
 /// Uses the SwissAlpine xDrip+ compatible data format.
 /// Sent as an array where the first entry contains all extended data fields.
 struct GarminWatchState: Hashable, Equatable, Sendable, Encodable {
-    /// Timestamp of the glucose reading in milliseconds since Unix epoch
+    /// Timestamp of the enacted loop determination in milliseconds since Unix epoch
+    /// Shows when the loop actually executed, used to indicate loop staleness
     var date: UInt64?
 
+    /// Timestamp of the glucose reading in milliseconds since Unix epoch
+    /// Used by watchface to determine glucose freshness for coloring logic
+    var glucoseDate: UInt64?
+
     /// Sensor glucose value in raw mg/dL (no unit conversion applied)
     var sgv: Int16?
 
@@ -55,6 +66,7 @@ struct GarminWatchState: Hashable, Equatable, Sendable, Encodable {
 
     static func == (lhs: GarminWatchState, rhs: GarminWatchState) -> Bool {
         lhs.date == rhs.date &&
+            lhs.glucoseDate == rhs.glucoseDate &&
             lhs.sgv == rhs.sgv &&
             lhs.delta == rhs.delta &&
             lhs.direction == rhs.direction &&
@@ -72,6 +84,7 @@ struct GarminWatchState: Hashable, Equatable, Sendable, Encodable {
 
     func hash(into hasher: inout Hasher) {
         hasher.combine(date)
+        hasher.combine(glucoseDate)
         hasher.combine(sgv)
         hasher.combine(delta)
         hasher.combine(direction)
@@ -89,6 +102,7 @@ struct GarminWatchState: Hashable, Equatable, Sendable, Encodable {
 
     enum CodingKeys: String, CodingKey {
         case date
+        case glucoseDate
         case sgv
         case delta
         case direction
@@ -105,20 +119,23 @@ struct GarminWatchState: Hashable, Equatable, Sendable, Encodable {
     }
 
     /// Custom encoding that excludes nil values from the JSON output
+    /// Double values are rounded to 2 decimal places to prevent floating point artifacts
     func encode(to encoder: Encoder) throws {
         var container = encoder.container(keyedBy: CodingKeys.self)
         try container.encodeIfPresent(date, forKey: .date)
+        try container.encodeIfPresent(glucoseDate, forKey: .glucoseDate)
         try container.encodeIfPresent(sgv, forKey: .sgv)
         try container.encodeIfPresent(delta, forKey: .delta)
         try container.encodeIfPresent(direction, forKey: .direction)
         try container.encodeIfPresent(noise, forKey: .noise)
         try container.encodeIfPresent(units_hint, forKey: .units_hint)
-        try container.encodeIfPresent(iob, forKey: .iob)
-        try container.encodeIfPresent(tbr, forKey: .tbr)
+        // Round Double values to 2 decimal places to prevent floating point artifacts like "0.5600000000000001"
+        try container.encodeIfPresent(iob?.roundedDouble(toPlaces: 2), forKey: .iob)
+        try container.encodeIfPresent(tbr?.roundedDouble(toPlaces: 2), forKey: .tbr)
         try container.encodeIfPresent(cob, forKey: .cob)
         try container.encodeIfPresent(eventualBG, forKey: .eventualBG)
         try container.encodeIfPresent(isf, forKey: .isf)
-        try container.encodeIfPresent(sensRatio, forKey: .sensRatio)
+        try container.encodeIfPresent(sensRatio?.roundedDouble(toPlaces: 2), forKey: .sensRatio)
         try container.encodeIfPresent(displayPrimaryAttributeChoice, forKey: .displayPrimaryAttributeChoice)
         try container.encodeIfPresent(displaySecondaryAttributeChoice, forKey: .displaySecondaryAttributeChoice)
     }

+ 3 - 0
Trio/Sources/Modules/WatchConfig/View/WatchConfigGarminAppConfigView.swift

@@ -53,6 +53,9 @@ struct WatchConfigGarminAppConfigView: View {
                             ).buttonStyle(BorderlessButtonStyle())
                         }.padding(.top)
                         Spacer()
+                        // Inverted binding: "Disable" toggle controls "isEnabled" boolean
+                        // When toggle is ON → data transmission is DISABLED (isEnabled = false)
+                        // When toggle is OFF → data transmission is ENABLED (isEnabled = true)
                         Toggle("Disable Watchface Data", isOn: Binding(
                             get: { !state.garminSettings.isWatchfaceDataEnabled },
                             set: { state.garminSettings.isWatchfaceDataEnabled = !$0 }

+ 1 - 34
Trio/Sources/Modules/WatchConfig/View/WatchConfigGarminView.swift

@@ -19,14 +19,8 @@ struct WatchConfigGarminView: View {
     #if targetEnvironment(simulator)
         /// Adds a mock Garmin device for simulator UI testing
         private func addMockDevice() {
-            // Create a mock IQDevice using a simple class that conforms to the protocol
-            let mockDevice = MockIQDevice(
-                uuid: UUID(),
-                friendlyName: "Mock Garmin Fenix 7",
-                modelName: "fenix7"
-            )
+            let mockDevice = BaseGarminManager.MockIQDevice.createSimulated()
             state.devices.append(mockDevice)
-            // Persist to garmin manager so it survives view reloads
             state.deleteGarminDevice()
         }
     #endif
@@ -87,7 +81,6 @@ struct WatchConfigGarminView: View {
                                 .buttonStyle(.bordered)
                             } else {
                                 Button {
-                                    // Remove all devices
                                     state.devices.removeAll()
                                     state.deleteGarminDevice()
                                 } label: {
@@ -200,29 +193,3 @@ struct WatchConfigGarminView: View {
         }
     }
 }
-
-#if targetEnvironment(simulator)
-    // Mock IQDevice class for simulator testing
-    // Minimal implementation just for UI testing - no actual Garmin functionality
-    class MockIQDevice: IQDevice {
-        private let _uuid: UUID
-        private let _friendlyName: String
-        private let _modelName: String
-
-        override var uuid: UUID { _uuid }
-        override var friendlyName: String { _friendlyName }
-        override var modelName: String { _modelName }
-        var status: IQDeviceStatus { .connected }
-
-        init(uuid: UUID, friendlyName: String, modelName: String) {
-            _uuid = uuid
-            _friendlyName = friendlyName
-            _modelName = modelName
-            super.init()
-        }
-
-        @available(*, unavailable) required init?(coder _: NSCoder) {
-            fatalError("init(coder:) not implemented for mock device")
-        }
-    }
-#endif

Разница между файлами не показана из-за своего большого размера
+ 609 - 1536
Trio/Sources/Services/WatchManager/GarminManager.swift