Skip to content
Draft
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: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[submodule "LoopKit"]
path = LoopKit
url = https://github.com/loopandlearn/LoopKit.git
url = https://github.com/bjorkert/LoopKit.git
[submodule "CGMBLEKit"]
path = CGMBLEKit
url = https://github.com/loopandlearn/CGMBLEKit.git
Expand Down Expand Up @@ -30,4 +30,4 @@
url = https://github.com/loopandlearn/MedtrumKit
[submodule "OmnipodKit"]
path = OmnipodKit
url = https://github.com/loopandlearn/OmnipodKit
url = https://github.com/bjorkert/OmnipodKit.git
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public extension BolusStored {
}

@NSManaged var amount: NSDecimalNumber?
@NSManaged var bolusOrigin: String?
@NSManaged var isExternal: Bool
@NSManaged var isSMB: Bool
@NSManaged var pumpEvent: PumpEventStored?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="25B78" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
<attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
<attribute name="bolusOrigin" optional="YES" attributeType="String"/>
<attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isSMB" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="pumpEvent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PumpEventStored" inverseName="bolus" inverseEntity="PumpEventStored"/>
Expand Down
2 changes: 1 addition & 1 deletion OmnipodKit
Submodule OmnipodKit updated 34 files
+367 −226 Localization/Localizable.xcstrings
+3 −8 OmniTests/O5/BolusExtraCommandO5Tests.swift
+7 −11 OmniTests/O5/O5AidCommandsTests.swift
+14 −0 OmniTests/O5/O5BleFramingFixtures.swift
+165 −0 OmniTests/O5/O5BleFramingTests.swift
+12 −31 OmniTests/O5/O5CommLogFixtures.swift
+43 −0 OmniTests/O5/O5CommLogRecvTests.swift
+54 −0 OmniTests/O5/O5FaultEventCodeTests.swift
+87 −0 OmniTests/O5/O5Type4MessageSigningTests.swift
+22 −0 OmniTests/O5/O5VersionResponseAndMessageTests.swift
+57 −0 OmniTests/O5/O5VersionResponseTests.swift
+0 −4 OmniTests/O5/PodAdvertisementO5Tests.swift
+5 −9 OmnipodKit/Bluetooth/Id.swift
+166 −0 OmnipodKit/Bluetooth/Pair/O5CertificateKeychain.swift
+27 −6 OmnipodKit/Bluetooth/Pair/O5CertificateStore.swift
+77 −0 OmnipodKit/Bluetooth/Pair/O5RegistrationData.swift
+2 −0 OmnipodKit/Info.plist
+15 −10 OmnipodKit/OmnipodCommon/LotDecode.swift
+8 −6 OmnipodKit/OmnipodCommon/PendingCommand.swift
+9 −9 OmnipodKit/OmnipodCommon/PodType.swift
+12 −4 OmnipodKit/OmnipodCommon/UnfinalizedDose.swift
+25 −4 OmnipodKit/PumpManager/OmniPumpManager.swift
+7 −7 OmnipodKit/PumpManager/PodCommsSession.swift
+1 −1 OmnipodKit/PumpManager/PodState.swift
+75 −12 OmnipodKit/PumpManagerUI/ViewControllers/OmniUICoordinator.swift
+7 −0 OmnipodKit/PumpManagerUI/ViewModels/OmniSettingsViewModel.swift
+167 −0 OmnipodKit/PumpManagerUI/Views/O5KeyFetchView.swift
+101 −0 OmnipodKit/PumpManagerUI/Views/O5KeySetupView.swift
+36 −0 OmnipodKit/PumpManagerUI/Views/OmniSettingsView.swift
+306 −0 OmnipodKit/PumpManagerUI/Views/Omnipod5SupportView.swift
+11 −12 OmnipodKit/PumpManagerUI/Views/PairPodView.swift
+2 −1 OmnipodKit/PumpManagerUI/Views/PodDiagnosticsView.swift
+523 −0 OmnipodKit/Services/O5AppAttestService.swift
+18 −5 patches/update_omnipodkit_for_LoopWorkspace_tidepool-sync-2026.patch
91 changes: 59 additions & 32 deletions Trio.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

37 changes: 32 additions & 5 deletions Trio/Sources/APS/APSManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Swinject

