Skip to content
Open
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
1 change: 1 addition & 0 deletions Common/Locked.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class Locked<T> {
}
}

@discardableResult
public func mutate(_ changes: (_ value: inout T) -> Void) -> T {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
Expand Down
728 changes: 366 additions & 362 deletions G6SensorKit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1B26058BD31E40741B0058A5"
BlueprintIdentifier = "1FC930737F45A671AE16478F"
BuildableName = "G6SensorKit.framework"
BlueprintName = "G6SensorKit"
ReferencedContainer = "container:G6SensorKit.xcodeproj">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EDCCBEADE99E54A732DD551F"
BlueprintIdentifier = "D59705DF2CF01716CBDBCA1E"
BuildableName = "G6SensorKitPlugin.loopplugin"
BlueprintName = "G6SensorKitPlugin"
ReferencedContainer = "container:G6SensorKit.xcodeproj">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3ADDB0CD055A33C01C7B54FE"
BlueprintIdentifier = "5FDFF54B3F109CF9EB807CE7"
BuildableName = "G6SensorKitUI.framework"
BlueprintName = "G6SensorKitUI"
ReferencedContainer = "container:G6SensorKit.xcodeproj">
Expand Down
70 changes: 33 additions & 37 deletions G6SensorKit/G6CGMManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public final class G6CGMManager: CGMManager {
public private(set) var session: TransmitterSession?

/// Commands queued for the next connection cycle.
private let lockedPendingCommands: Locked<[Command]> = Locked([])
private let lockedCommandQueue: Locked<CommandQueue> = Locked(CommandQueue())

private var lastBatteryReadDate: Date?

Expand Down Expand Up @@ -134,7 +134,7 @@ public final class G6CGMManager: CGMManager {

public init(state: G6CGMManagerState) {
self.lockedState = Locked(state)
lockedPendingCommands.mutate { $0 = state.pendingCommands.compactMap(Command.init(rawValue:)) }
lockedCommandQueue.mutate { $0 = CommandQueue(rawValues: state.pendingCommands) }
startSession()
}

Expand All @@ -151,7 +151,7 @@ public final class G6CGMManager: CGMManager {
}

self.lockedState = Locked(state)
lockedPendingCommands.mutate { $0 = state.pendingCommands.compactMap(Command.init(rawValue:)) }
lockedCommandQueue.mutate { $0 = CommandQueue(rawValues: state.pendingCommands) }
startSession()
}

Expand Down Expand Up @@ -304,22 +304,16 @@ public final class G6CGMManager: CGMManager {
// is persisted, a stale code-less start could reach the transmitter
// ahead of the one carrying the sensor code the user actually typed.
var superseded = 0
lockedPendingCommands.mutate { commands in
let before = commands.count
commands.removeAll { $0.supersededBy(command) }
superseded = before - commands.count
commands.append(command)
lockedCommandQueue.mutate { queue in
superseded = queue.enqueue(command)
}

if superseded > 0 {
log.default("Replaced %d queued command(s) with %{public}@", superseded, String(describing: command))
}

mutateState { state in
state.pendingCommands.removeAll { raw in
Command(rawValue: raw).map { $0.supersededBy(command) } ?? true
}
state.pendingCommands.append(command.rawValue)
state.pendingCommands = self.lockedCommandQueue.value.rawValues

// Reflect the entered code straight away. Waiting until the
// transmitter answered meant the screen said "Not used" for as
Expand All @@ -332,6 +326,27 @@ public final class G6CGMManager: CGMManager {

// Nudge the connection in case it is idle.
session?.start()

#if targetEnvironment(simulator)
// No link will ever drain the queue in the simulator; apply the
// command to state directly, mirroring didComplete on hardware,
// so End Sensor / Start New Sensor work end-to-end there.
lockedCommandQueue.mutate { _ = $0.dequeue() }
mutateState { state in
state.pendingCommands = self.lockedCommandQueue.value.rawValues
switch command {
case .stopSensor:
state.sensorStartDate = nil
state.sensorCode = nil
case .startSensor(let date, let sensorCode):
state.sensorCode = sensorCode.carriesParameters ? sensorCode.code : nil
state.sensorStartDate = date
.addingTimeInterval(-(state.warmupPeriod - G6CGMManager.simulatedWarmupRemaining))
case .calibrateSensor, .resetTransmitter:
break
}
}
#endif
}

public func setSensorLifeDays(_ days: Int) {
Expand Down Expand Up @@ -383,7 +398,7 @@ public final class G6CGMManager: CGMManager {
if connectionPhase == .connected {
log.default("Old transmitter still connected; sending session stop before switching")
session?.commandSource = self
lockedPendingCommands.mutate { $0 = [.stopSensor(at: Date())] }
lockedCommandQueue.mutate { $0 = CommandQueue([.stopSensor(at: Date())]) }
} else {
log.default("Old transmitter not connected; its session is left as-is")
}
Expand All @@ -407,7 +422,7 @@ public final class G6CGMManager: CGMManager {
// middle of the state reset. The session itself is kept and pointed at
// the new transmitter — the radio does not need rebuilding for a swap.
session?.stop()
lockedPendingCommands.mutate { $0.removeAll() }
lockedCommandQueue.mutate { $0.removeAll() }
raisedAlerts = []

mutateState { state in
Expand Down Expand Up @@ -846,36 +861,17 @@ extension G6CGMManager: TransmitterCommandSource {
public func dequeuePendingCommand(for session: TransmitterSession) -> Command? {
var next: Command?
var dropped: [Command] = []
lockedPendingCommands.mutate { commands in
// A calibration is only meaningful next to the fingerstick it came
// from; xDrip uses the same five-minute window. Sending a stale one
// would teach the sensor from a value that no longer holds.
while let candidate = commands.first {
commands.removeFirst()
if case .calibrateSensor(_, let date) = candidate,
Date().timeIntervalSince(date) > .minutes(5) {
dropped.append(candidate)
continue
}
next = candidate
break
}
lockedCommandQueue.mutate { queue in
(next, dropped) = queue.dequeue()
}

for command in dropped {
log.default("Dropping stale command: %{public}@", String(describing: command))
mutateState { state in
if !state.pendingCommands.isEmpty {
state.pendingCommands.removeFirst()
}
}
}

if next != nil {
if next != nil || !dropped.isEmpty {
mutateState { state in
if !state.pendingCommands.isEmpty {
state.pendingCommands.removeFirst()
}
state.pendingCommands = self.lockedCommandQueue.value.rawValues
}
}

Expand Down
7 changes: 6 additions & 1 deletion G6SensorKit/G6SimulatedSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ extension G6CGMManager {
// warming up.
state.transmitterStartDate = now.addingTimeInterval(-.hours(24 * 12))
state.sensorStartDate = now.addingTimeInterval(-(state.warmupPeriod - Self.simulatedWarmupRemaining))
state.transmitterExpiryInDays = 90
// 180 days so the simulator presents as an Anubis, making the
// longer session length and expiry visible in the UI.
state.transmitterExpiryInDays = 180
state.firmwareVersion = "1.0.0.0 (simulated)"
state.peripheralIdentifier = UUID()
}
Expand All @@ -64,6 +66,9 @@ extension G6CGMManager {
}

private func emitSimulatedReading() {
// No active session (ended, or not yet started): stay quiet.
guard state.sensorStartDate != nil else { return }

guard !state.isInWarmup else {
// Still warming up: the state change alone refreshes the UI.
notifySimulationObservers()
Expand Down
Loading