Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eeb9c93
Various OmniBLEParser updates and improvements
itsmojo Nov 17, 2024
366e9ec
Miscellaneous Omnipod code improvements & cleanup
itsmojo Nov 18, 2024
b167114
Unacknowledged command handling fixes and PodCommsSession improvements
itsmojo Nov 18, 2024
f049bfd
Add missing pod suspend test improvement and log type change to PumpM…
itsmojo Nov 20, 2024
9622656
Don't fail on an ack comms error when a validated response has been r…
itsmojo Nov 21, 2024
e4435c0
Use alternate DASH type 7 getStatus call for standalone getStatus ses…
itsmojo Nov 26, 2024
e64d45e
Improved podInfoType switch statement layout
itsmojo Nov 29, 2024
ec90313
Improved commenting on constants that vary from Eros and Dash
itsmojo Dec 3, 2024
50d1e77
Update comments to use U/hr instead of U/H for better consistency
itsmojo Dec 4, 2024
640d987
Reworked to better handle newer Xcode log files with a separate metad…
itsmojo Dec 5, 2024
c5d5836
Remove the commented out mostly redundant and unneeded log.debug line
itsmojo Dec 8, 2024
a42a164
Merge pull request #134 from itsmojo/OmniBLEParser-updates
marionbarker Dec 10, 2024
6eaaea9
Merge pull request #135 from itsmojo/Omnipod-improvements
marionbarker Dec 10, 2024
f0986ca
Merge pull request #136 from itsmojo/unacknowledge-command-improvement
marionbarker Dec 10, 2024
e1ec43c
Merge pull request #137 from itsmojo/improved-PodMessageTransport-err…
marionbarker Dec 10, 2024
9804037
Merge branch 'dev' into alternate-DASH-getStatus
itsmojo Dec 10, 2024
fee22b3
Merge pull request #138 from itsmojo/alternate-DASH-getStatus
marionbarker Dec 10, 2024
add51c6
Improved & automatic unacknowledged command recovery
itsmojo Dec 23, 2024
54f180b
Use getStatus() more for unacknowledged command handling & simplified…
itsmojo Dec 24, 2024
47c062b
Rename resolveUnacknowledgedCommand() to tryToResolvePendingCommand()
itsmojo Dec 25, 2024
e2933d5
Merge pull request #139 from itsmojo/improved-unacknowledged-command-…
marionbarker Jan 15, 2025
c46470e
Logic fix for 049 pod fault with concurrent temp basal commands
itsmojo Jan 18, 2025
1fa2874
Merge pull request #140 from itsmojo/concurrent-TB-logic-fixes
marionbarker Jan 18, 2025
7ded2bc
fix for pump manager returns bogus podSuspended;
marionbarker Feb 5, 2025
b5b0452
revise some comments and remove extra debug logging from testing phase
marionbarker Feb 10, 2025
af506f2
use correct .communication errors instead of .state or .deviceState
marionbarker Feb 11, 2025
6f65cba
Merge pull request #141 from LoopKit/try_to_validate_comms
marionbarker Feb 12, 2025
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
6 changes: 2 additions & 4 deletions OmniBLE/OmnipodCommon/MessageBlocks/PodInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ public enum PodInfoResponseSubType: UInt8, Equatable {
case detailedStatus = 0x02 // Returns detailed pod status, returned for most calls after a pod fault
case pulseLogPlus = 0x03 // Returns up to the last 60 pulse log entries plus additional info
case activationTime = 0x05 // Returns pod activation time and possible fault code & fault time
case noSeqStatusResponse = 0x07 // DASH only, returns the normal status response w/o incrementing msg seq #
case noSeqStatus = 0x07 // DASH only, returns the normal status response w/o incrementing msg seq #
case pulseLogRecent = 0x50 // Returns the last 50 pulse log entries
case pulseLogPrevious = 0x51 // Like 0x50, but returns up to the previous 50 entries before the last 50

public var podInfoType: PodInfo.Type {
switch self {
case .normal:
case .normal, .noSeqStatus: // noSeqStatus won't increment the message seq # from the last response
return StatusResponse.self as! PodInfo.Type
case .triggeredAlerts:
return PodInfoTriggeredAlerts.self
Expand All @@ -38,8 +38,6 @@ public enum PodInfoResponseSubType: UInt8, Equatable {
return PodInfoPulseLogPlus.self
case .activationTime:
return PodInfoActivationTime.self
case .noSeqStatusResponse:
return StatusResponse.self as! PodInfo.Type
case .pulseLogRecent:
return PodInfoPulseLogRecent.self
case .pulseLogPrevious:
Expand Down
52 changes: 41 additions & 11 deletions OmniBLE/OmnipodCommon/Pod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,15 @@ public struct Pod {
public static let reservoirCapacity: Double = 200

// Supported basal rates
// Eros minimum scheduled basal rate is 0.05 U/H while for Dash supports 0 U/H.
// Would need to have this value based on productID to be able to share this file with Eros.
// Eros minimum scheduled basal rate is 0.05 U/hr while Dash supports 0 U/hr.
public static let supportedBasalRates: [Double] = (0...600).map { Double($0) / Double(pulsesPerUnit) }

// The internal basal rate used for non-Eros pods
// Would need to have this value based on productID to be able to share this file with Eros.
// Supported temp basal rates
// Both Eros and Dash support a minimum temp basal rate of 0 U/hr.
public static let supportedTempBasalRates: [Double] = (0...600).map { Double($0) / Double(pulsesPerUnit) }

// The internal basal rate used for zero basal rates
// Eros uses 0.0 while Dash uses a near zero rate
public static let zeroBasalRate: Double = nearZeroBasalRate

// Maximum number of basal schedule entries supported
Expand All @@ -85,13 +88,13 @@ public struct Pod {
public static let defaultExpirationReminderOffset = TimeInterval(hours: 2)
public static let expirationReminderAlertMinHoursBeforeExpiration = 1
public static let expirationReminderAlertMaxHoursBeforeExpiration = 24

// Threshold used to display pod end of life warnings
public static let timeRemainingWarningThreshold = TimeInterval(days: 1)

// Default low reservoir alert limit in Units
public static let defaultLowReservoirReminder: Double = 10

// Allowed Low Reservoir reminder values
public static let allowedLowReservoirReminderValues = Array(stride(from: 1, through: 50, by: 1))
}
Expand All @@ -111,19 +114,46 @@ public enum DeliveryStatus: UInt8, CustomStringConvertible {
case extendedBolusAndTempBasal = 10

public var suspended: Bool {
return self == .suspended || self == .priming || self == .extendedBolusWhileSuspended
// returns true if both the tempBasal and basal bits are clear
let suspendedStates: Set<DeliveryStatus> = [
.suspended,
.priming,
.extendedBolusWhileSuspended,
]
return suspendedStates.contains(self)
}

public var bolusing: Bool {
return self == .bolusInProgress || self == .bolusAndTempBasal || self == .extendedBolusRunning || self == .extendedBolusAndTempBasal || self == .priming || self == .extendedBolusWhileSuspended
// returns true if either the immediateBolus or extendedBolus bits are set
let bolusingStates: Set<DeliveryStatus> = [
.priming,
.bolusInProgress,
.bolusAndTempBasal,
.extendedBolusWhileSuspended,
.extendedBolusRunning,
.extendedBolusAndTempBasal,
]
return bolusingStates.contains(self)
}

public var tempBasalRunning: Bool {
return self == .tempBasalRunning || self == .bolusAndTempBasal || self == .extendedBolusAndTempBasal
// returns true if the tempBasal bit is set
let tempBasalRunningStates: Set<DeliveryStatus> = [
.tempBasalRunning,
.bolusAndTempBasal,
.extendedBolusAndTempBasal,
]
return tempBasalRunningStates.contains(self)
}

public var extendedBolusRunning: Bool {
return self == .extendedBolusRunning || self == .extendedBolusAndTempBasal || self == .extendedBolusWhileSuspended
// returns true if the extendedBolus bit is set
let extendedBolusRunningStates: Set<DeliveryStatus> = [
.extendedBolusWhileSuspended,
.extendedBolusRunning,
.extendedBolusAndTempBasal,
]
return extendedBolusRunningStates.contains(self)
}

public var description: String {
Expand Down
25 changes: 20 additions & 5 deletions OmniBLE/PumpManager/MessageTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ class PodMessageTransport: MessageTransport {
return try enDecrypt.encrypt(msg, nonceSeq)
}

func readAndAckResponse() throws -> Message {
private func readAndAckResponse() throws -> Message {
guard let enDecrypt = self.enDecrypt else { throw PodCommsError.podNotConnected }

let readResponse = try manager.readMessagePacket()
Expand All @@ -276,22 +276,37 @@ class PodMessageTransport: MessageTransport {
incrementNonceSeq()
let ack = try getAck(response: decrypted)
let ackResult = manager.sendMessagePacket(ack)
guard case .sentWithAcknowledgment = ackResult else {
throw PodProtocolError.messageIOException("Could not write $msgType: \(ackResult)")
}

// verify that the Omnipod message # matches the expected value
guard response.sequenceNum == messageNumber else {
throw MessageError.invalidSequence
}

switch ackResult {
case .sentWithAcknowledgment:
break
case .sentWithError, .unsentWithError:
// We had a communications error trying to send the response ack to the pod.
let ackErrStr = String(format: "Send of ack failed: %@", String(describing: ackResult))

// The original behavior here was to throw for this error which will throw out the verified response
// for a received pod command which forces the unacknowledged response code to try to resolve any insulin
// delivery related commands while treating other commands types as failures even though they were received.
// throw PodProtocolError.messageIOException(ackErrStr)

// Since we already have a fully verified response, simply log the ack comms error and return
// the received response since the pod has already accepted the command and provided its response.
// This results in less bogus failures on successfully received and handled pod commands and
// could result in a failure trying to send the next pod command but with less ill side effects.
log.error("%@, but still using validated response %@", ackErrStr, String(describing: response))
}

return response
}

private func parseResponse(decrypted: MessagePacket) throws -> Message {

let data = try StringLengthPrefixEncoding.parseKeys([RESPONSE_PREFIX], decrypted.payload)[0]
log.debug("Received decrypted response: %{public}@ in packet: %{public}@", data.hexadecimalString, decrypted.payload.hexadecimalString)

// Dash pods generates a CRC16 for Omnipod Messages, but the actual algorithm is not understood and doesn't match the CRC16
// that the pod enforces for incoming Omnipod command message. The Dash PDM explicitly ignores the CRC16 for incoming messages,
Expand Down
83 changes: 73 additions & 10 deletions OmniBLE/PumpManager/OmniBLEPumpManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,7 @@ extension OmniBLEPumpManager {
podComms.runSession(withName: "Resuming pod setup") { (result) in
switch result {
case .success(let session):
let status = try? session.getStatus()
let status = try? session.getStatus(noSeqGetStatus: true)
if status == nil {
self.log.debug("### Pod setup resume getStatus failed, sleeping %d seconds", sleepTime)
sleep(sleepTime)
Expand All @@ -1050,6 +1050,27 @@ extension OmniBLEPumpManager {
}
}

// If the last delivery status received is invalid or there is an unacknowledged command, execute a getStatus command
// for the current PodCommsSession. If the getStatus fails, return its error to be passed on to the higher level.
// Return nil if comms looks OK or the getStatus was successful.
private func tryToValidateComms(session: PodCommsSession) -> LocalizedError? {

// Since we're already connected for this session, if we have a delivery status and no unacknowledged command, return nil
if self.state.podState?.lastDeliveryStatusReceived != nil && self.state.podState?.unacknowledgedCommand == nil {
return nil
}

// Attempt to do a getStatus to try to resolve any outstanding comms issues
do {
let _ = try session.getStatus()
self.log.debug("### tryToValidateComms getStatus resolved all pending comms issues")
return nil
} catch let error {
self.log.debug("### tryToValidateComms getStatus failed, returning: %@", error.localizedDescription)
return error as? LocalizedError
}
}

// MARK: - Pump Commands

public func getPodStatus(completion: ((_ result: PumpManagerResult<StatusResponse>) -> Void)? = nil) {
Expand All @@ -1062,7 +1083,7 @@ extension OmniBLEPumpManager {
do {
switch result {
case .success(let session):
let status = try session.getStatus()
let status = try session.getStatus(noSeqGetStatus: true)
session.dosesForStorage({ (doses) -> Bool in
self.store(doses: doses, in: session)
})
Expand Down Expand Up @@ -1163,6 +1184,11 @@ extension OmniBLEPumpManager {
switch result {
case .success(let session):
do {
if let error = self.tryToValidateComms(session: session) {
completion(.communication(error))
return
}

let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
let _ = try session.setTime(timeZone: timeZone, basalSchedule: self.state.basalSchedule, date: Date(), acknowledgementBeep: beep)
self.clearSuspendReminder()
Expand Down Expand Up @@ -1216,6 +1242,11 @@ extension OmniBLEPumpManager {
do {
switch result {
case .success(let session):
if let error = self.tryToValidateComms(session: session) {
completion(error)
return
}

let scheduleOffset = timeZone.scheduleOffset(forDate: Date())
let result = session.cancelDelivery(deliveryType: .all)
switch result {
Expand Down Expand Up @@ -1454,6 +1485,11 @@ extension OmniBLEPumpManager {
self.podComms.runSession(withName: name) { (result) in
switch result {
case .success(let session):
if let error = self.tryToValidateComms(session: session) {
completion(.communication(error))
return
}

// enable/disable Pod completion beep state for any unfinalized manual insulin delivery
let enabled = newPreference.shouldBeepForManualCommand
let beepType: BeepType = enabled ? .bipBip : .noBeepNonCancel
Expand Down Expand Up @@ -1504,6 +1540,11 @@ extension OmniBLEPumpManager {
return
}

if let error = self.tryToValidateComms(session: session) {
completion(.communication(error))
return
}

guard let configuredAlerts = self.state.podState?.configuredAlerts,
let activeAlertSlots = self.state.podState?.activeAlertSlots,
let reservoirLevel = self.state.podState?.lastInsulinMeasurements?.reservoirLevel?.rawValue else
Expand Down Expand Up @@ -1707,6 +1748,11 @@ extension OmniBLEPumpManager: PumpManager {
state.suspendEngageState = .engaging
})

if let error = self.tryToValidateComms(session: session) {
completion(error)
return
}

// Use a beepBlock for the confirmation beep to avoid getting 3 beeps using cancel command beeps!
let beepBlock = self.beepMessageBlock(beepType: .beeeeeep)
let result = session.suspendDelivery(suspendReminder: suspendReminder, silent: self.silencePod, beepBlock: beepBlock)
Expand Down Expand Up @@ -1753,6 +1799,11 @@ extension OmniBLEPumpManager: PumpManager {
state.suspendEngageState = .disengaging
})

if let error = self.tryToValidateComms(session: session) {
completion(error)
return
}

do {
let scheduleOffset = self.state.timeZone.scheduleOffset(forDate: Date())
let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
Expand Down Expand Up @@ -1848,8 +1899,15 @@ extension OmniBLEPumpManager: PumpManager {
state.bolusEngageState = .engaging
})

if let podState = self.state.podState, podState.isSuspended || podState.lastDeliveryStatusReceived?.suspended == true {
self.log.error("Not enacting bolus because podState or last status received indicates pod is suspended")
if let error = self.tryToValidateComms(session: session) {
completion(.communication(error))
return
}

// Use a lastDeliveryStatusReceived?.suspended != true test here to not return a pod suspended failure if
// there is not a valid last delivery status (which shouldn't even happen now with tryToValidateComms()).
guard let podState = self.state.podState, !podState.isSuspended && podState.lastDeliveryStatusReceived?.suspended != true else {
self.log.info("Not enacting bolus because podState or last status received indicates pod is suspended")
completion(.deviceState(PodCommsError.podSuspended))
return
}
Expand Down Expand Up @@ -1949,7 +2007,7 @@ extension OmniBLEPumpManager: PumpManager {

public func runTemporaryBasalProgram(unitsPerHour: Double, for duration: TimeInterval, automatic: Bool, completion: @escaping (PumpManagerError?) -> Void) {

guard self.hasActivePod, let podState = self.state.podState else {
guard self.hasActivePod else {
completion(.configuration(OmniBLEPumpManagerError.noPodPaired))
return
}
Expand Down Expand Up @@ -1989,7 +2047,14 @@ extension OmniBLEPumpManager: PumpManager {
return
}

if let podState = self.state.podState, podState.isSuspended || podState.lastDeliveryStatusReceived?.suspended == true {
if let error = self.tryToValidateComms(session: session) {
completion(.communication(error))
return
}

// Use a lastDeliveryStatusReceived?.suspended != true test here to not return a pod suspended failure if
// there is not a valid last delivery status (which shouldn't even happen now with tryToValidateComms()).
guard let podState = self.state.podState, !podState.isSuspended && podState.lastDeliveryStatusReceived?.suspended != true else {
self.log.info("Not enacting temp basal because podState or last status received indicates pod is suspended")
completion(.deviceState(PodCommsError.podSuspended))
return
Expand Down Expand Up @@ -2033,7 +2098,7 @@ extension OmniBLEPumpManager: PumpManager {
return
}

guard status.deliveryStatus != .suspended else {
guard !status.deliveryStatus.suspended else {
self.log.info("Canceling temp basal because status return indicates pod is suspended!")
completion(.communication(PodCommsError.podSuspended))
return
Expand Down Expand Up @@ -2422,12 +2487,10 @@ extension OmniBLEPumpManager: PumpManager {

extension OmniBLEPumpManager: MessageLogger {
func didSend(_ message: Data) {
log.default("didSend: %{public}@", message.hexadecimalString)
self.logDeviceCommunication(message.hexadecimalString, type: .send)
}

func didReceive(_ message: Data) {
log.default("didReceive: %{public}@", message.hexadecimalString)
self.logDeviceCommunication(message.hexadecimalString, type: .receive)
}

Expand All @@ -2443,7 +2506,7 @@ extension OmniBLEPumpManager: PodCommsDelegate {
podComms.runSession(withName: "Post-connect status fetch") { result in
switch result {
case .success(let session):
let _ = try? session.getStatus()
let _ = try? session.getStatus(noSeqGetStatus: true)
self.silenceAcknowledgedAlerts()
session.dosesForStorage() { (doses) -> Bool in
return self.store(doses: doses, in: session)
Expand Down
Loading