Skip to content

Update G7SensorKit tidepool-merge with improvements from main #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions G7SensorKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
3B0FD2A52D803BF100E5E921 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B0FD2A42D803BF000E5E921 /* LoopKitUI.framework */; };
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 */; };
C139829829295D7D0047DB5F /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17F514A291EB6F000555EB5 /* HKUnit.swift */; };
Expand Down Expand Up @@ -106,6 +108,8 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
3B0FD2A42D803BF000E5E921 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
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 = "<group>"; };
C1086B0F29C9169100D46E65 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
C109F149291ECCE2008EA5B6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -202,6 +206,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3B0FD2A52D803BF100E5E921 /* LoopKitUI.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -336,6 +341,7 @@
C17F5128291EAFA100555EB5 /* Frameworks */ = {
isa = PBXGroup;
children = (
3B0FD2A42D803BF000E5E921 /* LoopKitUI.framework */,
);
name = Frameworks;
sourceTree = "<group>";
Expand Down Expand Up @@ -366,6 +372,7 @@
C17F5154291EBD7100555EB5 /* Extensions */ = {
isa = PBXGroup;
children = (
B60BB2E32BC649DA00D2BB39 /* Bundle.swift */,
C17F5155291EBD8600555EB5 /* Image.swift */,
);
path = Extensions;
Expand Down Expand Up @@ -636,6 +643,7 @@
buildActionMask = 2147483647;
files = (
C17F5108291EAC9D00555EB5 /* G7SettingsView.swift in Sources */,
B60BB2E42BC649DA00D2BB39 /* Bundle.swift in Sources */,
C17F5157291EBD9900555EB5 /* TimeInterval.swift in Sources */,
C19C9F4E29C91C4C00A6D3D0 /* LocalizedString.swift in Sources */,
C17F5156291EBD8600555EB5 /* Image.swift in Sources */,
Expand Down
4 changes: 3 additions & 1 deletion G7SensorKit/AlgorithmError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ extension AlgorithmState {
return LocalizedString("Sensor is OK", comment: "The description of sensor algorithm state when sensor is ok.")
case .stopped:
return LocalizedString("Sensor is stopped", comment: "The description of sensor algorithm state when sensor is stopped.")
case .warmup, .questionMarks:
case .warmup, .temporarySensorIssue:
return LocalizedString("Sensor is warming up", comment: "The description of sensor algorithm state when sensor is warming up.")
case .expired:
return LocalizedString("Sensor expired", comment: "The description of sensor algorithm state when sensor is expired.")
case .sensorFailed:
return LocalizedString("Sensor failed", comment: "The description of sensor algorithm state when sensor failed.")
default:
return "Sensor state: \(String(describing: state))"
}
case .unknown(let rawValue):
return String(format: LocalizedString("Sensor is in unknown state %1$d", comment: "The description of sensor algorithm state when raw value is unknown. (1: missing data details)"), rawValue)
Expand Down
35 changes: 25 additions & 10 deletions G7SensorKit/AlgorithmState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,29 @@ public enum AlgorithmState: RawRepresentable {
public enum State: RawValue {
case stopped = 1
case warmup = 2
case excessNoise = 3
case firstOfTwoBGsNeeded = 4
case secondOfTwoBGsNeeded = 5
case ok = 6
case questionMarks = 18
case needsCalibration = 7
case calibrationError1 = 8
case calibrationError2 = 9
case calibrationLinearityFitFailure = 10
case sensorFailedDuetoCountsAberration = 11
case sensorFailedDuetoResidualAberration = 12
case outOfCalibrationDueToOutlier = 13
case outlierCalibrationRequest = 14
case sessionExpired = 15
case sessionFailedDueToUnrecoverableError = 16
case sessionFailedDueToTransmitterError = 17
case temporarySensorIssue = 18
case sensorFailedDueToProgressiveSensorDecline = 19
case sensorFailedDueToHighCountsAberration = 20
case sensorFailedDueToLowCountsAberration = 21
case sensorFailedDueToRestart = 22
case expired = 24
case sensorFailed = 25
case sessionEnded = 26
}

case known(State)
Expand Down Expand Up @@ -48,7 +67,7 @@ public enum AlgorithmState: RawRepresentable {
}

switch state {
case .sensorFailed:
case .sensorFailed, .sensorFailedDuetoCountsAberration, .sensorFailedDuetoResidualAberration, .sessionFailedDueToTransmitterError, .sessionFailedDueToUnrecoverableError, .sensorFailedDueToProgressiveSensorDecline, .sensorFailedDueToHighCountsAberration, .sensorFailedDueToLowCountsAberration, .sensorFailedDueToRestart:
return true
default:
return false
Expand All @@ -68,13 +87,13 @@ public enum AlgorithmState: RawRepresentable {
}
}

public var isInSensorError: Bool {
public var hasTemporaryError: Bool {
guard case .known(let state) = self else {
return false
}

switch state {
case .questionMarks:
case .temporarySensorIssue:
return true
default:
return false
Expand All @@ -88,14 +107,10 @@ public enum AlgorithmState: RawRepresentable {
}

switch state {
case .stopped,
.warmup,
.questionMarks,
.expired,
.sensorFailed:
return false
case .ok:
return true
default:
return false
}
}
}
Expand Down
11 changes: 0 additions & 11 deletions G7SensorKit/BluetoothServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,13 @@ extension CBUUIDRawValue where RawValue == String {
}
}


enum SensorServiceUUID: String, CBUUIDRawValue {
case deviceInfo = "180A"
case advertisement = "FEBC"
case cgmService = "F8083532-849E-531C-C594-30F1F86A4EA5"

case serviceB = "F8084532-849E-531C-C594-30F1F86A4EA5"
}


enum DeviceInfoCharacteristicUUID: String, CBUUIDRawValue {
// Read
// "DexcomUN"
case manufacturerNameString = "2A29"
}


enum CGMServiceCharacteristicUUID: String, CBUUIDRawValue {

// Read/Notify
Expand Down Expand Up @@ -61,7 +51,6 @@ extension G7PeripheralManager.Configuration {
return G7PeripheralManager.Configuration(
serviceCharacteristics: [
SensorServiceUUID.cgmService.cbUUID: [
//CGMServiceCharacteristicUUID.communication.cbUUID, // Unused for now
CGMServiceCharacteristicUUID.authentication.cbUUID,
CGMServiceCharacteristicUUID.control.cbUUID,
CGMServiceCharacteristicUUID.backfill.cbUUID,
Expand Down
7 changes: 4 additions & 3 deletions G7SensorKit/G7CGMManager/G7BackfillMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,19 @@ public struct G7BackfillMessage: Equatable {
return nil
}

timestamp = data[0..<4].toInt()

timestamp = data[0..<3].toInt()

let glucoseBytes = data[4..<6].to(UInt16.self)

if glucoseBytes != 0xffff {
glucose = glucoseBytes & 0xfff
glucoseIsDisplayOnly = (glucoseBytes & 0xf000) > 0
} else {
glucose = nil
glucoseIsDisplayOnly = false
}

glucoseIsDisplayOnly = data[7] & 0x10 != 0

algorithmState = AlgorithmState(rawValue: data[6])

if data[8] == 0x7f {
Expand Down
32 changes: 24 additions & 8 deletions G7SensorKit/G7CGMManager/G7BluetoothManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ class G7BluetoothManager: NSObject {

private func managerQueue_stopScanning() {
if centralManager.isScanning {
log.debug("Stopping scan")
log.default("Stopping scan")
centralManager.stopScan()
delegate?.bluetoothManagerScanningStatusDidChange(self)
}
Expand All @@ -167,7 +167,7 @@ class G7BluetoothManager: NSObject {

managerQueue.sync {
if centralManager.isScanning {
log.debug("Stopping scan on disconnect")
log.default("Stopping scan on disconnect")
centralManager.stopScan()
delegate?.bluetoothManagerScanningStatusDidChange(self)
}
Expand All @@ -178,6 +178,15 @@ class G7BluetoothManager: NSObject {
}
}

func centralManager(_ central: CBCentralManager, connectionEventDidOccur event: CBConnectionEvent, for peripheral: CBPeripheral) {
managerQueue.async {
if self.activePeripheralIdentifier == nil {
self.log.default("Discovered peripheral from connectionEventDidOccur %{public}@", peripheral.identifier.uuidString)
self.handleDiscoveredPeripheral(peripheral)
}
}
}

private func managerQueue_scanForPeripheral() {
dispatchPrecondition(condition: .onQueue(managerQueue))

Expand All @@ -191,19 +200,26 @@ class G7BluetoothManager: NSObject {
}

if let peripheralID = activePeripheralIdentifier, let peripheral = centralManager.retrievePeripherals(withIdentifiers: [peripheralID]).first {
log.debug("Retrieved peripheral %{public}@", peripheral.identifier.uuidString)
log.default("Retrieved peripheral %{public}@", peripheral.identifier.uuidString)
handleDiscoveredPeripheral(peripheral)
} else {
for peripheral in centralManager.retrieveConnectedPeripherals(withServices: [
SensorServiceUUID.advertisement.cbUUID,
SensorServiceUUID.cgmService.cbUUID
]) {
log.default("Found system-connected peripheral: %{public}@", peripheral.identifier.uuidString)
handleDiscoveredPeripheral(peripheral)
}
}

if activePeripheral == nil {
log.debug("Scanning for peripherals")
log.default("Scanning for peripherals and listening for connection events")

centralManager.registerForConnectionEvents(options: [CBConnectionEventMatchingOption.serviceUUIDs: [
SensorServiceUUID.advertisement.cbUUID,
SensorServiceUUID.cgmService.cbUUID
]])

centralManager.scanForPeripherals(withServices: [
SensorServiceUUID.advertisement.cbUUID
],
Expand Down Expand Up @@ -257,7 +273,7 @@ class G7BluetoothManager: NSObject {
if let delegate = delegate {
switch delegate.bluetoothManager(self, shouldConnectPeripheral: peripheral) {
case .makeActive:
log.debug("Making peripheral active: %{public}@", peripheral.identifier.uuidString)
log.default("Making peripheral active: %{public}@", peripheral.identifier.uuidString)

if let peripheralManager = activePeripheralManager {
peripheralManager.peripheral = peripheral
Expand All @@ -273,7 +289,7 @@ class G7BluetoothManager: NSObject {
self.centralManager.connect(peripheral)

case .connect:
log.debug("Connecting to peripheral: %{public}@", peripheral.identifier.uuidString)
log.default("Connecting to peripheral: %{public}@", peripheral.identifier.uuidString)
self.centralManager.connect(peripheral)
let peripheralManager = G7PeripheralManager(
peripheral: peripheral,
Expand Down Expand Up @@ -311,7 +327,7 @@ extension G7BluetoothManager: CBCentralManagerDelegate {
fallthrough
@unknown default:
if central.isScanning {
log.debug("Stopping scan on central not powered on")
log.default("Stopping scan on central not powered on")
central.stopScan()
delegate?.bluetoothManagerScanningStatusDidChange(self)
}
Expand All @@ -332,7 +348,7 @@ extension G7BluetoothManager: CBCentralManagerDelegate {
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
dispatchPrecondition(condition: .onQueue(managerQueue))

log.info("%{public}@: %{public}@, data = %{public}@", #function, peripheral, String(describing: advertisementData))
log.default("%{public}@: %{public}@, data = %{public}@", #function, peripheral, String(describing: advertisementData))

managerQueue.async {
self.handleDiscoveredPeripheral(peripheral)
Expand Down
16 changes: 16 additions & 0 deletions G7SensorKit/G7CGMManager/G7CGMManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ extension G7CGMManager: G7SensorDelegate {
}
}

public func sensor(_ sensor: G7Sensor, logComms comms: String) {
logDeviceCommunication("Sensor comms \(comms)", type: .receive)
}


public func sensor(_ sensor: G7Sensor, didError error: Error) {
logDeviceCommunication("Sensor error \(error)", type: .error)
}
Expand All @@ -335,6 +340,17 @@ extension G7CGMManager: G7SensorDelegate {
return
}

if message.algorithmState.sensorFailed {
logDeviceCommunication("Detected failed sensor... scanning for new sensor.", type: .receive)
scanForNewSensor()
}

if message.algorithmState == .known(.sessionEnded) {
logDeviceCommunication("Detected session ended... scanning for new sensor.", type: .receive)
scanForNewSensor()
}


guard let activationDate = sensor.activationDate else {
logDeviceCommunication("Unable to process sensor reading without activation date.", type: .error)
return
Expand Down
33 changes: 23 additions & 10 deletions G7SensorKit/G7CGMManager/G7Sensor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public protocol G7SensorDelegate: AnyObject {

func sensor(_ sensor: G7Sensor, didError error: Error)

func sensor(_ sensor: G7Sensor, logComms comms: String)

func sensor(_ sensor: G7Sensor, didRead glucose: G7GlucoseMessage)

func sensor(_ sensor: G7Sensor, didReadBackfill backfill: [G7BackfillMessage])
Expand Down Expand Up @@ -193,8 +195,13 @@ public final class G7Sensor: G7BluetoothManagerDelegate {
func peripheralDidDisconnect(_ manager: G7BluetoothManager, peripheralManager: G7PeripheralManager, wasRemoteDisconnect: Bool) {
if let sensorID = sensorID, sensorID == peripheralManager.peripheral.name {

// Sometimes we do not receive the backfillFinished message before disconnect
flushBackfillBuffer()

let suspectedEndOfSession: Bool
if pendingAuth && wasRemoteDisconnect {

self.log.info("Sensor disconnected: wasRemoteDisconnect:%{public}@", String(describing: wasRemoteDisconnect))
if pendingAuth, wasRemoteDisconnect {
suspectedEndOfSession = true // Normal disconnect without auth is likely that G7 app stopped this session
} else {
suspectedEndOfSession = false
Expand All @@ -215,7 +222,8 @@ public final class G7Sensor: G7BluetoothManagerDelegate {
}

/// The Dexcom G7 advertises a peripheral name of "DXCMxx", and later reports a full name of "Dexcomxx"
if name.hasPrefix("DXCM") {
/// Dexcom One+ peripheral name start with "DX02"
if name.hasPrefix("DXCM") || 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
Expand All @@ -232,7 +240,7 @@ public final class G7Sensor: G7BluetoothManagerDelegate {

guard response.count > 0 else { return }

log.debug("Received control response: %{public}@", response.hexadecimalString)
log.default("Received control response: %{public}@", response.hexadecimalString)

switch G7Opcode(rawValue: response[0]) {
case .glucoseTx?:
Expand All @@ -244,18 +252,23 @@ public final class G7Sensor: G7BluetoothManagerDelegate {
}
}
case .backfillFinished:
if backfillBuffer.count > 0 {
delegateQueue.async {
self.delegate?.sensor(self, didReadBackfill: self.backfillBuffer)
self.backfillBuffer = []
}
}
flushBackfillBuffer()
default:
// We ignore all other known opcodes
self.delegate?.sensor(self, logComms: response.hexadecimalString)
break
}
}

func flushBackfillBuffer() {
if backfillBuffer.count > 0 {
let backfill = backfillBuffer
self.backfillBuffer = []
delegateQueue.async {
self.delegate?.sensor(self, didReadBackfill: backfill)
}
}
}

func bluetoothManager(_ manager: G7BluetoothManager, didReceiveBackfillResponse response: Data) {

log.debug("Received backfill response: %{public}@", response.hexadecimalString)
Expand Down
Loading