Created
October 4, 2016 01:38
-
-
Save mddub/7514cc172d74f7c6e2269699db3fd958 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// UrchinDataManager.swift | |
// Loop | |
// | |
// Created by Mark Wilson on 8/22/16. | |
// Copyright © 2016 Nathan Racklyeft. All rights reserved. | |
// | |
import Foundation | |
import CarbKit | |
import HealthKit | |
import InsulinKit | |
import LoopKit | |
import PebbleKit | |
class UrchinDataManager: NSObject, PBPebbleCentralDelegate, PBWatchDelegate { | |
// Urchin | |
let MESSAGE_KEY_msgType = 0 | |
let MESSAGE_KEY_recency = 1 | |
let MESSAGE_KEY_sgvs = 3 | |
let MESSAGE_KEY_lastSgv = 4 | |
let MESSAGE_KEY_trend = 5 | |
let MESSAGE_KEY_delta = 6 | |
let MESSAGE_KEY_statusText = 7 | |
// Meal input | |
// let MESSAGE_KEY_msgType = 0 (duplicated above) | |
let MESSAGE_KEY_value = 1 | |
let MESSAGE_KEY_statusString = 2 | |
let MSG_TYPE_CARB_INPUT: Int32 = 0 | |
let MSG_TYPE_LOOP_STATE: Int32 = 1 | |
let MSG_TYPE_SUCCESS: Int32 = 2 | |
let MSG_TYPE_FAILED: Int32 = 3 | |
// TODO should be 72 | |
let HISTORY_LENGTH = 84 | |
unowned let deviceDataManager: DeviceDataManager | |
// for now we need our own cache of glucose because HealthKit data becomes inaccessible when the phone is locked | |
var localGlucoseCache: [GlucoseValue] | |
var pebbleCentral: PBPebbleCentral! | |
var activeWatch: PBWatch? | |
let urchinWatchfaceUUID = NSUUID(UUIDString: "ea361603-0373-4865-9824-8f52c65c6e07")! | |
let inputAppUUID = NSUUID(UUIDString: "58327bfb-c6d9-4aed-9e1a-9b33b2a1de99")! | |
init(deviceDataManager: DeviceDataManager) { | |
self.deviceDataManager = deviceDataManager | |
self.localGlucoseCache = [] | |
self.activeWatch = nil | |
super.init() | |
UIDevice.currentDevice().batteryMonitoringEnabled = true | |
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(loopDataUpdated(_:)), name: LoopDataManager.LoopDataUpdatedNotification, object: deviceDataManager.loopManager) | |
pebbleCentral = PBPebbleCentral.defaultCentral() | |
//pebbleCentral.addAppUUID(urchinWatchfaceUUID) | |
//pebbleCentral.addAppUUID(inputAppUUID) | |
//pebbleCentral.delegate = self | |
//pebbleCentral.run() | |
} | |
@objc func loopDataUpdated(note: NSNotification) { | |
if let rawContext = note.userInfo?[LoopDataManager.LoopUpdateContextKey] as? LoopDataManager.LoopUpdateContext.RawValue, | |
let context = LoopDataManager.LoopUpdateContext(rawValue: rawContext) { | |
// TODO maybe limit status updates to a subset of events (glucose, bolus, carbs, temp basal?) | |
print("context \(context)") | |
} | |
/* | |
self.getStatus() { (status, pastGlucose) in | |
self.sendToUrchin(self.formatStatusBarString(status), pastGlucose: pastGlucose) | |
} | |
*/ | |
sendLoopStatusToInput() | |
} | |
struct PebbleLoopStatus { | |
let timeString: String | |
let iobString: String | |
let cobString: String | |
let cobSeriesString: String | |
let lastBGString: String | |
let lastBGRecencyString: String | |
let evBGString: String | |
let currentTempString: String | |
let batteryString: String | |
let recommendedBolusString: String | |
} | |
private func getStatus(completion: (PebbleLoopStatus, [GlucoseValue]) -> Void) { | |
guard let glucoseStore = self.deviceDataManager.glucoseStore, | |
let carbStore = self.deviceDataManager.carbStore else { | |
return | |
} | |
deviceDataManager.loopManager.getLoopStatus { (predictedGlucose, recommendedTempBasal, lastTempBasal, lastLoopCompleted, insulinOnBoard, loopError) in | |
glucoseStore.getRecentGlucoseValues(startDate: NSDate(timeIntervalSinceNow: NSTimeInterval(minutes: -5 * Double(self.HISTORY_LENGTH + 1)))) { (recentGlucose, getGlucoseValuesError) in | |
if getGlucoseValuesError != nil { | |
self.deviceDataManager.logger.addError(getGlucoseValuesError!, fromSource: "UrchinDataManager") | |
} else { | |
print("got \(recentGlucose.count) recentGlucose") | |
for glucose in recentGlucose { | |
if !self.localGlucoseCache.contains({$0.startDate == glucose.startDate}) { | |
self.localGlucoseCache.append(glucose) | |
} | |
} | |
self.localGlucoseCache.sortInPlace({$0.startDate < $1.startDate}) | |
if self.localGlucoseCache.count > self.HISTORY_LENGTH { | |
self.localGlucoseCache.removeFirst(self.localGlucoseCache.count - self.HISTORY_LENGTH) | |
} | |
carbStore.getCarbsOnBoardValues(startDate:NSDate()) { (cobValues, cobError) in | |
if cobError != nil { | |
self.deviceDataManager.logger.addError(cobError!, fromSource: "UrchinDataManager") | |
} else { | |
self.deviceDataManager.loopManager.getRecommendedBolus() { (units, recommendedBolusError) in | |
if recommendedBolusError != nil { | |
self.deviceDataManager.logger.addError(recommendedBolusError!, fromSource: "UrchinDataManager") | |
} else { | |
completion(self.formatStatus(insulinOnBoard, cobValues: cobValues, predictedGlucose: predictedGlucose, lastTempBasal: lastTempBasal, pastGlucose: self.localGlucoseCache, recommendedBolus: units), self.localGlucoseCache) | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
private func formatStatus(insulinOnBoard: InsulinValue?, cobValues: [CarbValue], predictedGlucose: [GlucoseValue]?, lastTempBasal: DoseEntry?, pastGlucose: [GlucoseValue], recommendedBolus: Double?) -> PebbleLoopStatus { | |
// TODO match locale, or show recency instead | |
let timeFormatter = NSDateFormatter() | |
timeFormatter.dateFormat = "h:mm" | |
timeFormatter.AMSymbol = "a" | |
timeFormatter.PMSymbol = "p" | |
let formattedTime = timeFormatter.stringFromDate(NSDate()) | |
let insulinFormatter = NSNumberFormatter() | |
insulinFormatter.numberStyle = .DecimalStyle | |
insulinFormatter.usesSignificantDigits = false | |
insulinFormatter.minimumFractionDigits = 1 | |
insulinFormatter.maximumFractionDigits = 1 | |
let iobString: String | |
if let insulinOnBoard = insulinOnBoard, | |
roundedIob = insulinFormatter.stringFromNumber(insulinOnBoard.value) { | |
iobString = "\(roundedIob)U" | |
} else { | |
iobString = "" | |
} | |
let cobString: String | |
let cobSeriesString: String | |
if let cob = cobValues.first { | |
cobString = " \(formatCarbs(cob))g" | |
if cobValues.count >= 3 { | |
cobSeriesString = " \(formatCarbs(cobValues[0])) / \(formatCarbs(cobValues[1])) / \(formatCarbs(cobValues[2])) g" | |
} else { | |
cobSeriesString = cobString | |
} | |
} else { | |
cobString = "" | |
cobSeriesString = "" | |
} | |
let evBGString: String | |
if let predictedGlucose = predictedGlucose, | |
last = predictedGlucose.last { | |
evBGString = "\(formatGlucose(last))" | |
} else { | |
evBGString = "" | |
} | |
let currentTempString: String | |
let basalRateFormatter = NSNumberFormatter() | |
basalRateFormatter.numberStyle = .DecimalStyle | |
basalRateFormatter.usesSignificantDigits = false | |
basalRateFormatter.minimumFractionDigits = 2 | |
basalRateFormatter.maximumFractionDigits = 2 | |
if let tempBasal = lastTempBasal where tempBasal.unit == .unitsPerHour { | |
let remaining = Int(round(tempBasal.endDate.timeIntervalSinceNow.minutes)) | |
if let formattedRate = basalRateFormatter.stringFromNumber(tempBasal.value) where remaining > 0 { | |
currentTempString = "\(formattedRate) " | |
} else { | |
currentTempString = "" | |
} | |
} else { | |
currentTempString = "" | |
} | |
let batteryString: String | |
let device = UIDevice.currentDevice() | |
if device.batteryMonitoringEnabled { | |
batteryString = " \(Int(device.batteryLevel * 100))%%" | |
} else { | |
batteryString = "" | |
} | |
let lastBGString: String | |
let lastBGRecencyString: String | |
if let lastBG = pastGlucose.last { | |
let delta: Int? | |
if pastGlucose.count > 1 && pastGlucose.last!.startDate.timeIntervalSinceDate(pastGlucose[pastGlucose.count - 2].startDate).minutes < 10 { | |
delta = formatGlucose(pastGlucose.last!) - formatGlucose(pastGlucose[pastGlucose.count - 2]) | |
} else { | |
delta = nil | |
} | |
let deltaString: String | |
if let delta = delta { | |
deltaString = " " + (delta < 0 ? "-" : "+") + String(delta) | |
} else { | |
deltaString = "" | |
} | |
//lastBGString = " \(formatGlucose(lastBG))" + deltaString | |
lastBGString = " \(formatGlucose(lastBG))" | |
lastBGRecencyString = " (\(Int(round(-lastBG.startDate.timeIntervalSinceNow.minutes))))" | |
} else { | |
lastBGString = "" | |
lastBGRecencyString = "" | |
} | |
let recBolusString: String | |
if let units = recommendedBolus, | |
formattedBolus = insulinFormatter.stringFromNumber(units) { | |
recBolusString = "\(formattedBolus)U" | |
} else { | |
recBolusString = "" | |
} | |
return PebbleLoopStatus( | |
timeString: formattedTime, | |
iobString: iobString, | |
cobString: cobString, | |
cobSeriesString: cobSeriesString, | |
lastBGString: lastBGString, | |
lastBGRecencyString: lastBGRecencyString, | |
evBGString: evBGString, | |
currentTempString: currentTempString, | |
batteryString: batteryString, | |
recommendedBolusString: recBolusString | |
) | |
} | |
private func formatStatusBarString(s: PebbleLoopStatus) -> String { | |
return "\(s.timeString)\(s.batteryString)\(s.evBGString)\n\(s.iobString)\(s.cobString)\(s.currentTempString)" | |
} | |
private func sendToUrchin(status: String, pastGlucose: [GlucoseValue]) { | |
let recency: Int | |
let lastSgv: Int | |
let sgvs: [UInt8] | |
let trend: Int | |
let delta: Int | |
if let last = pastGlucose.last { | |
recency = -Int(round(last.startDate.timeIntervalSinceNow.minutes * 60.0)) | |
lastSgv = formatGlucose(last) | |
sgvs = graphArray(pastGlucose) | |
if let lastTrend = self.deviceDataManager.sensorInfo?.trendType { | |
// TODO maybe need to check that this is the same value as `last` | |
trend = lastTrend.rawValue | |
} else { | |
trend = 0 | |
} | |
if pastGlucose.count > 1 && last.startDate.timeIntervalSinceDate(pastGlucose[pastGlucose.count - 2].startDate).minutes < 10 { | |
delta = formatGlucose(last) - formatGlucose(pastGlucose[pastGlucose.count - 2]) | |
} else { | |
delta = 0 | |
} | |
} else { | |
recency = 99999 | |
lastSgv = 0 | |
sgvs = [0] | |
trend = 0 | |
delta = 0 | |
} | |
let update = [ | |
MESSAGE_KEY_msgType: NSNumber(int32: 1), | |
MESSAGE_KEY_recency: NSNumber(int32: Int32(recency)), | |
MESSAGE_KEY_sgvs: NSData(bytes: sgvs, length: sgvs.count), | |
MESSAGE_KEY_lastSgv: NSNumber(int32: Int32(lastSgv)), | |
MESSAGE_KEY_trend: NSNumber(int32: Int32(trend)), | |
MESSAGE_KEY_delta: NSNumber(int32: Int32(delta)), | |
MESSAGE_KEY_statusText: status, | |
] | |
sendAppMessage(update, withUUID: self.urchinWatchfaceUUID) | |
} | |
private func graphArray(pastGlucose: [GlucoseValue]) -> [UInt8] { | |
let endDate: NSDate | |
if let lastGlucose = pastGlucose.last { | |
endDate = lastGlucose.startDate | |
} else { | |
endDate = NSDate() | |
} | |
return (0...(HISTORY_LENGTH - 1)).map { | |
let x = endDate.dateByAddingTimeInterval(NSTimeInterval(minutes: -5 * Double($0))) | |
let nearby = pastGlucose.filter { abs($0.startDate.timeIntervalSinceDate(x).minutes) <= 2.5 } | |
if let match = nearby.first { | |
return UInt8(min(255, max(0, formatGlucose(match) / 2))) | |
} else { | |
return 0 | |
} | |
} | |
} | |
private func formatGlucose(glucoseValue: GlucoseValue) -> Int { | |
return Int(round(glucoseValue.quantity.doubleValueForUnit(HKUnit.milligramsPerDeciliterUnit()))) | |
} | |
private func formatCarbs(carbValue: CarbValue) -> Int { | |
return Int(round(carbValue.quantity.doubleValueForUnit(HKUnit.gramUnit()))) | |
} | |
@objc func pebbleCentral(central: PBPebbleCentral, watchDidConnect watch: PBWatch, isNew: Bool) { | |
print("Hi, \(watch.name)!") | |
guard activeWatch == nil else { | |
return | |
} | |
activeWatch = watch | |
dispatch_async(dispatch_get_main_queue()) { | |
watch.appMessagesAddReceiveUpdateHandler(self.handleWatchInput, withUUID: self.inputAppUUID) | |
} | |
// TODO maybe do this after a delay | |
// apparently we need to prime the comms first | |
// sendAppMessage([:], withUUID: self.inputAppUUID) | |
} | |
@objc func pebbleCentral(central: PBPebbleCentral, watchDidDisconnect watch: PBWatch) { | |
print("Bye, \(watch.name)!") | |
guard activeWatch == watch else { | |
return | |
} | |
activeWatch = nil | |
} | |
func sendAppMessage(message: [NSNumber : AnyObject], withUUID uuid: NSUUID) { | |
guard let watch = activeWatch else { | |
print("Can't send message: no Pebble watch connected") | |
return | |
} | |
print("sending \(message)") | |
dispatch_async(dispatch_get_main_queue()) { | |
watch.appMessagesPushUpdate(message, withUUID: uuid) { (watch, _, error) -> Void in | |
// TODO, may have to come back to background thread if using DiagnosticLogger | |
// TODO the annotation should be NSError?, need to do something about that... | |
} | |
} | |
} | |
func sendInputSuccess(success: Bool) { | |
self.sendAppMessage([self.MESSAGE_KEY_msgType: NSNumber(int32: success ? MSG_TYPE_SUCCESS : MSG_TYPE_FAILED)], withUUID: self.inputAppUUID) | |
} | |
func handleWatchInput(watch: PBWatch, message: [NSNumber : AnyObject]) -> Bool { | |
print("received \(message)") | |
guard let carbStore = self.deviceDataManager.carbStore, | |
let requestTypeTuple = message[MESSAGE_KEY_msgType], | |
let requestType = requestTypeTuple as? NSNumber else { | |
self.sendInputSuccess(false) | |
return true | |
} | |
if requestType.intValue == MSG_TYPE_CARB_INPUT { | |
guard let valueTuple = message[MESSAGE_KEY_value], | |
carbs = valueTuple as? NSNumber else { | |
self.sendInputSuccess(false) | |
return true | |
} | |
let newEntry = NewCarbEntry( | |
quantity: HKQuantity(unit: carbStore.preferredUnit, doubleValue: carbs.doubleValue), | |
startDate: NSDate(), | |
foodType: nil, | |
absorptionTime: NSTimeInterval(minutes: 180) | |
) | |
deviceDataManager.loopManager.addCarbEntryAndRecommendBolus(newEntry) { (units, error) in | |
if let error = error where !(error is LoopError) { | |
print(error) | |
self.sendInputSuccess(false) | |
} else { | |
AnalyticsManager.sharedManager.didAddCarbsFromWatch(carbs.doubleValue) | |
self.sendInputSuccess(true) | |
} | |
} | |
} else if requestType.intValue == MSG_TYPE_LOOP_STATE { | |
sendLoopStatusToInput() | |
} | |
return true | |
} | |
func sendLoopStatusToInput() { | |
self.getStatus() { (status, _) in | |
var state = "" | |
state += status.timeString + "\n" | |
state += status.batteryString + "\n" | |
state += status.lastBGString + status.lastBGRecencyString + " -> " + status.evBGString + "\n" | |
state += "Temp: " + status.currentTempString + "\n" | |
state += "Rec bolus: " + status.recommendedBolusString + "\n" | |
state += "IOB: " + status.iobString + "\n" | |
state += "COB: " + status.cobString + "\n" | |
/* | |
self.sendAppMessage([ | |
self.MESSAGE_KEY_msgType: NSNumber(int32: self.MSG_TYPE_LOOP_STATE), | |
self.MESSAGE_KEY_statusString: state, | |
], withUUID: self.inputAppUUID) | |
*/ | |
let notification = UILocalNotification() | |
notification.alertTitle = status.currentTempString + status.iobString + status.cobString + status.batteryString | |
notification.alertBody = NSLocalizedString(status.timeString + status.lastBGString + " ->" + status.evBGString, comment: "foo") | |
notification.soundName = UILocalNotificationDefaultSoundName | |
notification.category = NotificationManager.Category.LoopNotRunning.rawValue | |
UIApplication.sharedApplication().presentLocalNotificationNow(notification) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment