Skip to content
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
4 changes: 4 additions & 0 deletions CGMBLEKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
438621CE2292074A00741DFE /* ShareClientUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4325E9EE210EAF3F00969CE5 /* ShareClientUI.framework */; };
43880F981D9E19FC009061A8 /* TransmitterVersionRxMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43880F971D9E19FC009061A8 /* TransmitterVersionRxMessage.swift */; };
43880F9A1D9E1BD7009061A8 /* TransmitterVersionRxMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43880F991D9E1BD7009061A8 /* TransmitterVersionRxMessageTests.swift */; };
CA04000000000000000010C2 /* AnubisDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA04000000000000000010C1 /* AnubisDetectionTests.swift */; };
43A8EC4A210D09BE00A81379 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43A8EC49210D09BE00A81379 /* LoopKitUI.framework */; };
43A8EC4C210D09DA00A81379 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43A8EC49210D09BE00A81379 /* LoopKitUI.framework */; };
43A8EC56210D0A7400A81379 /* CGMBLEKitUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 43A8EC54210D0A7400A81379 /* CGMBLEKitUI.h */; settings = {ATTRIBUTES = (Public, ); }; };
Expand Down Expand Up @@ -391,6 +392,7 @@
43846AC71D8F89BE00799272 /* CalibrationDataRxMessageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationDataRxMessageTests.swift; sourceTree = "<group>"; };
43880F971D9E19FC009061A8 /* TransmitterVersionRxMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransmitterVersionRxMessage.swift; sourceTree = "<group>"; };
43880F991D9E1BD7009061A8 /* TransmitterVersionRxMessageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransmitterVersionRxMessageTests.swift; sourceTree = "<group>"; };
CA04000000000000000010C1 /* AnubisDetectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnubisDetectionTests.swift; sourceTree = "<group>"; };
43A8EC49210D09BE00A81379 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
43A8EC52210D0A7400A81379 /* CGMBLEKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CGMBLEKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
43A8EC54210D0A7400A81379 /* CGMBLEKitUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CGMBLEKitUI.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -730,6 +732,7 @@
43460F87200B30D10030C0E3 /* TransmitterIDTests.swift */,
43F82BCB1D035AA4006F5DD7 /* TransmitterTimeRxMessageTests.swift */,
43880F991D9E1BD7009061A8 /* TransmitterVersionRxMessageTests.swift */,
CA04000000000000000010C1 /* AnubisDetectionTests.swift */,
);
path = CGMBLEKitTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -1281,6 +1284,7 @@
43F82BD21D037040006F5DD7 /* SessionStopRxMessageTests.swift in Sources */,
43E397911D5692080028E321 /* GlucoseTests.swift in Sources */,
43880F9A1D9E1BD7009061A8 /* TransmitterVersionRxMessageTests.swift in Sources */,
CA04000000000000000010C2 /* AnubisDetectionTests.swift in Sources */,
43DC87C21C8B520F005BC30D /* GlucoseRxMessageTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
4 changes: 2 additions & 2 deletions CGMBLEKit/Glucose.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public struct Glucose {
if timeMessage.hasValidSensorSession {
let sessionStartDate = activationDate.addingTimeInterval(TimeInterval(timeMessage.sessionStartTime))
self.sessionStartDate = sessionStartDate
self.sessionExpDate = sessionStartDate.addingTimeInterval(10*24*60*60)
self.sessionExpDate = sessionStartDate.addingTimeInterval(.hours(24 * Double(TransmitterManagerState.defaultSensorLifeDays)))
} else {
sessionStartDate = nil
sessionExpDate = nil
Expand All @@ -66,7 +66,7 @@ public struct Glucose {
public let status: TransmitterStatus
public let activationDate: Date
public let sessionStartDate: Date?
public let sessionExpDate: Date?
public internal(set) var sessionExpDate: Date?

public var hasValidSensorSession: Bool {
return sessionStartDate != nil
Expand Down
16 changes: 12 additions & 4 deletions CGMBLEKit/Messages/TransmitterVersionRxMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
import Foundation


struct TransmitterVersionRxMessage: TransmitterRxMessage {
let status: UInt8
let firmwareVersion: [UInt8]
public struct TransmitterVersionRxMessage: TransmitterRxMessage {
public let status: UInt8
public let firmwareVersion: [UInt8]
/// Lifetime the transmitter reports for itself (stock G6 = 90, Anubis-modded G6 = 180).
public let transmitterExpiryInDays: UInt16

init?(data: Data) {
public init?(data: Data) {
guard data.count == 19 && data.isCRCValid else {
return nil
}
Expand All @@ -24,6 +26,12 @@ struct TransmitterVersionRxMessage: TransmitterRxMessage {

status = data[1]
firmwareVersion = data[2..<6].map { $0 }
transmitterExpiryInDays = (UInt16(data[14]) << 8) + UInt16(data[13])
}

/// Heuristic borrowed from xDrip4iOS: Anubis-modded G6 transmitters
/// report a 180-day expiry in the version-rx frame; stock G6 reports 90.
public var isAnubis: Bool {
return transmitterExpiryInDays == 180
}
}
6 changes: 4 additions & 2 deletions CGMBLEKit/Messages/TransmitterVersionTxMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import Foundation


struct TransmitterVersionTxMessage {
struct TransmitterVersionTxMessage: RespondableMessage {
typealias Response = TransmitterVersionRxMessage

let opcode: Opcode = .transmitterVersionTx
var data: Data {
return Data(for: .transmitterVersionTx).appendingCRC()
}
}
42 changes: 42 additions & 0 deletions CGMBLEKit/Transmitter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ public protocol TransmitterDelegate: AnyObject {
func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose])

func transmitter(_ transmitter: Transmitter, didReadUnknownData data: Data)

func transmitter(_ transmitter: Transmitter, didReadTransmitterVersion message: TransmitterVersionRxMessage)
}

public extension TransmitterDelegate {
// Default no-op so existing implementors don't need to opt in.
func transmitter(_ transmitter: Transmitter, didReadTransmitterVersion message: TransmitterVersionRxMessage) {}
}

/// These methods are called on a private background queue. It is the responsibility of the client to ensure thread-safety.
Expand Down Expand Up @@ -64,6 +71,8 @@ public final class Transmitter: BluetoothManagerDelegate {

public var passiveModeEnabled: Bool

public var needsExpiryRead: Bool = false

public weak var delegate: TransmitterDelegate?

public weak var commandSource: TransmitterCommandSource?
Expand Down Expand Up @@ -206,6 +215,16 @@ public final class Transmitter: BluetoothManagerDelegate {
self.log.debug("Reading calibration data")
let calibrationMessage = try? peripheral.readCalibrationData()

// Best-effort version read — surfaces transmitter-reported
// expiry (Anubis detection). Optional: don't fail the
// connect cycle if the transmitter doesn't answer.
self.log.debug("Reading transmitter version")
if let versionMessage = try? peripheral.readTransmitterVersion() {
self.delegateQueue.async {
self.delegate?.transmitter(self, didReadTransmitterVersion: versionMessage)
}
}

let glucose = Glucose(
transmitterID: self.id.id,
glucoseMessage: glucoseMessage,
Expand Down Expand Up @@ -267,6 +286,13 @@ public final class Transmitter: BluetoothManagerDelegate {
self.delegate?.transmitter(self, didError: error)
}
}

if self.needsExpiryRead, let versionMessage = try? peripheral.readTransmitterVersion() {
self.needsExpiryRead = false
self.delegateQueue.async {
self.delegate?.transmitter(self, didReadTransmitterVersion: versionMessage)
}
}
}
case .transmitterTimeRx?:
if let timeMessage = TransmitterTimeRxMessage(data: response) {
Expand Down Expand Up @@ -338,6 +364,14 @@ public final class Transmitter: BluetoothManagerDelegate {
}

lastCalibrationMessage = calibrationDataMessage
case .transmitterVersionRx?:
guard let versionMessage = TransmitterVersionRxMessage(data: response) else {
break
}

delegateQueue.async {
self.delegate?.transmitter(self, didReadTransmitterVersion: versionMessage)
}
case .none:
delegateQueue.async {
self.delegate?.transmitter(self, didReadUnknownData: response)
Expand Down Expand Up @@ -551,6 +585,14 @@ fileprivate extension PeripheralManager {
}
}

func readTransmitterVersion() throws -> TransmitterVersionRxMessage {
do {
return try writeMessage(TransmitterVersionTxMessage(), for: .control)
} catch let error {
throw TransmitterError.controlError("Error getting transmitter version: \(error)")
}
}

func disconnect() {
do {
try setNotifyValue(false, for: .control)
Expand Down
54 changes: 52 additions & 2 deletions CGMBLEKit/TransmitterManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ public class TransmitterManager: TransmitterDelegate {

self.transmitter.delegate = self

self.transmitter.needsExpiryRead = state.transmitterExpiryInDays == nil

#if targetEnvironment(simulator)
setupSimulatedSampleGenerator()
#endif
Expand Down Expand Up @@ -270,6 +272,9 @@ public class TransmitterManager: TransmitterDelegate {
"latestConnection: \(String(describing: latestConnection))",
"dataIsFresh: \(dataIsFresh)",
"providesBLEHeartbeat: \(providesBLEHeartbeat)",
"transmitterExpiryInDays: \(String(describing: state.transmitterExpiryInDays))",
"isAnubis: \(isAnubis)",
"sensorLifeDays: \(state.sensorLifeDays)",
shareManager.debugDescription,
"observers.count: \(observers.cleanupDeallocatedElements().count)",
String(reflecting: transmitter),
Expand Down Expand Up @@ -310,6 +315,9 @@ public class TransmitterManager: TransmitterDelegate {
}

public func transmitter(_ transmitter: Transmitter, didRead glucose: Glucose) {
var glucose = glucose
glucose.sessionExpDate = glucose.sessionStartDate?.addingTimeInterval(state.sensorLife)

guard glucose != latestReading else {
updateDelegate(with: .noData)
return
Expand Down Expand Up @@ -341,8 +349,8 @@ public class TransmitterManager: TransmitterDelegate {
date: sessionStartDate,
type: .sensorStart,
deviceIdentifier: transmitter.ID,
expectedLifetime: .hours(24 * 10),
warmupPeriod: .hours(2)
expectedLifetime: state.sensorLife,
warmupPeriod: state.isAnubis ? .minutes(50) : .hours(2)
))
} else {
log.error("Ignoring sensor start event with invalid session start time: %{public}@", String(describing: glucose))
Expand Down Expand Up @@ -431,6 +439,48 @@ public class TransmitterManager: TransmitterDelegate {

logDeviceCommunication("Unknown sensor data: \(data.hexadecimalString)", type: .error)
}

public func transmitter(_ transmitter: Transmitter, didReadTransmitterVersion message: TransmitterVersionRxMessage) {
log.default("Transmitter reports expiry of %d days (isAnubis=%@)",
message.transmitterExpiryInDays, String(describing: message.isAnubis))
logDeviceCommunication("Transmitter version: expiry \(message.transmitterExpiryInDays) days", type: .receive)
transmitter.needsExpiryRead = false
mutateState { state in
state.transmitterExpiryInDays = message.transmitterExpiryInDays
}
updateLatestReadingSessionExpDate()
}

/// `true` once the transmitter has reported the Anubis 180-day lifetime.
public var isAnubis: Bool {
return state.isAnubis
}

/// User-configured session length; only honored for Anubis (see `sensorLife`).
public var sensorLifeDays: Int {
get {
return state.sensorLifeDays
}
set {
mutateState { state in
state.sensorLifeDays = TransmitterManagerState.clampedSensorLifeDays(newValue)
}
updateLatestReadingSessionExpDate()
}
}

public var sensorLife: TimeInterval {
return state.sensorLife
}

/// Re-stamps the cached reading so a sensor-life change applies immediately.
private func updateLatestReadingSessionExpDate() {
guard var reading = latestReading else {
return
}
reading.sessionExpDate = reading.sessionStartDate?.addingTimeInterval(state.sensorLife)
latestReading = reading
}
}


Expand Down
43 changes: 41 additions & 2 deletions CGMBLEKit/TransmitterManagerState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ public struct TransmitterManagerState: RawRepresentable, Equatable {

public static let version = 1

public static let defaultSensorLifeDays = 10

/// Selectable session lengths for Anubis-modded transmitters.
public static let sensorLifeDaysRange = 10...60

static func clampedSensorLifeDays(_ days: Int) -> Int {
return min(max(days, sensorLifeDaysRange.lowerBound), sensorLifeDaysRange.upperBound)
}

public var transmitterID: String

public var passiveModeEnabled: Bool = true
Expand All @@ -24,16 +33,27 @@ public struct TransmitterManagerState: RawRepresentable, Equatable {

public var shouldSyncToRemoteService: Bool

/// Transmitter-reported lifetime in days (90 for stock G6, 180 for
/// Anubis-modded). `nil` until the first version-rx frame comes in.
public var transmitterExpiryInDays: UInt16?

/// User-configured session length; only honored for Anubis (see `sensorLife`).
public var sensorLifeDays: Int

public init(
transmitterID: String,
shouldSyncToRemoteService: Bool = true,
transmitterStartDate: Date? = nil,
sensorStartOffset: UInt32? = nil
sensorStartOffset: UInt32? = nil,
transmitterExpiryInDays: UInt16? = nil,
sensorLifeDays: Int = Self.defaultSensorLifeDays
) {
self.transmitterID = transmitterID
self.shouldSyncToRemoteService = shouldSyncToRemoteService
self.transmitterStartDate = transmitterStartDate
self.sensorStartOffset = sensorStartOffset
self.transmitterExpiryInDays = transmitterExpiryInDays
self.sensorLifeDays = Self.clampedSensorLifeDays(sensorLifeDays)
}

public init?(rawValue: RawValue) {
Expand All @@ -48,11 +68,18 @@ public struct TransmitterManagerState: RawRepresentable, Equatable {

let sensorStartOffset = rawValue["sensorStartOffset"] as? UInt32

let transmitterExpiryInDays = (rawValue["transmitterExpiryInDays"] as? UInt16)
?? (rawValue["transmitterExpiryInDays"] as? Int).map { UInt16($0) }

let sensorLifeDays = rawValue["sensorLifeDays"] as? Int ?? Self.defaultSensorLifeDays

self.init(
transmitterID: transmitterID,
shouldSyncToRemoteService: shouldSyncToRemoteService,
transmitterStartDate: transmitterStartDate,
sensorStartOffset: sensorStartOffset
sensorStartOffset: sensorStartOffset,
transmitterExpiryInDays: transmitterExpiryInDays,
sensorLifeDays: sensorLifeDays
)
}

Expand All @@ -64,7 +91,19 @@ public struct TransmitterManagerState: RawRepresentable, Equatable {

rval["transmitterStartDate"] = transmitterStartDate
rval["sensorStartOffset"] = sensorStartOffset
rval["transmitterExpiryInDays"] = transmitterExpiryInDays.map { Int($0) }
rval["sensorLifeDays"] = sensorLifeDays

return rval
}

/// `true` once the transmitter has reported the Anubis 180-day lifetime.
public var isAnubis: Bool {
return transmitterExpiryInDays == 180
}

/// Active session length: `sensorLifeDays` for Anubis, else the stock 10 days.
public var sensorLife: TimeInterval {
return .hours(24 * Double(isAnubis ? sensorLifeDays : Self.defaultSensorLifeDays))
}
}
Loading