Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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 .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@
[submodule "DanaKit"]
path = DanaKit
url = https://github.com/loopandlearn/DanaKit
[submodule "MedtrumKit"]
path = MedtrumKit
branch = dev
url = https://github.com/loopandlearn/MedtrumKit
2 changes: 1 addition & 1 deletion DanaKit
1 change: 1 addition & 0 deletions MedtrumKit
Submodule MedtrumKit added at aedb7b
6 changes: 6 additions & 0 deletions Trio.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@
3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687E2D8DDD8800899469 /* CryptoSwift */; };
3BF85FE32E427312000D7351 /* IOBService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF85FE12E427312000D7351 /* IOBService.swift */; };
3E28F2AB2EB5337F00FB9EEB /* ConnectIQ in Frameworks */ = {isa = PBXBuildFile; productRef = 3E28F2AA2EB5337F00FB9EEB /* ConnectIQ */; };
3E54EF2C2E476DA40006F54D /* MedtrumKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */; };
3E54EF2D2E476DA40006F54D /* MedtrumKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */; };
Expand Down Expand Up @@ -783,6 +785,7 @@
3B4BA7792D8DBD690069D5B8 /* MinimedKit.framework in Embed Frameworks */,
CE95BF5C2BA770C300DC3DE3 /* LoopKit.framework in Embed Frameworks */,
3B4BA7712D8DBD690069D5B8 /* G7SensorKit.framework in Embed Frameworks */,
3E54EF2D2E476DA40006F54D /* MedtrumKit.framework in Embed Frameworks */,
CEB434FE28B90B8C00B70274 /* SwiftCharts in Embed Frameworks */,
3B4BA7812D8DBD690069D5B8 /* OmniKitUI.framework in Embed Frameworks */,
3B4BA76F2D8DBD690069D5B8 /* DanaKit.framework in Embed Frameworks */,
Expand Down Expand Up @@ -1086,6 +1089,7 @@
3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
3BF85FE12E427312000D7351 /* IOBService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBService.swift; sourceTree = "<group>"; };
3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MedtrumKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigStateModel.swift; sourceTree = "<group>"; };
3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorDataFlow.swift; sourceTree = "<group>"; };
42369F66CF91F30624C0B3A6 /* BasalProfileEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorProvider.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1578,6 +1582,7 @@
3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */,
3B4BA7782D8DBD690069D5B8 /* MinimedKit.framework in Frameworks */,
3B4BA7762D8DBD690069D5B8 /* LibreTransmitterUI.framework in Frameworks */,
3E54EF2C2E476DA40006F54D /* MedtrumKit.framework in Frameworks */,
3B4BA7902D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Frameworks */,
3B4BA76A2D8DBD690069D5B8 /* CGMBLEKit.framework in Frameworks */,
3B4BA77C2D8DBD690069D5B8 /* OmniBLE.framework in Frameworks */,
Expand Down Expand Up @@ -2182,6 +2187,7 @@
3818AA48274C267000843DB3 /* Frameworks */ = {
isa = PBXGroup;
children = (
3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */,
3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */,
3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */,
3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */,
Expand Down
3 changes: 3 additions & 0 deletions Trio.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Trio/Sources/APS/APSManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ protocol APSManager {
var lastLoopDateSubject: PassthroughSubject<Date, Never> { get }
var bolusProgress: CurrentValueSubject<Decimal?, Never> { get }
var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
var pumpActivatedAtDate: CurrentValueSubject<Date?, Never> { get }
var isManualTempBasal: Bool { get }
var isScheduledBasal: Bool? { get }
var isSuspended: Bool { get }
Expand Down Expand Up @@ -129,6 +130,10 @@ final class BaseAPSManager: APSManager, Injectable {
deviceDataManager.pumpExpiresAtDate
}

var pumpActivatedAtDate: CurrentValueSubject<Date?, Never> {
deviceDataManager.pumpActivatedAtDate
}

var settings: TrioSettings {
get { settingsManager.settings }
set { settingsManager.settings = newValue }
Expand Down
42 changes: 42 additions & 0 deletions Trio/Sources/APS/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import DanaKit
import Foundation
import LoopKit
import LoopKitUI
import MedtrumKit
import MinimedKit
import MockKit
import OmniBLE
Expand All @@ -27,6 +28,7 @@ protocol DeviceDataManager: GlucoseSource {
var errorSubject: PassthroughSubject<Error, Never> { get }
var pumpName: CurrentValueSubject<String, Never> { get }
var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
var pumpActivatedAtDate: CurrentValueSubject<Date?, Never> { get }

func heartbeat(date: Date)
func createBolusProgressReporter() -> DoseProgressReporter?
Expand All @@ -38,6 +40,7 @@ private let staticPumpManagers: [PumpManagerUI.Type] = [
OmnipodPumpManager.self,
OmniBLEPumpManager.self,
DanaKitPumpManager.self,
MedtrumPumpManager.self,
MockPumpManager.self
]

Expand All @@ -46,6 +49,7 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
OmnipodPumpManager.pluginIdentifier: OmnipodPumpManager.self,
OmniBLEPumpManager.pluginIdentifier: OmniBLEPumpManager.self,
DanaKitPumpManager.pluginIdentifier: DanaKitPumpManager.self,
MedtrumPumpManager.pluginIdentifier: MedtrumPumpManager.self,
MockPumpManager.pluginIdentifier: MockPumpManager.self
]

Expand Down Expand Up @@ -106,19 +110,34 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
storage.save(modifiedPreferences, as: OpenAPS.Settings.preferences)

if let omnipod = pumpManager as? OmnipodPumpManager {
pumpActivatedAtDate.send(nil)
guard let endTime = omnipod.state.podState?.expiresAt else {
pumpExpiresAtDate.send(nil)
return
}
pumpExpiresAtDate.send(endTime)
}
if let omnipodBLE = pumpManager as? OmniBLEPumpManager {
pumpActivatedAtDate.send(nil)
guard let endTime = omnipodBLE.state.podState?.expiresAt else {
pumpExpiresAtDate.send(nil)
return
}
pumpExpiresAtDate.send(endTime)
}
if let medtrum = pumpManager as? MedtrumPumpManager {
if medtrum.state.expirationTimer == 1 {
pumpActivatedAtDate.send(nil)
guard let endTime = medtrum.state.patchExpiresAt else {
pumpExpiresAtDate.send(nil)
return
}
pumpExpiresAtDate.send(endTime)
} else {
pumpActivatedAtDate.send(medtrum.state.patchActivatedAt)
pumpExpiresAtDate.send(nil)
}
}
if let simulatorPump = pumpManager as? MockPumpManager {
pumpDisplayState.value = PumpDisplayState(name: simulatorPump.localizedTitle, image: simulatorPump.smallImage)
pumpName.send(simulatorPump.localizedTitle)
Expand Down Expand Up @@ -163,6 +182,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
} else {
pumpDisplayState.value = nil
pumpExpiresAtDate.send(nil)
pumpActivatedAtDate.send(nil)
pumpName.send("")
// Reset bolusIncrement setting to default value, which is 0.1 U
var modifiedPreferences = settingsManager.preferences
Expand Down Expand Up @@ -202,6 +222,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {

let pumpDisplayState = CurrentValueSubject<PumpDisplayState?, Never>(nil)
let pumpExpiresAtDate = CurrentValueSubject<Date?, Never>(nil)
let pumpActivatedAtDate = CurrentValueSubject<Date?, Never>(nil)
let pumpName = CurrentValueSubject<String, Never>("Pump")

init(resolver: Resolver) {
Expand Down Expand Up @@ -460,6 +481,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
manualTempBasal.send(false)
}

pumpActivatedAtDate.send(nil)
guard let endTime = omnipod.state.podState?.expiresAt else {
pumpExpiresAtDate.send(nil)
return
Expand Down Expand Up @@ -493,6 +515,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
manualTempBasal.send(false)
}

pumpActivatedAtDate.send(nil)
guard let endTime = omnipodBLE.state.podState?.expiresAt else {
pumpExpiresAtDate.send(nil)
return
Expand All @@ -504,6 +527,25 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
}
}

if let medtrumPump = pumpManager as? MedtrumPumpManager {
storage.save(Decimal(medtrumPump.state.reservoir), as: OpenAPS.Monitor.reservoir)
broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
$0.pumpReservoirDidChange(Decimal(medtrumPump.state.reservoir))
}

if medtrumPump.state.expirationTimer == 1 {
pumpActivatedAtDate.send(nil)
guard let endTime = medtrumPump.state.patchExpiresAt else {
pumpExpiresAtDate.send(nil)
return
}
pumpExpiresAtDate.send(endTime)
} else {
pumpActivatedAtDate.send(medtrumPump.state.patchActivatedAt)
pumpExpiresAtDate.send(nil)
}
}

if let simulatorPump = pumpManager as? MockPumpManager {
broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
$0.pumpReservoirDidChange(Decimal(simulatorPump.state.reservoirUnitsRemaining))
Expand Down
6 changes: 6 additions & 0 deletions Trio/Sources/Localizations/Main/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -16296,6 +16296,9 @@
}
}
}
},
"• Medtrum Nano (200u/300u)" : {

},
"• Nightscout" : {
"localizations" : {
Expand Down Expand Up @@ -158068,6 +158071,9 @@
}
}
}
},
"Medtrum Nano" : {

},
"Menu" : {
"comment" : "Menu",
Expand Down
6 changes: 6 additions & 0 deletions Trio/Sources/Modules/Home/HomeStateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ extension Home {
var reservoir: Decimal?
var pumpName = ""
var pumpExpiresAtDate: Date?
var pumpActivatedAtDate: Date?
var highTTraisesSens: Bool = false
var lowTTlowersSens: Bool = false
var isExerciseModeActive: Bool = false
Expand Down Expand Up @@ -344,6 +345,11 @@ extension Home {
.weakAssign(to: \.pumpExpiresAtDate, on: self)
.store(in: &lifetime)

apsManager.pumpActivatedAtDate
.receive(on: DispatchQueue.main)
.weakAssign(to: \.pumpActivatedAtDate, on: self)
.store(in: &lifetime)

apsManager.lastError
.receive(on: DispatchQueue.main)
.map { [weak self] error in
Expand Down
91 changes: 67 additions & 24 deletions Trio/Sources/Modules/Home/View/Header/PumpView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ struct PumpView: View {
let reservoir: Decimal?
let name: String
let expiresAtDate: Date?
let activatedAtDate: Date?
let timerDate: Date
let pumpStatusHighlightMessage: String?
let battery: [OpenAPS_Battery]
@Environment(\.colorScheme) var colorScheme

let NORMAL_PATCH_AGE = TimeInterval.hours(80)
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constant name should follow Swift naming conventions and be lowercase (e.g., 'normalPatchAge').

Suggested change
let NORMAL_PATCH_AGE = TimeInterval.hours(80)
let normalPatchAge = TimeInterval.hours(80)

Copilot uses AI. Check for mistakes.

private var batteryFormatter: NumberFormatter {
let formatter = NumberFormatter()
formatter.numberStyle = .percent
return formatter
}

private var hourglassIcon: String {
if activatedAtDate != nil { return "hourglass.badge.plus" }
guard let expiration = expiresAtDate else { return "hourglass" }

let hoursRemaining = expiration.timeIntervalSince(timerDate) / 3600
Expand Down Expand Up @@ -96,34 +100,44 @@ struct PumpView: View {
}

if let date = expiresAtDate {
HStack {
Image(systemName: hourglassIcon)
.font(.callout)
.foregroundStyle(timerColor, Color.yellow)
.symbolRenderingMode(.palette)

let remainingTimeString = remainingTimeString(time: date.timeIntervalSince(timerDate))
PatchTimer(date, isExpiration: true)
}

Text(remainingTimeString)
.font(date.timeIntervalSince(timerDate) > 0 ? .callout : .subheadline)
.fontWeight(.bold)
.fontDesign(.rounded)
.lineLimit(2)
.multilineTextAlignment(.leading)
.frame(
// If the string is > 6 chars, i.e., exceeds "xd yh", limit width to 80 pts
// This forces the "Replace pod" string to wrap to 2 lines.
maxWidth: remainingTimeString.count > 6 ? 80 : .infinity,
alignment: .leading
)
}
// aligns the stopwatch icon exactly with the first pixel of the reservoir icon
.padding(.leading, date.timeIntervalSince(timerDate) > 0 ? 12 : 0)
if let date = activatedAtDate {
PatchTimer(date, isExpiration: false)
}
}
}
}

@ViewBuilder private func PatchTimer(_ date: Date, isExpiration: Bool) -> some View {
HStack {
Image(systemName: hourglassIcon)
.font(.callout)
.foregroundStyle(timerColor, timerColorSecondary)
.symbolRenderingMode(.palette)

let remainingTimeString = isExpiration ?
remainingTimeString(time: date.timeIntervalSince(timerDate)) :
activeTimeString(time: timerDate.timeIntervalSince(date))

Text(remainingTimeString)
.font(date.timeIntervalSince(timerDate) > 0 ? .callout : .subheadline)
.fontWeight(.bold)
.fontDesign(.rounded)
.lineLimit(2)
.multilineTextAlignment(.leading)
.frame(
// If the string is > 6 chars, i.e., exceeds "xd yh", limit width to 80 pts
// This forces the "Replace pod" string to wrap to 2 lines.
maxWidth: remainingTimeString.count > 6 ? 80 : .infinity,
alignment: .leading
)
}
// aligns the stopwatch icon exactly with the first pixel of the reservoir icon
.padding(.leading, date.timeIntervalSince(timerDate) > 0 || !isExpiration ? 12 : 0)
}

private func remainingTimeString(time: TimeInterval) -> String {
guard time > 0 else {
return String(localized: "Replace pod", comment: "View/Header when pod expired")
Expand All @@ -148,6 +162,23 @@ struct PumpView: View {
return "\(minutes)" + String(localized: "m", comment: "abbreviation for minutes")
}

private func activeTimeString(time: TimeInterval) -> String {
var time = time
let days = Int(time / 1.days.timeInterval)
time -= days.days.timeInterval
let hours = Int(time / 1.hours.timeInterval)
time -= hours.hours.timeInterval
let minutes = Int(time / 1.minutes.timeInterval)

if days >= 1 {
return "\(days)" + String(localized: "d", comment: "abbreviation for days") + " \(hours)" +
String(localized: "h", comment: "abbreviation for hours")
}

return "\(hours)" + String(localized: "h", comment: "abbreviation for hours") + "\(minutes)" +
String(localized: "m", comment: "abbreviation for minutes")
}

private var batteryColor: Color {
guard let battery = battery.first else {
return .gray
Expand Down Expand Up @@ -179,11 +210,15 @@ struct PumpView: View {
}

private var timerColor: Color {
guard let expisesAt = expiresAtDate else {
if let activatedAt = activatedAtDate {
return abs(activatedAt.timeIntervalSinceNow) > NORMAL_PATCH_AGE ? Color.yellow : Color.loopGreen
}

guard let expiresAt = expiresAtDate else {
return .gray
}

let time = expisesAt.timeIntervalSince(timerDate)
let time = expiresAt.timeIntervalSince(timerDate)

switch time {
case ...8.hours.timeInterval:
Expand All @@ -194,6 +229,14 @@ struct PumpView: View {
return Color.loopGreen
}
}

private var timerColorSecondary: Color {
if activatedAtDate != nil {
return Color.gray
}

return Color.yellow
}
}

// #Preview("message") {
Expand Down
Loading
Loading