Skip to content

Comments and refactoring #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 27, 2016
Merged
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
190 changes: 113 additions & 77 deletions Loop/Managers/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,27 @@ enum State<T> {


class DeviceDataManager: NSObject, CarbStoreDelegate, TransmitterDelegate, WCSessionDelegate {
/// Notification posted by the instance when new glucose data was processed
static let GlucoseUpdatedNotification = "com.loudnate.Naterade.notification.GlucoseUpdated"

/// Notification posted by the instance when new pump data was processed
static let PumpStatusUpdatedNotification = "com.loudnate.Naterade.notification.PumpStatusUpdated"

enum Error: ErrorType {
case ValueError(String)
}

// MARK: - Observed state
// MARK: - Utilities

lazy var logger = DiagnosticLogger()

/// Manages all the RileyLinks
let rileyLinkManager: RileyLinkDeviceManager

let shareClient: ShareClient?
/// The share server client
private let shareClient: ShareClient?

/// The G5 transmitter object
var transmitter: Transmitter? {
switch transmitterState {
case .Ready(let transmitter):
Expand All @@ -51,27 +57,27 @@ class DeviceDataManager: NSObject, CarbStoreDelegate, TransmitterDelegate, WCSes

// MARK: - RileyLink

var rileyLinkManagerObserver: AnyObject? {
private var rileyLinkManagerObserver: AnyObject? {
willSet {
if let observer = rileyLinkManagerObserver {
NSNotificationCenter.defaultCenter().removeObserver(observer)
}
}
}

var rileyLinkDevicePacketObserver: AnyObject? {
private var rileyLinkDevicePacketObserver: AnyObject? {
willSet {
if let observer = rileyLinkDevicePacketObserver {
NSNotificationCenter.defaultCenter().removeObserver(observer)
}
}
}

func receivedRileyLinkManagerNotification(note: NSNotification) {
private func receivedRileyLinkManagerNotification(note: NSNotification) {
NSNotificationCenter.defaultCenter().postNotificationName(note.name, object: self, userInfo: note.userInfo)
}

func receivedRileyLinkPacketNotification(note: NSNotification) {
private func receivedRileyLinkPacketNotification(note: NSNotification) {
if let
device = note.object as? RileyLinkDevice,
data = note.userInfo?[RileyLinkDevice.IdleMessageDataKey] as? NSData,
Expand All @@ -82,18 +88,10 @@ class DeviceDataManager: NSObject, CarbStoreDelegate, TransmitterDelegate, WCSes
switch message.messageBody {
case let body as MySentryPumpStatusMessageBody:
updatePumpStatus(body, fromDevice: device)
case is MySentryAlertMessageBody:
break
// TODO: de-dupe
// logger?.addMessage(body.dictionaryRepresentation, toCollection: "sentryAlert")
case is MySentryAlertClearedMessageBody:
break
// TODO: de-dupe
// logger?.addMessage(body.dictionaryRepresentation, toCollection: "sentryAlert")
case let body as UnknownMessageBody:
logger?.addMessage(body.dictionaryRepresentation, toCollection: "sentryOther")
default:
case is MySentryAlertMessageBody, is MySentryAlertClearedMessageBody:
break
case let body:
logger?.addMessage(["messageType": Int(message.messageType.rawValue), "messageBody": body.txData.hexadecimalString], toCollection: "sentryOther")
}
default:
break
Expand All @@ -117,74 +115,79 @@ class DeviceDataManager: NSObject, CarbStoreDelegate, TransmitterDelegate, WCSes
AnalyticsManager.didChangeRileyLinkConnectionState()
}

// MARK: Pump data

var latestPumpStatus: MySentryPumpStatusMessageBody?

var latestReservoirValue: ReservoirValue?

/**
Handles receiving a MySentry status message, which are only posted by MM x23 pumps.

This message has two important pieces of info about the pump: reservoir volume and battery.

Because the RileyLink must actively listen for these packets, they are not the most reliable heartbeat. However, we can still use them to assert glucose data is current.

- parameter status: The status message body
- parameter device: The RileyLink that received the message
*/
private func updatePumpStatus(status: MySentryPumpStatusMessageBody, fromDevice device: RileyLinkDevice) {
status.pumpDateComponents.timeZone = pumpState?.timeZone

if status != latestPumpStatus, let pumpDate = status.pumpDateComponents.date {
latestPumpStatus = status
// The pump sends the same message 3x, so ignore it if we've already seen it.
guard status != latestPumpStatus, let pumpDate = status.pumpDateComponents.date else {
return
}

doseStore.addReservoirValue(status.reservoirRemainingUnits, atDate: pumpDate) { (newValue, previousValue, error) -> Void in
if let error = error {
self.logger?.addError(error, fromSource: "DoseStore")
} else if self.latestGlucoseMessageDate == nil,
let shareClient = self.shareClient,
glucoseStore = self.glucoseStore,
lastGlucose = glucoseStore.latestGlucose
where lastGlucose.startDate.timeIntervalSinceNow < -NSTimeInterval(minutes: 5)
{
// Load glucose from Share if our xDripG5 connection hasn't started
shareClient.fetchLast(1) { (error, glucose) in
if let error = error {
self.logger?.addError(error, fromSource: "ShareClient")
}

guard let glucose = glucose?.first where lastGlucose.startDate.timeIntervalSinceDate(glucose.startDate) < -NSTimeInterval(minutes: 1) else {
NSNotificationCenter.defaultCenter().postNotificationName(self.dynamicType.PumpStatusUpdatedNotification, object: self)
return
}

glucoseStore.addGlucose(glucose.quantity, date: glucose.startDate, displayOnly: false, device: nil) { (_, value, error) -> Void in
if let error = error {
self.logger?.addError(error, fromSource: "GlucoseStore")
}

NSNotificationCenter.defaultCenter().postNotificationName(self.dynamicType.GlucoseUpdatedNotification, object: self)
NSNotificationCenter.defaultCenter().postNotificationName(self.dynamicType.PumpStatusUpdatedNotification, object: self)
}
}
} else {
NSNotificationCenter.defaultCenter().postNotificationName(self.dynamicType.PumpStatusUpdatedNotification, object: self)
}
latestPumpStatus = status

if let newVolume = newValue?.unitVolume, previousVolume = previousValue?.unitVolume {
self.checkPumpReservoirForAmount(newVolume, previousAmount: previousVolume, timeLeft: NSTimeInterval(minutes: Double(status.reservoirRemainingMinutes)))
}
}
backfillGlucoseFromShareIfNeeded()

if status.batteryRemainingPercent == 0 {
NotificationManager.sendPumpBatteryLowNotification()
}
updateReservoirVolume(status.reservoirRemainingUnits, atDate: pumpDate, withTimeLeft: NSTimeInterval(minutes: Double(status.reservoirRemainingMinutes)))

// Check for an empty battery. Sentry packets are still broadcast for a few hours after this value reaches 0.
if status.batteryRemainingPercent == 0 {
NotificationManager.sendPumpBatteryLowNotification()
}
}

private func checkPumpReservoirForAmount(newAmount: Double, previousAmount: Double, timeLeft: NSTimeInterval) {
/**
Store a new reservoir volume and notify observers of new pump data.

guard newAmount > 0 else {
NotificationManager.sendPumpReservoirEmptyNotification()
return
}
- parameter units: The number of units remaining
- parameter date: The date the reservoir was read
- parameter timeLeft: The approximate time before the reservoir is empty
*/
private func updateReservoirVolume(units: Double, atDate date: NSDate, withTimeLeft timeLeft: NSTimeInterval?) {
doseStore.addReservoirValue(units, atDate: date) { (newValue, previousValue, error) -> Void in
if let error = error {
self.logger?.addError(error, fromSource: "DoseStore")
return
}

let warningThresholds: [Double] = [10, 20, 30]
self.latestReservoirValue = newValue

for threshold in warningThresholds {
if newAmount <= threshold && previousAmount > threshold {
NotificationManager.sendPumpReservoirLowNotificationForAmount(newAmount, andTimeRemaining: timeLeft)
return
NSNotificationCenter.defaultCenter().postNotificationName(self.dynamicType.PumpStatusUpdatedNotification, object: self)

// Send notifications for low reservoir if necessary
if let newVolume = newValue?.unitVolume, previousVolume = previousValue?.unitVolume {
guard newVolume > 0 else {
NotificationManager.sendPumpReservoirEmptyNotification()
return
}

let warningThresholds: [Double] = [10, 20, 30]

for threshold in warningThresholds {
if newVolume <= threshold && previousVolume > threshold {
NotificationManager.sendPumpReservoirLowNotificationForAmount(newVolume, andTimeRemaining: timeLeft)
}
}
}
}
}

// MARK: - Transmitter
// MARK: - G5 Transmitter

// MARK: TransmitterDelegate

Expand All @@ -195,7 +198,7 @@ class DeviceDataManager: NSObject, CarbStoreDelegate, TransmitterDelegate, WCSes
], toCollection: "g5"
)

self.rileyLinkManager.firstConnectedDevice?.assertIdleListening()
rileyLinkManager.firstConnectedDevice?.assertIdleListening()
}

func transmitter(transmitter: Transmitter, didReadGlucose glucose: GlucoseRxMessage) {
Expand Down Expand Up @@ -223,12 +226,12 @@ class DeviceDataManager: NSObject, CarbStoreDelegate, TransmitterDelegate, WCSes
}
}

self.rileyLinkManager.firstConnectedDevice?.assertIdleListening()
rileyLinkManager.firstConnectedDevice?.assertIdleListening()
}

// MARK: - Managed state
// MARK: G5 data

var transmitterStartTime: NSTimeInterval? = NSUserDefaults.standardUserDefaults().transmitterStartTime {
private var transmitterStartTime: NSTimeInterval? = NSUserDefaults.standardUserDefaults().transmitterStartTime {
didSet {
if oldValue != transmitterStartTime {
NSUserDefaults.standardUserDefaults().transmitterStartTime = transmitterStartTime
Expand All @@ -250,9 +253,42 @@ class DeviceDataManager: NSObject, CarbStoreDelegate, TransmitterDelegate, WCSes
return NSDate(timeIntervalSince1970: startTime).dateByAddingTimeInterval(NSTimeInterval(glucose.timestamp))
}

var latestPumpStatus: MySentryPumpStatusMessageBody?
/**
Attempts to backfill glucose data from the share servers if the G5 connection hasn't been established.
*/
private func backfillGlucoseFromShareIfNeeded() {
if self.latestGlucoseMessageDate == nil,
let shareClient = self.shareClient, glucoseStore = self.glucoseStore
{
// Load glucose from Share if our xDripG5 connection hasn't started
shareClient.fetchLast(1) { (error, glucose) in
if let error = error {
self.logger?.addError(error, fromSource: "ShareClient")
}

guard let glucose = glucose?.first else {
return
}

// Ignore glucose values that are less than a minute newer than our previous value
if let latestGlucose = glucoseStore.latestGlucose where latestGlucose.startDate.timeIntervalSinceDate(glucose.startDate) > -NSTimeInterval(minutes: 1) {
return
}

glucoseStore.addGlucose(glucose.quantity, date: glucose.startDate, displayOnly: false, device: nil) { (_, value, error) -> Void in
if let error = error {
self.logger?.addError(error, fromSource: "GlucoseStore")
}

NSNotificationCenter.defaultCenter().postNotificationName(self.dynamicType.GlucoseUpdatedNotification, object: self)
}
}
}
}

// MARK: - Configuration

var transmitterState: State<Transmitter> = .NeedsConfiguration {
private var transmitterState: State<Transmitter> = .NeedsConfiguration {
didSet {
switch transmitterState {
case .Ready(let transmitter):
Expand All @@ -263,7 +299,7 @@ class DeviceDataManager: NSObject, CarbStoreDelegate, TransmitterDelegate, WCSes
}
}

var connectedPeripheralIDs: Set<String> = Set(NSUserDefaults.standardUserDefaults().connectedPeripheralIDs) {
private var connectedPeripheralIDs: Set<String> = Set(NSUserDefaults.standardUserDefaults().connectedPeripheralIDs) {
didSet {
NSUserDefaults.standardUserDefaults().connectedPeripheralIDs = Array(connectedPeripheralIDs)
}
Expand Down Expand Up @@ -312,7 +348,7 @@ class DeviceDataManager: NSObject, CarbStoreDelegate, TransmitterDelegate, WCSes
}
}

func pumpStateValuesDidChange(note: NSNotification) {
@objc private func pumpStateValuesDidChange(note: NSNotification) {
switch note.userInfo?[PumpState.PropertyKey] as? String {
case "timeZone"?:
NSUserDefaults.standardUserDefaults().pumpTimeZone = pumpState?.timeZone
Expand Down
10 changes: 6 additions & 4 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ class LoopDataManager {
self.glucoseMomentumEffect = nil
self.notify()

// Try to troubleshoot communications errors
if let pumpStatusDate = self.deviceDataManager.latestPumpStatus?.pumpDateComponents.date where pumpStatusDate.timeIntervalSinceNow < NSTimeInterval(minutes: -15),
// Try to troubleshoot communications errors with the pump
if let pumpStatusDate = self.deviceDataManager.latestReservoirValue?.startDate where pumpStatusDate.timeIntervalSinceNow < NSTimeInterval(minutes: -15),
let device = self.deviceDataManager.rileyLinkManager.firstConnectedDevice where device.lastTuned?.timeIntervalSinceNow < NSTimeInterval(minutes: -15) {
device.tunePumpWithResultHandler { (result) in
switch result {
Expand All @@ -77,7 +77,9 @@ class LoopDataManager {
NSNotificationCenter.defaultCenter().postNotificationName(self.dynamicType.LoopRunningNotification, object: self)

// Sentry packets are sent in groups of 3, 5s apart. Wait 11s to avoid conflicting comms.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(11 * NSEC_PER_SEC)), self.dataAccessQueue) {
let waitTime = self.deviceDataManager.latestPumpStatus != nil ? Int64(11 * NSEC_PER_SEC) : 0

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, waitTime), self.dataAccessQueue) {
self.waitingForSentryPackets = false
self.insulinEffect = nil
self.loop()
Expand Down Expand Up @@ -314,7 +316,7 @@ class LoopDataManager {
private func updatePredictedGlucoseAndRecommendedBasal() throws {
guard let
glucose = self.deviceDataManager.glucoseStore?.latestGlucose,
pumpStatusDate = self.deviceDataManager.latestPumpStatus?.pumpDateComponents.date
pumpStatusDate = self.deviceDataManager.latestReservoirValue?.startDate
else
{
self.predictedGlucose = nil
Expand Down
10 changes: 5 additions & 5 deletions Loop/Managers/NotificationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ struct NotificationManager {
UIApplication.sharedApplication().presentLocalNotificationNow(notification)
}

static func sendPumpReservoirLowNotificationForAmount(units: Double, andTimeRemaining remaining: NSTimeInterval) {
static func sendPumpReservoirLowNotificationForAmount(units: Double, andTimeRemaining remaining: NSTimeInterval?) {
let notification = UILocalNotification()

notification.alertTitle = NSLocalizedString("Pump Reservoir Low", comment: "The notification title for a low pump reservoir")
Expand All @@ -144,12 +144,12 @@ struct NotificationManager {
intervalFormatter.includesApproximationPhrase = true
intervalFormatter.includesTimeRemainingPhrase = true

guard let timeString = intervalFormatter.stringFromTimeInterval(remaining) else {
return
if let remaining = remaining, timeString = intervalFormatter.stringFromTimeInterval(remaining) {
notification.alertBody = String(format: NSLocalizedString("%1$@ U left: %2$@", comment: "Low reservoir alert with time remaining format string. (1: Number of units remaining)(2: approximate time remaining)"), unitsString, timeString)
} else {
notification.alertBody = String(format: NSLocalizedString("%1$@ U left", comment: "Low reservoir alert format string. (1: Number of units remaining)"), unitsString)
}

notification.alertBody = NSLocalizedString("\(unitsString) U left: \(timeString)", comment: "")

notification.soundName = UILocalNotificationDefaultSoundName
notification.category = Category.PumpReservoirLow.rawValue

Expand Down
7 changes: 3 additions & 4 deletions Loop/View Controllers/StatusTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,9 @@ class StatusTableViewController: UITableViewController, UIGestureRecognizerDeleg
}
}

reservoirVolume = dataManager.latestReservoirValue?.unitVolume

if let status = dataManager.latestPumpStatus {
reservoirVolume = status.reservoirRemainingUnits
reservoirLevel = Double(status.reservoirRemainingPercent) / 100
batteryLevel = Double(status.batteryRemainingPercent) / 100
}
Expand Down Expand Up @@ -484,9 +485,7 @@ class StatusTableViewController: UITableViewController, UIGestureRecognizerDeleg
case .Date:
cell.textLabel?.text = NSLocalizedString("Last Sensor", comment: "The title of the cell containing the last updated sensor date")

if let glucose = dataManager.latestGlucoseMessage, startTime = dataManager.transmitterStartTime {
let date = NSDate(timeIntervalSince1970: startTime).dateByAddingTimeInterval(NSTimeInterval(glucose.timestamp))

if let date = dataManager.latestGlucoseMessageDate {
cell.detailTextLabel?.text = dateFormatter.stringFromDate(date)
} else {
cell.detailTextLabel?.text = emptyValueString
Expand Down