Skip to content

Commit

Permalink
Improvements in extension support, logging (#207)
Browse files Browse the repository at this point in the history
– Allow extension clients to disable observation queries
– Improved diagnostic logger sharing: supports attachments with a filename
– Fix retain cycles
  • Loading branch information
ps2 committed May 24, 2018
1 parent 7727bf8 commit 1f06e92
Show file tree
Hide file tree
Showing 22 changed files with 332 additions and 122 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -22,6 +22,7 @@ xcuserdata
*.moved-aside
*.xcuserstate
*.xcscmblueprint
project.xcworkspace

## Obj-C/Swift specific
*.hmap
Expand Down
34 changes: 34 additions & 0 deletions Extensions/NSData.swift
Expand Up @@ -9,7 +9,41 @@
import Foundation


// String conversion methods, adapted from https://stackoverflow.com/questions/40276322/hex-binary-string-conversion-in-swift/40278391#40278391
extension Data {
init?(hexadecimalString: String) {
self.init(capacity: hexadecimalString.utf16.count / 2)

// Convert 0 ... 9, a ... f, A ...F to their decimal value,
// return nil for all other input characters
func decodeNibble(u: UInt16) -> UInt8? {
switch u {
case 0x30 ... 0x39: // '0'-'9'
return UInt8(u - 0x30)
case 0x41 ... 0x46: // 'A'-'F'
return UInt8(u - 0x41 + 10) // 10 since 'A' is 10, not 0
case 0x61 ... 0x66: // 'a'-'f'
return UInt8(u - 0x61 + 10) // 10 since 'a' is 10, not 0
default:
return nil
}
}

var even = true
var byte: UInt8 = 0
for c in hexadecimalString.utf16 {
guard let val = decodeNibble(u: c) else { return nil }
if even {
byte = val << 4
} else {
byte += val
self.append(byte)
}
even = !even
}
guard even else { return nil }
}

var hexadecimalString: String {
return map { String(format: "%02hhx", $0) }.joined()
}
Expand Down
2 changes: 0 additions & 2 deletions LoopKit Example/Managers/DeviceDataManager.swift
Expand Up @@ -13,8 +13,6 @@ import LoopKit

class DeviceDataManager : CarbStoreDelegate {

static let shared = DeviceDataManager()

init() {
let healthStore = HKHealthStore()
let cacheStore = PersistenceController(directoryURL: FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!)
Expand Down
61 changes: 36 additions & 25 deletions LoopKit Example/MasterViewController.swift
Expand Up @@ -13,15 +13,15 @@ import LoopKitUI

class MasterViewController: UITableViewController, DailyValueScheduleTableViewControllerDelegate {

private var dataManager: DeviceDataManager {
get {
return DeviceDataManager.shared
}
}
private var dataManager: DeviceDataManager? = DeviceDataManager()

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

guard let dataManager = dataManager else {
return
}

let sampleTypes = Set([
dataManager.glucoseStore.sampleType,
dataManager.carbStore.sampleType,
Expand All @@ -35,9 +35,9 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo
dataManager.carbStore.healthStore.requestAuthorization(toShare: sampleTypes, read: sampleTypes) { (success, error) in
if success {
// Call the individual authorization methods to trigger query creation
self.dataManager.carbStore.authorize({ _ in })
self.dataManager.doseStore.insulinDeliveryStore.authorize({ _ in })
self.dataManager.glucoseStore.authorize({ _ in })
dataManager.carbStore.authorize({ _ in })
dataManager.doseStore.insulinDeliveryStore.authorize({ _ in })
dataManager.glucoseStore.authorize({ _ in })
}
}
}
Expand All @@ -57,8 +57,9 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo
case reservoir
case diagnostic
case generate
case reset

static let count = 4
static let count = 5
}

private enum ConfigurationRow: Int {
Expand Down Expand Up @@ -107,6 +108,8 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo
cell.textLabel?.text = NSLocalizedString("Diagnostic", comment: "The title for the cell displaying diagnostic data")
case .generate:
cell.textLabel?.text = NSLocalizedString("Generate Data", comment: "The title for the cell displaying data generation")
case .reset:
cell.textLabel?.text = NSLocalizedString("Reset", comment: "Title for the cell resetting the data manager")
}
}

Expand All @@ -123,9 +126,9 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo
let row = ConfigurationRow(rawValue: indexPath.row)!
switch row {
case .basalRate:
let scheduleVC = SingleValueScheduleTableViewController()
let scheduleVC = SingleValueScheduleTableViewController(style: .grouped)

if let profile = dataManager.basalRateSchedule {
if let profile = dataManager?.basalRateSchedule {
scheduleVC.timeZone = profile.timeZone
scheduleVC.scheduleItems = profile.items
}
Expand All @@ -139,14 +142,14 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo
scheduleVC.delegate = self
scheduleVC.title = sender?.textLabel?.text

if let schedule = dataManager.glucoseTargetRangeSchedule {
if let schedule = dataManager?.glucoseTargetRangeSchedule {
scheduleVC.timeZone = schedule.timeZone
scheduleVC.scheduleItems = schedule.items
scheduleVC.unit = schedule.unit
scheduleVC.overrideRanges = schedule.overrideRanges

show(scheduleVC, sender: sender)
} else {
} else if let dataManager = dataManager {
dataManager.glucoseStore.preferredUnit { (result) -> Void in
DispatchQueue.main.async {
switch result {
Expand All @@ -165,7 +168,7 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo
// textFieldVC.delegate = self
textFieldVC.title = sender?.textLabel?.text
textFieldVC.placeholder = NSLocalizedString("Enter the 6-digit pump ID", comment: "The placeholder text instructing users how to enter a pump ID")
textFieldVC.value = dataManager.pumpID
textFieldVC.value = dataManager?.pumpID
textFieldVC.keyboardType = .numberPad
textFieldVC.contextHelp = NSLocalizedString("The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N).", comment: "Instructions on where to find the pump ID on a Minimed pump")

Expand All @@ -178,18 +181,23 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo
case .reservoir:
performSegue(withIdentifier: InsulinDeliveryTableViewController.className, sender: sender)
case .diagnostic:
let vc = CommandResponseViewController(command: { (completionHandler) -> String in
let vc = CommandResponseViewController(command: { [weak self] (completionHandler) -> String in
let group = DispatchGroup()

guard let dataManager = self?.dataManager else {
completionHandler("")
return "nil"
}

var doseStoreResponse = ""
group.enter()
self.dataManager.doseStore.generateDiagnosticReport { (report) in
dataManager.doseStore.generateDiagnosticReport { (report) in
doseStoreResponse = report
group.leave()
}

var carbStoreResponse = ""
if let carbStore = self.dataManager.carbStore {
if let carbStore = dataManager.carbStore {
group.enter()
carbStore.generateDiagnosticReport { (report) in
carbStoreResponse = report
Expand All @@ -199,7 +207,7 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo

var glucoseStoreResponse = ""
group.enter()
self.dataManager.glucoseStore.generateDiagnosticReport { (report) in
dataManager.glucoseStore.generateDiagnosticReport { (report) in
glucoseStoreResponse = report
group.leave()
}
Expand All @@ -218,7 +226,7 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo

show(vc, sender: sender)
case .generate:
let vc = CommandResponseViewController(command: { (completionHandler) -> String in
let vc = CommandResponseViewController(command: { [weak self] (completionHandler) -> String in
let group = DispatchGroup()

var unitVolume = 150.0
Expand All @@ -231,7 +239,7 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo
unitVolume -= (drand48() * 2.0)

group.enter()
self.dataManager.doseStore.addReservoirValue(unitVolume, at: Date(timeIntervalSinceNow: index)) { (_, _, _, error) in
self?.dataManager?.doseStore.addReservoirValue(unitVolume, at: Date(timeIntervalSinceNow: index)) { (_, _, _, error) in
group.leave()
}
}
Expand All @@ -245,6 +253,9 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo
vc.title = sender?.textLabel?.text

show(vc, sender: sender)
case .reset:
dataManager = nil
tableView.reloadData()
}
}
}
Expand All @@ -262,14 +273,14 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo

switch targetViewController {
case let vc as CarbEntryTableViewController:
vc.carbStore = dataManager.carbStore
vc.carbStore = dataManager?.carbStore
case let vc as CarbEntryEditViewController:
if let carbStore = dataManager.carbStore {
if let carbStore = dataManager?.carbStore {
vc.defaultAbsorptionTimes = carbStore.defaultAbsorptionTimes
vc.preferredUnit = carbStore.preferredUnit
}
case let vc as InsulinDeliveryTableViewController:
vc.doseStore = dataManager.doseStore
vc.doseStore = dataManager?.doseStore
default:
break
}
Expand All @@ -284,11 +295,11 @@ class MasterViewController: UITableViewController, DailyValueScheduleTableViewCo
switch ConfigurationRow(rawValue: indexPath.row)! {
case .basalRate:
if let controller = controller as? SingleValueScheduleTableViewController {
dataManager.basalRateSchedule = BasalRateSchedule(dailyItems: controller.scheduleItems, timeZone: controller.timeZone)
dataManager?.basalRateSchedule = BasalRateSchedule(dailyItems: controller.scheduleItems, timeZone: controller.timeZone)
}
case .glucoseTargetRange:
if let controller = controller as? GlucoseRangeScheduleTableViewController {
dataManager.glucoseTargetRangeSchedule = GlucoseRangeSchedule(unit: controller.unit, dailyItems: controller.scheduleItems, timeZone: controller.timeZone, overrideRanges: controller.overrideRanges)
dataManager?.glucoseTargetRangeSchedule = GlucoseRangeSchedule(unit: controller.unit, dailyItems: controller.scheduleItems, timeZone: controller.timeZone, overrideRanges: controller.overrideRanges)
}
/*case let row:
if let controller = controller as? DailyQuantityScheduleTableViewController {
Expand Down
27 changes: 17 additions & 10 deletions LoopKit/CarbKit/CarbStore.swift
Expand Up @@ -147,6 +147,7 @@ public final class CarbStore: HealthKitSampleStore {
public init(
healthStore: HKHealthStore,
cacheStore: PersistenceController,
observationEnabled: Bool = true,
defaultAbsorptionTimes: DefaultAbsorptionTimes = defaultAbsorptionTimes,
carbRatioSchedule: CarbRatioSchedule? = nil,
insulinSensitivitySchedule: InsulinSensitivitySchedule? = nil,
Expand All @@ -162,9 +163,9 @@ public final class CarbStore: HealthKitSampleStore {
self.delta = calculationDelta
self.delay = effectDelay

super.init(healthStore: healthStore, type: carbType, observationStart: Date(timeIntervalSinceNow: -defaultAbsorptionTimes.slow * 2))
super.init(healthStore: healthStore, type: carbType, observationStart: Date(timeIntervalSinceNow: -defaultAbsorptionTimes.slow * 2), observationEnabled: observationEnabled)

cacheStore.onReady { [unowned self] (error) in
cacheStore.onReady { (error) in
guard error == nil else { return }

// Migrate modifiedCarbEntries and deletedCarbEntryIDs
Expand All @@ -180,7 +181,7 @@ public final class CarbStore: HealthKitSampleStore {
object.externalID = externalID
}

try? self.cacheStore.managedObjectContext.save()
self.cacheStore.save()
}

UserDefaults.standard.purgeLegacyCarbEntryKeys()
Expand Down Expand Up @@ -212,6 +213,7 @@ public final class CarbStore: HealthKitSampleStore {
if let samples = added as? [HKQuantitySample] {
for sample in samples {
if self.addCachedObject(for: sample) {
self.log.debug("Saved sample %@ into cache from HKAnchoredObjectQuery", sample.uuid.uuidString)
notificationRequired = true
}
}
Expand All @@ -220,13 +222,14 @@ public final class CarbStore: HealthKitSampleStore {
// Remove deleted samples
for sample in deleted {
if self.deleteCachedObject(for: sample) {
self.log.debug("Deleted sample %@ from cache from HKAnchoredObjectQuery", sample.uuid.uuidString)
notificationRequired = true
}
}

// Notify listeners only if a meaningful change was made
if notificationRequired {
try? self.cacheStore.managedObjectContext.save()
self.cacheStore.save()
self.syncExternalDB()

NotificationCenter.default.post(name: .CarbEntriesDidUpdate, object: self, userInfo: [CarbStore.notificationUpdateSourceKey: UpdateSource.queriedByHealthKit.rawValue])
Expand Down Expand Up @@ -348,6 +351,7 @@ extension CarbStore {
NotificationCenter.default.post(name: .CarbEntriesDidUpdate, object: self, userInfo: [CarbStore.notificationUpdateSourceKey: UpdateSource.changedInApp.rawValue])
self.syncExternalDB()
} else if let error = error {
self.log.error("Error saving entry %@: %@", sample.uuid.uuidString, String(describing: error))
completion(.failure(.healthStoreError(error)))
} else {
assertionFailure()
Expand All @@ -373,6 +377,7 @@ extension CarbStore {
NotificationCenter.default.post(name: .CarbEntriesDidUpdate, object: self, userInfo: [CarbStore.notificationUpdateSourceKey: UpdateSource.changedInApp.rawValue])
self.syncExternalDB()
} else if let error = error {
self.log.error("Error replacing entry %@: %@", oldEntry.sampleUUID.uuidString, String(describing: error))
completion(.failure(.healthStoreError(error)))
} else {
assertionFailure()
Expand All @@ -396,6 +401,7 @@ extension CarbStore {
NotificationCenter.default.post(name: .CarbEntriesDidUpdate, object: self, userInfo: [CarbStore.notificationUpdateSourceKey: UpdateSource.changedInApp.rawValue])
self.syncExternalDB()
} else if let error = error {
self.log.error("Error deleting entry %@: %@", entry.sampleUUID.uuidString, String(describing: error))
completion(.failure(.healthStoreError(error)))
} else {
assertionFailure()
Expand Down Expand Up @@ -454,7 +460,7 @@ extension CarbStore {
let object = CachedCarbObject(context: self.cacheStore.managedObjectContext)
object.update(from: entry)

try? self.cacheStore.managedObjectContext.save()
self.cacheStore.save()
created = true
}

Expand All @@ -470,7 +476,7 @@ extension CarbStore {
object.uploadState = .notUploaded
}

try? self.cacheStore.managedObjectContext.save()
self.cacheStore.save()
}
}

Expand Down Expand Up @@ -501,7 +507,7 @@ extension CarbStore {
deleted = true
}

try? self.cacheStore.managedObjectContext.save()
self.cacheStore.save()
}

return deleted
Expand Down Expand Up @@ -581,7 +587,7 @@ extension CarbStore {
objectsToDelete.forEach { $0.uploadState = .uploading }
}

try? self.cacheStore.managedObjectContext.save()
self.cacheStore.save()
}

if entriesToUpload.count > 0 {
Expand All @@ -603,13 +609,14 @@ extension CarbStore {
entry.startDate > self.earliestCacheDate,
let externalID = entry.externalID
{
self.log.info("Uploaded entry %@ not found in cache", entry.sampleUUID.uuidString)
let deleted = DeletedCarbObject(context: self.cacheStore.managedObjectContext)
deleted.externalID = externalID
hasMissingObjects = true
}
}

try? self.cacheStore.managedObjectContext.save()
self.cacheStore.save()

if hasMissingObjects {
self.queue.async {
Expand Down Expand Up @@ -638,7 +645,7 @@ extension CarbStore {
}
}

try? self.cacheStore.managedObjectContext.save()
self.cacheStore.save()
}
}
}
Expand Down

0 comments on commit 1f06e92

Please sign in to comment.