protocol APSManager {
func heartbeat(date: Date)
func enactBolus(amount: Double, isSMB: Bool, callback: ((Bool, String) -> Void)?) async
func enactBolus(amount: Double, isSMB: Bool, origin: BolusOrigin?, callback: ((Bool, String) -> Void)?) async
var pumpManager: PumpManagerUI? { get set }
var bluetoothManager: BluetoothStateManager? { get }
var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
Expand Down Expand Up @@ -36,6 +36,13 @@ protocol APSManager {
var iobFileDidUpdate: PassthroughSubject<Void, Never> { get }
}

extension APSManager {
/// Convenience for callers that do not tag a bolus origin.
func enactBolus(amount: Double, isSMB: Bool, callback: ((Bool, String) -> Void)?) async {
await enactBolus(amount: amount, isSMB: isSMB, origin: nil, callback: callback)
}
}

enum APSError: LocalizedError {
case pumpError(Error)
case invalidPumpState(message: String)
Expand Down Expand Up @@ -550,7 +557,7 @@ final class BaseAPSManager: APSManager, Injectable {
return min(rounded, maxBolus)
}

func enactBolus(amount: Double, isSMB: Bool, callback: ((Bool, String) -> Void)?) async {
func enactBolus(amount: Double, isSMB: Bool, origin: BolusOrigin?, callback: ((Bool, String) -> Void)?) async {
if amount <= 0 {
return
}
Expand All @@ -575,10 +582,14 @@ final class BaseAPSManager: APSManager, Injectable {

let roundedAmount = pump.roundToSupportedBolusVolume(units: amount)

// Mint the correlation reference only once the command is actually going to the pump, so none of the
// early returns above can leave an orphaned origin mapping behind.
let bolusReference = origin.map { BolusOriginStore.shared.makeReference(for: $0) }

debug(.apsManager, "Enact bolus \(roundedAmount), manual \(!isSMB)")

do {
try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
try await pump.enactBolus(units: roundedAmount, automatic: isSMB, bolusReference: bolusReference)
debug(.apsManager, "Bolus succeeded")
bolusProgress.send(0)
callback?(true, String(localized: "Bolus enacted successfully.", comment: "Success message for enacting a bolus"))
Expand All @@ -594,6 +605,11 @@ final class BaseAPSManager: APSManager, Injectable {
}
} catch {
warning(.apsManager, "Bolus failed with error: \(error)")
// Drop the origin mapping on a definite failure. On uncertain delivery we keep it, because the
// dose may still be reported (and reconciled) later and should still resolve its origin.
if let bolusReference = bolusReference, !error.isUncertainDelivery {
BolusOriginStore.shared.remove(reference: bolusReference)
}
processError(APSError.pumpError(error))
if !isSMB {
// Use MainActor to handle broadcaster notification
Expand Down Expand Up @@ -755,6 +771,7 @@ final class BaseAPSManager: APSManager, Injectable {
}

private func performBolus(pump: PumpManager, smbToDeliver: NSDecimalNumber) async throws {
// SMB is not origin-tagged: it is already distinguishable in Nightscout via eventType "SMB".
try await pump.enactBolus(units: Double(truncating: smbToDeliver), automatic: true)
bolusProgress.send(0)
}
Expand Down Expand Up @@ -1245,11 +1262,11 @@ private extension PumpManager {
}
}

func enactBolus(units: Double, automatic: Bool) async throws {
func enactBolus(units: Double, automatic: Bool, bolusReference: UUID? = nil) async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
let automaticValue = automatic ? BolusActivationType.automatic : BolusActivationType.manualRecommendationAccepted

self.enactBolus(units: units, activationType: automaticValue) { error in
self.enactBolus(units: units, activationType: automaticValue, bolusReference: bolusReference) { error in
if let error = error {
debug(.apsManager, "Bolus failed: \(units)")
continuation.resume(throwing: error)
Expand Down Expand Up @@ -1358,3 +1375,13 @@ extension PumpManagerStatus {
return PumpStatus(status: type, bolusing: bolusing, suspended: suspended, timestamp: Date())
}
}

private extension Error {
/// True when a bolus command failed with uncertain delivery — the pump may still report the dose later,
/// so any correlation state (e.g. the bolus-origin mapping) must be kept rather than cleaned up.
var isUncertainDelivery: Bool {
guard let pumpError = self as? PumpManagerError else { return false }
if case .uncertainDelivery = pumpError { return true }
return false
}
}
23 changes: 22 additions & 1 deletion Trio/Sources/APS/Storage/PumpHistoryStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {

func storePumpEvents(_ events: [NewPumpEvent]) async throws {
try await context.perform {
// Origin mappings consumed while tagging boluses below; removed from the store only after the
// context saves, so a failed save leaves them resolvable when the pump re-delivers the events.
var consumedBolusReferences: [UUID] = []

for event in events {
let existingEvents: [PumpEventStored] = try CoreDataStack.shared.fetchEntities(
ofType: PumpEventStored.self,
Expand Down Expand Up @@ -103,6 +107,18 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
newBolusEntry.isExternal = dose.manuallyEntered
newBolusEntry.isSMB = dose.automatic ?? true

// Record the finer origin of a user-initiated bolus (remote/watch/manual/shortcut) resolved
// from the reference the pump echoed back. SMB is not tagged here — it is already conveyed by
// isSMB / the SMB event type. The machine value goes to `bolusOrigin`, the display to `note`.
if let reference = dose.bolusReference,
let resolved = BolusOriginStore.shared.origin(forReference: reference)
{
newBolusEntry.bolusOrigin = resolved.rawValue
newPumpEvent.note = resolved.displayName
consumedBolusReferences.append(reference)
debug(.coreData, "Tagged bolus origin: \(resolved.displayName)")
}

case .tempBasal:
guard let dose = event.dose else { continue }

Expand Down Expand Up @@ -229,6 +245,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
guard self.context.hasChanges else { return }
try self.context.save()

for reference in consumedBolusReferences {
BolusOriginStore.shared.remove(reference: reference)
}

self.updateSubject.send(())
debug(.coreData, "\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
} catch let error as NSError {
Expand Down Expand Up @@ -348,7 +368,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
enteredBy: NightscoutTreatment.local,
bolus: nil,
insulin: event.bolus?.amount as Decimal?,
notes: nil,
notes: event.note,
bolusOrigin: event.bolus?.bolusOrigin,
carbs: nil,
fat: nil,
protein: nil,
Expand Down
3 changes: 3 additions & 0 deletions Trio/Sources/Models/NightscoutTreatment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ struct NightscoutTreatment: JSON, Hashable, Equatable {
var bolus: PumpHistoryEvent?
var insulin: Decimal?
var notes: String?
/// Machine-readable bolus origin, stable across locales (`notes` carries the human-readable label).
var bolusOrigin: String?
var carbs: Decimal?
var fat: Decimal?
var protein: Decimal?
Expand Down Expand Up @@ -50,6 +52,7 @@ extension NightscoutTreatment {
case bolus
case insulin
case notes
case bolusOrigin
case carbs
case fat
case protein
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ extension Treatments {
await MainActor.run {
self.isAwaitingDeterminationResult = true
}
await apsManager.enactBolus(amount: maxAmount, isSMB: false, callback: nil)
await apsManager.enactBolus(amount: maxAmount, isSMB: false, origin: .manual, callback: nil)
}
} catch {
debug(.bolusState, "Authentication error for pump bolus: \(error)")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import HealthKit
import LoopKit

extension TrioRemoteControl {
func handleBolusCommand(_ payload: CommandPayload) async throws {
Expand Down Expand Up @@ -41,7 +42,11 @@ extension TrioRemoteControl {
}

await apsManager
.enactBolus(amount: Double(truncating: bolusAmount as NSNumber), isSMB: false) { [weak self] success, message in
.enactBolus(
amount: Double(truncating: bolusAmount as NSNumber),
isSMB: false,
origin: .remote
) { [weak self] success, message in
guard let self = self else { return }
Task {
if success {
Expand Down
35 changes: 19 additions & 16 deletions Trio/Sources/Services/WatchManager/AppleWatchManager.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Combine
import CoreData
import Foundation
import LoopKit
import Swinject
import UIKit
import WatchConnectivity
Expand Down Expand Up @@ -728,14 +729,15 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
/// - Parameter amount: The requested bolus amount in units
private func handleBolusRequest(_ amount: Decimal) {
Task {
await apsManager.enactBolus(amount: Double(amount), isSMB: false) { success, message in
// Acknowledge success or error of bolus
self.sendAcknowledgment(
toWatch: success,
message: message,
ackCode: success == true ? .genericSuccess : .genericFailure
)
}
await apsManager
.enactBolus(amount: Double(amount), isSMB: false, origin: .watch) { success, message in
// Acknowledge success or error of bolus
self.sendAcknowledgment(
toWatch: success,
message: message,
ackCode: success == true ? .genericSuccess : .genericFailure
)
}
debug(.watchManager, "📱 Enacted bolus via APS Manager: \(amount)U")
}
}
Expand Down Expand Up @@ -848,14 +850,15 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana

// Enact bolus via APS Manager
let bolusDouble = NSDecimalNumber(decimal: bolusAmount).doubleValue
await apsManager.enactBolus(amount: bolusDouble, isSMB: false) { success, message in
// Acknowledge success or error of bolus
self.sendAcknowledgment(
toWatch: success,
message: message,
ackCode: success == true ? .genericSuccess : .genericFailure
)
}
await apsManager
.enactBolus(amount: bolusDouble, isSMB: false, origin: .watch) { success, message in
// Acknowledge success or error of bolus
self.sendAcknowledgment(
toWatch: success,
message: message,
ackCode: success == true ? .genericSuccess : .genericFailure
)
}
debug(.watchManager, "📱 Enacted bolus from watch via APS Manager: \(bolusDouble) U")
// Notify Watch: "Carbs and bolus logged successfully"
sendAcknowledgment(
Expand Down
8 changes: 7 additions & 1 deletion Trio/Sources/Shortcuts/Bolus/BolusIntentRequest.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Combine
import CoreData
import Foundation
import LoopKit

final class BolusIntentRequest: BaseIntentsRequest {
func bolus(_ bolusAmount: Double) async throws -> String {
Expand All @@ -23,7 +24,12 @@ final class BolusIntentRequest: BaseIntentsRequest {
}

let bolusQuantity = apsManager.roundBolus(amount: requestedAmount)
await apsManager.enactBolus(amount: Double(bolusQuantity), isSMB: false, callback: nil)
await apsManager.enactBolus(
amount: Double(bolusQuantity),
isSMB: false,
origin: .shortcut,
callback: nil
)
return String(
localized:
"A bolus command of \(bolusQuantity.formatted()) U of insulin was sent."
Expand Down
Loading