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
12 changes: 10 additions & 2 deletions LoopFollow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
656F8C102E49F36F0008DC1D /* QRCodeDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */; };
656F8C122E49F3780008DC1D /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */; };
656F8C142E49F3D20008DC1D /* RemoteCommandSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */; };
65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; };
6584B1012E4A263900135D4D /* TOTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6584B1002E4A263900135D4D /* TOTPService.swift */; };
65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; };
65E8A2862E44B0300065037B /* VolumeButtonHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */; };
DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; };
DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; };
DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */; };
DD026E5B2EA2C9C300A39CB5 /* InsulinFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD026E5A2EA2C9C300A39CB5 /* InsulinFormatter.swift */; };
DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */; };
DD0650EB2DCE8385004D3B41 /* LowBGCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */; };
DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */; };
Expand Down Expand Up @@ -408,12 +410,14 @@
656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeDisplayView.swift; sourceTree = "<group>"; };
656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = "<group>"; };
656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCommandSettings.swift; sourceTree = "<group>"; };
65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = "<group>"; };
6584B1002E4A263900135D4D /* TOTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPService.swift; sourceTree = "<group>"; };
65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = "<group>"; };
65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtonHandler.swift; sourceTree = "<group>"; };
A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = "<group>"; };
DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = "<group>"; };
DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinPrecisionManager.swift; sourceTree = "<group>"; };
DD026E5A2EA2C9C300A39CB5 /* InsulinFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinFormatter.swift; sourceTree = "<group>"; };
DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGSection.swift; sourceTree = "<group>"; };
DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBGCondition.swift; sourceTree = "<group>"; };
DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBgAlarmEditor.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1505,6 +1509,8 @@
FCC688542489367300A0279D /* Helpers */ = {
isa = PBXGroup;
children = (
DD026E5A2EA2C9C300A39CB5 /* InsulinFormatter.swift */,
DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */,
656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */,
DD4A407D2E6AFEE6007B318B /* AuthService.swift */,
DD1D52B82E1EB5DC00432050 /* TabPosition.swift */,
Expand Down Expand Up @@ -1971,6 +1977,7 @@
DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */,
DDEF503A2D31615000999A5D /* LogManager.swift in Sources */,
DD4878172C7B75350048F05C /* BolusView.swift in Sources */,
DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */,
DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */,
DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */,
FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */,
Expand Down Expand Up @@ -2085,6 +2092,7 @@
DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */,
DD58171C2D299F940041FB98 /* BluetoothDevice.swift in Sources */,
DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */,
DD026E5B2EA2C9C300A39CB5 /* InsulinFormatter.swift in Sources */,
DD5334B02D1447C500CDD6EA /* BLEManager.swift in Sources */,
DD4878032C7B297E0048F05C /* StorageValue.swift in Sources */,
DD4878192C7C56D60048F05C /* TrioNightscoutRemoteController.swift in Sources */,
Expand Down
8 changes: 8 additions & 0 deletions LoopFollow/Controllers/Nightscout/DeviceStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import Charts
import Foundation
import HealthKit
import UIKit

extension MainViewController {
Expand Down Expand Up @@ -96,6 +97,13 @@ extension MainViewController {
.withDashSeparatorInDate,
.withColonSeparatorInTime]
if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String: AnyObject]? {
if let bolusIncrement = lastPumpRecord["bolusIncrement"] as? Double, bolusIncrement > 0 {
Storage.shared.bolusIncrement.value = HKQuantity(unit: .internationalUnit(), doubleValue: bolusIncrement)
Storage.shared.bolusIncrementDetected.value = true
} else {
Storage.shared.bolusIncrementDetected.value = false
}

if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 {
if let reservoirData = lastPumpRecord["reservoir"] as? Double {
latestPumpVolume = reservoirData
Expand Down
9 changes: 9 additions & 0 deletions LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ extension MainViewController {
updatedTime = parsedTime
let formattedTime = Localizer.formatTimestampToLocalString(parsedTime)
infoManager.updateInfoData(type: .updated, value: formattedTime)
Observable.shared.enactedOrSuggested.value = updatedTime
}

// ISF
Expand Down Expand Up @@ -122,6 +123,14 @@ extension MainViewController {
infoManager.updateInfoData(type: .autosens, value: formattedSens)
}

// Recommended Bolus
if let rec = InsulinMetric(from: lastLoopRecord, key: "recommendedBolus") {
infoManager.updateInfoData(type: .recBolus, value: rec)
Observable.shared.deviceRecBolus.value = rec.value
} else {
Observable.shared.deviceRecBolus.value = nil
}

// Eventual BG
if let eventualBGValue = enactedOrSuggested["eventualBG"] as? Double {
let eventualBGQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: eventualBGValue)
Expand Down
2 changes: 1 addition & 1 deletion LoopFollow/Extensions/HKUnit+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension HKUnit {
case .millimolesPerLiter:
return 1
case .internationalUnit():
return 3
return InsulinPrecisionManager.shared.fractionDigits
default:
return 0
}
Expand Down
24 changes: 24 additions & 0 deletions LoopFollow/Helpers/InsulinFormatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// LoopFollow
// InsulinFormatter.swift

import Foundation
import HealthKit

final class InsulinFormatter {
static let shared = InsulinFormatter()
private let precision = InsulinPrecisionManager.shared
private init() {}

func string(_ q: HKQuantity) -> String {
string(q.doubleValue(for: .internationalUnit()))
}

func string(_ units: Double) -> String {
let fd = precision.fractionDigits
let nf = NumberFormatter()
nf.minimumFractionDigits = fd
nf.maximumFractionDigits = fd
nf.numberStyle = .decimal
return nf.string(from: NSNumber(value: units)) ?? String(format: "%.\(fd)f", units)
}
}
34 changes: 34 additions & 0 deletions LoopFollow/Helpers/InsulinPrecisionManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// LoopFollow
// InsulinPrecisionManager.swift

import Combine
import Foundation
import HealthKit

final class InsulinPrecisionManager: ObservableObject {
static let shared = InsulinPrecisionManager()

@Published private(set) var fractionDigits: Int = 3
private var cancellables = Set<AnyCancellable>()

private init() {
fractionDigits = Self.computeDigits(from: Storage.shared.bolusIncrement.value)

Storage.shared.bolusIncrement.$value
.map(Self.computeDigits(from:))
.removeDuplicates()
.receive(on: DispatchQueue.main)
.assign(to: &$fractionDigits)
}

private static func computeDigits(from q: HKQuantity) -> Int {
let step = max(0.001, q.doubleValue(for: .internationalUnit()))
if step >= 1 { return 0 }
var v = step
var d = 0
while d < 6 && abs(round(v) - v) > 1e-10 {
v *= 10; d += 1
}
return min(max(d, 0), 5)
}
}
3 changes: 2 additions & 1 deletion LoopFollow/Helpers/Views/HKQuantityInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ struct HKQuantityInputView: View {
var minValue: HKQuantity
var maxValue: HKQuantity
@FocusState.Binding var isFocused: Bool

var onValidationError: (String) -> Void

@ObservedObject private var insulinPrecision = InsulinPrecisionManager.shared

var body: some View {
HStack {
Text(label)
Expand Down
32 changes: 31 additions & 1 deletion LoopFollow/Remote/Settings/RemoteSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SwiftUI
struct RemoteSettingsView: View {
@ObservedObject var viewModel: RemoteSettingsViewModel
@ObservedObject private var device = Storage.shared.device
@ObservedObject var bolusIncrement = Storage.shared.bolusIncrement

@State private var showAlert: Bool = false
@State private var alertType: AlertType? = nil
Expand Down Expand Up @@ -109,6 +110,29 @@ struct RemoteSettingsView: View {
guardrailsSection
}

if !Storage.shared.bolusIncrementDetected.value {
Section(header: Text("Bolus Increment")) {
HStack {
Text("Increment")
Spacer()
TextFieldWithToolBar(
quantity: $bolusIncrement.value,
maxLength: 5,
unit: HKUnit.internationalUnit(),
allowDecimalSeparator: true,
minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.001),
maxValue: HKQuantity(unit: .internationalUnit(), doubleValue: 1),
onValidationError: { message in
handleValidationError(message)
}
)
.frame(width: 100)
Text("U")
.foregroundColor(.secondary)
}
}
}

// MARK: - User Information Section

if viewModel.remoteType != .none && viewModel.remoteType != .loopAPNS {
Expand Down Expand Up @@ -160,9 +184,12 @@ struct RemoteSettingsView: View {

Section(header: Text("Debug / Info")) {
Text("Device Token: \(Storage.shared.deviceToken.value)")
Text("Production Env.: \(Storage.shared.productionEnvironment.value ? "True" : "False")")
Text("APNS Environment: \(Storage.shared.productionEnvironment.value ? "Production" : "Development")")
Text("Team ID: \(Storage.shared.teamId.value ?? "")")
Text("Bundle ID: \(Storage.shared.bundleId.value)")
if Storage.shared.bolusIncrementDetected.value {
Text("Bolus Increment: \(Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit()), specifier: "%.3f") U")
}
}
}

Expand Down Expand Up @@ -260,6 +287,9 @@ struct RemoteSettingsView: View {
Text("TOTP Code: Invalid QR code URL")
.foregroundColor(.red)
}
if Storage.shared.bolusIncrementDetected.value {
Text("Bolus Increment: \(Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit()), specifier: "%.3f") U")
}
}

if viewModel.areTeamIdsDifferent {
Expand Down
Loading