diff --git a/OmniBLE/OmnipodCommon/MessageBlocks/PodInfo.swift b/OmniBLE/OmnipodCommon/MessageBlocks/PodInfo.swift index dfbfe181..a922ec23 100644 --- a/OmniBLE/OmnipodCommon/MessageBlocks/PodInfo.swift +++ b/OmniBLE/OmnipodCommon/MessageBlocks/PodInfo.swift @@ -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 @@ -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: diff --git a/OmniBLE/OmnipodCommon/Pod.swift b/OmniBLE/OmnipodCommon/Pod.swift index d0daef2e..75db1abc 100644 --- a/OmniBLE/OmnipodCommon/Pod.swift +++ b/OmniBLE/OmnipodCommon/Pod.swift @@ -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 @@ -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)) } @@ -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 = [ + .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 = [ + .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 = [ + .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 = [ + .extendedBolusWhileSuspended, + .extendedBolusRunning, + .extendedBolusAndTempBasal, + ] + return extendedBolusRunningStates.contains(self) } public var description: String { diff --git a/OmniBLE/PumpManager/MessageTransport.swift b/OmniBLE/PumpManager/MessageTransport.swift index 571e5907..9c357617 100644 --- a/OmniBLE/PumpManager/MessageTransport.swift +++ b/OmniBLE/PumpManager/MessageTransport.swift @@ -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() @@ -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, diff --git a/OmniBLE/PumpManager/OmniBLEPumpManager.swift b/OmniBLE/PumpManager/OmniBLEPumpManager.swift index afea8dc1..68e45804 100644 --- a/OmniBLE/PumpManager/OmniBLEPumpManager.swift +++ b/OmniBLE/PumpManager/OmniBLEPumpManager.swift @@ -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) @@ -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) -> Void)? = nil) { @@ -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) }) @@ -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() @@ -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 { @@ -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 @@ -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 @@ -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) @@ -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 @@ -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 } @@ -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 } @@ -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 @@ -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 @@ -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) } @@ -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) diff --git a/OmniBLE/PumpManager/PodCommsSession.swift b/OmniBLE/PumpManager/PodCommsSession.swift index e23b178a..aa17c2cd 100644 --- a/OmniBLE/PumpManager/PodCommsSession.swift +++ b/OmniBLE/PumpManager/PodCommsSession.swift @@ -275,26 +275,40 @@ public class PodCommsSession { /// - PodCommsError.unexpectedResponse /// - PodCommsError.rejectedMessage /// - PodCommsError.nonceResyncFailed + /// - PodCommsError.unacknowledgedMessage /// - PodCommsError.commsError.MessageError /// - PodCommsError.commsError.PeripheralManagerError /// - PodCommsError.commsError.PodProtocolError + /// - MessageError func send(_ messageBlocks: [MessageBlock], beepBlock: MessageBlock? = nil, expectFollowOnMessage: Bool = false) throws -> T { var triesRemaining = 2 // Retries only happen for nonce resync var blocksToSend = messageBlocks - - // If a beep block was specified & pod isn't faulted, append the beep block to emit the confirmation beep - if let beepBlock = beepBlock, podState.isFaulted == false { + + // If a beep block was specified & the pod isn't faulted AND there isn't an unacknowledged + // command for a getStatus command, append the beep block to emit the confirmation beep. + // Since a beep command changes lastProgrammingMessageSeqNum, we need skip appending a beep + // block while still trying to resolve an unacknowldged delivery command with getStatus calls. + if let beepBlock = beepBlock, podState.isFaulted == false && + !(podState.unacknowledgedCommand != nil && blocksToSend[0].blockType == .getStatus) + { blocksToSend += [beepBlock] } // if blocksToSend.contains(where: { $0 as? NonceResyncableMessageBlock != nil }) { // podState.advanceToNextNonce() // } - - let messageNumber = transport.messageNumber var sentNonce: UInt32? + var messageNumber = transport.messageNumber + if let getStatusCommand = messageBlocks[0] as? GetStatusCommand, + getStatusCommand.podInfoType == .noSeqStatus + { + // For the special type 7 DASH noSeqStatus getStatus command, + // back up the Omnipod msg # here to its previous value so that + // this message will have same msg # the last received response. + messageNumber = messageNumber == 0 ? 0b1111 : messageNumber - 1 + } while (triesRemaining > 0) { triesRemaining -= 1 @@ -374,8 +388,7 @@ public class PodCommsSession { try configureAlerts([finishSetupReminder]) } else { // Not the first time through, check to see if prime bolus was successfully started - let status: StatusResponse = try send([GetStatusCommand()]) - podState.updateFromStatusResponse(status, at: currentDate) + let status = try getStatus() if status.podProgressStatus == .priming || status.podProgressStatus == .primingCompleted { podState.setupProgress = .priming return podState.primeFinishTime?.timeIntervalSinceNow ?? primeDuration @@ -400,8 +413,7 @@ public class PodCommsSession { public func programInitialBasalSchedule(_ basalSchedule: BasalSchedule, scheduleOffset: TimeInterval) throws { if podState.setupProgress == .settingInitialBasalSchedule { // We started basal schedule programming, but didn't get confirmation somehow, so check status - let status: StatusResponse = try send([GetStatusCommand()]) - podState.updateFromStatusResponse(status, at: currentDate) + let status = try getStatus() if status.podProgressStatus == .basalInitialized { podState.setupProgress = .initialBasalScheduleSet podState.finalizedDoses.append(UnfinalizedDose(resumeStartTime: currentDate, scheduledCertainty: .certain, insulinType: podState.insulinType)) @@ -416,11 +428,43 @@ public class PodCommsSession { podState.finalizedDoses.append(UnfinalizedDose(resumeStartTime: currentDate, scheduledCertainty: .certain, insulinType: podState.insulinType)) } + // + // Attempts to resolve any pending unacknowledged command by calling getStatus(). + // podState.unacknowledgeCommand is guaranteed to be nil upon successful return. + // Throws PodCommsError.unacknowledgedCommandPending if unsuccessful for any reason. + // + private func tryToResolvePendingCommand() throws { + + guard podState.unacknowledgedCommand != nil else { + return // no pending unacknowledged command to resolve + } + + do { + _ = try getStatus() // should resolve the pending unacknowledged command if successful + } catch let error { + log.error("GetStatus failed trying to resolve pending unacknowledged command: %{public}@", String(describing: error)) + throw PodCommsError.unacknowledgedCommandPending + } + + // Verify that getStatus successfully resolved the pending unacknowledged command. + guard podState.unacknowledgedCommand == nil else { + log.error("Successful getStatus didn't resolve the pending unacknowledged command!") + throw PodCommsError.unacknowledgedCommandPending + } + + log.info("Successfully resolved pending unacknowledged command") + } + // Configures the given pod alert(s) and registers the newly configured alert slot(s). // When re-configuring all the pod alerts for a silence pod toggle, the optional acknowledgeAll can be // specified to first acknowledge and clear all possible pending pod alerts and pod alert configurations. @discardableResult func configureAlerts(_ alerts: [PodAlert], acknowledgeAll: Bool = false, beepBlock: MessageBlock? = nil) throws -> StatusResponse { + + if podState.unacknowledgedCommand != nil { + try tryToResolvePendingCommand() + } + let configurations = alerts.map { $0.configuration } let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: configurations) let blocksToSend: [MessageBlock] @@ -445,7 +489,15 @@ public class PodCommsSession { log.info("Skip beep config with faulted pod") return .failure(PodCommsError.podFault(fault: fault)) } - + + if podState.unacknowledgedCommand != nil { + do { + try tryToResolvePendingCommand() + } catch let error { + return .failure(error) + } + } + let beepConfigCommand = BeepConfigCommand(beepType: beepType, tempBasalCompletionBeep: tempBasalCompletionBeep, bolusCompletionBeep: bolusCompletionBeep) do { let statusResponse: StatusResponse = try send([beepConfigCommand]) @@ -473,19 +525,16 @@ public class PodCommsSession { if podState.setupProgress == .startingInsertCannula || podState.setupProgress == .cannulaInserting { // We started cannula insertion, but didn't get confirmation somehow, so check status - let status: StatusResponse = try send([GetStatusCommand()]) + let status = try getStatus() if status.podProgressStatus == .insertingCannula { podState.setupProgress = .cannulaInserting - podState.updateFromStatusResponse(status, at: currentDate) // return a non-zero wait time based on the bolus not yet delivered return (status.bolusNotDelivered / Pod.primeDeliveryRate) + 1 } if status.podProgressStatus.readyForDelivery { markSetupProgressCompleted(statusResponse: status) - podState.updateFromStatusResponse(status, at: currentDate) return TimeInterval(0) // Already done; no need to wait } - podState.updateFromStatusResponse(status, at: currentDate) } else { let elapsed: TimeInterval = -(podState.podTimeUpdated?.timeIntervalSinceNow ?? 0) let podTime = podState.podTime + elapsed @@ -512,11 +561,10 @@ public class PodCommsSession { public func checkInsertionCompleted() throws { if podState.setupProgress == .cannulaInserting { - let response: StatusResponse = try send([GetStatusCommand()]) + let response = try getStatus() if response.podProgressStatus.readyForDelivery { markSetupProgressCompleted(statusResponse: response) } - podState.updateFromStatusResponse(response, at: currentDate) } } @@ -536,8 +584,12 @@ public class PodCommsSession { public func bolus(units: Double, automatic: Bool = false, acknowledgementBeep: Bool = false, completionBeep: Bool = false, programReminderInterval: TimeInterval = 0, extendedUnits: Double = 0.0, extendedDuration: TimeInterval = 0) -> DeliveryCommandResult { - guard podState.unacknowledgedCommand == nil else { - return DeliveryCommandResult.certainFailure(error: .unacknowledgedCommandPending) + if podState.unacknowledgedCommand != nil { + do { + try tryToResolvePendingCommand() + } catch { + return DeliveryCommandResult.certainFailure(error: .unacknowledgedCommandPending) + } } let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerBolusPulse) @@ -583,8 +635,12 @@ public class PodCommsSession { public func setTempBasal(rate: Double, duration: TimeInterval, isHighTemp: Bool, automatic: Bool, acknowledgementBeep: Bool = false, completionBeep: Bool = false, programReminderInterval: TimeInterval = 0) -> DeliveryCommandResult { - guard podState.unacknowledgedCommand == nil else { - return DeliveryCommandResult.certainFailure(error: .unacknowledgedCommandPending) + if podState.unacknowledgedCommand != nil { + do { + try tryToResolvePendingCommand() + } catch { + return DeliveryCommandResult.certainFailure(error: .unacknowledgedCommandPending) + } } let tempBasalCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, tempBasalRate: rate, duration: duration) @@ -657,8 +713,12 @@ public class PodCommsSession { // The configured alerts will set up as silent pod alerts if silent is true. public func suspendDelivery(suspendReminder: TimeInterval? = nil, silent: Bool, beepBlock: MessageBlock? = nil) -> CancelDeliveryResult { - guard podState.unacknowledgedCommand == nil else { - return .certainFailure(error: .unacknowledgedCommandPending) + if podState.unacknowledgedCommand != nil { + do { + try tryToResolvePendingCommand() + } catch { + return .certainFailure(error: .unacknowledgedCommandPending) + } } guard podState.setupProgress == .completed else { @@ -724,7 +784,6 @@ public class PodCommsSession { } // Cancels any suspend related alerts, called when setting a basal schedule with active suspend alerts - @discardableResult private func cancelSuspendAlerts() throws -> StatusResponse { do { @@ -742,8 +801,12 @@ public class PodCommsSession { // N.B., Using the built-in cancel delivery command beepType method when cancelling all insulin delivery will emit 3 different sets of cancel beeps!!! public func cancelDelivery(deliveryType: CancelDeliveryCommand.DeliveryType, beepType: BeepType = .noBeepCancel, beepBlock: MessageBlock? = nil) -> CancelDeliveryResult { - guard podState.unacknowledgedCommand == nil else { - return .certainFailure(error: .unacknowledgedCommandPending) + if podState.unacknowledgedCommand != nil { + do { + try tryToResolvePendingCommand() + } catch { + return .certainFailure(error: .unacknowledgedCommandPending) + } } guard podState.setupProgress == .completed else { @@ -772,8 +835,9 @@ public class PodCommsSession { } public func setTime(timeZone: TimeZone, basalSchedule: BasalSchedule, date: Date, acknowledgementBeep: Bool = false) throws -> StatusResponse { - guard podState.unacknowledgedCommand == nil else { - throw PodCommsError.unacknowledgedCommandPending + + if podState.unacknowledgedCommand != nil { + try tryToResolvePendingCommand() } let result = cancelDelivery(deliveryType: .all) @@ -791,8 +855,8 @@ public class PodCommsSession { public func setBasalSchedule(schedule: BasalSchedule, scheduleOffset: TimeInterval, acknowledgementBeep: Bool = false, programReminderInterval: TimeInterval = 0) throws -> StatusResponse { - guard podState.unacknowledgedCommand == nil else { - throw PodCommsError.unacknowledgedCommandPending + if podState.unacknowledgedCommand != nil { + try tryToResolvePendingCommand() } let basalScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, basalSchedule: schedule, scheduleOffset: scheduleOffset) @@ -832,11 +896,10 @@ public class PodCommsSession { public func resumeBasal(schedule: BasalSchedule, scheduleOffset: TimeInterval, acknowledgementBeep: Bool = false, programReminderInterval: TimeInterval = 0) throws -> StatusResponse { - guard podState.unacknowledgedCommand == nil else { - throw PodCommsError.unacknowledgedCommandPending + if podState.unacknowledgedCommand != nil { + try tryToResolvePendingCommand() } - let status = try setBasalSchedule(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, programReminderInterval: programReminderInterval) podState.suspendState = .resumed(currentDate) @@ -846,7 +909,6 @@ public class PodCommsSession { // use cancelDelivery with .none to get status as well as to validate & advance the nonce // Throws PodCommsError - @discardableResult public func cancelNone(beepBlock: MessageBlock? = nil) throws -> StatusResponse { var statusResponse: StatusResponse @@ -864,9 +926,10 @@ public class PodCommsSession { } // Throws PodCommsError - @discardableResult - public func getStatus(beepBlock: MessageBlock? = nil) throws -> StatusResponse { - let statusResponse: StatusResponse = try send([GetStatusCommand()], beepBlock: beepBlock) + public func getStatus(noSeqGetStatus: Bool = false, beepBlock: MessageBlock? = nil) throws -> StatusResponse { + // For noSeqSetStatus, use an alternative DASH noSeqStatus (type 7) request instead of a normal (type 0) request + let statusType: PodInfoResponseSubType = noSeqGetStatus ? .noSeqStatus : .normal + let statusResponse: StatusResponse = try send([GetStatusCommand(podInfoType: statusType)], beepBlock: beepBlock) if podState.unacknowledgedCommand != nil { recoverUnacknowledgedCommand(using: statusResponse) @@ -874,8 +937,7 @@ public class PodCommsSession { podState.updateFromStatusResponse(statusResponse, at: currentDate) return statusResponse } - - @discardableResult + public func getDetailedStatus(beepBlock: MessageBlock? = nil) throws -> DetailedStatus { let infoResponse: PodInfoResponse = try send([GetStatusCommand(podInfoType: .detailedStatus)], beepBlock: beepBlock) @@ -895,14 +957,13 @@ public class PodCommsSession { return detailedStatus } - @discardableResult public func readPodInfo(podInfoResponseSubType: PodInfoResponseSubType, beepBlock: MessageBlock? = nil) throws -> PodInfoResponse { let podInfoCommand = GetStatusCommand(podInfoType: podInfoResponseSubType) let podInfoResponse: PodInfoResponse = try send([podInfoCommand], beepBlock: beepBlock) return podInfoResponse } - // Reconnected to the pod, and we know program was successful + // Reconnected to the pod, and we know program was successful based on lastProgrammingMessageSeqNum private func unacknowledgedCommandWasReceived(pendingCommand: PendingCommand, podStatus: StatusResponse) { switch pendingCommand { case .program(let program, _, let commandDate, _): @@ -919,7 +980,6 @@ public class PodCommsSession { } } case .stopProgram(let stopProgram, _, let commandDate, _): - if stopProgram.contains(.bolus), let bolus = podState.unfinalizedBolus, !bolus.isFinished(at: commandDate) { podState.unfinalizedBolus?.cancel(at: commandDate, withRemaining: podStatus.bolusNotDelivered) } @@ -933,6 +993,60 @@ public class PodCommsSession { } } + // Reconnected to the pod and we didn't match lastProgrammingMessageSeqNum which indicates + // that the command was not received. Now verify the pendingCommand against the current pod + // delivery status to decide whether the delivery related command might have been received or not. + // Returns true if the command was received based on the pod delivery status and podState was updated. + private func checkCommandAgainstStatus(pendingCommand: PendingCommand, podStatus: StatusResponse) -> Bool { + let deliveryStatus = podStatus.deliveryStatus + var podStatusMatched = false + switch pendingCommand { + case .program(let program, _, let commandDate, _): + if let dose = program.unfinalizedDose(at: commandDate, withCertainty: .certain, insulinType: podState.insulinType) { + switch dose.doseType { + case .bolus: + if deliveryStatus.bolusing { + podState.unfinalizedBolus = dose + podStatusMatched = true + } + case .tempBasal: + if deliveryStatus.tempBasalRunning { + podState.unfinalizedTempBasal = dose + podStatusMatched = true + } + case .resume: + if !deliveryStatus.suspended { + podState.suspendState = .resumed(commandDate) + podStatusMatched = true + } + default: + break + } + } + case .stopProgram(let stopProgram, _, let commandDate, _): + if stopProgram.contains(.bolus), let bolus = podState.unfinalizedBolus, !bolus.isFinished(at: commandDate) { + if !deliveryStatus.bolusing { + podState.unfinalizedBolus?.cancel(at: commandDate, withRemaining: podStatus.bolusNotDelivered) + podStatusMatched = true + } + } + if stopProgram.contains(.tempBasal), let tempBasal = podState.unfinalizedTempBasal, !tempBasal.isFinished(at: commandDate) { + if !deliveryStatus.tempBasalRunning { + podState.unfinalizedTempBasal?.cancel(at: commandDate) + podStatusMatched = true + } + } + if stopProgram.contains(.basal) { + if !deliveryStatus.suspended { + podState.finalizedDoses.append(UnfinalizedDose(suspendStartTime: commandDate, scheduledCertainty: .certain)) + podState.suspendState = .suspended(commandDate) + podStatusMatched = true + } + } + } + return podStatusMatched + } + public func recoverUnacknowledgedCommand(using status: StatusResponse) { if let pendingCommand = podState.unacknowledgedCommand { self.log.default("Recovering from unacknowledged command %{public}@, status = %{public}@", String(describing: pendingCommand), String(describing: status)) @@ -940,6 +1054,8 @@ public class PodCommsSession { if status.lastProgrammingMessageSeqNum == pendingCommand.sequence { self.log.default("Unacknowledged command was received by pump") unacknowledgedCommandWasReceived(pendingCommand: pendingCommand, podStatus: status) + } else if checkCommandAgainstStatus(pendingCommand: pendingCommand, podStatus: status) { + self.log.default("Accepted unacknowledged command was received based on pod delivery status of ${public}@", String(describing: status.deliveryStatus)) } else { self.log.default("Unacknowledged command was not received by pump") } @@ -972,12 +1088,14 @@ public class PodCommsSession { } do { - let deactivatePod = DeactivatePodCommand(nonce: podState.currentNonce) - let status: StatusResponse = try send([deactivatePod]) - if podState.unacknowledgedCommand != nil { - recoverUnacknowledgedCommand(using: status) + // Try to resolve the unacknowledged command now as DeactivatePodCommand + // destroys any chance of correctly handling the unacknowledged command. + try? tryToResolvePendingCommand() } + + let deactivatePod = DeactivatePodCommand(nonce: podState.currentNonce) + let status: StatusResponse = try send([deactivatePod]) podState.updateFromStatusResponse(status, at: currentDate) if podState.activeTime == nil, let activatedAt = podState.activatedAt { @@ -994,6 +1112,11 @@ public class PodCommsSession { } public func acknowledgeAlerts(alerts: AlertSet, beepBlock: MessageBlock? = nil) throws -> AlertSet { + + if podState.unacknowledgedCommand != nil { + try tryToResolvePendingCommand() + } + let cmd = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: alerts) let status: StatusResponse = try send([cmd], beepBlock: beepBlock) podState.updateFromStatusResponse(status, at: currentDate) diff --git a/OmniBLE/PumpManager/PodState.swift b/OmniBLE/PumpManager/PodState.swift index dc71bde0..2c38b2db 100644 --- a/OmniBLE/PumpManager/PodState.swift +++ b/OmniBLE/PumpManager/PodState.swift @@ -312,7 +312,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl if deliveryStatus.tempBasalRunning && unfinalizedTempBasal == nil { // active temp basal that we aren't tracking // unfinalizedTempBasal = UnfinalizedDose(tempBasalRate: 0, startTime: Date(), duration: .minutes(30), isHighTemp: false, scheduledCertainty: .certain, insulinType: insulinType) } - if deliveryStatus != .suspended && isSuspended { // active basal that we aren't tracking + if !deliveryStatus.suspended && isSuspended { // active basal that we aren't tracking let resumeStartTime = Date() suspendState = .resumed(resumeStartTime) unfinalizedResume = UnfinalizedDose(resumeStartTime: resumeStartTime, scheduledCertainty: .certain, insulinType: insulinType) @@ -570,7 +570,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl "* expiresAt: \(String(reflecting: expiresAt))", "* podTime: \(podTime.timeIntervalStr)", "* podTimeUpdated: \(String(reflecting: podTimeUpdated))", - "* setupUnitsDelivered: \(String(reflecting: setupUnitsDelivered))", + "* setupUnitsDelivered: \(setupUnitsDelivered == nil ? "?" : setupUnitsDelivered!.twoDecimals) U", "* firmwareVersion: \(firmwareVersion)", "* bleFirmwareVersion: \(bleFirmwareVersion)", "* lotNo: \(lotNo)", @@ -583,6 +583,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl "* unfinalizedResume: \(String(describing: unfinalizedResume))", "* finalizedDoses: \(String(describing: finalizedDoses))", "* activeAlertsSlots: \(alertSetString(alertSet: activeAlertSlots))", + "* delivered: \(lastInsulinMeasurements == nil ? "?" : lastInsulinMeasurements!.delivered.twoDecimals) U", + "* reservoirLevel: \(lastInsulinMeasurements == nil || lastInsulinMeasurements!.reservoirLevel == nil || lastInsulinMeasurements!.reservoirLevel == Pod.reservoirLevelAboveThresholdMagicNumber ? "50+" : lastInsulinMeasurements!.reservoirLevel!.twoDecimals) U", "* messageTransportState: \(String(describing: messageTransportState))", "* setupProgress: \(setupProgress)", "* primeFinishTime: \(String(describing: primeFinishTime))", diff --git a/OmniBLE/PumpManagerUI/ViewModels/OmniBLESettingsViewModel.swift b/OmniBLE/PumpManagerUI/ViewModels/OmniBLESettingsViewModel.swift index 8aca8d42..014f85c4 100644 --- a/OmniBLE/PumpManagerUI/ViewModels/OmniBLESettingsViewModel.swift +++ b/OmniBLE/PumpManagerUI/ViewModels/OmniBLESettingsViewModel.swift @@ -476,7 +476,7 @@ class OmniBLESettingsViewModel: ObservableObject { } public var allowedTempBasalRates: [Double] { - return Pod.supportedBasalRates.filter { $0 <= pumpManager.state.maximumTempBasalRate } + return Pod.supportedTempBasalRates.filter { $0 <= pumpManager.state.maximumTempBasalRate } } } diff --git a/OmniBLEParser/main.swift b/OmniBLEParser/main.swift index b883e888..95df8cbb 100644 --- a/OmniBLEParser/main.swift +++ b/OmniBLEParser/main.swift @@ -9,9 +9,12 @@ import Foundation -// These options can be forced off by using the -q option argument +// The following default values can all be forced to false or to true using the -q and -v command line options respectively fileprivate var printDate: Bool = true // whether to print the date (when available) along with the time (when available) -fileprivate var printFullMessage: Bool = true // whether to print full message decode including the address and seq +fileprivate var printUnacknowledgedMessageLines: Bool = true // whether to print "Unacknowledged message" lines +fileprivate var printAddressAndSeq: Bool = false // whether to print full message decode including the pod address and seq # +fileprivate var printPodConnectionLines: Bool = false // whether to print "connection Pod" lines + //from NSHipster - http://nshipster.com/swift-literal-convertible/ struct Regex { @@ -46,10 +49,20 @@ func ~=(pattern: Regex, matchable: T) -> Bool { return matchable.match(regex: pattern) } -func printDecoded(timeStr: String, hexString: String) +extension String { + func subString(location: Int, length: Int? = nil) -> String { + let start = min(max(0, location), self.count) + let limitedLength = min(self.count - start, length ?? Int.max) + let from = index(startIndex, offsetBy: start) + let to = index(startIndex, offsetBy: start + limitedLength) + return String(self[from..= 10 else { - print("Bad hex string: \(hexString)") + guard let data = Data(hexadecimalString: hexStr), data.count >= 10 else { + print("Bad hex string: \(hexStr)") return } do { @@ -69,42 +82,71 @@ func printDecoded(timeStr: String, hexString: String) checkCRC = true } let message = try Message(encodedData: data, checkCRC: checkCRC) - if printFullMessage { + var dateTimeStr: String + if printDate && !dateStr.isEmpty { + dateTimeStr = dateStr + " " + timeStr + " " + } else if !timeStr.isEmpty { + dateTimeStr = timeStr + " " + } else { + dateTimeStr = "" + } + if printAddressAndSeq { // print the complete message with the address and seq - print("\(type)\(timeStr) \(message)") + print("\(type)\(dateTimeStr)\(message)") } else { // skip printing the address and seq for each message - print("\(type)\(timeStr) \(message.messageBlocks)") + print("\(type)\(dateTimeStr)\(message.messageBlocks)") } } catch let error { - print("Could not parse \(hexString): \(error)") + print("Could not parse \(hexStr): \(error)") } } // * 2022-04-05 06:56:14 +0000 Omnipod-Dash 17CAE1DD send 17cae1dd00030e010003b1 // * 2022-04-05 06:56:14 +0000 Omnipod-Dash 17CAE1DD receive 17cae1dd040a1d18002ab00000019fff0198 -func parseLoopReportLine(_ line: String) { +func parseLoopReportLine(line: String) { let components = line.components(separatedBy: .whitespaces) let hexString = components[components.count - 1] - let date = components[1] - let time = components[2] - let timeStr = printDate ? date + " " + time : time - - printDecoded(timeStr: timeStr, hexString: hexString) + printDecoded(dateStr: components[1], timeStr: components[2], hexStr: hexString) } +// Older Xcode log file with inline metadata // 2023-02-02 15:23:13.094289-0800 Loop[60606:22880823] [PodMessageTransport] Send(Hex): 1776c2c63c030e010000a0 // 2023-02-02 15:23:13.497849-0800 Loop[60606:22880823] [PodMessageTransport] Recv(Hex): 1776c2c6000a1d180064d800000443ff0000 -func parseLoopXcodeLine(_ line: String) { +func parseLoopXcodeInlineMetadataLine(line: String) { let components = line.components(separatedBy: .whitespaces) let hexString = components[components.count - 1] - let date = components[0] - let time = components[1].padding(toLength: 15, withPad: " ", startingAt: 0) // skip the -0800 portion - let timeStr = printDate ? date + " " + time : time + let time = components[1].subString(location: 0, length: 15) // use the 15 detailed time chars w/o TZ (e.g., "15:23:13.497849") - printDecoded(timeStr: timeStr, hexString: hexString) + printDecoded(dateStr: components[0], timeStr: time, hexStr: hexString) +} + +// Newer Xcode log file using separate metadata lines (app independent) +// Send(Hex): 1f074dca1c201a0ea814ef4e01007901384000000000160e000000006b49d20000006b49d200013a +// Timestamp: 2024-01-14 12:02:27.095438-08:00 | Library: OmniKit | Category: PodMessageTransport +// Recv(Hex): 1f074dca200a1d280059b800001aa7ff01c0 +// Timestamp: 2024-01-14 12:02:30.391271-08:00 | Library: OmniKit | Category: PodMessageTransport +func parseXcodeLine(line: String, timestampLine: String) { + var date = "" + var time = "" + + let timeStampLineComponents = timestampLine.components(separatedBy: .whitespaces) + if timeStampLineComponents.count >= 3 { + for i in 0...timeStampLineComponents.count - 2 { + if timeStampLineComponents[i] == "Timestamp:" { + date = timeStampLineComponents[i + 1] + time = timeStampLineComponents[i + 2].subString(location: 0, length: 15) // use the 15 detailed time chars w/o TZ + break + } + } + } + + let components = line.components(separatedBy: .whitespaces) + let hexString = components[components.count - 1] + + printDecoded(dateStr: date, timeStr: time, hexStr: hexString) } // N.B. Simulator output typically has a space after the hex string! @@ -112,7 +154,7 @@ func parseLoopXcodeLine(_ line: String) { // INFO[7699] pkg response 0x1d; HEX, 1776c2c6000a1d280064e80000057bff0000 // INFO[2023-09-04T18:17:06-07:00] pkg command; 0x07; GET_VERSION; HEX, ffffffff00060704ffffffff82b2 // INFO[2023-09-04T18:17:06-07:00] pkg response 0x1; HEX, ffffffff04170115040a00010300040208146db10006e45100ffffffff0000 -func parseSimulatorLogLine(_ line: String) { +func parseSimulatorLogLine(line: String) { let components = line.components(separatedBy: .whitespaces) var hexStringIndex = components.count - 1 let hexString: String @@ -122,61 +164,144 @@ func parseSimulatorLogLine(_ line: String) { hexString = components[hexStringIndex] let c0 = components[0] - // start at 5 for printDate or shorter "INFO[7699]" format - let offset = printDate || c0.count <= 16 ? 5 : 16 - let startIndex = c0.index(c0.startIndex, offsetBy: offset) - let endIndex = c0.index(c0.startIndex, offsetBy: c0.count - 2) - let timeStr = String(c0[startIndex...endIndex]) + let date: String + let time: String + + if c0.count <= 16 { + // seconds only format, e.g., "INFO[7699]" + date = "" + time = c0.subString(location: 5, length: c0.count - 6) // six less for the "INFO[]" chars + } else { + // full time format, e.g., "INFO[2023-09-04T18:17:06-07:00]" + date = c0.subString(location: 5, length: 10) + time = c0.subString(location: 16, length: 8) // the time w/o TZ (e.g., "18:17:06") + } - printDecoded(timeStr: timeStr, hexString: hexString) + printDecoded(dateStr: date, timeStr: time, hexStr: hexString) } -// iAPS or Trio log file -// iAPS_log 2024-05-08T00:03:57-0700 [DeviceManager] DeviceDataManager.swift - deviceManager(_:logEventForDeviceIdentifier:type:message:completion:) - 576 - DEV: Device message: 17ab48aa20071f05494e532e0201d5 -// iAPS or Trio Xcode log with timestamp -// 2024-05-25 14:16:54.933281-0700 FreeAPS[2973:2299225] [DeviceManager] DeviceDataManager.swift - deviceManager(_:logEventForDeviceIdentifier:type:message:completion:) - 566 DEV: Device message: 170f1e3710080806494e532e000081ab -// iAPS or Trio Xcode log with no timestamp -// DeviceDataManager.swift - deviceManager(_:logEventForDeviceIdentifier:type:message:completion:) - 566 DEV: Device message: 170f1e3710080806494e532e000081ab -func parseFreeAPSLogOrXcodeLine(_ line: String) { +// FreeAPS style log file or Xcode log file with inline metadata +// 2024-05-08T00:03:57-0700 [DeviceManager] DeviceDataManager.swift - deviceManager(_:logEventForDeviceIdentifier:type:message:completion:) - 576 - DEV: Device message: 17ab48aa20071f05494e532e0201d5 +func parseFreeAPSLogOrXcodeInlineMetadataLine(line: String) { let components = line.components(separatedBy: .whitespaces) let hexString = components[components.count - 1] + let date, time: String if components.count > 9 { - // have a timestamp - let date = components[0].prefix(10) - let time: String - if components.count == 12 { - // iAPS or Trio log file with date and time joined with a "T", e.g., 2024-05-25T00:26:05-0700 - let dateAndTimeComponents = components[0].components(separatedBy: "T") - time = dateAndTimeComponents[1].padding(toLength: 8, withPad: " ", startingAt: 0) // skip the -0700 portion + // have a timestamp like "2024-05-08T00:03:57-0700" or "2024-05-25" "14:16:54.933281-0700" + date = components[0].subString(location: 0, length: 10) // the first 10 chars are the date (e.g,. "2024-05-25") + if components[0].contains("T") { + // iAPS or Trio log file with date and time joined with a "T", e.g., "2024-05-25T00:26:05-0700" + time = components[0].subString(location: 11, length: 8) // the 8 time chars w/o TZ (e.g., "00:26:05") } else { - // Xcode log file with separate date and time, e.g., 2024-05-25 14:16:53.571361-0700 - time = components[1].padding(toLength: 15, withPad: " ", startingAt: 11) // skip the -0700 portion + // Xcode log file with separate date and time, e.g., "2024-05-25" "14:16:53.571361-0700" + time = components[1].subString(location: 0, length: 15) // the 15 detailed time chars w/o TZ (e.g., "14:16:53.571361") } - let timeStr = printDate ? date + " " + time : time - printDecoded(timeStr: timeStr, hexString: hexString) } else { // no timestamp - printDecoded(timeStr: "", hexString: hexString) + date = "" + time = "" } + printDecoded(dateStr: date, timeStr: time, hexStr: hexString) } // 2020-11-04 13:38:34.256 1336 6945 I PodComm pod command: 08202EAB08030E01070319 // 2020-11-04 13:38:34.979 1336 1378 V PodComm response (hex) 08202EAB0C0A1D9800EB80A400042FFF8320 -func parseDashPDMLogLine(_ line: String) { +func parseDashPDMLogLine(line: String) { let components = line.components(separatedBy: .whitespaces) let hexString = components[components.count - 1] - let date = components[0] - let time = components[1] - let timeStr = printDate ? date + " " + time : time + printDecoded(dateStr: components[0], timeStr: components[1], hexStr: hexString) +} + +// Disconnect and connect messages +// +// Loop Report +// * 2024-07-09 23:10:17 +0000 Omnipod-Dash 170C4026 connection Pod disconnected 80635530-69E1-E701-9C57-190CC608CE6F Optional(Error Domain=CBErrorDomain Code=7 "The specified device has disconnected from us." UserInfo={NSLocalizedDescription=The specified device has disconnected from us.}) +// iAPS or Trio log file +// 2024-05-25T00:05:22-0700 [DeviceManager] DeviceDataManager.swift - deviceManager(_:logEventForDeviceIdentifier:type:message:completion:) - 576 - DEV: Device message: Pod connected C8AA0FAE-7BF3-D682-38D7-DD7314F0F128 +// +// Loop xcode log +// 2024-05-25 14:04:19.799014-0700 Loop[2042:132457] [PersistentDeviceLog] connection (17FC3D73) Pod disconnected 86779FC4-EB9B-6ED6-6A38-C345BE12FDB6 nil +// iAPS or Trio xcode log +// 2024-05-25 14:22:47.988314-0700 FreeAPS[2973:2299227] [DeviceManager] DeviceDataManager.swift - deviceManager(_:logEventForDeviceIdentifier:type:message:completion:) - 566 DEV: Device message: Pod connected F74B4012-5849-3E00-792E-66726A675CED +// +// With newer Xcode logging, metadata could be on a separate line (app independent) +// +// Unacknowledged messages +// Old style +// * 2024-07-09 23:25:25 +0000 Omnipod-Dash 170C4026 error Unacknowledged message. seq:10, error = ... +// Newer styles +// * 2024-07-09 23:25:25 +0000 Omnipod-Dash 170C4026 error Unacknowledged message sending command seq:11, error = ... +// * 2024-07-09 23:25:25 +0000 Omnipod-Dash 170C4026 error Unacknowledged message reading response for sent command seq:12, error = ... +func printPodInfoLine(line: String, timestampLine: String) { + let components = line.components(separatedBy: .whitespaces) + var endIndex = components.endIndex - 1 + var startIndex = components[0] == "*" ? 1 : 0 // skip any leading "*" + + var date = "" + var time = "" + let timeStampLineComponents = timestampLine.components(separatedBy: .whitespaces) + if timeStampLineComponents.count >= 3 { + // newer Xcode logging with a separate line for metadata + for i in 0...timeStampLineComponents.count - 2 { + if timeStampLineComponents[i] == "Timestamp:" { + date = timeStampLineComponents[i + 1] + time = timeStampLineComponents[i + 2].subString(location: 0, length: 15) // use the 15 detailed time chars w/o TZ + break + } + } + } else if components[startIndex].contains("T") { + // iAPS or Trio log file with date and time with TZ joined with a 'T', e.g., "2024-05-25T00:26:05-0700" + date = components[startIndex].subString(location: 0, length: 10) // the first 10 chars are date (e.g., "2024-05-25") + time = components[startIndex].subString(location: 11, length: 8) // the 8 time chars w/o TZ (e.g., "00:26:05) + startIndex += 1 + } else if components[startIndex + 1].contains(".") { + // Xcode log file with separate date and precise time with TZ, e.g., "2024-05-25" "14:16:53.571361-0700" + date = components[startIndex] + time = components[1].subString(location: 0, length: 15) // the 15 detailed time chars w/o TZ (e.g., "14:16:53.571361") + startIndex += 2 + } else if components[startIndex + 2].hasPrefix("+") { + // Loop log file with separate date, time & timezone, e.g., "2023-04-05" "06:07:08" "+0000" + date = components[startIndex] + time = components[startIndex + 1] + startIndex += 3 + } + + // Trim the fat to simplify the output depending on whether it's a connection or unacknowledged message + for i in startIndex...endIndex { + // For disconnected & connected messages, only keep 2 words + if components[i].contains("disconnected") || components[i].contains("connected") && i > 1 { + startIndex = i - 1 // "Pod" + endIndex = i // "disconnected" or "connected" + break + } + if components[i].contains("Unacknowledged") { + startIndex = i // strip earlier cruft + break + } + } - printDecoded(timeStr: timeStr, hexString: hexString) + var podInfoLine = " " // aligns with "RESPONSE: " or "COMMAND: " prefixes + if printDate && !date.isEmpty { + podInfoLine += date + " " + } + if !time.isEmpty { + podInfoLine += time + " " + } + + for i in startIndex...endIndex { + podInfoLine += components[i] + if i < endIndex { + podInfoLine += " " + } + } + print(podInfoLine) } func usage() { - print("Usage: [-q] file...") + print("Usage: [-qv] file...") print("Set the Xcode Arguments Passed on Launch using Product->Scheme->Edit Scheme...") print("to specify the full path to Loop Report, Xcode log, pod simulator log, iAPS log, Trio log or DASH PDM log file(s) to parse.\n") exit(1) @@ -189,52 +314,101 @@ if CommandLine.argc <= 1 { for arg in CommandLine.arguments[1...] { if arg == "-q" { printDate = false - printFullMessage = false + printUnacknowledgedMessageLines = false + printAddressAndSeq = false + printPodConnectionLines = false + continue + } else if arg == "-v" { + printDate = true + printUnacknowledgedMessageLines = true + printAddressAndSeq = true + printPodConnectionLines = true + continue + } else if arg == "" || arg == "--" { continue } else if arg.starts(with: "-") { // no other arguments curently supported usage() } + var timestampLine: String print("\nParsing \(arg)") do { let data = try String(contentsOfFile: arg, encoding: .utf8) let lines = data.components(separatedBy: .newlines) - for line in lines { + for i in 0..