Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
b083df9
[COASTAL-1291] plugin identifier is no longer class property (#92)
nhamming Sep 25, 2023
f4f3c4d
[COASTAL-1291] plugin identifier is no longer class property (#92)
nhamming Sep 25, 2023
9975401
Merge fixes
ps2 Sep 26, 2023
fec0c5c
Merge remote-tracking branch 'origin/dev' into ps2/LOOP-4735/cgm-even…
ps2 Sep 27, 2023
a8dd5ab
Merge remote-tracking branch 'origin/dev' into ps2/LOOP-4735/cgm-even…
ps2 Sep 27, 2023
df245e8
Merge pull request #93 from tidepool-org/ps2/LOOP-4735/cgm-event-store
ps2 Sep 27, 2023
6555a71
Merge pull request #93 from tidepool-org/ps2/LOOP-4735/cgm-event-store
ps2 Sep 27, 2023
f75856d
added testflight configuration (#94)
nhamming Oct 10, 2023
6cf2c22
added testflight configuration (#94)
nhamming Oct 10, 2023
96ca6cc
Update test for api change
ps2 Oct 24, 2023
103fb13
Update test for api change
ps2 Oct 24, 2023
a4d2d62
Merge pull request #95 from tidepool-org/ps2/LOOP-4665/algo-recommend…
ps2 Oct 24, 2023
d630579
Merge pull request #95 from tidepool-org/ps2/LOOP-4665/algo-recommend…
ps2 Oct 24, 2023
5b7a17b
Temporary preset activations moved out of LoopSettings. (#96)
ps2 Dec 19, 2023
9fd01cf
Temporary preset activations moved out of LoopSettings. (#96)
ps2 Dec 19, 2023
8bbb51d
[LOOP-4788] Fix Unit Tests for iOS 17
Camji55 Jan 16, 2024
5d582ad
[LOOP-4788] Fix Unit Tests for iOS 17
Camji55 Jan 16, 2024
57996fa
[LOOP-4788] Fix Unit Tests for iOS 17
Camji55 Jan 16, 2024
ea6095f
[LOOP-4788] Fix Unit Tests for iOS 17
Camji55 Jan 16, 2024
65ececa
LOOP-4781 Types moved to LoopAlgorithm (#98)
ps2 Mar 5, 2024
2b8dc51
LOOP-4781 Types moved to LoopAlgorithm (#98)
ps2 Mar 5, 2024
b4e9cb3
[LOOP-4801] adding pump inoperable (#101)
nhamming Jun 7, 2024
bbc2c54
[LOOP-4801] adding pump inoperable (#101)
nhamming Jun 7, 2024
b9b6940
LOOP-1169 - Upload device logs (#100)
ps2 Jun 10, 2024
a723ade
LOOP-1169 - Upload device logs (#100)
ps2 Jun 10, 2024
0aa876d
Upload temporary presets (#103)
ps2 Aug 27, 2024
0b9dbdb
Upload temporary presets (#103)
ps2 Aug 27, 2024
8e856dc
LOOP-4098 - Specify correct duration for basal segments (#104)
ps2 Sep 13, 2024
5caeee3
LOOP-4098 - Specify correct duration for basal segments (#104)
ps2 Sep 13, 2024
edf8616
[LOOP-5035] report time zone changes (#105)
nhamming Sep 18, 2024
e192e32
[LOOP-5035] report time zone changes (#105)
nhamming Sep 18, 2024
8f3457e
LOOP-5065 Device log upload fixes (#106)
ps2 Sep 18, 2024
b98c311
LOOP-5065 Device log upload fixes (#106)
ps2 Sep 18, 2024
2bdca0b
[LOOP-5035] Pump event time zone sync (#107)
nhamming Sep 23, 2024
78ed7b7
[LOOP-5035] Pump event time zone sync (#107)
nhamming Sep 23, 2024
aa23383
LOOP-5071 Overlay basal and automation history (#108)
ps2 Oct 2, 2024
54f12b5
LOOP-5071 Overlay basal and automation history (#108)
ps2 Oct 2, 2024
5df0cf8
Use continue button during onboarding (#110)
ps2 Oct 25, 2024
e7ad5fe
Use continue button during onboarding (#110)
ps2 Oct 25, 2024
4075197
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
65e5c77
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
752d049
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
98545f0
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
3b3216c
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
8c72563
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
5366d60
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
da6df5b
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
6da74c2
[PAL-818] plugin dependency (#112)
nhamming Oct 30, 2024
342c8e9
[PAL-818] plugin dependency (#112)
nhamming Oct 30, 2024
90f5430
Enable dsym production (#113)
ps2 Oct 31, 2024
feb23ad
Enable dsym production (#113)
ps2 Oct 31, 2024
78d5f70
[LOOP-5150] detect demo account (#114)
nhamming Nov 20, 2024
530723f
[LOOP-5150] detect demo account (#114)
nhamming Nov 20, 2024
ecae6f2
[LOOP-5153] Remove HealthKit dependency from LoopAlgorithm
Camji55 Nov 21, 2024
1c1ae33
[LOOP-5153] Remove HealthKit dependency from LoopAlgorithm
Camji55 Nov 21, 2024
1682de8
Merge branch 'dev' into cameron/LOOP-5153-algo-sans-hk
Camji55 Nov 21, 2024
ecf3b2e
Merge branch 'dev' into cameron/LOOP-5153-algo-sans-hk
Camji55 Nov 21, 2024
19158f3
[LOOP-5153] Remove HealthKit dependency from LoopAlgorithm
Camji55 Nov 22, 2024
afe3357
[LOOP-5153] Remove HealthKit dependency from LoopAlgorithm
Camji55 Nov 22, 2024
34da6bf
[LOOP-4754] Presets Storage
Camji55 Nov 28, 2024
a6c05ba
[LOOP-4754] Presets Storage
Camji55 Nov 28, 2024
3958200
[LOOP-5153] Remove HealthKit dependency from LoopAlgorithm
Camji55 Dec 4, 2024
eb2e501
[LOOP-5153] Remove HealthKit dependency from LoopAlgorithm
Camji55 Dec 4, 2024
ecb2d85
[LOOP-4754] Presets Storage
Camji55 Dec 6, 2024
5ae9a8b
[LOOP-4754] Presets Storage
Camji55 Dec 6, 2024
be8eb14
LOOP-5149 Handle zero duration dose (#117)
ps2 Mar 21, 2025
c3af3f8
LOOP-5149 Handle zero duration dose (#117)
ps2 Mar 21, 2025
69c0c84
Update for new preset type names (#118)
ps2 Apr 24, 2025
417f42c
Update for new preset type names (#118)
ps2 Apr 24, 2025
aaf3e3c
[LOOP-5295] decisionId on DoseEntry and PersistedPumpEvent
Camji55 Apr 29, 2025
256dcdc
[LOOP-5295] decisionId on DoseEntry and PersistedPumpEvent
Camji55 Apr 29, 2025
bdca0a6
Fix empty abbreviation encoding (#120)
ps2 May 22, 2025
9cbb85d
Fix empty abbreviation encoding (#120)
ps2 May 22, 2025
82929ca
Merge branch 'dev' into cameron/LOOP-5295-insulin-delivery-log
Camji55 May 28, 2025
37da3f5
Merge branch 'dev' into cameron/LOOP-5295-insulin-delivery-log
Camji55 May 28, 2025
0b5f1ab
Merge branch 'dev' into cameron/LOOP-5295-insulin-delivery-log
Camji55 May 30, 2025
3b0267a
Merge branch 'dev' into cameron/LOOP-5295-insulin-delivery-log
Camji55 May 30, 2025
320c03a
[LOOP-5295] decisionId on DoseEntry and PersistedPumpEvent
Camji55 Jun 6, 2025
d534339
[LOOP-5295] decisionId on DoseEntry and PersistedPumpEvent
Camji55 Jun 6, 2025
508cdde
Enable scheduled presets (#121)
ps2 Jul 29, 2025
a81a2bd
Enable scheduled presets (#121)
ps2 Jul 29, 2025
845589d
[LOOP-5405] Activity Presets Core
Camji55 Aug 20, 2025
52a8de4
[LOOP-5405] Activity Presets Core
Camji55 Aug 20, 2025
f849868
[LOOP-5405] Activity Presets Core
Camji55 Aug 21, 2025
390134b
[LOOP-5405] Activity Presets Core
Camji55 Aug 21, 2025
a6f9842
[LOOP-5405] Activity Presets Core
Camji55 Aug 21, 2025
a3aa317
[LOOP-5405] Activity Presets Core
Camji55 Aug 21, 2025
edec290
[LOOP-5405] Activity Presets Core
Camji55 Aug 22, 2025
21a9cb1
[LOOP-5405] Activity Presets Core
Camji55 Aug 22, 2025
af177f1
Abbreviation and preset name cannot be empty string (#124)
ps2 Sep 24, 2025
c82dffb
Abbreviation and preset name cannot be empty string (#124)
ps2 Sep 24, 2025
c3229fa
Merge tidepool/dev into tidepool-sync/2026-03-10
loopkitdev Mar 10, 2026
7b6a704
Restore import LoopAlgorithm in DoseEntry and DeviceLogUploader
loopkitdev Mar 13, 2026
babbfa9
Update TidepoolKit to latest dev (4f4747ff)
loopkitdev Mar 25, 2026
8c69327
Merge pull request #34 from LoopKit/translations
marionbarker Mar 28, 2026
33072a4
Update string catalogs from Xcode build after Tidepool sync
loopkitdev Apr 9, 2026
61c0417
Merge remote-tracking branch 'upstream/dev' into tidepool-sync/2026-0…
loopkitdev Apr 9, 2026
b6a2fcd
Merge remote-tracking branch 'origin/dev' into tidepool-sync/2026-05-11
ps2 May 11, 2026
6ba6b24
Merge tidepool/dev into tidepool-sync/2026-05-11
ps2 May 11, 2026
765bcb9
DoseEntry: dedupe duplicate import LoopAlgorithm
ps2 May 11, 2026
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
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "tidepool-org/LoopKit" "899f958b50dd22014d97c4730e44266ad8135805"
github "tidepool-org/TidepoolKit" "2a1858cf1040d8b01b6f7357551536417ad55b04"
github "tidepool-org/TidepoolKit" "4f4747ff647d836c5a27cc1b9c275e5717901e83"
258 changes: 251 additions & 7 deletions TidepoolService.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1640"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
134 changes: 134 additions & 0 deletions TidepoolServiceKit/DeviceLogUploader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//
// DeviceLogUploader.swift
// TidepoolServiceKit
//
// Created by Pete Schwamb on 5/28/24.
// Copyright © 2024 LoopKit Authors. All rights reserved.
//

import Foundation
import LoopAlgorithm
import LoopKit
import os.log
import TidepoolKit

/// Periodically uploads device logs in hourly chunks to backend
actor DeviceLogUploader {
private let log = OSLog(category: "DeviceLogUploader")

private let api: TAPI

private var delegate: RemoteDataServiceDelegate?

private var logChunkDuration = TimeInterval(hours: 1)

private let backfillLimitInterval = TimeInterval(days: 2)


func setDelegate(_ delegate: RemoteDataServiceDelegate?) {
self.delegate = delegate
}

init(api: TAPI) {
self.api = api

Task {
await main()
}
}

func main() async {
var nextLogStart: Date?

// Start upload loop
while true {
if nextLogStart == nil {
do {
nextLogStart = try await getMostRecentUploadEndTime()
} catch {
log.error("Unable to fetch device log metadata: %{public}@", String(describing: error))
}
}

if nextLogStart != nil {
let nextLogEnd = nextLogStart!.addingTimeInterval(logChunkDuration)
let timeUntilNextUpload = nextLogEnd.timeIntervalSinceNow
if timeUntilNextUpload > 0 {
log.debug("Waiting %{public}@s until next upload", String(timeUntilNextUpload))
try? await Task.sleep(nanoseconds: timeUntilNextUpload.nanoseconds)
}
do {
try await upload(from: nextLogStart!, to: nextLogEnd)
nextLogStart = nextLogEnd
} catch {
log.error("Upload failed: %{public}@", String(describing: error))
// Upload failed, retry in 5 minutes.
try? await Task.sleep(nanoseconds: TimeInterval(minutes: 5).nanoseconds)
}
} else {
// Haven't been able to talk to backend to find any previous log uploads. Retry in 15 minutes.
try? await Task.sleep(nanoseconds: TimeInterval(minutes: 15).nanoseconds)
}
}
}

func getMostRecentUploadEndTime() async throws -> Date {
var uploadMetadata = try await api.listDeviceLogs(start: Date().addingTimeInterval(-backfillLimitInterval), end: Date())
uploadMetadata.sort { a, b in
return a.endAtTime < b.endAtTime
}
if let lastEnd = uploadMetadata.last?.endAtTime {
return lastEnd
} else {
// No previous uploads found in last two days
return Date().addingTimeInterval(-backfillLimitInterval).dateFlooredToTimeInterval(logChunkDuration)
}
}

func upload(from start: Date, to end: Date) async throws {
if let logs = try await delegate?.fetchDeviceLogs(startDate: start, endDate: end) {
if logs.count > 0 {
let data = logs.map({
entry in
TDeviceLogEntry(
type: entry.type.tidepoolType,
managerIdentifier: entry.managerIdentifier,
deviceIdentifier: entry.deviceIdentifier ?? "unknown",
timestamp: entry.timestamp,
message: entry.message
)
})
let metatdata = try await api.uploadDeviceLogs(logs: data, start: start, end: end)
log.debug("Uploaded %d entries from %{public}@ to %{public}@", logs.count, String(describing: start), String(describing: end))
log.debug("metadata: %{public}@", String(describing: metatdata))
} else {
log.debug("No device log entries from %{public}@ to %{public}@", String(describing: start), String(describing: end))
}
}
}
}

extension TimeInterval {
var nanoseconds: UInt64 {
return UInt64(self * 1e+9)
}
}

extension DeviceLogEntryType {
var tidepoolType: TDeviceLogEntry.TDeviceLogEntryType {
switch self {
case .send:
return .send
case .receive:
return .receive
case .error:
return .error
case .delegate:
return .delegate
case .delegateResponse:
return .delegateResponse
case .connection:
return .connection
}
}
}
208 changes: 199 additions & 9 deletions TidepoolServiceKit/Extensions/DoseEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright © 2022 LoopKit Authors. All rights reserved.
//

import LoopAlgorithm
import LoopKit
import TidepoolKit

Expand Down Expand Up @@ -82,8 +83,8 @@ extension DoseEntry: IdentifiableDatum {
payload["deliveredUnits"] = datumBasalDeliveredUnits

var datum = TAutomatedBasalDatum(time: datumTime,
duration: !isMutable ? datumDuration : 0,
expectedDuration: !isMutable && datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil,
duration: datumDuration,
expectedDuration: nil,
rate: datumScheduledBasalRate,
scheduleName: StoredSettings.activeScheduleNameDefault,
insulinFormulation: datumInsulinFormulation)
Expand All @@ -96,11 +97,9 @@ extension DoseEntry: IdentifiableDatum {
}

private func dataForBolus(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] {
// TODO: revert to using .insulin datum type once fully supported in Tidepool frontend
// if manuallyEntered {
// return dataForBolusManuallyEntered(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
// } else
if automatic != true {
if manuallyEntered {
return dataForBolusManuallyEntered(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
} else if automatic != true {
return dataForBolusManual(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
} else {
return dataForBolusAutomatic(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
Expand Down Expand Up @@ -209,8 +208,8 @@ extension DoseEntry: IdentifiableDatum {
payload["deliveredUnits"] = deliveredUnits

var datum = TAutomatedBasalDatum(time: datumTime,
duration: !isMutable ? datumDuration : 0,
expectedDuration: !isMutable && datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil,
duration: datumDuration,
expectedDuration: datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil,
rate: datumRate,
scheduleName: StoredSettings.activeScheduleNameDefault,
insulinFormulation: datumInsulinFormulation)
Expand Down Expand Up @@ -347,3 +346,194 @@ extension TNormalBolusDatum: TypedDatum {
extension TInsulinDatum: TypedDatum {
static var resolvedType: String { TDatum.DatumType.insulin.rawValue }
}

extension DoseEntry {

/// Annotates a dose with the context of a history of scheduled basal rates
///
/// If the dose crosses a schedule boundary, it will be split into multiple doses so each dose has a
/// single scheduled basal rate.
///
/// - Parameter basalHistory: The history of basal schedule values to apply. Only schedule values overlapping the dose should be included.
/// - Returns: An array of annotated doses
fileprivate func annotated(with basalHistory: [AbsoluteScheduleValue<Double>]) -> [DoseEntry] {

guard type == .tempBasal || type == .suspend, !basalHistory.isEmpty else {
return [self]
}

if type == .suspend {
guard value == 0 else {
preconditionFailure("suspend with non-zero delivery")
}
} else {
guard unit != .units else {
preconditionFailure("temp basal without rate unsupported")
}
}

if isMutable {
var newDose = self
let basal = basalHistory.first!
newDose.scheduledBasalRate = LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: basal.value)
return [newDose]
}

var doses: [DoseEntry] = []

for (index, basalItem) in basalHistory.enumerated() {
let startDate: Date
let endDate: Date

if index == 0 {
startDate = self.startDate
} else {
startDate = basalItem.startDate
}

if index == basalHistory.count - 1 {
endDate = self.endDate
} else {
endDate = basalHistory[index + 1].startDate
}

let segmentStartDate = max(startDate, self.startDate)
let segmentEndDate = max(startDate, min(endDate, self.endDate))
let segmentDuration = segmentEndDate.timeIntervalSince(segmentStartDate)
let segmentPortion = (segmentDuration / duration)

var annotatedDose = self
annotatedDose.startDate = segmentStartDate
annotatedDose.endDate = segmentEndDate
annotatedDose.scheduledBasalRate = LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: basalItem.value)

if let deliveredUnits {
annotatedDose.deliveredUnits = deliveredUnits * segmentPortion
}

doses.append(annotatedDose)
}

if doses.count > 1 {
for (index, dose) in doses.enumerated() {
if let originalIdentifier = dose.syncIdentifier, index>0 {
doses[index].syncIdentifier = originalIdentifier + "\(index+1)/\(doses.count)"
}
}
}

return doses
}

}


extension Collection where Element == DoseEntry {

/// Annotates a sequence of dose entries with the configured basal history
///
/// Doses which cross time boundaries in the basal rate schedule are split into multiple entries.
///
/// - Parameter basalHistory: A history of basal rates covering the timespan of these doses.
/// - Returns: An array of annotated dose entries
public func annotated(with basalHistory: [AbsoluteScheduleValue<Double>]) -> [DoseEntry] {
var annotatedDoses: [DoseEntry] = []

for dose in self {
let basalItems = basalHistory.filterDateRange(dose.startDate, dose.endDate)
annotatedDoses += dose.annotated(with: basalItems)
}

return annotatedDoses
}


/// Assigns an automation status to any dose where automation is not already specified
///
/// - Parameters:
/// - automationHistory: A history of automation periods.
/// - Returns: An array of doses, with the automation flag set based on automation history. Doses will be split if the automation state changes mid-dose.

public func overlayAutomationHistory(
_ automationHistory: [AbsoluteScheduleValue<Bool>]
) -> [DoseEntry] {

guard count > 0 else {
return []
}

var newEntries = [DoseEntry]()

var automation = automationHistory

// Assume automation if doses start before automationHistory
if let firstAutomation = automation.first, firstAutomation.startDate > first!.startDate {
automation.insert(AbsoluteScheduleValue(startDate: first!.startDate, endDate: firstAutomation.startDate, value: true), at: 0)
}

// Overlay automation periods
func annotateDoseWithAutomation(dose: DoseEntry) {

var addedCount = 0
for period in automation {
if period.endDate > dose.startDate && period.startDate < dose.endDate {
var newDose = dose

if dose.isMutable {
newDose.automatic = period.value
newEntries.append(newDose)
return
}

newDose.startDate = Swift.max(period.startDate, dose.startDate)
newDose.endDate = Swift.min(period.endDate, dose.endDate)
if let delivered = dose.deliveredUnits {
if dose.duration == 0 || delivered == 0 {
newDose.deliveredUnits = dose.deliveredUnits
} else {
newDose.deliveredUnits = newDose.duration / dose.duration * delivered
}
}
newDose.automatic = period.value
if addedCount > 0 {
newDose.syncIdentifier = "\(dose.syncIdentifierAsString)\(addedCount+1)"
}
newEntries.append(newDose)
addedCount += 1
}
}
if addedCount == 0 {
// automation history did not cover dose; mark automatic as default
var newDose = dose
newDose.automatic = true
newEntries.append(newDose)
}
}

for dose in self {
switch dose.type {
case .tempBasal, .basal, .suspend:
if dose.automatic == nil {
annotateDoseWithAutomation(dose: dose)
} else {
newEntries.append(dose)
}
default:
newEntries.append(dose)
break
}
}
return newEntries
}

}

extension DoseEntry {
var simpleDesc: String {
let seconds = Int(duration)
let automatic = automatic?.description ?? "na"
return "\(startDate) (\(seconds)s) - \(type) - isMutable:\(isMutable) automatic:\(automatic) value:\(value) delivered:\(String(describing: deliveredUnits)) scheduled:\(String(describing: scheduledBasalRate)) syncId:\(String(describing: syncIdentifier))"
}
}


Loading