From f50002da2ea028ef0da711edf6b1020a50f950d2 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 31 Dec 2025 12:31:52 -0600 Subject: [PATCH 1/4] Request extended version message for dynamic lifetime determination --- G7SensorKit.xcodeproj/project.pbxproj | 4 ++ G7SensorKit/G7CGMManager/G7CGMManager.swift | 20 ++++++++- .../G7CGMManager/G7CGMManagerState.swift | 5 +++ .../G7CGMManager/G7PeripheralManager.swift | 14 ++++++ G7SensorKit/G7CGMManager/G7Sensor.swift | 40 ++++++++++++++--- .../Messages/ExtendedVersionMessage.swift | 43 +++++++++++++++++++ G7SensorKit/Messages/G7Opcode.swift | 2 + .../G7CGMManager/G7CGMManager+UI.swift | 2 +- G7SensorKitUI/Views/G7SettingsView.swift | 4 +- G7SensorKitUI/Views/G7SettingsViewModel.swift | 5 ++- 10 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 G7SensorKit/Messages/ExtendedVersionMessage.swift diff --git a/G7SensorKit.xcodeproj/project.pbxproj b/G7SensorKit.xcodeproj/project.pbxproj index 9582049..85e78fd 100644 --- a/G7SensorKit.xcodeproj/project.pbxproj +++ b/G7SensorKit.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ B60BB2E42BC649DA00D2BB39 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60BB2E32BC649DA00D2BB39 /* Bundle.swift */; }; B66D1F6D2E6A803800471149 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B66D1F6C2E6A803800471149 /* Localizable.xcstrings */; }; B66D1F6F2E6A803800471149 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B66D1F6E2E6A803800471149 /* Localizable.xcstrings */; }; + C107607F2F059130008B2B39 /* ExtendedVersionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C107607E2F05912B008B2B39 /* ExtendedVersionMessage.swift */; }; C109F14A291ECCE2008EA5B6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C109F149291ECCE2008EA5B6 /* Assets.xcassets */; }; C109F14C291ED66F008EA5B6 /* G7GlucoseMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C109F14B291ED66F008EA5B6 /* G7GlucoseMessageTests.swift */; }; C139829829295D7D0047DB5F /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17F514A291EB6F000555EB5 /* HKUnit.swift */; }; @@ -112,6 +113,7 @@ B60BB2E32BC649DA00D2BB39 /* Bundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Bundle.swift; path = G7SensorKitUI/Extensions/Bundle.swift; sourceTree = SOURCE_ROOT; }; B66D1F6C2E6A803800471149 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; B66D1F6E2E6A803800471149 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + C107607E2F05912B008B2B39 /* ExtendedVersionMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedVersionMessage.swift; sourceTree = ""; }; C109F149291ECCE2008EA5B6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C109F14B291ED66F008EA5B6 /* G7GlucoseMessageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = G7GlucoseMessageTests.swift; sourceTree = ""; }; C1409A08291EC22F006BE8D0 /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; @@ -319,6 +321,7 @@ C17F5141291EB34800555EB5 /* Messages */ = { isa = PBXGroup; children = ( + C107607E2F05912B008B2B39 /* ExtendedVersionMessage.swift */, C17F50E3291EAC6500555EB5 /* G7Opcode.swift */, C17F5146291EB57700555EB5 /* SensorMessage.swift */, C17F50E8291EAC6500555EB5 /* G7GlucoseMessage.swift */, @@ -571,6 +574,7 @@ C17F5140291EB27D00555EB5 /* TimeInterval.swift in Sources */, C17F50F0291EAC6500555EB5 /* G7CGMManagerState.swift in Sources */, C17F5145291EB45900555EB5 /* CBPeripheral.swift in Sources */, + C107607F2F059130008B2B39 /* ExtendedVersionMessage.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 ccddaeb..6a74d61 100644 --- a/G7SensorKit/G7CGMManager/G7CGMManager.swift +++ b/G7SensorKit/G7CGMManager/G7CGMManager.swift @@ -112,18 +112,26 @@ public class G7CGMManager: CGMManager { return state.activatedAt } + public var lifetime: TimeInterval { + if let maxLifetimeDays = state.extendedVersion?.maxLifetimeDays { + return TimeInterval(hours: Double(maxLifetimeDays) * 24) + } else { + return G7Sensor.defaultLifetime + } + } + public var sensorExpiresAt: Date? { guard let activatedAt = sensorActivatedAt else { return nil } - return activatedAt.addingTimeInterval(G7Sensor.lifetime) + return activatedAt.addingTimeInterval(lifetime) } public var sensorEndsAt: Date? { guard let activatedAt = sensorActivatedAt else { return nil } - return activatedAt.addingTimeInterval(G7Sensor.lifetime + G7Sensor.gracePeriod) + return activatedAt.addingTimeInterval(lifetime + G7Sensor.gracePeriod) } @@ -201,6 +209,7 @@ public class G7CGMManager: CGMManager { lockedState = Locked(state) sensor = G7Sensor(sensorID: state.sensorID) sensor.delegate = self + sensor.needsVersionInfo = state.extendedVersion == nil } public var rawState: RawStateValue { @@ -243,6 +252,7 @@ public class G7CGMManager: CGMManager { mutateState { state in state.sensorID = nil state.activatedAt = nil + state.extendedVersion = nil } sensor.scanForNewSensor() } @@ -309,6 +319,12 @@ extension G7CGMManager: G7SensorDelegate { return shouldSwitchToNewSensor } + public func sensor(_ sensor: G7Sensor, didReceive extendedVersion: ExtendedVersionMessage) { + mutateState { state in + state.extendedVersion = extendedVersion + } + } + public func sensorDidConnect(_ sensor: G7Sensor, name: String) { mutateState { state in state.latestConnect = Date() diff --git a/G7SensorKit/G7CGMManager/G7CGMManagerState.swift b/G7SensorKit/G7CGMManager/G7CGMManagerState.swift index 948b02f..e1d020c 100644 --- a/G7SensorKit/G7CGMManager/G7CGMManagerState.swift +++ b/G7SensorKit/G7CGMManager/G7CGMManagerState.swift @@ -15,6 +15,7 @@ public struct G7CGMManagerState: RawRepresentable, Equatable { public var sensorID: String? public var activatedAt: Date? + public var extendedVersion: ExtendedVersionMessage? public var latestReading: G7GlucoseMessage? public var latestReadingTimestamp: Date? public var latestConnect: Date? @@ -29,6 +30,9 @@ public struct G7CGMManagerState: RawRepresentable, Equatable { if let readingData = rawValue["latestReading"] as? Data { latestReading = G7GlucoseMessage(data: readingData) } + if let extendedVersionData = rawValue["extendedVersion"] as? Data { + extendedVersion = ExtendedVersionMessage(data: extendedVersionData) + } self.latestReadingTimestamp = rawValue["latestReadingTimestamp"] as? Date self.latestConnect = rawValue["latestConnect"] as? Date self.uploadReadings = rawValue["uploadReadings"] as? Bool ?? false @@ -39,6 +43,7 @@ public struct G7CGMManagerState: RawRepresentable, Equatable { rawValue["sensorID"] = sensorID rawValue["activatedAt"] = activatedAt rawValue["latestReading"] = latestReading?.data + rawValue["extendedVersion"] = extendedVersion?.data rawValue["latestReadingTimestamp"] = latestReadingTimestamp rawValue["latestConnect"] = latestConnect rawValue["uploadReadings"] = uploadReadings diff --git a/G7SensorKit/G7CGMManager/G7PeripheralManager.swift b/G7SensorKit/G7CGMManager/G7PeripheralManager.swift index 2bea009..cffae13 100644 --- a/G7SensorKit/G7CGMManager/G7PeripheralManager.swift +++ b/G7SensorKit/G7CGMManager/G7PeripheralManager.swift @@ -80,6 +80,20 @@ class G7PeripheralManager: NSObject { assertConfiguration() } + + func requestExtendedVersion() throws { + self.log.default("Requesting sensor extended version"); + guard let service = peripheral.services?.itemWithUUID(SensorServiceUUID.cgmService.cbUUID) else { + self.log.error("Peripheral missing cgm service. Services = %{public}@", String(describing: peripheral.services)); + throw PeripheralManagerError.invalidConfiguration + } + + guard let characteristic = service.characteristics?.itemWithUUID(CGMServiceCharacteristicUUID.control.cbUUID) else { + throw PeripheralManagerError.unknownCharacteristic + } + + try writeValue(Data([G7Opcode.extendedVersionTx.rawValue]), for: characteristic, type: .withResponse, timeout: 1) + } } diff --git a/G7SensorKit/G7CGMManager/G7Sensor.swift b/G7SensorKit/G7CGMManager/G7Sensor.swift index aa88883..23dd7c1 100644 --- a/G7SensorKit/G7CGMManager/G7Sensor.swift +++ b/G7SensorKit/G7CGMManager/G7Sensor.swift @@ -28,6 +28,8 @@ public protocol G7SensorDelegate: AnyObject { // If this returns true, then start following this sensor func sensor(_ sensor: G7Sensor, didDiscoverNewSensor name: String, activatedAt: Date) -> Bool + func sensor(_ sensor: G7Sensor, didReceive extendedVersion: ExtendedVersionMessage) + // This is triggered for connection/disconnection events, and enabling/disabling scan func sensorConnectionStatusDidUpdate(_ sensor: G7Sensor) } @@ -62,7 +64,7 @@ public enum G7SensorLifecycleState { public final class G7Sensor: G7BluetoothManagerDelegate { - public static let lifetime = TimeInterval(hours: 10 * 24) + public static let defaultLifetime = TimeInterval(hours: 10 * 24) public static let warmupDuration = TimeInterval(minutes: 25) public static let gracePeriod = TimeInterval(hours: 12) @@ -73,6 +75,9 @@ public final class G7Sensor: G7BluetoothManagerDelegate { /// The initial activation date of the sensor var activationDate: Date? + /// The initial activation date of the sensor + var needsVersionInfo: Bool = false + /// The date of last connection private var lastConnection: Date? @@ -92,10 +97,6 @@ public final class G7Sensor: G7BluetoothManagerDelegate { private var sensorID: String? - public func setSensorId(_ newId: String) { - self.sensorID = newId - } - public init(sensorID: String?) { self.sensorID = sensorID bluetoothManager.delegate = self @@ -138,6 +139,17 @@ public final class G7Sensor: G7BluetoothManagerDelegate { } } } + + if needsVersionInfo, let name = peripheralManager.peripheral.name, name == sensorID { + peripheralManager.perform { (peripheral) in + do { + try peripheral.requestExtendedVersion() + } catch let error { + self.log.error("Error trying to request extended version: %{public}@", String(describing: error)) + } + } + } + if sensorID == nil, let name = peripheralManager.peripheral.name, let activationDate = activationDate { delegateQueue.async { guard let delegate = self.delegate else { @@ -147,8 +159,18 @@ public final class G7Sensor: G7BluetoothManagerDelegate { if delegate.sensor(self, didDiscoverNewSensor: name, activatedAt: activationDate) { self.sensorID = name self.activationDate = activationDate + self.needsVersionInfo = true self.delegate?.sensor(self, didRead: message) self.bluetoothManager.stopScanning() + if self.needsVersionInfo, let name = peripheralManager.peripheral.name, name == self.sensorID { + peripheralManager.perform { (peripheral) in + do { + try peripheral.requestExtendedVersion() + } catch let error { + self.log.error("Error trying to request extended version on initial detection: %{public}@", String(describing: error)) + } + } + } } } } else if sensorID != nil { @@ -251,6 +273,14 @@ public final class G7Sensor: G7BluetoothManagerDelegate { self.delegate?.sensor(self, didError: G7SensorError.observationError("Unable to handle glucose control response")) } } + case .extendedVersionRx: + if let extendedVersionMessage = ExtendedVersionMessage(data: response) { + log.default("Received %{public}@", String(describing: extendedVersionMessage)) + delegateQueue.async { + self.delegate?.sensor(self, didReceive: extendedVersionMessage) + self.needsVersionInfo = false + } + } case .backfillFinished: flushBackfillBuffer() default: diff --git a/G7SensorKit/Messages/ExtendedVersionMessage.swift b/G7SensorKit/Messages/ExtendedVersionMessage.swift new file mode 100644 index 0000000..43603d8 --- /dev/null +++ b/G7SensorKit/Messages/ExtendedVersionMessage.swift @@ -0,0 +1,43 @@ +// +// ExtendedVersion.swift +// G7SensorKit +// +// Created by Pete Schwamb on 12/31/25. +// Copyright © 2025 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit + +public struct ExtendedVersionMessage: SensorMessage, Equatable { + public let sessionLength: UInt32 + public let warmupLength: UInt16 + public let algorithmVersion: UInt32 + public let hardwareVersion: UInt8 + public let maxLifetimeDays: UInt16 + public let data: Data + + init?(data: Data) { + self.data = data + + guard data.starts(with: .extendedVersionRx) else { + return nil + } + + guard data.count >= 14 else { + return nil + } + + sessionLength = data[1..<5].toInt() + warmupLength = data[5..<7].toInt() + algorithmVersion = data[7..<11].toInt() + hardwareVersion = data[11] + maxLifetimeDays = data[12..<16].toInt() + } +} + +extension ExtendedVersionMessage: CustomDebugStringConvertible { + public var debugDescription: String { + return "ExtendedVersionMessage(sessionLength:\(sessionLength), warmupLength:\(warmupLength) algorithmVersion:\(algorithmVersion) hardwareVersion:\(hardwareVersion) maxLifetimeDays:\(maxLifetimeDays))" + } +} diff --git a/G7SensorKit/Messages/G7Opcode.swift b/G7SensorKit/Messages/G7Opcode.swift index 5198462..4db9fe5 100644 --- a/G7SensorKit/Messages/G7Opcode.swift +++ b/G7SensorKit/Messages/G7Opcode.swift @@ -12,5 +12,7 @@ enum G7Opcode: UInt8 { case authChallengeRx = 0x05 case sessionStopTx = 0x28 case glucoseTx = 0x4e + case extendedVersionTx = 0x52 + case extendedVersionRx = 0x53 case backfillFinished = 0x59 } diff --git a/G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift b/G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift index fb91acb..fe8fa5d 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/lifetime), progressState: .warning) } return nil case .gracePeriod: diff --git a/G7SensorKitUI/Views/G7SettingsView.swift b/G7SensorKitUI/Views/G7SettingsView.swift index b5b50e4..ea71de9 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.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.lifetime + G7Sensor.gracePeriod))) .foregroundColor(.secondary) } } diff --git a/G7SensorKitUI/Views/G7SettingsViewModel.swift b/G7SensorKitUI/Views/G7SettingsViewModel.swift index 93bff10..67cf8a0 100644 --- a/G7SensorKitUI/Views/G7SettingsViewModel.swift +++ b/G7SensorKitUI/Views/G7SettingsViewModel.swift @@ -22,6 +22,7 @@ class G7SettingsViewModel: ObservableObject { @Published private(set) var sensorName: String? @Published private(set) var activatedAt: Date? @Published private(set) var lastConnect: Date? + @Published private(set) var lifetime: TimeInterval @Published private(set) var latestReadingTimestamp: Date? @Published var uploadReadings: Bool = false { didSet { @@ -62,6 +63,7 @@ class G7SettingsViewModel: ObservableObject { init(cgmManager: G7CGMManager, displayGlucosePreference: DisplayGlucosePreference) { self.cgmManager = cgmManager self.displayGlucosePreference = displayGlucosePreference + self.lifetime = cgmManager.lifetime updateValues() self.cgmManager.addStateObserver(self, queue: DispatchQueue.main) @@ -76,6 +78,7 @@ class G7SettingsViewModel: ObservableObject { lastReading = cgmManager.latestReading latestReadingTimestamp = cgmManager.latestReadingTimestamp uploadReadings = cgmManager.state.uploadReadings + lifetime = cgmManager.lifetime } var progressBarColorStyle: ColorStyle { @@ -113,7 +116,7 @@ class G7SettingsViewModel: ObservableObject { guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / G7Sensor.lifetime + return 1 - value / lifetime case .gracePeriodRemaining: guard let value = progressValue, value > 0 else { return 0 From 6c3cc0a832f83b08d5b211d9e97ce0bb21dbef2b Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 31 Dec 2025 14:11:20 -0600 Subject: [PATCH 2/4] Decoding fixes --- G7SensorKit.xcodeproj/project.pbxproj | 4 ++++ G7SensorKit/G7CGMManager/G7Sensor.swift | 2 +- .../Messages/ExtendedVersionMessage.swift | 21 +++++++++------- .../ExtendedVersionMessageTests.swift | 24 +++++++++++++++++++ 4 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 G7SensorKitTests/ExtendedVersionMessageTests.swift diff --git a/G7SensorKit.xcodeproj/project.pbxproj b/G7SensorKit.xcodeproj/project.pbxproj index 85e78fd..9868d2d 100644 --- a/G7SensorKit.xcodeproj/project.pbxproj +++ b/G7SensorKit.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ B66D1F6D2E6A803800471149 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B66D1F6C2E6A803800471149 /* Localizable.xcstrings */; }; B66D1F6F2E6A803800471149 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B66D1F6E2E6A803800471149 /* Localizable.xcstrings */; }; C107607F2F059130008B2B39 /* ExtendedVersionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C107607E2F05912B008B2B39 /* ExtendedVersionMessage.swift */; }; + C10760812F05B41B008B2B39 /* ExtendedVersionMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10760802F05B412008B2B39 /* ExtendedVersionMessageTests.swift */; }; C109F14A291ECCE2008EA5B6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C109F149291ECCE2008EA5B6 /* Assets.xcassets */; }; C109F14C291ED66F008EA5B6 /* G7GlucoseMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C109F14B291ED66F008EA5B6 /* G7GlucoseMessageTests.swift */; }; C139829829295D7D0047DB5F /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17F514A291EB6F000555EB5 /* HKUnit.swift */; }; @@ -114,6 +115,7 @@ B66D1F6C2E6A803800471149 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; B66D1F6E2E6A803800471149 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; C107607E2F05912B008B2B39 /* ExtendedVersionMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedVersionMessage.swift; sourceTree = ""; }; + C10760802F05B412008B2B39 /* ExtendedVersionMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedVersionMessageTests.swift; sourceTree = ""; }; C109F149291ECCE2008EA5B6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C109F14B291ED66F008EA5B6 /* G7GlucoseMessageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = G7GlucoseMessageTests.swift; sourceTree = ""; }; C1409A08291EC22F006BE8D0 /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; @@ -242,6 +244,7 @@ C17F50D2291EAC3800555EB5 /* G7SensorKitTests */ = { isa = PBXGroup; children = ( + C10760802F05B412008B2B39 /* ExtendedVersionMessageTests.swift */, C17F50D3291EAC3800555EB5 /* G7SensorKitTests.swift */, C109F14B291ED66F008EA5B6 /* G7GlucoseMessageTests.swift */, ); @@ -599,6 +602,7 @@ buildActionMask = 2147483647; files = ( C109F14C291ED66F008EA5B6 /* G7GlucoseMessageTests.swift in Sources */, + C10760812F05B41B008B2B39 /* ExtendedVersionMessageTests.swift in Sources */, C17F50D4291EAC3800555EB5 /* G7SensorKitTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/G7SensorKit/G7CGMManager/G7Sensor.swift b/G7SensorKit/G7CGMManager/G7Sensor.swift index 23dd7c1..a58d928 100644 --- a/G7SensorKit/G7CGMManager/G7Sensor.swift +++ b/G7SensorKit/G7CGMManager/G7Sensor.swift @@ -273,7 +273,7 @@ public final class G7Sensor: G7BluetoothManagerDelegate { self.delegate?.sensor(self, didError: G7SensorError.observationError("Unable to handle glucose control response")) } } - case .extendedVersionRx: + case .extendedVersionTx: if let extendedVersionMessage = ExtendedVersionMessage(data: response) { log.default("Received %{public}@", String(describing: extendedVersionMessage)) delegateQueue.async { diff --git a/G7SensorKit/Messages/ExtendedVersionMessage.swift b/G7SensorKit/Messages/ExtendedVersionMessage.swift index 43603d8..ef2e4f8 100644 --- a/G7SensorKit/Messages/ExtendedVersionMessage.swift +++ b/G7SensorKit/Messages/ExtendedVersionMessage.swift @@ -10,29 +10,32 @@ import Foundation import LoopKit public struct ExtendedVersionMessage: SensorMessage, Equatable { - public let sessionLength: UInt32 - public let warmupLength: UInt16 + public let sessionLength: TimeInterval + public let warmupLength: TimeInterval public let algorithmVersion: UInt32 public let hardwareVersion: UInt8 public let maxLifetimeDays: UInt16 + public let data: Data init?(data: Data) { self.data = data - guard data.starts(with: .extendedVersionRx) else { + // 52 00 c0d70d00 5406 00020404 ff 0c00 + + guard data.starts(with: .extendedVersionTx) else { return nil } - guard data.count >= 14 else { + guard data.count >= 15 else { return nil } - sessionLength = data[1..<5].toInt() - warmupLength = data[5..<7].toInt() - algorithmVersion = data[7..<11].toInt() - hardwareVersion = data[11] - maxLifetimeDays = data[12..<16].toInt() + sessionLength = TimeInterval(data[2..<6].to(UInt32.self)) + warmupLength = TimeInterval(data[6..<8].to(UInt16.self)) + algorithmVersion = data[8..<12].to(UInt32.self) + hardwareVersion = data[12] + maxLifetimeDays = data[13..<15].to(UInt16.self) } } diff --git a/G7SensorKitTests/ExtendedVersionMessageTests.swift b/G7SensorKitTests/ExtendedVersionMessageTests.swift new file mode 100644 index 0000000..0621adc --- /dev/null +++ b/G7SensorKitTests/ExtendedVersionMessageTests.swift @@ -0,0 +1,24 @@ +// +// ExtendedVersionMessageTests.swift +// G7SensorKit +// +// Created by Pete Schwamb on 12/31/25. +// Copyright © 2025 LoopKit Authors. All rights reserved. +// + +import XCTest +@testable import G7SensorKit + +final class ExtendedVersionMessageTests: XCTestCase { + + func testBasicMessage() { + let data = Data(hexadecimalString: "5200c0d70d00540600020404ff0c00")! + let message = ExtendedVersionMessage(data: data)! + + XCTAssertEqual(10.5, message.sessionLength.hours / 24) + XCTAssertEqual(27, message.warmupLength.minutes) + XCTAssertEqual(67371520, message.algorithmVersion) + XCTAssertEqual(255, message.hardwareVersion) + XCTAssertEqual(12, message.maxLifetimeDays) + } +} From 9c7b3b7f24f163b0f775e5c9fb29f3ed17be7133 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 1 Jan 2026 08:53:54 -0600 Subject: [PATCH 3/4] Dynamic warmup period --- G7SensorKit/G7CGMManager/G7CGMManager.swift | 14 +++++++++----- G7SensorKit/G7CGMManager/G7Sensor.swift | 2 +- G7SensorKit/Messages/ExtendedVersionMessage.swift | 6 +++--- G7SensorKitTests/ExtendedVersionMessageTests.swift | 2 +- G7SensorKitUI/Views/G7SettingsViewModel.swift | 5 ++++- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/G7SensorKit/G7CGMManager/G7CGMManager.swift b/G7SensorKit/G7CGMManager/G7CGMManager.swift index 6a74d61..3fdc27b 100644 --- a/G7SensorKit/G7CGMManager/G7CGMManager.swift +++ b/G7SensorKit/G7CGMManager/G7CGMManager.swift @@ -113,13 +113,17 @@ public class G7CGMManager: CGMManager { } public var lifetime: TimeInterval { - if let maxLifetimeDays = state.extendedVersion?.maxLifetimeDays { - return TimeInterval(hours: Double(maxLifetimeDays) * 24) + if let sessionLength = state.extendedVersion?.sessionLength { + return sessionLength - G7Sensor.gracePeriod } else { return G7Sensor.defaultLifetime } } + public var warmupDuration: TimeInterval { + state.extendedVersion?.warmupDuration ?? G7Sensor.defaultWarmupDuration + } + public var sensorExpiresAt: Date? { guard let activatedAt = sensorActivatedAt else { return nil @@ -139,7 +143,7 @@ public class G7CGMManager: CGMManager { guard let activatedAt = sensorActivatedAt else { return nil } - return activatedAt.addingTimeInterval(G7Sensor.warmupDuration) + return activatedAt.addingTimeInterval(warmupDuration) } public var latestReading: G7GlucoseMessage? { @@ -308,8 +312,8 @@ extension G7CGMManager: G7SensorDelegate { date: activatedAt, type: .sensorStart, deviceIdentifier: name, - expectedLifetime: .hours(24 * 10 + 12), - warmupPeriod: .hours(2) + expectedLifetime: lifetime + G7Sensor.gracePeriod, + warmupPeriod: warmupDuration ) delegate.notify { delegate in delegate?.cgmManager(self, hasNew: [event]) diff --git a/G7SensorKit/G7CGMManager/G7Sensor.swift b/G7SensorKit/G7CGMManager/G7Sensor.swift index a58d928..c7bcbe9 100644 --- a/G7SensorKit/G7CGMManager/G7Sensor.swift +++ b/G7SensorKit/G7CGMManager/G7Sensor.swift @@ -65,7 +65,7 @@ public enum G7SensorLifecycleState { public final class G7Sensor: G7BluetoothManagerDelegate { public static let defaultLifetime = TimeInterval(hours: 10 * 24) - public static let warmupDuration = TimeInterval(minutes: 25) + public static let defaultWarmupDuration = TimeInterval(minutes: 27) public static let gracePeriod = TimeInterval(hours: 12) public weak var delegate: G7SensorDelegate? diff --git a/G7SensorKit/Messages/ExtendedVersionMessage.swift b/G7SensorKit/Messages/ExtendedVersionMessage.swift index ef2e4f8..082400c 100644 --- a/G7SensorKit/Messages/ExtendedVersionMessage.swift +++ b/G7SensorKit/Messages/ExtendedVersionMessage.swift @@ -11,7 +11,7 @@ import LoopKit public struct ExtendedVersionMessage: SensorMessage, Equatable { public let sessionLength: TimeInterval - public let warmupLength: TimeInterval + public let warmupDuration: TimeInterval public let algorithmVersion: UInt32 public let hardwareVersion: UInt8 public let maxLifetimeDays: UInt16 @@ -32,7 +32,7 @@ public struct ExtendedVersionMessage: SensorMessage, Equatable { } sessionLength = TimeInterval(data[2..<6].to(UInt32.self)) - warmupLength = TimeInterval(data[6..<8].to(UInt16.self)) + warmupDuration = TimeInterval(data[6..<8].to(UInt16.self)) algorithmVersion = data[8..<12].to(UInt32.self) hardwareVersion = data[12] maxLifetimeDays = data[13..<15].to(UInt16.self) @@ -41,6 +41,6 @@ public struct ExtendedVersionMessage: SensorMessage, Equatable { extension ExtendedVersionMessage: CustomDebugStringConvertible { public var debugDescription: String { - return "ExtendedVersionMessage(sessionLength:\(sessionLength), warmupLength:\(warmupLength) algorithmVersion:\(algorithmVersion) hardwareVersion:\(hardwareVersion) maxLifetimeDays:\(maxLifetimeDays))" + return "ExtendedVersionMessage(sessionLength:\(sessionLength), warmupDuration:\(warmupDuration) algorithmVersion:\(algorithmVersion) hardwareVersion:\(hardwareVersion) maxLifetimeDays:\(maxLifetimeDays))" } } diff --git a/G7SensorKitTests/ExtendedVersionMessageTests.swift b/G7SensorKitTests/ExtendedVersionMessageTests.swift index 0621adc..c0894da 100644 --- a/G7SensorKitTests/ExtendedVersionMessageTests.swift +++ b/G7SensorKitTests/ExtendedVersionMessageTests.swift @@ -16,7 +16,7 @@ final class ExtendedVersionMessageTests: XCTestCase { let message = ExtendedVersionMessage(data: data)! XCTAssertEqual(10.5, message.sessionLength.hours / 24) - XCTAssertEqual(27, message.warmupLength.minutes) + XCTAssertEqual(27, message.warmupDuration.minutes) XCTAssertEqual(67371520, message.algorithmVersion) XCTAssertEqual(255, message.hardwareVersion) XCTAssertEqual(12, message.maxLifetimeDays) diff --git a/G7SensorKitUI/Views/G7SettingsViewModel.swift b/G7SensorKitUI/Views/G7SettingsViewModel.swift index 67cf8a0..14f19b7 100644 --- a/G7SensorKitUI/Views/G7SettingsViewModel.swift +++ b/G7SensorKitUI/Views/G7SettingsViewModel.swift @@ -23,6 +23,7 @@ class G7SettingsViewModel: ObservableObject { @Published private(set) var activatedAt: Date? @Published private(set) var lastConnect: Date? @Published private(set) var lifetime: TimeInterval + @Published private(set) var warmupDuration: TimeInterval @Published private(set) var latestReadingTimestamp: Date? @Published var uploadReadings: Bool = false { didSet { @@ -64,6 +65,7 @@ class G7SettingsViewModel: ObservableObject { self.cgmManager = cgmManager self.displayGlucosePreference = displayGlucosePreference self.lifetime = cgmManager.lifetime + self.warmupDuration = cgmManager.warmupDuration updateValues() self.cgmManager.addStateObserver(self, queue: DispatchQueue.main) @@ -79,6 +81,7 @@ class G7SettingsViewModel: ObservableObject { latestReadingTimestamp = cgmManager.latestReadingTimestamp uploadReadings = cgmManager.state.uploadReadings lifetime = cgmManager.lifetime + warmupDuration = cgmManager.warmupDuration } var progressBarColorStyle: ColorStyle { @@ -111,7 +114,7 @@ class G7SettingsViewModel: ObservableObject { guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / G7Sensor.warmupDuration + return 1 - value / warmupDuration case .lifetimeRemaining: guard let value = progressValue, value > 0 else { return 0 From 7c267f579a860424c94647ec9fb2ebd81194605d Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 1 Jan 2026 10:35:03 -0600 Subject: [PATCH 4/4] Interpreting last field in extended version message as grace period in hours --- G7SensorKit/G7CGMManager/G7CGMManager.swift | 12 ++++++++---- G7SensorKit/G7CGMManager/G7Sensor.swift | 2 +- G7SensorKit/Messages/ExtendedVersionMessage.swift | 10 +++++----- G7SensorKitTests/ExtendedVersionMessageTests.swift | 4 ++-- G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift | 2 +- G7SensorKitUI/Views/G7SettingsView.swift | 2 +- G7SensorKitUI/Views/G7SettingsViewModel.swift | 4 +++- 7 files changed, 21 insertions(+), 15 deletions(-) diff --git a/G7SensorKit/G7CGMManager/G7CGMManager.swift b/G7SensorKit/G7CGMManager/G7CGMManager.swift index 3fdc27b..096f018 100644 --- a/G7SensorKit/G7CGMManager/G7CGMManager.swift +++ b/G7SensorKit/G7CGMManager/G7CGMManager.swift @@ -113,8 +113,8 @@ public class G7CGMManager: CGMManager { } public var lifetime: TimeInterval { - if let sessionLength = state.extendedVersion?.sessionLength { - return sessionLength - G7Sensor.gracePeriod + if let sessionLength = state.extendedVersion?.sessionDuration { + return sessionLength - gracePeriod } else { return G7Sensor.defaultLifetime } @@ -124,6 +124,10 @@ public class G7CGMManager: CGMManager { state.extendedVersion?.warmupDuration ?? G7Sensor.defaultWarmupDuration } + public var gracePeriod: TimeInterval { + state.extendedVersion?.gracePeriodDuration ?? G7Sensor.defaultGracePeriod + } + public var sensorExpiresAt: Date? { guard let activatedAt = sensorActivatedAt else { return nil @@ -135,7 +139,7 @@ public class G7CGMManager: CGMManager { guard let activatedAt = sensorActivatedAt else { return nil } - return activatedAt.addingTimeInterval(lifetime + G7Sensor.gracePeriod) + return activatedAt.addingTimeInterval(lifetime + gracePeriod) } @@ -312,7 +316,7 @@ extension G7CGMManager: G7SensorDelegate { date: activatedAt, type: .sensorStart, deviceIdentifier: name, - expectedLifetime: lifetime + G7Sensor.gracePeriod, + expectedLifetime: lifetime + gracePeriod, warmupPeriod: warmupDuration ) delegate.notify { delegate in diff --git a/G7SensorKit/G7CGMManager/G7Sensor.swift b/G7SensorKit/G7CGMManager/G7Sensor.swift index c7bcbe9..c3bf75a 100644 --- a/G7SensorKit/G7CGMManager/G7Sensor.swift +++ b/G7SensorKit/G7CGMManager/G7Sensor.swift @@ -66,7 +66,7 @@ public enum G7SensorLifecycleState { public final class G7Sensor: G7BluetoothManagerDelegate { public static let defaultLifetime = TimeInterval(hours: 10 * 24) public static let defaultWarmupDuration = TimeInterval(minutes: 27) - public static let gracePeriod = TimeInterval(hours: 12) + public static let defaultGracePeriod = TimeInterval(hours: 12) public weak var delegate: G7SensorDelegate? diff --git a/G7SensorKit/Messages/ExtendedVersionMessage.swift b/G7SensorKit/Messages/ExtendedVersionMessage.swift index 082400c..2ed9c4d 100644 --- a/G7SensorKit/Messages/ExtendedVersionMessage.swift +++ b/G7SensorKit/Messages/ExtendedVersionMessage.swift @@ -10,11 +10,11 @@ import Foundation import LoopKit public struct ExtendedVersionMessage: SensorMessage, Equatable { - public let sessionLength: TimeInterval + public let sessionDuration: TimeInterval public let warmupDuration: TimeInterval public let algorithmVersion: UInt32 public let hardwareVersion: UInt8 - public let maxLifetimeDays: UInt16 + public let gracePeriodDuration: TimeInterval public let data: Data @@ -31,16 +31,16 @@ public struct ExtendedVersionMessage: SensorMessage, Equatable { return nil } - sessionLength = TimeInterval(data[2..<6].to(UInt32.self)) + sessionDuration = TimeInterval(data[2..<6].to(UInt32.self)) warmupDuration = TimeInterval(data[6..<8].to(UInt16.self)) algorithmVersion = data[8..<12].to(UInt32.self) hardwareVersion = data[12] - maxLifetimeDays = data[13..<15].to(UInt16.self) + gracePeriodDuration = TimeInterval(hours: Double(data[13..<15].to(UInt16.self))) } } extension ExtendedVersionMessage: CustomDebugStringConvertible { public var debugDescription: String { - return "ExtendedVersionMessage(sessionLength:\(sessionLength), warmupDuration:\(warmupDuration) algorithmVersion:\(algorithmVersion) hardwareVersion:\(hardwareVersion) maxLifetimeDays:\(maxLifetimeDays))" + return "ExtendedVersionMessage(sessionDuration:\(sessionDuration), warmupDuration:\(warmupDuration) algorithmVersion:\(algorithmVersion) hardwareVersion:\(hardwareVersion) gracePeriodDuration:\(gracePeriodDuration))" } } diff --git a/G7SensorKitTests/ExtendedVersionMessageTests.swift b/G7SensorKitTests/ExtendedVersionMessageTests.swift index c0894da..97bf0d3 100644 --- a/G7SensorKitTests/ExtendedVersionMessageTests.swift +++ b/G7SensorKitTests/ExtendedVersionMessageTests.swift @@ -15,10 +15,10 @@ final class ExtendedVersionMessageTests: XCTestCase { let data = Data(hexadecimalString: "5200c0d70d00540600020404ff0c00")! let message = ExtendedVersionMessage(data: data)! - XCTAssertEqual(10.5, message.sessionLength.hours / 24) + XCTAssertEqual(10.5, message.sessionDuration.hours / 24) XCTAssertEqual(27, message.warmupDuration.minutes) XCTAssertEqual(67371520, message.algorithmVersion) XCTAssertEqual(255, message.hardwareVersion) - XCTAssertEqual(12, message.maxLifetimeDays) + XCTAssertEqual(12, message.gracePeriodDuration.hours) } } diff --git a/G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift b/G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift index fe8fa5d..877d68e 100644 --- a/G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift +++ b/G7SensorKitUI/G7CGMManager/G7CGMManager+UI.swift @@ -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/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 ea71de9..66ff2f4 100644 --- a/G7SensorKitUI/Views/G7SettingsView.swift +++ b/G7SensorKitUI/Views/G7SettingsView.swift @@ -71,7 +71,7 @@ struct G7SettingsView: View { 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.lifetime + G7Sensor.gracePeriod))) + Text(timeFormatter.string(from: activatedAt.addingTimeInterval(viewModel.lifetime + viewModel.gracePeriod))) .foregroundColor(.secondary) } } diff --git a/G7SensorKitUI/Views/G7SettingsViewModel.swift b/G7SensorKitUI/Views/G7SettingsViewModel.swift index 14f19b7..424a0ae 100644 --- a/G7SensorKitUI/Views/G7SettingsViewModel.swift +++ b/G7SensorKitUI/Views/G7SettingsViewModel.swift @@ -24,6 +24,7 @@ class G7SettingsViewModel: ObservableObject { @Published private(set) var lastConnect: Date? @Published private(set) var lifetime: TimeInterval @Published private(set) var warmupDuration: TimeInterval + @Published private(set) var gracePeriod: TimeInterval @Published private(set) var latestReadingTimestamp: Date? @Published var uploadReadings: Bool = false { didSet { @@ -66,6 +67,7 @@ class G7SettingsViewModel: ObservableObject { self.displayGlucosePreference = displayGlucosePreference self.lifetime = cgmManager.lifetime self.warmupDuration = cgmManager.warmupDuration + self.gracePeriod = cgmManager.gracePeriod updateValues() self.cgmManager.addStateObserver(self, queue: DispatchQueue.main) @@ -124,7 +126,7 @@ class G7SettingsViewModel: ObservableObject { guard let value = progressValue, value > 0 else { return 0 } - return 1 - value / G7Sensor.gracePeriod + return 1 - value / gracePeriod case .sensorExpired, .sensorFailed: return 1 }