Skip to content
Open
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
2 changes: 1 addition & 1 deletion Config.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group

// The developers set the version numbers, please leave them alone
APP_VERSION = 1.0.2
APP_DEV_VERSION = 1.0.2.66
APP_DEV_VERSION = 1.0.2.71
APP_BUILD_NUMBER = 1
COPYRIGHT_NOTICE = autoISF 3.01

Expand Down
13 changes: 2 additions & 11 deletions Trio Watch App Extension/Views/AcknowledgementPendingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,8 @@ struct AcknowledgementPendingView: View {
.navigationBarBackButtonHidden(true)
.toolbar(.hidden)
.background(trioBackgroundColor)
.onChange(of: state.showCommsAnimation) { oldValue, newValue in
if newValue && !oldValue {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
// If after 5 seconds there is still no acknowledgement banner, return to root
if !state.showAcknowledgmentBanner {
// Navigate back to the root
navigationPath.removeLast(navigationPath.count)
}
}
}
}
// WatchState times out a missing answer by showing a banner (beginAwaitingAcknowledgment), so every
// request ends here. A timer in this view never started: the flag is already true when the view appears.
.onChange(of: state.showAcknowledgmentBanner) { _, newValue in
if !newValue {
// Navigate back to the root when acknowledgment banner disappears
Expand Down
15 changes: 3 additions & 12 deletions Trio Watch App Extension/Views/BolusConfirmationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,7 @@ struct BolusConfirmationView: View {
Spacer()

Button("Cancel") {
if state.carbsAmount > 0 {
state.carbsAmount = 0 // reset carbs in state
}
bolusAmount = 0 // reset bolus in state
confirmationProgress = 0 // reset auth progress
state.clearEntry()
navigationPath.removeLast(navigationPath.count)
}
.buttonStyle(.bordered)
Expand All @@ -80,13 +76,8 @@ struct BolusConfirmationView: View {
WKInterfaceDevice.current().play(.success)

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if state.carbsAmount > 0 {
state.sendCarbsRequest(state.carbsAmount, Date())
state.carbsAmount = 0 // reset carbs in state
}
state.sendBolusRequest(Decimal(bolusAmount))
bolusAmount = 0 // reset bolus in state
confirmationProgress = 0 // reset auth progress
// Sends carbs and bolus, or refuses an entry older than 2 min; either way the entry is cleared
state.sendConfirmedEntry(bolusAmount: bolusAmount)
navigationPath.append(NavigationDestinations.acknowledgmentPending)
}
} else if newValue > 0 {
Expand Down
6 changes: 4 additions & 2 deletions Trio Watch App Extension/Views/BolusInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,7 @@ struct BolusInputView: View {

Button(actionButtonLabel) {
if isCarbsOnly {
state.sendCarbsRequest(state.carbsAmount)
state.carbsAmount = 0 // reset carbs in state
state.sendCarbsEntry(state.carbsAmount)
navigationPath.append(NavigationDestinations.acknowledgmentPending)
} else {
state.bolusAmount = min(bolusAmount, effectiveBolusLimit)
Expand Down Expand Up @@ -156,7 +155,10 @@ struct BolusInputView: View {
.clipShape(Circle())
}
}
// The entry's 2 min clock restarts whenever the amount changes, including a recommendation landing
.onChange(of: bolusAmount) { state.markEntryEdited() }
.onAppear {
state.markEntryEdited()
// Set initial bolus amount to recommended value
// Only do this if user has not updated amount previously, e.g., when navigating to next and then back to this view
if bolusAmount == 0 {
Expand Down
5 changes: 4 additions & 1 deletion Trio Watch App Extension/Views/CarbsInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ struct CarbsInputView: View {
state.carbsAmount = Int(min(carbsAmount, effectiveCarbsLimit))
navigationPath.append(NavigationDestinations.bolusInput)
} else {
state.sendCarbsRequest(Int(min(carbsAmount, effectiveCarbsLimit)))
state.sendCarbsEntry(Int(min(carbsAmount, effectiveCarbsLimit)))
navigationPath.append(NavigationDestinations.acknowledgmentPending)
}
}
Expand All @@ -103,6 +103,9 @@ struct CarbsInputView: View {
.disabled(!(carbsAmount > 0.0) || carbsAmount > effectiveCarbsLimit)
}
.background(trioBackgroundColor)
// The entry's 2 min clock restarts whenever the user touches it
.onAppear { state.markEntryEdited() }
.onChange(of: carbsAmount) { state.markEntryEdited() }
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Image("premeal")
Expand Down
10 changes: 10 additions & 0 deletions Trio Watch App Extension/Views/TrioMainWatchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI
import WatchKit

struct TrioMainWatchView: View {
@Environment(\.scenePhase) private var scenePhase
@State private var state = WatchState()

// misc
Expand Down Expand Up @@ -233,6 +234,15 @@ struct TrioMainWatchView: View {
if newPath.isEmpty {
// Reset conditional view navigation when returning to root view
continueToBolus = false
// Back on the main screen, so no entry is in progress
state.clearEntry()
}
}
.onChange(of: scenePhase) { _, newPhase in
// An entry left for more than 2 min (wrist down, app in the background) is
// cleared and the input screens are closed, so a stale amount can't be confirmed
if newPhase == .active, state.expireStaleEntry() {
navigationPath = NavigationPath()
}
}
}
Expand Down
164 changes: 88 additions & 76 deletions Trio Watch App Extension/WatchState+Requests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,138 +4,150 @@ import WatchConnectivity
// MARK: - Send Data to Phone

extension WatchState {
/// Sends the entry confirmed with the crown: its carbs, if any, then the bolus.
/// Refuses an entry older than `entryLifetime` and clears it instead.
func sendConfirmedEntry(bolusAmount amount: Double, at now: Date = Date()) {
guard refuseIfExpired(at: now) == false else { return }

let carbs = carbsAmount
clearEntry()
if carbs > 0 {
sendCarbsRequest(carbs, now)
}
sendBolusRequest(Decimal(amount))
}

/// Sends a carbs-only entry ("Log Carbs"). Refuses an entry older than `entryLifetime` and clears it instead.
func sendCarbsEntry(_ amount: Int, at now: Date = Date()) {
guard refuseIfExpired(at: now) == false else { return }

clearEntry()
sendCarbsRequest(amount, now)
}

private func refuseIfExpired(at now: Date) -> Bool {
guard !isEntryFresh(at: now) else { return false }
WatchLogger.shared.warning(.treatments, "Entry older than \(Int(Self.entryLifetime / 60)) min not sent")
clearEntry()
handleRequestNotSent(Self.entryExpiredMessage)
return true
}

/// Sends a bolus insulin request to the paired iPhone
/// - Parameters:
/// - amount: The insulin amount to be delivered
func sendBolusRequest(_ amount: Decimal) {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.bolus, "Bolus request not sent, phone unreachable")
guard amount > 0 else {
WatchLogger.shared.warning(.bolus, "Bolus of \(amount) U not sent, amount must be above 0")
handleRequestNotSent(Self.invalidBolusAmountMessage)
return
}

WatchLogger.shared.info(.bolus, "Bolus of \(amount) U requested")

let message: [String: Any] = [
WatchMessageKeys.bolus: amount
]
guard let session = reachableSession(.bolus, notSentLog: "Bolus request not sent, phone unreachable") else { return }

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.bolus, "Sending bolus request failed", error: error)
}
WatchLogger.shared.info(.bolus, "Bolus of \(amount) U requested")

// Display pending communication animation
showCommsAnimation = true
send([WatchMessageKeys.bolus: amount], on: session, .bolus, failureLog: "Sending bolus request failed")
}

/// Sends a carbohydrate entry request to the paired iPhone
/// - Parameters:
/// - amount: The amount of carbs in grams
/// - date: The timestamp for the carb entry (defaults to current time)
func sendCarbsRequest(_ amount: Int, _ date: Date = Date()) {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.treatments, "Carbs request not sent, phone unreachable")
return
}
guard let session = reachableSession(.treatments, notSentLog: "Carbs request not sent, phone unreachable") else { return }

WatchLogger.shared.info(.treatments, "Carbs of \(amount) g requested for \(date)")

let message: [String: Any] = [
WatchMessageKeys.carbs: amount,
WatchMessageKeys.date: date.timeIntervalSince1970
]

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.treatments, "Sending carbs request failed", error: error)
}

// Display pending communication animation
showCommsAnimation = true
send(message, on: session, .treatments, failureLog: "Sending carbs request failed")
}

