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
27 changes: 18 additions & 9 deletions Trio/Sources/APS/APSManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ final class BaseAPSManager: APSManager, Injectable {
@Injected() private var settingsManager: SettingsManager!
@Injected() private var tddStorage: TDDStorage!
@Injected() private var broadcaster: Broadcaster!
@Injected() private var concentrationService: ConcentrationService!
@Persisted(key: "lastLoopStartDate") private var lastLoopStartDate: Date = .distantPast
@Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
didSet {
Expand Down Expand Up @@ -546,9 +547,12 @@ final class BaseAPSManager: APSManager, Injectable {

func roundBolus(amount: Decimal) -> Decimal {
guard let pump = pumpManager else { return amount }
let rounded = Decimal(pump.roundToSupportedBolusVolume(units: Double(amount)))
// Convert real units to pump units, round to pump's supported volumes, convert back
let pumpAmount = concentrationService.toPumpUnits(realUnits: Double(amount))
let rounded = Decimal(pump.roundToSupportedBolusVolume(units: pumpAmount))
let realRounded = Decimal(concentrationService.toRealUnits(pumpUnits: Double(rounded)))
let maxBolus = Decimal(pump.roundToSupportedBolusVolume(units: Double(settingsManager.pumpSettings.maxBolus)))
return min(rounded, maxBolus)
return min(realRounded, maxBolus)
}

private var bolusReporter: DoseProgressReporter?
Expand Down Expand Up @@ -576,9 +580,10 @@ final class BaseAPSManager: APSManager, Injectable {
return
}

let roundedAmount = pump.roundToSupportedBolusVolume(units: amount)
let pumpAmount = concentrationService.toPumpUnits(realUnits: amount)
let roundedAmount = pump.roundToSupportedBolusVolume(units: pumpAmount)

debug(.apsManager, "Enact bolus \(roundedAmount), manual \(!isSMB)")
debug(.apsManager, "Enact bolus \(amount) real units (\(roundedAmount) pump units), manual \(!isSMB)")

do {
try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
Expand Down Expand Up @@ -644,9 +649,10 @@ final class BaseAPSManager: APSManager, Injectable {
return
}

debug(.apsManager, "Enact temp basal \(rate) - \(duration)")
debug(.apsManager, "Enact temp basal \(rate) real U/hr - \(duration)")

let roundedAmout = pump.roundToSupportedBasalRate(unitsPerHour: rate)
let pumpRate = concentrationService.toPumpRate(realUnitsPerHour: rate)
let roundedAmout = pump.roundToSupportedBasalRate(unitsPerHour: pumpRate)

do {
try await pump.enactTempBasal(unitsPerHour: roundedAmout, for: duration)
Expand Down Expand Up @@ -688,7 +694,8 @@ final class BaseAPSManager: APSManager, Injectable {
case .active:
return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: date)
case let .tempBasal(dose):
let rate = Decimal(dose.unitsPerHour)
let pumpRate = Decimal(dose.unitsPerHour)
let rate = concentrationService.toRealRate(pumpUnitsPerHour: pumpRate)
let durationMin = max(0, Int((dose.endDate.timeIntervalSince1970 - date.timeIntervalSince1970) / 60))
return TempBasal(duration: durationMin, rate: rate, temp: .absolute, timestamp: date)
default:
Expand Down Expand Up @@ -749,11 +756,13 @@ final class BaseAPSManager: APSManager, Injectable {
}

private func performBasal(pump: PumpManager, rate: NSDecimalNumber, duration: TimeInterval) async throws {
try await pump.enactTempBasal(unitsPerHour: Double(truncating: rate), for: duration)
let pumpRate = concentrationService.toPumpRate(realUnitsPerHour: Double(truncating: rate))
try await pump.enactTempBasal(unitsPerHour: pumpRate, for: duration)
}

private func performBolus(pump: PumpManager, smbToDeliver: NSDecimalNumber) async throws {
try await pump.enactBolus(units: Double(truncating: smbToDeliver), automatic: true)
let pumpUnits = concentrationService.toPumpUnits(realUnits: Double(truncating: smbToDeliver))
try await pump.enactBolus(units: pumpUnits, automatic: true)
bolusProgress.send(0)
}

Expand Down
11 changes: 7 additions & 4 deletions Trio/Sources/APS/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
@Injected() private var glucoseStorage: GlucoseStorage!
@Injected() private var settingsManager: SettingsManager!
@Injected() private var bluetoothProvider: BluetoothStateManager!
@Injected() private var concentrationService: ConcentrationService!

@Persisted(key: "BaseDeviceDataManager.lastEventDate") var lastEventDate: Date? = nil
@SyncAccess(lock: accessLock) @Persisted(key: "BaseDeviceDataManager.lastHeartBeatTime") var lastHeartBeatTime: Date =
Expand Down Expand Up @@ -614,14 +615,16 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
>) -> Void
) {
dispatchPrecondition(condition: .onQueue(processQueue))
debug(.deviceManager, "Reservoir Value \(units), at: \(date)")
storage.save(Decimal(units), as: OpenAPS.Monitor.reservoir)
// Convert pump-reported reservoir volume to real insulin units
let realUnits = concentrationService.toRealUnits(pumpUnits: units)
debug(.deviceManager, "Reservoir Value \(realUnits) real units (\(units) pump units), at: \(date)")
storage.save(Decimal(realUnits), as: OpenAPS.Monitor.reservoir)
broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
$0.pumpReservoirDidChange(Decimal(units))
$0.pumpReservoirDidChange(Decimal(realUnits))
}

completion(.success((
newValue: Reservoir(startDate: Date(), unitVolume: units),
newValue: Reservoir(startDate: Date(), unitVolume: realUnits),
lastValue: nil,
areStoredValuesContinuous: true
)))
Expand Down
9 changes: 7 additions & 2 deletions Trio/Sources/APS/Storage/PumpHistoryStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
@Injected() private var storage: FileStorage!
@Injected() private var broadcaster: Broadcaster!
@Injected() private var settings: SettingsManager!
@Injected() private var concentrationService: ConcentrationService!

private let updateSubject = PassthroughSubject<Void, Never>()

Expand Down Expand Up @@ -62,8 +63,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
case .bolus:

guard let dose = event.dose else { continue }
// Convert pump-reported units to real insulin units
let realDoseUnits = self.concentrationService.toRealUnits(pumpUnits: dose.unitsInDeliverableIncrements)
let amount = self.roundDose(
dose.unitsInDeliverableIncrements,
realDoseUnits,
toIncrement: Double(self.settings.preferences.bolusIncrement)
)

Expand Down Expand Up @@ -112,8 +115,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
continue
}

let rate = Decimal(dose.unitsPerHour)
// Convert pump-reported rate to real insulin units/hr
let rate = self.concentrationService.toRealRate(pumpUnitsPerHour: Decimal(dose.unitsPerHour))
let minutes = (dose.endDate - dose.startDate).timeInterval / 60
// deliveredUnits is only checked for nil (cancel detection); value is not stored
let delivered = dose.deliveredUnits
let date = event.date

Expand Down
1 change: 1 addition & 0 deletions Trio/Sources/Assemblies/ServiceAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ final class ServiceAssembly: Assembly {
}
}
container.register(IOBService.self) { r in BaseIOBService(resolver: r) }
container.register(ConcentrationService.self) { r in BaseConcentrationService(resolver: r) }
}
}
42 changes: 42 additions & 0 deletions Trio/Sources/Models/InsulinConcentration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

