Skip to content

Instantly share code, notes, and snippets.

@mddub
Created October 4, 2016 01:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mddub/7514cc172d74f7c6e2269699db3fd958 to your computer and use it in GitHub Desktop.
Save mddub/7514cc172d74f7c6e2269699db3fd958 to your computer and use it in GitHub Desktop.
//
// 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