/// Sends a request to cancel the current override preset to the paired iPhone
func sendCancelOverrideRequest() {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.treatments, "Override cancel not sent, phone unreachable")
return
}
guard let session = reachableSession(.treatments, notSentLog: "Override cancel not sent, phone unreachable")
else { return }

WatchLogger.shared.info(.treatments, "Override cancel requested")

let message: [String: Any] = [
WatchMessageKeys.cancelOverride: true
]

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.treatments, "Sending override cancel failed", error: error)
}

// Display pending communication animation
showCommsAnimation = true
send([WatchMessageKeys.cancelOverride: true], on: session, .treatments, failureLog: "Sending override cancel failed")
}

/// Sends a request to activate an override preset to the paired iPhone
/// - Parameter presetName: The name of the override preset to activate
func sendActivateOverrideRequest(presetName: String) {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.treatments, "Override request not sent, phone unreachable")
return
}
guard let session = reachableSession(.treatments, notSentLog: "Override request not sent, phone unreachable")
else { return }

WatchLogger.shared.info(.treatments, "Override preset \(presetName) requested")

let message: [String: Any] = [
WatchMessageKeys.activateOverride: presetName
]

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.treatments, "Sending override request failed", error: error)
}

// Display pending communication animation
showCommsAnimation = true
send(
[WatchMessageKeys.activateOverride: presetName],
on: session,
.treatments,
failureLog: "Sending override request failed"
)
}

