From bcf64477fb713e4eac032d67b402515535cce41d Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 20 Sep 2024 06:48:42 -0700 Subject: [PATCH 1/5] Add Stelo support --- G7SensorKit/G7CGMManager/G7Sensor.swift | 5 +++-- G7SensorKitUI/Views/G7StartupView.swift | 2 +- G7SensorPlugin/Info.plist | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/G7SensorKit/G7CGMManager/G7Sensor.swift b/G7SensorKit/G7CGMManager/G7Sensor.swift index b1745a1..cea7644 100644 --- a/G7SensorKit/G7CGMManager/G7Sensor.swift +++ b/G7SensorKit/G7CGMManager/G7Sensor.swift @@ -215,8 +215,9 @@ public final class G7Sensor: G7BluetoothManagerDelegate { } /// The Dexcom G7 advertises a peripheral name of "DXCMxx", and later reports a full name of "Dexcomxx" - /// Dexcom One+ peripheral name start with "DX02" - if name.hasPrefix("DXCM") || name.hasPrefix("DX02"){ + /// The Dexcom Stelo prelix is "DX01" + /// The Dexcom One+ prefix is "DX02" + if name.hasPrefix("DXCM") || name.hasPrefix("DX01") || name.hasPrefix("DX02"){ // If we're following this name or if we're scanning, connect if let sensorName = sensorID, name.suffix(2) == sensorName.suffix(2) { return .makeActive diff --git a/G7SensorKitUI/Views/G7StartupView.swift b/G7SensorKitUI/Views/G7StartupView.swift index ccd84a3..0418acd 100644 --- a/G7SensorKitUI/Views/G7StartupView.swift +++ b/G7SensorKitUI/Views/G7StartupView.swift @@ -28,7 +28,7 @@ struct G7StartupView: View { .frame(height: 120) .padding(.horizontal) }.frame(maxWidth: .infinity) - Text(String(format: LocalizedString("%1$@ can read G7 CGM data, but you must still use the Dexcom G7 App for pairing, calibration, and other sensor management.", comment: "Descriptive text on G7StartupView (1: appName)"), self.appName)) + Text(String(format: LocalizedString("%1$@ can read CGM data from the G7 platform, but you must still use the Dexcom App for pairing, calibration, and other sensor management available to the sensor series (G7, ONE+, Stelo).", comment: "Descriptive text on G7StartupView (1: appName)"), self.appName)) .fixedSize(horizontal: false, vertical: true) .foregroundColor(.secondary) Spacer() diff --git a/G7SensorPlugin/Info.plist b/G7SensorPlugin/Info.plist index 996e0e1..b97e45d 100644 --- a/G7SensorPlugin/Info.plist +++ b/G7SensorPlugin/Info.plist @@ -23,7 +23,7 @@ NSPrincipalClass G7SensorPlugin com.loopkit.Loop.CGMManagerDisplayName - Dexcom G7 / ONE+ + Dexcom G7 / ONE+ / Stelo com.loopkit.Loop.CGMManagerIdentifier G7CGMManager From 8b18b0f7d9142a874748fc87331307cd33055422 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Sun, 22 Sep 2024 20:49:24 -0700 Subject: [PATCH 2/5] add warning about no glucose alerts for Stelo --- G7SensorKitUI/Views/G7StartupView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/G7SensorKitUI/Views/G7StartupView.swift b/G7SensorKitUI/Views/G7StartupView.swift index 0418acd..58ab48b 100644 --- a/G7SensorKitUI/Views/G7StartupView.swift +++ b/G7SensorKitUI/Views/G7StartupView.swift @@ -28,7 +28,7 @@ struct G7StartupView: View { .frame(height: 120) .padding(.horizontal) }.frame(maxWidth: .infinity) - Text(String(format: LocalizedString("%1$@ can read CGM data from the G7 platform, but you must still use the Dexcom App for pairing, calibration, and other sensor management available to the sensor series (G7, ONE+, Stelo).", comment: "Descriptive text on G7StartupView (1: appName)"), self.appName)) + Text(String(format: LocalizedString("%1$@ can read CGM data from the G7 platform, but you must still use the Dexcom App for pairing, calibration, alarms and other sensor management available to the sensor series (G7, ONE+, Stelo).\n\nWARNING: Dexcom Stelo app provides no alerts and alarms. Glucose alerts and alarms are not provided by %2$@.", comment: "Descriptive text on G7StartupView (1: appName, 2: appName)"), self.appName, self.appName)) .fixedSize(horizontal: false, vertical: true) .foregroundColor(.secondary) Spacer() From 6ee0cb4cb4e1624dc098a32761fb17f4fff96024 Mon Sep 17 00:00:00 2001 From: codebymini Date: Thu, 4 Sep 2025 08:24:20 +0200 Subject: [PATCH 3/5] Add manager for G7 platform sensors (G7, ONE+, Stelo) --- G7SensorKit.xcodeproj/project.pbxproj | 4 + G7SensorKit/G7CGMManager/G7CGMManager.swift | 18 ++-- .../G7CGMManager/G7CGMManagerState.swift | 10 ++ G7SensorKit/G7CGMManager/G7Sensor.swift | 9 +- G7SensorKit/G7CGMManager/G7SensorType.swift | 98 +++++++++++++++++++ .../G7CGMManager/G7CGMManager+UI.swift | 4 +- G7SensorKitUI/Views/G7SettingsView.swift | 22 ++--- G7SensorKitUI/Views/G7SettingsViewModel.swift | 12 ++- 8 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 G7SensorKit/G7CGMManager/G7SensorType.swift diff --git a/G7SensorKit.xcodeproj/project.pbxproj b/G7SensorKit.xcodeproj/project.pbxproj index 238d8fc..d852848 100644 --- a/G7SensorKit.xcodeproj/project.pbxproj +++ b/G7SensorKit.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 6515DB522E695F77005C42DC /* G7SensorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6515DB512E695F77005C42DC /* G7SensorType.swift */; }; B60BB2E42BC649DA00D2BB39 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60BB2E32BC649DA00D2BB39 /* Bundle.swift */; }; C109F14A291ECCE2008EA5B6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C109F149291ECCE2008EA5B6 /* Assets.xcassets */; }; C109F14C291ED66F008EA5B6 /* G7GlucoseMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C109F14B291ED66F008EA5B6 /* G7GlucoseMessageTests.swift */; }; @@ -107,6 +108,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 6515DB512E695F77005C42DC /* G7SensorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = G7SensorType.swift; sourceTree = ""; }; B60BB2E32BC649DA00D2BB39 /* Bundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Bundle.swift; path = G7SensorKitUI/Extensions/Bundle.swift; sourceTree = SOURCE_ROOT; }; C1086B0E29C9169100D46E65 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; C1086B0F29C9169100D46E65 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -288,6 +290,7 @@ C17F50DE291EAC6500555EB5 /* G7CGMManager */ = { isa = PBXGroup; children = ( + 6515DB512E695F77005C42DC /* G7SensorType.swift */, C17F50DF291EAC6500555EB5 /* G7BackfillMessage.swift */, C17F50E7291EAC6500555EB5 /* G7BluetoothManager.swift */, C17F50E5291EAC6500555EB5 /* G7CGMManager.swift */, @@ -606,6 +609,7 @@ C17F5140291EB27D00555EB5 /* TimeInterval.swift in Sources */, C17F50F0291EAC6500555EB5 /* G7CGMManagerState.swift in Sources */, C17F5145291EB45900555EB5 /* CBPeripheral.swift in Sources */, + 6515DB522E695F77005C42DC /* G7SensorType.swift in Sources */, C17F513A291EB0D900555EB5 /* GlucoseLimits.swift in Sources */, C17F5143291EB36700555EB5 /* AuthChallengeRxMessage.swift in Sources */, C17F50EA291EAC6500555EB5 /* G7DeviceStatus.swift in Sources */, diff --git a/G7SensorKit/G7CGMManager/G7CGMManager.swift b/G7SensorKit/G7CGMManager/G7CGMManager.swift index 198d5b3..1c4fc6e 100644 --- a/G7SensorKit/G7CGMManager/G7CGMManager.swift +++ b/G7SensorKit/G7CGMManager/G7CGMManager.swift @@ -116,14 +116,14 @@ public class G7CGMManager: CGMManager { guard let activatedAt = sensorActivatedAt else { return nil } - return activatedAt.addingTimeInterval(G7Sensor.lifetime) + return activatedAt.addingTimeInterval(state.sensorType.lifetime) } public var sensorEndsAt: Date? { guard let activatedAt = sensorActivatedAt else { return nil } - return activatedAt.addingTimeInterval(G7Sensor.lifetime + G7Sensor.gracePeriod) + return activatedAt.addingTimeInterval(state.sensorType.lifetime + state.sensorType.gracePeriod) } @@ -131,7 +131,7 @@ public class G7CGMManager: CGMManager { guard let activatedAt = sensorActivatedAt else { return nil } - return activatedAt.addingTimeInterval(G7Sensor.warmupDuration) + return activatedAt.addingTimeInterval(state.sensorType.warmupDuration) } public var latestReading: G7GlucoseMessage? { @@ -229,7 +229,9 @@ public class G7CGMManager: CGMManager { public static let pluginIdentifier: String = "G7CGMManager" - public let localizedTitle = LocalizedString("Dexcom G7", comment: "CGM display title") + public var localizedTitle: String { + return state.sensorType.displayName + } public let isOnboarded = true // No distinction between created and onboarded @@ -242,6 +244,7 @@ public class G7CGMManager: CGMManager { mutateState { state in state.sensorID = nil + state.sensorType = .unknown state.activatedAt = nil } sensor.scanForNewSensor() @@ -251,7 +254,7 @@ public class G7CGMManager: CGMManager { return HKDevice( name: state.sensorID ?? "Unknown", manufacturer: "Dexcom", - model: "G7", + model: state.sensorType.rawValue, hardwareVersion: nil, firmwareVersion: nil, softwareVersion: "CGMBLEKit" + String(G7SensorKitVersionNumber), @@ -292,14 +295,15 @@ extension G7CGMManager: G7SensorDelegate { if shouldSwitchToNewSensor { mutateState { state in state.sensorID = name + state.sensorType = sensor.sensorType state.activatedAt = activatedAt } let event = PersistedCgmEvent( date: activatedAt, type: .sensorStart, deviceIdentifier: name, - expectedLifetime: .hours(24 * 10 + 12), - warmupPeriod: .hours(2) + expectedLifetime: .hours(sensor.sensorType.lifetime.hours + sensor.sensorType.gracePeriod.hours), + warmupPeriod: .hours(sensor.sensorType.warmupDuration.hours) ) delegate.notify { delegate in delegate?.cgmManager(self, hasNew: [event]) diff --git a/G7SensorKit/G7CGMManager/G7CGMManagerState.swift b/G7SensorKit/G7CGMManager/G7CGMManagerState.swift index 948b02f..cae351e 100644 --- a/G7SensorKit/G7CGMManager/G7CGMManagerState.swift +++ b/G7SensorKit/G7CGMManager/G7CGMManagerState.swift @@ -14,6 +14,7 @@ public struct G7CGMManagerState: RawRepresentable, Equatable { public typealias RawValue = CGMManager.RawStateValue public var sensorID: String? + public var sensorType: G7SensorType = .unknown public var activatedAt: Date? public var latestReading: G7GlucoseMessage? public var latestReadingTimestamp: Date? @@ -25,6 +26,14 @@ public struct G7CGMManagerState: RawRepresentable, Equatable { public init(rawValue: RawValue) { self.sensorID = rawValue["sensorID"] as? String + if let sensorTypeString = rawValue["sensorType"] as? String, + let sensorType = G7SensorType(rawValue: sensorTypeString) { + self.sensorType = sensorType + } else { + if let sensorID = rawValue["sensorID"] as? String { + self.sensorType = G7SensorType.detect(from: sensorID) + } + } self.activatedAt = rawValue["activatedAt"] as? Date if let readingData = rawValue["latestReading"] as? Data { latestReading = G7GlucoseMessage(data: readingData) @@ -37,6 +46,7 @@ public struct G7CGMManagerState: RawRepresentable, Equatable { public var rawValue: RawValue { var rawValue: RawValue = [:] rawValue["sensorID"] = sensorID + rawValue["sensorType"] = sensorType.rawValue rawValue["activatedAt"] = activatedAt rawValue["latestReading"] = latestReading?.data rawValue["latestReadingTimestamp"] = latestReadingTimestamp diff --git a/G7SensorKit/G7CGMManager/G7Sensor.swift b/G7SensorKit/G7CGMManager/G7Sensor.swift index cea7644..aadb01e 100644 --- a/G7SensorKit/G7CGMManager/G7Sensor.swift +++ b/G7SensorKit/G7CGMManager/G7Sensor.swift @@ -60,9 +60,13 @@ public enum G7SensorLifecycleState { public final class G7Sensor: G7BluetoothManagerDelegate { + // Legacy static properties for backward compatibility public static let lifetime = TimeInterval(hours: 10 * 24) public static let warmupDuration = TimeInterval(minutes: 25) public static let gracePeriod = TimeInterval(hours: 12) + + // Current sensor type for dynamic timing + public var sensorType: G7SensorType = .unknown public weak var delegate: G7SensorDelegate? @@ -215,9 +219,12 @@ public final class G7Sensor: G7BluetoothManagerDelegate { } /// The Dexcom G7 advertises a peripheral name of "DXCMxx", and later reports a full name of "Dexcomxx" - /// The Dexcom Stelo prelix is "DX01" + /// The Dexcom Stelo prefix is "DX01" /// The Dexcom One+ prefix is "DX02" if name.hasPrefix("DXCM") || name.hasPrefix("DX01") || name.hasPrefix("DX02"){ + // Auto-detect sensor type when connecting + sensorType = G7SensorType.detect(from: name) + // If we're following this name or if we're scanning, connect if let sensorName = sensorID, name.suffix(2) == sensorName.suffix(2) { return .makeActive diff --git a/G7SensorKit/G7CGMManager/G7SensorType.swift b/G7SensorKit/G7CGMManager/G7SensorType.swift new file mode 100644 index 0000000..f2d1a0d --- /dev/null +++ b/G7SensorKit/G7CGMManager/G7SensorType.swift @@ -0,0 +1,98 @@ +// +// G7SensorType.swift +// G7SensorKit +// +// Created by Daniel Johansson on 12/19/24. +// Copyright © 2024 LoopKit Authors. All rights reserved. +// + +import Foundation + +public enum G7SensorType: String, CaseIterable, CustomStringConvertible { + case g7 = "G7" + case onePlus = "ONE+" + case stelo = "Stelo" + case unknown = "Unknown" + + public var description: String { + switch self { + case .g7: + return "Dexcom G7" + case .onePlus: + return "Dexcom ONE+" + case .stelo: + return "Dexcom Stelo" + case .unknown: + return "Unknown Sensor" + } + } + + public var displayName: String { + return description + } + + public var lifetime: TimeInterval { + switch self { + case .g7: + return TimeInterval(hours: 10 * 24) // 10 days + case .onePlus: + return TimeInterval(hours: 10 * 24) // 10 days + case .stelo: + return TimeInterval(hours: 15 * 24) // 15 days + case .unknown: + return TimeInterval(hours: 10 * 24) // Default to 10 days + } + } + + public var gracePeriod: TimeInterval { + switch self { + case .g7, .onePlus, .stelo, .unknown: + return TimeInterval(hours: 12) // 12 hours for all + } + } + + public var warmupDuration: TimeInterval { + switch self { + case .g7, .onePlus, .stelo, .unknown: + return TimeInterval(minutes: 25) // 25 minutes for all + } + } + public var totalLifetimeHours: Double { + return (lifetime + gracePeriod).hours + } + + public var warmupHours: Double { + return warmupDuration.hours + } + + public var dexcomAppURL: String { + switch self { + case .g7: + return "dexcomg7://" + case .onePlus: + return "dexcomg7://" // ONE+ Uses same URL as G7 app. If G7 and One+ is installed, the G7 app will open + case .stelo: + return "stelo://" + case .unknown: + return "dexcomg7://" // Default to G7 app + } + } + + /// Detects sensor type based on the sensor name/ID + public static func detect(from sensorName: String) -> G7SensorType { + let name = sensorName.uppercased() + + if name.hasPrefix("DXCM") { + // Check for 15-day G7 sensors (these might have a different prefix pattern) + // For now, assume all DXCM are 10-day G7, but this could be enhanced + // based on additional sensor data or naming patterns + return .g7 + } else if name.hasPrefix("DX01") { + return .stelo + } else if name.hasPrefix("DX02") { + return .onePlus + } else { + return .unknown + } + } +} diff --git a/G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift b/G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift index f3e0306..fbb9314 100644 --- a/G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift +++ b/G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift @@ -109,7 +109,7 @@ extension G7CGMManager: CGMManagerUI { let remaining = max(0, expiration.timeIntervalSinceNow) if remaining < .hours(24) { - return G7LifecycleProgress(percentComplete: 1-(remaining/G7Sensor.lifetime), progressState: .warning) + return G7LifecycleProgress(percentComplete: 1-(remaining/state.sensorType.lifetime), progressState: .warning) } return nil case .gracePeriod: @@ -117,7 +117,7 @@ extension G7CGMManager: CGMManagerUI { return nil } let remaining = max(0, endTime.timeIntervalSinceNow) - return G7LifecycleProgress(percentComplete: 1-(remaining/G7Sensor.gracePeriod), progressState: .critical) + return G7LifecycleProgress(percentComplete: 1-(remaining/state.sensorType.gracePeriod), progressState: .critical) case .expired: return G7LifecycleProgress(percentComplete: 1, progressState: .critical) default: diff --git a/G7SensorKitUI/Views/G7SettingsView.swift b/G7SensorKitUI/Views/G7SettingsView.swift index b5b50e4..2d4ab96 100644 --- a/G7SensorKitUI/Views/G7SettingsView.swift +++ b/G7SensorKitUI/Views/G7SettingsView.swift @@ -65,13 +65,13 @@ struct G7SettingsView: View { HStack { Text(LocalizedString("Sensor Expiration", comment: "title for g7 settings row showing sensor expiration time")) Spacer() - Text(timeFormatter.string(from: activatedAt.addingTimeInterval(G7Sensor.lifetime))) + Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.sensorType.lifetime))) .foregroundColor(.secondary) } HStack { Text(LocalizedString("Grace Period End", comment: "title for g7 settings row showing sensor grace period end time")) Spacer() - Text(timeFormatter.string(from: activatedAt.addingTimeInterval(G7Sensor.lifetime + G7Sensor.gracePeriod))) + Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.sensorType.lifetime + viewModel.sensorType.gracePeriod))) .foregroundColor(.secondary) } } @@ -85,6 +85,14 @@ struct G7SettingsView: View { LabeledValueView(label: LocalizedString("Trend", comment: "Field label"), value: viewModel.lastGlucoseTrendString) } + + Section () { + Button(LocalizedString("Open Dexcom App", comment:"Opens the dexcom app to allow users to manage active sensors"), action: { + if let appURL = URL(string: viewModel.sensorType.dexcomAppURL) { + UIApplication.shared.open(appURL) + } + }) + } Section("Bluetooth") { if let name = viewModel.sensorName { @@ -123,14 +131,6 @@ struct G7SettingsView: View { Toggle(LocalizedString("Upload Readings", comment: "title for g7 config settings to upload readings"), isOn: $viewModel.uploadReadings) } } - - Section () { - Button(LocalizedString("Open Dexcom App", comment:"Opens the dexcom G7 app to allow users to manage active sensors"), action: { - if let appURL = URL(string: "dexcomg7://") { - UIApplication.shared.open(appURL) - } - }) - } Section () { if !self.viewModel.scanning { @@ -144,7 +144,7 @@ struct G7SettingsView: View { } .insetGroupedListStyle() .navigationBarItems(trailing: doneButton) - .navigationBarTitle(LocalizedString("Dexcom G7", comment: "Navigation bar title for G7SettingsView")) + .navigationBarTitle(viewModel.sensorTypeDisplayName) } private var deleteCGMButton: some View { diff --git a/G7SensorKitUI/Views/G7SettingsViewModel.swift b/G7SensorKitUI/Views/G7SettingsViewModel.swift index 93bff10..856e70a 100644 --- a/G7SensorKitUI/Views/G7SettingsViewModel.swift +++ b/G7SensorKitUI/Views/G7SettingsViewModel.swift @@ -20,6 +20,7 @@ class G7SettingsViewModel: ObservableObject { @Published private(set) var scanning: Bool = false @Published private(set) var connected: Bool = false @Published private(set) var sensorName: String? + @Published private(set) var sensorType: G7SensorType = .unknown @Published private(set) var activatedAt: Date? @Published private(set) var lastConnect: Date? @Published private(set) var latestReadingTimestamp: Date? @@ -67,9 +68,14 @@ class G7SettingsViewModel: ObservableObject { self.cgmManager.addStateObserver(self, queue: DispatchQueue.main) } + var sensorTypeDisplayName: String { + return sensorType.displayName + } + func updateValues() { scanning = cgmManager.isScanning sensorName = cgmManager.sensorName + sensorType = cgmManager.state.sensorType activatedAt = cgmManager.sensorActivatedAt connected = cgmManager.isConnected lastConnect = cgmManager.lastConnect @@ -108,17 +114,17 @@ class G7SettingsViewModel: ObservableObject { guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / G7Sensor.warmupDuration + return 1 - value / sensorType.warmupDuration case .lifetimeRemaining: guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / G7Sensor.lifetime + return 1 - value / sensorType.lifetime case .gracePeriodRemaining: guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / G7Sensor.gracePeriod + return 1 - value / sensorType.gracePeriod case .sensorExpired, .sensorFailed: return 1 } From ffc9c9c02f98c7030bf84a6f3cc29b2fd3c7dda5 Mon Sep 17 00:00:00 2001 From: codebymini Date: Tue, 16 Dec 2025 09:12:53 +0100 Subject: [PATCH 4/5] Add toggle for 15 day sensor --- G7SensorKit/G7CGMManager/G7SensorType.swift | 24 +++++++++++++++---- G7SensorKitUI/Views/G7SettingsView.swift | 13 ++++++---- G7SensorKitUI/Views/G7SettingsViewModel.swift | 15 ++++++++---- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/G7SensorKit/G7CGMManager/G7SensorType.swift b/G7SensorKit/G7CGMManager/G7SensorType.swift index f2d1a0d..bce5c3a 100644 --- a/G7SensorKit/G7CGMManager/G7SensorType.swift +++ b/G7SensorKit/G7CGMManager/G7SensorType.swift @@ -10,6 +10,7 @@ import Foundation public enum G7SensorType: String, CaseIterable, CustomStringConvertible { case g7 = "G7" + case g7extended = "G7 Extended" case onePlus = "ONE+" case stelo = "Stelo" case unknown = "Unknown" @@ -18,6 +19,8 @@ public enum G7SensorType: String, CaseIterable, CustomStringConvertible { switch self { case .g7: return "Dexcom G7" + case .g7extended: + return "Dexcom G7 Extended" case .onePlus: return "Dexcom ONE+" case .stelo: @@ -35,6 +38,8 @@ public enum G7SensorType: String, CaseIterable, CustomStringConvertible { switch self { case .g7: return TimeInterval(hours: 10 * 24) // 10 days + case .g7extended: + return TimeInterval(hours: 15 * 24) // 15 days case .onePlus: return TimeInterval(hours: 10 * 24) // 10 days case .stelo: @@ -46,7 +51,7 @@ public enum G7SensorType: String, CaseIterable, CustomStringConvertible { public var gracePeriod: TimeInterval { switch self { - case .g7, .onePlus, .stelo, .unknown: + case .g7, .g7extended, .onePlus, .stelo, .unknown: return TimeInterval(hours: 12) // 12 hours for all } } @@ -54,7 +59,9 @@ public enum G7SensorType: String, CaseIterable, CustomStringConvertible { public var warmupDuration: TimeInterval { switch self { case .g7, .onePlus, .stelo, .unknown: - return TimeInterval(minutes: 25) // 25 minutes for all + return TimeInterval(minutes: 25) // 25 minutes for all other + case .g7extended: + return TimeInterval(minutes: 60) // 60 minutes for extended } } public var totalLifetimeHours: Double { @@ -67,7 +74,7 @@ public enum G7SensorType: String, CaseIterable, CustomStringConvertible { public var dexcomAppURL: String { switch self { - case .g7: + case .g7, .g7extended: return "dexcomg7://" case .onePlus: return "dexcomg7://" // ONE+ Uses same URL as G7 app. If G7 and One+ is installed, the G7 app will open @@ -81,9 +88,7 @@ public enum G7SensorType: String, CaseIterable, CustomStringConvertible { /// Detects sensor type based on the sensor name/ID public static func detect(from sensorName: String) -> G7SensorType { let name = sensorName.uppercased() - if name.hasPrefix("DXCM") { - // Check for 15-day G7 sensors (these might have a different prefix pattern) // For now, assume all DXCM are 10-day G7, but this could be enhanced // based on additional sensor data or naming patterns return .g7 @@ -95,4 +100,13 @@ public enum G7SensorType: String, CaseIterable, CustomStringConvertible { return .unknown } } + + public static func forFifteenDayOption(baseType: G7SensorType, isFifteenDaySensor: Bool) -> G7SensorType { + switch baseType { + case .g7: + return isFifteenDaySensor ? .g7extended : .g7 + default: + return baseType + } + } } diff --git a/G7SensorKitUI/Views/G7SettingsView.swift b/G7SensorKitUI/Views/G7SettingsView.swift index 2d4ab96..9bcefdc 100644 --- a/G7SensorKitUI/Views/G7SettingsView.swift +++ b/G7SensorKitUI/Views/G7SettingsView.swift @@ -65,13 +65,13 @@ struct G7SettingsView: View { HStack { Text(LocalizedString("Sensor Expiration", comment: "title for g7 settings row showing sensor expiration time")) Spacer() - Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.sensorType.lifetime))) + Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.effectiveSensorType.lifetime))) .foregroundColor(.secondary) } HStack { Text(LocalizedString("Grace Period End", comment: "title for g7 settings row showing sensor grace period end time")) Spacer() - Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.sensorType.lifetime + viewModel.sensorType.gracePeriod))) + Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.effectiveSensorType.lifetime + viewModel.effectiveSensorType.gracePeriod))) .foregroundColor(.secondary) } } @@ -127,9 +127,12 @@ struct G7SettingsView: View { } Section("Configuration") { - HStack { - Toggle(LocalizedString("Upload Readings", comment: "title for g7 config settings to upload readings"), isOn: $viewModel.uploadReadings) - } + HStack { + Toggle(LocalizedString("Upload Readings", comment: "title for g7 config settings to upload readings"), isOn: $viewModel.uploadReadings) + } + HStack { + Toggle(LocalizedString("15 Day Sensor", comment: "title for g7 config settings to enable 15 day sensor option"), isOn: $viewModel.isFifteenDaySensor) + } } Section () { diff --git a/G7SensorKitUI/Views/G7SettingsViewModel.swift b/G7SensorKitUI/Views/G7SettingsViewModel.swift index 856e70a..77e2029 100644 --- a/G7SensorKitUI/Views/G7SettingsViewModel.swift +++ b/G7SensorKitUI/Views/G7SettingsViewModel.swift @@ -29,6 +29,8 @@ class G7SettingsViewModel: ObservableObject { cgmManager.uploadReadings = uploadReadings } } + + @Published var isFifteenDaySensor: Bool = false let displayGlucosePreference: DisplayGlucosePreference @@ -69,7 +71,12 @@ class G7SettingsViewModel: ObservableObject { } var sensorTypeDisplayName: String { - return sensorType.displayName + return effectiveSensorType.displayName + } + + /// Returns the effective sensor type based on the toggle + var effectiveSensorType: G7SensorType { + G7SensorType.forFifteenDayOption(baseType: sensorType, isFifteenDaySensor: isFifteenDaySensor) } func updateValues() { @@ -114,17 +121,17 @@ class G7SettingsViewModel: ObservableObject { guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / sensorType.warmupDuration + return 1 - value / effectiveSensorType.warmupDuration case .lifetimeRemaining: guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / sensorType.lifetime + return 1 - value / effectiveSensorType.lifetime case .gracePeriodRemaining: guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / sensorType.gracePeriod + return 1 - value / effectiveSensorType.gracePeriod case .sensorExpired, .sensorFailed: return 1 } From 75ae85b44f16169f3cc74dfd14c987ed909c1759 Mon Sep 17 00:00:00 2001 From: codebymini Date: Thu, 18 Dec 2025 00:26:25 +0100 Subject: [PATCH 5/5] Persist setting for 15 day sensor --- G7SensorKit/G7CGMManager/G7CGMManager.swift | 15 +++++++------ .../G7CGMManager/G7CGMManagerState.swift | 5 ++++- G7SensorKit/G7CGMManager/G7Sensor.swift | 2 +- G7SensorKit/G7CGMManager/G7SensorType.swift | 7 ++++--- G7SensorKitUI/Views/G7SettingsView.swift | 4 ++-- G7SensorKitUI/Views/G7SettingsViewModel.swift | 21 ++++++++++--------- 6 files changed, 29 insertions(+), 25 deletions(-) diff --git a/G7SensorKit/G7CGMManager/G7CGMManager.swift b/G7SensorKit/G7CGMManager/G7CGMManager.swift index 8a0af52..b3182a6 100644 --- a/G7SensorKit/G7CGMManager/G7CGMManager.swift +++ b/G7SensorKit/G7CGMManager/G7CGMManager.swift @@ -147,14 +147,13 @@ public class G7CGMManager: CGMManager { } public var uploadReadings: Bool { - get { - return state.uploadReadings - } - set { - mutateState { state in - state.uploadReadings = newValue - } - } + get { state.uploadReadings } + set { mutateState { $0.uploadReadings = newValue } } + } + + public var isFifteenDaySensor: Bool { + get { state.isFifteenDaySensor } + set { mutateState { $0.isFifteenDaySensor = newValue } } } public let sensor: G7Sensor diff --git a/G7SensorKit/G7CGMManager/G7CGMManagerState.swift b/G7SensorKit/G7CGMManager/G7CGMManagerState.swift index cae351e..a382dca 100644 --- a/G7SensorKit/G7CGMManager/G7CGMManagerState.swift +++ b/G7SensorKit/G7CGMManager/G7CGMManagerState.swift @@ -20,6 +20,7 @@ public struct G7CGMManagerState: RawRepresentable, Equatable { public var latestReadingTimestamp: Date? public var latestConnect: Date? public var uploadReadings: Bool = false + public var isFifteenDaySensor: Bool = false init() { } @@ -31,7 +32,7 @@ public struct G7CGMManagerState: RawRepresentable, Equatable { self.sensorType = sensorType } else { if let sensorID = rawValue["sensorID"] as? String { - self.sensorType = G7SensorType.detect(from: sensorID) + self.sensorType = G7SensorType.detect(from: sensorID, isFifteenDaySensor: rawValue["isFifteenDaySensor"] as? Bool ?? false) } } self.activatedAt = rawValue["activatedAt"] as? Date @@ -41,6 +42,7 @@ public struct G7CGMManagerState: RawRepresentable, Equatable { self.latestReadingTimestamp = rawValue["latestReadingTimestamp"] as? Date self.latestConnect = rawValue["latestConnect"] as? Date self.uploadReadings = rawValue["uploadReadings"] as? Bool ?? false + self.isFifteenDaySensor = rawValue["isFifteenDaySensor"] as? Bool ?? false } public var rawValue: RawValue { @@ -52,6 +54,7 @@ public struct G7CGMManagerState: RawRepresentable, Equatable { rawValue["latestReadingTimestamp"] = latestReadingTimestamp rawValue["latestConnect"] = latestConnect rawValue["uploadReadings"] = uploadReadings + rawValue["isFifteenDaySensor"] = isFifteenDaySensor return rawValue } } diff --git a/G7SensorKit/G7CGMManager/G7Sensor.swift b/G7SensorKit/G7CGMManager/G7Sensor.swift index 223aaaf..1977095 100644 --- a/G7SensorKit/G7CGMManager/G7Sensor.swift +++ b/G7SensorKit/G7CGMManager/G7Sensor.swift @@ -230,7 +230,7 @@ public final class G7Sensor: G7BluetoothManagerDelegate { /// The Dexcom One+ prefix is "DX02" if name.hasPrefix("DXCM") || name.hasPrefix("DX01") || name.hasPrefix("DX02"){ // Auto-detect sensor type when connecting - sensorType = G7SensorType.detect(from: name) + sensorType = G7SensorType.detect(from: name, isFifteenDaySensor: false) // If we're following this name or if we're scanning, connect if let sensorName = sensorID, name.suffix(2) == sensorName.suffix(2) { diff --git a/G7SensorKit/G7CGMManager/G7SensorType.swift b/G7SensorKit/G7CGMManager/G7SensorType.swift index bce5c3a..c1785f8 100644 --- a/G7SensorKit/G7CGMManager/G7SensorType.swift +++ b/G7SensorKit/G7CGMManager/G7SensorType.swift @@ -86,11 +86,12 @@ public enum G7SensorType: String, CaseIterable, CustomStringConvertible { } /// Detects sensor type based on the sensor name/ID - public static func detect(from sensorName: String) -> G7SensorType { + public static func detect(from sensorName: String, isFifteenDaySensor: Bool) -> G7SensorType { let name = sensorName.uppercased() if name.hasPrefix("DXCM") { - // For now, assume all DXCM are 10-day G7, but this could be enhanced - // based on additional sensor data or naming patterns + if isFifteenDaySensor { + return .g7extended + } return .g7 } else if name.hasPrefix("DX01") { return .stelo diff --git a/G7SensorKitUI/Views/G7SettingsView.swift b/G7SensorKitUI/Views/G7SettingsView.swift index 9bcefdc..e52407b 100644 --- a/G7SensorKitUI/Views/G7SettingsView.swift +++ b/G7SensorKitUI/Views/G7SettingsView.swift @@ -65,13 +65,13 @@ struct G7SettingsView: View { HStack { Text(LocalizedString("Sensor Expiration", comment: "title for g7 settings row showing sensor expiration time")) Spacer() - Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.effectiveSensorType.lifetime))) + Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.sensorType.lifetime))) .foregroundColor(.secondary) } HStack { Text(LocalizedString("Grace Period End", comment: "title for g7 settings row showing sensor grace period end time")) Spacer() - Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.effectiveSensorType.lifetime + viewModel.effectiveSensorType.gracePeriod))) + Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.sensorType.lifetime + viewModel.sensorType.gracePeriod))) .foregroundColor(.secondary) } } diff --git a/G7SensorKitUI/Views/G7SettingsViewModel.swift b/G7SensorKitUI/Views/G7SettingsViewModel.swift index 77e2029..1b4382b 100644 --- a/G7SensorKitUI/Views/G7SettingsViewModel.swift +++ b/G7SensorKitUI/Views/G7SettingsViewModel.swift @@ -30,7 +30,11 @@ class G7SettingsViewModel: ObservableObject { } } - @Published var isFifteenDaySensor: Bool = false + @Published var isFifteenDaySensor: Bool = false { + didSet { + cgmManager.isFifteenDaySensor = isFifteenDaySensor + } + } let displayGlucosePreference: DisplayGlucosePreference @@ -67,16 +71,12 @@ class G7SettingsViewModel: ObservableObject { self.displayGlucosePreference = displayGlucosePreference updateValues() + self.isFifteenDaySensor = cgmManager.isFifteenDaySensor self.cgmManager.addStateObserver(self, queue: DispatchQueue.main) } var sensorTypeDisplayName: String { - return effectiveSensorType.displayName - } - - /// Returns the effective sensor type based on the toggle - var effectiveSensorType: G7SensorType { - G7SensorType.forFifteenDayOption(baseType: sensorType, isFifteenDaySensor: isFifteenDaySensor) + return sensorType.displayName } func updateValues() { @@ -89,6 +89,7 @@ class G7SettingsViewModel: ObservableObject { lastReading = cgmManager.latestReading latestReadingTimestamp = cgmManager.latestReadingTimestamp uploadReadings = cgmManager.state.uploadReadings + isFifteenDaySensor = cgmManager.isFifteenDaySensor } var progressBarColorStyle: ColorStyle { @@ -121,17 +122,17 @@ class G7SettingsViewModel: ObservableObject { guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / effectiveSensorType.warmupDuration + return 1 - value / sensorType.warmupDuration case .lifetimeRemaining: guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / effectiveSensorType.lifetime + return 1 - value / sensorType.lifetime case .gracePeriodRemaining: guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / effectiveSensorType.gracePeriod + return 1 - value / sensorType.gracePeriod case .sensorExpired, .sensorFailed: return 1 }