|
@@ -13,12 +13,35 @@ protocol APSManager {
|
|
|
var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
|
|
var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
|
|
|
var pumpName: CurrentValueSubject<String, Never> { get }
|
|
var pumpName: CurrentValueSubject<String, Never> { get }
|
|
|
var isLooping: CurrentValueSubject<Bool, Never> { get }
|
|
var isLooping: CurrentValueSubject<Bool, Never> { get }
|
|
|
- var lastLoopDate: PassthroughSubject<Date, Never> { get }
|
|
|
|
|
|
|
+ var lastLoopDate: Date { get }
|
|
|
|
|
+ var lastLoopDateSubject: PassthroughSubject<Date, Never> { get }
|
|
|
|
|
+ var bolusProgress: CurrentValueSubject<Decimal?, Never> { get }
|
|
|
var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
|
|
var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
|
|
|
func enactTempBasal(rate: Double, duration: TimeInterval)
|
|
func enactTempBasal(rate: Double, duration: TimeInterval)
|
|
|
func makeProfiles() -> AnyPublisher<Bool, Never>
|
|
func makeProfiles() -> AnyPublisher<Bool, Never>
|
|
|
func determineBasal() -> AnyPublisher<Bool, Never>
|
|
func determineBasal() -> AnyPublisher<Bool, Never>
|
|
|
func roundBolus(amount: Decimal) -> Decimal
|
|
func roundBolus(amount: Decimal) -> Decimal
|
|
|
|
|
+ var lastError: CurrentValueSubject<Error?, Never> { get }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+enum APSError: LocalizedError {
|
|
|
|
|
+ case pumpError(Error)
|
|
|
|
|
+ case invalidPumpState(message: String)
|
|
|
|
|
+ case glucoseError(message: String)
|
|
|
|
|
+ case apsError(message: String)
|
|
|
|
|
+
|
|
|
|
|
+ var errorDescription: String? {
|
|
|
|
|
+ switch self {
|
|
|
|
|
+ case let .pumpError(error):
|
|
|
|
|
+ return "Pump error: \(error.localizedDescription)"
|
|
|
|
|
+ case let .invalidPumpState(message):
|
|
|
|
|
+ return "Invalid Pump State: \(message)"
|
|
|
|
|
+ case let .glucoseError(message):
|
|
|
|
|
+ return "Invalid glucose: \(message)"
|
|
|
|
|
+ case let .apsError(message):
|
|
|
|
|
+ return "APS error: \(message)"
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
final class BaseAPSManager: APSManager, Injectable {
|
|
final class BaseAPSManager: APSManager, Injectable {
|
|
@@ -34,6 +57,11 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
@Injected() private var settingsManager: SettingsManager!
|
|
@Injected() private var settingsManager: SettingsManager!
|
|
|
@Injected() private var broadcaster: Broadcaster!
|
|
@Injected() private var broadcaster: Broadcaster!
|
|
|
@Persisted(key: "lastAutotuneDate") private var lastAutotuneDate: Date = .distantPast
|
|
@Persisted(key: "lastAutotuneDate") private var lastAutotuneDate: Date = .distantPast
|
|
|
|
|
+ @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
|
|
|
|
|
+ didSet {
|
|
|
|
|
+ lastLoopDateSubject.send(lastLoopDate)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
private var openAPS: OpenAPS!
|
|
private var openAPS: OpenAPS!
|
|
|
|
|
|
|
@@ -45,7 +73,10 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let isLooping = CurrentValueSubject<Bool, Never>(false)
|
|
let isLooping = CurrentValueSubject<Bool, Never>(false)
|
|
|
- let lastLoopDate = PassthroughSubject<Date, Never>()
|
|
|
|
|
|
|
+ let lastLoopDateSubject = PassthroughSubject<Date, Never>()
|
|
|
|
|
+ let lastError = CurrentValueSubject<Error?, Never>(nil)
|
|
|
|
|
+
|
|
|
|
|
+ let bolusProgress = CurrentValueSubject<Decimal?, Never>(nil)
|
|
|
|
|
|
|
|
var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
|
|
var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
|
|
|
deviceDataManager.pumpDisplayState
|
|
deviceDataManager.pumpDisplayState
|
|
@@ -72,11 +103,20 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
|
|
|
|
|
private func subscribe() {
|
|
private func subscribe() {
|
|
|
deviceDataManager.recommendsLoop
|
|
deviceDataManager.recommendsLoop
|
|
|
|
|
+ .receive(on: processQueue)
|
|
|
.sink { [weak self] in
|
|
.sink { [weak self] in
|
|
|
self?.fetchAndLoop()
|
|
self?.fetchAndLoop()
|
|
|
}
|
|
}
|
|
|
.store(in: &lifetime)
|
|
.store(in: &lifetime)
|
|
|
pumpManager?.addStatusObserver(self, queue: processQueue)
|
|
pumpManager?.addStatusObserver(self, queue: processQueue)
|
|
|
|
|
+
|
|
|
|
|
+ deviceDataManager.errorSubject
|
|
|
|
|
+ .receive(on: processQueue)
|
|
|
|
|
+ .map { APSError.pumpError($0) }
|
|
|
|
|
+ .sink {
|
|
|
|
|
+ self.processError($0)
|
|
|
|
|
+ }
|
|
|
|
|
+ .store(in: &lifetime)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func heartbeat(date: Date, force: Bool) {
|
|
func heartbeat(date: Date, force: Bool) {
|
|
@@ -116,7 +156,7 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
self.enactSuggested()
|
|
self.enactSuggested()
|
|
|
} else {
|
|
} else {
|
|
|
self.isLooping.send(false)
|
|
self.isLooping.send(false)
|
|
|
- self.lastLoopDate.send(Date())
|
|
|
|
|
|
|
+ self.lastLoopDate = Date()
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
self.isLooping.send(false)
|
|
self.isLooping.send(false)
|
|
@@ -127,18 +167,21 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
private func verifyStatus() -> Bool {
|
|
private func verifyStatus() -> Bool {
|
|
|
guard let pump = pumpManager else {
|
|
guard let pump = pumpManager else {
|
|
|
debug(.apsManager, "Pump is not set")
|
|
debug(.apsManager, "Pump is not set")
|
|
|
|
|
+ processError(APSError.invalidPumpState(message: "Pump is not set"))
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
let status = pump.status.pumpStatus
|
|
let status = pump.status.pumpStatus
|
|
|
|
|
|
|
|
guard !status.bolusing, !status.suspended else {
|
|
guard !status.bolusing, !status.suspended else {
|
|
|
debug(.apsManager, "Pump is bolusing or suspended")
|
|
debug(.apsManager, "Pump is bolusing or suspended")
|
|
|
|
|
+ processError(APSError.invalidPumpState(message: "Pump is bolusing or suspended"))
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
|
|
let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
|
|
|
guard reservoir > 0 else {
|
|
guard reservoir > 0 else {
|
|
|
debug(.apsManager, "Reservoir is empty")
|
|
debug(.apsManager, "Reservoir is empty")
|
|
|
|
|
+ processError(APSError.invalidPumpState(message: "Reservoir is empty"))
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -160,12 +203,14 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
func determineBasal() -> AnyPublisher<Bool, Never> {
|
|
func determineBasal() -> AnyPublisher<Bool, Never> {
|
|
|
guard let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.count >= 36 else {
|
|
guard let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.count >= 36 else {
|
|
|
debug(.apsManager, "Not enough glucose data")
|
|
debug(.apsManager, "Not enough glucose data")
|
|
|
|
|
+ processError(APSError.glucoseError(message: "Not enough glucose data"))
|
|
|
return Just(false).eraseToAnyPublisher()
|
|
return Just(false).eraseToAnyPublisher()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
|
|
let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
|
|
|
guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
|
|
guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
|
|
|
debug(.apsManager, "Glucose data is stale")
|
|
debug(.apsManager, "Glucose data is stale")
|
|
|
|
|
+ processError(APSError.glucoseError(message: "Glucose data is stale"))
|
|
|
return Just(false).eraseToAnyPublisher()
|
|
return Just(false).eraseToAnyPublisher()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -224,21 +269,28 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
return Decimal(pump.roundToSupportedBolusVolume(units: Double(amount)))
|
|
return Decimal(pump.roundToSupportedBolusVolume(units: Double(amount)))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private var bolusReporter: DoseProgressReporter?
|
|
|
|
|
+
|
|
|
func enactBolus(amount: Double, isSMB: Bool) {
|
|
func enactBolus(amount: Double, isSMB: Bool) {
|
|
|
guard let pump = pumpManager, verifyStatus() else { return }
|
|
guard let pump = pumpManager, verifyStatus() else { return }
|
|
|
|
|
|
|
|
let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
|
|
let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
|
|
|
- pump.enactBolus(units: roundedAmout, automatic: false) { result in
|
|
|
|
|
- switch result {
|
|
|
|
|
- case .success:
|
|
|
|
|
|
|
+
|
|
|
|
|
+ pump.enactBolus(units: roundedAmout, automatic: isSMB).sink { completion in
|
|
|
|
|
+ if case let .failure(error) = completion {
|
|
|
|
|
+ debug(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
|
|
|
|
|
+ self.processError(APSError.pumpError(error))
|
|
|
|
|
+ } else {
|
|
|
debug(.apsManager, "Bolus succeeded")
|
|
debug(.apsManager, "Bolus succeeded")
|
|
|
if !isSMB {
|
|
if !isSMB {
|
|
|
self.determineBasal().sink { _ in }.store(in: &self.lifetime)
|
|
self.determineBasal().sink { _ in }.store(in: &self.lifetime)
|
|
|
}
|
|
}
|
|
|
- case let .failure(error):
|
|
|
|
|
- debug(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
|
|
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
+
|
|
|
|
|
+ self.bolusReporter = pump.createBolusProgressReporter(reportingOn: self.processQueue)
|
|
|
|
|
+ self.bolusReporter?.addObserver(self)
|
|
|
|
|
+ } receiveValue: { _ in }
|
|
|
|
|
+ .store(in: &lifetime)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func enactTempBasal(rate: Double, duration: TimeInterval) {
|
|
func enactTempBasal(rate: Double, duration: TimeInterval) {
|
|
@@ -253,6 +305,7 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
|
|
self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
|
|
|
case let .failure(error):
|
|
case let .failure(error):
|
|
|
debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
|
|
debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
|
|
|
|
|
+ self.processError(APSError.pumpError(error))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -366,18 +419,21 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
|
|
guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
|
|
|
isLooping.send(false)
|
|
isLooping.send(false)
|
|
|
debug(.apsManager, "Suggestion not found")
|
|
debug(.apsManager, "Suggestion not found")
|
|
|
|
|
+ processError(APSError.apsError(message: "Suggestion not found"))
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
|
|
guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
|
|
|
isLooping.send(false)
|
|
isLooping.send(false)
|
|
|
debug(.apsManager, "Suggestion expired")
|
|
debug(.apsManager, "Suggestion expired")
|
|
|
|
|
+ processError(APSError.apsError(message: "Suggestion expired"))
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
guard let pump = pumpManager, verifyStatus() else {
|
|
guard let pump = pumpManager, verifyStatus() else {
|
|
|
isLooping.send(false)
|
|
isLooping.send(false)
|
|
|
- debug(.apsManager, "Invalid pump status")
|
|
|
|
|
|
|
+ debug(.apsManager, "Invalid pump state")
|
|
|
|
|
+ processError(APSError.invalidPumpState(message: "Pump is bolusing or suspended"))
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -409,13 +465,15 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
if case let .failure(error) = completion {
|
|
if case let .failure(error) = completion {
|
|
|
debug(.apsManager, "Loop failed with error: \(error.localizedDescription)")
|
|
debug(.apsManager, "Loop failed with error: \(error.localizedDescription)")
|
|
|
self?.reportEnacted(suggestion: suggested, received: false)
|
|
self?.reportEnacted(suggestion: suggested, received: false)
|
|
|
|
|
+ self?.processError(APSError.pumpError(error))
|
|
|
} else {
|
|
} else {
|
|
|
self?.reportEnacted(suggestion: suggested, received: true)
|
|
self?.reportEnacted(suggestion: suggested, received: true)
|
|
|
}
|
|
}
|
|
|
self?.isLooping.send(false)
|
|
self?.isLooping.send(false)
|
|
|
} receiveValue: {
|
|
} receiveValue: {
|
|
|
debug(.apsManager, "Loop succeeded")
|
|
debug(.apsManager, "Loop succeeded")
|
|
|
- self.lastLoopDate.send(Date())
|
|
|
|
|
|
|
+ self.lastError.send(nil)
|
|
|
|
|
+ self.lastLoopDate = Date()
|
|
|
}.store(in: &lifetime)
|
|
}.store(in: &lifetime)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -434,6 +492,11 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
nightscout.uploadStatus()
|
|
nightscout.uploadStatus()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private func processError(_ error: Error) {
|
|
|
|
|
+ debug(.apsManager, "\(error.localizedDescription)")
|
|
|
|
|
+ lastError.send(error)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private extension PumpManager {
|
|
private extension PumpManager {
|
|
@@ -502,6 +565,17 @@ extension BaseAPSManager: PumpManagerStatusObserver {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+extension BaseAPSManager: DoseProgressObserver {
|
|
|
|
|
+ func doseProgressReporterDidUpdate(_ doseProgressReporter: DoseProgressReporter) {
|
|
|
|
|
+ bolusProgress.send(Decimal(doseProgressReporter.progress.percentComplete))
|
|
|
|
|
+ if doseProgressReporter.progress.isComplete {
|
|
|
|
|
+ bolusReporter?.removeObserver(self)
|
|
|
|
|
+ bolusReporter = nil
|
|
|
|
|
+ bolusProgress.send(nil)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
extension PumpManagerStatus {
|
|
extension PumpManagerStatus {
|
|
|
var pumpStatus: PumpStatus {
|
|
var pumpStatus: PumpStatus {
|
|
|
let bolusing = bolusState != .noBolus
|
|
let bolusing = bolusState != .noBolus
|