Skip to content

Commit 6ee0cb4

Browse files
committed
Add manager for G7 platform sensors (G7, ONE+, Stelo)
1 parent 8b18b0f commit 6ee0cb4

File tree

8 files changed

+153
-24
lines changed

8 files changed

+153
-24
lines changed

G7SensorKit.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
6515DB522E695F77005C42DC /* G7SensorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6515DB512E695F77005C42DC /* G7SensorType.swift */; };
1011
B60BB2E42BC649DA00D2BB39 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60BB2E32BC649DA00D2BB39 /* Bundle.swift */; };
1112
C109F14A291ECCE2008EA5B6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C109F149291ECCE2008EA5B6 /* Assets.xcassets */; };
1213
C109F14C291ED66F008EA5B6 /* G7GlucoseMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C109F14B291ED66F008EA5B6 /* G7GlucoseMessageTests.swift */; };
@@ -107,6 +108,7 @@
107108
/* End PBXCopyFilesBuildPhase section */
108109

109110
/* Begin PBXFileReference section */
111+
6515DB512E695F77005C42DC /* G7SensorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = G7SensorType.swift; sourceTree = "<group>"; };
110112
B60BB2E32BC649DA00D2BB39 /* Bundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Bundle.swift; path = G7SensorKitUI/Extensions/Bundle.swift; sourceTree = SOURCE_ROOT; };
111113
C1086B0E29C9169100D46E65 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
112114
C1086B0F29C9169100D46E65 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -288,6 +290,7 @@
288290
C17F50DE291EAC6500555EB5 /* G7CGMManager */ = {
289291
isa = PBXGroup;
290292
children = (
293+
6515DB512E695F77005C42DC /* G7SensorType.swift */,
291294
C17F50DF291EAC6500555EB5 /* G7BackfillMessage.swift */,
292295
C17F50E7291EAC6500555EB5 /* G7BluetoothManager.swift */,
293296
C17F50E5291EAC6500555EB5 /* G7CGMManager.swift */,
@@ -606,6 +609,7 @@
606609
C17F5140291EB27D00555EB5 /* TimeInterval.swift in Sources */,
607610
C17F50F0291EAC6500555EB5 /* G7CGMManagerState.swift in Sources */,
608611
C17F5145291EB45900555EB5 /* CBPeripheral.swift in Sources */,
612+
6515DB522E695F77005C42DC /* G7SensorType.swift in Sources */,
609613
C17F513A291EB0D900555EB5 /* GlucoseLimits.swift in Sources */,
610614
C17F5143291EB36700555EB5 /* AuthChallengeRxMessage.swift in Sources */,
611615
C17F50EA291EAC6500555EB5 /* G7DeviceStatus.swift in Sources */,

G7SensorKit/G7CGMManager/G7CGMManager.swift

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,22 +116,22 @@ public class G7CGMManager: CGMManager {
116116
guard let activatedAt = sensorActivatedAt else {
117117
return nil
118118
}
119-
return activatedAt.addingTimeInterval(G7Sensor.lifetime)
119+
return activatedAt.addingTimeInterval(state.sensorType.lifetime)
120120
}
121121

122122
public var sensorEndsAt: Date? {
123123
guard let activatedAt = sensorActivatedAt else {
124124
return nil
125125
}
126-
return activatedAt.addingTimeInterval(G7Sensor.lifetime + G7Sensor.gracePeriod)
126+
return activatedAt.addingTimeInterval(state.sensorType.lifetime + state.sensorType.gracePeriod)
127127
}
128128

129129

130130
public var sensorFinishesWarmupAt: Date? {
131131
guard let activatedAt = sensorActivatedAt else {
132132
return nil
133133
}
134-
return activatedAt.addingTimeInterval(G7Sensor.warmupDuration)
134+
return activatedAt.addingTimeInterval(state.sensorType.warmupDuration)
135135
}
136136

137137
public var latestReading: G7GlucoseMessage? {
@@ -229,7 +229,9 @@ public class G7CGMManager: CGMManager {
229229

230230
public static let pluginIdentifier: String = "G7CGMManager"
231231

232-
public let localizedTitle = LocalizedString("Dexcom G7", comment: "CGM display title")
232+
public var localizedTitle: String {
233+
return state.sensorType.displayName
234+
}
233235

234236
public let isOnboarded = true // No distinction between created and onboarded
235237

@@ -242,6 +244,7 @@ public class G7CGMManager: CGMManager {
242244

243245
mutateState { state in
244246
state.sensorID = nil
247+
state.sensorType = .unknown
245248
state.activatedAt = nil
246249
}
247250
sensor.scanForNewSensor()
@@ -251,7 +254,7 @@ public class G7CGMManager: CGMManager {
251254
return HKDevice(
252255
name: state.sensorID ?? "Unknown",
253256
manufacturer: "Dexcom",
254-
model: "G7",
257+
model: state.sensorType.rawValue,
255258
hardwareVersion: nil,
256259
firmwareVersion: nil,
257260
softwareVersion: "CGMBLEKit" + String(G7SensorKitVersionNumber),
@@ -292,14 +295,15 @@ extension G7CGMManager: G7SensorDelegate {
292295
if shouldSwitchToNewSensor {
293296
mutateState { state in
294297
state.sensorID = name
298+
state.sensorType = sensor.sensorType
295299
state.activatedAt = activatedAt
296300
}
297301
let event = PersistedCgmEvent(
298302
date: activatedAt,
299303
type: .sensorStart,
300304
deviceIdentifier: name,
301-
expectedLifetime: .hours(24 * 10 + 12),
302-
warmupPeriod: .hours(2)
305+
expectedLifetime: .hours(sensor.sensorType.lifetime.hours + sensor.sensorType.gracePeriod.hours),
306+
warmupPeriod: .hours(sensor.sensorType.warmupDuration.hours)
303307
)
304308
delegate.notify { delegate in
305309
delegate?.cgmManager(self, hasNew: [event])

G7SensorKit/G7CGMManager/G7CGMManagerState.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public struct G7CGMManagerState: RawRepresentable, Equatable {
1414
public typealias RawValue = CGMManager.RawStateValue
1515

1616
public var sensorID: String?
17+
public var sensorType: G7SensorType = .unknown
1718
public var activatedAt: Date?
1819
public var latestReading: G7GlucoseMessage?
1920
public var latestReadingTimestamp: Date?
@@ -25,6 +26,14 @@ public struct G7CGMManagerState: RawRepresentable, Equatable {
2526

2627
public init(rawValue: RawValue) {
2728
self.sensorID = rawValue["sensorID"] as? String
29+
if let sensorTypeString = rawValue["sensorType"] as? String,
30+
let sensorType = G7SensorType(rawValue: sensorTypeString) {
31+
self.sensorType = sensorType
32+
} else {
33+
if let sensorID = rawValue["sensorID"] as? String {
34+
self.sensorType = G7SensorType.detect(from: sensorID)
35+
}
36+
}
2837
self.activatedAt = rawValue["activatedAt"] as? Date
2938
if let readingData = rawValue["latestReading"] as? Data {
3039
latestReading = G7GlucoseMessage(data: readingData)
@@ -37,6 +46,7 @@ public struct G7CGMManagerState: RawRepresentable, Equatable {
3746
public var rawValue: RawValue {
3847
var rawValue: RawValue = [:]
3948
rawValue["sensorID"] = sensorID
49+
rawValue["sensorType"] = sensorType.rawValue
4050
rawValue["activatedAt"] = activatedAt
4151
rawValue["latestReading"] = latestReading?.data
4252
rawValue["latestReadingTimestamp"] = latestReadingTimestamp

G7SensorKit/G7CGMManager/G7Sensor.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ public enum G7SensorLifecycleState {
6060

6161

6262
public final class G7Sensor: G7BluetoothManagerDelegate {
63+
// Legacy static properties for backward compatibility
6364
public static let lifetime = TimeInterval(hours: 10 * 24)
6465
public static let warmupDuration = TimeInterval(minutes: 25)
6566
public static let gracePeriod = TimeInterval(hours: 12)
67+
68+
// Current sensor type for dynamic timing
69+
public var sensorType: G7SensorType = .unknown
6670

6771
public weak var delegate: G7SensorDelegate?
6872

@@ -215,9 +219,12 @@ public final class G7Sensor: G7BluetoothManagerDelegate {
215219
}
216220

217221
/// The Dexcom G7 advertises a peripheral name of "DXCMxx", and later reports a full name of "Dexcomxx"
218-
/// The Dexcom Stelo prelix is "DX01"
222+
/// The Dexcom Stelo prefix is "DX01"
219223
/// The Dexcom One+ prefix is "DX02"
220224
if name.hasPrefix("DXCM") || name.hasPrefix("DX01") || name.hasPrefix("DX02"){
225+
// Auto-detect sensor type when connecting
226+
sensorType = G7SensorType.detect(from: name)
227+
221228
// If we're following this name or if we're scanning, connect
222229
if let sensorName = sensorID, name.suffix(2) == sensorName.suffix(2) {
223230
return .makeActive
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//
2+
// G7SensorType.swift
3+
// G7SensorKit
4+
//
5+
// Created by Daniel Johansson on 12/19/24.
6+
// Copyright © 2024 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public enum G7SensorType: String, CaseIterable, CustomStringConvertible {
12+
case g7 = "G7"
13+
case onePlus = "ONE+"
14+
case stelo = "Stelo"
15+
case unknown = "Unknown"
16+
17+
public var description: String {
18+
switch self {
19+
case .g7:
20+
return "Dexcom G7"
21+
case .onePlus:
22+
return "Dexcom ONE+"
23+
case .stelo:
24+
return "Dexcom Stelo"
25+
case .unknown:
26+
return "Unknown Sensor"
27+
}
28+
}
29+
30+
public var displayName: String {
31+
return description
32+
}
33+
34+
public var lifetime: TimeInterval {
35+
switch self {
36+
case .g7:
37+
return TimeInterval(hours: 10 * 24) // 10 days
38+
case .onePlus:
39+
return TimeInterval(hours: 10 * 24) // 10 days
40+
case .stelo:
41+
return TimeInterval(hours: 15 * 24) // 15 days
42+
case .unknown:
43+
return TimeInterval(hours: 10 * 24) // Default to 10 days
44+
}
45+
}
46+
47+
public var gracePeriod: TimeInterval {
48+
switch self {
49+
case .g7, .onePlus, .stelo, .unknown:
50+
return TimeInterval(hours: 12) // 12 hours for all
51+
}
52+
}
53+
54+
public var warmupDuration: TimeInterval {
55+
switch self {
56+
case .g7, .onePlus, .stelo, .unknown:
57+
return TimeInterval(minutes: 25) // 25 minutes for all
58+
}
59+
}
60+
public var totalLifetimeHours: Double {
61+
return (lifetime + gracePeriod).hours
62+
}
63+
64+
public var warmupHours: Double {
65+
return warmupDuration.hours
66+
}
67+
68+
public var dexcomAppURL: String {
69+
switch self {
70+
case .g7:
71+
return "dexcomg7://"
72+
case .onePlus:
73+
return "dexcomg7://" // ONE+ Uses same URL as G7 app. If G7 and One+ is installed, the G7 app will open
74+
case .stelo:
75+
return "stelo://"
76+
case .unknown:
77+
return "dexcomg7://" // Default to G7 app
78+
}
79+
}
80+
81+
/// Detects sensor type based on the sensor name/ID
82+
public static func detect(from sensorName: String) -> G7SensorType {
83+
let name = sensorName.uppercased()
84+
85+
if name.hasPrefix("DXCM") {
86+
// Check for 15-day G7 sensors (these might have a different prefix pattern)
87+
// For now, assume all DXCM are 10-day G7, but this could be enhanced
88+
// based on additional sensor data or naming patterns
89+
return .g7
90+
} else if name.hasPrefix("DX01") {
91+
return .stelo
92+
} else if name.hasPrefix("DX02") {
93+
return .onePlus
94+
} else {
95+
return .unknown
96+
}
97+
}
98+
}

G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,15 @@ extension G7CGMManager: CGMManagerUI {
109109
let remaining = max(0, expiration.timeIntervalSinceNow)
110110

111111
if remaining < .hours(24) {
112-
return G7LifecycleProgress(percentComplete: 1-(remaining/G7Sensor.lifetime), progressState: .warning)
112+
return G7LifecycleProgress(percentComplete: 1-(remaining/state.sensorType.lifetime), progressState: .warning)
113113
}
114114
return nil
115115
case .gracePeriod:
116116
guard let endTime = sensorEndsAt else {
117117
return nil
118118
}
119119
let remaining = max(0, endTime.timeIntervalSinceNow)
120-
return G7LifecycleProgress(percentComplete: 1-(remaining/G7Sensor.gracePeriod), progressState: .critical)
120+
return G7LifecycleProgress(percentComplete: 1-(remaining/state.sensorType.gracePeriod), progressState: .critical)
121121
case .expired:
122122
return G7LifecycleProgress(percentComplete: 1, progressState: .critical)
123123
default:

G7SensorKitUI/Views/G7SettingsView.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ struct G7SettingsView: View {
6565
HStack {
6666
Text(LocalizedString("Sensor Expiration", comment: "title for g7 settings row showing sensor expiration time"))
6767
Spacer()
68-
Text(timeFormatter.string(from: activatedAt.addingTimeInterval(G7Sensor.lifetime)))
68+
Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.sensorType.lifetime)))
6969
.foregroundColor(.secondary)
7070
}
7171
HStack {
7272
Text(LocalizedString("Grace Period End", comment: "title for g7 settings row showing sensor grace period end time"))
7373
Spacer()
74-
Text(timeFormatter.string(from: activatedAt.addingTimeInterval(G7Sensor.lifetime + G7Sensor.gracePeriod)))
74+
Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.sensorType.lifetime + viewModel.sensorType.gracePeriod)))
7575
.foregroundColor(.secondary)
7676
}
7777
}
@@ -85,6 +85,14 @@ struct G7SettingsView: View {
8585
LabeledValueView(label: LocalizedString("Trend", comment: "Field label"),
8686
value: viewModel.lastGlucoseTrendString)
8787
}
88+
89+
Section () {
90+
Button(LocalizedString("Open Dexcom App", comment:"Opens the dexcom app to allow users to manage active sensors"), action: {
91+
if let appURL = URL(string: viewModel.sensorType.dexcomAppURL) {
92+
UIApplication.shared.open(appURL)
93+
}
94+
})
95+
}
8896

8997
Section("Bluetooth") {
9098
if let name = viewModel.sensorName {
@@ -123,14 +131,6 @@ struct G7SettingsView: View {
123131
Toggle(LocalizedString("Upload Readings", comment: "title for g7 config settings to upload readings"), isOn: $viewModel.uploadReadings)
124132
}
125133
}
126-
127-
Section () {
128-
Button(LocalizedString("Open Dexcom App", comment:"Opens the dexcom G7 app to allow users to manage active sensors"), action: {
129-
if let appURL = URL(string: "dexcomg7://") {
130-
UIApplication.shared.open(appURL)
131-
}
132-
})
133-
}
134134

135135
Section () {
136136
if !self.viewModel.scanning {
@@ -144,7 +144,7 @@ struct G7SettingsView: View {
144144
}
145145
.insetGroupedListStyle()
146146
.navigationBarItems(trailing: doneButton)
147-
.navigationBarTitle(LocalizedString("Dexcom G7", comment: "Navigation bar title for G7SettingsView"))
147+
.navigationBarTitle(viewModel.sensorTypeDisplayName)
148148
}
149149

150150
private var deleteCGMButton: some View {

G7SensorKitUI/Views/G7SettingsViewModel.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class G7SettingsViewModel: ObservableObject {
2020
@Published private(set) var scanning: Bool = false
2121
@Published private(set) var connected: Bool = false
2222
@Published private(set) var sensorName: String?
23+
@Published private(set) var sensorType: G7SensorType = .unknown
2324
@Published private(set) var activatedAt: Date?
2425
@Published private(set) var lastConnect: Date?
2526
@Published private(set) var latestReadingTimestamp: Date?
@@ -67,9 +68,14 @@ class G7SettingsViewModel: ObservableObject {
6768
self.cgmManager.addStateObserver(self, queue: DispatchQueue.main)
6869
}
6970

71+
var sensorTypeDisplayName: String {
72+
return sensorType.displayName
73+
}
74+
7075
func updateValues() {
7176
scanning = cgmManager.isScanning
7277
sensorName = cgmManager.sensorName
78+
sensorType = cgmManager.state.sensorType
7379
activatedAt = cgmManager.sensorActivatedAt
7480
connected = cgmManager.isConnected
7581
lastConnect = cgmManager.lastConnect
@@ -108,17 +114,17 @@ class G7SettingsViewModel: ObservableObject {
108114
guard let value = progressValue, value > 0 else {
109115
return 0
110116
}
111-
return 1 - value / G7Sensor.warmupDuration
117+
return 1 - value / sensorType.warmupDuration
112118
case .lifetimeRemaining:
113119
guard let value = progressValue, value > 0 else {
114120
return 0
115121
}
116-
return 1 - value / G7Sensor.lifetime
122+
return 1 - value / sensorType.lifetime
117123
case .gracePeriodRemaining:
118124
guard let value = progressValue, value > 0 else {
119125
return 0
120126
}
121-
return 1 - value / G7Sensor.gracePeriod
127+
return 1 - value / sensorType.gracePeriod
122128
case .sensorExpired, .sensorFailed:
123129
return 1
124130
}

0 commit comments

Comments
 (0)