/// Represents the concentration of insulin loaded in the pump.
///
/// Standard insulin is U-100 (100 units per mL). When using concentrated (U-200, U-500)
/// or diluted (U-10, U-25, U-50) insulin in a standard pump, the pump delivers a different
/// number of actual insulin units than it believes, because concentration affects volume.
///
/// The `factor` converts between real insulin units and pump-native (volume-equivalent) units:
/// - To pump: pumpUnits = realUnits / factor
/// - From pump: realUnits = pumpUnits × factor
enum InsulinConcentration: Int, JSON, CaseIterable, Identifiable, Equatable {
case u10 = 10
case u25 = 25
case u50 = 50
case u100 = 100
case u200 = 200
case u300 = 300
case u500 = 500

/// Units of insulin per milliliter
var unitsPerML: Int { rawValue }

/// Conversion factor relative to U-100.
///
/// - U-100: 1.0 (identity — no conversion needed)
/// - U-200: 2.0 (pump delivers twice the units per volume)
/// - U-50: 0.5 (pump delivers half the units per volume)
var factor: Decimal {
Decimal(rawValue) / 100
}

/// Whether this is the standard concentration (no translation needed)
var isStandard: Bool { self == .u100 }

/// Human-readable display label
var displayName: String {
"U-\(rawValue)"
}

var id: Int { rawValue }
}
9 changes: 7 additions & 2 deletions Trio/Sources/Models/TrioSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ struct TrioSettings: JSON, Equatable, Encodable {
var yGridLines: Bool = true
var hideInsulinBadge: Bool = false
var allowDilution: Bool = false
var insulinConcentration: Decimal = 1
var insulinConcentration: InsulinConcentration = .u100
var showCobIobChart: Bool = true
var rulerMarks: Bool = true
var bolusDisplayThreshold: BolusDisplayThreshold = .allUnits
Expand Down Expand Up @@ -294,8 +294,13 @@ extension TrioSettings: Decodable {
settings.allowDilution = allowDilution
}

if let insulinConcentration = try? container.decode(Decimal.self, forKey: .insulinConcentration) {
if let insulinConcentration = try? container.decode(InsulinConcentration.self, forKey: .insulinConcentration) {
settings.insulinConcentration = insulinConcentration
} else if let legacyFactor = try? container.decode(Decimal.self, forKey: .insulinConcentration) {
// Backward compatibility: the legacy field stored a Decimal factor (1.0 = U-100, 2.0 = U-200, 0.5 = U-50).
// Convert to rawValue (units per mL) by multiplying by 100.
let rawValue = Int(truncating: (legacyFactor * 100) as NSDecimalNumber)
settings.insulinConcentration = InsulinConcentration(rawValue: rawValue) ?? .u100
}

if let rulerMarks = try? container.decode(Bool.self, forKey: .rulerMarks) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ extension UnitsLimitsSettings {
@Published var maxCOB: Decimal = 120
@Published var hasChanged: Bool = false
@Published var threshold_setting: Decimal = 60
@Published var allowDilution: Bool = false
@Published var insulinConcentration: InsulinConcentration = .u100

var preferences: Preferences {
settingsManager.preferences
Expand All @@ -34,6 +36,9 @@ extension UnitsLimitsSettings {
subscribePreferencesSetting(\.maxCOB, on: $maxCOB) { maxCOB = $0 }
subscribePreferencesSetting(\.threshold_setting, on: $threshold_setting) { threshold_setting = $0 }

subscribeSetting(\.allowDilution, on: $allowDilution) { allowDilution = $0 }
subscribeSetting(\.insulinConcentration, on: $insulinConcentration) { insulinConcentration = $0 }

maxBasal = pumpSettings.maxBasal
maxBolus = pumpSettings.maxBolus
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,52 @@ extension UnitsLimitsSettings {
}
}
)

// MARK: - Insulin Concentration

Section(
header: Text("Insulin Concentration"),
footer: state.allowDilution ? Text(
"Active concentration: \(state.insulinConcentration.displayName) (factor: \(NSDecimalNumber(decimal: state.insulinConcentration.factor))×). All doses, IOB, and safety limits are in real insulin units."
) : nil
) {
Toggle(isOn: $state.allowDilution) {
VStack(alignment: .leading) {
Text("Enable Concentration Adjustment")
Text("Allow non-standard insulin concentrations (U-10 to U-500)")
.font(.caption)
.foregroundStyle(.secondary)
}
}

if state.allowDilution {
Picker("Concentration", selection: $state.insulinConcentration) {
ForEach(InsulinConcentration.allCases) { concentration in
Text(concentration.displayName).tag(concentration)
}
}

if !state.insulinConcentration.isStandard {
VStack(alignment: .leading, spacing: 6) {
Label {
Text("Safety Notice")
.font(.subheadline.bold())
} icon: {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
}

Text(
"Using \(state.insulinConcentration.displayName) insulin: each volume unit from the pump contains \(NSDecimalNumber(decimal: state.insulinConcentration.factor))× the insulin of U-100. Trio automatically translates all commands and readings. Verify your concentration matches the insulin loaded in your pump."
)
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}
}
}
.listRowBackground(Color.chart)
}
.listSectionSpacing(sectionSpacing)
.sheet(isPresented: $shouldDisplayHint) {
Expand Down
7 changes: 6 additions & 1 deletion Trio/Sources/Modules/Settings/SettingItems.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ enum SettingItems {
"Max IOB",
"Max COB",
"Minimum Safety Threshold",
"Delivery Limits"
"Delivery Limits",
"Insulin Concentration",
"U-100",
"U-200",
"U-500",
"Dilution"
],
path: ["Therapy Settings", "Units and Limits"]
),
Expand Down
Loading