/// Sends a request to cancel the current temporary target to the paired iPhone
func sendCancelTempTargetRequest() {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.treatments, "Temp target cancel not sent, phone unreachable")
return
}
guard let session = reachableSession(.treatments, notSentLog: "Temp target cancel not sent, phone unreachable")
else { return }

WatchLogger.shared.info(.treatments, "Temp target cancel requested")

let message: [String: Any] = [
WatchMessageKeys.cancelTempTarget: true
]

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.treatments, "Sending temp target cancel failed", error: error)
}

// Display pending communication animation
showCommsAnimation = true
send(
[WatchMessageKeys.cancelTempTarget: true],
on: session,
.treatments,
failureLog: "Sending temp target cancel failed"
)
}

/// Sends a request to activate a temporary target preset to the paired iPhone
/// - Parameter presetName: The name of the temporary target preset to activate
func sendActivateTempTargetRequest(presetName: String) {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(.treatments, "Temp target request not sent, phone unreachable")
return
}
guard let session = reachableSession(.treatments, notSentLog: "Temp target request not sent, phone unreachable")
else { return }

WatchLogger.shared.info(.treatments, "Temp target preset \(presetName) requested")

let message: [String: Any] = [
WatchMessageKeys.activateTempTarget: presetName
]
send(
[WatchMessageKeys.activateTempTarget: presetName],
on: session,
.treatments,
failureLog: "Sending temp target request failed"
)
}

session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(.treatments, "Sending temp target request failed", error: error)
/// The session when the phone can take a request; otherwise tells the user nothing was sent
private func reachableSession(_ category: WatchLogEntry.Category, notSentLog: String) -> WCSession? {
guard let session = session, session.isReachable else {
WatchLogger.shared.warning(category, notSentLog)
handleRequestNotSent(Self.phoneUnreachableMessage)
return nil
}
return session
}

/// Sends a treatment request and waits for the phone's acknowledgment
private func send(
_ message: [String: Any],
on session: WCSession,
_ category: WatchLogEntry.Category,
failureLog: String
) {
beginAwaitingAcknowledgment()

// Display pending communication animation
showCommsAnimation = true
session.sendMessage(message, replyHandler: nil) { error in
WatchLogger.shared.error(category, failureLog, error: error)
DispatchQueue.main.async {
self.handleMissingAcknowledgment()
}
}
}

/// Sends a request to calculate a bolus recommendation based on the current carbs amount
Expand Down
Loading
Loading