diff --git a/.gitignore b/.gitignore index 15e0e61d3..8c43aaa07 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ DerivedData *.ipa *.xcuserstate *.xcscmblueprint +project.xcworkspace .DS_Store *.log diff --git a/.travis.yml b/.travis.yml index 0aeaf06f6..4fbfbf211 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: objective-c -osx_image: xcode9 -xcode_sdk: iphonesimulator11 +osx_image: xcode9.3 xcode_project: RileyLink.xcodeproj xcode_scheme: RileyLink script: diff --git a/RileyLinkKit/Extensions/IdentifiableClass.swift b/Common/IdentifiableClass.swift similarity index 100% rename from RileyLinkKit/Extensions/IdentifiableClass.swift rename to Common/IdentifiableClass.swift diff --git a/MinimedKit/Extensions/NSData.swift b/Common/NSData.swift similarity index 89% rename from MinimedKit/Extensions/NSData.swift rename to Common/NSData.swift index 02075ac2c..c5cc9a66b 100644 --- a/MinimedKit/Extensions/NSData.swift +++ b/Common/NSData.swift @@ -10,8 +10,10 @@ import Foundation extension Data { - func to(_: T.Type) -> T { - return self.withUnsafeBytes { $0.pointee } + func to(_: T.Type) -> T { + return self.withUnsafeBytes { (bytes: UnsafePointer) in + return T(littleEndian: bytes.pointee) + } } } diff --git a/Common/NumberFormatter.swift b/Common/NumberFormatter.swift new file mode 100644 index 000000000..676a21f61 --- /dev/null +++ b/Common/NumberFormatter.swift @@ -0,0 +1,18 @@ +// +// NumberFormatter.swift +// RileyLink +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import Foundation + +extension NumberFormatter { + func decibleString(from decibles: Int?) -> String? { + if let decibles = decibles, let formatted = string(from: NSNumber(value: decibles)) { + return String(format: NSLocalizedString("%@ dB", comment: "Unit format string for an RSSI value in decibles"), formatted) + } else { + return nil + } + } +} diff --git a/Common/OSLog.swift b/Common/OSLog.swift new file mode 100644 index 000000000..e49507632 --- /dev/null +++ b/Common/OSLog.swift @@ -0,0 +1,44 @@ +// +// OSLog.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import os.log + + +extension OSLog { + convenience init(category: String) { + self.init(subsystem: "com.ps2.rileylink", category: category) + } + + func debug(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .debug, args) + } + + func info(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .info, args) + } + + func error(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .error, args) + } + + private func log(_ message: StaticString, type: OSLogType, _ args: [CVarArg]) { + switch args.count { + case 0: + os_log(message, log: self, type: type) + case 1: + os_log(message, log: self, type: type, args[0]) + case 2: + os_log(message, log: self, type: type, args[0], args[1]) + case 3: + os_log(message, log: self, type: type, args[0], args[1], args[2]) + case 4: + os_log(message, log: self, type: type, args[0], args[1], args[2], args[3]) + default: + os_log(message, log: self, type: type, args) + } + } +} diff --git a/Common/TimeInterval.swift b/Common/TimeInterval.swift index fc7251e7c..5a8046a6c 100644 --- a/Common/TimeInterval.swift +++ b/Common/TimeInterval.swift @@ -10,6 +10,10 @@ import Foundation extension TimeInterval { + static func hours(_ hours: Double) -> TimeInterval { + return self.init(hours: hours) + } + static func minutes(_ minutes: Int) -> TimeInterval { return self.init(minutes: Double(minutes)) } @@ -18,6 +22,14 @@ extension TimeInterval { return self.init(minutes: minutes) } + static func seconds(_ seconds: Double) -> TimeInterval { + return self.init(seconds) + } + + static func milliseconds(_ milliseconds: Double) -> TimeInterval { + return self.init(milliseconds / 1000) + } + init(minutes: Double) { self.init(minutes * 60) } @@ -26,6 +38,14 @@ extension TimeInterval { self.init(minutes: hours * 60) } + init(seconds: Double) { + self.init(seconds) + } + + init(milliseconds: Double) { + self.init(milliseconds / 1000) + } + var milliseconds: Double { return self * 1000 } diff --git a/RileyLinkKit/Extensions/TimeZone.swift b/Common/TimeZone.swift similarity index 100% rename from RileyLinkKit/Extensions/TimeZone.swift rename to Common/TimeZone.swift diff --git a/Crypto/Info.plist b/Crypto/Info.plist index fd435638f..d87c8b3c8 100644 --- a/Crypto/Info.plist +++ b/Crypto/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.2.2 + 2.0.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/Crypto/es.lproj/InfoPlist.strings b/Crypto/es.lproj/InfoPlist.strings new file mode 100644 index 000000000..f8e9a2b43 --- /dev/null +++ b/Crypto/es.lproj/InfoPlist.strings @@ -0,0 +1,3 @@ +/* (No Comment) */ +"CFBundleName" = "$(PRODUCT_NAME)"; + diff --git a/Crypto/ru.lproj/InfoPlist.strings b/Crypto/ru.lproj/InfoPlist.strings new file mode 100644 index 000000000..874e8a453 --- /dev/null +++ b/Crypto/ru.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* No Localized Strings */ diff --git a/MinimedKit/BasalSchedule.swift b/MinimedKit/BasalSchedule.swift index e2bc62285..28ad48219 100644 --- a/MinimedKit/BasalSchedule.swift +++ b/MinimedKit/BasalSchedule.swift @@ -13,7 +13,7 @@ public struct BasalScheduleEntry { public let timeOffset: TimeInterval public let rate: Double // U/hour - internal init(index: Int, timeOffset: TimeInterval, rate: Double) { + public init(index: Int, timeOffset: TimeInterval, rate: Double) { self.index = index self.timeOffset = timeOffset self.rate = rate @@ -23,37 +23,94 @@ public struct BasalScheduleEntry { public struct BasalSchedule { public let entries: [BasalScheduleEntry] - - public init(data: Data) { - let beginPattern = Data(bytes: [0, 0, 0]) - let endPattern = Data(bytes: [0, 0, 0x3F]) - var acc = [BasalScheduleEntry]() - + + public init(entries: [BasalScheduleEntry]) { + self.entries = entries + } +} + + +extension BasalSchedule { + static let rawValueLength = 192 + public typealias RawValue = Data + + public init?(rawValue: RawValue) { + var entries = [BasalScheduleEntry]() + for tuple in sequence(first: (index: 0, offset: 0), next: { (index: $0.index + 1, $0.offset + 3) }) { let beginOfRange = tuple.offset let endOfRange = beginOfRange + 3 - - guard endOfRange < data.count else { + + guard endOfRange < rawValue.count else { break } - - let section = data.subdata(in: beginOfRange..= entry.timeOffset { + // Stop if the new timeOffset isn't greater than the last one + break + } + + entries.append(entry) + } else { + // Stop if we can't decode the entry break } + } - let rate = Double(section.subdata(in: 0..<2).to(UInt16.self)) / 40.0 - let offsetMinutes = Double(section[2]) * 30 - - let newBasalScheduleEntry = BasalScheduleEntry( - index: tuple.index, - timeOffset: TimeInterval(minutes: offsetMinutes), - rate: rate - ) - acc.append(newBasalScheduleEntry) + guard entries.count > 0 else { + return nil } - self.entries = acc + + self.init(entries: entries) } + public var rawValue: RawValue { + var buffer = Data(count: BasalSchedule.rawValueLength) + var byteIndex = 0 + + for rawEntry in entries.map({ $0.rawValue }) { + buffer.replaceSubrange(byteIndex..<(byteIndex + rawEntry.count), with: rawEntry) + byteIndex += rawEntry.count + } + + return buffer + } +} + + +private extension BasalScheduleEntry { + static let rawValueLength = 3 + typealias RawValue = Data + + init?(index: Int, rawValue: RawValue) { + guard rawValue.count == BasalScheduleEntry.rawValueLength else { + return nil + } + + let rawRate = rawValue[rawValue.startIndex.. UInt8 { - - var crc: UInt8 = 0 +public extension Sequence where Element == UInt8 { - var pdata = (data as NSData).bytes.bindMemory(to: UInt8.self, capacity: data.count) - var nbytes = data.count - /* loop over the buffer data */ - while nbytes > 0 { - crc = crcTable[Int((crc ^ pdata.pointee) & 0xff)] - pdata = pdata.successor() - nbytes -= 1 + public func crc8() -> UInt8 { + + var crc: UInt8 = 0 + for byte in self { + crc = crcTable[Int((crc ^ byte) & 0xff)] + } + return crc } - return crc } - diff --git a/MinimedKit/Extensions/Int.swift b/MinimedKit/Extensions/Int.swift index 6ee5519dc..6d91d309a 100644 --- a/MinimedKit/Extensions/Int.swift +++ b/MinimedKit/Extensions/Int.swift @@ -10,7 +10,7 @@ import Foundation extension Int { - init(bigEndianBytes bytes: T) where T.Iterator.Element == UInt8, T.IndexDistance == Int { + init(bigEndianBytes bytes: T) where T.Element == UInt8 { assert(bytes.count <= 4) var result: UInt = 0 diff --git a/MinimedKit/Extensions/NSDateFormatter.swift b/MinimedKit/Extensions/NSDateFormatter.swift index a2a93fcd0..4d05d770c 100644 --- a/MinimedKit/Extensions/NSDateFormatter.swift +++ b/MinimedKit/Extensions/NSDateFormatter.swift @@ -10,6 +10,7 @@ import Foundation extension DateFormatter { + // TODO: Replace with Foundation.ISO8601DateFormatter class func ISO8601DateFormatter() -> Self { let formatter = self.init() formatter.calendar = Calendar(identifier: Calendar.Identifier.iso8601) diff --git a/MinimedKit/FourByteSixByteEncoding.swift b/MinimedKit/FourByteSixByteEncoding.swift new file mode 100644 index 000000000..82bb13a50 --- /dev/null +++ b/MinimedKit/FourByteSixByteEncoding.swift @@ -0,0 +1,69 @@ +// +// FourByteSixByteEncoding.swift +// RileyLink +// +// Created by Pete Schwamb on 2/27/16. +// Copyright © 2016 Pete Schwamb. All rights reserved. +// + +import Foundation + +fileprivate let codes = [21, 49, 50, 35, 52, 37, 38, 22, 26, 25, 42, 11, 44, 13, 14, 28] + +fileprivate let codesRev = Dictionary(uniqueKeysWithValues: codes.enumerated().map({ ($1, UInt8($0)) })) + +public extension Sequence where Element == UInt8 { + + public func decode4b6b() -> [UInt8]? { + var buffer = [UInt8]() + var availBits = 0 + var bitAccumulator = 0 + for byte in self { + if byte == 0 { + break + } + + bitAccumulator = (bitAccumulator << 8) + Int(byte) + availBits += 8 + if availBits >= 12 { + guard let hiNibble = codesRev[bitAccumulator >> (availBits - 6)], + let loNibble = codesRev[(bitAccumulator >> (availBits - 12)) & 0b111111] + else { + return nil + } + let decoded = UInt8((hiNibble << 4) + loNibble) + buffer.append(decoded) + availBits -= 12 + bitAccumulator = bitAccumulator & (0xffff >> (16-availBits)) + } + } + return buffer + } + + public func encode4b6b() -> [UInt8] { + var buffer = [UInt8]() + var bitAccumulator = 0x0 + var bitcount = 0 + for byte in self { + bitAccumulator <<= 6 + bitAccumulator |= codes[Int(byte >> 4)] + bitcount += 6 + + bitAccumulator <<= 6 + bitAccumulator |= codes[Int(byte & 0x0f)] + bitcount += 6 + + while bitcount >= 8 { + buffer.append(UInt8(bitAccumulator >> (bitcount-8)) & 0xff) + bitcount -= 8 + bitAccumulator &= (0xffff >> (16-bitcount)) + } + } + if bitcount > 0 { + bitAccumulator <<= (8-bitcount) + buffer.append(UInt8(bitAccumulator) & 0xff) + } + return buffer + } +} + diff --git a/MinimedKit/HistoryPage.swift b/MinimedKit/HistoryPage.swift index 6442cdd38..a8b3c9040 100644 --- a/MinimedKit/HistoryPage.swift +++ b/MinimedKit/HistoryPage.swift @@ -8,7 +8,7 @@ import Foundation -public class HistoryPage { +public struct HistoryPage { public enum HistoryPageError: Error { case invalidCRC @@ -16,7 +16,12 @@ public class HistoryPage { } public let events: [PumpEvent] - + + // Useful interface for testing + init(events: [PumpEvent]) { + self.events = events + } + public init(pageData: Data, pumpModel: PumpModel) throws { guard checkCRC16(pageData) else { diff --git a/MinimedKit/Info.plist b/MinimedKit/Info.plist index 0eb186c06..7e7479f00 100644 --- a/MinimedKit/Info.plist +++ b/MinimedKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.2.2 + 2.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/MinimedKit/MessageBody.swift b/MinimedKit/MessageBody.swift index aeb209499..a221af9dc 100644 --- a/MinimedKit/MessageBody.swift +++ b/MinimedKit/MessageBody.swift @@ -22,13 +22,6 @@ public protocol MessageBody { } -extension MessageBody { - static var emptyBuffer: [UInt8] { - return [UInt8](repeating: 0, count: self.length) - } -} - - public protocol DictionaryRepresentable { var dictionaryRepresentation: [String: Any] { get diff --git a/MinimedKit/MessageType.swift b/MinimedKit/MessageType.swift index f3de05951..1704ea4a8 100644 --- a/MinimedKit/MessageType.swift +++ b/MinimedKit/MessageType.swift @@ -19,15 +19,48 @@ public enum MessageType: UInt8 { case writeGlucoseHistoryTimestamp = 0x28 case changeTime = 0x40 case bolus = 0x42 + + case PumpExperiment_OP67 = 0x43 + case PumpExperiment_OP68 = 0x44 + case PumpExperiment_OP69 = 0x45 + + case selectBasalProfile = 0x4a + case changeTempBasal = 0x4c + + case PumpExperiment_OP80 = 0x50 + case PumpExperiment_OP81 = 0x51 + case PumpExperiment_OP82 = 0x52 + case PumpExperiment_OP83 = 0x53 + case PumpExperiment_OP84 = 0x54 + case PumpExperiment_OP85 = 0x55 + case PumpExperiment_OP86 = 0x56 + case PumpExperiment_OP87 = 0x57 + case PumpExperiment_OP88 = 0x58 + case PumpExperiment_OP89 = 0x59 + case PumpExperiment_OP90 = 0x5a + case buttonPress = 0x5b + + case PumpExperiment_OP92 = 0x5c + case powerOn = 0x5d + + case PumpExperiment_OP97 = 0x61 + case PumpExperiment_OP98 = 0x62 + case PumpExperiment_OP99 = 0x63 + case PumpExperiment_O100 = 0x64 + case PumpExperiment_O101 = 0x65 + case PumpExperiment_O103 = 0x67 + case readTime = 0x70 case getBattery = 0x72 case readRemainingInsulin = 0x73 case getHistoryPage = 0x80 case getPumpModel = 0x8d case readProfileSTD512 = 0x92 + case readProfileA512 = 0x93 + case readProfileB512 = 0x94 case readTempBasal = 0x98 case getGlucosePage = 0x9A case readCurrentPageNumber = 0x9d @@ -61,6 +94,10 @@ public enum MessageType: UInt8 { return GetPumpModelCarelinkMessageBody.self case .readProfileSTD512: return DataFrameMessageBody.self + case .readProfileA512: + return DataFrameMessageBody.self + case .readProfileB512: + return DataFrameMessageBody.self case .getHistoryPage: return GetHistoryPageCarelinkMessageBody.self case .getBattery: diff --git a/MinimedKit/Messages/DataFrameMessageBody.swift b/MinimedKit/Messages/DataFrameMessageBody.swift index daf994637..98472652e 100644 --- a/MinimedKit/Messages/DataFrameMessageBody.swift +++ b/MinimedKit/Messages/DataFrameMessageBody.swift @@ -8,15 +8,71 @@ import Foundation -public class DataFrameMessageBody : CarelinkLongMessageBody { - public let lastFrameFlag: Bool +public class DataFrameMessageBody: CarelinkLongMessageBody { + public let isLastFrame: Bool public let frameNumber: Int public let contents: Data public required init?(rxData: Data) { - self.lastFrameFlag = rxData[0] & 0x80 != 0 - self.frameNumber = Int(rxData[0] & 0x7f) + guard rxData.count == type(of: self).length else { + return nil + } + + self.isLastFrame = rxData[0] & 0b1000_0000 != 0 + self.frameNumber = Int(rxData[0] & 0b0111_1111) self.contents = rxData.subdata(in: (1.. [DataFrameMessageBody] { + var frames = [DataFrameMessageBody]() + let frameContentsSize = DataFrameMessageBody.length - 1 + + for frameNumber in sequence(first: 0, next: { $0 + 1 }) { + let startIndex = frameNumber * frameContentsSize + var endIndex = startIndex + frameContentsSize + var isLastFrame = false + + if endIndex >= contents.count { + isLastFrame = true + endIndex = contents.count + } + + frames.append(DataFrameMessageBody( + frameNumber: frameNumber + 1, + isLastFrame: isLastFrame, + contents: contents[startIndex..) -> UInt32 in - return UInt32(bigEndian: bytes.pointee) - }) + + self.pageNum = rxData[1..<5 + ].withUnsafeBytes { UInt32(bigEndian: $0.pointee) } self.glucose = Int(rxData[6] as UInt8) self.isig = Int(rxData[8] as UInt8) diff --git a/MinimedKit/Messages/ReadSettingsCarelinkMessageBody.swift b/MinimedKit/Messages/ReadSettingsCarelinkMessageBody.swift index 858543d7f..bbd59393b 100644 --- a/MinimedKit/Messages/ReadSettingsCarelinkMessageBody.swift +++ b/MinimedKit/Messages/ReadSettingsCarelinkMessageBody.swift @@ -10,6 +10,7 @@ import Foundation public enum BasalProfile { + typealias RawValue = UInt8 case standard case profileA @@ -25,6 +26,17 @@ public enum BasalProfile { self = .standard } } + + var rawValue: RawValue { + switch self { + case .standard: + return 0 + case .profileA: + return 1 + case .profileB: + return 2 + } + } } diff --git a/MinimedKit/Messages/ReadTempBasalCarelinkMessageBody.swift b/MinimedKit/Messages/ReadTempBasalCarelinkMessageBody.swift index a9f455154..5f04f6c4e 100644 --- a/MinimedKit/Messages/ReadTempBasalCarelinkMessageBody.swift +++ b/MinimedKit/Messages/ReadTempBasalCarelinkMessageBody.swift @@ -39,10 +39,6 @@ public class ReadTempBasalCarelinkMessageBody: CarelinkLongMessageBody { let rawRate: UInt8 = rxData[2] rate = Double(rawRate) default: - timeRemaining = 0 - rate = 0 - rateType = .absolute - super.init(rxData: rxData) return nil } diff --git a/MinimedKit/Messages/SelectBasalProfileMessageBody.swift b/MinimedKit/Messages/SelectBasalProfileMessageBody.swift new file mode 100644 index 000000000..6cf3dbd1d --- /dev/null +++ b/MinimedKit/Messages/SelectBasalProfileMessageBody.swift @@ -0,0 +1,14 @@ +// +// SelectBasalProfileMessageBody.swift +// MinimedKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import Foundation + +public class SelectBasalProfileMessageBody: CarelinkLongMessageBody { + public convenience init(newProfile: BasalProfile) { + self.init(rxData: Data(bytes: [1, newProfile.rawValue]))! + } +} diff --git a/MinimedKit/MinimedPacket.swift b/MinimedKit/MinimedPacket.swift new file mode 100644 index 000000000..9c97bdeaf --- /dev/null +++ b/MinimedKit/MinimedPacket.swift @@ -0,0 +1,44 @@ +// +// MinimedPacket.swift +// RileyLinkBLEKit +// +// Created by Pete Schwamb on 10/7/17. +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import Foundation + +public struct MinimedPacket { + public let data: Data + + public init(outgoingData: Data) { + self.data = outgoingData + } + + public init?(encodedData: Data) { + + if let decoded = encodedData.decode4b6b() { + if decoded.count == 0 { + return nil + } + let msg = decoded.prefix(upTo: (decoded.count - 1)) + if decoded.last != msg.crc8() { + // CRC invalid + return nil + } + self.data = Data(msg) + } else { + // Could not decode message + return nil + } + } + + public func encodedData() -> Data { + var dataWithCRC = self.data + dataWithCRC.append(data.crc8()) + var encodedData = dataWithCRC.encode4b6b() + encodedData.append(0) + return Data(encodedData) + } +} + diff --git a/MinimedKit/PumpEvents/BasalProfileStartPumpEvent.swift b/MinimedKit/PumpEvents/BasalProfileStartPumpEvent.swift index 106ec9356..35c2af481 100644 --- a/MinimedKit/PumpEvents/BasalProfileStartPumpEvent.swift +++ b/MinimedKit/PumpEvents/BasalProfileStartPumpEvent.swift @@ -25,7 +25,7 @@ public struct BasalProfileStartPumpEvent: TimestampedPumpEvent { rawData = availableData.subdata(in: 0.. = [21: 0, 49: 1, 50: 2, 35: 3, 52: 4, 37: 5, 38: 6, 22: 7, 26: 8, 25: 9, 42: 10, 11: 11, 44: 12, 13: 13, 14: 14, 28: 15] - -private let codes = [21,49,50,35,52,37,38,22,26,25,42,11,44,13,14,28] - -public func decode4b6b(_ rawData: Data) -> Data? { - var buffer = [UInt8]() - var availBits = 0 - var x = 0 - for byte in rawData { - x = (x << 8) + Int(byte) - availBits += 8 - if availBits >= 12 { - guard let - hiNibble = codesRev[x >> (availBits - 6)], - let loNibble = codesRev[(x >> (availBits - 12)) & 0b111111] - else { - return nil - } - let decoded = UInt8((hiNibble << 4) + loNibble) - buffer.append(decoded) - availBits -= 12 - x = x & (0xffff >> (16-availBits)) - } - } - return Data(bytes: buffer) -} - -public func encode4b6b(_ rawData: Data) -> Data { - var buffer = [UInt8]() - var acc = 0x0 - var bitcount = 0 - for byte in rawData { - acc <<= 6 - acc |= codes[Int(byte >> 4)] - bitcount += 6 - - acc <<= 6 - acc |= codes[Int(byte & 0x0f)] - bitcount += 6 - - while bitcount >= 8 { - buffer.append(UInt8(acc >> (bitcount-8)) & 0xff) - bitcount -= 8 - acc &= (0xffff >> (16-bitcount)) - } - } - if bitcount > 0 { - acc <<= (8-bitcount) - buffer.append(UInt8(acc) & 0xff) - } - return Data(bytes: buffer) -} - diff --git a/MinimedKit/es.lproj/InfoPlist.strings b/MinimedKit/es.lproj/InfoPlist.strings new file mode 100644 index 000000000..f8e9a2b43 --- /dev/null +++ b/MinimedKit/es.lproj/InfoPlist.strings @@ -0,0 +1,3 @@ +/* (No Comment) */ +"CFBundleName" = "$(PRODUCT_NAME)"; + diff --git a/MinimedKit/es.lproj/Localizable.strings b/MinimedKit/es.lproj/Localizable.strings new file mode 100644 index 000000000..ac542e3e4 --- /dev/null +++ b/MinimedKit/es.lproj/Localizable.strings @@ -0,0 +1,42 @@ +/* The description of AlarmClockReminderPumpEvent */ +"AlarmClockReminder" = "AlarmClockReminder"; + +/* The description of AlarmSensorPumpEvent */ +"AlarmSensor" = "AlarmSensor"; + +/* Describing the battery chemistry as Alkaline */ +"Alkaline" = "Alcalina"; + +/* The format string description of a BasalProfileStartPumpEvent. (1: The index of the profile)(2: The basal rate) */ +"Basal Profile %1$@: %2$@ U/hour" = "Perfil Basal %1$@: %2$@ U/hora"; + +/* Pump error code when bolus is in progress */ +"Bolus in progress" = "Bolo en progreso"; + +/* Pump error code returned when command refused */ +"Command refused" = "Comando rechazado"; + +/* Describing the battery chemistry as Lithium */ +"Lithium" = "Litio"; + +/* Pump error code describing max setting exceeded */ +"Max setting exceeded" = "Ajuste máximo excedido"; + +/* Describing the North America pump region */ +"North America" = "Norte America"; + +/* The format string describing a pump message. (1: The packet type)(2: The message type)(3: The message address)(4: The message data */ +"PumpMessage(%1$@, %2$@, %3$@, %4$@)" = "MensageMicroinfusadora(%1$@, %2$@, %3$@, %4$@)"; + +/* The format string description of a TempBasalPumpEvent. (1: The rate of the temp basal in minutes) */ +"Temporary Basal: %1$.3f U/hour" = "Basal Temporal: %1$.3f U/hora"; + +/* The format string description of a TempBasalDurationPumpEvent. (1: The duration of the temp basal in minutes) */ +"Temporary Basal: %1$d min" = "Basal Temporal: %1$d min"; + +/* The format string description of a TempBasalPumpEvent. (1: The rate of the temp basal in percent) */ +"Temporary Basal: %1$d%%" = "Basal Temporal: %1$d%%"; + +/* Describing the worldwide pump region */ +"World-Wide" = "Mundial"; + diff --git a/MinimedKit/ru.lproj/InfoPlist.strings b/MinimedKit/ru.lproj/InfoPlist.strings new file mode 100644 index 000000000..874e8a453 --- /dev/null +++ b/MinimedKit/ru.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* No Localized Strings */ diff --git a/MinimedKit/ru.lproj/Localizable.strings b/MinimedKit/ru.lproj/Localizable.strings new file mode 100644 index 000000000..848a1af69 --- /dev/null +++ b/MinimedKit/ru.lproj/Localizable.strings @@ -0,0 +1,42 @@ +/* The description of AlarmClockReminderPumpEvent */ +"AlarmClockReminder" = "Напоминание будильника"; + +/* The description of AlarmSensorPumpEvent */ +"AlarmSensor" = "Предупреждение от сенсора"; + +/* Describing the battery chemistry as Alkaline */ +"Alkaline" = "Щелочная"; + +/* The format string description of a BasalProfileStartPumpEvent. (1: The index of the profile)(2: The basal rate) */ +"Basal Profile %1$@: %2$@ U/hour" = "Профиль базала %1$@: %2$@ ед/ч"; + +/* Pump error code when bolus is in progress */ +"Bolus in progress" = "Подается болюс"; + +/* Pump error code returned when command refused */ +"Command refused" = "Отказ в выполнении команды"; + +/* Describing the battery chemistry as Lithium */ +"Lithium" = "Литиевая"; + +/* Pump error code describing max setting exceeded */ +"Max setting exceeded" = "Максимальное значение превышено"; + +/* Describing the North America pump region */ +"North America" = "Сев Америка"; + +/* The format string describing a pump message. (1: The packet type)(2: The message type)(3: The message address)(4: The message data */ +"PumpMessage(%1$@, %2$@, %3$@, %4$@)" = "Сообщение помпы(%1$@, %2$@, %3$@, %4$@)"; + +/* The format string description of a TempBasalPumpEvent. (1: The rate of the temp basal in minutes) */ +"Temporary Basal: %1$.3f U/hour" = "Временный базал: %1$.3f ед/ч"; + +/* The format string description of a TempBasalDurationPumpEvent. (1: The duration of the temp basal in minutes) */ +"Temporary Basal: %1$d min" = "Временный базал: %1$d мин"; + +/* The format string description of a TempBasalPumpEvent. (1: The rate of the temp basal in percent) */ +"Temporary Basal: %1$d%%" = "Временный базал: %1$d%%"; + +/* Describing the worldwide pump region */ +"World-Wide" = "Глобальный"; + diff --git a/MinimedKitTests/BasalScheduleTests.swift b/MinimedKitTests/BasalScheduleTests.swift index 2b9dc4718..2bfe61ca7 100644 --- a/MinimedKitTests/BasalScheduleTests.swift +++ b/MinimedKitTests/BasalScheduleTests.swift @@ -11,11 +11,14 @@ import XCTest class BasalScheduleTests: XCTestCase { - func testBasicConversion() { + var sampleData: Data { let sampleDataString = "06000052000178050202000304000402000504000602000704000802000904000a02000b04000c02000d02000e02000f040010020011040012020013040014020015040016020017040018020019000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - - let rxData = Data(hexadecimalString: sampleDataString)! - let profile = BasalSchedule(data: rxData) + + return Data(hexadecimalString: sampleDataString)! + } + + func testBasicConversion() { + let profile = BasalSchedule(rawValue: sampleData)! XCTAssertEqual(profile.entries.count, 26) @@ -39,5 +42,26 @@ class BasalScheduleTests: XCTestCase { XCTAssertEqual(basalSchedule[25].index, 25) XCTAssertEqual(basalSchedule[25].timeOffset, TimeInterval(minutes: 750)) XCTAssertEqual(basalSchedule[25].rate, 0.05, accuracy: .ulpOfOne) + + XCTAssertEqual(sampleData.hexadecimalString, profile.rawValue.hexadecimalString) + } + + func testTxData() { + let profile = BasalSchedule(entries: [ + BasalScheduleEntry(index: 0, timeOffset: .hours(0), rate: 1.0), + BasalScheduleEntry(index: 1, timeOffset: .hours(4), rate: 2.0), + ]) + + XCTAssertEqual("280000500008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", profile.rawValue.hexadecimalString) + } + + func testDataFrameParsing() { + let frames = DataFrameMessageBody.dataFramesFromContents(sampleData) + + XCTAssertEqual("0106000052000178050202000304000402000504000602000704000802000904000a02000b04000c02000d02000e02000f04001002001104001202001304001402", frames[0].txData.hexadecimalString) + XCTAssertEqual("0200150400160200170400180200190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", frames[1].txData.hexadecimalString) + XCTAssertEqual("8300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", frames[2].txData.hexadecimalString) + + XCTAssertEqual(3, frames.count) } } diff --git a/MinimedKitTests/CRC8Tests.swift b/MinimedKitTests/CRC8Tests.swift index 6f4d1abf5..d0dd7e4f4 100644 --- a/MinimedKitTests/CRC8Tests.swift +++ b/MinimedKitTests/CRC8Tests.swift @@ -9,11 +9,10 @@ import XCTest @testable import MinimedKit - class CRC8Tests: XCTestCase { func testComputeCRC8() { let input = Data(hexadecimalString: "a259705504a24117043a0e080b003d3d00015b030105d817790a0f00000300008b1702000e080b0000")! - XCTAssertEqual(0x71, computeCRC8(input)) + XCTAssertEqual(0x71, input.crc8()) } } diff --git a/MinimedKitTests/Info.plist b/MinimedKitTests/Info.plist index e4ede6701..2d0a14240 100644 --- a/MinimedKitTests/Info.plist +++ b/MinimedKitTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.2 + 2.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/MinimedKitTests/RFToolsTests.swift b/MinimedKitTests/MinimedPacketTests.swift similarity index 50% rename from MinimedKitTests/RFToolsTests.swift rename to MinimedKitTests/MinimedPacketTests.swift index d3a750b3a..14141923f 100644 --- a/MinimedKitTests/RFToolsTests.swift +++ b/MinimedKitTests/MinimedPacketTests.swift @@ -1,5 +1,5 @@ // -// RFToolsTests.swift +// MinimedPacketTests.swift // RileyLink // // Created by Pete Schwamb on 2/27/16. @@ -9,37 +9,34 @@ import XCTest @testable import MinimedKit -class RFToolsTests: XCTestCase { +class MinimedPacketTests: XCTestCase { func testDecode4b6b() { let input = Data(hexadecimalString: "ab2959595965574ab2d31c565748ea54e55a54b5558cd8cd55557194b56357156535ac5659956a55c55555556355555568bc5657255554e55a54b5555555b100")! - - let result = decode4b6b(input) - - if let result = result { - let expectedOutput = Data(hexadecimalString: "a259705504a24117043a0e080b003d3d00015b030105d817790a0f00000300008b1702000e080b000071") - XCTAssertTrue(result == expectedOutput) + let packet = MinimedPacket(encodedData: input) + if let result = packet?.data { + let expectedOutput = Data(hexadecimalString: "a259705504a24117043a0e080b003d3d00015b030105d817790a0f00000300008b1702000e080b0000") + XCTAssertEqual(result, expectedOutput) } else { - XCTFail("\(String(describing: result)) is nil") + XCTFail("Unable to decode packet data") } } func testDecode4b6bWithBadData() { - let input = Data(hexadecimalString: "0102030405")! - - let result = decode4b6b(input) - XCTAssertTrue(result == nil) + let packet = MinimedPacket(encodedData: Data(hexadecimalString: "0102030405")!) + XCTAssertNil(packet) } - + func testInvalidCRC() { + let inputWithoutCRC = Data(hexadecimalString: "a259705504a24117043a0e080b003d3d00015b030105d817790a0f00000300008b1702000e080b0000")! + let packet = MinimedPacket(encodedData: Data(inputWithoutCRC.encode4b6b())) + XCTAssertNil(packet) + } + func testEncode4b6b() { let input = Data(hexadecimalString: "a259705504a24117043a0e080b003d3d00015b030105d817790a0f00000300008b1702000e080b000071")! - - let result = encode4b6b(input) - - let expectedOutput = Data(hexadecimalString: "ab2959595965574ab2d31c565748ea54e55a54b5558cd8cd55557194b56357156535ac5659956a55c55555556355555568bc5657255554e55a54b5555555b1") - XCTAssertTrue(result == expectedOutput) + let packet = MinimedPacket(outgoingData: input) + let expectedOutput = Data(hexadecimalString: "ab2959595965574ab2d31c565748ea54e55a54b5558cd8cd55557194b56357156535ac5659956a55c55555556355555568bc5657255554e55a54b5555555b1555000") + XCTAssertEqual(packet.encodedData(), expectedOutput) } - - } diff --git a/MinimedKitTests/NSStringExtensions.swift b/MinimedKitTests/NSStringExtensions.swift index 2d76acf5f..99985b139 100644 --- a/MinimedKitTests/NSStringExtensions.swift +++ b/MinimedKitTests/NSStringExtensions.swift @@ -10,7 +10,7 @@ import Foundation extension String { func leftPadding(toLength: Int, withPad character: Character) -> String { - let newLength = self.characters.count + let newLength = self.count if newLength < toLength { return String(repeatElement(character, count: toLength - newLength)) + self } else { diff --git a/NightscoutUploadKit/Info.plist b/NightscoutUploadKit/Info.plist index 0eb186c06..7e7479f00 100644 --- a/NightscoutUploadKit/Info.plist +++ b/NightscoutUploadKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.2.2 + 2.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/NightscoutUploadKit/NSUserDefaults.swift b/NightscoutUploadKit/NSUserDefaults.swift deleted file mode 100644 index fc8cc2c06..000000000 --- a/NightscoutUploadKit/NSUserDefaults.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// NSUserDefaults.swift -// RileyLink -// -// Created by Pete Schwamb on 6/23/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -import Foundation - - -extension UserDefaults { - private enum Key: String { - case LastStoredTreatmentTimestamp = "com.rileylink.NightscoutUploadKit.LastStoredTreatmentTimestamp" - } - - var lastStoredTreatmentTimestamp: Date? { - get { - return object(forKey: Key.LastStoredTreatmentTimestamp.rawValue) as? Date - } - set { - set(newValue, forKey: Key.LastStoredTreatmentTimestamp.rawValue) - } - } -} diff --git a/NightscoutUploadKit/NightscoutUploader.swift b/NightscoutUploadKit/NightscoutUploader.swift index e0a3d6c88..31a741dc1 100644 --- a/NightscoutUploadKit/NightscoutUploader.swift +++ b/NightscoutUploadKit/NightscoutUploader.swift @@ -6,7 +6,6 @@ // Copyright © 2016 Pete Schwamb. All rights reserved. // -import UIKit import MinimedKit import Crypto @@ -38,33 +37,14 @@ public class NightscoutUploader { private(set) var treatmentsQueue = [NightscoutTreatment]() private(set) var lastMeterMessageRxTime: Date? - - public private(set) var observingPumpEventsSince: Date! - - private(set) var lastStoredTreatmentTimestamp: Date? { - get { - return UserDefaults.standard.lastStoredTreatmentTimestamp - } - set { - UserDefaults.standard.lastStoredTreatmentTimestamp = newValue - } - } public var errorHandler: ((_ error: Error, _ context: String) -> Void)? - private var dataAccessQueue: DispatchQueue = DispatchQueue(label: "com.rileylink.NightscoutUploadKit.dataAccessQueue", attributes: []) - - - public func reset() { - observingPumpEventsSince = Date(timeIntervalSinceNow: TimeInterval(hours: -24)) - lastStoredTreatmentTimestamp = nil - } + private var dataAccessQueue: DispatchQueue = DispatchQueue(label: "com.rileylink.NightscoutUploadKit.dataAccessQueue", qos: .utility) public init(siteURL: URL, APISecret: String) { self.siteURL = siteURL self.apiSecret = APISecret - - observingPumpEventsSince = lastStoredTreatmentTimestamp ?? Date(timeIntervalSinceNow: TimeInterval(hours: -24)) } // MARK: - Processing data from pump @@ -77,33 +57,6 @@ public class NightscoutUploader { - parameter pumpModel: The pump model info associated with the events */ public func processPumpEvents(_ events: [TimestampedHistoryEvent], source: String, pumpModel: PumpModel) { - - // Find valid event times - let newestEventTime = events.last?.date - - // Find the oldest event that might still be updated. - var oldestUpdatingEventDate: Date? - - for event in events { - switch event.pumpEvent { - case let bolus as BolusNormalPumpEvent: - let deliveryFinishDate = event.date.addingTimeInterval(bolus.duration) - if newestEventTime == nil || deliveryFinishDate.compare(newestEventTime!) == .orderedDescending { - // This event might still be updated. - oldestUpdatingEventDate = event.date - break - } - default: - continue - } - } - - if oldestUpdatingEventDate != nil { - observingPumpEventsSince = oldestUpdatingEventDate! - } else if newestEventTime != nil { - observingPumpEventsSince = newestEventTime! - } - for treatment in NightscoutPumpEvents.translate(events, eventSource: source) { treatmentsQueue.append(treatment) } @@ -476,10 +429,8 @@ public class NightscoutUploader { self.errorHandler?(error, "Uploading nightscout treatment records") // Requeue self.treatmentsQueue.append(contentsOf: inFlight) - case .success(_): - if let last = inFlight.last { - self.lastStoredTreatmentTimestamp = last.timestamp - } + case .success: + break } } } diff --git a/NightscoutUploadKit/es.lproj/InfoPlist.strings b/NightscoutUploadKit/es.lproj/InfoPlist.strings new file mode 100644 index 000000000..f8e9a2b43 --- /dev/null +++ b/NightscoutUploadKit/es.lproj/InfoPlist.strings @@ -0,0 +1,3 @@ +/* (No Comment) */ +"CFBundleName" = "$(PRODUCT_NAME)"; + diff --git a/NightscoutUploadKit/ru.lproj/InfoPlist.strings b/NightscoutUploadKit/ru.lproj/InfoPlist.strings new file mode 100644 index 000000000..874e8a453 --- /dev/null +++ b/NightscoutUploadKit/ru.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* No Localized Strings */ diff --git a/NightscoutUploadKitTests/Info.plist b/NightscoutUploadKitTests/Info.plist index f14c03683..f931463ef 100644 --- a/NightscoutUploadKitTests/Info.plist +++ b/NightscoutUploadKitTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.2 + 2.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/RileyLink.xcodeproj/project.pbxproj b/RileyLink.xcodeproj/project.pbxproj index 25ac15119..b9d9710d7 100644 --- a/RileyLink.xcodeproj/project.pbxproj +++ b/RileyLink.xcodeproj/project.pbxproj @@ -10,59 +10,70 @@ 2B19B9881DF3EF68006AB65F /* NewTimePumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B19B9871DF3EF68006AB65F /* NewTimePumpEvent.swift */; }; 2F962EBF1E678BAA0070EFBD /* PumpOpsSynchronousTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F962EBE1E678BAA0070EFBD /* PumpOpsSynchronousTests.swift */; }; 2F962EC11E6872170070EFBD /* TimestampedHistoryEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F962EC01E6872170070EFBD /* TimestampedHistoryEventTests.swift */; }; - 2F962EC31E6873A10070EFBD /* PumpOpsCommunication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F962EC21E6873A10070EFBD /* PumpOpsCommunication.swift */; }; + 2F962EC31E6873A10070EFBD /* PumpMessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F962EC21E6873A10070EFBD /* PumpMessageSender.swift */; }; 2F962EC51E705C6E0070EFBD /* PumpOpsSynchronousBuildFromFramesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F962EC41E705C6D0070EFBD /* PumpOpsSynchronousBuildFromFramesTests.swift */; }; 2F962EC81E7074E60070EFBD /* BolusNormalPumpEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F962EC71E7074E60070EFBD /* BolusNormalPumpEventTests.swift */; }; 2F962ECA1E70831F0070EFBD /* PumpModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F962EC91E70831F0070EFBD /* PumpModelTests.swift */; }; 2FDE1A071E57B12D00B56A27 /* ReadCurrentPageNumberMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FDE1A061E57B12D00B56A27 /* ReadCurrentPageNumberMessageBody.swift */; }; - 430D64CE1CB855AB00FCA750 /* RileyLinkBLEKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 430D64CD1CB855AB00FCA750 /* RileyLinkBLEKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 430D64D51CB855AB00FCA750 /* RileyLinkBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 430D64CB1CB855AB00FCA750 /* RileyLinkBLEKit.framework */; }; - 430D64DC1CB855AB00FCA750 /* RileyLinkBLEKitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 430D64DB1CB855AB00FCA750 /* RileyLinkBLEKitTests.m */; }; - 430D64E01CB855AB00FCA750 /* RileyLinkBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 430D64CB1CB855AB00FCA750 /* RileyLinkBLEKit.framework */; }; - 430D64E11CB855AB00FCA750 /* RileyLinkBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 430D64CB1CB855AB00FCA750 /* RileyLinkBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 430D64EC1CB85A4300FCA750 /* RileyLinkBLEDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 430D64E81CB85A4300FCA750 /* RileyLinkBLEDevice.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 430D64ED1CB85A4300FCA750 /* RileyLinkBLEDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = 430D64E91CB85A4300FCA750 /* RileyLinkBLEDevice.m */; }; - 430D64EE1CB85A4300FCA750 /* RileyLinkBLEManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 430D64EA1CB85A4300FCA750 /* RileyLinkBLEManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 430D64EF1CB85A4300FCA750 /* RileyLinkBLEManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 430D64EB1CB85A4300FCA750 /* RileyLinkBLEManager.m */; }; - 430D65001CB89FC000FCA750 /* CmdBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 430D64F01CB89FC000FCA750 /* CmdBase.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 430D65011CB89FC000FCA750 /* CmdBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 430D64F11CB89FC000FCA750 /* CmdBase.m */; }; - 430D65021CB89FC000FCA750 /* GetPacketCmd.h in Headers */ = {isa = PBXBuildFile; fileRef = 430D64F21CB89FC000FCA750 /* GetPacketCmd.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 430D65031CB89FC000FCA750 /* GetPacketCmd.m in Sources */ = {isa = PBXBuildFile; fileRef = 430D64F31CB89FC000FCA750 /* GetPacketCmd.m */; }; - 430D65041CB89FC000FCA750 /* GetVersionCmd.h in Headers */ = {isa = PBXBuildFile; fileRef = 430D64F41CB89FC000FCA750 /* GetVersionCmd.h */; }; - 430D65051CB89FC000FCA750 /* GetVersionCmd.m in Sources */ = {isa = PBXBuildFile; fileRef = 430D64F51CB89FC000FCA750 /* GetVersionCmd.m */; }; - 430D65061CB89FC000FCA750 /* ReceivingPacketCmd.h in Headers */ = {isa = PBXBuildFile; fileRef = 430D64F61CB89FC000FCA750 /* ReceivingPacketCmd.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 430D65071CB89FC000FCA750 /* ReceivingPacketCmd.m in Sources */ = {isa = PBXBuildFile; fileRef = 430D64F71CB89FC000FCA750 /* ReceivingPacketCmd.m */; }; - 430D65081CB89FC000FCA750 /* RFPacket.h in Headers */ = {isa = PBXBuildFile; fileRef = 430D64F81CB89FC000FCA750 /* RFPacket.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 430D65091CB89FC000FCA750 /* RFPacket.m in Sources */ = {isa = PBXBuildFile; fileRef = 430D64F91CB89FC000FCA750 /* RFPacket.m */; }; - 430D650A1CB89FC000FCA750 /* SendAndListenCmd.h in Headers */ = {isa = PBXBuildFile; fileRef = 430D64FA1CB89FC000FCA750 /* SendAndListenCmd.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 430D650B1CB89FC000FCA750 /* SendAndListenCmd.m in Sources */ = {isa = PBXBuildFile; fileRef = 430D64FB1CB89FC000FCA750 /* SendAndListenCmd.m */; }; - 430D650C1CB89FC000FCA750 /* SendPacketCmd.h in Headers */ = {isa = PBXBuildFile; fileRef = 430D64FC1CB89FC000FCA750 /* SendPacketCmd.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 430D650D1CB89FC000FCA750 /* SendPacketCmd.m in Sources */ = {isa = PBXBuildFile; fileRef = 430D64FD1CB89FC000FCA750 /* SendPacketCmd.m */; }; - 430D650E1CB89FC000FCA750 /* UpdateRegisterCmd.h in Headers */ = {isa = PBXBuildFile; fileRef = 430D64FE1CB89FC000FCA750 /* UpdateRegisterCmd.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 430D650F1CB89FC000FCA750 /* UpdateRegisterCmd.m in Sources */ = {isa = PBXBuildFile; fileRef = 430D64FF1CB89FC000FCA750 /* UpdateRegisterCmd.m */; }; - 431185AA1CF257D10059ED98 /* RileyLinkDeviceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 431185A91CF257D10059ED98 /* RileyLinkDeviceTableViewCell.xib */; }; - 431185AF1CF25A590059ED98 /* IdentifiableClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431185AE1CF25A590059ED98 /* IdentifiableClass.swift */; }; + 43047FC41FAEC70600508343 /* RadioFirmwareVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43047FC31FAEC70600508343 /* RadioFirmwareVersionTests.swift */; }; + 43047FC61FAEC83000508343 /* RFPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43047FC51FAEC83000508343 /* RFPacketTests.swift */; }; + 43047FC71FAEC9BC00508343 /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6BA1C826B92006DBA60 /* NSData.swift */; }; + 43047FC91FAECA8700508343 /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6BA1C826B92006DBA60 /* NSData.swift */; }; + 431CE7781F98564200255374 /* RileyLinkBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 431CE76F1F98564100255374 /* RileyLinkBLEKit.framework */; }; + 431CE7811F98564200255374 /* RileyLinkBLEKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 431CE7711F98564100255374 /* RileyLinkBLEKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 431CE7841F98564200255374 /* RileyLinkBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 431CE76F1F98564100255374 /* RileyLinkBLEKit.framework */; }; + 431CE7851F98564200255374 /* RileyLinkBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 431CE76F1F98564100255374 /* RileyLinkBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 431CE78D1F985B5400255374 /* PeripheralManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE78C1F985B5400255374 /* PeripheralManager.swift */; }; + 431CE78F1F985B6E00255374 /* CBPeripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE78E1F985B6E00255374 /* CBPeripheral.swift */; }; + 431CE7911F985D8D00255374 /* RileyLinkDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7901F985D8D00255374 /* RileyLinkDeviceManager.swift */; }; + 431CE7931F985DE700255374 /* PeripheralManager+RileyLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7921F985DE700255374 /* PeripheralManager+RileyLink.swift */; }; + 431CE7961F9B0F0200255374 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7941F9B0DAE00255374 /* OSLog.swift */; }; + 431CE7971F9B0F0200255374 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7941F9B0DAE00255374 /* OSLog.swift */; }; + 431CE7981F9B0F0200255374 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7941F9B0DAE00255374 /* OSLog.swift */; }; + 431CE7991F9B0F0200255374 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7941F9B0DAE00255374 /* OSLog.swift */; }; + 431CE79A1F9B0F1600255374 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7941F9B0DAE00255374 /* OSLog.swift */; }; + 431CE79C1F9B21BA00255374 /* RileyLinkDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE79B1F9B21BA00255374 /* RileyLinkDevice.swift */; }; + 431CE79E1F9BE73900255374 /* BLEFirmwareVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE79D1F9BE73900255374 /* BLEFirmwareVersion.swift */; }; + 431CE79F1F9C670600255374 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBE4501EAD238C0073A0B5 /* TimeInterval.swift */; }; + 431CE7A11F9D195600255374 /* CBCentralManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7A01F9D195600255374 /* CBCentralManager.swift */; }; + 431CE7A31F9D737F00255374 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7A21F9D737F00255374 /* Command.swift */; }; + 431CE7A51F9D78F500255374 /* RFPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7A41F9D78F500255374 /* RFPacket.swift */; }; + 431CE7A71F9D98F700255374 /* CommandSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7A61F9D98F700255374 /* CommandSession.swift */; }; + 4322B75620282DA60002837D /* ResponseBufferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4322B75520282DA60002837D /* ResponseBufferTests.swift */; }; + 432847C11FA1737400CDE69C /* RileyLinkBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 431CE76F1F98564100255374 /* RileyLinkBLEKit.framework */; }; + 432847C31FA57C0F00CDE69C /* RadioFirmwareVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432847C21FA57C0F00CDE69C /* RadioFirmwareVersion.swift */; }; + 432CF9061FF74CCB003AB446 /* RileyLinkKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43722FAE1CB9F7630038B7F2 /* RileyLinkKit.framework */; }; + 43323EA71FA81A0F003FB0FA /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43323EA61FA81A0F003FB0FA /* NumberFormatter.swift */; }; + 43323EAA1FA81C1B003FB0FA /* RileyLinkDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43323EA91FA81C1B003FB0FA /* RileyLinkDevice.swift */; }; 433568761CF67FA800FD9D54 /* ReadRemainingInsulinMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433568751CF67FA800FD9D54 /* ReadRemainingInsulinMessageBody.swift */; }; + 433ABFFC2016FDF700E6C1FF /* RileyLinkDeviceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433ABFFB2016FDF700E6C1FF /* RileyLinkDeviceError.swift */; }; 4345D1CE1DA16AF300BAAD22 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4345D1CD1DA16AF300BAAD22 /* TimeZone.swift */; }; 43462E8B1CCB06F500F958A8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43462E8A1CCB06F500F958A8 /* AppDelegate.swift */; }; - 434AB0951CBA0DF600422F4A /* Either.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434AB0911CBA0DF600422F4A /* Either.swift */; }; 434AB0961CBA0DF600422F4A /* PumpOps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434AB0921CBA0DF600422F4A /* PumpOps.swift */; }; - 434AB0971CBA0DF600422F4A /* PumpOpsSynchronous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434AB0931CBA0DF600422F4A /* PumpOpsSynchronous.swift */; }; + 434AB0971CBA0DF600422F4A /* PumpOpsSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434AB0931CBA0DF600422F4A /* PumpOpsSession.swift */; }; 434AB0981CBA0DF600422F4A /* PumpState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434AB0941CBA0DF600422F4A /* PumpState.swift */; }; - 434AB0BE1CBB4E3200422F4A /* RileyLinkDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434AB0BD1CBB4E3200422F4A /* RileyLinkDeviceManager.swift */; }; - 434AB0C61CBCB41500422F4A /* RileyLinkDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434AB0C51CBCB41500422F4A /* RileyLinkDevice.swift */; }; 434AB0C71CBCB76400422F4A /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6BA1C826B92006DBA60 /* NSData.swift */; }; 434FF1DC1CF268BD000DB779 /* IdentifiableClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431185AE1CF25A590059ED98 /* IdentifiableClass.swift */; }; 434FF1DE1CF268F3000DB779 /* RileyLinkDeviceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434FF1DD1CF268F3000DB779 /* RileyLinkDeviceTableViewCell.swift */; }; - 43523ED71CC2C558001850F1 /* NSData+Conversion.m in Sources */ = {isa = PBXBuildFile; fileRef = C1E535E91991E36700C2AC49 /* NSData+Conversion.m */; }; + 435535D61FB6D98400CE5A23 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435535D51FB6D98400CE5A23 /* UserDefaults.swift */; }; + 435535D81FB7987000CE5A23 /* PumpOps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435535D71FB7987000CE5A23 /* PumpOps.swift */; }; + 435535DA1FB836CB00CE5A23 /* CommandResponseViewController+RileyLinkDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435535D91FB836CB00CE5A23 /* CommandResponseViewController+RileyLinkDevice.swift */; }; + 435535DC1FB8B37E00CE5A23 /* PumpMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435535DB1FB8B37E00CE5A23 /* PumpMessage.swift */; }; + 436CCEF21FB953E800A6822B /* CommandSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436CCEF11FB953E800A6822B /* CommandSession.swift */; }; + 4370A37E1FAF8EF200EC666A /* TextFieldTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C1B4A9531D1E613A003B8985 /* TextFieldTableViewCell.xib */; }; 43722FB11CB9F7640038B7F2 /* RileyLinkKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 43722FB01CB9F7640038B7F2 /* RileyLinkKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 43722FB81CB9F7640038B7F2 /* RileyLinkKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43722FAE1CB9F7630038B7F2 /* RileyLinkKit.framework */; }; 43722FC31CB9F7640038B7F2 /* RileyLinkKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43722FAE1CB9F7630038B7F2 /* RileyLinkKit.framework */; }; 43722FC41CB9F7640038B7F2 /* RileyLinkKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 43722FAE1CB9F7630038B7F2 /* RileyLinkKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 43722FCB1CB9F7DB0038B7F2 /* MinimedKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C10D9BC11C8269D500378342 /* MinimedKit.framework */; }; - 43722FCC1CB9F7DB0038B7F2 /* RileyLinkBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 430D64CB1CB855AB00FCA750 /* RileyLinkBLEKit.framework */; }; + 437462391FA9287A00643383 /* RileyLinkDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437462381FA9287A00643383 /* RileyLinkDevice.swift */; }; + 437F54071FBD52120070FF2C /* DeviceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437F54061FBD52120070FF2C /* DeviceState.swift */; }; + 437F54091FBF9E0A0070FF2C /* RileyLinkDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437F54081FBF9E0A0070FF2C /* RileyLinkDevice.swift */; }; + 437F540A1FBFDAA60070FF2C /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6BA1C826B92006DBA60 /* NSData.swift */; }; + 4384C8C61FB92F8100D916E6 /* HistoryPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4384C8C51FB92F8100D916E6 /* HistoryPage.swift */; }; + 4384C8C81FB937E500D916E6 /* PumpSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4384C8C71FB937E500D916E6 /* PumpSettings.swift */; }; + 4384C8C91FB941FB00D916E6 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBE4501EAD238C0073A0B5 /* TimeInterval.swift */; }; 438D39221D19011700D40CA4 /* PlaceholderPumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438D39211D19011700D40CA4 /* PlaceholderPumpEvent.swift */; }; - 439731271CF21C3C00F474E5 /* RileyLinkDeviceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439731261CF21C3C00F474E5 /* RileyLinkDeviceTableViewCell.swift */; }; 43A068EC1CF6BA6900F9EFE4 /* ReadRemainingInsulinMessageBodyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A068EB1CF6BA6900F9EFE4 /* ReadRemainingInsulinMessageBodyTests.swift */; }; 43A9E50E1F6B865000307931 /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6BA1C826B92006DBA60 /* NSData.swift */; }; 43B0ADC01D0FC03200AAD278 /* NSDateComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B0ADBF1D0FC03200AAD278 /* NSDateComponentsTests.swift */; }; @@ -72,6 +83,12 @@ 43B0ADCB1D126B1100AAD278 /* SelectBasalProfilePumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B0ADCA1D126B1100AAD278 /* SelectBasalProfilePumpEvent.swift */; }; 43B0ADCC1D126E3000AAD278 /* NSDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B0ADC31D12506A00AAD278 /* NSDateFormatter.swift */; }; 43B6E0121D24E2320022E6D7 /* NightscoutPumpEventsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14C8A7F1C9CFBEE000F72C5 /* NightscoutPumpEventsTests.swift */; }; + 43BA719B202591A70058961E /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BA719A202591A70058961E /* Response.swift */; }; + 43BA719D2026C9B00058961E /* ResponseBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BA719C2026C9B00058961E /* ResponseBuffer.swift */; }; + 43BF58B01FF594CB00499C46 /* SelectBasalProfileMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BF58AF1FF594CB00499C46 /* SelectBasalProfileMessageBody.swift */; }; + 43BF58B21FF5A22200499C46 /* BasalProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BF58B11FF5A22200499C46 /* BasalProfile.swift */; }; + 43BF58B31FF6079600499C46 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBE4501EAD238C0073A0B5 /* TimeInterval.swift */; }; + 43C0196C1FA6B8AE007ABFA1 /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43CA93241CB8BB33000026B5 /* CoreBluetooth.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 43C246971D8918AE0031F8D1 /* Crypto.h in Headers */ = {isa = PBXBuildFile; fileRef = 43C246951D8918AE0031F8D1 /* Crypto.h */; settings = {ATTRIBUTES = (Public, ); }; }; 43C246A01D8919E20031F8D1 /* Crypto.m in Sources */ = {isa = PBXBuildFile; fileRef = 43C2469F1D8919E20031F8D1 /* Crypto.m */; }; 43C246A11D891BA80031F8D1 /* NSData+Conversion.m in Sources */ = {isa = PBXBuildFile; fileRef = C1E535E91991E36700C2AC49 /* NSData+Conversion.m */; }; @@ -80,7 +97,6 @@ 43C246AA1D8A31540031F8D1 /* Crypto.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 43C246931D8918AE0031F8D1 /* Crypto.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 43C9071B1D863772002BAD29 /* MinimedKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C10D9BC11C8269D500378342 /* MinimedKit.framework */; }; 43C9071C1D863782002BAD29 /* MinimedKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = C10D9BC11C8269D500378342 /* MinimedKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 43CA93251CB8BB33000026B5 /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43CA93241CB8BB33000026B5 /* CoreBluetooth.framework */; }; 43CA93291CB8CF22000026B5 /* ChangeTempBasalCarelinkMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CA93281CB8CF22000026B5 /* ChangeTempBasalCarelinkMessageBody.swift */; }; 43CA932B1CB8CF76000026B5 /* ChangeTimeCarelinkMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CA932A1CB8CF76000026B5 /* ChangeTimeCarelinkMessageBody.swift */; }; 43CA932E1CB8CFA1000026B5 /* ReadTempBasalCarelinkMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CA932C1CB8CFA1000026B5 /* ReadTempBasalCarelinkMessageBody.swift */; }; @@ -88,6 +104,21 @@ 43CA93311CB97191000026B5 /* ReadTempBasalCarelinkMessageBodyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CA93301CB97191000026B5 /* ReadTempBasalCarelinkMessageBodyTests.swift */; }; 43CA93331CB9726A000026B5 /* ChangeTimeCarelinMessageBodyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CA93321CB9726A000026B5 /* ChangeTimeCarelinMessageBodyTests.swift */; }; 43CA93351CB9727F000026B5 /* ChangeTempBasalCarelinkMessageBodyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CA93341CB9727F000026B5 /* ChangeTempBasalCarelinkMessageBodyTests.swift */; }; + 43D5E7881FAEDAC4004ACDB7 /* PeripheralManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D5E7871FAEDAC4004ACDB7 /* PeripheralManagerError.swift */; }; + 43D5E7921FAF7BFB004ACDB7 /* RileyLinkKitUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 43D5E7901FAF7BFB004ACDB7 /* RileyLinkKitUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 43D5E7951FAF7BFB004ACDB7 /* RileyLinkKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43D5E78E1FAF7BFB004ACDB7 /* RileyLinkKitUI.framework */; }; + 43D5E7961FAF7BFB004ACDB7 /* RileyLinkKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 43D5E78E1FAF7BFB004ACDB7 /* RileyLinkKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 43D5E79A1FAF7C47004ACDB7 /* RileyLinkDeviceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439731261CF21C3C00F474E5 /* RileyLinkDeviceTableViewCell.swift */; }; + 43D5E79B1FAF7C47004ACDB7 /* CommandResponseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C170C9961CECD80000F3D8E5 /* CommandResponseViewController.swift */; }; + 43D5E79C1FAF7C47004ACDB7 /* RileyLinkDeviceTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C170C9981CECD80000F3D8E5 /* RileyLinkDeviceTableViewController.swift */; }; + 43D5E79D1FAF7C47004ACDB7 /* TextFieldTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B4A9521D1E613A003B8985 /* TextFieldTableViewCell.swift */; }; + 43D5E79E1FAF7C47004ACDB7 /* TextFieldTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B4A9541D1E613A003B8985 /* TextFieldTableViewController.swift */; }; + 43D5E79F1FAF7C98004ACDB7 /* IdentifiableClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431185AE1CF25A590059ED98 /* IdentifiableClass.swift */; }; + 43D5E7A01FAF7CCA004ACDB7 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43323EA61FA81A0F003FB0FA /* NumberFormatter.swift */; }; + 43D5E7A11FAF7CE0004ACDB7 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B4A9581D1E6357003B8985 /* UITableViewCell.swift */; }; + 43D5E7A21FAF7CF2004ACDB7 /* CaseCountable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C659181E16BA9D0025CC58 /* CaseCountable.swift */; }; + 43D5E7A31FAF7D05004ACDB7 /* CBPeripheralState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C170C98D1CECD6F300F3D8E5 /* CBPeripheralState.swift */; }; + 43D5E7A41FAF7D4D004ACDB7 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4345D1CD1DA16AF300BAAD22 /* TimeZone.swift */; }; 43EBE4521EAD23C40073A0B5 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBE4501EAD238C0073A0B5 /* TimeInterval.swift */; }; 43EBE4531EAD23CE0073A0B5 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBE4501EAD238C0073A0B5 /* TimeInterval.swift */; }; 43EBE4541EAD23EC0073A0B5 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBE4501EAD238C0073A0B5 /* TimeInterval.swift */; }; @@ -141,6 +172,14 @@ 54BC44B71DB81B5100340EED /* GetGlucosePageMessageBodyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54BC44B61DB81B5100340EED /* GetGlucosePageMessageBodyTests.swift */; }; 54BC44B91DB81D6100340EED /* GetGlucosePageMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54BC44B81DB81D6100340EED /* GetGlucosePageMessageBody.swift */; }; 54DA4E851DFDC0A70007F489 /* SensorValueGlucoseEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DA4E841DFDC0A70007F489 /* SensorValueGlucoseEvent.swift */; }; + 7D70766D1FE092D4004AC8EA /* LoopKit.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D70766F1FE092D4004AC8EA /* LoopKit.strings */; }; + 7D7076721FE092D5004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076741FE092D5004AC8EA /* Localizable.strings */; }; + 7D7076771FE092D6004AC8EA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076791FE092D6004AC8EA /* InfoPlist.strings */; }; + 7D70767C1FE092D6004AC8EA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D70767E1FE092D6004AC8EA /* InfoPlist.strings */; }; + 7D7076811FE092D7004AC8EA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076831FE092D7004AC8EA /* InfoPlist.strings */; }; + 7D70768B1FE09310004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D70768D1FE09310004AC8EA /* Localizable.strings */; }; + 7D7076901FE09311004AC8EA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076921FE09311004AC8EA /* InfoPlist.strings */; }; + 7D7076951FE09311004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076971FE09311004AC8EA /* Localizable.strings */; }; C10AB08D1C855613000F102E /* FindDeviceMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10AB08C1C855613000F102E /* FindDeviceMessageBody.swift */; }; C10AB08F1C855F34000F102E /* DeviceLinkMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10AB08E1C855F34000F102E /* DeviceLinkMessageBody.swift */; }; C10D9BC41C8269D500378342 /* MinimedKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C10D9BC31C8269D500378342 /* MinimedKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -156,20 +195,12 @@ C12198AD1C8F332500BC374C /* TimestampedPumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12198AC1C8F332500BC374C /* TimestampedPumpEvent.swift */; }; C12198B31C8F730700BC374C /* BolusWizardEstimatePumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12198B21C8F730700BC374C /* BolusWizardEstimatePumpEvent.swift */; }; C12616441B685F0A001FAD87 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C12616431B685F0A001FAD87 /* CoreData.framework */; }; - C126164B1B685F93001FAD87 /* RileyLink.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C12616491B685F93001FAD87 /* RileyLink.xcdatamodeld */; }; - C12616541B6892DB001FAD87 /* RileyLinkRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = C12616531B6892DB001FAD87 /* RileyLinkRecord.m */; }; - C126165A1B6B2D20001FAD87 /* PacketTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = C12616591B6B2D20001FAD87 /* PacketTableViewCell.m */; }; - C12616671B76E8DC001FAD87 /* TPKeyboardAvoidingCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = C12616601B76E8DC001FAD87 /* TPKeyboardAvoidingCollectionView.m */; }; - C12616681B76E8DC001FAD87 /* TPKeyboardAvoidingScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = C12616621B76E8DC001FAD87 /* TPKeyboardAvoidingScrollView.m */; }; - C12616691B76E8DC001FAD87 /* TPKeyboardAvoidingTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = C12616641B76E8DC001FAD87 /* TPKeyboardAvoidingTableView.m */; }; - C126166A1B76E8DC001FAD87 /* UIScrollView+TPKeyboardAvoidingAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = C12616661B76E8DC001FAD87 /* UIScrollView+TPKeyboardAvoidingAdditions.m */; }; C1271B071A9A34E900B7C949 /* Log.m in Sources */ = {isa = PBXBuildFile; fileRef = C1271B061A9A34E900B7C949 /* Log.m */; }; C1274F771D8232580002912B /* DailyTotal515PumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1274F761D8232580002912B /* DailyTotal515PumpEvent.swift */; }; C1274F791D823A550002912B /* ChangeMeterIDPumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1274F781D823A550002912B /* ChangeMeterIDPumpEvent.swift */; }; C1274F7F1D82411C0002912B /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1274F7B1D82411C0002912B /* AuthenticationViewController.swift */; }; C1274F801D82411C0002912B /* RileyLinkListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1274F7C1D82411C0002912B /* RileyLinkListTableViewController.swift */; }; C1274F811D82411C0002912B /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1274F7D1D82411C0002912B /* SettingsTableViewController.swift */; }; - C1274F821D82411C0002912B /* TextFieldTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1274F7E1D82411C0002912B /* TextFieldTableViewController.swift */; }; C1274F841D82420F0002912B /* RadioSelectionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1274F831D82420F0002912B /* RadioSelectionTableViewController.swift */; }; C1274F861D8242BE0002912B /* PumpRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1274F851D8242BE0002912B /* PumpRegion.swift */; }; C12EA23B198B436800309FA4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C12EA23A198B436800309FA4 /* Foundation.framework */; }; @@ -185,7 +216,6 @@ C12EA26A198B442100309FA4 /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C12EA269198B442100309FA4 /* Storyboard.storyboard */; }; C1330F431DBDA46400569064 /* ChangeSensorAlarmSilenceConfigPumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1330F421DBDA46400569064 /* ChangeSensorAlarmSilenceConfigPumpEvent.swift */; }; C133CF931D5943780034B82D /* PredictedBG.swift in Sources */ = {isa = PBXBuildFile; fileRef = C133CF921D5943780034B82D /* PredictedBG.swift */; }; - C139AC241BFD84B500B0518F /* RuntimeUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = C139AC231BFD84B500B0518F /* RuntimeUtils.m */; }; C13D155A1DAACE8400ADC044 /* Either.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13D15591DAACE8400ADC044 /* Either.swift */; }; C14303161C97C98000A40450 /* PumpAckMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14303151C97C98000A40450 /* PumpAckMessageBody.swift */; }; C14303181C97CC6B00A40450 /* GetPumpModelCarelinkMessageBodyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14303171C97CC6B00A40450 /* GetPumpModelCarelinkMessageBodyTests.swift */; }; @@ -212,14 +242,10 @@ C15AF2B11D7498DD0031FC9D /* RestoreMystery55PumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15AF2B01D7498DD0031FC9D /* RestoreMystery55PumpEvent.swift */; }; C16843771CF00C0100D53CCD /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16843761CF00C0100D53CCD /* SwitchTableViewCell.swift */; }; C16A08311D389205001A200C /* JournalEntryMealMarkerPumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16A08301D389205001A200C /* JournalEntryMealMarkerPumpEvent.swift */; }; - C170C98E1CECD6F300F3D8E5 /* CBPeripheralState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C170C98D1CECD6F300F3D8E5 /* CBPeripheralState.swift */; }; - C170C9991CECD80000F3D8E5 /* CommandResponseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C170C9961CECD80000F3D8E5 /* CommandResponseViewController.swift */; }; - C170C99B1CECD80000F3D8E5 /* RileyLinkDeviceTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C170C9981CECD80000F3D8E5 /* RileyLinkDeviceTableViewController.swift */; }; C1711A561C94F13400CB25BD /* ButtonPressCarelinkMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1711A551C94F13400CB25BD /* ButtonPressCarelinkMessageBody.swift */; }; C1711A5A1C952D2900CB25BD /* GetPumpModelCarelinkMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1711A591C952D2900CB25BD /* GetPumpModelCarelinkMessageBody.swift */; }; C1711A5C1C953F3000CB25BD /* GetBatteryCarelinkMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1711A5B1C953F3000CB25BD /* GetBatteryCarelinkMessageBody.swift */; }; C1711A5E1C977BD000CB25BD /* GetHistoryPageCarelinkMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1711A5D1C977BD000CB25BD /* GetHistoryPageCarelinkMessageBody.swift */; }; - C174F26B19EB824D00398C72 /* ISO8601DateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = C174F26A19EB824D00398C72 /* ISO8601DateFormatter.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; C178845D1D4EF3D800405663 /* ReadPumpStatusMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C178845C1D4EF3D800405663 /* ReadPumpStatusMessageBody.swift */; }; C178845F1D5166BE00405663 /* COBStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C178845E1D5166BE00405663 /* COBStatus.swift */; }; C17884611D519F1E00405663 /* BatteryIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17884601D519F1E00405663 /* BatteryIndicator.swift */; }; @@ -280,7 +306,7 @@ C1A492651D4A5DEB008964FF /* BatteryStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A492641D4A5DEB008964FF /* BatteryStatus.swift */; }; C1A492671D4A65D9008964FF /* RecommendedTempBasal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A492661D4A65D9008964FF /* RecommendedTempBasal.swift */; }; C1A492691D4A66C0008964FF /* LoopEnacted.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A492681D4A66C0008964FF /* LoopEnacted.swift */; }; - C1A721601EC29C0B0080FAD7 /* PumpCommsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A7215F1EC29C0B0080FAD7 /* PumpCommsError.swift */; }; + C1A721601EC29C0B0080FAD7 /* PumpOpsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A7215F1EC29C0B0080FAD7 /* PumpOpsError.swift */; }; C1A721621EC3E0500080FAD7 /* PumpErrorMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A721611EC3E0500080FAD7 /* PumpErrorMessageBody.swift */; }; C1A721661EC4BCE30080FAD7 /* PartialDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A721651EC4BCE30080FAD7 /* PartialDecode.swift */; }; C1AF21E21D4838C90088C41D /* DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1AF21E11D4838C90088C41D /* DeviceStatus.swift */; }; @@ -299,21 +325,13 @@ C1B383281CD0668600CE7782 /* NightscoutUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1842C281C908A3C00DB42AC /* NightscoutUploader.swift */; }; C1B383291CD0668600CE7782 /* NightscoutPumpEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1842C2A1C90DFB600DB42AC /* NightscoutPumpEvents.swift */; }; C1B383301CD0680800CE7782 /* MinimedKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C10D9BC11C8269D500378342 /* MinimedKit.framework */; }; - C1B383311CD068C300CE7782 /* RileyLinkBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 430D64CB1CB855AB00FCA750 /* RileyLinkBLEKit.framework */; }; C1B383361CD1BA8100CE7782 /* DeviceDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B383351CD1BA8100CE7782 /* DeviceDataManager.swift */; }; - C1B4A94E1D1C423D003B8985 /* NSUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B4A94D1D1C423D003B8985 /* NSUserDefaults.swift */; }; - C1B4A9551D1E613A003B8985 /* TextFieldTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B4A9521D1E613A003B8985 /* TextFieldTableViewCell.swift */; }; - C1B4A9561D1E613A003B8985 /* TextFieldTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C1B4A9531D1E613A003B8985 /* TextFieldTableViewCell.xib */; }; - C1B4A9571D1E613A003B8985 /* TextFieldTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B4A9541D1E613A003B8985 /* TextFieldTableViewController.swift */; }; - C1B4A9591D1E6357003B8985 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B4A9581D1E6357003B8985 /* UITableViewCell.swift */; }; C1BAD1181E63984C009BA1C6 /* RadioAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1BAD1171E63984C009BA1C6 /* RadioAdapter.swift */; }; C1C3578F1C927303009BDD4F /* MeterMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C3578E1C927303009BDD4F /* MeterMessage.swift */; }; C1C357911C92733A009BDD4F /* MeterMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C357901C92733A009BDD4F /* MeterMessageTests.swift */; }; - C1C659191E16BA9D0025CC58 /* CaseCountable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C659181E16BA9D0025CC58 /* CaseCountable.swift */; }; C1C73F1D1DE6306A0022FC89 /* BatteryChemistryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C73F1C1DE6306A0022FC89 /* BatteryChemistryType.swift */; }; C1D00E9D1E8986A400B733B7 /* PumpSuspendTreatment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D00E9C1E8986A400B733B7 /* PumpSuspendTreatment.swift */; }; C1D00EA11E8986F900B733B7 /* PumpResumeTreatment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D00EA01E8986F900B733B7 /* PumpResumeTreatment.swift */; }; - C1E535EA1991E36700C2AC49 /* NSData+Conversion.m in Sources */ = {isa = PBXBuildFile; fileRef = C1E535E91991E36700C2AC49 /* NSData+Conversion.m */; }; C1E5BEAD1D5E26F200BD4390 /* RileyLinkStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E5BEAC1D5E26F200BD4390 /* RileyLinkStatus.swift */; }; C1EAD6B31C826B6D006DBA60 /* AlertType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6AE1C826B6D006DBA60 /* AlertType.swift */; }; C1EAD6B41C826B6D006DBA60 /* MessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6AF1C826B6D006DBA60 /* MessageBody.swift */; }; @@ -334,9 +352,6 @@ C1EAD6D61C826C43006DBA60 /* MySentryPumpStatusMessageBodyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6D21C826C43006DBA60 /* MySentryPumpStatusMessageBodyTests.swift */; }; C1EAD6D71C826C43006DBA60 /* NSDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6D31C826C43006DBA60 /* NSDataTests.swift */; }; C1EAD6D81C826C43006DBA60 /* ReadSettingsCarelinkMessageBodyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6D41C826C43006DBA60 /* ReadSettingsCarelinkMessageBodyTests.swift */; }; - C1EAD6DA1C829104006DBA60 /* RFTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6D91C829104006DBA60 /* RFTools.swift */; }; - C1EAD6DC1C82A4AB006DBA60 /* RFToolsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6DB1C82A4AB006DBA60 /* RFToolsTests.swift */; }; - C1EAD6DE1C82B78C006DBA60 /* CRC8.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6DD1C82B78C006DBA60 /* CRC8.swift */; }; C1EAD6E01C82B910006DBA60 /* CRC8Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6DF1C82B910006DBA60 /* CRC8Tests.swift */; }; C1EAD6E21C82BA7A006DBA60 /* CRC16.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6E11C82BA7A006DBA60 /* CRC16.swift */; }; C1EAD6E41C82BA87006DBA60 /* CRC16Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6E31C82BA87006DBA60 /* CRC16Tests.swift */; }; @@ -346,22 +361,33 @@ C1F0004C1EBE68A600F65163 /* DataFrameMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F0004B1EBE68A600F65163 /* DataFrameMessageBody.swift */; }; C1F000501EBE727C00F65163 /* BasalScheduleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F0004F1EBE727C00F65163 /* BasalScheduleTests.swift */; }; C1F000521EBE73F400F65163 /* BasalSchedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F000511EBE73F400F65163 /* BasalSchedule.swift */; }; + C1F6EB871F89C3B100CFE393 /* CRC8.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EAD6DD1C82B78C006DBA60 /* CRC8.swift */; }; + C1F6EB891F89C3E200CFE393 /* FourByteSixByteEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F6EB881F89C3E200CFE393 /* FourByteSixByteEncoding.swift */; }; + C1F6EB8B1F89C41200CFE393 /* MinimedPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F6EB8A1F89C41200CFE393 /* MinimedPacket.swift */; }; + C1F6EB8D1F89C45500CFE393 /* MinimedPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F6EB8C1F89C45500CFE393 /* MinimedPacketTests.swift */; }; C1FDFCA91D964A3E00ADBC31 /* BolusReminderPumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FDFCA81D964A3E00ADBC31 /* BolusReminderPumpEvent.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 430D64D61CB855AB00FCA750 /* PBXContainerItemProxy */ = { + 431CE7791F98564200255374 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = C12EA22F198B436800309FA4 /* Project object */; proxyType = 1; - remoteGlobalIDString = 430D64CA1CB855AB00FCA750; + remoteGlobalIDString = 431CE76E1F98564100255374; remoteInfo = RileyLinkBLEKit; }; - 430D64DE1CB855AB00FCA750 /* PBXContainerItemProxy */ = { + 431CE77B1F98564200255374 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = C12EA22F198B436800309FA4 /* Project object */; proxyType = 1; - remoteGlobalIDString = 430D64CA1CB855AB00FCA750; + remoteGlobalIDString = C12EA236198B436800309FA4; + remoteInfo = RileyLink; + }; + 431CE7821F98564200255374 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C12EA22F198B436800309FA4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 431CE76E1F98564100255374; remoteInfo = RileyLinkBLEKit; }; 43722FB91CB9F7640038B7F2 /* PBXContainerItemProxy */ = { @@ -392,6 +418,13 @@ remoteGlobalIDString = 43C246921D8918AE0031F8D1; remoteInfo = Crypto; }; + 43D5E7931FAF7BFB004ACDB7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C12EA22F198B436800309FA4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 43D5E78D1FAF7BFB004ACDB7; + remoteInfo = RileyLinkKitUI; + }; C10D9BCC1C8269D500378342 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = C12EA22F198B436800309FA4 /* Project object */; @@ -456,8 +489,9 @@ 43722FC41CB9F7640038B7F2 /* RileyLinkKit.framework in Embed Frameworks */, 43C246AA1D8A31540031F8D1 /* Crypto.framework in Embed Frameworks */, C10D9BD71C8269D500378342 /* MinimedKit.framework in Embed Frameworks */, + 43D5E7961FAF7BFB004ACDB7 /* RileyLinkKitUI.framework in Embed Frameworks */, C1B383211CD0665D00CE7782 /* NightscoutUploadKit.framework in Embed Frameworks */, - 430D64E11CB855AB00FCA750 /* RileyLinkBLEKit.framework in Embed Frameworks */, + 431CE7851F98564200255374 /* RileyLinkBLEKit.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -468,54 +502,58 @@ 2B19B9871DF3EF68006AB65F /* NewTimePumpEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewTimePumpEvent.swift; sourceTree = ""; }; 2F962EBE1E678BAA0070EFBD /* PumpOpsSynchronousTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PumpOpsSynchronousTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2F962EC01E6872170070EFBD /* TimestampedHistoryEventTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimestampedHistoryEventTests.swift; sourceTree = ""; }; - 2F962EC21E6873A10070EFBD /* PumpOpsCommunication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpOpsCommunication.swift; sourceTree = ""; }; + 2F962EC21E6873A10070EFBD /* PumpMessageSender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpMessageSender.swift; sourceTree = ""; }; 2F962EC41E705C6D0070EFBD /* PumpOpsSynchronousBuildFromFramesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PumpOpsSynchronousBuildFromFramesTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2F962EC71E7074E60070EFBD /* BolusNormalPumpEventTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = BolusNormalPumpEventTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2F962EC91E70831F0070EFBD /* PumpModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PumpModelTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2FDE1A061E57B12D00B56A27 /* ReadCurrentPageNumberMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadCurrentPageNumberMessageBody.swift; sourceTree = ""; }; - 430D64CB1CB855AB00FCA750 /* RileyLinkBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RileyLinkBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 430D64CD1CB855AB00FCA750 /* RileyLinkBLEKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RileyLinkBLEKit.h; sourceTree = ""; }; - 430D64CF1CB855AB00FCA750 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 430D64D41CB855AB00FCA750 /* RileyLinkBLEKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RileyLinkBLEKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 430D64DB1CB855AB00FCA750 /* RileyLinkBLEKitTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RileyLinkBLEKitTests.m; sourceTree = ""; }; - 430D64DD1CB855AB00FCA750 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 430D64E81CB85A4300FCA750 /* RileyLinkBLEDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RileyLinkBLEDevice.h; sourceTree = ""; }; - 430D64E91CB85A4300FCA750 /* RileyLinkBLEDevice.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.objc; path = RileyLinkBLEDevice.m; sourceTree = ""; tabWidth = 4; }; - 430D64EA1CB85A4300FCA750 /* RileyLinkBLEManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RileyLinkBLEManager.h; sourceTree = ""; }; - 430D64EB1CB85A4300FCA750 /* RileyLinkBLEManager.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.objc; path = RileyLinkBLEManager.m; sourceTree = ""; tabWidth = 4; }; - 430D64F01CB89FC000FCA750 /* CmdBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CmdBase.h; sourceTree = ""; }; - 430D64F11CB89FC000FCA750 /* CmdBase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CmdBase.m; sourceTree = ""; }; - 430D64F21CB89FC000FCA750 /* GetPacketCmd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GetPacketCmd.h; sourceTree = ""; }; - 430D64F31CB89FC000FCA750 /* GetPacketCmd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GetPacketCmd.m; sourceTree = ""; }; - 430D64F41CB89FC000FCA750 /* GetVersionCmd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GetVersionCmd.h; sourceTree = ""; }; - 430D64F51CB89FC000FCA750 /* GetVersionCmd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GetVersionCmd.m; sourceTree = ""; }; - 430D64F61CB89FC000FCA750 /* ReceivingPacketCmd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReceivingPacketCmd.h; sourceTree = ""; }; - 430D64F71CB89FC000FCA750 /* ReceivingPacketCmd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReceivingPacketCmd.m; sourceTree = ""; }; - 430D64F81CB89FC000FCA750 /* RFPacket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RFPacket.h; sourceTree = ""; }; - 430D64F91CB89FC000FCA750 /* RFPacket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RFPacket.m; sourceTree = ""; }; - 430D64FA1CB89FC000FCA750 /* SendAndListenCmd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SendAndListenCmd.h; sourceTree = ""; }; - 430D64FB1CB89FC000FCA750 /* SendAndListenCmd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SendAndListenCmd.m; sourceTree = ""; }; - 430D64FC1CB89FC000FCA750 /* SendPacketCmd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SendPacketCmd.h; sourceTree = ""; }; - 430D64FD1CB89FC000FCA750 /* SendPacketCmd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SendPacketCmd.m; sourceTree = ""; }; - 430D64FE1CB89FC000FCA750 /* UpdateRegisterCmd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UpdateRegisterCmd.h; sourceTree = ""; }; - 430D64FF1CB89FC000FCA750 /* UpdateRegisterCmd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UpdateRegisterCmd.m; sourceTree = ""; }; - 431185A91CF257D10059ED98 /* RileyLinkDeviceTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RileyLinkDeviceTableViewCell.xib; sourceTree = ""; }; + 43047FC31FAEC70600508343 /* RadioFirmwareVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioFirmwareVersionTests.swift; sourceTree = ""; }; + 43047FC51FAEC83000508343 /* RFPacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RFPacketTests.swift; sourceTree = ""; }; 431185AE1CF25A590059ED98 /* IdentifiableClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentifiableClass.swift; sourceTree = ""; }; + 431CE76F1F98564100255374 /* RileyLinkBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RileyLinkBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 431CE7711F98564100255374 /* RileyLinkBLEKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RileyLinkBLEKit.h; sourceTree = ""; }; + 431CE7721F98564100255374 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 431CE7771F98564200255374 /* RileyLinkBLEKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RileyLinkBLEKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 431CE7801F98564200255374 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 431CE78C1F985B5400255374 /* PeripheralManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralManager.swift; sourceTree = ""; }; + 431CE78E1F985B6E00255374 /* CBPeripheral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBPeripheral.swift; sourceTree = ""; }; + 431CE7901F985D8D00255374 /* RileyLinkDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDeviceManager.swift; sourceTree = ""; }; + 431CE7921F985DE700255374 /* PeripheralManager+RileyLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PeripheralManager+RileyLink.swift"; sourceTree = ""; }; + 431CE7941F9B0DAE00255374 /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; + 431CE79B1F9B21BA00255374 /* RileyLinkDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDevice.swift; sourceTree = ""; }; + 431CE79D1F9BE73900255374 /* BLEFirmwareVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEFirmwareVersion.swift; sourceTree = ""; }; + 431CE7A01F9D195600255374 /* CBCentralManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBCentralManager.swift; sourceTree = ""; }; + 431CE7A21F9D737F00255374 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; + 431CE7A41F9D78F500255374 /* RFPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RFPacket.swift; sourceTree = ""; }; + 431CE7A61F9D98F700255374 /* CommandSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandSession.swift; sourceTree = ""; }; + 4322B75520282DA60002837D /* ResponseBufferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseBufferTests.swift; sourceTree = ""; }; + 432847C21FA57C0F00CDE69C /* RadioFirmwareVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioFirmwareVersion.swift; sourceTree = ""; }; + 43323EA61FA81A0F003FB0FA /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = ""; }; + 43323EA91FA81C1B003FB0FA /* RileyLinkDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDevice.swift; sourceTree = ""; }; 433568751CF67FA800FD9D54 /* ReadRemainingInsulinMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadRemainingInsulinMessageBody.swift; sourceTree = ""; }; + 433ABFFB2016FDF700E6C1FF /* RileyLinkDeviceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDeviceError.swift; sourceTree = ""; }; 4345D1CD1DA16AF300BAAD22 /* TimeZone.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeZone.swift; sourceTree = ""; }; 43462E8A1CCB06F500F958A8 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 434AB0911CBA0DF600422F4A /* Either.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Either.swift; sourceTree = ""; }; 434AB0921CBA0DF600422F4A /* PumpOps.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpOps.swift; sourceTree = ""; }; - 434AB0931CBA0DF600422F4A /* PumpOpsSynchronous.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpOpsSynchronous.swift; sourceTree = ""; }; + 434AB0931CBA0DF600422F4A /* PumpOpsSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpOpsSession.swift; sourceTree = ""; }; 434AB0941CBA0DF600422F4A /* PumpState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpState.swift; sourceTree = ""; }; - 434AB0BD1CBB4E3200422F4A /* RileyLinkDeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RileyLinkDeviceManager.swift; sourceTree = ""; }; - 434AB0C51CBCB41500422F4A /* RileyLinkDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RileyLinkDevice.swift; sourceTree = ""; }; 434FF1DD1CF268F3000DB779 /* RileyLinkDeviceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RileyLinkDeviceTableViewCell.swift; sourceTree = ""; }; + 435535D51FB6D98400CE5A23 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; + 435535D71FB7987000CE5A23 /* PumpOps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpOps.swift; sourceTree = ""; }; + 435535D91FB836CB00CE5A23 /* CommandResponseViewController+RileyLinkDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommandResponseViewController+RileyLinkDevice.swift"; sourceTree = ""; }; + 435535DB1FB8B37E00CE5A23 /* PumpMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpMessage.swift; sourceTree = ""; }; + 436CCEF11FB953E800A6822B /* CommandSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandSession.swift; sourceTree = ""; }; + 4370A3791FAF8A7400EC666A /* CoreBluetooth.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreBluetooth.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS4.1.sdk/System/Library/Frameworks/CoreBluetooth.framework; sourceTree = DEVELOPER_DIR; }; 43722FAE1CB9F7630038B7F2 /* RileyLinkKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RileyLinkKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 43722FB01CB9F7640038B7F2 /* RileyLinkKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RileyLinkKit.h; sourceTree = ""; }; 43722FB21CB9F7640038B7F2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43722FB71CB9F7640038B7F2 /* RileyLinkKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RileyLinkKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 43722FC01CB9F7640038B7F2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 437462381FA9287A00643383 /* RileyLinkDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDevice.swift; sourceTree = ""; }; + 437F54061FBD52120070FF2C /* DeviceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceState.swift; sourceTree = ""; }; + 437F54081FBF9E0A0070FF2C /* RileyLinkDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDevice.swift; sourceTree = ""; }; + 4384C8C51FB92F8100D916E6 /* HistoryPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryPage.swift; sourceTree = ""; }; + 4384C8C71FB937E500D916E6 /* PumpSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpSettings.swift; sourceTree = ""; }; 438D39211D19011700D40CA4 /* PlaceholderPumpEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderPumpEvent.swift; sourceTree = ""; }; 439731261CF21C3C00F474E5 /* RileyLinkDeviceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RileyLinkDeviceTableViewCell.swift; sourceTree = ""; }; 43A068EB1CF6BA6900F9EFE4 /* ReadRemainingInsulinMessageBodyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ReadRemainingInsulinMessageBodyTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; @@ -524,6 +562,10 @@ 43B0ADC31D12506A00AAD278 /* NSDateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDateFormatter.swift; sourceTree = ""; }; 43B0ADC81D1268B300AAD278 /* TimeFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeFormat.swift; sourceTree = ""; }; 43B0ADCA1D126B1100AAD278 /* SelectBasalProfilePumpEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectBasalProfilePumpEvent.swift; sourceTree = ""; }; + 43BA719A202591A70058961E /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; + 43BA719C2026C9B00058961E /* ResponseBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseBuffer.swift; sourceTree = ""; }; + 43BF58AF1FF594CB00499C46 /* SelectBasalProfileMessageBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectBasalProfileMessageBody.swift; sourceTree = ""; }; + 43BF58B11FF5A22200499C46 /* BasalProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalProfile.swift; sourceTree = ""; }; 43C246931D8918AE0031F8D1 /* Crypto.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Crypto.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 43C246951D8918AE0031F8D1 /* Crypto.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Crypto.h; sourceTree = ""; }; 43C246961D8918AE0031F8D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -536,6 +578,10 @@ 43CA93301CB97191000026B5 /* ReadTempBasalCarelinkMessageBodyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadTempBasalCarelinkMessageBodyTests.swift; sourceTree = ""; }; 43CA93321CB9726A000026B5 /* ChangeTimeCarelinMessageBodyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeTimeCarelinMessageBodyTests.swift; sourceTree = ""; }; 43CA93341CB9727F000026B5 /* ChangeTempBasalCarelinkMessageBodyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeTempBasalCarelinkMessageBodyTests.swift; sourceTree = ""; }; + 43D5E7871FAEDAC4004ACDB7 /* PeripheralManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralManagerError.swift; sourceTree = ""; }; + 43D5E78E1FAF7BFB004ACDB7 /* RileyLinkKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RileyLinkKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 43D5E7901FAF7BFB004ACDB7 /* RileyLinkKitUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RileyLinkKitUI.h; sourceTree = ""; }; + 43D5E7911FAF7BFB004ACDB7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43EBE4501EAD238C0073A0B5 /* TimeInterval.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = ""; }; 43EC9DCA1B786C6200DB0D18 /* LaunchScreen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LaunchScreen.xib; sourceTree = ""; }; 43F348051D596270009933DC /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; @@ -586,6 +632,28 @@ 54BC44B61DB81B5100340EED /* GetGlucosePageMessageBodyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetGlucosePageMessageBodyTests.swift; sourceTree = ""; }; 54BC44B81DB81D6100340EED /* GetGlucosePageMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetGlucosePageMessageBody.swift; sourceTree = ""; }; 54DA4E841DFDC0A70007F489 /* SensorValueGlucoseEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SensorValueGlucoseEvent.swift; sourceTree = ""; }; + 7D4F0A611F8F226F00A55FB2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D4F0A621F8F226F00A55FB2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D68AACB1FE31CE500522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D68AACC1FE31CE500522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D68AACD1FE31DEA00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LoopKit.strings; sourceTree = ""; }; + 7D68AACE1FE31DEB00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 7D68AACF1FE31DEB00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D68AAD01FE31DEB00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D68AAD11FE31DEB00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D68AAD21FE31DEC00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D68AAD31FE31DEC00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 7D68AAD41FE31DEC00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D68AAD51FE31DEC00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 7D70766E1FE092D4004AC8EA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/LoopKit.strings; sourceTree = ""; }; + 7D7076731FE092D5004AC8EA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + 7D7076781FE092D6004AC8EA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D70767D1FE092D6004AC8EA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D7076821FE092D7004AC8EA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D7076871FE092D7004AC8EA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D70768C1FE09310004AC8EA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + 7D7076911FE09311004AC8EA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + 7D7076961FE09311004AC8EA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; C10AB08C1C855613000F102E /* FindDeviceMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindDeviceMessageBody.swift; sourceTree = ""; }; C10AB08E1C855F34000F102E /* DeviceLinkMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceLinkMessageBody.swift; sourceTree = ""; }; C10D9BC11C8269D500378342 /* MinimedKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MinimedKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -605,16 +673,6 @@ C126164A1B685F93001FAD87 /* RileyLink.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = RileyLink.xcdatamodel; sourceTree = ""; }; C12616521B6892DB001FAD87 /* RileyLinkRecord.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RileyLinkRecord.h; sourceTree = ""; }; C12616531B6892DB001FAD87 /* RileyLinkRecord.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RileyLinkRecord.m; sourceTree = ""; }; - C12616581B6B2D20001FAD87 /* PacketTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PacketTableViewCell.h; sourceTree = ""; }; - C12616591B6B2D20001FAD87 /* PacketTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PacketTableViewCell.m; sourceTree = ""; }; - C126165F1B76E8DC001FAD87 /* TPKeyboardAvoidingCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPKeyboardAvoidingCollectionView.h; sourceTree = ""; }; - C12616601B76E8DC001FAD87 /* TPKeyboardAvoidingCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPKeyboardAvoidingCollectionView.m; sourceTree = ""; }; - C12616611B76E8DC001FAD87 /* TPKeyboardAvoidingScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPKeyboardAvoidingScrollView.h; sourceTree = ""; }; - C12616621B76E8DC001FAD87 /* TPKeyboardAvoidingScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPKeyboardAvoidingScrollView.m; sourceTree = ""; }; - C12616631B76E8DC001FAD87 /* TPKeyboardAvoidingTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPKeyboardAvoidingTableView.h; sourceTree = ""; }; - C12616641B76E8DC001FAD87 /* TPKeyboardAvoidingTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPKeyboardAvoidingTableView.m; sourceTree = ""; }; - C12616651B76E8DC001FAD87 /* UIScrollView+TPKeyboardAvoidingAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+TPKeyboardAvoidingAdditions.h"; sourceTree = ""; }; - C12616661B76E8DC001FAD87 /* UIScrollView+TPKeyboardAvoidingAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = "UIScrollView+TPKeyboardAvoidingAdditions.m"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; C1271B061A9A34E900B7C949 /* Log.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Log.m; sourceTree = ""; }; C1271B081A9A350400B7C949 /* Log.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Log.h; sourceTree = ""; }; C1274F761D8232580002912B /* DailyTotal515PumpEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DailyTotal515PumpEvent.swift; sourceTree = ""; }; @@ -622,7 +680,6 @@ C1274F7B1D82411C0002912B /* AuthenticationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = ""; }; C1274F7C1D82411C0002912B /* RileyLinkListTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RileyLinkListTableViewController.swift; sourceTree = ""; }; C1274F7D1D82411C0002912B /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = ""; }; - C1274F7E1D82411C0002912B /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = ""; }; C1274F831D82420F0002912B /* RadioSelectionTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioSelectionTableViewController.swift; sourceTree = ""; }; C1274F851D8242BE0002912B /* PumpRegion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpRegion.swift; sourceTree = ""; }; C12EA237198B436800309FA4 /* RileyLink.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RileyLink.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -640,8 +697,6 @@ C12EA269198B442100309FA4 /* Storyboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Storyboard.storyboard; sourceTree = ""; }; C1330F421DBDA46400569064 /* ChangeSensorAlarmSilenceConfigPumpEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeSensorAlarmSilenceConfigPumpEvent.swift; sourceTree = ""; }; C133CF921D5943780034B82D /* PredictedBG.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictedBG.swift; sourceTree = ""; }; - C139AC221BFD84B500B0518F /* RuntimeUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RuntimeUtils.h; sourceTree = ""; }; - C139AC231BFD84B500B0518F /* RuntimeUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RuntimeUtils.m; sourceTree = ""; }; C13D15591DAACE8400ADC044 /* Either.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Either.swift; sourceTree = ""; }; C14303151C97C98000A40450 /* PumpAckMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpAckMessageBody.swift; sourceTree = ""; }; C14303171C97CC6B00A40450 /* GetPumpModelCarelinkMessageBodyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetPumpModelCarelinkMessageBodyTests.swift; sourceTree = ""; }; @@ -676,8 +731,6 @@ C1711A591C952D2900CB25BD /* GetPumpModelCarelinkMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetPumpModelCarelinkMessageBody.swift; sourceTree = ""; }; C1711A5B1C953F3000CB25BD /* GetBatteryCarelinkMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetBatteryCarelinkMessageBody.swift; sourceTree = ""; }; C1711A5D1C977BD000CB25BD /* GetHistoryPageCarelinkMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetHistoryPageCarelinkMessageBody.swift; sourceTree = ""; }; - C174F26919EB824D00398C72 /* ISO8601DateFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ISO8601DateFormatter.h; sourceTree = ""; }; - C174F26A19EB824D00398C72 /* ISO8601DateFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ISO8601DateFormatter.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; C178845C1D4EF3D800405663 /* ReadPumpStatusMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPumpStatusMessageBody.swift; sourceTree = ""; }; C178845E1D5166BE00405663 /* COBStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = COBStatus.swift; sourceTree = ""; }; C17884601D519F1E00405663 /* BatteryIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryIndicator.swift; sourceTree = ""; }; @@ -740,7 +793,7 @@ C1A492641D4A5DEB008964FF /* BatteryStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryStatus.swift; sourceTree = ""; }; C1A492661D4A65D9008964FF /* RecommendedTempBasal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecommendedTempBasal.swift; sourceTree = ""; }; C1A492681D4A66C0008964FF /* LoopEnacted.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopEnacted.swift; sourceTree = ""; }; - C1A7215F1EC29C0B0080FAD7 /* PumpCommsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpCommsError.swift; sourceTree = ""; }; + C1A7215F1EC29C0B0080FAD7 /* PumpOpsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpOpsError.swift; sourceTree = ""; }; C1A721611EC3E0500080FAD7 /* PumpErrorMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpErrorMessageBody.swift; sourceTree = ""; }; C1A721651EC4BCE30080FAD7 /* PartialDecode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PartialDecode.swift; sourceTree = ""; }; C1AF21E11D4838C90088C41D /* DeviceStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = ""; }; @@ -758,7 +811,6 @@ C1B383141CD0665D00CE7782 /* NightscoutUploadKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NightscoutUploadKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C1B3831D1CD0665D00CE7782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C1B383351CD1BA8100CE7782 /* DeviceDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceDataManager.swift; sourceTree = ""; }; - C1B4A94D1D1C423D003B8985 /* NSUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NSUserDefaults.swift; path = NightscoutUploadKit/NSUserDefaults.swift; sourceTree = SOURCE_ROOT; }; C1B4A9521D1E613A003B8985 /* TextFieldTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewCell.swift; sourceTree = ""; }; C1B4A9531D1E613A003B8985 /* TextFieldTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TextFieldTableViewCell.xib; sourceTree = ""; }; C1B4A9541D1E613A003B8985 /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = ""; }; @@ -792,8 +844,6 @@ C1EAD6D21C826C43006DBA60 /* MySentryPumpStatusMessageBodyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MySentryPumpStatusMessageBodyTests.swift; path = ../MySentryPumpStatusMessageBodyTests.swift; sourceTree = ""; }; C1EAD6D31C826C43006DBA60 /* NSDataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDataTests.swift; sourceTree = ""; }; C1EAD6D41C826C43006DBA60 /* ReadSettingsCarelinkMessageBodyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReadSettingsCarelinkMessageBodyTests.swift; path = ../ReadSettingsCarelinkMessageBodyTests.swift; sourceTree = ""; }; - C1EAD6D91C829104006DBA60 /* RFTools.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RFTools.swift; sourceTree = ""; }; - C1EAD6DB1C82A4AB006DBA60 /* RFToolsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RFToolsTests.swift; sourceTree = ""; }; C1EAD6DD1C82B78C006DBA60 /* CRC8.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CRC8.swift; sourceTree = ""; }; C1EAD6DF1C82B910006DBA60 /* CRC8Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CRC8Tests.swift; sourceTree = ""; }; C1EAD6E11C82BA7A006DBA60 /* CRC16.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CRC16.swift; sourceTree = ""; }; @@ -805,23 +855,26 @@ C1F0004B1EBE68A600F65163 /* DataFrameMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataFrameMessageBody.swift; sourceTree = ""; }; C1F0004F1EBE727C00F65163 /* BasalScheduleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasalScheduleTests.swift; sourceTree = ""; }; C1F000511EBE73F400F65163 /* BasalSchedule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasalSchedule.swift; sourceTree = ""; }; + C1F6EB881F89C3E200CFE393 /* FourByteSixByteEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FourByteSixByteEncoding.swift; sourceTree = ""; }; + C1F6EB8A1F89C41200CFE393 /* MinimedPacket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinimedPacket.swift; sourceTree = ""; }; + C1F6EB8C1F89C45500CFE393 /* MinimedPacketTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinimedPacketTests.swift; sourceTree = ""; }; C1FDFCA81D964A3E00ADBC31 /* BolusReminderPumpEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusReminderPumpEvent.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 430D64C71CB855AB00FCA750 /* Frameworks */ = { + 431CE76B1F98564100255374 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 43CA93251CB8BB33000026B5 /* CoreBluetooth.framework in Frameworks */, + 43C0196C1FA6B8AE007ABFA1 /* CoreBluetooth.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 430D64D11CB855AB00FCA750 /* Frameworks */ = { + 431CE7741F98564200255374 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 430D64D51CB855AB00FCA750 /* RileyLinkBLEKit.framework in Frameworks */, + 431CE7781F98564200255374 /* RileyLinkBLEKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -829,8 +882,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 432847C11FA1737400CDE69C /* RileyLinkBLEKit.framework in Frameworks */, 43722FCB1CB9F7DB0038B7F2 /* MinimedKit.framework in Frameworks */, - 43722FCC1CB9F7DB0038B7F2 /* RileyLinkBLEKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -849,6 +902,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 43D5E78A1FAF7BFB004ACDB7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 432CF9061FF74CCB003AB446 /* RileyLinkKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C10D9BBD1C8269D500378342 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -870,11 +931,12 @@ files = ( 43C246A91D8A31540031F8D1 /* Crypto.framework in Frameworks */, C1B383201CD0665D00CE7782 /* NightscoutUploadKit.framework in Frameworks */, - 430D64E01CB855AB00FCA750 /* RileyLinkBLEKit.framework in Frameworks */, C12EA23D198B436800309FA4 /* CoreGraphics.framework in Frameworks */, + 43D5E7951FAF7BFB004ACDB7 /* RileyLinkKitUI.framework in Frameworks */, C12EA23F198B436800309FA4 /* UIKit.framework in Frameworks */, 43722FC31CB9F7640038B7F2 /* RileyLinkKit.framework in Frameworks */, C12EA23B198B436800309FA4 /* Foundation.framework in Frameworks */, + 431CE7841F98564200255374 /* RileyLinkBLEKit.framework in Frameworks */, C10D9BD61C8269D500378342 /* MinimedKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -896,7 +958,6 @@ buildActionMask = 2147483647; files = ( 43C246A61D891DBF0031F8D1 /* Crypto.framework in Frameworks */, - C1B383311CD068C300CE7782 /* RileyLinkBLEKit.framework in Frameworks */, C1B383301CD0680800CE7782 /* MinimedKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -921,40 +982,38 @@ path = PumpEvents; sourceTree = ""; }; - 430D64CC1CB855AB00FCA750 /* RileyLinkBLEKit */ = { + 431CE7701F98564100255374 /* RileyLinkBLEKit */ = { isa = PBXGroup; children = ( - 430D64F01CB89FC000FCA750 /* CmdBase.h */, - 430D64F11CB89FC000FCA750 /* CmdBase.m */, - 430D64F21CB89FC000FCA750 /* GetPacketCmd.h */, - 430D64F31CB89FC000FCA750 /* GetPacketCmd.m */, - 430D64F41CB89FC000FCA750 /* GetVersionCmd.h */, - 430D64F51CB89FC000FCA750 /* GetVersionCmd.m */, - 430D64CF1CB855AB00FCA750 /* Info.plist */, - 430D64F61CB89FC000FCA750 /* ReceivingPacketCmd.h */, - 430D64F71CB89FC000FCA750 /* ReceivingPacketCmd.m */, - 430D64F81CB89FC000FCA750 /* RFPacket.h */, - 430D64F91CB89FC000FCA750 /* RFPacket.m */, - 430D64E81CB85A4300FCA750 /* RileyLinkBLEDevice.h */, - 430D64E91CB85A4300FCA750 /* RileyLinkBLEDevice.m */, - 430D64CD1CB855AB00FCA750 /* RileyLinkBLEKit.h */, - 430D64EA1CB85A4300FCA750 /* RileyLinkBLEManager.h */, - 430D64EB1CB85A4300FCA750 /* RileyLinkBLEManager.m */, - 430D64FA1CB89FC000FCA750 /* SendAndListenCmd.h */, - 430D64FB1CB89FC000FCA750 /* SendAndListenCmd.m */, - 430D64FC1CB89FC000FCA750 /* SendPacketCmd.h */, - 430D64FD1CB89FC000FCA750 /* SendPacketCmd.m */, - 430D64FE1CB89FC000FCA750 /* UpdateRegisterCmd.h */, - 430D64FF1CB89FC000FCA750 /* UpdateRegisterCmd.m */, + 431CE7711F98564100255374 /* RileyLinkBLEKit.h */, + 431CE7721F98564100255374 /* Info.plist */, + 7D7076881FE092D7004AC8EA /* InfoPlist.strings */, + 431CE79D1F9BE73900255374 /* BLEFirmwareVersion.swift */, + 431CE7A01F9D195600255374 /* CBCentralManager.swift */, + 431CE78E1F985B6E00255374 /* CBPeripheral.swift */, + 431CE7A21F9D737F00255374 /* Command.swift */, + 431CE7A61F9D98F700255374 /* CommandSession.swift */, + 431CE78C1F985B5400255374 /* PeripheralManager.swift */, + 43D5E7871FAEDAC4004ACDB7 /* PeripheralManagerError.swift */, + 431CE7921F985DE700255374 /* PeripheralManager+RileyLink.swift */, + 431CE7A41F9D78F500255374 /* RFPacket.swift */, + 432847C21FA57C0F00CDE69C /* RadioFirmwareVersion.swift */, + 43BA719A202591A70058961E /* Response.swift */, + 43BA719C2026C9B00058961E /* ResponseBuffer.swift */, + 431CE79B1F9B21BA00255374 /* RileyLinkDevice.swift */, + 433ABFFB2016FDF700E6C1FF /* RileyLinkDeviceError.swift */, + 431CE7901F985D8D00255374 /* RileyLinkDeviceManager.swift */, ); path = RileyLinkBLEKit; sourceTree = ""; }; - 430D64DA1CB855AB00FCA750 /* RileyLinkBLEKitTests */ = { + 431CE77D1F98564200255374 /* RileyLinkBLEKitTests */ = { isa = PBXGroup; children = ( - 430D64DB1CB855AB00FCA750 /* RileyLinkBLEKitTests.m */, - 430D64DD1CB855AB00FCA750 /* Info.plist */, + 43047FC51FAEC83000508343 /* RFPacketTests.swift */, + 43047FC31FAEC70600508343 /* RadioFirmwareVersionTests.swift */, + 4322B75520282DA60002837D /* ResponseBufferTests.swift */, + 431CE7801F98564200255374 /* Info.plist */, ); path = RileyLinkBLEKitTests; sourceTree = ""; @@ -962,18 +1021,18 @@ 43722FAF1CB9F7630038B7F2 /* RileyLinkKit */ = { isa = PBXGroup; children = ( - C1B4A9511D1E610F003B8985 /* UI */, + 7D7076831FE092D7004AC8EA /* InfoPlist.strings */, + 7D7076741FE092D5004AC8EA /* Localizable.strings */, C170C98C1CECD6CE00F3D8E5 /* Extensions */, 43722FB01CB9F7640038B7F2 /* RileyLinkKit.h */, 43722FB21CB9F7640038B7F2 /* Info.plist */, - 434AB0911CBA0DF600422F4A /* Either.swift */, + 437F54061FBD52120070FF2C /* DeviceState.swift */, + 2F962EC21E6873A10070EFBD /* PumpMessageSender.swift */, 434AB0921CBA0DF600422F4A /* PumpOps.swift */, - 434AB0931CBA0DF600422F4A /* PumpOpsSynchronous.swift */, + C1A7215F1EC29C0B0080FAD7 /* PumpOpsError.swift */, + 434AB0931CBA0DF600422F4A /* PumpOpsSession.swift */, + 4384C8C71FB937E500D916E6 /* PumpSettings.swift */, 434AB0941CBA0DF600422F4A /* PumpState.swift */, - 434AB0C51CBCB41500422F4A /* RileyLinkDevice.swift */, - 434AB0BD1CBB4E3200422F4A /* RileyLinkDeviceManager.swift */, - 2F962EC21E6873A10070EFBD /* PumpOpsCommunication.swift */, - C1A7215F1EC29C0B0080FAD7 /* PumpCommsError.swift */, ); path = RileyLinkKit; sourceTree = ""; @@ -981,9 +1040,9 @@ 43722FBD1CB9F7640038B7F2 /* RileyLinkKitTests */ = { isa = PBXGroup; children = ( + 2F962EC41E705C6D0070EFBD /* PumpOpsSynchronousBuildFromFramesTests.swift */, 2F962EBE1E678BAA0070EFBD /* PumpOpsSynchronousTests.swift */, 43722FC01CB9F7640038B7F2 /* Info.plist */, - 2F962EC41E705C6D0070EFBD /* PumpOpsSynchronousBuildFromFramesTests.swift */, ); path = RileyLinkKitTests; sourceTree = ""; @@ -991,6 +1050,7 @@ 43C246941D8918AE0031F8D1 /* Crypto */ = { isa = PBXGroup; children = ( + 7D7076791FE092D6004AC8EA /* InfoPlist.strings */, 43C246951D8918AE0031F8D1 /* Crypto.h */, 43C2469F1D8919E20031F8D1 /* Crypto.m */, 43C246961D8918AE0031F8D1 /* Info.plist */, @@ -998,10 +1058,36 @@ path = Crypto; sourceTree = ""; }; + 43D5E78F1FAF7BFB004ACDB7 /* RileyLinkKitUI */ = { + isa = PBXGroup; + children = ( + 43D5E7901FAF7BFB004ACDB7 /* RileyLinkKitUI.h */, + 43D5E7911FAF7BFB004ACDB7 /* Info.plist */, + C170C98D1CECD6F300F3D8E5 /* CBPeripheralState.swift */, + C1C659181E16BA9D0025CC58 /* CaseCountable.swift */, + C170C9961CECD80000F3D8E5 /* CommandResponseViewController.swift */, + 435535D91FB836CB00CE5A23 /* CommandResponseViewController+RileyLinkDevice.swift */, + 435535D71FB7987000CE5A23 /* PumpOps.swift */, + 437F54081FBF9E0A0070FF2C /* RileyLinkDevice.swift */, + 439731261CF21C3C00F474E5 /* RileyLinkDeviceTableViewCell.swift */, + C170C9981CECD80000F3D8E5 /* RileyLinkDeviceTableViewController.swift */, + C1B4A9521D1E613A003B8985 /* TextFieldTableViewCell.swift */, + C1B4A9531D1E613A003B8985 /* TextFieldTableViewCell.xib */, + C1B4A9541D1E613A003B8985 /* TextFieldTableViewController.swift */, + C1B4A9581D1E6357003B8985 /* UITableViewCell.swift */, + ); + path = RileyLinkKitUI; + sourceTree = ""; + }; 43EBE44F1EAD234F0073A0B5 /* Common */ = { isa = PBXGroup; children = ( + 431185AE1CF25A590059ED98 /* IdentifiableClass.swift */, + 431CE7941F9B0DAE00255374 /* OSLog.swift */, + C1EAD6BA1C826B92006DBA60 /* NSData.swift */, + 43323EA61FA81A0F003FB0FA /* NumberFormatter.swift */, 43EBE4501EAD238C0073A0B5 /* TimeInterval.swift */, + 4345D1CD1DA16AF300BAAD22 /* TimeZone.swift */, ); path = Common; sourceTree = ""; @@ -1058,6 +1144,8 @@ C10D9BC21C8269D500378342 /* MinimedKit */ = { isa = PBXGroup; children = ( + 7D70768D1FE09310004AC8EA /* Localizable.strings */, + 7D70767E1FE092D6004AC8EA /* InfoPlist.strings */, C1EAD6B81C826B92006DBA60 /* Extensions */, C1EAD6BC1C826B92006DBA60 /* Messages */, 54BC44761DB46C3100340EED /* GlucoseEvents */, @@ -1067,6 +1155,7 @@ C1C73F1C1DE6306A0022FC89 /* BatteryChemistryType.swift */, C1EAD6DD1C82B78C006DBA60 /* CRC8.swift */, C1EAD6E11C82BA7A006DBA60 /* CRC16.swift */, + C1F6EB881F89C3E200CFE393 /* FourByteSixByteEncoding.swift */, 54BC447B1DB4742F00340EED /* GlucoseEventType.swift */, 54BC44741DB46B0A00340EED /* GlucosePage.swift */, C1EB955C1C887FE5002517DF /* HistoryPage.swift */, @@ -1075,12 +1164,12 @@ C1EAD6B01C826B6D006DBA60 /* MessageType.swift */, C1C3578E1C927303009BDD4F /* MeterMessage.swift */, C10D9BC31C8269D500378342 /* MinimedKit.h */, + C1F6EB8A1F89C41200CFE393 /* MinimedPacket.swift */, C1EAD6B11C826B6D006DBA60 /* PacketType.swift */, C1842BBE1C8E855A00DB42AC /* PumpEventType.swift */, C1EAD6B21C826B6D006DBA60 /* PumpMessage.swift */, C1842BBA1C8E184300DB42AC /* PumpModel.swift */, C1274F851D8242BE0002912B /* PumpRegion.swift */, - C1EAD6D91C829104006DBA60 /* RFTools.swift */, 43B0ADC11D12454700AAD278 /* TimestampedHistoryEvent.swift */, 541688DE1DB82E72005B1891 /* TimestampedGlucoseEvent.swift */, C1A721651EC4BCE30080FAD7 /* PartialDecode.swift */, @@ -1101,11 +1190,11 @@ C12198621C8DF4C800BC374C /* HistoryPageTests.swift */, C10D9BD31C8269D500378342 /* Info.plist */, C1C357901C92733A009BDD4F /* MeterMessageTests.swift */, + C1F6EB8C1F89C45500CFE393 /* MinimedPacketTests.swift */, C1EAD6D31C826C43006DBA60 /* NSDataTests.swift */, 43FF221B1CB9B9DE00024F30 /* NSDateComponents.swift */, 43B0ADBF1D0FC03200AAD278 /* NSDateComponentsTests.swift */, 2F962EC91E70831F0070EFBD /* PumpModelTests.swift */, - C1EAD6DB1C82A4AB006DBA60 /* RFToolsTests.swift */, 54BC44B41DB7184D00340EED /* NSStringExtensions.swift */, 2F962EC01E6872170070EFBD /* TimestampedHistoryEventTests.swift */, ); @@ -1140,8 +1229,6 @@ C14FFC521D3D70170049CF85 /* ValidatingIndicatorView.swift */, C14FFC5C1D3D75200049CF85 /* ButtonTableViewCell.swift */, C14FFC5D1D3D75200049CF85 /* ButtonTableViewCell.xib */, - C12616581B6B2D20001FAD87 /* PacketTableViewCell.h */, - C12616591B6B2D20001FAD87 /* PacketTableViewCell.m */, 434FF1DD1CF268F3000DB779 /* RileyLinkDeviceTableViewCell.swift */, C16843761CF00C0100D53CCD /* SwitchTableViewCell.swift */, ); @@ -1158,21 +1245,6 @@ name = CoreData; sourceTree = ""; }; - C126165E1B76E8DC001FAD87 /* TPKeyboardAvoiding */ = { - isa = PBXGroup; - children = ( - C126165F1B76E8DC001FAD87 /* TPKeyboardAvoidingCollectionView.h */, - C12616601B76E8DC001FAD87 /* TPKeyboardAvoidingCollectionView.m */, - C12616611B76E8DC001FAD87 /* TPKeyboardAvoidingScrollView.h */, - C12616621B76E8DC001FAD87 /* TPKeyboardAvoidingScrollView.m */, - C12616631B76E8DC001FAD87 /* TPKeyboardAvoidingTableView.h */, - C12616641B76E8DC001FAD87 /* TPKeyboardAvoidingTableView.m */, - C12616651B76E8DC001FAD87 /* UIScrollView+TPKeyboardAvoidingAdditions.h */, - C12616661B76E8DC001FAD87 /* UIScrollView+TPKeyboardAvoidingAdditions.m */, - ); - path = TPKeyboardAvoiding; - sourceTree = ""; - }; C1274F7A1D8240D00002912B /* View Controllers */ = { isa = PBXGroup; children = ( @@ -1180,7 +1252,6 @@ C1274F7B1D82411C0002912B /* AuthenticationViewController.swift */, C1274F7C1D82411C0002912B /* RileyLinkListTableViewController.swift */, C1274F7D1D82411C0002912B /* SettingsTableViewController.swift */, - C1274F7E1D82411C0002912B /* TextFieldTableViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -1192,14 +1263,15 @@ C12EA259198B436900309FA4 /* RileyLinkTests */, C10D9BC21C8269D500378342 /* MinimedKit */, C10D9BD01C8269D500378342 /* MinimedKitTests */, - 430D64CC1CB855AB00FCA750 /* RileyLinkBLEKit */, - 430D64DA1CB855AB00FCA750 /* RileyLinkBLEKitTests */, 43722FAF1CB9F7630038B7F2 /* RileyLinkKit */, 43722FBD1CB9F7640038B7F2 /* RileyLinkKitTests */, C1B3830C1CD0665D00CE7782 /* NightscoutUploadKit */, C1B3831A1CD0665D00CE7782 /* NightscoutUploadKitTests */, 43EBE44F1EAD234F0073A0B5 /* Common */, 43C246941D8918AE0031F8D1 /* Crypto */, + 431CE7701F98564100255374 /* RileyLinkBLEKit */, + 431CE77D1F98564200255374 /* RileyLinkBLEKitTests */, + 43D5E78F1FAF7BFB004ACDB7 /* RileyLinkKitUI */, C12EA239198B436800309FA4 /* Frameworks */, C12EA238198B436800309FA4 /* Products */, ); @@ -1212,13 +1284,14 @@ C12EA252198B436800309FA4 /* RileyLinkTests.xctest */, C10D9BC11C8269D500378342 /* MinimedKit.framework */, C10D9BCA1C8269D500378342 /* MinimedKitTests.xctest */, - 430D64CB1CB855AB00FCA750 /* RileyLinkBLEKit.framework */, - 430D64D41CB855AB00FCA750 /* RileyLinkBLEKitTests.xctest */, 43722FAE1CB9F7630038B7F2 /* RileyLinkKit.framework */, 43722FB71CB9F7640038B7F2 /* RileyLinkKitTests.xctest */, C1B3830B1CD0665D00CE7782 /* NightscoutUploadKit.framework */, C1B383141CD0665D00CE7782 /* NightscoutUploadKitTests.xctest */, 43C246931D8918AE0031F8D1 /* Crypto.framework */, + 431CE76F1F98564100255374 /* RileyLinkBLEKit.framework */, + 431CE7771F98564200255374 /* RileyLinkBLEKitTests.xctest */, + 43D5E78E1FAF7BFB004ACDB7 /* RileyLinkKitUI.framework */, ); name = Products; sourceTree = ""; @@ -1227,6 +1300,7 @@ isa = PBXGroup; children = ( 43CA93241CB8BB33000026B5 /* CoreBluetooth.framework */, + 4370A3791FAF8A7400EC666A /* CoreBluetooth.framework */, C12616431B685F0A001FAD87 /* CoreData.framework */, C12EA23A198B436800309FA4 /* Foundation.framework */, C12EA23C198B436800309FA4 /* CoreGraphics.framework */, @@ -1246,18 +1320,13 @@ C12616451B685F35001FAD87 /* CoreData */, C12EA241198B436800309FA4 /* Supporting Files */, C12616391B67CB9C001FAD87 /* Views */, - C126165E1B76E8DC001FAD87 /* TPKeyboardAvoiding */, 43462E8A1CCB06F500F958A8 /* AppDelegate.swift */, C1EF58861B3F93FE001C8C80 /* Config.h */, C1EF58871B3F93FE001C8C80 /* Config.m */, C12EA24C198B436800309FA4 /* Images.xcassets */, - C174F26919EB824D00398C72 /* ISO8601DateFormatter.h */, - C174F26A19EB824D00398C72 /* ISO8601DateFormatter.m */, 43EC9DCA1B786C6200DB0D18 /* LaunchScreen.xib */, C1271B081A9A350400B7C949 /* Log.h */, C1271B061A9A34E900B7C949 /* Log.m */, - C139AC221BFD84B500B0518F /* RuntimeUtils.h */, - C139AC231BFD84B500B0518F /* RuntimeUtils.m */, C12EA269198B442100309FA4 /* Storyboard.storyboard */, ); path = RileyLink; @@ -1266,6 +1335,8 @@ C12EA241198B436800309FA4 /* Supporting Files */ = { isa = PBXGroup; children = ( + 7D7076971FE09311004AC8EA /* Localizable.strings */, + 7D70766F1FE092D4004AC8EA /* LoopKit.strings */, C1EAD6EA1C8409A9006DBA60 /* RileyLink-Bridging-Header.h */, C12EA242198B436800309FA4 /* RileyLink-Info.plist */, C12EA243198B436800309FA4 /* InfoPlist.strings */, @@ -1304,11 +1375,11 @@ C170C98C1CECD6CE00F3D8E5 /* Extensions */ = { isa = PBXGroup; children = ( - C170C98D1CECD6F300F3D8E5 /* CBPeripheralState.swift */, - 431185AE1CF25A590059ED98 /* IdentifiableClass.swift */, - 4345D1CD1DA16AF300BAAD22 /* TimeZone.swift */, - C1B4A9581D1E6357003B8985 /* UITableViewCell.swift */, - C1C659181E16BA9D0025CC58 /* CaseCountable.swift */, + 43BF58B11FF5A22200499C46 /* BasalProfile.swift */, + 436CCEF11FB953E800A6822B /* CommandSession.swift */, + 4384C8C51FB92F8100D916E6 /* HistoryPage.swift */, + 435535DB1FB8B37E00CE5A23 /* PumpMessage.swift */, + 43323EA91FA81C1B003FB0FA /* RileyLinkDevice.swift */, ); path = Extensions; sourceTree = ""; @@ -1395,8 +1466,10 @@ C14FFC5A1D3D74F90049CF85 /* NibLoadable.swift */, C1E535E81991E36700C2AC49 /* NSData+Conversion.h */, C1E535E91991E36700C2AC49 /* NSData+Conversion.m */, + 437462381FA9287A00643383 /* RileyLinkDevice.swift */, C14FFC601D3D75470049CF85 /* UIColor.swift */, C14FFC541D3D72A50049CF85 /* UIViewController.swift */, + 435535D51FB6D98400CE5A23 /* UserDefaults.swift */, ); path = Extensions; sourceTree = ""; @@ -1438,6 +1511,7 @@ C1B3830C1CD0665D00CE7782 /* NightscoutUploadKit */ = { isa = PBXGroup; children = ( + 7D7076921FE09311004AC8EA /* InfoPlist.strings */, C1AF21E91D4900300088C41D /* DeviceStatus */, C13D15591DAACE8400ADC044 /* Either.swift */, 43F348051D596270009933DC /* HKUnit.swift */, @@ -1446,7 +1520,6 @@ C1842C2A1C90DFB600DB42AC /* NightscoutPumpEvents.swift */, C1842C281C908A3C00DB42AC /* NightscoutUploader.swift */, C1B3830D1CD0665D00CE7782 /* NightscoutUploadKit.h */, - C1B4A94D1D1C423D003B8985 /* NSUserDefaults.swift */, 43B0ADC81D1268B300AAD278 /* TimeFormat.swift */, C1AF21EA1D4900880088C41D /* Treatments */, ); @@ -1473,25 +1546,10 @@ name = Managers; sourceTree = ""; }; - C1B4A9511D1E610F003B8985 /* UI */ = { - isa = PBXGroup; - children = ( - C170C9961CECD80000F3D8E5 /* CommandResponseViewController.swift */, - 439731261CF21C3C00F474E5 /* RileyLinkDeviceTableViewCell.swift */, - 431185A91CF257D10059ED98 /* RileyLinkDeviceTableViewCell.xib */, - C170C9981CECD80000F3D8E5 /* RileyLinkDeviceTableViewController.swift */, - C1B4A9521D1E613A003B8985 /* TextFieldTableViewCell.swift */, - C1B4A9531D1E613A003B8985 /* TextFieldTableViewCell.xib */, - C1B4A9541D1E613A003B8985 /* TextFieldTableViewController.swift */, - ); - path = UI; - sourceTree = ""; - }; C1EAD6B81C826B92006DBA60 /* Extensions */ = { isa = PBXGroup; children = ( C1EAD6B91C826B92006DBA60 /* Int.swift */, - C1EAD6BA1C826B92006DBA60 /* NSData.swift */, C1EAD6BB1C826B92006DBA60 /* NSDateComponents.swift */, 43B0ADC31D12506A00AAD278 /* NSDateFormatter.swift */, ); @@ -1528,6 +1586,7 @@ 2FDE1A061E57B12D00B56A27 /* ReadCurrentPageNumberMessageBody.swift */, C1F0004B1EBE68A600F65163 /* DataFrameMessageBody.swift */, C1A721611EC3E0500080FAD7 /* PumpErrorMessageBody.swift */, + 43BF58AF1FF594CB00499C46 /* SelectBasalProfileMessageBody.swift */, ); path = Messages; sourceTree = ""; @@ -1535,21 +1594,11 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ - 430D64C81CB855AB00FCA750 /* Headers */ = { + 431CE76C1F98564100255374 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 430D65081CB89FC000FCA750 /* RFPacket.h in Headers */, - 430D650C1CB89FC000FCA750 /* SendPacketCmd.h in Headers */, - 430D650E1CB89FC000FCA750 /* UpdateRegisterCmd.h in Headers */, - 430D650A1CB89FC000FCA750 /* SendAndListenCmd.h in Headers */, - 430D64EE1CB85A4300FCA750 /* RileyLinkBLEManager.h in Headers */, - 430D64EC1CB85A4300FCA750 /* RileyLinkBLEDevice.h in Headers */, - 430D65021CB89FC000FCA750 /* GetPacketCmd.h in Headers */, - 430D65001CB89FC000FCA750 /* CmdBase.h in Headers */, - 430D64CE1CB855AB00FCA750 /* RileyLinkBLEKit.h in Headers */, - 430D65061CB89FC000FCA750 /* ReceivingPacketCmd.h in Headers */, - 430D65041CB89FC000FCA750 /* GetVersionCmd.h in Headers */, + 431CE7811F98564200255374 /* RileyLinkBLEKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1569,6 +1618,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 43D5E78B1FAF7BFB004ACDB7 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 43D5E7921FAF7BFB004ACDB7 /* RileyLinkKitUI.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C10D9BBE1C8269D500378342 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -1588,14 +1645,14 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - 430D64CA1CB855AB00FCA750 /* RileyLinkBLEKit */ = { + 431CE76E1F98564100255374 /* RileyLinkBLEKit */ = { isa = PBXNativeTarget; - buildConfigurationList = 430D64E61CB855AB00FCA750 /* Build configuration list for PBXNativeTarget "RileyLinkBLEKit" */; + buildConfigurationList = 431CE78A1F98564200255374 /* Build configuration list for PBXNativeTarget "RileyLinkBLEKit" */; buildPhases = ( - 430D64C61CB855AB00FCA750 /* Sources */, - 430D64C71CB855AB00FCA750 /* Frameworks */, - 430D64C81CB855AB00FCA750 /* Headers */, - 430D64C91CB855AB00FCA750 /* Resources */, + 431CE76A1F98564100255374 /* Sources */, + 431CE76B1F98564100255374 /* Frameworks */, + 431CE76C1F98564100255374 /* Headers */, + 431CE76D1F98564100255374 /* Resources */, ); buildRules = ( ); @@ -1603,25 +1660,26 @@ ); name = RileyLinkBLEKit; productName = RileyLinkBLEKit; - productReference = 430D64CB1CB855AB00FCA750 /* RileyLinkBLEKit.framework */; + productReference = 431CE76F1F98564100255374 /* RileyLinkBLEKit.framework */; productType = "com.apple.product-type.framework"; }; - 430D64D31CB855AB00FCA750 /* RileyLinkBLEKitTests */ = { + 431CE7761F98564200255374 /* RileyLinkBLEKitTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 430D64E71CB855AB00FCA750 /* Build configuration list for PBXNativeTarget "RileyLinkBLEKitTests" */; + buildConfigurationList = 431CE78B1F98564200255374 /* Build configuration list for PBXNativeTarget "RileyLinkBLEKitTests" */; buildPhases = ( - 430D64D01CB855AB00FCA750 /* Sources */, - 430D64D11CB855AB00FCA750 /* Frameworks */, - 430D64D21CB855AB00FCA750 /* Resources */, + 431CE7731F98564200255374 /* Sources */, + 431CE7741F98564200255374 /* Frameworks */, + 431CE7751F98564200255374 /* Resources */, ); buildRules = ( ); dependencies = ( - 430D64D71CB855AB00FCA750 /* PBXTargetDependency */, + 431CE77A1F98564200255374 /* PBXTargetDependency */, + 431CE77C1F98564200255374 /* PBXTargetDependency */, ); name = RileyLinkBLEKitTests; productName = RileyLinkBLEKitTests; - productReference = 430D64D41CB855AB00FCA750 /* RileyLinkBLEKitTests.xctest */; + productReference = 431CE7771F98564200255374 /* RileyLinkBLEKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 43722FAD1CB9F7630038B7F2 /* RileyLinkKit */ = { @@ -1678,6 +1736,24 @@ productReference = 43C246931D8918AE0031F8D1 /* Crypto.framework */; productType = "com.apple.product-type.framework"; }; + 43D5E78D1FAF7BFB004ACDB7 /* RileyLinkKitUI */ = { + isa = PBXNativeTarget; + buildConfigurationList = 43D5E7971FAF7BFB004ACDB7 /* Build configuration list for PBXNativeTarget "RileyLinkKitUI" */; + buildPhases = ( + 43D5E7891FAF7BFB004ACDB7 /* Sources */, + 43D5E78A1FAF7BFB004ACDB7 /* Frameworks */, + 43D5E78B1FAF7BFB004ACDB7 /* Headers */, + 43D5E78C1FAF7BFB004ACDB7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RileyLinkKitUI; + productName = RileyLinkKitUI; + productReference = 43D5E78E1FAF7BFB004ACDB7 /* RileyLinkKitUI.framework */; + productType = "com.apple.product-type.framework"; + }; C10D9BC01C8269D500378342 /* MinimedKit */ = { isa = PBXNativeTarget; buildConfigurationList = C10D9BD81C8269D600378342 /* Build configuration list for PBXNativeTarget "MinimedKit" */; @@ -1728,9 +1804,10 @@ dependencies = ( 43C246A31D891D6C0031F8D1 /* PBXTargetDependency */, C10D9BD51C8269D500378342 /* PBXTargetDependency */, - 430D64DF1CB855AB00FCA750 /* PBXTargetDependency */, 43722FC21CB9F7640038B7F2 /* PBXTargetDependency */, C1B3831F1CD0665D00CE7782 /* PBXTargetDependency */, + 431CE7831F98564200255374 /* PBXTargetDependency */, + 43D5E7941FAF7BFB004ACDB7 /* PBXTargetDependency */, ); name = RileyLink; productName = RileyLink; @@ -1800,15 +1877,19 @@ C12EA22F198B436800309FA4 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0900; + LastSwiftUpdateCheck = 0900; + LastUpgradeCheck = 0930; ORGANIZATIONNAME = "Pete Schwamb"; TargetAttributes = { - 430D64CA1CB855AB00FCA750 = { - CreatedOnToolsVersion = 7.3; + 431CE76E1F98564100255374 = { + CreatedOnToolsVersion = 9.0; + ProvisioningStyle = Manual; }; - 430D64D31CB855AB00FCA750 = { - CreatedOnToolsVersion = 7.3; + 431CE7761F98564200255374 = { + CreatedOnToolsVersion = 9.0; + LastSwiftMigration = 0910; + ProvisioningStyle = Automatic; + TestTargetID = C12EA236198B436800309FA4; }; 43722FAD1CB9F7630038B7F2 = { CreatedOnToolsVersion = 7.3; @@ -1822,6 +1903,10 @@ CreatedOnToolsVersion = 8.0; ProvisioningStyle = Manual; }; + 43D5E78D1FAF7BFB004ACDB7 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; C10D9BC01C8269D500378342 = { CreatedOnToolsVersion = 7.2.1; LastSwiftMigration = 0900; @@ -1854,6 +1939,8 @@ hasScannedForEncodings = 0; knownRegions = ( en, + es, + ru, ); mainGroup = C12EA22E198B436800309FA4; productRefGroup = C12EA238198B436800309FA4 /* Products */; @@ -1864,26 +1951,27 @@ C12EA251198B436800309FA4 /* RileyLinkTests */, C10D9BC01C8269D500378342 /* MinimedKit */, C10D9BC91C8269D500378342 /* MinimedKitTests */, - 430D64CA1CB855AB00FCA750 /* RileyLinkBLEKit */, - 430D64D31CB855AB00FCA750 /* RileyLinkBLEKitTests */, 43722FAD1CB9F7630038B7F2 /* RileyLinkKit */, 43722FB61CB9F7640038B7F2 /* RileyLinkKitTests */, C1B3830A1CD0665D00CE7782 /* NightscoutUploadKit */, C1B383131CD0665D00CE7782 /* NightscoutUploadKitTests */, 43C246921D8918AE0031F8D1 /* Crypto */, + 431CE76E1F98564100255374 /* RileyLinkBLEKit */, + 431CE7761F98564200255374 /* RileyLinkBLEKitTests */, + 43D5E78D1FAF7BFB004ACDB7 /* RileyLinkKitUI */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 430D64C91CB855AB00FCA750 /* Resources */ = { + 431CE76D1F98564100255374 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 430D64D21CB855AB00FCA750 /* Resources */ = { + 431CE7751F98564200255374 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -1894,8 +1982,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 431185AA1CF257D10059ED98 /* RileyLinkDeviceTableViewCell.xib in Resources */, - C1B4A9561D1E613A003B8985 /* TextFieldTableViewCell.xib in Resources */, + 7D7076811FE092D7004AC8EA /* InfoPlist.strings in Resources */, + 7D7076721FE092D5004AC8EA /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1910,6 +1998,15 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7D7076771FE092D6004AC8EA /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 43D5E78C1FAF7BFB004ACDB7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4370A37E1FAF8EF200EC666A /* TextFieldTableViewCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1917,6 +2014,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7D70767C1FE092D6004AC8EA /* InfoPlist.strings in Resources */, + 7D70768B1FE09310004AC8EA /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1933,8 +2032,10 @@ files = ( 43EC9DCB1B786C6200DB0D18 /* LaunchScreen.xib in Resources */, C14FFC5F1D3D75200049CF85 /* ButtonTableViewCell.xib in Resources */, + 7D70766D1FE092D4004AC8EA /* LoopKit.strings in Resources */, C12EA245198B436800309FA4 /* InfoPlist.strings in Resources */, C12EA24D198B436800309FA4 /* Images.xcassets in Resources */, + 7D7076951FE09311004AC8EA /* Localizable.strings in Resources */, C12EA26A198B442100309FA4 /* Storyboard.storyboard in Resources */, C14FFC591D3D72D30049CF85 /* AuthenticationTableViewCell.xib in Resources */, ); @@ -1952,6 +2053,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7D7076901FE09311004AC8EA /* InfoPlist.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1965,29 +2067,39 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 430D64C61CB855AB00FCA750 /* Sources */ = { + 431CE76A1F98564100255374 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 430D64ED1CB85A4300FCA750 /* RileyLinkBLEDevice.m in Sources */, - 43523ED71CC2C558001850F1 /* NSData+Conversion.m in Sources */, - 430D65031CB89FC000FCA750 /* GetPacketCmd.m in Sources */, - 430D65091CB89FC000FCA750 /* RFPacket.m in Sources */, - 430D65011CB89FC000FCA750 /* CmdBase.m in Sources */, - 430D64EF1CB85A4300FCA750 /* RileyLinkBLEManager.m in Sources */, - 430D650B1CB89FC000FCA750 /* SendAndListenCmd.m in Sources */, - 430D65051CB89FC000FCA750 /* GetVersionCmd.m in Sources */, - 430D650D1CB89FC000FCA750 /* SendPacketCmd.m in Sources */, - 430D650F1CB89FC000FCA750 /* UpdateRegisterCmd.m in Sources */, - 430D65071CB89FC000FCA750 /* ReceivingPacketCmd.m in Sources */, + 43BA719D2026C9B00058961E /* ResponseBuffer.swift in Sources */, + 431CE78F1F985B6E00255374 /* CBPeripheral.swift in Sources */, + 431CE79F1F9C670600255374 /* TimeInterval.swift in Sources */, + 433ABFFC2016FDF700E6C1FF /* RileyLinkDeviceError.swift in Sources */, + 43BA719B202591A70058961E /* Response.swift in Sources */, + 43D5E7881FAEDAC4004ACDB7 /* PeripheralManagerError.swift in Sources */, + 431CE79C1F9B21BA00255374 /* RileyLinkDevice.swift in Sources */, + 431CE7A71F9D98F700255374 /* CommandSession.swift in Sources */, + 431CE7A31F9D737F00255374 /* Command.swift in Sources */, + 431CE7A11F9D195600255374 /* CBCentralManager.swift in Sources */, + 431CE7911F985D8D00255374 /* RileyLinkDeviceManager.swift in Sources */, + 431CE78D1F985B5400255374 /* PeripheralManager.swift in Sources */, + 431CE79E1F9BE73900255374 /* BLEFirmwareVersion.swift in Sources */, + 432847C31FA57C0F00CDE69C /* RadioFirmwareVersion.swift in Sources */, + 431CE7931F985DE700255374 /* PeripheralManager+RileyLink.swift in Sources */, + 431CE79A1F9B0F1600255374 /* OSLog.swift in Sources */, + 43047FC91FAECA8700508343 /* NSData.swift in Sources */, + 431CE7A51F9D78F500255374 /* RFPacket.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 430D64D01CB855AB00FCA750 /* Sources */ = { + 431CE7731F98564200255374 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 430D64DC1CB855AB00FCA750 /* RileyLinkBLEKitTests.m in Sources */, + 4322B75620282DA60002837D /* ResponseBufferTests.swift in Sources */, + 43047FC41FAEC70600508343 /* RadioFirmwareVersionTests.swift in Sources */, + 43047FC71FAEC9BC00508343 /* NSData.swift in Sources */, + 43047FC61FAEC83000508343 /* RFPacketTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1995,26 +2107,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4384C8C81FB937E500D916E6 /* PumpSettings.swift in Sources */, 434AB0961CBA0DF600422F4A /* PumpOps.swift in Sources */, + 43323EAA1FA81C1B003FB0FA /* RileyLinkDevice.swift in Sources */, 43EBE4531EAD23CE0073A0B5 /* TimeInterval.swift in Sources */, - 431185AF1CF25A590059ED98 /* IdentifiableClass.swift in Sources */, - C1B4A9551D1E613A003B8985 /* TextFieldTableViewCell.swift in Sources */, - C1B4A9571D1E613A003B8985 /* TextFieldTableViewController.swift in Sources */, - 434AB0C61CBCB41500422F4A /* RileyLinkDevice.swift in Sources */, - C1A721601EC29C0B0080FAD7 /* PumpCommsError.swift in Sources */, + 436CCEF21FB953E800A6822B /* CommandSession.swift in Sources */, + C1A721601EC29C0B0080FAD7 /* PumpOpsError.swift in Sources */, 434AB0C71CBCB76400422F4A /* NSData.swift in Sources */, - 439731271CF21C3C00F474E5 /* RileyLinkDeviceTableViewCell.swift in Sources */, + 4384C8C61FB92F8100D916E6 /* HistoryPage.swift in Sources */, 434AB0981CBA0DF600422F4A /* PumpState.swift in Sources */, - C170C9991CECD80000F3D8E5 /* CommandResponseViewController.swift in Sources */, - 434AB0971CBA0DF600422F4A /* PumpOpsSynchronous.swift in Sources */, - C170C98E1CECD6F300F3D8E5 /* CBPeripheralState.swift in Sources */, - 2F962EC31E6873A10070EFBD /* PumpOpsCommunication.swift in Sources */, - C1C659191E16BA9D0025CC58 /* CaseCountable.swift in Sources */, - C170C99B1CECD80000F3D8E5 /* RileyLinkDeviceTableViewController.swift in Sources */, - 434AB0BE1CBB4E3200422F4A /* RileyLinkDeviceManager.swift in Sources */, + 431CE7961F9B0F0200255374 /* OSLog.swift in Sources */, + 437F54071FBD52120070FF2C /* DeviceState.swift in Sources */, + 435535DC1FB8B37E00CE5A23 /* PumpMessage.swift in Sources */, + 434AB0971CBA0DF600422F4A /* PumpOpsSession.swift in Sources */, + 2F962EC31E6873A10070EFBD /* PumpMessageSender.swift in Sources */, + 43BF58B21FF5A22200499C46 /* BasalProfile.swift in Sources */, 4345D1CE1DA16AF300BAAD22 /* TimeZone.swift in Sources */, - 434AB0951CBA0DF600422F4A /* Either.swift in Sources */, - C1B4A9591D1E6357003B8985 /* UITableViewCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2023,6 +2131,7 @@ buildActionMask = 2147483647; files = ( 2F962EBF1E678BAA0070EFBD /* PumpOpsSynchronousTests.swift in Sources */, + 4384C8C91FB941FB00D916E6 /* TimeInterval.swift in Sources */, 43A9E50E1F6B865000307931 /* NSData.swift in Sources */, 2F962EC51E705C6E0070EFBD /* PumpOpsSynchronousBuildFromFramesTests.swift in Sources */, ); @@ -2037,6 +2146,28 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 43D5E7891FAF7BFB004ACDB7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 43D5E79E1FAF7C47004ACDB7 /* TextFieldTableViewController.swift in Sources */, + 43BF58B31FF6079600499C46 /* TimeInterval.swift in Sources */, + 437F54091FBF9E0A0070FF2C /* RileyLinkDevice.swift in Sources */, + 435535DA1FB836CB00CE5A23 /* CommandResponseViewController+RileyLinkDevice.swift in Sources */, + 43D5E7A11FAF7CE0004ACDB7 /* UITableViewCell.swift in Sources */, + 43D5E7A31FAF7D05004ACDB7 /* CBPeripheralState.swift in Sources */, + 43D5E7A21FAF7CF2004ACDB7 /* CaseCountable.swift in Sources */, + 43D5E7A41FAF7D4D004ACDB7 /* TimeZone.swift in Sources */, + 43D5E79A1FAF7C47004ACDB7 /* RileyLinkDeviceTableViewCell.swift in Sources */, + 43D5E79F1FAF7C98004ACDB7 /* IdentifiableClass.swift in Sources */, + 43D5E79B1FAF7C47004ACDB7 /* CommandResponseViewController.swift in Sources */, + 435535D81FB7987000CE5A23 /* PumpOps.swift in Sources */, + 43D5E79C1FAF7C47004ACDB7 /* RileyLinkDeviceTableViewController.swift in Sources */, + 43D5E7A01FAF7CCA004ACDB7 /* NumberFormatter.swift in Sources */, + 43D5E79D1FAF7C47004ACDB7 /* TextFieldTableViewCell.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C10D9BBC1C8269D500378342 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2092,6 +2223,7 @@ C15AF2B11D7498DD0031FC9D /* RestoreMystery55PumpEvent.swift in Sources */, 54BC449C1DB483F700340EED /* RelativeTimestampedGlucoseEvent.swift in Sources */, C1330F431DBDA46400569064 /* ChangeSensorAlarmSilenceConfigPumpEvent.swift in Sources */, + C1F6EB871F89C3B100CFE393 /* CRC8.swift in Sources */, 546A85D81DF7C83A00733213 /* SensorDataHighGlucoseEvent.swift in Sources */, C1842C091C8FA45100DB42AC /* ChangeWatchdogEnablePumpEvent.swift in Sources */, C1842BC91C8F968B00DB42AC /* BGReceivedPumpEvent.swift in Sources */, @@ -2117,6 +2249,7 @@ 54BC447E1DB4753A00340EED /* GlucoseSensorDataGlucoseEvent.swift in Sources */, C1842BBD1C8E7C6E00DB42AC /* PumpEvent.swift in Sources */, 54DA4E851DFDC0A70007F489 /* SensorValueGlucoseEvent.swift in Sources */, + C1F6EB8B1F89C41200CFE393 /* MinimedPacket.swift in Sources */, C1842BCB1C8F9A7200DB42AC /* RewindPumpEvent.swift in Sources */, C1842BCD1C8F9BBD00DB42AC /* PrimePumpEvent.swift in Sources */, C1842C071C8FA45100DB42AC /* ClearAlarmPumpEvent.swift in Sources */, @@ -2127,11 +2260,12 @@ C1EAD6CE1C826B92006DBA60 /* ReadSettingsCarelinkMessageBody.swift in Sources */, C1F0004C1EBE68A600F65163 /* DataFrameMessageBody.swift in Sources */, C12198AD1C8F332500BC374C /* TimestampedPumpEvent.swift in Sources */, + C1F6EB891F89C3E200CFE393 /* FourByteSixByteEncoding.swift in Sources */, 54BC44751DB46B0A00340EED /* GlucosePage.swift in Sources */, - C1EAD6DE1C82B78C006DBA60 /* CRC8.swift in Sources */, C1EAD6C51C826B92006DBA60 /* Int.swift in Sources */, C1842C021C8FA45100DB42AC /* JournalEntryExerciseMarkerPumpEvent.swift in Sources */, 541688DF1DB82E72005B1891 /* TimestampedGlucoseEvent.swift in Sources */, + 43BF58B01FF594CB00499C46 /* SelectBasalProfileMessageBody.swift in Sources */, C1EAD6CB1C826B92006DBA60 /* MySentryAlertMessageBody.swift in Sources */, 54BC449A1DB47DFE00340EED /* NineteenSomethingGlucoseEvent.swift in Sources */, C15AF2AD1D74929D0031FC9D /* ChangeBolusWizardSetupPumpEvent.swift in Sources */, @@ -2153,6 +2287,7 @@ C1842BC31C8E931E00DB42AC /* BolusNormalPumpEvent.swift in Sources */, 541688DD1DB82213005B1891 /* ReadCurrentGlucosePageMessageBody.swift in Sources */, 43CA932F1CB8CFA1000026B5 /* ReadTimeCarelinkMessageBody.swift in Sources */, + 431CE7971F9B0F0200255374 /* OSLog.swift in Sources */, 2FDE1A071E57B12D00B56A27 /* ReadCurrentPageNumberMessageBody.swift in Sources */, 43EBE4521EAD23C40073A0B5 /* TimeInterval.swift in Sources */, 54BC44781DB46C7D00340EED /* GlucoseEvent.swift in Sources */, @@ -2174,7 +2309,6 @@ C1842C171C8FA45100DB42AC /* ChangeCaptureEventEnablePumpEvent.swift in Sources */, 54BC44B91DB81D6100340EED /* GetGlucosePageMessageBody.swift in Sources */, 43B0ADC41D12506A00AAD278 /* NSDateFormatter.swift in Sources */, - C1EAD6DA1C829104006DBA60 /* RFTools.swift in Sources */, 43CA932B1CB8CF76000026B5 /* ChangeTimeCarelinkMessageBody.swift in Sources */, 54B3F9021DFB984800B0ABFA /* DataEndGlucoseEvent.swift in Sources */, C121989F1C8DFC2200BC374C /* BolusCarelinkMessageBody.swift in Sources */, @@ -2216,6 +2350,7 @@ C12198A31C8DFC3600BC374C /* BolusCarelinkMessageBodyTests.swift in Sources */, C121985F1C8DE77D00BC374C /* FindDeviceMessageBodyTests.swift in Sources */, 541688DB1DB820BF005B1891 /* ReadCurrentGlucosePageMessageBodyTests.swift in Sources */, + C1F6EB8D1F89C45500CFE393 /* MinimedPacketTests.swift in Sources */, C1EAD6D71C826C43006DBA60 /* NSDataTests.swift in Sources */, 54BC44731DB46A5200340EED /* GlucosePageTests.swift in Sources */, 54BC44AF1DB70C3E00340EED /* TenSomethingGlucoseEventTests.swift in Sources */, @@ -2229,7 +2364,6 @@ 545AEFB21DF5D7DB00DF9433 /* SensorDataLowGlucoseEventTests.swift in Sources */, 54BC44AB1DB7093700340EED /* SensorTimestampGlucoseEventTests.swift in Sources */, 43CA93331CB9726A000026B5 /* ChangeTimeCarelinMessageBodyTests.swift in Sources */, - C1EAD6DC1C82A4AB006DBA60 /* RFToolsTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2237,42 +2371,36 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 435535D61FB6D98400CE5A23 /* UserDefaults.swift in Sources */, C14FFC5E1D3D75200049CF85 /* ButtonTableViewCell.swift in Sources */, 43EBE4551EAD24410073A0B5 /* TimeInterval.swift in Sources */, C14FFC691D3D7E560049CF85 /* KeychainManager+RileyLink.swift in Sources */, - C12616691B76E8DC001FAD87 /* TPKeyboardAvoidingTableView.m in Sources */, + 437F540A1FBFDAA60070FF2C /* NSData.swift in Sources */, C14FFC511D3D6DEF0049CF85 /* ServiceCredential.swift in Sources */, C14FFC5B1D3D74F90049CF85 /* NibLoadable.swift in Sources */, C14FFC611D3D75470049CF85 /* UIColor.swift in Sources */, C1274F811D82411C0002912B /* SettingsTableViewController.swift in Sources */, - C12616681B76E8DC001FAD87 /* TPKeyboardAvoidingScrollView.m in Sources */, - C139AC241BFD84B500B0518F /* RuntimeUtils.m in Sources */, + 43323EA71FA81A0F003FB0FA /* NumberFormatter.swift in Sources */, C14FFC671D3D7E390049CF85 /* KeychainManager.swift in Sources */, - C12616541B6892DB001FAD87 /* RileyLinkRecord.m in Sources */, C14FFC501D3D6DEF0049CF85 /* ServiceAuthentication.swift in Sources */, C1274F7F1D82411C0002912B /* AuthenticationViewController.swift in Sources */, - C12616671B76E8DC001FAD87 /* TPKeyboardAvoidingCollectionView.m in Sources */, - C126165A1B6B2D20001FAD87 /* PacketTableViewCell.m in Sources */, 43462E8B1CCB06F500F958A8 /* AppDelegate.swift in Sources */, - C174F26B19EB824D00398C72 /* ISO8601DateFormatter.m in Sources */, - C126166A1B76E8DC001FAD87 /* UIScrollView+TPKeyboardAvoidingAdditions.m in Sources */, C16843771CF00C0100D53CCD /* SwitchTableViewCell.swift in Sources */, C14FFC551D3D72A50049CF85 /* UIViewController.swift in Sources */, 434FF1DE1CF268F3000DB779 /* RileyLinkDeviceTableViewCell.swift in Sources */, C14FFC651D3D7E250049CF85 /* RemoteDataManager.swift in Sources */, C17884611D519F1E00405663 /* BatteryIndicator.swift in Sources */, + 437462391FA9287A00643383 /* RileyLinkDevice.swift in Sources */, C1274F801D82411C0002912B /* RileyLinkListTableViewController.swift in Sources */, - C1274F821D82411C0002912B /* TextFieldTableViewController.swift in Sources */, C1274F841D82420F0002912B /* RadioSelectionTableViewController.swift in Sources */, + 431CE7981F9B0F0200255374 /* OSLog.swift in Sources */, C1EF58881B3F93FE001C8C80 /* Config.m in Sources */, - C126164B1B685F93001FAD87 /* RileyLink.xcdatamodeld in Sources */, C14FFC581D3D72D30049CF85 /* AuthenticationTableViewCell.swift in Sources */, C14FFC631D3D7CE20049CF85 /* NightscoutService.swift in Sources */, C1B383361CD1BA8100CE7782 /* DeviceDataManager.swift in Sources */, C1271B071A9A34E900B7C949 /* Log.m in Sources */, C14FFC531D3D70170049CF85 /* ValidatingIndicatorView.swift in Sources */, 434FF1DC1CF268BD000DB779 /* IdentifiableClass.swift in Sources */, - C1E535EA1991E36700C2AC49 /* NSData+Conversion.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2291,7 +2419,6 @@ C1B383291CD0668600CE7782 /* NightscoutPumpEvents.swift in Sources */, 43B0ADCC1D126E3000AAD278 /* NSDateFormatter.swift in Sources */, C1AF21E81D4866960088C41D /* PumpStatus.swift in Sources */, - C1B4A94E1D1C423D003B8985 /* NSUserDefaults.swift in Sources */, C1AF21F01D4901220088C41D /* BolusNightscoutTreatment.swift in Sources */, C1AF21F21D4901220088C41D /* BGCheckNightscoutTreatment.swift in Sources */, C1D00EA11E8986F900B733B7 /* PumpResumeTreatment.swift in Sources */, @@ -2302,6 +2429,7 @@ C1A492691D4A66C0008964FF /* LoopEnacted.swift in Sources */, C1A492651D4A5DEB008964FF /* BatteryStatus.swift in Sources */, C1A492631D4A5A19008964FF /* IOBStatus.swift in Sources */, + 431CE7991F9B0F0200255374 /* OSLog.swift in Sources */, 43F348061D596270009933DC /* HKUnit.swift in Sources */, C133CF931D5943780034B82D /* PredictedBG.swift in Sources */, C1D00E9D1E8986A400B733B7 /* PumpSuspendTreatment.swift in Sources */, @@ -2331,15 +2459,20 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 430D64D71CB855AB00FCA750 /* PBXTargetDependency */ = { + 431CE77A1F98564200255374 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 431CE76E1F98564100255374 /* RileyLinkBLEKit */; + targetProxy = 431CE7791F98564200255374 /* PBXContainerItemProxy */; + }; + 431CE77C1F98564200255374 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 430D64CA1CB855AB00FCA750 /* RileyLinkBLEKit */; - targetProxy = 430D64D61CB855AB00FCA750 /* PBXContainerItemProxy */; + target = C12EA236198B436800309FA4 /* RileyLink */; + targetProxy = 431CE77B1F98564200255374 /* PBXContainerItemProxy */; }; - 430D64DF1CB855AB00FCA750 /* PBXTargetDependency */ = { + 431CE7831F98564200255374 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 430D64CA1CB855AB00FCA750 /* RileyLinkBLEKit */; - targetProxy = 430D64DE1CB855AB00FCA750 /* PBXContainerItemProxy */; + target = 431CE76E1F98564100255374 /* RileyLinkBLEKit */; + targetProxy = 431CE7821F98564200255374 /* PBXContainerItemProxy */; }; 43722FBA1CB9F7640038B7F2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -2361,6 +2494,11 @@ target = 43C246921D8918AE0031F8D1 /* Crypto */; targetProxy = 43C246A41D891DB80031F8D1 /* PBXContainerItemProxy */; }; + 43D5E7941FAF7BFB004ACDB7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 43D5E78D1FAF7BFB004ACDB7 /* RileyLinkKitUI */; + targetProxy = 43D5E7931FAF7BFB004ACDB7 /* PBXContainerItemProxy */; + }; C10D9BCD1C8269D500378342 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C10D9BC01C8269D500378342 /* MinimedKit */; @@ -2394,10 +2532,93 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 7D70766F1FE092D4004AC8EA /* LoopKit.strings */ = { + isa = PBXVariantGroup; + children = ( + 7D70766E1FE092D4004AC8EA /* es */, + 7D68AACD1FE31DEA00522C49 /* ru */, + ); + name = LoopKit.strings; + sourceTree = ""; + }; + 7D7076741FE092D5004AC8EA /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 7D7076731FE092D5004AC8EA /* es */, + 7D68AAD51FE31DEC00522C49 /* ru */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 7D7076791FE092D6004AC8EA /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 7D7076781FE092D6004AC8EA /* es */, + 7D68AACF1FE31DEB00522C49 /* ru */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 7D70767E1FE092D6004AC8EA /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 7D70767D1FE092D6004AC8EA /* es */, + 7D68AAD01FE31DEB00522C49 /* ru */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 7D7076831FE092D7004AC8EA /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 7D7076821FE092D7004AC8EA /* es */, + 7D68AAD11FE31DEB00522C49 /* ru */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 7D7076881FE092D7004AC8EA /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 7D7076871FE092D7004AC8EA /* es */, + 7D68AAD21FE31DEC00522C49 /* ru */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 7D70768D1FE09310004AC8EA /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 7D70768C1FE09310004AC8EA /* es */, + 7D68AAD31FE31DEC00522C49 /* ru */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 7D7076921FE09311004AC8EA /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 7D7076911FE09311004AC8EA /* es */, + 7D68AAD41FE31DEC00522C49 /* ru */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 7D7076971FE09311004AC8EA /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 7D7076961FE09311004AC8EA /* es */, + 7D68AACE1FE31DEB00522C49 /* ru */, + ); + name = Localizable.strings; + sourceTree = ""; + }; C12EA243198B436800309FA4 /* InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( C12EA244198B436800309FA4 /* en */, + 7D4F0A611F8F226F00A55FB2 /* es */, + 7D68AACB1FE31CE500522C49 /* ru */, ); name = InfoPlist.strings; sourceTree = ""; @@ -2406,6 +2627,8 @@ isa = PBXVariantGroup; children = ( C12EA25D198B436900309FA4 /* en */, + 7D4F0A621F8F226F00A55FB2 /* es */, + 7D68AACC1FE31CE500522C49 /* ru */, ); name = InfoPlist.strings; sourceTree = ""; @@ -2413,109 +2636,140 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 430D64E21CB855AB00FCA750 /* Debug */ = { + 431CE7861F98564200255374 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - CURRENT_PROJECT_VERSION = 35; + CODE_SIGN_STYLE = Manual; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 35; + DYLIB_CURRENT_VERSION = 36; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_NO_COMMON_BLOCKS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RileyLinkBLEKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = com.rileylink.RileyLinkBLEKit; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - VERSIONING_SYSTEM = "apple-generic"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos watchos watchsimulator"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2,4"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; - 430D64E31CB855AB00FCA750 /* Release */ = { + 431CE7871F98564200255374 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 35; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 35; + DYLIB_CURRENT_VERSION = 36; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_NO_COMMON_BLOCKS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RileyLinkBLEKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.rileylink.RileyLinkBLEKit; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - VERSIONING_SYSTEM = "apple-generic"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos watchos watchsimulator"; + TARGETED_DEVICE_FAMILY = "1,2,4"; VERSION_INFO_PREFIX = ""; }; name = Release; }; - 430D64E41CB855AB00FCA750 /* Debug */ = { + 431CE7881F98564200255374 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ANALYZER_NONNULL = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_NO_COMMON_BLOCKS = YES; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RileyLinkBLEKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = com.rileylink.RileyLinkBLEKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RileyLink.app/RileyLink"; }; name = Debug; }; - 430D64E51CB855AB00FCA750 /* Release */ = { + 431CE7891F98564200255374 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ANALYZER_NONNULL = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_NO_COMMON_BLOCKS = YES; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RileyLinkBLEKitTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.rileylink.RileyLinkBLEKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RileyLink.app/RileyLink"; }; name = Release; }; 43722FC51CB9F7640038B7F2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 35; + DYLIB_CURRENT_VERSION = 36; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -2535,17 +2789,18 @@ 43722FC61CB9F7640038B7F2 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 35; + DYLIB_CURRENT_VERSION = 36; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -2568,6 +2823,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ""; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = RileyLinkKitTests/Info.plist; @@ -2587,6 +2843,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = RileyLinkKitTests/Info.plist; @@ -2600,21 +2857,21 @@ 43C2469C1D8918AE0031F8D1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 35; + DYLIB_CURRENT_VERSION = 36; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Crypto/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = com.rileylink.Crypto; @@ -2627,22 +2884,22 @@ 43C2469D1D8918AE0031F8D1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 35; + DYLIB_CURRENT_VERSION = 36; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Crypto/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.rileylink.Crypto; @@ -2652,18 +2909,89 @@ }; name = Release; }; + 43D5E7981FAF7BFB004ACDB7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 36; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 36; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RileyLinkKitUI/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.rileylink.RileyLinkKitUI; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 43D5E7991FAF7BFB004ACDB7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 36; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 36; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RileyLinkKitUI/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.rileylink.RileyLinkKitUI; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; C10D9BD91C8269D600378342 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 35; + DYLIB_CURRENT_VERSION = 36; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -2683,16 +3011,17 @@ C10D9BDA1C8269D600378342 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 35; + DYLIB_CURRENT_VERSION = 36; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -2715,6 +3044,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ""; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = MinimedKitTests/Info.plist; @@ -2734,6 +3064,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = MinimedKitTests/Info.plist; @@ -2748,6 +3079,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -2756,12 +3088,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -2770,7 +3104,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -2788,13 +3122,15 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.2; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; WARNING_CFLAGS = "-Wall"; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Debug; }; @@ -2802,6 +3138,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -2810,12 +3147,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -2824,7 +3163,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -2835,14 +3174,16 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.2; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; WARNING_CFLAGS = "-Wall"; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Release; }; @@ -2928,16 +3269,17 @@ C1B383231CD0665D00CE7782 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 35; + DYLIB_CURRENT_VERSION = 36; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -2956,17 +3298,18 @@ C1B383241CD0665D00CE7782 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 35; + DYLIB_CURRENT_VERSION = 36; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -2988,6 +3331,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ""; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = NightscoutUploadKitTests/Info.plist; @@ -3007,6 +3351,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = NightscoutUploadKitTests/Info.plist; @@ -3020,20 +3365,20 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 430D64E61CB855AB00FCA750 /* Build configuration list for PBXNativeTarget "RileyLinkBLEKit" */ = { + 431CE78A1F98564200255374 /* Build configuration list for PBXNativeTarget "RileyLinkBLEKit" */ = { isa = XCConfigurationList; buildConfigurations = ( - 430D64E21CB855AB00FCA750 /* Debug */, - 430D64E31CB855AB00FCA750 /* Release */, + 431CE7861F98564200255374 /* Debug */, + 431CE7871F98564200255374 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 430D64E71CB855AB00FCA750 /* Build configuration list for PBXNativeTarget "RileyLinkBLEKitTests" */ = { + 431CE78B1F98564200255374 /* Build configuration list for PBXNativeTarget "RileyLinkBLEKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 430D64E41CB855AB00FCA750 /* Debug */, - 430D64E51CB855AB00FCA750 /* Release */, + 431CE7881F98564200255374 /* Debug */, + 431CE7891F98564200255374 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -3065,6 +3410,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 43D5E7971FAF7BFB004ACDB7 /* Build configuration list for PBXNativeTarget "RileyLinkKitUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 43D5E7981FAF7BFB004ACDB7 /* Debug */, + 43D5E7991FAF7BFB004ACDB7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; C10D9BD81C8269D600378342 /* Build configuration list for PBXNativeTarget "MinimedKit" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/RileyLink.xcodeproj/xcshareddata/xcschemes/Crypto.xcscheme b/RileyLink.xcodeproj/xcshareddata/xcschemes/Crypto.xcscheme index 54e731243..c2a4ee56c 100644 --- a/RileyLink.xcodeproj/xcshareddata/xcschemes/Crypto.xcscheme +++ b/RileyLink.xcodeproj/xcshareddata/xcschemes/Crypto.xcscheme @@ -1,6 +1,6 @@ @@ -37,7 +36,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/RileyLink.xcodeproj/xcshareddata/xcschemes/MinimedKit.xcscheme b/RileyLink.xcodeproj/xcshareddata/xcschemes/MinimedKit.xcscheme index 8ca49000e..bc10afe45 100644 --- a/RileyLink.xcodeproj/xcshareddata/xcschemes/MinimedKit.xcscheme +++ b/RileyLink.xcodeproj/xcshareddata/xcschemes/MinimedKit.xcscheme @@ -1,6 +1,6 @@ + codeCoverageEnabled = "YES" + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -68,9 +67,9 @@ skipped = "NO"> @@ -78,9 +77,9 @@ skipped = "NO"> @@ -88,9 +87,9 @@ skipped = "NO"> @@ -111,7 +110,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/RileyLink.xcodeproj/xcshareddata/xcschemes/RileyLinkBLEKit.xcscheme b/RileyLink.xcodeproj/xcshareddata/xcschemes/RileyLinkBLEKit.xcscheme index 75b9ee9a8..4c270b428 100644 --- a/RileyLink.xcodeproj/xcshareddata/xcschemes/RileyLinkBLEKit.xcscheme +++ b/RileyLink.xcodeproj/xcshareddata/xcschemes/RileyLinkBLEKit.xcscheme @@ -1,6 +1,6 @@ @@ -26,14 +26,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" shouldUseLaunchSchemeArgsEnv = "YES"> @@ -43,7 +42,7 @@ @@ -56,7 +55,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" @@ -66,7 +64,7 @@ @@ -84,7 +82,7 @@ diff --git a/RileyLink.xcodeproj/xcshareddata/xcschemes/RileyLinkKit.xcscheme b/RileyLink.xcodeproj/xcshareddata/xcschemes/RileyLinkKit.xcscheme index 599f2f333..3c12aec8e 100644 --- a/RileyLink.xcodeproj/xcshareddata/xcschemes/RileyLinkKit.xcscheme +++ b/RileyLink.xcodeproj/xcshareddata/xcschemes/RileyLinkKit.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RileyLink/AppDelegate.swift b/RileyLink/AppDelegate.swift index d6373416d..10985f27c 100644 --- a/RileyLink/AppDelegate.swift +++ b/RileyLink/AppDelegate.swift @@ -60,48 +60,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { NSLog(#function) } - // MARK: - Notifications - - func application(_ application: UIApplication, didReceive notification: UILocalNotification) { - - } - - func application(_ application: UIApplication, handleActionWithIdentifier identifier: String?, for notification: UILocalNotification, withResponseInfo responseInfo: [AnyHashable: Any], completionHandler: @escaping () -> Void) { - - completionHandler() - } - // MARK: - 3D Touch func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { completionHandler(false) } - - // MARK: - Core Data - - lazy var managedObjectContext: NSManagedObjectContext? = { - guard let managedObjectModel = { () -> NSManagedObjectModel? in - let modelURL = Bundle.main.url(forResource: "RileyLink", withExtension: "momd")! - return NSManagedObjectModel(contentsOf: modelURL) - }() else { - return nil - } - - guard let coordinator = { () -> NSPersistentStoreCoordinator? in - let storeURL = applicationDocumentsDirectory().appendingPathComponent("RileyLink.sqlite") - let coordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel) - - try! coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: nil) - - return coordinator - }() else { - return nil - } - - let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) - context.persistentStoreCoordinator = coordinator - return context - }() } diff --git a/RileyLink/Config.h b/RileyLink/Config.h index 7190e02e9..341888c2e 100644 --- a/RileyLink/Config.h +++ b/RileyLink/Config.h @@ -16,11 +16,7 @@ @property (nonatomic, nullable, strong) NSURL *nightscoutURL; @property (nonatomic, nullable, strong) NSString *nightscoutAPISecret; -@property (nonatomic, nullable, strong) NSString *pumpID; -@property (nonatomic, nullable, strong) NSString *pumpModelNumber; -@property (nonatomic, nullable, strong) NSTimeZone *pumpTimeZone; @property (nonatomic, nullable, strong) NSSet *autoConnectIds; -@property (nonatomic, assign) NSInteger pumpRegion; @property (nonatomic, assign) BOOL uploadEnabled; @property (nonatomic, assign) BOOL fetchCGMEnabled; diff --git a/RileyLink/Config.m b/RileyLink/Config.m index 97d19d8cb..54673374a 100644 --- a/RileyLink/Config.m +++ b/RileyLink/Config.m @@ -8,8 +8,6 @@ @import CoreData; #import "Config.h" -#import "RileyLinkRecord.h" -#import "RileyLink-Swift.h" #import @implementation Config @@ -56,42 +54,6 @@ - (NSString*) nightscoutAPISecret { return [_defaults stringForKey:@"nightscoutAPISecret"]; } -- (void) setPumpID:(NSString *)pumpID { - [_defaults setValue:pumpID forKey:@"pumpID"]; -} - -- (NSString*) pumpID { - return [_defaults stringForKey:@"pumpID"]; -} - -- (void) setPumpModelNumber:(NSString *)pumpModelNumber { - [_defaults setValue:pumpModelNumber forKey:@"pumpModelNumber"]; -} - -- (NSString*) pumpModelNumber { - return [_defaults stringForKey:@"pumpModelNumber"]; -} - - -- (void) setPumpTimeZone:(NSTimeZone *)pumpTimeZone { - - if (pumpTimeZone) { - NSNumber *rawValue = [NSNumber numberWithInteger:pumpTimeZone.secondsFromGMT]; - [_defaults setObject:rawValue forKey:@"pumpTimeZone"]; - } else { - [_defaults removeObjectForKey:@"pumpTimeZone"]; - } -} - -- (NSTimeZone*) pumpTimeZone { - NSNumber *offset = (NSNumber*)[_defaults objectForKey:@"pumpTimeZone"]; - if (offset) { - return [NSTimeZone timeZoneForSecondsFromGMT: offset.integerValue]; - } else { - return nil; - } -} - - (NSSet*) autoConnectIds { NSSet *set = [[NSUserDefaults standardUserDefaults] objectForKey:@"autoConnectIds"]; if (!set) { @@ -120,13 +82,5 @@ - (void) setFetchCGMEnabled:(BOOL)fetchCGMEnabled { [[NSUserDefaults standardUserDefaults] setBool:fetchCGMEnabled forKey:@"fetchCGMEnabled"]; } -- (NSInteger) pumpRegion { - return [[NSUserDefaults standardUserDefaults] integerForKey:@"pumpRegion"]; -} - -- (void) setPumpRegion:(NSInteger)pumpRegion { - [[NSUserDefaults standardUserDefaults] setInteger:pumpRegion forKey:@"pumpRegion"]; -} - @end diff --git a/RileyLink/DeviceDataManager.swift b/RileyLink/DeviceDataManager.swift index a9223fc6e..b3ac36bf3 100644 --- a/RileyLink/DeviceDataManager.swift +++ b/RileyLink/DeviceDataManager.swift @@ -8,150 +8,104 @@ import Foundation import RileyLinkKit +import RileyLinkKitUI import RileyLinkBLEKit import MinimedKit import NightscoutUploadKit class DeviceDataManager { - var getHistoryTimer: Timer? - let rileyLinkManager: RileyLinkDeviceManager - /// Manages remote data (TODO: the lazy initialization isn't thread-safe) - lazy var remoteDataManager = RemoteDataManager() + /// Manages remote data + let remoteDataManager = RemoteDataManager() - var connectedPeripheralIDs: Set = Config.sharedInstance().autoConnectIds as! Set { + private var connectedPeripheralIDs: Set = Config.sharedInstance().autoConnectIds as! Set { didSet { Config.sharedInstance().autoConnectIds = connectedPeripheralIDs } } - - var latestPumpStatusDate: Date? - - var latestPumpStatusFromMySentry: MySentryPumpStatusMessageBody? { - didSet { - if let update = latestPumpStatusFromMySentry, let timeZone = pumpState?.timeZone { - var pumpClock = update.pumpDateComponents - pumpClock.timeZone = timeZone - latestPumpStatusDate = pumpClock.date - } - } - } - - - var latestPolledPumpStatus: RileyLinkKit.PumpStatus? { + + var deviceStates: [UUID: DeviceState] = [:] + + private(set) var pumpOps: PumpOps? { didSet { - if let update = latestPolledPumpStatus, let timeZone = pumpState?.timeZone { - var pumpClock = update.clock - pumpClock.timeZone = timeZone - latestPumpStatusDate = pumpClock.date + if pumpOps == nil { + UserDefaults.standard.pumpState = nil } } } - - var pumpID: String? { + + private(set) var pumpSettings: PumpSettings? { get { - return pumpState?.pumpID + return UserDefaults.standard.pumpSettings } set { - guard newValue?.characters.count == 6 && newValue != pumpState?.pumpID else { - return - } - - if let pumpID = newValue { - let pumpState = PumpState(pumpID: pumpID, pumpRegion: pumpRegion) - - if let timeZone = self.pumpState?.timeZone { - pumpState.timeZone = timeZone + if let settings = newValue { + if let pumpOps = pumpOps { + pumpOps.updateSettings(settings) + } else { + pumpOps = PumpOps(pumpSettings: settings, pumpState: nil, delegate: self) } - - self.pumpState = pumpState } else { - self.pumpState = nil + pumpOps = nil } - - remoteDataManager.nightscoutUploader?.reset() - - Config.sharedInstance().pumpID = pumpID + + UserDefaults.standard.pumpSettings = newValue } } - - var pumpRegion: PumpRegion { - get { - return PumpRegion(rawValue: Config.sharedInstance().pumpRegion) ?? .northAmerica + + func setPumpID(_ pumpID: String?) { + var newValue = pumpID + + if newValue?.count != 6 { + newValue = nil } - set { - self.pumpState?.pumpRegion = newValue - Config.sharedInstance().pumpRegion = newValue.rawValue + + if let newValue = newValue { + if pumpSettings != nil { + pumpSettings?.pumpID = newValue + } else { + pumpSettings = PumpSettings(pumpID: newValue) + } } } + func setPumpRegion(_ pumpRegion: PumpRegion) { + pumpSettings?.pumpRegion = pumpRegion + } var pumpState: PumpState? { + return UserDefaults.standard.pumpState + } + + // MARK: - Operation helpers + + var latestPumpStatusDate: Date? + + var latestPumpStatusFromMySentry: MySentryPumpStatusMessageBody? { didSet { - rileyLinkManager.pumpState = pumpState - - if let oldValue = oldValue { - NotificationCenter.default.removeObserver(self, name: .PumpStateValuesDidChange, object: oldValue) - } - - if let pumpState = pumpState { - NotificationCenter.default.addObserver(self, selector: #selector(pumpStateValuesDidChange(_:)), name: .PumpStateValuesDidChange, object: pumpState) + if let update = latestPumpStatusFromMySentry, let timeZone = pumpState?.timeZone { + var pumpClock = update.pumpDateComponents + pumpClock.timeZone = timeZone + latestPumpStatusDate = pumpClock.date } } } - - @objc private func pumpStateValuesDidChange(_ note: Notification) { - switch note.userInfo?[PumpState.PropertyKey] as? String { - case "timeZone"?: - Config.sharedInstance().pumpTimeZone = pumpState?.timeZone - case "pumpModel"?: - if let sentrySupported = pumpState?.pumpModel?.hasMySentry { - rileyLinkManager.idleListeningEnabled = sentrySupported + + + var latestPolledPumpStatus: RileyLinkKit.PumpStatus? { + didSet { + if let update = latestPolledPumpStatus { + latestPumpStatusDate = update.clock.date } - Config.sharedInstance().pumpModelNumber = pumpState?.pumpModel?.rawValue - case "lastHistoryDump"?, "awakeUntil"?: - break - default: - break } } - + var lastHistoryAttempt: Date? = nil var lastGlucoseEntry: Date = Date(timeIntervalSinceNow: TimeInterval(hours: -24)) - - var lastRileyLinkHeardFrom: RileyLinkDevice? = nil - - - var rileyLinkManagerObserver: Any? { - willSet { - if let observer = rileyLinkManagerObserver { - NotificationCenter.default.removeObserver(observer) - } - } - } - - var rileyLinkDevicePacketObserver: Any? { - willSet { - if let observer = rileyLinkDevicePacketObserver { - NotificationCenter.default.removeObserver(observer) - } - } - } - - @objc private func receivedRileyLinkManagerNotification(_ note: Notification) { - NotificationCenter.default.post(name: note.name, object: self, userInfo: note.userInfo) - } - - func preferredRileyLink() -> RileyLinkDevice? { - if let device = lastRileyLinkHeardFrom { - return device - } - return self.rileyLinkManager.firstConnectedDevice - } - + /** Called when a new idle message is received by the RileyLink. @@ -160,42 +114,61 @@ class DeviceDataManager { - parameter note: The notification object */ @objc private func receivedRileyLinkPacketNotification(_ note: Notification) { - if let - device = note.object as? RileyLinkDevice, - let data = note.userInfo?[RileyLinkDevice.IdleMessageDataKey] as? Data, - let message = PumpMessage(rxData: data) - { - switch message.packetType { - case .mySentry: - switch message.messageBody { - case let body as MySentryPumpStatusMessageBody: - pumpStatusUpdateReceived(body, fromDevice: device) - default: - break - } + guard + let device = note.object as? RileyLinkDevice, + let packet = note.userInfo?[RileyLinkDevice.notificationPacketKey] as? RFPacket, + let decoded = MinimedPacket(encodedData: packet.data), + let message = PumpMessage(rxData: decoded.data), + let address = pumpSettings?.pumpID, + message.address.hexadecimalString == address + else { + return + } + + switch message.packetType { + case .mySentry: + switch message.messageBody { + case let body as MySentryPumpStatusMessageBody: + pumpStatusUpdateReceived(body, fromDevice: device) default: break } + default: + break } } @objc private func receivedRileyLinkTimerTickNotification(_ note: Notification) { if Config.sharedInstance().uploadEnabled { - self.assertCurrentPumpData() + rileyLinkManager.getDevices { (devices) in + if let device = devices.firstConnected { + self.assertCurrentPumpData(from: device) + } + } } } + @objc private func deviceStateDidChange(_ note: Notification) { + guard + let device = note.object as? RileyLinkDevice, + let deviceState = note.userInfo?[RileyLinkDevice.notificationDeviceStateKey] as? DeviceState + else { + return + } + + deviceStates[device.peripheralIdentifier] = deviceState + } func connectToRileyLink(_ device: RileyLinkDevice) { - connectedPeripheralIDs.insert(device.peripheral.identifier.uuidString) + connectedPeripheralIDs.insert(device.peripheralIdentifier.uuidString) - rileyLinkManager.connectDevice(device) + rileyLinkManager.connect(device) } func disconnectFromRileyLink(_ device: RileyLinkDevice) { - connectedPeripheralIDs.remove(device.peripheral.identifier.uuidString) + connectedPeripheralIDs.remove(device.peripheralIdentifier.uuidString) - rileyLinkManager.disconnectDevice(device) + rileyLinkManager.disconnect(device) } private func pumpStatusUpdateReceived(_ status: MySentryPumpStatusMessageBody, fromDevice device: RileyLinkDevice) { @@ -220,7 +193,7 @@ class DeviceDataManager { //NotificationManager.sendPumpBatteryLowNotification() } - guard Config.sharedInstance().uploadEnabled, let pumpID = pumpID else { + guard Config.sharedInstance().uploadEnabled, let pumpID = pumpSettings?.pumpID else { return } @@ -264,11 +237,7 @@ class DeviceDataManager { /** Ensures pump data is current by either waking and polling, or ensuring we're listening to sentry packets. */ - private func assertCurrentPumpData() { - guard let device = rileyLinkManager.firstConnectedDevice else { - return - } - + private func assertCurrentPumpData(from device: RileyLinkDevice) { device.assertIdleListening() // How long should we wait before we poll for new pump data? @@ -276,38 +245,36 @@ class DeviceDataManager { // If we don't yet have pump status, or it's old, poll for it. if latestPumpStatusDate == nil || latestPumpStatusDate!.timeIntervalSinceNow <= -pumpStatusAgeTolerance { - guard let device = rileyLinkManager.firstConnectedDevice else { - return - } - guard let ops = device.ops else { + guard let pumpOps = pumpOps else { self.troubleshootPumpCommsWithDevice(device) return } - - ops.readPumpStatus({ (result) in - switch result { - case .success(let status): - self.latestPolledPumpStatus = status - let battery = BatteryStatus(voltage: status.batteryVolts, status: BatteryIndicator(batteryStatus: status.batteryStatus)) - var clock = status.clock - clock.timeZone = ops.pumpState.timeZone - guard let date = clock.date else { - print("Could not interpret clock") - return + + pumpOps.runSession(withName: "Read pump status", using: device) { (session) in + do { + let status = try session.getCurrentPumpStatus() + DispatchQueue.main.async { + self.latestPolledPumpStatus = status + let battery = BatteryStatus(voltage: status.batteryVolts, status: BatteryIndicator(batteryStatus: status.batteryStatus)) + guard let date = status.clock.date else { + print("Could not interpret clock") + return + } + let nsPumpStatus = NightscoutUploadKit.PumpStatus(clock: date, pumpID: status.pumpID, iob: nil, battery: battery, suspended: status.suspended, bolusing: status.bolusing, reservoir: status.reservoir) + self.uploadDeviceStatus(nsPumpStatus) + } + } catch { + DispatchQueue.main.async { + self.troubleshootPumpCommsWithDevice(device) } - let nsPumpStatus = NightscoutUploadKit.PumpStatus(clock: date, pumpID: ops.pumpState.pumpID, iob: nil, battery: battery, suspended: status.suspended, bolusing: status.bolusing, reservoir: status.reservoir) - self.uploadDeviceStatus(nsPumpStatus) - case .failure: - self.troubleshootPumpCommsWithDevice(device) } - }) + } } if lastHistoryAttempt == nil || lastHistoryAttempt!.timeIntervalSinceNow < TimeInterval(minutes: -5) { getPumpHistory(device) } - } /** @@ -319,13 +286,24 @@ class DeviceDataManager { // How long we should wait before we re-tune the RileyLink let tuneTolerance = TimeInterval(minutes: 14) - - if device.lastTuned == nil || device.lastTuned!.timeIntervalSinceNow <= -tuneTolerance { - device.tunePump { (result) in - switch result { - case .success(let scanResult): - print("Device auto-tuned to \(scanResult.bestFrequency) MHz") - case .failure(let error): + + guard let pumpOps = pumpOps else { + return + } + + let deviceState = deviceStates[device.peripheralIdentifier, default: DeviceState()] + let lastTuned = deviceState.lastTuned ?? .distantPast + + if lastTuned.timeIntervalSinceNow <= -tuneTolerance { + pumpOps.runSession(withName: "Tune pump", using: device) { (session) in + do { + let scanResult = try session.tuneRadio(current: deviceState.lastValidFrequency) + print("Device auto-tuned to \(scanResult.bestFrequency)") + + DispatchQueue.main.async { + self.deviceStates[device.peripheralIdentifier] = DeviceState(lastTuned: Date(), lastValidFrequency: scanResult.bestFrequency) + } + } catch let error { print("Device auto-tune failed with error: \(error)") } } @@ -334,27 +312,25 @@ class DeviceDataManager { private func getPumpHistory(_ device: RileyLinkDevice) { lastHistoryAttempt = Date() - - guard let ops = device.ops else { + + guard let pumpOps = pumpOps else { print("Missing pumpOps; is your pumpId configured?") return } - - + let oneDayAgo = Date(timeIntervalSinceNow: TimeInterval(hours: -24)) - let observingPumpEventsSince = remoteDataManager.nightscoutUploader?.observingPumpEventsSince ?? oneDayAgo - - - ops.getHistoryEvents(since: observingPumpEventsSince) { (response) -> Void in - switch response { - case .success(let (events, pumpModel)): + + pumpOps.runSession(withName: "Get pump history", using: device) { (session) in + do { + let (events, pumpModel) = try session.getHistoryEvents(since: oneDayAgo) NSLog("fetchHistory succeeded.") - self.handleNewHistoryEvents(events, pumpModel: pumpModel, device: device) - NotificationCenter.default.post(name: .PumpEventsUpdated, object: self) - case .failure(let error): + DispatchQueue.main.async { + self.handleNewHistoryEvents(events, pumpModel: pumpModel, device: device) + } + } catch let error { NSLog("History fetch failed: %@", String(describing: error)) } - + if Config.sharedInstance().fetchCGMEnabled, self.lastGlucoseEntry.timeIntervalSinceNow < TimeInterval(minutes: -5) { self.getPumpGlucoseHistory(device) } @@ -369,21 +345,19 @@ class DeviceDataManager { } private func getPumpGlucoseHistory(_ device: RileyLinkDevice) { - - guard let ops = device.ops else { + guard let pumpOps = pumpOps else { print("Missing pumpOps; is your pumpId configured?") return } - - ops.getGlucoseHistoryEvents(since: lastGlucoseEntry) { (response) -> Void in - switch response { - case .success(let events): + + pumpOps.runSession(withName: "Get glucose history", using: device) { (session) in + do { + let events = try session.getGlucoseHistoryEvents(since: self.lastGlucoseEntry) NSLog("fetchGlucoseHistory succeeded.") if let latestEntryDate: Date = self.handleNewGlucoseHistoryEvents(events, device: device) { self.lastGlucoseEntry = latestEntryDate } - - case .failure(let error): + } catch let error { NSLog("Glucose History fetch failed: %@", String(describing: error)) } } @@ -401,66 +375,40 @@ class DeviceDataManager { static let sharedManager = DeviceDataManager() init() { - - let pumpID = Config.sharedInstance().pumpID + rileyLinkManager = RileyLinkDeviceManager(autoConnectIDs: connectedPeripheralIDs) var idleListeningEnabled = true - let pumpRegion = PumpRegion(rawValue: Config.sharedInstance().pumpRegion) ?? .northAmerica - - if let pumpID = pumpID { - let pumpState = PumpState(pumpID: pumpID, pumpRegion: pumpRegion) - - if let timeZone = Config.sharedInstance().pumpTimeZone { - pumpState.timeZone = timeZone - } - - if let pumpModelNumber = Config.sharedInstance().pumpModelNumber { - if let model = PumpModel(rawValue: pumpModelNumber) { - pumpState.pumpModel = model - - idleListeningEnabled = model.hasMySentry - } - } - - self.pumpState = pumpState + if let pumpSettings = UserDefaults.standard.pumpSettings { + idleListeningEnabled = self.pumpState?.pumpModel?.hasMySentry ?? true + + self.pumpOps = PumpOps(pumpSettings: pumpSettings, pumpState: self.pumpState, delegate: self) } - rileyLinkManager = RileyLinkDeviceManager( - pumpState: self.pumpState, - autoConnectIDs: connectedPeripheralIDs - ) - rileyLinkManager.idleListeningEnabled = idleListeningEnabled + rileyLinkManager.idleListeningState = idleListeningEnabled ? .enabledWithDefaults : .disabled UIDevice.current.isBatteryMonitoringEnabled = true - - NotificationCenter.default.addObserver(self, selector: #selector(receivedRileyLinkManagerNotification(_:)), name: nil, object: rileyLinkManager) - NotificationCenter.default.addObserver(self, selector: #selector(receivedRileyLinkPacketNotification(_:)), name: .RileyLinkDeviceDidReceiveIdleMessage, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(receivedRileyLinkTimerTickNotification(_:)), name: .RileyLinkDeviceDidUpdateTimerTick, object: nil) - - if let pumpState = pumpState { - NotificationCenter.default.addObserver(self, selector: #selector(pumpStateValuesDidChange(_:)), name: .PumpStateValuesDidChange, object: pumpState) - } + // Device observers + NotificationCenter.default.addObserver(self, selector: #selector(receivedRileyLinkPacketNotification(_:)), name: .DevicePacketReceived, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(receivedRileyLinkTimerTickNotification(_:)), name: .DeviceTimerDidTick, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(deviceStateDidChange(_:)), name: .DeviceStateDidChange, object: nil) } - - deinit { - rileyLinkManagerObserver = nil - rileyLinkDevicePacketObserver = nil - } - - // MARK: - Device updates - func rileyLinkAdded(_ note: Notification) - { - if let device = note.object as? RileyLinkBLEDevice { - device.enableIdleListening(onChannel: 0) - } - } - } -extension Notification.Name { - static let PumpEventsUpdated = Notification.Name(rawValue: "com.rileylink.notification.PumpEventsUpdated") -} +extension DeviceDataManager: PumpOpsDelegate { + func pumpOps(_ pumpOps: PumpOps, didChange state: PumpState) { + if let sentrySupported = state.pumpModel?.hasMySentry { + rileyLinkManager.idleListeningState = sentrySupported ? .enabledWithDefaults : .disabled + } + UserDefaults.standard.pumpState = state + + NotificationCenter.default.post( + name: .PumpOpsStateDidChange, + object: pumpOps, + userInfo: [PumpOps.notificationPumpStateKey: state] + ) + } +} diff --git a/RileyLink/Extensions/RileyLinkDevice.swift b/RileyLink/Extensions/RileyLinkDevice.swift new file mode 100644 index 000000000..fae80993f --- /dev/null +++ b/RileyLink/Extensions/RileyLinkDevice.swift @@ -0,0 +1,15 @@ +// +// RileyLinkDevice.swift +// RileyLink +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import RileyLinkBLEKit + + +extension RileyLinkDevice.IdleListeningState { + static var enabledWithDefaults: RileyLinkDevice.IdleListeningState { + return .enabled(timeout: .minutes(1), channel: 0) + } +} diff --git a/RileyLink/Extensions/UserDefaults.swift b/RileyLink/Extensions/UserDefaults.swift new file mode 100644 index 000000000..c1b07c565 --- /dev/null +++ b/RileyLink/Extensions/UserDefaults.swift @@ -0,0 +1,44 @@ +// +// UserDefaults.swift +// RileyLink +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import Foundation +import RileyLinkKit + + +extension UserDefaults { + private enum Key: String { + case pumpSettings = "com.rileylink.pumpSettings" + case pumpState = "com.rileylink.pumpState" + } + + var pumpSettings: PumpSettings? { + get { + guard let raw = dictionary(forKey: Key.pumpSettings.rawValue) else { + return nil + } + + return PumpSettings(rawValue: raw) + } + set { + set(newValue?.rawValue + , forKey: Key.pumpSettings.rawValue) + } + } + + var pumpState: PumpState? { + get { + guard let raw = dictionary(forKey: Key.pumpState.rawValue) else { + return nil + } + + return PumpState(rawValue: raw) + } + set { + set(newValue?.rawValue, forKey: Key.pumpState.rawValue) + } + } +} diff --git a/RileyLink/ISO8601DateFormatter.h b/RileyLink/ISO8601DateFormatter.h deleted file mode 100644 index b2ba1331a..000000000 --- a/RileyLink/ISO8601DateFormatter.h +++ /dev/null @@ -1,230 +0,0 @@ -/*ISO8601DateFormatter.h - * - *Created by Peter Hosey on 2009-04-11. - *Copyright 2009–2013 Peter Hosey. All rights reserved. - */ - -#import - -///Which of ISO 8601's three date formats the formatter should produce. -typedef NS_ENUM(NSUInteger, ISO8601DateFormat) { - ///YYYY-MM-DD. - ISO8601DateFormatCalendar, - ///YYYY-DDD, where DDD ranges from 1 to 366; for example, 2009-32 is 2009-02-01. - ISO8601DateFormatOrdinal, - ///YYYY-Www-D, where ww ranges from 1 to 53 (the 'W' is literal) and D ranges from 1 to 7; for example, 2009-W05-07. - ISO8601DateFormatWeek, -}; - -///The default separator for time values. Currently, this is ':'. -extern const unichar ISO8601DefaultTimeSeparatorCharacter; - -/*! - * @brief This class converts dates to and from ISO 8601 strings. - * - * @details TL;DR: You want to use ISO 8601 for any and all dates you send or receive over the internet, unless the spec for the protocol or format you're working with specifically tells you otherwise. See http://xkcd.com/1179/ . - * - * ISO 8601 is most recognizable as “year-month-date” strings, such as “2013-09-08T15:06:11-0800”. Of course, as you might expect of a formal standard, it's more sophisticated (some might say complicated) than that. - * - * For one thing, ISO 8601 actually defines *three* different date formats. The most common one, shown above, is called the calendar date format. The other two are week dates, where the month is replaced by a week of the year and the day is a day-of-the-week (1 being Monday) rather than day-of-month, and ordinal dates, where the middle segment is dispensed with entirely and the day component is day-of-year (1–366). - * - * The week format is the most bizarre of them, since 7 × 52 ≠ 365. The start and end of the year for purposes of week dates usually don't line up with the start and end of the calendar year. As a result, the first and/or last day of a year in the week-date “calendar” more often than not is on a different year from the first and/or last day of that year on the actual Gregorian calendar. - * - * In practice, almost all ISO 8601 dates you see in the wild will be in the calendar format. - * - * At any rate, this formatter can both parse and unparse dates in all three formats. (By “unparse”, I mean “produce a string from”—the reverse of parsing.) - * - * For a good and more detailed introduction to ISO 8601, see [“A summary of the international standard date and time notation” by Markus Kuhn](http://www.cl.cam.ac.uk/~mgk25/iso-time.html). The actual standard itself can be found in PDF format online with a well-crafted web search. - */ - -@interface ISO8601DateFormatter: NSFormatter -{ - NSString *lastUsedFormatString; - NSDateFormatter *unparsingFormatter; - - NSCalendar *parsingCalendar, *unparsingCalendar; - - NSTimeZone *defaultTimeZone; - ISO8601DateFormat format; - unichar timeSeparator; - unichar timeZoneSeparator; - BOOL includeTime; - BOOL useMillisecondPrecision; - BOOL parsesStrictly; -} - -@property(nonatomic, retain) NSTimeZone *defaultTimeZone; - -#pragma mark Parsing -/*! - * @name Parsing - */ - -//As a formatter, this object converts strings to dates. - -/*! - * @brief Disables various leniencies in how the formatter parses strings. - * - * @details By default, the parser allows these extensions to the ISO 8601 standard: - * - * - Whitespace is allowed before the date. - * - Numbers don't have to be within range for a component. For example, you can have a string that refers to the 56th day of the hundredth month. - * - Since 0.6, allows a single whitespace character before the time-zone specification. This extension provides compatibility with NSDate output (`description`) and input (`dateWithString:`). - * - “Superfluous” hyphens in date specifications such as “`--DDD`” (where DDD is an ordinal day-of-year number and the year is implied) are allowed. (The standard recommends writing such a date as “`-DDD`”, with only a single hyphen. This is consistent with ordinal dates having only two components: the year and the day-of-year, separated by one hyphen.) - * - The same goes for week dates such as “`--Www-DD`”. Again, the extra hyphen really is superfluous, but is allowed as an extension. - * - Calendar dates with no separator between month and day-of-month are allowed, at least when they total four digits. (For example, 2013-0908, which would be interpreted as 2013-09-08.) - * - Single-digit components are allowed. (If you wish to specify a date in a single-digit year with the strict parser, pad it with zeroes.) - * - “YYYY-W” (without a week number after the 'W') is allowed, interpreted as “YYYY-W01-01”. - * - * Setting this property to `YES` will disable all of those extensions. - * - * These extensions are intended to help you process degenerate input (received from programs and services that use broken date-formatting libraries); whenever *you* create ISO 8601 strings, you should generate strictly-conforming ones. - * - * This property does not affect unparsing. The formatter always creates valid ISO 8601 strings. Any invalid string (loosely, any string that would require turning this property off to re-parse) should be considered a bug; please report it. - */ -@property BOOL parsesStrictly; - -/*! - * @brief Parse a string into individual date components. - * - * @param string The string to parse. Must represent a date in one of the ISO 8601 formats. - * @returns An NSDateComponents object containing most of the information parsed from the string, aside from the fraction of second and time zone (which are lost). - * @sa dateComponentsFromString:timeZone: - * @sa dateComponentsFromString:timeZone:range:fractionOfSecond: - */ -- (NSDateComponents *) dateComponentsFromString:(NSString *)string; -/*! - * @brief Parse a string into individual date components. - * - * @param string The string to parse. Must represent a date in one of the ISO 8601 formats. - * @param outTimeZone If non-`NULL`, an NSTimeZone object or `nil` will be stored here, depending on whether the string specified a time zone. - * @returns An NSDateComponents object containing most of the information parsed from the string, aside from the fraction of second (which is lost) and time zone. - * @sa dateComponentsFromString: - * @sa dateComponentsFromString:timeZone:range:fractionOfSecond: - */ -- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone; -/*! - * @brief Parse a string into individual date components. - * - * @param string The string to parse. Must represent a date in one of the ISO 8601 formats. - * @param outTimeZone If non-`NULL`, an NSTimeZone object or `nil` will be stored here, depending on whether the string specified a time zone. - * @param outRange If non-`NULL`, an NSRange structure will be stored here, identifying the substring of `string` that specified the date. - * @param outFractionOfSecond If non-`NULL`, an NSTimeInterval value will be stored here, containing the fraction of a second, if the string specified one. If it didn't, this will be set to zero. - * @returns An NSDateComponents object containing most of the information parsed from the string, aside from the fraction of second and time zone. - * @sa dateComponentsFromString: - * @sa dateComponentsFromString:timeZone: - */ -- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange fractionOfSecond:(NSTimeInterval *)outFractionOfSecond; - -/*! - * @brief Parse a string. - * - * @param string The string to parse. Must represent a date in one of the ISO 8601 formats. - * @returns An NSDate object containing most of the information parsed from the string, aside from the time zone (which is lost). - * @sa dateComponentsFromString: - * @sa dateFromString:timeZone: - * @sa dateFromString:timeZone:range: - */ -- (NSDate *) dateFromString:(NSString *)string; -/*! - * @brief Parse a string. - * - * @param string The string to parse. Must represent a date in one of the ISO 8601 formats. - * @param outTimeZone If non-`NULL`, an NSTimeZone object or `nil` will be stored here, depending on whether the string specified a time zone. - * @returns An NSDate object containing most of the information parsed from the string, aside from the time zone. - * @sa dateComponentsFromString:timeZone: - * @sa dateFromString: - * @sa dateFromString:timeZone:range: - */ -- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone; -/*! - * @brief Parse a string into a single date, identified by an NSDate object. - * - * @param string The string to parse. Must represent a date in one of the ISO 8601 formats. - * @param outTimeZone If non-`NULL`, an NSTimeZone object or `nil` will be stored here, depending on whether the string specified a time zone. - * @param outRange If non-`NULL`, an NSRange structure will be stored here, identifying the substring of `string` that specified the date. - * @returns An NSDate object containing most of the information parsed from the string, aside from the time zone. - * @sa dateComponentsFromString:timeZone:range:fractionOfSecond: - * @sa dateFromString: - * @sa dateFromString:timeZone: - */ -- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange; - -#pragma mark Unparsing -/*! - * @name Unparsing - */ - -/*! - * @brief Which ISO 8601 format to format dates in. - * - * @details See ISO8601DateFormat for possible values. - */ -@property ISO8601DateFormat format; -/*! - * @brief Whether strings should include time of day. - * - * @details If `NO`, strings include only the date, nothing after it. - * - * @sa timeSeparator - * @sa timeZoneSeparator - */ -@property BOOL includeTime; -/*! - * @brief Whether strings should include millisecond precision time. - * - * @details If `YES`, strings include three millisecond digits. Only has an effect if `includeTime` is `YES` - * - */ -@property BOOL useMillisecondPrecision; -/*! - * @brief The character to use to separate components of the time of day. - * - * @details This is used in both parsing and unparsing. - * - * The default value is ISO8601DefaultTimeSeparatorCharacter. - * - * When parsesStrictly is set to `YES`, this property is ignored. Otherwise, the parser will raise an exception if this is set to zero. - * - * @sa includeTime - * @sa timeZoneSeparator - */ -@property unichar timeSeparator; -/*! - * @brief The character to use to separate the hour and minute in a time zone specification. - * - * @details This is used in both parsing and unparsing. - * - * If zero, no separator is inserted into time zone specifications. - * - * The default value is zero (no separator). - * - * @sa includeTime - * @sa timeSeparator - */ -@property unichar timeZoneSeparator; - -/*! - * @brief Produce a string that represents a date in UTC. - * - * @param date The string to parse. Must represent a date in one of the ISO 8601 formats. - * @returns A string that represents the date in UTC. - * @sa stringFromDate:timeZone: - */ -- (NSString *) stringFromDate:(NSDate *)date; -/*! - * @brief Produce a string that represents a date. - * - * @param date The string to parse. Must represent a date in one of the ISO 8601 formats. - * @param timeZone An NSTimeZone object identifying the time zone in which to specify the date. - * @returns A string that represents the date in the requested time zone, if possible. - * - * @details Not all dates are representable in all time zones (because of historical calendar changes, such as transitions from the Julian to the Gregorian calendar). - * For an example, see http://stackoverflow.com/questions/18663407/date-formatter-returns-nil-for-june . - * This method *should* return `nil` in such cases. - * - * @sa stringFromDate: - */ -- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone; - -@end diff --git a/RileyLink/ISO8601DateFormatter.m b/RileyLink/ISO8601DateFormatter.m deleted file mode 100644 index 785bcc6b0..000000000 --- a/RileyLink/ISO8601DateFormatter.m +++ /dev/null @@ -1,1036 +0,0 @@ -/*ISO8601DateFormatter.m - * - *Created by Peter Hosey on 2009-04-11. - *Copyright 2009–2013 Peter Hosey. All rights reserved. - */ - -#import -#import -#if TARGET_OS_IPHONE -# import -#endif -#import "ISO8601DateFormatter.h" - -#ifndef DEFAULT_TIME_SEPARATOR -# define DEFAULT_TIME_SEPARATOR ':' -#endif -const unichar ISO8601DefaultTimeSeparatorCharacter = DEFAULT_TIME_SEPARATOR; - -//Unicode date formats. -#define ISO_CALENDAR_DATE_FORMAT @"yyyy-MM-dd" -//#define ISO_WEEK_DATE_FORMAT @"YYYY-'W'ww-ee" //Doesn't actually work because NSDateComponents counts the weekday starting at 1. -#define ISO_ORDINAL_DATE_FORMAT @"yyyy-DDD" -#define ISO_TIME_FORMAT @"HH:mm:ss" -#define ISO_TIME_FORMAT_MS_PRECISION @"HH:mm:ss.SSS" -//printf formats. -#define ISO_TIMEZONE_UTC_FORMAT @"Z" -#define ISO_TIMEZONE_OFFSET_FORMAT_NO_SEPARATOR @"%+.2d%.2d" -#define ISO_TIMEZONE_OFFSET_FORMAT_WITH_SEPARATOR @"%+.2d%C%.2d" - -static NSString * const ISO8601TwoCharIntegerFormat = @"%.2d"; - -@interface ISO8601DateFormatter () -+ (void) createGlobalCachesThatDoNotAlreadyExist; -//Used when a memory warning occurs (if at least one ISO 8601 Date Formatter exists at the time). -+ (void) purgeGlobalCaches; -@end - -@interface ISO8601DateFormatter(UnparsingPrivate) - -- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep; - -- (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone; -- (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone; - -@end - -@interface ISO8601TimeZoneCache: NSObject -{} - -//The property being read-only means that the formatter cannot change the cache's dictionary, but the formatter is explicitly allowed to mutate the dictionary. -@property(nonatomic, readonly, strong) NSMutableDictionary *timeZonesByOffset; - -@end - -static ISO8601TimeZoneCache *timeZoneCache; - -#if ISO8601_TESTING_PURPOSES_ONLY -//This method only exists for use by the project's test cases. DO NOT use this in an application. -extern bool ISO8601DateFormatter_GlobalCachesAreWarm(void); - -bool ISO8601DateFormatter_GlobalCachesAreWarm(void) { - return (timeZoneCache != nil) && (timeZoneCache.timeZonesByOffset.count > 0); -} -#endif - -@implementation ISO8601DateFormatter -+ (void) initialize { - [self createGlobalCachesThatDoNotAlreadyExist]; -} - -+ (void) createGlobalCachesThatDoNotAlreadyExist { - if (!timeZoneCache) { - timeZoneCache = [[ISO8601TimeZoneCache alloc] init]; - } -} - -+ (void) purgeGlobalCaches { - ISO8601TimeZoneCache *oldCache = timeZoneCache; - timeZoneCache = nil; - [oldCache release]; -} - -- (NSCalendar *) makeCalendarWithDesiredConfiguration { - NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian] autorelease]; - calendar.firstWeekday = 2; //Monday - calendar.timeZone = [NSTimeZone defaultTimeZone]; - return calendar; -} - -- (instancetype) init { - if ((self = [super init])) { - parsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain]; - unparsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain]; - - format = ISO8601DateFormatCalendar; - timeSeparator = ISO8601DefaultTimeSeparatorCharacter; - includeTime = NO; - parsesStrictly = NO; - useMillisecondPrecision = NO; - -#if TARGET_OS_IPHONE - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(didReceiveMemoryWarning:) - name:UIApplicationDidReceiveMemoryWarningNotification - object:nil]; -#endif - } - return self; -} - -- (void) dealloc { -#if TARGET_OS_IPHONE - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; -#endif - - [defaultTimeZone release]; - - [unparsingFormatter release]; - [lastUsedFormatString release]; - [parsingCalendar release]; - [unparsingCalendar release]; - - [super dealloc]; -} - -- (void) didReceiveMemoryWarning:(NSNotification *)notification { - [[self class] purgeGlobalCaches]; -} - -@synthesize defaultTimeZone; -- (void) setDefaultTimeZone:(NSTimeZone *)tz { - if (defaultTimeZone != tz) { - [defaultTimeZone release]; - defaultTimeZone = [tz retain]; - - unparsingCalendar.timeZone = defaultTimeZone; - } -} - -//The following properties are only here because GCC doesn't like @synthesize in category implementations. - -#pragma mark Parsing - -@synthesize parsesStrictly; - -static NSUInteger read_segment(const unichar *str, const unichar **next, NSUInteger *out_num_digits); -static NSUInteger read_segment_4digits(const unichar *str, const unichar **next, NSUInteger *out_num_digits); -static NSUInteger read_segment_2digits(const unichar *str, const unichar **next); -static double read_double(const unichar *str, const unichar **next); -static BOOL is_leap_year(NSUInteger year); - -/*Valid ISO 8601 date formats: - * - *YYYYMMDD - *YYYY-MM-DD - *YYYY-MM - *YYYY - *YY //century - * //Implied century: YY is 00-99 - * YYMMDD - * YY-MM-DD - * -YYMM - * -YY-MM - * -YY - * //Implied year - * --MMDD - * --MM-DD - * --MM - * //Implied year and month - * ---DD - * //Ordinal dates: DDD is the number of the day in the year (1-366) - *YYYYDDD - *YYYY-DDD - * YYDDD - * YY-DDD - * -DDD - * //Week-based dates: ww is the number of the week, and d is the number (1-7) of the day in the week - *yyyyWwwd - *yyyy-Www-d - *yyyyWww - *yyyy-Www - *yyWwwd - *yy-Www-d - *yyWww - *yy-Www - * //Year of the implied decade - *-yWwwd - *-y-Www-d - *-yWww - *-y-Www - * //Week and day of implied year - * -Wwwd - * -Www-d - * //Week only of implied year - * -Www - * //Day only of implied week - * -W-d - */ - -- (NSDateComponents *) dateComponentsFromString:(NSString *)string { - return [self dateComponentsFromString:string timeZone:NULL]; -} -- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone { - return [self dateComponentsFromString:string timeZone:outTimeZone range:NULL fractionOfSecond:NULL]; -} -- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange fractionOfSecond:(out NSTimeInterval *)outFractionOfSecond { - if (string == nil) - return nil; - // Bail if the string contains a slash delimiter (we don't yet support ISO 8601 intervals and we don't support slash-separated dates) - if ([string rangeOfString:@"/"].location != NSNotFound) - return nil; - - NSDate *now = [NSDate date]; - - NSDateComponents *components = [[[NSDateComponents alloc] init] autorelease]; - NSDateComponents *nowComponents = [parsingCalendar components:(NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay) fromDate:now]; - - NSUInteger - //Date - year = 0U, - month_or_week = 0U, - day = 0U, - //Time - hour = 0U; - NSTimeInterval - minute = 0.0, - second = 0.0; - //Time zone - NSInteger tz_hour = 0; - NSInteger tz_minute = 0; - - enum { - monthAndDate, - week, - dateOnly - } dateSpecification = monthAndDate; - - BOOL strict = self.parsesStrictly; - unichar timeSep = self.timeSeparator; - - if (strict) timeSep = ISO8601DefaultTimeSeparatorCharacter; - NSAssert(timeSep != '\0', @"Time separator must not be NUL."); - - BOOL isValidDate = (string.length > 0U); - NSTimeZone *timeZone = nil; - - const unichar *ch = (const unichar *)[string cStringUsingEncoding:NSUnicodeStringEncoding]; - - NSRange range = { 0U, 0U }; - const unichar *start_of_date = NULL; - if (strict && isspace(*ch)) { - range.location = NSNotFound; - isValidDate = NO; - } else { - //Skip leading whitespace. - NSUInteger i = 0U; - while (isspace(ch[i])) - ++i; - - range.location = i; - ch += i; - start_of_date = ch; - - NSUInteger segment; - NSUInteger num_leading_hyphens = 0U, num_digits = 0U; - - if (*ch == 'T') { - //There is no date here, only a time. Set the date to now; then we'll parse the time. - isValidDate = isdigit(*++ch); - - year = nowComponents.year; - month_or_week = nowComponents.month; - day = nowComponents.day; - } else { - while(*ch == '-') { - ++num_leading_hyphens; - ++ch; - } - - segment = read_segment(ch, &ch, &num_digits); - switch(num_digits) { - case 0: - if (*ch == 'W') { - if ((ch[1] == '-') && isdigit(ch[2]) && ((num_leading_hyphens == 1U) || ((num_leading_hyphens == 2U) && !strict))) { - year = nowComponents.year; - month_or_week = 1U; - ch += 2; - goto parseDayAfterWeek; - } else if (num_leading_hyphens == 1U) { - year = nowComponents.year; - goto parseWeekAndDay; - } else - isValidDate = NO; - } else - isValidDate = NO; - break; - - case 8: //YYYY MM DD - if (num_leading_hyphens > 0U) - isValidDate = NO; - else { - day = segment % 100U; - segment /= 100U; - month_or_week = segment % 100U; - year = segment / 100U; - } - break; - - case 6: //YYMMDD (implicit century) - if (num_leading_hyphens > 0U || strict) - isValidDate = NO; - else { - day = segment % 100U; - segment /= 100U; - month_or_week = segment % 100U; - year = nowComponents.year; - year -= (year % 100U); - year += segment / 100U; - } - break; - - case 4: - switch(num_leading_hyphens) { - case 0: //YYYY - year = segment; - - if (*ch == '-') ++ch; - - if (!isdigit(*ch)) { - if (*ch == 'W') - goto parseWeekAndDay; - else - month_or_week = day = 1U; - } else { - segment = read_segment(ch, &ch, &num_digits); - switch(num_digits) { - case 4: //MMDD - if (strict) - isValidDate = NO; - else { - day = segment % 100U; - month_or_week = segment / 100U; - } - break; - - case 2: //MM - month_or_week = segment; - - if (*ch == '-') ++ch; - if (!isdigit(*ch)) - day = 1U; - else - day = read_segment(ch, &ch, NULL); - break; - - case 3: //DDD - day = segment % 1000U; - dateSpecification = dateOnly; - if (strict && (day > (365U + is_leap_year(year)))) - isValidDate = NO; - break; - - default: - isValidDate = NO; - } - } - break; - - case 1: //YYMM - month_or_week = segment % 100U; - year = segment / 100U; - - if (*ch == '-') ++ch; - if (!isdigit(*ch)) - day = 1U; - else - day = read_segment(ch, &ch, NULL); - - break; - - case 2: //MMDD - day = segment % 100U; - month_or_week = segment / 100U; - year = nowComponents.year; - - break; - - default: - isValidDate = NO; - } //switch(num_leading_hyphens) (4 digits) - break; - - case 1: - if (strict) { - //Two digits only - never just one. - if (num_leading_hyphens == 1U) { - if (*ch == '-') ++ch; - if (*++ch == 'W') { - year = nowComponents.year; - year -= (year % 10U); - year += segment; - goto parseWeekAndDay; - } else - isValidDate = NO; - } else - isValidDate = NO; - break; - } - case 2: - switch(num_leading_hyphens) { - case 0: - if (*ch == '-') { - //Implicit century - year = nowComponents.year; - year -= (year % 100U); - year += segment; - - if (*++ch == 'W') - goto parseWeekAndDay; - else if (!isdigit(*ch)) { - goto centuryOnly; - } else { - //Get month and/or date. - segment = read_segment_4digits(ch, &ch, &num_digits); - NSLog(@"(%@) parsing month; segment is %lu and ch is %@", string, (unsigned long)segment, [NSString stringWithCString:(const char *)ch encoding:NSUnicodeStringEncoding]); - switch(num_digits) { - case 4: //YY-MMDD - day = segment % 100U; - month_or_week = segment / 100U; - break; - - case 1: //YY-M; YY-M-DD (extension) - if (strict) { - isValidDate = NO; - break; - } - case 2: //YY-MM; YY-MM-DD - month_or_week = segment; - if (*ch == '-') { - if (isdigit(*++ch)) - day = read_segment_2digits(ch, &ch); - else - day = 1U; - } else - day = 1U; - break; - - case 3: //Ordinal date. - day = segment; - dateSpecification = dateOnly; - break; - } - } - } else if (*ch == 'W') { - year = nowComponents.year; - year -= (year % 100U); - year += segment; - - parseWeekAndDay: //*ch should be 'W' here. - if (!isdigit(*++ch)) { - //Not really a week-based date; just a year followed by '-W'. - if (strict) - isValidDate = NO; - else - month_or_week = day = 1U; - } else { - month_or_week = read_segment_2digits(ch, &ch); - if (*ch == '-') ++ch; - parseDayAfterWeek: - day = isdigit(*ch) ? read_segment_2digits(ch, &ch) : 1U; - dateSpecification = week; - } - } else { - //Century only. Assume current year. - centuryOnly: - year = segment * 100U + nowComponents.year % 100U; - month_or_week = day = 1U; - } - break; - - case 1:; //-YY; -YY-MM (implicit century) - NSLog(@"(%@) found %lu digits and one hyphen, so this is either -YY or -YY-MM; segment (year) is %lu", string, (unsigned long)num_digits, (unsigned long)segment); - NSUInteger current_year = nowComponents.year; - NSUInteger current_century = (current_year % 100U); - year = segment + (current_year - current_century); - if (num_digits == 1U) //implied decade - year += current_century - (current_year % 10U); - - if (*ch == '-') { - ++ch; - month_or_week = read_segment_2digits(ch, &ch); - NSLog(@"(%@) month is %lu", string, (unsigned long)month_or_week); - } - - day = 1U; - break; - - case 2: //--MM; --MM-DD - year = nowComponents.year; - month_or_week = segment; - if (*ch == '-') { - ++ch; - day = read_segment_2digits(ch, &ch); - } - break; - - case 3: //---DD - year = nowComponents.year; - month_or_week = nowComponents.month; - day = segment; - break; - - default: - isValidDate = NO; - } //switch(num_leading_hyphens) (2 digits) - break; - - case 7: //YYYY DDD (ordinal date) - if (num_leading_hyphens > 0U) - isValidDate = NO; - else { - day = segment % 1000U; - year = segment / 1000U; - dateSpecification = dateOnly; - if (strict && (day > (365U + is_leap_year(year)))) - isValidDate = NO; - } - break; - - case 3: //--DDD (ordinal date, implicit year) - //Technically, the standard only allows one hyphen. But it says that two hyphens is the logical implementation, and one was dropped for brevity. So I have chosen to allow the missing hyphen. - if ((num_leading_hyphens < 1U) || ((num_leading_hyphens > 2U) && !strict)) - isValidDate = NO; - else { - day = segment; - year = nowComponents.year; - dateSpecification = dateOnly; - if (strict && (day > (365U + is_leap_year(year)))) - isValidDate = NO; - } - break; - - default: - isValidDate = NO; - } - } - - if (isValidDate) { - if (isspace(*ch) || (*ch == 'T')) ++ch; - - if (isdigit(*ch)) { - hour = read_segment_2digits(ch, &ch); - if (*ch == timeSep) { - ++ch; - if ((timeSep == ',') || (timeSep == '.')) { - //We can't do fractional minutes when '.' is the segment separator. - //Only allow whole minutes and whole seconds. - minute = read_segment_2digits(ch, &ch); - if (*ch == timeSep) { - ++ch; - second = read_segment_2digits(ch, &ch); - } - } else { - //Allow a fractional minute. - //If we don't get a fraction, look for a seconds segment. - //Otherwise, the fraction of a minute is the seconds. - minute = read_double(ch, &ch); - second = modf(minute, &minute); - if (second > DBL_EPSILON) - second *= 60.0; //Convert fraction (e.g. .5) into seconds (e.g. 30). - else if (*ch == timeSep) { - ++ch; - second = read_double(ch, &ch); - } - } - } - - if (!strict) { - if (isspace(*ch)) ++ch; - } - - switch(*ch) { - case 'Z': - timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; - ++ch; //So that the Z is included in the range. - break; - - case '+': - case '-':; - BOOL negative = (*ch == '-'); - if (isdigit(*++ch)) { - //Read hour offset. - segment = *ch - '0'; - if (isdigit(*++ch)) { - segment *= 10U; - segment += *(ch++) - '0'; - } - tz_hour = (NSInteger)segment; - if (negative) tz_hour = -tz_hour; - - //Optional separator. - if (*ch == self.timeZoneSeparator) ++ch; - - if (isdigit(*ch)) { - //Read minute offset. - segment = *ch - '0'; - if (isdigit(*++ch)) { - segment *= 10U; - segment += *ch - '0'; - } - tz_minute = segment; - if (negative) tz_minute = -tz_minute; - } - - [[self class] createGlobalCachesThatDoNotAlreadyExist]; - - NSInteger timeZoneOffset = (tz_hour * 3600) + (tz_minute * 60); - NSNumber *offsetNum = @(timeZoneOffset); - timeZone = (timeZoneCache.timeZonesByOffset)[offsetNum]; - if (!timeZone) { - timeZone = [NSTimeZone timeZoneForSecondsFromGMT:timeZoneOffset]; - if (timeZone) - (timeZoneCache.timeZonesByOffset)[offsetNum] = timeZone; - } - } - } - } - } - - if (isValidDate) { - components.year = year; - components.day = day; - components.hour = hour; - components.minute = (NSInteger)minute; - components.second = (NSInteger)second; - - if (outFractionOfSecond != NULL) { - NSTimeInterval fractionOfSecond = second - components.second; - if (fractionOfSecond > 0.0) { - *outFractionOfSecond = fractionOfSecond; - } - } - - switch(dateSpecification) { - case monthAndDate: - components.month = month_or_week; - break; - - case week:; - //Adapted from . - //This works by converting the week date into an ordinal date, then letting the next case handle it. - NSUInteger prevYear = year - 1U; - NSUInteger YY = prevYear % 100U; - NSUInteger C = prevYear - YY; - NSUInteger G = YY + YY / 4U; - NSUInteger isLeapYear = (((C / 100U) % 4U) * 5U); - NSUInteger Jan1Weekday = (isLeapYear + G) % 7U; - enum { monday, tuesday, wednesday, thursday/*, friday, saturday, sunday*/ }; - components.day = ((8U - Jan1Weekday) + (7U * (Jan1Weekday > thursday))) + (day - 1U) + (7U * (month_or_week - 2)); - - case dateOnly: //An "ordinal date". - break; - } - } - } //if (!(strict && isdigit(ch[0]))) - - if (outRange) { - if (isValidDate) - range.length = ch - start_of_date; - else - range.location = NSNotFound; - - *outRange = range; - } - if (outTimeZone) { - *outTimeZone = timeZone; - } - - return isValidDate ? components : nil; -} - -- (NSDate *) dateFromString:(NSString *)string { - return [self dateFromString:string timeZone:NULL]; -} -- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone { - return [self dateFromString:string timeZone:outTimeZone range:NULL]; -} -- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange { - NSTimeZone *timeZone = nil; - NSTimeInterval parsedFractionOfSecond = 0.0; - - NSDateComponents *components = [self dateComponentsFromString:string timeZone:&timeZone range:outRange fractionOfSecond:&parsedFractionOfSecond]; - - if (outTimeZone) - *outTimeZone = timeZone; - if (components == nil) - return nil; - - parsingCalendar.timeZone = timeZone; - - NSDate *parsedDate = [parsingCalendar dateFromComponents:components]; - - if (parsedFractionOfSecond > 0.0) { - parsedDate = [parsedDate dateByAddingTimeInterval:parsedFractionOfSecond]; - } - - return parsedDate; -} - -- (BOOL)getObjectValue:(id *)outValue forString:(NSString *)string errorDescription:(NSString **)error { - NSDate *date = [self dateFromString:string]; - if (outValue) - *outValue = date; - return (date != nil); -} - -#pragma mark Unparsing - -@synthesize format; -@synthesize includeTime; -@synthesize useMillisecondPrecision; -@synthesize timeSeparator; -@synthesize timeZoneSeparator; - -- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep { - if (timeSep != ':') { - NSMutableString *timeFormatMutable = [[timeFormat mutableCopy] autorelease]; - [timeFormatMutable replaceOccurrencesOfString:@":" - withString:[NSString stringWithCharacters:&timeSep length:1U] - options:NSBackwardsSearch | NSLiteralSearch - range:(NSRange){ 0UL, timeFormat.length }]; - timeFormat = timeFormatMutable; - } - return timeFormat; -} - -- (NSString *) stringFromDate:(NSDate *)date { - NSTimeZone *timeZone = self.defaultTimeZone; - if (!timeZone) timeZone = [NSTimeZone defaultTimeZone]; - return [self stringFromDate:date timeZone:timeZone]; -} - -- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone { - switch (self.format) { - case ISO8601DateFormatCalendar: - return [self stringFromDate:date formatString:ISO_CALENDAR_DATE_FORMAT timeZone:timeZone]; - case ISO8601DateFormatWeek: - return [self weekDateStringForDate:date timeZone:timeZone]; - case ISO8601DateFormatOrdinal: - return [self stringFromDate:date formatString:ISO_ORDINAL_DATE_FORMAT timeZone:timeZone]; - default: - [NSException raise:NSInternalInconsistencyException format:@"self.format was %tu, not calendar (%tu), week (%tu), or ordinal (%tu)", self.format, ISO8601DateFormatCalendar, ISO8601DateFormatWeek, ISO8601DateFormatOrdinal]; - return nil; - } -} - -- (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone { - if (includeTime){ - NSString *timeFormat = self.useMillisecondPrecision ? ISO_TIME_FORMAT_MS_PRECISION : ISO_TIME_FORMAT; - dateFormat = [dateFormat stringByAppendingFormat:@"'T'%@", [self replaceColonsInString:timeFormat withTimeSeparator:self.timeSeparator]]; - } - - - if ([dateFormat isEqualToString:lastUsedFormatString] == NO) { - [unparsingFormatter release]; - unparsingFormatter = nil; - - [lastUsedFormatString release]; - lastUsedFormatString = [dateFormat retain]; - } - - if (!unparsingFormatter) { - unparsingFormatter = [[NSDateFormatter alloc] init]; - unparsingFormatter.formatterBehavior = NSDateFormatterBehavior10_4; - unparsingFormatter.dateFormat = dateFormat; - unparsingFormatter.calendar = unparsingCalendar; - unparsingFormatter.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"] autorelease]; - } - - unparsingCalendar.timeZone = timeZone; - unparsingFormatter.timeZone = timeZone; - NSString *str = [unparsingFormatter stringForObjectValue:date]; - - if (includeTime) { - NSInteger offset = [timeZone secondsFromGMTForDate:date]; - offset /= 60; //bring down to minutes - if (offset == 0) - str = [str stringByAppendingString:ISO_TIMEZONE_UTC_FORMAT]; - else { - int timeZoneOffsetHour = abs((int)(offset / 60)); - int timeZoneOffsetMinute = abs((int)(offset % 60)); - - if (offset > 0) str = [str stringByAppendingString:@"+"]; - else str = [str stringByAppendingString:@"-"]; - - str = [str stringByAppendingFormat:ISO8601TwoCharIntegerFormat, timeZoneOffsetHour]; - - if (self.timeZoneSeparator) str = [str stringByAppendingFormat:@"%C", self.timeZoneSeparator]; - - str = [str stringByAppendingFormat:ISO8601TwoCharIntegerFormat, timeZoneOffsetMinute]; - } - } - - //Undo the change we made earlier - unparsingCalendar.timeZone = self.defaultTimeZone; - unparsingFormatter.timeZone = self.defaultTimeZone; - - return str; -} - -- (NSString *) stringForObjectValue:(id)value { - if ( ! [value isKindOfClass:[NSDate class]]) { - NSLog(@"%s: Can only format NSDate objects, not objects like %@", __func__, value); - return nil; - } - - return [self stringFromDate:(NSDate *)value]; -} - -/*Adapted from: - * Algorithm for Converting Gregorian Dates to ISO 8601 Week Date - * Rick McCarty, 1999 - * http://personal.ecu.edu/mccartyr/ISOwdALG.txt - */ -- (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone { - unparsingCalendar.timeZone = timeZone; - NSDateComponents *components = [unparsingCalendar components:NSCalendarUnitYear | NSCalendarUnitWeekday | NSCalendarUnitDay fromDate:date]; - - //Determine the ordinal date. - NSDateComponents *startOfYearComponents = [unparsingCalendar components:NSCalendarUnitYear fromDate:date]; - startOfYearComponents.month = 1; - startOfYearComponents.day = 1; - NSDateComponents *ordinalComponents = [unparsingCalendar components:NSCalendarUnitDay fromDate:[unparsingCalendar dateFromComponents:startOfYearComponents] toDate:date options:0]; - ordinalComponents.day += 1; - - enum { - monday, tuesday, wednesday, thursday, friday, saturday, sunday - }; - enum { - january = 1, february, march, - april, may, june, - july, august, september, - october, november, december - }; - - NSInteger year = components.year; - NSInteger week = 0; - //The old unparser added 6 to [calendarDate dayOfWeek], which was zero-based; components.weekday is one-based, so we now add only 5. - NSInteger dayOfWeek = (components.weekday + 5) % 7; - NSInteger dayOfYear = ordinalComponents.day; - - NSInteger prevYear = year - 1; - - BOOL yearIsLeapYear = is_leap_year(year); - BOOL prevYearIsLeapYear = is_leap_year(prevYear); - - NSInteger YY = prevYear % 100; - NSInteger C = prevYear - YY; - NSInteger G = YY + YY / 4; - NSInteger Jan1Weekday = (((((C / 100) % 4) * 5) + G) % 7); - - NSInteger weekday = ((dayOfYear + Jan1Weekday) - 1) % 7; - - if((dayOfYear <= (7 - Jan1Weekday)) && (Jan1Weekday > thursday)) { - week = 52 + ((Jan1Weekday == friday) || ((Jan1Weekday == saturday) && prevYearIsLeapYear)); - --year; - } else { - NSInteger lengthOfYear = 365 + yearIsLeapYear; - if((lengthOfYear - dayOfYear) < (thursday - weekday)) { - ++year; - week = 1; - } else { - NSInteger J = dayOfYear + (sunday - weekday) + Jan1Weekday; - week = J / 7 - (Jan1Weekday > thursday); - } - } - - NSString *string = [NSString stringWithFormat:@"%lu-W%02lu-%02lu", (unsigned long)year, (unsigned long)week, ((unsigned long)dayOfWeek) + 1U]; - - NSString *timeString; - if(includeTime) { - NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; - unichar timeSep = self.timeSeparator; - if (!timeSep) timeSep = ISO8601DefaultTimeSeparatorCharacter; - - NSString *timeFormat = self.useMillisecondPrecision ? ISO_TIME_FORMAT_MS_PRECISION : ISO_TIME_FORMAT; - formatter.dateFormat = [self replaceColonsInString:timeFormat withTimeSeparator:timeSep]; - formatter.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"] autorelease]; - formatter.timeZone = timeZone; - - timeString = [formatter stringForObjectValue:date]; - - [formatter release]; - - //TODO: This is copied from the calendar-date code. It should be isolated in a method. - NSInteger offset = [timeZone secondsFromGMTForDate:date]; - offset /= 60; //bring down to minutes - if (offset == 0) - timeString = [timeString stringByAppendingString:ISO_TIMEZONE_UTC_FORMAT]; - else { - int timeZoneOffsetHour = (int)(offset / 60); - int timeZoneOffsetMinute = (int)(offset % 60); - if (self.timeZoneSeparator) - timeString = [timeString stringByAppendingFormat:ISO_TIMEZONE_OFFSET_FORMAT_WITH_SEPARATOR, - timeZoneOffsetHour, self.timeZoneSeparator, - timeZoneOffsetMinute]; - else - timeString = [timeString stringByAppendingFormat:ISO_TIMEZONE_OFFSET_FORMAT_NO_SEPARATOR, - timeZoneOffsetHour, timeZoneOffsetMinute]; - } - - string = [string stringByAppendingFormat:@"T%@", timeString]; - } - - return string; -} - -@end - -static NSUInteger read_segment(const unichar *str, const unichar **next, NSUInteger *out_num_digits) { - NSUInteger num_digits = 0U; - NSUInteger value = 0U; - - while(isdigit(*str)) { - value *= 10U; - value += *str - '0'; - ++num_digits; - ++str; - } - - if (next) *next = str; - if (out_num_digits) *out_num_digits = num_digits; - - return value; -} -static NSUInteger read_segment_4digits(const unichar *str, const unichar **next, NSUInteger *out_num_digits) { - NSUInteger num_digits = 0U; - NSUInteger value = 0U; - - if (isdigit(*str)) { - value += *(str++) - '0'; - ++num_digits; - } - - if (isdigit(*str)) { - value *= 10U; - value += *(str++) - '0'; - ++num_digits; - } - - if (isdigit(*str)) { - value *= 10U; - value += *(str++) - '0'; - ++num_digits; - } - - if (isdigit(*str)) { - value *= 10U; - value += *(str++) - '0'; - ++num_digits; - } - - if (next) *next = str; - if (out_num_digits) *out_num_digits = num_digits; - - return value; -} -static NSUInteger read_segment_2digits(const unichar *str, const unichar **next) { - NSUInteger value = 0U; - - if (isdigit(*str)) - value += *str - '0'; - - if (isdigit(*++str)) { - value *= 10U; - value += *(str++) - '0'; - } - - if (next) *next = str; - - return value; -} - -//strtod doesn't support ',' as a separator. This does. -static double read_double(const unichar *str, const unichar **next) { - double value = 0.0; - - if (str) { - NSUInteger int_value = 0; - - while(isdigit(*str)) { - int_value *= 10U; - int_value += (*(str++) - '0'); - } - value = int_value; - - if (((*str == ',') || (*str == '.'))) { - ++str; - - register double multiplier, multiplier_multiplier; - multiplier = multiplier_multiplier = 0.1; - - while(isdigit(*str)) { - value += (*(str++) - '0') * multiplier; - multiplier *= multiplier_multiplier; - } - } - } - - if (next) *next = str; - - return value; -} - -static BOOL is_leap_year(NSUInteger year) { - return \ - ((year % 4U) == 0U) - && (((year % 100U) != 0U) - || ((year % 400U) == 0U)); -} - -static NSString *const ISO8601ThreadStorageTimeZoneCacheKey = @"org.boredzo.ISO8601ThreadStorageTimeZoneCacheKey"; - -@implementation ISO8601TimeZoneCache: NSObject - -- (NSMutableDictionary *) timeZonesByOffset { - NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary; - NSMutableDictionary *currentCacheDict = threadDict[ISO8601ThreadStorageTimeZoneCacheKey]; - if (currentCacheDict == nil) { - currentCacheDict = [NSMutableDictionary dictionaryWithCapacity:2UL]; - threadDict[ISO8601ThreadStorageTimeZoneCacheKey] = currentCacheDict; - } - return currentCacheDict; -} - -@end diff --git a/RileyLink/Images.xcassets/AppIcon.appiconset/Contents.json b/RileyLink/Images.xcassets/AppIcon.appiconset/Contents.json index 950a6bc64..8027b002d 100644 --- a/RileyLink/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/RileyLink/Images.xcassets/AppIcon.appiconset/Contents.json @@ -134,6 +134,12 @@ "filename" : "Icon-ipadpro.png", "scale" : "2x" }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "iTunesArtwork@2x-2.png", + "scale" : "1x" + }, { "size" : "24x24", "idiom" : "watch", @@ -183,6 +189,12 @@ "role" : "quickLook", "subtype" : "42mm" }, + { + "size" : "1024x1024", + "idiom" : "watch-marketing", + "filename" : "iTunesArtwork@2x-1.png", + "scale" : "1x" + }, { "idiom" : "mac", "size" : "16x16", diff --git a/RileyLink/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x-1.png b/RileyLink/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x-1.png new file mode 100644 index 000000000..dec8a274f Binary files /dev/null and b/RileyLink/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x-1.png differ diff --git a/RileyLink/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x-2.png b/RileyLink/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x-2.png new file mode 100644 index 000000000..dec8a274f Binary files /dev/null and b/RileyLink/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x-2.png differ diff --git a/RileyLink/RileyLink-Bridging-Header.h b/RileyLink/RileyLink-Bridging-Header.h index 65b734fcf..cc86ad5b7 100644 --- a/RileyLink/RileyLink-Bridging-Header.h +++ b/RileyLink/RileyLink-Bridging-Header.h @@ -5,4 +5,3 @@ #import "Config.h" #import "Log.h" #import -#import "NSData+Conversion.h" diff --git a/RileyLink/RileyLink-Info.plist b/RileyLink/RileyLink-Info.plist index 7da1229af..f8c865ba6 100644 --- a/RileyLink/RileyLink-Info.plist +++ b/RileyLink/RileyLink-Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.2.2 + 2.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/RileyLink/RileyLink.xcdatamodeld/RileyLink.xcdatamodel/contents b/RileyLink/RileyLink.xcdatamodeld/RileyLink.xcdatamodel/contents index 1a542262d..101023af1 100644 --- a/RileyLink/RileyLink.xcdatamodeld/RileyLink.xcdatamodel/contents +++ b/RileyLink/RileyLink.xcdatamodeld/RileyLink.xcdatamodel/contents @@ -1,12 +1,12 @@ - + - - + + - + \ No newline at end of file diff --git a/RileyLink/RuntimeUtils.h b/RileyLink/RuntimeUtils.h deleted file mode 100644 index ddd65c129..000000000 --- a/RileyLink/RuntimeUtils.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// RuntimeUtils.h -// RileyLink -// -// Created by Pete Schwamb on 11/18/15. -// Copyright © 2015 Pete Schwamb. All rights reserved. -// - -#import - -@interface RuntimeUtils : NSObject - -+ (NSArray *)classStringsForClassesOfType:(Class)filterType; - - -@end diff --git a/RileyLink/RuntimeUtils.m b/RileyLink/RuntimeUtils.m deleted file mode 100644 index 8f9590ee1..000000000 --- a/RileyLink/RuntimeUtils.m +++ /dev/null @@ -1,45 +0,0 @@ -// -// RuntimeUtils.m -// RileyLink -// -// Created by Pete Schwamb on 11/18/15. -// Copyright © 2015 Pete Schwamb. All rights reserved. -// - -#import - -#import "RuntimeUtils.h" - -@implementation RuntimeUtils - -+ (NSArray *)classStringsForClassesOfType:(Class)filterType { - - int numClasses = 0, newNumClasses = objc_getClassList(NULL, 0); - Class *classList = NULL; - - while (numClasses < newNumClasses) { - numClasses = newNumClasses; - classList = (Class*)realloc(classList, sizeof(Class) * numClasses); - newNumClasses = objc_getClassList(classList, numClasses); - } - - NSMutableArray *classesArray = [NSMutableArray array]; - - for (int i = 0; i < numClasses; i++) { - Class superClass = classList[i]; - do { - // recursively walk the inheritance hierarchy - superClass = class_getSuperclass(superClass); - if (superClass == filterType) { - [classesArray addObject:NSStringFromClass(classList[i])]; - break; - } - } while (superClass); - } - - free(classList); - - return classesArray; -} - -@end diff --git a/RileyLink/Storyboard.storyboard b/RileyLink/Storyboard.storyboard index 4fb4cc888..3b7ef7574 100644 --- a/RileyLink/Storyboard.storyboard +++ b/RileyLink/Storyboard.storyboard @@ -1,8 +1,11 @@ - - + + + + + - + @@ -14,7 +17,7 @@ - + @@ -30,7 +33,7 @@ - + @@ -59,7 +62,7 @@ - + @@ -67,15 +70,18 @@ - + - + + + @@ -93,18 +99,18 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.h b/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.h deleted file mode 100644 index c7ab484ef..000000000 --- a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// TPKeyboardAvoidingCollectionView.h -// TPKeyboardAvoiding -// -// Created by Michael Tyson on 30/09/2013. -// Copyright 2015 A Tasty Pixel & The CocoaBots. All rights reserved. -// - -#import -#import "UIScrollView+TPKeyboardAvoidingAdditions.h" - -@interface TPKeyboardAvoidingCollectionView : UICollectionView -@property (NS_NONATOMIC_IOSONLY, readonly) BOOL focusNextTextField; -- (void)scrollToActiveTextField; -@end diff --git a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.m b/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.m deleted file mode 100644 index 9eb205169..000000000 --- a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.m +++ /dev/null @@ -1,100 +0,0 @@ -// -// TPKeyboardAvoidingCollectionView.m -// TPKeyboardAvoiding -// -// Created by Michael Tyson on 30/09/2013. -// Copyright 2015 A Tasty Pixel & The CocoaBots. All rights reserved. -// - -#import "TPKeyboardAvoidingCollectionView.h" - -@interface TPKeyboardAvoidingCollectionView () -@end - -@implementation TPKeyboardAvoidingCollectionView - -#pragma mark - Setup/Teardown - -- (void)setup { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillShow:) name:UIKeyboardWillChangeFrameNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextViewTextDidBeginEditingNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextFieldTextDidBeginEditingNotification object:nil]; -} - --(instancetype)initWithFrame:(CGRect)frame { - if ( !(self = [super initWithFrame:frame]) ) return nil; - [self setup]; - return self; -} - -- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout { - if ( !(self = [super initWithFrame:frame collectionViewLayout:layout]) ) return nil; - [self setup]; - return self; -} - --(void)awakeFromNib { - [super awakeFromNib]; - - [self setup]; -} - --(void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -#if !__has_feature(objc_arc) - [super dealloc]; -#endif -} - --(void)setFrame:(CGRect)frame { - super.frame = frame; - [self TPKeyboardAvoiding_updateContentInset]; -} - --(void)setContentSize:(CGSize)contentSize { - if (CGSizeEqualToSize(contentSize, self.contentSize)) { - // Prevent triggering contentSize when it's already the same that - // cause weird infinte scrolling and locking bug - return; - } - super.contentSize = contentSize; - [self TPKeyboardAvoiding_updateContentInset]; -} - -- (BOOL)focusNextTextField { - return [self TPKeyboardAvoiding_focusNextTextField]; - -} -- (void)scrollToActiveTextField { - return [self TPKeyboardAvoiding_scrollToActiveTextField]; -} - -#pragma mark - Responders, events - --(void)willMoveToSuperview:(UIView *)newSuperview { - [super willMoveToSuperview:newSuperview]; - if ( !newSuperview ) { - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; - } -} - -- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { - [[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] resignFirstResponder]; - [super touchesEnded:touches withEvent:event]; -} - --(BOOL)textFieldShouldReturn:(UITextField *)textField { - if ( ![self focusNextTextField] ) { - [textField resignFirstResponder]; - } - return YES; -} - --(void)layoutSubviews { - [super layoutSubviews]; - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; - [self performSelector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) withObject:self afterDelay:0.1]; -} - -@end diff --git a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.h b/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.h deleted file mode 100755 index 2a8e73f18..000000000 --- a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// TPKeyboardAvoidingScrollView.h -// TPKeyboardAvoiding -// -// Created by Michael Tyson on 30/09/2013. -// Copyright 2015 A Tasty Pixel. All rights reserved. -// - -#import -#import "UIScrollView+TPKeyboardAvoidingAdditions.h" - -@interface TPKeyboardAvoidingScrollView : UIScrollView -- (void)contentSizeToFit; -@property (NS_NONATOMIC_IOSONLY, readonly) BOOL focusNextTextField; -- (void)scrollToActiveTextField; -@end diff --git a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.m b/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.m deleted file mode 100644 index 1bfd313f7..000000000 --- a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.m +++ /dev/null @@ -1,93 +0,0 @@ -// -// TPKeyboardAvoidingScrollView.m -// TPKeyboardAvoiding -// -// Created by Michael Tyson on 30/09/2013. -// Copyright 2015 A Tasty Pixel. All rights reserved. -// - -#import "TPKeyboardAvoidingScrollView.h" - -@interface TPKeyboardAvoidingScrollView () -@end - -@implementation TPKeyboardAvoidingScrollView - -#pragma mark - Setup/Teardown - -- (void)setup { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillShow:) name:UIKeyboardWillChangeFrameNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextViewTextDidBeginEditingNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextFieldTextDidBeginEditingNotification object:nil]; -} - --(instancetype)initWithFrame:(CGRect)frame { - if ( !(self = [super initWithFrame:frame]) ) return nil; - [self setup]; - return self; -} - --(void)awakeFromNib { - [super awakeFromNib]; - - [self setup]; -} - --(void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -#if !__has_feature(objc_arc) - [super dealloc]; -#endif -} - --(void)setFrame:(CGRect)frame { - super.frame = frame; - [self TPKeyboardAvoiding_updateContentInset]; -} - --(void)setContentSize:(CGSize)contentSize { - super.contentSize = contentSize; - [self TPKeyboardAvoiding_updateFromContentSizeChange]; -} - -- (void)contentSizeToFit { - self.contentSize = [self TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames]; -} - -- (BOOL)focusNextTextField { - return [self TPKeyboardAvoiding_focusNextTextField]; - -} -- (void)scrollToActiveTextField { - return [self TPKeyboardAvoiding_scrollToActiveTextField]; -} - -#pragma mark - Responders, events - --(void)willMoveToSuperview:(UIView *)newSuperview { - [super willMoveToSuperview:newSuperview]; - if ( !newSuperview ) { - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; - } -} - -- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { - [[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] resignFirstResponder]; - [super touchesEnded:touches withEvent:event]; -} - --(BOOL)textFieldShouldReturn:(UITextField *)textField { - if ( ![self focusNextTextField] ) { - [textField resignFirstResponder]; - } - return YES; -} - --(void)layoutSubviews { - [super layoutSubviews]; - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; - [self performSelector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) withObject:self afterDelay:0.1]; -} - -@end diff --git a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.h b/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.h deleted file mode 100644 index e63c8298d..000000000 --- a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// TPKeyboardAvoidingTableView.h -// TPKeyboardAvoiding -// -// Created by Michael Tyson on 30/09/2013. -// Copyright 2015 A Tasty Pixel. All rights reserved. -// - -#import -#import "UIScrollView+TPKeyboardAvoidingAdditions.h" - -@interface TPKeyboardAvoidingTableView : UITableView -@property (NS_NONATOMIC_IOSONLY, readonly) BOOL focusNextTextField; -- (void)scrollToActiveTextField; -@end diff --git a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.m b/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.m deleted file mode 100644 index 4aa766e8d..000000000 --- a/RileyLink/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.m +++ /dev/null @@ -1,118 +0,0 @@ -// -// TPKeyboardAvoidingTableView.m -// TPKeyboardAvoiding -// -// Created by Michael Tyson on 30/09/2013. -// Copyright 2015 A Tasty Pixel. All rights reserved. -// - -#import "TPKeyboardAvoidingTableView.h" - -@interface TPKeyboardAvoidingTableView () -@end - -@implementation TPKeyboardAvoidingTableView - -#pragma mark - Setup/Teardown - -- (void)setup { - if ( [self hasAutomaticKeyboardAvoidingBehaviour] ) return; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillShow:) name:UIKeyboardWillChangeFrameNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextViewTextDidBeginEditingNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextFieldTextDidBeginEditingNotification object:nil]; -} - --(instancetype)initWithFrame:(CGRect)frame { - if ( !(self = [super initWithFrame:frame]) ) return nil; - [self setup]; - return self; -} - --(instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)withStyle { - if ( !(self = [super initWithFrame:frame style:withStyle]) ) return nil; - [self setup]; - return self; -} - --(void)awakeFromNib { - [super awakeFromNib]; - - [self setup]; -} - --(void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -#if !__has_feature(objc_arc) - [super dealloc]; -#endif -} - --(BOOL)hasAutomaticKeyboardAvoidingBehaviour { - if ( [self.delegate isKindOfClass:[UITableViewController class]] ) { - // Theory: Apps built using the iOS 8.3 SDK (probably: older SDKs not tested) seem to handle keyboard - // avoiding automatically with UITableViewController. This doesn't seem to be documented anywhere - // by Apple, so results obtained only empirically. - return YES; - } - - return NO; -} - --(void)setFrame:(CGRect)frame { - super.frame = frame; - if ( [self hasAutomaticKeyboardAvoidingBehaviour] ) return; - [self TPKeyboardAvoiding_updateContentInset]; -} - --(void)setContentSize:(CGSize)contentSize { - if ( [self hasAutomaticKeyboardAvoidingBehaviour] ) { - super.contentSize = contentSize; - return; - } - if (CGSizeEqualToSize(contentSize, self.contentSize)) { - // Prevent triggering contentSize when it's already the same - // this cause table view to scroll to top on contentInset changes - return; - } - super.contentSize = contentSize; - [self TPKeyboardAvoiding_updateContentInset]; -} - -- (BOOL)focusNextTextField { - return [self TPKeyboardAvoiding_focusNextTextField]; - -} -- (void)scrollToActiveTextField { - return [self TPKeyboardAvoiding_scrollToActiveTextField]; -} - -#pragma mark - Responders, events - --(void)willMoveToSuperview:(UIView *)newSuperview { - [super willMoveToSuperview:newSuperview]; - if ( !newSuperview ) { - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; - } -} - -- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { - [[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] resignFirstResponder]; - [super touchesEnded:touches withEvent:event]; -} - --(BOOL)textFieldShouldReturn:(UITextField *)textField { - if ( ![self focusNextTextField] ) { - [textField resignFirstResponder]; - } - return YES; -} - --(void)layoutSubviews { - [super layoutSubviews]; - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self]; - [self performSelector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) withObject:self afterDelay:0.1]; -} - -@end diff --git a/RileyLink/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.h b/RileyLink/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.h deleted file mode 100644 index 2e6f87b42..000000000 --- a/RileyLink/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// UIScrollView+TPKeyboardAvoidingAdditions.h -// TPKeyboardAvoiding -// -// Created by Michael Tyson on 30/09/2013. -// Copyright 2015 A Tasty Pixel. All rights reserved. -// - -#import - -@interface UIScrollView (TPKeyboardAvoidingAdditions) -@property (NS_NONATOMIC_IOSONLY, readonly) BOOL TPKeyboardAvoiding_focusNextTextField; -- (void)TPKeyboardAvoiding_scrollToActiveTextField; - -- (void)TPKeyboardAvoiding_keyboardWillShow:(NSNotification*)notification; -- (void)TPKeyboardAvoiding_keyboardWillHide:(NSNotification*)notification; -- (void)TPKeyboardAvoiding_updateContentInset; -- (void)TPKeyboardAvoiding_updateFromContentSizeChange; -- (void)TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:(UIView*)view; -- (UIView*)TPKeyboardAvoiding_findFirstResponderBeneathView:(UIView*)view; -@property (NS_NONATOMIC_IOSONLY, readonly) CGSize TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames; -@end diff --git a/RileyLink/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m b/RileyLink/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m deleted file mode 100644 index e3cfadb76..000000000 --- a/RileyLink/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m +++ /dev/null @@ -1,335 +0,0 @@ -// -// UIScrollView+TPKeyboardAvoidingAdditions.m -// TPKeyboardAvoiding -// -// Created by Michael Tyson on 30/09/2013. -// Copyright 2015 A Tasty Pixel. All rights reserved. -// - -#import "UIScrollView+TPKeyboardAvoidingAdditions.h" -#import "TPKeyboardAvoidingScrollView.h" -#import - -static const CGFloat kCalculatedContentPadding = 10; -static const CGFloat kMinimumScrollOffsetPadding = 20; - -static const int kStateKey; - -#define _UIKeyboardFrameEndUserInfoKey (&UIKeyboardFrameEndUserInfoKey != NULL ? UIKeyboardFrameEndUserInfoKey : @"UIKeyboardBoundsUserInfoKey") - -@interface TPKeyboardAvoidingState : NSObject -@property (nonatomic, assign) UIEdgeInsets priorInset; -@property (nonatomic, assign) UIEdgeInsets priorScrollIndicatorInsets; -@property (nonatomic, assign) BOOL keyboardVisible; -@property (nonatomic, assign) CGRect keyboardRect; -@property (nonatomic, assign) CGSize priorContentSize; - - -@property (nonatomic) BOOL priorPagingEnabled; -@end - -@implementation UIScrollView (TPKeyboardAvoidingAdditions) - -- (TPKeyboardAvoidingState*)keyboardAvoidingState { - TPKeyboardAvoidingState *state = objc_getAssociatedObject(self, &kStateKey); - if ( !state ) { - state = [[TPKeyboardAvoidingState alloc] init]; - objc_setAssociatedObject(self, &kStateKey, state, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -#if !__has_feature(objc_arc) - [state release]; -#endif - } - return state; -} - -- (void)TPKeyboardAvoiding_keyboardWillShow:(NSNotification*)notification { - CGRect keyboardRect = [self convertRect:[notification.userInfo[_UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil]; - if (CGRectIsEmpty(keyboardRect)) { - return; - } - - TPKeyboardAvoidingState *state = self.keyboardAvoidingState; - - UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self]; - - if ( !firstResponder ) { - return; - } - - state.keyboardRect = keyboardRect; - - if ( !state.keyboardVisible ) { - state.priorInset = self.contentInset; - state.priorScrollIndicatorInsets = self.scrollIndicatorInsets; - state.priorPagingEnabled = self.pagingEnabled; - } - - state.keyboardVisible = YES; - self.pagingEnabled = NO; - - if ( [self isKindOfClass:[TPKeyboardAvoidingScrollView class]] ) { - state.priorContentSize = self.contentSize; - - if ( CGSizeEqualToSize(self.contentSize, CGSizeZero) ) { - // Set the content size, if it's not set. Do not set content size explicitly if auto-layout - // is being used to manage subviews - self.contentSize = [self TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames]; - } - } - - // Shrink view's inset by the keyboard's height, and scroll to show the text field/view being edited - [UIView beginAnimations:nil context:NULL]; - [UIView setAnimationCurve:[notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue]]; - [UIView setAnimationDuration:[notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] floatValue]]; - - self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard]; - - CGFloat viewableHeight = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom; - [self setContentOffset:CGPointMake(self.contentOffset.x, - [self TPKeyboardAvoiding_idealOffsetForView:firstResponder - withViewingAreaHeight:viewableHeight]) - animated:NO]; - - self.scrollIndicatorInsets = self.contentInset; - [self layoutIfNeeded]; - - [UIView commitAnimations]; -} - -- (void)TPKeyboardAvoiding_keyboardWillHide:(NSNotification*)notification { - CGRect keyboardRect = [self convertRect:[notification.userInfo[_UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil]; - if (CGRectIsEmpty(keyboardRect)) { - return; - } - - TPKeyboardAvoidingState *state = self.keyboardAvoidingState; - - if ( !state.keyboardVisible ) { - return; - } - - state.keyboardRect = CGRectZero; - state.keyboardVisible = NO; - - // Restore dimensions to prior size - [UIView beginAnimations:nil context:NULL]; - [UIView setAnimationCurve:[notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue]]; - [UIView setAnimationDuration:[notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] floatValue]]; - - if ( [self isKindOfClass:[TPKeyboardAvoidingScrollView class]] ) { - self.contentSize = state.priorContentSize; - } - - self.contentInset = state.priorInset; - self.scrollIndicatorInsets = state.priorScrollIndicatorInsets; - self.pagingEnabled = state.priorPagingEnabled; - [self layoutIfNeeded]; - [UIView commitAnimations]; -} - -- (void)TPKeyboardAvoiding_updateContentInset { - TPKeyboardAvoidingState *state = self.keyboardAvoidingState; - if ( state.keyboardVisible ) { - self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard]; - } -} - -- (void)TPKeyboardAvoiding_updateFromContentSizeChange { - TPKeyboardAvoidingState *state = self.keyboardAvoidingState; - if ( state.keyboardVisible ) { - state.priorContentSize = self.contentSize; - self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard]; - } -} - -#pragma mark - Utilities - -- (BOOL)TPKeyboardAvoiding_focusNextTextField { - UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self]; - if ( !firstResponder ) { - return NO; - } - - UIView *view = [self TPKeyboardAvoiding_findNextInputViewAfterView:firstResponder beneathView:self]; - - if ( view ) { - [view performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0.0]; - return YES; - } - - return NO; -} - --(void)TPKeyboardAvoiding_scrollToActiveTextField { - TPKeyboardAvoidingState *state = self.keyboardAvoidingState; - - if ( !state.keyboardVisible ) return; - - CGFloat visibleSpace = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom; - - CGPoint idealOffset = CGPointMake(0, [self TPKeyboardAvoiding_idealOffsetForView:[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] - withViewingAreaHeight:visibleSpace]); - - // Ordinarily we'd use -setContentOffset:animated:YES here, but it interferes with UIScrollView - // behavior which automatically ensures that the first responder is within its bounds - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self setContentOffset:idealOffset animated:YES]; - }); -} - -#pragma mark - Helpers - -- (UIView*)TPKeyboardAvoiding_findFirstResponderBeneathView:(UIView*)view { - // Search recursively for first responder - for ( UIView *childView in view.subviews ) { - if ( [childView respondsToSelector:@selector(isFirstResponder)] && [childView isFirstResponder] ) return childView; - UIView *result = [self TPKeyboardAvoiding_findFirstResponderBeneathView:childView]; - if ( result ) return result; - } - return nil; -} - -- (UIView*)TPKeyboardAvoiding_findNextInputViewAfterView:(UIView*)priorView beneathView:(UIView*)view { - UIView * candidate = nil; - [self TPKeyboardAvoiding_findNextInputViewAfterView:priorView beneathView:view bestCandidate:&candidate]; - return candidate; -} - -- (void)TPKeyboardAvoiding_findNextInputViewAfterView:(UIView*)priorView beneathView:(UIView*)view bestCandidate:(UIView**)bestCandidate { - // Search recursively for input view below/to right of priorTextField - CGRect priorFrame = [self convertRect:priorView.frame fromView:priorView.superview]; - CGRect candidateFrame = *bestCandidate ? [self convertRect:(*bestCandidate).frame fromView:(*bestCandidate).superview] : CGRectZero; - CGFloat bestCandidateHeuristic = [self TPKeyboardAvoiding_nextInputViewHeuristicForViewFrame:candidateFrame]; - - for ( UIView *childView in view.subviews ) { - if ( [self TPKeyboardAvoiding_viewIsValidKeyViewCandidate:childView] ) { - CGRect frame = [self convertRect:childView.frame fromView:view]; - - // Use a heuristic to evaluate candidates - CGFloat heuristic = [self TPKeyboardAvoiding_nextInputViewHeuristicForViewFrame:frame]; - - // Find views beneath, or to the right. For those views that match, choose the view closest to the top left - if ( childView != priorView - && ((fabs(CGRectGetMinY(frame) - CGRectGetMinY(priorFrame)) < FLT_EPSILON && CGRectGetMinX(frame) > CGRectGetMinX(priorFrame)) - || CGRectGetMinY(frame) > CGRectGetMinY(priorFrame)) - && (!*bestCandidate || heuristic > bestCandidateHeuristic) ) { - - *bestCandidate = childView; - bestCandidateHeuristic = heuristic; - } - } else { - [self TPKeyboardAvoiding_findNextInputViewAfterView:priorView beneathView:childView bestCandidate:bestCandidate]; - } - } -} - -- (CGFloat)TPKeyboardAvoiding_nextInputViewHeuristicForViewFrame:(CGRect)frame { - return (-frame.origin.y * 1000.0) // Prefer elements closest to top (most important) - + (-frame.origin.x); // Prefer elements closest to left -} - -- (BOOL)TPKeyboardAvoiding_viewIsValidKeyViewCandidate:(UIView *)view { - if ( view.hidden || !view.userInteractionEnabled ) return NO; - - if ( [view isKindOfClass:[UITextField class]] && ((UITextField*)view).enabled ) { - return YES; - } - - if ( [view isKindOfClass:[UITextView class]] && ((UITextView*)view).isEditable ) { - return YES; - } - - return NO; -} - -- (void)TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:(UIView*)view { - for ( UIView *childView in view.subviews ) { - if ( ([childView isKindOfClass:[UITextField class]] || [childView isKindOfClass:[UITextView class]]) ) { - [self TPKeyboardAvoiding_initializeView:childView]; - } else { - [self TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:childView]; - } - } -} - --(CGSize)TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames { - - BOOL wasShowingVerticalScrollIndicator = self.showsVerticalScrollIndicator; - BOOL wasShowingHorizontalScrollIndicator = self.showsHorizontalScrollIndicator; - - self.showsVerticalScrollIndicator = NO; - self.showsHorizontalScrollIndicator = NO; - - CGRect rect = CGRectZero; - for ( UIView *view in self.subviews ) { - rect = CGRectUnion(rect, view.frame); - } - rect.size.height += kCalculatedContentPadding; - - self.showsVerticalScrollIndicator = wasShowingVerticalScrollIndicator; - self.showsHorizontalScrollIndicator = wasShowingHorizontalScrollIndicator; - - return rect.size; -} - - -- (UIEdgeInsets)TPKeyboardAvoiding_contentInsetForKeyboard { - TPKeyboardAvoidingState *state = self.keyboardAvoidingState; - UIEdgeInsets newInset = self.contentInset; - CGRect keyboardRect = state.keyboardRect; - newInset.bottom = keyboardRect.size.height - MAX((CGRectGetMaxY(keyboardRect) - CGRectGetMaxY(self.bounds)), 0); - return newInset; -} - --(CGFloat)TPKeyboardAvoiding_idealOffsetForView:(UIView *)view withViewingAreaHeight:(CGFloat)viewAreaHeight { - CGSize contentSize = self.contentSize; - CGFloat offset = 0.0; - - CGRect subviewRect = [view convertRect:view.bounds toView:self]; - - // Attempt to center the subview in the visible space, but if that means there will be less than kMinimumScrollOffsetPadding - // pixels above the view, then substitute kMinimumScrollOffsetPadding - CGFloat padding = (viewAreaHeight - subviewRect.size.height) / 2; - if ( padding < kMinimumScrollOffsetPadding ) { - padding = kMinimumScrollOffsetPadding; - } - - // Ideal offset places the subview rectangle origin "padding" points from the top of the scrollview. - // If there is a top contentInset, also compensate for this so that subviewRect will not be placed under - // things like navigation bars. - offset = subviewRect.origin.y - padding - self.contentInset.top; - - // Constrain the new contentOffset so we can't scroll past the bottom. Note that we don't take the bottom - // inset into account, as this is manipulated to make space for the keyboard. - if ( offset > (contentSize.height - viewAreaHeight) ) { - offset = contentSize.height - viewAreaHeight; - } - - // Constrain the new contentOffset so we can't scroll past the top, taking contentInsets into account - if ( offset < -self.contentInset.top ) { - offset = -self.contentInset.top; - } - - return offset; -} - -- (void)TPKeyboardAvoiding_initializeView:(UIView*)view { - if ( [view isKindOfClass:[UITextField class]] - && ((UITextField*)view).returnKeyType == UIReturnKeyDefault - && (!((UITextField*)view).delegate || ((UITextField*)view).delegate == (id)self) ) { - ((UITextField*)view).delegate = (id)self; - UIView *otherView = [self TPKeyboardAvoiding_findNextInputViewAfterView:view beneathView:self]; - - if ( otherView ) { - ((UITextField*)view).returnKeyType = UIReturnKeyNext; - } else { - ((UITextField*)view).returnKeyType = UIReturnKeyDone; - } - } -} - -@end - - -@implementation TPKeyboardAvoidingState -@end diff --git a/RileyLink/View Controllers/RadioSelectionTableViewController.swift b/RileyLink/View Controllers/RadioSelectionTableViewController.swift index 57377afa4..165c5c655 100644 --- a/RileyLink/View Controllers/RadioSelectionTableViewController.swift +++ b/RileyLink/View Controllers/RadioSelectionTableViewController.swift @@ -75,11 +75,11 @@ class RadioSelectionTableViewController: UITableViewController, IdentifiableClas extension RadioSelectionTableViewController { typealias T = RadioSelectionTableViewController - static func pumpRegion(_ value: PumpRegion) -> T { + static func pumpRegion(_ value: PumpRegion?) -> T { let vc = T() - vc.selectedIndex = value.rawValue - vc.options = (0..<2).flatMap({ PumpRegion(rawValue: $0) }).map { String(describing: $0) } + vc.selectedIndex = value?.rawValue + vc.options = (0..<2).compactMap({ PumpRegion(rawValue: $0) }).map { String(describing: $0) } vc.contextHelp = NSLocalizedString("Pump Region is listed on the back of your pump as two of the last three characters of the model string, which reads something like this: MMT-551NAB, or MMT-515LWWS. If your model has an \"NA\" in it, then the region is NorthAmerica. If your model has an \"WW\" in it, then the region is WorldWide.", comment: "Instructions on selecting the pump region") return vc } diff --git a/RileyLink/View Controllers/RileyLinkListTableViewController.swift b/RileyLink/View Controllers/RileyLinkListTableViewController.swift index 5bee6f9a3..3b50abac2 100644 --- a/RileyLink/View Controllers/RileyLinkListTableViewController.swift +++ b/RileyLink/View Controllers/RileyLinkListTableViewController.swift @@ -7,64 +7,109 @@ // import UIKit +import RileyLinkBLEKit import RileyLinkKit +import RileyLinkKitUI + class RileyLinkListTableViewController: UITableViewController { - - // Retreive the managedObjectContext from AppDelegate - let managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext + + private lazy var numberFormatter = NumberFormatter() override func viewDidLoad() { - super.viewDidLoad() + super.viewDidLoad() + + tableView.register(RileyLinkDeviceTableViewCell.self, forCellReuseIdentifier: RileyLinkDeviceTableViewCell.className) + + // Register for manager notifications + NotificationCenter.default.addObserver(self, selector: #selector(reloadDevices), name: .ManagerDevicesDidChange, object: dataManager.rileyLinkManager) + + // Register for device notifications + for name in [.DeviceConnectionStateDidChange, .DeviceRSSIDidChange, .DeviceNameDidChange] as [Notification.Name] { + NotificationCenter.default.addObserver(self, selector: #selector(deviceDidUpdate(_:)), name: name, object: nil) + } - tableView.register(RileyLinkDeviceTableViewCell.nib(), forCellReuseIdentifier: RileyLinkDeviceTableViewCell.className) + reloadDevices() + } - dataManagerObserver = NotificationCenter.default.addObserver(forName: nil, object: dataManager, queue: nil) { [weak self = self] (note) -> Void in + @objc private func reloadDevices() { + self.dataManager.rileyLinkManager.getDevices { (devices) in DispatchQueue.main.async { - if let deviceManager = self?.dataManager.rileyLinkManager { - switch note.name { - case Notification.Name.DeviceManagerDidDiscoverDevice: - self?.tableView.insertRows(at: [IndexPath(row: deviceManager.devices.count - 1, section: 0)], with: .automatic) - case Notification.Name.DeviceConnectionStateDidChange, - Notification.Name.DeviceRSSIDidChange, - Notification.Name.DeviceNameDidChange: - if let device = note.userInfo?[RileyLinkDeviceManager.RileyLinkDeviceKey] as? RileyLinkDevice, let index = deviceManager.devices.index(where: { $0 === device }) { - self?.tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .none) - } - default: - break - } - } + self.devices = devices } } } - - deinit { - dataManagerObserver = nil - } - - private var dataManagerObserver: Any? { - willSet { - if let observer = dataManagerObserver { - NotificationCenter.default.removeObserver(observer) + + @objc private func deviceDidUpdate(_ note: Notification) { + DispatchQueue.main.async { + if let device = note.object as? RileyLinkDevice, let index = self.devices.index(where: { $0 === device }) { + if let rssi = note.userInfo?[RileyLinkDevice.notificationRSSIKey] as? Int { + self.deviceRSSI[device.peripheralIdentifier] = rssi + } + + if let cell = self.tableView.cellForRow(at: IndexPath(row: index, section: 0)) as? RileyLinkDeviceTableViewCell { + cell.configureCellWithName(device.name, + signal: self.numberFormatter.decibleString(from: self.deviceRSSI[device.peripheralIdentifier]), + peripheralState: device.peripheralState + ) + } } } } - + private var dataManager: DeviceDataManager { return DeviceDataManager.sharedManager } - + + private var devices: [RileyLinkDevice] = [] { + didSet { + // Assume only appends are possible when count changes for algorithmic simplicity + guard oldValue.count < devices.count else { + tableView.reloadSections(IndexSet(integer: 0), with: .fade) + return + } + + tableView.beginUpdates() + + let insertedPaths = (oldValue.count.. IndexPath in + return IndexPath(row: index, section: 0) + } + tableView.insertRows(at: insertedPaths, with: .automatic) + + tableView.endUpdates() + } + } + + private var deviceRSSI: [UUID: Int] = [:] + + var rssiFetchTimer: Timer? { + willSet { + rssiFetchTimer?.invalidate() + } + } + + @objc func updateRSSI() { + for device in devices { + device.readRSSI() + } + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - dataManager.rileyLinkManager.setDeviceScanningEnabled(true) + dataManager.rileyLinkManager.setScanningEnabled(true) + + rssiFetchTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(updateRSSI), userInfo: nil, repeats: true) + + updateRSSI() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - dataManager.rileyLinkManager.setDeviceScanningEnabled(false) + dataManager.rileyLinkManager.setScanningEnabled(false) + + rssiFetchTimer = nil } // MARK: Table view data source @@ -74,7 +119,7 @@ class RileyLinkListTableViewController: UITableViewController { } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return dataManager.rileyLinkManager.devices.count + return devices.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -82,25 +127,25 @@ class RileyLinkListTableViewController: UITableViewController { let deviceCell = tableView.dequeueReusableCell(withIdentifier: RileyLinkDeviceTableViewCell.className) as! RileyLinkDeviceTableViewCell - let device = dataManager.rileyLinkManager.devices[indexPath.row] + let device = devices[indexPath.row] - deviceCell.configureCellWithName(device.name, - signal: device.RSSI, - peripheralState: device.peripheral.state + deviceCell.configureCellWithName( + device.name, + signal: numberFormatter.decibleString(from: deviceRSSI[device.peripheralIdentifier]), + peripheralState: device.peripheralState ) - deviceCell.connectSwitch.addTarget(self, action: #selector(deviceConnectionChanged(_:)), for: .valueChanged) + deviceCell.connectSwitch?.addTarget(self, action: #selector(changeDeviceConnection(_:)), for: .valueChanged) cell = deviceCell return cell } - @objc func deviceConnectionChanged(_ connectSwitch: UISwitch) { + @objc func changeDeviceConnection(_ connectSwitch: UISwitch) { let switchOrigin = connectSwitch.convert(CGPoint.zero, to: tableView) - if let indexPath = tableView.indexPathForRow(at: switchOrigin) - { - let device = dataManager.rileyLinkManager.devices[indexPath.row] + if let indexPath = tableView.indexPathForRow(at: switchOrigin) { + let device = devices[indexPath.row] if connectSwitch.isOn { dataManager.connectToRileyLink(device) @@ -113,45 +158,15 @@ class RileyLinkListTableViewController: UITableViewController { // MARK: - UITableViewDelegate override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let vc = RileyLinkDeviceTableViewController() - - vc.device = dataManager.rileyLinkManager.devices[indexPath.row] + let device = devices[indexPath.row] + let vc = RileyLinkDeviceTableViewController( + device: device, + deviceState: dataManager.deviceStates[device.peripheralIdentifier, default: DeviceState()], + pumpSettings: dataManager.pumpSettings, + pumpState: dataManager.pumpState, + pumpOps: dataManager.pumpOps + ) show(vc, sender: indexPath) } - - /* - // Override to support conditional editing of the table view. - - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { - // Return NO if you do not want the specified item to be editable. - return YES; - } - */ - - /* - // Override to support editing the table view. - - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { - if (editingStyle == UITableViewCellEditingStyleDelete) { - // Delete the row from the data source - [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; - } else if (editingStyle == UITableViewCellEditingStyleInsert) { - // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view - } - } - */ - - /* - // Override to support rearranging the table view. - - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { - } - */ - - /* - // Override to support conditional rearranging of the table view. - - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { - // Return NO if you do not want the item to be re-orderable. - return YES; - } - */ - } diff --git a/RileyLink/View Controllers/SettingsTableViewController.swift b/RileyLink/View Controllers/SettingsTableViewController.swift index 201c39592..d858c884e 100644 --- a/RileyLink/View Controllers/SettingsTableViewController.swift +++ b/RileyLink/View Controllers/SettingsTableViewController.swift @@ -7,13 +7,18 @@ // import UIKit -import RileyLinkKit import MinimedKit +import RileyLinkKit +import RileyLinkKitUI private let ConfigCellIdentifier = "ConfigTableViewCell" private let TapToSetString = NSLocalizedString("Tap to set", comment: "The empty-state text for a configuration value") + +extension TextFieldTableViewController: IdentifiableClass { } + + class SettingsTableViewController: UITableViewController, TextFieldTableViewControllerDelegate { fileprivate enum Section: Int { @@ -96,12 +101,18 @@ class SettingsTableViewController: UITableViewController, TextFieldTableViewCont case .pumpID: let configCell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath) configCell.textLabel?.text = NSLocalizedString("Pump ID", comment: "The title text for the pump ID config value") - configCell.detailTextLabel?.text = DeviceDataManager.sharedManager.pumpID ?? TapToSetString + configCell.detailTextLabel?.text = dataManager.pumpSettings?.pumpID ?? TapToSetString cell = configCell case .pumpRegion: let configCell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath) configCell.textLabel?.text = NSLocalizedString("Pump Region", comment: "The title text for the pump Region config value") - configCell.detailTextLabel?.text = String(describing: DeviceDataManager.sharedManager.pumpRegion) + + if let pumpRegion = dataManager.pumpSettings?.pumpRegion { + configCell.detailTextLabel?.text = String(describing: pumpRegion) + } else { + configCell.detailTextLabel?.text = nil + } + cell = configCell case .nightscout: let configCell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath) @@ -142,9 +153,20 @@ class SettingsTableViewController: UITableViewController, TextFieldTableViewCont switch ConfigurationRow(rawValue: indexPath.row)! { case .pumpID: - performSegue(withIdentifier: TextFieldTableViewController.className, sender: sender) + let vc = TextFieldTableViewController() + + vc.placeholder = NSLocalizedString("Enter the 6-digit pump ID", comment: "The placeholder text instructing users how to enter a pump ID") + vc.value = dataManager.pumpSettings?.pumpID + + if let cell = tableView.cellForRow(at: indexPath) { + vc.title = cell.textLabel?.text + } + vc.indexPath = indexPath + vc.delegate = self + + show(vc, sender: indexPath) case .pumpRegion: - let vc = RadioSelectionTableViewController.pumpRegion(dataManager.pumpRegion) + let vc = RadioSelectionTableViewController.pumpRegion(dataManager.pumpSettings?.pumpRegion) vc.title = sender?.textLabel?.text vc.delegate = self @@ -174,32 +196,6 @@ class SettingsTableViewController: UITableViewController, TextFieldTableViewCont } } - // MARK: - Navigation - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if let - cell = sender as? UITableViewCell, - let indexPath = tableView.indexPath(for: cell) - { - switch segue.destination { - case let vc as TextFieldTableViewController: - switch ConfigurationRow(rawValue: indexPath.row)! { - case .pumpID: - vc.placeholder = NSLocalizedString("Enter the 6-digit pump ID", comment: "The placeholder text instructing users how to enter a pump ID") - vc.value = DeviceDataManager.sharedManager.pumpID - default: - break - } - - vc.title = cell.textLabel?.text - vc.indexPath = indexPath - vc.delegate = self - default: - break - } - } - } - // MARK: - Uploader management @objc func uploadEnabledChanged(_ sender: UISwitch) { @@ -218,7 +214,7 @@ class SettingsTableViewController: UITableViewController, TextFieldTableViewCont if let indexPath = controller.indexPath { switch ConfigurationRow(rawValue: indexPath.row)! { case .pumpID: - DeviceDataManager.sharedManager.pumpID = controller.value + dataManager.setPumpID(controller.value) default: break } @@ -226,13 +222,17 @@ class SettingsTableViewController: UITableViewController, TextFieldTableViewCont tableView.reloadData() } + + func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) { + + } } extension SettingsTableViewController: RadioSelectionTableViewControllerDelegate { func radioSelectionTableViewControllerDidChangeSelectedIndex(_ controller: RadioSelectionTableViewController) { if let selectedIndex = controller.selectedIndex, let pumpRegion = PumpRegion(rawValue: selectedIndex) { - dataManager.pumpRegion = pumpRegion + dataManager.setPumpRegion(pumpRegion) tableView.reloadRows(at: [IndexPath(row: ConfigurationRow.pumpRegion.rawValue, section: Section.configuration.rawValue)], with: .none) } diff --git a/RileyLink/View Controllers/TextFieldTableViewController.swift b/RileyLink/View Controllers/TextFieldTableViewController.swift deleted file mode 100644 index 2c4803256..000000000 --- a/RileyLink/View Controllers/TextFieldTableViewController.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// TextFieldTableViewController.swift -// Naterade -// -// Created by Nathan Racklyeft on 8/30/15. -// Copyright © 2015 Nathan Racklyeft. All rights reserved. -// - -import UIKit -import RileyLinkKit - -protocol TextFieldTableViewControllerDelegate: class { - func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) -} - - -class TextFieldTableViewController: UITableViewController, IdentifiableClass, UITextFieldDelegate { - - @IBOutlet weak var textField: UITextField! - - var indexPath: IndexPath? - - var placeholder: String? - - var value: String? { - didSet { - delegate?.textFieldTableViewControllerDidEndEditing(self) - } - } - - var keyboardType = UIKeyboardType.default - var autocapitalizationType = UITextAutocapitalizationType.none - - weak var delegate: TextFieldTableViewControllerDelegate? - - override func viewDidLoad() { - super.viewDidLoad() - - textField.text = value - textField.keyboardType = keyboardType - textField.placeholder = placeholder - textField.autocapitalizationType = autocapitalizationType - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - textField.becomeFirstResponder() - } - - // MARK: - UITextFieldDelegate - - func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { - value = textField.text - - return true - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - value = textField.text - - textField.delegate = nil - - _ = navigationController?.popViewController(animated: true) - - return false - } -} diff --git a/RileyLink/Views/PacketTableViewCell.h b/RileyLink/Views/PacketTableViewCell.h deleted file mode 100644 index 3b35129ff..000000000 --- a/RileyLink/Views/PacketTableViewCell.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// PacketTableViewCell.h -// RileyLink -// -// Created by Pete Schwamb on 7/30/15. -// Copyright (c) 2015 Pete Schwamb. All rights reserved. -// - -#import -#import "RFPacket.h" - -@interface PacketTableViewCell : UITableViewCell - -@property (nonatomic, strong) RFPacket *packet; - -@end diff --git a/RileyLink/Views/PacketTableViewCell.m b/RileyLink/Views/PacketTableViewCell.m deleted file mode 100644 index fa9acbd2d..000000000 --- a/RileyLink/Views/PacketTableViewCell.m +++ /dev/null @@ -1,58 +0,0 @@ -// -// PacketTableViewCell.m -// RileyLink -// -// Created by Pete Schwamb on 7/30/15. -// Copyright (c) 2015 Pete Schwamb. All rights reserved. -// - -#import "PacketTableViewCell.h" -#import "MinimedKit.h" -#import "NSData+Conversion.h" - -static NSDateFormatter *dateFormatter; -static NSDateFormatter *timeFormatter; - -@interface PacketTableViewCell () { - IBOutlet UILabel *rawDataLabel; - IBOutlet UILabel *dateLabel; - IBOutlet UILabel *timeLabel; - IBOutlet UILabel *rssiLabel; - IBOutlet UILabel *packetNumberLabel; -} - -@end - -@implementation PacketTableViewCell - -+ (void)initialize { - dateFormatter = [[NSDateFormatter alloc] init]; - dateFormatter.locale = [NSLocale currentLocale]; - dateFormatter.dateStyle = NSDateFormatterShortStyle; - timeFormatter = [[NSDateFormatter alloc] init]; - timeFormatter.locale = [NSLocale currentLocale]; - timeFormatter.timeStyle = NSDateFormatterShortStyle; -} - -- (void)awakeFromNib { - [super awakeFromNib]; - // Initialization code -} - -- (void)setPacket:(RFPacket *)packet { - _packet = packet; - - rawDataLabel.text = packet.data.hexadecimalString; - dateLabel.text = [dateFormatter stringFromDate:packet.capturedAt]; - timeLabel.text = [timeFormatter stringFromDate:packet.capturedAt]; - rssiLabel.text = [NSString stringWithFormat:@"%d", packet.rssi]; - packetNumberLabel.text = [NSString stringWithFormat:@"#%d", packet.packetNumber]; -} - -- (void)setSelected:(BOOL)selected animated:(BOOL)animated { - [super setSelected:selected animated:animated]; - - // Configure the view for the selected state -} - -@end diff --git a/RileyLink/Views/RileyLinkDeviceTableViewCell.swift b/RileyLink/Views/RileyLinkDeviceTableViewCell.swift index 61ba2dc13..b296c587d 100644 --- a/RileyLink/Views/RileyLinkDeviceTableViewCell.swift +++ b/RileyLink/Views/RileyLinkDeviceTableViewCell.swift @@ -7,6 +7,6 @@ // import UIKit -import RileyLinkKit +import RileyLinkKitUI extension RileyLinkDeviceTableViewCell: IdentifiableClass { } diff --git a/RileyLink/es.lproj/InfoPlist.strings b/RileyLink/es.lproj/InfoPlist.strings new file mode 100644 index 000000000..452bfea68 --- /dev/null +++ b/RileyLink/es.lproj/InfoPlist.strings @@ -0,0 +1,6 @@ +/* (No Comment) */ +"CFBundleDisplayName" = "${PRODUCT_NAME}"; + +/* (No Comment) */ +"CFBundleName" = "${PRODUCT_NAME}"; + diff --git a/RileyLink/es.lproj/Localizable.strings b/RileyLink/es.lproj/Localizable.strings new file mode 100644 index 000000000..25639514c --- /dev/null +++ b/RileyLink/es.lproj/Localizable.strings @@ -0,0 +1,51 @@ +/* The title of the nightscout API secret credential */ +"API Secret" = "Secreto API"; + +/* The title of the about section */ +"About" = "Respecto a"; + +/* The title of the button to add the credentials for a service */ +"Add Account" = "Agregar Cuenta"; + +/* The title of the configuration section in settings */ +"Configuration" = "Configuración"; + +/* The title of the button to remove the credentials for a service */ +"Delete Account" = "Eliminar Cuenta"; + +/* The placeholder text instructing users how to enter a pump ID */ +"Enter the 6-digit pump ID" = "Ingrese ID de 6 dígitios de la microinfusora"; + +/* The title text for the pull cgm Data cell */ +"Fetch CGM" = "Obtener de CGM"; + +/* The title of the Nightscout service */ +"Nightscout" = "Nightscout"; + +/* The title text for the pump ID config value */ +"Pump ID" = "ID de Microinfusora"; + +/* The title text for the pump Region config value */ +"Pump Region" = "Región de Microinfusora"; + +/* Instructions on selecting the pump region */ +"Pump Region is listed on the back of your pump as two of the last three characters of the model string, which reads something like this: MMT-551NAB, or MMT-515LWWS. If your model has an \"NA\" in it, then the region is NorthAmerica. If your model has an \"WW\" in it, then the region is WorldWide." = "La región de la microinfusora puede ser encontrada impresa en la parte trasera como parte del código de modelo ( REF ), por ejemplo MMT-551AB o MMT-515LWWS. Si el código de modelo contiene \"NA\" o \"CA\", la región es Norte América. Si contiene \"WW\" la región es Mundial."; + +/* The default placeholder string for a credential */ +"Required" = "Requerido"; + +/* The title of the nightscout site URL credential */ +"Site URL" = "URL de sitio"; + +/* The empty-state text for a configuration value */ +"Tap to set" = "Toca para definir"; + +/* The title text for the nightscout upload enabled switch cell */ +"Upload To Nightscout" = "Subir a Nightscout"; + +/* Label indicating validation is occurring */ +"Verifying" = "verificando"; + +/* The placeholder text for the nightscout site URL credential */ +"http://mysite.azurewebsites.net" = "http://mysite.azurewebsites.net"; + diff --git a/RileyLink/es.lproj/LoopKit.strings b/RileyLink/es.lproj/LoopKit.strings new file mode 100644 index 000000000..bc1fda9fb --- /dev/null +++ b/RileyLink/es.lproj/LoopKit.strings @@ -0,0 +1,3 @@ +/* The title of the action used to dismiss an error alert */ +"com.loudnate.LoopKit.errorAlertActionTitle" = "OK"; + diff --git a/RileyLink/ru.lproj/InfoPlist.strings b/RileyLink/ru.lproj/InfoPlist.strings new file mode 100644 index 000000000..874e8a453 --- /dev/null +++ b/RileyLink/ru.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* No Localized Strings */ diff --git a/RileyLink/ru.lproj/Localizable.strings b/RileyLink/ru.lproj/Localizable.strings new file mode 100644 index 000000000..26b71b186 --- /dev/null +++ b/RileyLink/ru.lproj/Localizable.strings @@ -0,0 +1,51 @@ +/* The title of the nightscout API secret credential */ +"API Secret" = "API secret"; + +/* The title of the about section */ +"About" = "Про"; + +/* The title of the button to add the credentials for a service */ +"Add Account" = "Добавить учетную запись"; + +/* The title of the configuration section in settings */ +"Configuration" = "Конфигурация"; + +/* The title of the button to remove the credentials for a service */ +"Delete Account" = "Удалить учетную запись"; + +/* The placeholder text instructing users how to enter a pump ID */ +"Enter the 6-digit pump ID" = "Ввести 6-значный инд № помпы"; + +/* The title text for the pull cgm Data cell */ +"Fetch CGM" = "Получить данные мониторинга"; + +/* The title of the Nightscout service */ +"Nightscout" = "Nightscout"; + +/* The title text for the pump ID config value */ +"Pump ID" = "Инд № помпы"; + +/* The title text for the pump Region config value */ +"Pump Region" = "Регион помпы"; + +/* Instructions on selecting the pump region */ +"Pump Region is listed on the back of your pump as two of the last three characters of the model string, which reads something like this: MMT-551NAB, or MMT-515LWWS. If your model has an \"NA\" in it, then the region is NorthAmerica. If your model has an \"WW\" in it, then the region is WorldWide." = "Регион помпы находится на задней стенке помпы в виде двух из последних трех знаков вида MMT-551NAB или MMT-515LWWS. Если ваша модель имеет \"NA\" то это Северная Америка. Если \"WW\", то это остальной мир."; + +/* The default placeholder string for a credential */ +"Required" = "Обязательное значение"; + +/* The title of the nightscout site URL credential */ +"Site URL" = "URL сайта"; + +/* The empty-state text for a configuration value */ +"Tap to set" = "Щелкнуть для ввода"; + +/* The title text for the nightscout upload enabled switch cell */ +"Upload To Nightscout" = "Передать в Nightscout"; + +/* Label indicating validation is occurring */ +"Verifying" = "Верифицируется"; + +/* The placeholder text for the nightscout site URL credential */ +"http://mysite.azurewebsites.net" = "https://мойсайт. azurewebsites.net"; + diff --git a/RileyLink/ru.lproj/LoopKit.strings b/RileyLink/ru.lproj/LoopKit.strings new file mode 100644 index 000000000..bc1fda9fb --- /dev/null +++ b/RileyLink/ru.lproj/LoopKit.strings @@ -0,0 +1,3 @@ +/* The title of the action used to dismiss an error alert */ +"com.loudnate.LoopKit.errorAlertActionTitle" = "OK"; + diff --git a/RileyLinkBLEKit/BLEFirmwareVersion.swift b/RileyLinkBLEKit/BLEFirmwareVersion.swift new file mode 100644 index 000000000..00973270b --- /dev/null +++ b/RileyLinkBLEKit/BLEFirmwareVersion.swift @@ -0,0 +1,44 @@ +// +// BLEFirmwareVersion.swift +// RileyLinkBLEKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +public struct BLEFirmwareVersion { + private static let prefix = "ble_rfspy " + + let components: [Int] + + let versionString: String + + init?(versionString: String) { + guard + versionString.hasPrefix(BLEFirmwareVersion.prefix), + let versionIndex = versionString.index(versionString.startIndex, offsetBy: BLEFirmwareVersion.prefix.count, limitedBy: versionString.endIndex) + else { + return nil + } + + self.versionString = versionString + components = versionString[versionIndex...].split(separator: ".").compactMap({ Int($0) }) + } +} + + +extension BLEFirmwareVersion: CustomStringConvertible { + public var description: String { + return versionString + } +} + + +extension BLEFirmwareVersion { + var responseType: PeripheralManager.ResponseType { + guard let major = components.first, major >= 2 else { + return .buffered + } + + return .single + } +} diff --git a/RileyLinkBLEKit/CBCentralManager.swift b/RileyLinkBLEKit/CBCentralManager.swift new file mode 100644 index 000000000..465e1da9b --- /dev/null +++ b/RileyLinkBLEKit/CBCentralManager.swift @@ -0,0 +1,58 @@ +// +// CBCentralManager.swift +// RileyLinkBLEKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import CoreBluetooth + + +// MARK: - It's only valid to call these methods on the central manager's queue +extension CBCentralManager { + func connectIfNecessary(_ peripheral: CBPeripheral, options: [String: Any]? = nil) { + guard case .poweredOn = state else { + return + } + + switch peripheral.state { + case .connected: + delegate?.centralManager?(self, didConnect: peripheral) + case .connecting, .disconnected, .disconnecting: + connect(peripheral, options: options) + } + } + + func cancelPeripheralConnectionIfNecessary(_ peripheral: CBPeripheral) { + guard case .poweredOn = state else { + return + } + + switch peripheral.state { + case .disconnected: + delegate?.centralManager?(self, didDisconnectPeripheral: peripheral, error: nil) + case .connected, .connecting, .disconnecting: + cancelPeripheralConnection(peripheral) + } + } +} + + +extension CBManagerState { + var description: String { + switch self { + case .poweredOff: + return "Powered Off" + case .poweredOn: + return "Powered On" + case .resetting: + return "Resetting" + case .unauthorized: + return "Unauthorized" + case .unknown: + return "Unknown" + case .unsupported: + return "Unsupported" + } + } +} diff --git a/RileyLinkBLEKit/CBPeripheral.swift b/RileyLinkBLEKit/CBPeripheral.swift new file mode 100644 index 000000000..921462232 --- /dev/null +++ b/RileyLinkBLEKit/CBPeripheral.swift @@ -0,0 +1,35 @@ +// +// CBPeripheral.swift +// xDripG5 +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import CoreBluetooth + + +// MARK: - Discovery helpers. +extension CBPeripheral { + func servicesToDiscover(from serviceUUIDs: [CBUUID]) -> [CBUUID] { + let knownServiceUUIDs = services?.compactMap({ $0.uuid }) ?? [] + return serviceUUIDs.filter({ !knownServiceUUIDs.contains($0) }) + } + + func characteristicsToDiscover(from characteristicUUIDs: [CBUUID], for service: CBService) -> [CBUUID] { + let knownCharacteristicUUIDs = service.characteristics?.compactMap({ $0.uuid }) ?? [] + return characteristicUUIDs.filter({ !knownCharacteristicUUIDs.contains($0) }) + } +} + + +extension Collection where Element: CBAttribute { + func itemWithUUID(_ uuid: CBUUID) -> Element? { + for attribute in self { + if attribute.uuid == uuid { + return attribute + } + } + + return nil + } +} diff --git a/RileyLinkBLEKit/CmdBase.h b/RileyLinkBLEKit/CmdBase.h deleted file mode 100644 index fd81d1311..000000000 --- a/RileyLinkBLEKit/CmdBase.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// BaseCmd.h -// RileyLink -// -// Created by Pete Schwamb on 12/26/15. -// Copyright © 2015 Pete Schwamb. All rights reserved. -// - -@import Foundation; - -#define RILEYLINK_CMD_GET_STATE 1 -#define RILEYLINK_CMD_GET_VERSION 2 -#define RILEYLINK_CMD_GET_PACKET 3 -#define RILEYLINK_CMD_SEND_PACKET 4 -#define RILEYLINK_CMD_SEND_AND_LISTEN 5 -#define RILEYLINK_CMD_UPDATE_REGISTER 6 -#define RILEYLINK_CMD_RESET 7 - -@interface CmdBase : NSObject - -@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSData *data; - -@property (nonatomic, strong) NSData *response; - -@end diff --git a/RileyLinkBLEKit/CmdBase.m b/RileyLinkBLEKit/CmdBase.m deleted file mode 100644 index 5f175dbec..000000000 --- a/RileyLinkBLEKit/CmdBase.m +++ /dev/null @@ -1,17 +0,0 @@ -// -// BaseCmd.m -// RileyLink -// -// Created by Pete Schwamb on 12/26/15. -// Copyright © 2015 Pete Schwamb. All rights reserved. -// - -#import "CmdBase.h" - -@implementation CmdBase - -- (NSData*)data { - return nil; -} - -@end diff --git a/RileyLinkBLEKit/Command.swift b/RileyLinkBLEKit/Command.swift new file mode 100644 index 000000000..284ade5bf --- /dev/null +++ b/RileyLinkBLEKit/Command.swift @@ -0,0 +1,279 @@ +// +// Command.swift +// RileyLinkBLEKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import Foundation + + +// CmdBase +enum RileyLinkCommand: UInt8 { + case getState = 1 + case getVersion = 2 + case getPacket = 3 + case sendPacket = 4 + case sendAndListen = 5 + case updateRegister = 6 + case reset = 7 + case led = 8 + case readRegister = 9 + case setModeRegisters = 10 + case setSWEncoding = 11 + case setPreamble = 12 + case resetRadioConfig = 13 +} + +protocol Command { + associatedtype ResponseType: Response + + var data: Data { get } +} + +struct GetPacket: Command { + typealias ResponseType = PacketResponse + + let listenChannel: UInt8 + let timeoutMS: UInt32 + + init(listenChannel: UInt8, timeoutMS: UInt32) { + self.listenChannel = listenChannel + self.timeoutMS = timeoutMS + } + + var data: Data { + var data = Data(bytes: [ + RileyLinkCommand.getPacket.rawValue, + listenChannel + ]) + data.appendBigEndian(timeoutMS) + + return data + } +} + +struct GetVersion: Command { + typealias ResponseType = GetVersionResponse + + var data: Data { + return Data(bytes: [RileyLinkCommand.getVersion.rawValue]) + } +} + +struct SendAndListen: Command { + typealias ResponseType = PacketResponse + + let outgoing: Data + + /// In general, 0 = meter, cgm. 2 = pump + let sendChannel: UInt8 + + /// 0 = no repeat, i.e. only one packet. 1 repeat = 2 packets sent total. + let repeatCount: UInt8 + let delayBetweenPacketsMS: UInt16 + let listenChannel: UInt8 + let timeoutMS: UInt32 + let retryCount: UInt8 + let preambleExtensionMS: UInt16 + let firmwareVersion: RadioFirmwareVersion + + init(outgoing: Data, sendChannel: UInt8, repeatCount: UInt8, delayBetweenPacketsMS: UInt16, listenChannel: UInt8, timeoutMS: UInt32, retryCount: UInt8, preambleExtensionMS: UInt16, firmwareVersion: RadioFirmwareVersion) { + self.outgoing = outgoing + self.sendChannel = sendChannel + self.repeatCount = repeatCount + self.delayBetweenPacketsMS = delayBetweenPacketsMS + self.listenChannel = listenChannel + self.timeoutMS = timeoutMS + self.retryCount = retryCount + self.preambleExtensionMS = preambleExtensionMS + self.firmwareVersion = firmwareVersion + } + + var data: Data { + var data = Data(bytes: [ + RileyLinkCommand.sendAndListen.rawValue, + sendChannel, + repeatCount + ]) + + if firmwareVersion.supports16BitPacketDelay { + data.appendBigEndian(delayBetweenPacketsMS) + } else { + data.append(UInt8(clamping: Int(delayBetweenPacketsMS))) + } + + data.append(listenChannel); + data.appendBigEndian(timeoutMS) + data.append(retryCount) + if firmwareVersion.supportsPreambleExtension { + data.appendBigEndian(preambleExtensionMS) + } + data.append(outgoing) + + return data + } +} + +struct SendPacket: Command { + typealias ResponseType = CodeResponse + + let outgoing: Data + + /// In general, 0 = meter, cgm. 2 = pump + let sendChannel: UInt8 + + /// 0 = no repeat, i.e. only one packet. 1 repeat = 2 packets sent total. + let repeatCount: UInt8 + let delayBetweenPacketsMS: UInt16 + let preambleExtensionMS: UInt16 + let firmwareVersion: RadioFirmwareVersion + + init(outgoing: Data, sendChannel: UInt8, repeatCount: UInt8, delayBetweenPacketsMS: UInt16, preambleExtensionMS: UInt16, firmwareVersion: RadioFirmwareVersion) { + self.outgoing = outgoing + self.sendChannel = sendChannel + self.repeatCount = repeatCount + self.delayBetweenPacketsMS = delayBetweenPacketsMS + self.preambleExtensionMS = preambleExtensionMS + self.firmwareVersion = firmwareVersion; + } + + var data: Data { + var data = Data(bytes: [ + RileyLinkCommand.sendPacket.rawValue, + sendChannel, + repeatCount, + ]) + if firmwareVersion.supports16BitPacketDelay { + data.appendBigEndian(delayBetweenPacketsMS) + } else { + data.append(UInt8(clamping: Int(delayBetweenPacketsMS))) + } + + if firmwareVersion.supportsPreambleExtension { + data.appendBigEndian(preambleExtensionMS) + } + data.append(outgoing) + + return data + } +} + +struct RegisterSetting { + let address: CC111XRegister + let value: UInt8 +} + +struct UpdateRegister: Command { + typealias ResponseType = UpdateRegisterResponse + + enum Response: UInt8 { + case success = 1 + case invalidRegister = 2 + } + + let register: RegisterSetting + let firmwareVersion: RadioFirmwareVersion + + + init(_ address: CC111XRegister, value: UInt8, firmwareVersion: RadioFirmwareVersion) { + register = RegisterSetting(address: address, value: value) + self.firmwareVersion = firmwareVersion + } + + var data: Data { + var data = Data(bytes: [ + RileyLinkCommand.updateRegister.rawValue, + register.address.rawValue, + register.value + ]) + if firmwareVersion.needsExtraByteForUpdateRegisterCommand { + data.append(0) + } + return data + } +} + +struct SetModeRegisters: Command { + typealias ResponseType = UpdateRegisterResponse + + enum RegisterModeType: UInt8 { + case tx = 0x01 + case rx = 0x02 + } + + private var settings: [RegisterSetting] = [] + + let registerMode: RegisterModeType + + mutating func append(_ registerSetting: RegisterSetting) { + settings.append(registerSetting) + } + + var data: Data { + var data = Data(bytes: [ + RileyLinkCommand.setModeRegisters.rawValue, + registerMode.rawValue + ]) + + for setting in settings { + data.append(setting.address.rawValue) + data.append(setting.value) + } + + return data + } +} + +struct SetSoftwareEncoding: Command { + typealias ResponseType = CodeResponse + + let encodingType: SoftwareEncodingType + + + init(_ encodingType: SoftwareEncodingType) { + self.encodingType = encodingType + } + + var data: Data { + return Data(bytes: [ + RileyLinkCommand.setSWEncoding.rawValue, + encodingType.rawValue + ]) + } +} + +struct SetPreamble: Command { + typealias ResponseType = CodeResponse + + let preambleValue: UInt16 + + + init(_ value: UInt16) { + self.preambleValue = value + } + + var data: Data { + var data = Data(bytes: [RileyLinkCommand.setPreamble.rawValue]) + data.appendBigEndian(preambleValue) + return data + + } +} + +struct ResetRadioConfig: Command { + typealias ResponseType = CodeResponse + + var data: Data { + return Data(bytes: [RileyLinkCommand.resetRadioConfig.rawValue]) + } +} + + +// MARK: - Helpers +extension Data { + fileprivate mutating func appendBigEndian(_ newElement: T) { + var element = newElement.byteSwapped + append(UnsafeBufferPointer(start: &element, count: 1)) + } +} diff --git a/RileyLinkBLEKit/CommandSession.swift b/RileyLinkBLEKit/CommandSession.swift new file mode 100644 index 000000000..3bd8a9b37 --- /dev/null +++ b/RileyLinkBLEKit/CommandSession.swift @@ -0,0 +1,208 @@ +// +// RileyLinkCmdSession.swift +// RileyLinkBLEKit +// +// Created by Pete Schwamb on 10/8/17. +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import Foundation + +public enum RXFilterMode: UInt8 { + case wide = 0x50 // 300KHz + case narrow = 0x90 // 150KHz +} + +public enum SoftwareEncodingType: UInt8 { + case none = 0x00 + case manchester = 0x01 + case fourbsixb = 0x02 +} + +public enum CC111XRegister: UInt8 { + case sync1 = 0x00 + case sync0 = 0x01 + case pktlen = 0x02 + case pktctrl1 = 0x03 + case pktctrl0 = 0x04 + case fsctrl1 = 0x07 + case freq2 = 0x09 + case freq1 = 0x0a + case freq0 = 0x0b + case mdmcfg4 = 0x0c + case mdmcfg3 = 0x0d + case mdmcfg2 = 0x0e + case mdmcfg1 = 0x0f + case mdmcfg0 = 0x10 + case deviatn = 0x11 + case mcsm0 = 0x14 + case foccfg = 0x15 + case agcctrl2 = 0x17 + case agcctrl1 = 0x18 + case agcctrl0 = 0x19 + case frend1 = 0x1a + case frend0 = 0x1b + case fscal3 = 0x1c + case fscal2 = 0x1d + case fscal1 = 0x1e + case fscal0 = 0x1f + case test1 = 0x24 + case test0 = 0x25 + case paTable0 = 0x2e +} + +public struct CommandSession { + let manager: PeripheralManager + let responseType: PeripheralManager.ResponseType + let firmwareVersion: RadioFirmwareVersion + + /// Invokes a command expecting a response + /// + /// Unsuccessful responses are thrown as errors. + /// + /// - Parameters: + /// - command: The command + /// - timeout: The amount of time to wait for the pump to respond before throwing a timeout error. This should not include any expected BLE latency. + /// - Returns: The successful response + /// - Throws: RileyLinkDeviceError + private func writeCommand(_ command: C, timeout: TimeInterval) throws -> C.ResponseType { + let response = try manager.writeCommand(command, + timeout: timeout + PeripheralManager.expectedMaxBLELatency, + responseType: responseType + ) + + switch response.code { + case .rxTimeout: + throw RileyLinkDeviceError.responseTimeout + case .commandInterrupted: + throw RileyLinkDeviceError.responseTimeout + case .zeroData: + throw RileyLinkDeviceError.invalidResponse(Data()) + case .invalidParam, .unknownCommand: + throw RileyLinkDeviceError.invalidInput(String(describing: command.data)) + case .success: + return response + } + } + + /// Invokes a command expecting an RF packet response + /// + /// - Parameters: + /// - command: The command + /// - timeout: The amount of time to wait for the pump to respond before throwing a timeout error. This should not include any expected BLE latency. + /// - Returns: The successful packet response + /// - Throws: RileyLinkDeviceError + private func writeCommand(_ command: C, timeout: TimeInterval) throws -> RFPacket where C.ResponseType == PacketResponse { + let response: C.ResponseType = try writeCommand(command, timeout: timeout) + + guard let packet = response.packet else { + throw RileyLinkDeviceError.invalidResponse(Data()) + } + return packet + } + + /// - Throws: RileyLinkDeviceError + public func updateRegister(_ address: CC111XRegister, value: UInt8) throws { + let command = UpdateRegister(address, value: value, firmwareVersion: firmwareVersion) + _ = try writeCommand(command, timeout: 0) + } + + private static let xtalFrequency = Measurement(value: 24, unit: .megahertz) + + /// - Throws: RileyLinkDeviceError + public func setBaseFrequency(_ frequency: Measurement) throws { + let val = Int( + frequency.converted(to: .hertz).value / + (CommandSession.xtalFrequency / pow(2, 16)).converted(to: .hertz + ).value) + + try updateRegister(.freq0, value: UInt8(val & 0xff)) + try updateRegister(.freq1, value: UInt8((val >> 8) & 0xff)) + try updateRegister(.freq2, value: UInt8((val >> 16) & 0xff)) + } + + /// Sends data to the pump, listening for a reply + /// + /// - Parameters: + /// - data: The data to send + /// - repeatCount: The number of times to repeat the message before listening begins + /// - timeout: The length of time to listen for a response before timing out + /// - retryCount: The number of times to repeat the send & listen sequence + /// - Returns: The packet reply + /// - Throws: RileyLinkDeviceError + public func sendAndListen(_ data: Data, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> RFPacket? { + let delayBetweenPackets: TimeInterval = 0 + + let command = SendAndListen( + outgoing: data, + sendChannel: 0, + repeatCount: UInt8(clamping: repeatCount), + delayBetweenPacketsMS: UInt16(clamping: Int(delayBetweenPackets)), + listenChannel: 0, + timeoutMS: UInt32(clamping: Int(timeout.milliseconds)), + retryCount: UInt8(clamping: retryCount), + preambleExtensionMS: 0, + firmwareVersion: firmwareVersion + ) + + // At least 17 ms between packets for radio to stop/start + let radioTimeBetweenPackets = TimeInterval(milliseconds: 17) + let timeBetweenPackets = delayBetweenPackets + radioTimeBetweenPackets + + // 16384 = bitrate, 8 = bits per byte + let singlePacketSendTime: TimeInterval = (Double(data.count * 8) / 16_384) + let totalRepeatSendTime: TimeInterval = (singlePacketSendTime + timeBetweenPackets) * Double(repeatCount) + let totalTimeout = (totalRepeatSendTime + timeout) * Double(retryCount + 1) + + return try writeCommand(command, timeout: totalTimeout) + } + + /// - Throws: RileyLinkDeviceError + public func listen(onChannel channel: Int, timeout: TimeInterval) throws -> RFPacket? { + let command = GetPacket( + listenChannel: 0, + timeoutMS: UInt32(clamping: Int(timeout.milliseconds)) + ) + + return try writeCommand(command, timeout: timeout) + } + + /// - Throws: RileyLinkDeviceError + public func send(_ data: Data, onChannel channel: Int, timeout: TimeInterval) throws { + let command = SendPacket( + outgoing: data, + sendChannel: UInt8(clamping: channel), + repeatCount: 0, + delayBetweenPacketsMS: 0, + preambleExtensionMS: 0, + firmwareVersion: firmwareVersion + ) + + _ = try writeCommand(command, timeout: timeout) + } + + /// - Throws: RileyLinkDeviceError + public func setSoftwareEncoding(_ swEncodingType: SoftwareEncodingType) throws { + guard firmwareVersion.supportsSoftwareEncoding else { + throw RileyLinkDeviceError.unsupportedCommand(.setSWEncoding) + } + + let command = SetSoftwareEncoding(swEncodingType) + + let response = try writeCommand(command, timeout: 0) + + guard response.code == .success else { + throw RileyLinkDeviceError.invalidInput(String(describing: swEncodingType)) + } + } + + public func resetRadioConfig() throws { + guard firmwareVersion.supportsResetRadioConfig else { + return + } + + let command = ResetRadioConfig() + _ = try writeCommand(command, timeout: 0) + } + +} diff --git a/RileyLinkBLEKit/GetPacketCmd.h b/RileyLinkBLEKit/GetPacketCmd.h deleted file mode 100644 index ae43054d1..000000000 --- a/RileyLinkBLEKit/GetPacketCmd.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// GetPacketCmd.h -// RileyLink -// -// Created by Pete Schwamb on 1/2/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -@import Foundation; -#import "ReceivingPacketCmd.h" - - -@interface GetPacketCmd : ReceivingPacketCmd - -@property (nonatomic, assign) uint8_t listenChannel; -@property (nonatomic, assign) uint32_t timeoutMS; - -@end diff --git a/RileyLinkBLEKit/GetPacketCmd.m b/RileyLinkBLEKit/GetPacketCmd.m deleted file mode 100644 index 979bbda5d..000000000 --- a/RileyLinkBLEKit/GetPacketCmd.m +++ /dev/null @@ -1,25 +0,0 @@ -// -// GetPacketCmd.m -// RileyLink -// -// Created by Pete Schwamb on 1/2/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -#import "GetPacketCmd.h" - -@implementation GetPacketCmd - -- (NSData*)data { - uint8_t cmd[6]; - cmd[0] = RILEYLINK_CMD_GET_PACKET; - cmd[1] = _listenChannel; - cmd[2] = _timeoutMS >> 24; - cmd[3] = (_timeoutMS >> 16) & 0xff; - cmd[4] = (_timeoutMS >> 8) & 0xff; - cmd[5] = _timeoutMS & 0xff; - - return [NSData dataWithBytes:cmd length:6]; -} - -@end diff --git a/RileyLinkBLEKit/GetVersionCmd.h b/RileyLinkBLEKit/GetVersionCmd.h deleted file mode 100644 index ce03e17e7..000000000 --- a/RileyLinkBLEKit/GetVersionCmd.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// GetVersionCmd.h -// RileyLink -// -// Created by Pete Schwamb on 1/28/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -#import "CmdBase.h" - -@interface GetVersionCmd : CmdBase - -@end diff --git a/RileyLinkBLEKit/GetVersionCmd.m b/RileyLinkBLEKit/GetVersionCmd.m deleted file mode 100644 index f65209031..000000000 --- a/RileyLinkBLEKit/GetVersionCmd.m +++ /dev/null @@ -1,19 +0,0 @@ -// -// GetVersionCmd.m -// RileyLink -// -// Created by Pete Schwamb on 1/28/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -#import "GetVersionCmd.h" - -@implementation GetVersionCmd - -- (NSData*)data { - uint8_t cmd[1]; - cmd[0] = RILEYLINK_CMD_GET_VERSION; - return [NSData dataWithBytes:cmd length:1]; -} - -@end diff --git a/RileyLinkBLEKit/Info.plist b/RileyLinkBLEKit/Info.plist index 0eb186c06..d74989676 100644 --- a/RileyLinkBLEKit/Info.plist +++ b/RileyLinkBLEKit/Info.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,9 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.2.2 - CFBundleSignature - ???? + 2.0.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/RileyLinkBLEKit/PeripheralManager+RileyLink.swift b/RileyLinkBLEKit/PeripheralManager+RileyLink.swift new file mode 100644 index 000000000..d688bec15 --- /dev/null +++ b/RileyLinkBLEKit/PeripheralManager+RileyLink.swift @@ -0,0 +1,405 @@ +// +// PeripheralManager+RileyLink.swift +// xDripG5 +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import CoreBluetooth +import os.log + + +protocol CBUUIDRawValue: RawRepresentable {} +extension CBUUIDRawValue where RawValue == String { + var cbUUID: CBUUID { + return CBUUID(string: rawValue) + } +} + + +enum RileyLinkServiceUUID: String, CBUUIDRawValue { + case main = "0235733B-99C5-4197-B856-69219C2A3845" +} + +enum MainServiceCharacteristicUUID: String, CBUUIDRawValue { + case data = "C842E849-5028-42E2-867C-016ADADA9155" + case responseCount = "6E6C7910-B89E-43A5-A0FE-50C5E2B81F4A" + case customName = "D93B2AF0-1E28-11E4-8C21-0800200C9A66" + case timerTick = "6E6C7910-B89E-43A5-78AF-50C5E2B86F7E" + case firmwareVersion = "30D99DC9-7C91-4295-A051-0A104D238CF2" +} + + +extension PeripheralManager.Configuration { + static var rileyLink: PeripheralManager.Configuration { + return PeripheralManager.Configuration( + serviceCharacteristics: [ + RileyLinkServiceUUID.main.cbUUID: [ + MainServiceCharacteristicUUID.data.cbUUID, + MainServiceCharacteristicUUID.responseCount.cbUUID, + MainServiceCharacteristicUUID.customName.cbUUID, + MainServiceCharacteristicUUID.timerTick.cbUUID, + MainServiceCharacteristicUUID.firmwareVersion.cbUUID + ] + ], + notifyingCharacteristics: [ + RileyLinkServiceUUID.main.cbUUID: [ + MainServiceCharacteristicUUID.responseCount.cbUUID + // TODO: Should timer tick default to on? + ] + ], + valueUpdateMacros: [ + // When the responseCount changes, the data characteristic should be read. + MainServiceCharacteristicUUID.responseCount.cbUUID: { (manager: PeripheralManager) in + guard let dataCharacteristic = manager.peripheral.getCharacteristicWithUUID(.data) + else { + return + } + + manager.peripheral.readValue(for: dataCharacteristic) + } + ] + ) + } +} + + +fileprivate extension CBPeripheral { + func getCharacteristicWithUUID(_ uuid: MainServiceCharacteristicUUID, serviceUUID: RileyLinkServiceUUID = .main) -> CBCharacteristic? { + guard let service = services?.itemWithUUID(serviceUUID.cbUUID) else { + return nil + } + + return service.characteristics?.itemWithUUID(uuid.cbUUID) + } +} + + +extension CBCentralManager { + func scanForPeripherals(withOptions options: [String: Any]? = nil) { + scanForPeripherals(withServices: [RileyLinkServiceUUID.main.cbUUID], options: options) + } +} + + +extension Command { + /// Encodes a command's data by validating and prepending its length + /// + /// - Returns: Writable command data + /// - Throws: RileyLinkDeviceError.writeSizeLimitExceeded if the command data is too long + fileprivate func writableData() throws -> Data { + var data = self.data + + guard data.count <= 220 else { + throw RileyLinkDeviceError.writeSizeLimitExceeded(maxLength: 220) + } + + data.insert(UInt8(clamping: data.count), at: 0) + return data + } +} + + +private let log = OSLog(category: "PeripheralManager+RileyLink") + + +extension PeripheralManager { + static let expectedMaxBLELatency: TimeInterval = 2 + + var timerTickEnabled: Bool { + return peripheral.getCharacteristicWithUUID(.timerTick)?.isNotifying ?? false + } + + func setTimerTickEnabled(_ enabled: Bool, timeout: TimeInterval = expectedMaxBLELatency, completion: ((_ error: RileyLinkDeviceError?) -> Void)? = nil) { + perform { (manager) in + do { + guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.timerTick) else { + throw PeripheralManagerError.unknownCharacteristic + } + + try manager.setNotifyValue(enabled, for: characteristic, timeout: timeout) + completion?(nil) + } catch let error as PeripheralManagerError { + completion?(.peripheralManagerError(error)) + } catch { + assertionFailure() + } + } + } + + func startIdleListening(idleTimeout: TimeInterval, channel: UInt8, timeout: TimeInterval = expectedMaxBLELatency, completion: @escaping (_ error: RileyLinkDeviceError?) -> Void) { + perform { (manager) in + let command = GetPacket(listenChannel: channel, timeoutMS: UInt32(clamping: Int(idleTimeout.milliseconds))) + + do { + try manager.writeCommandWithoutResponse(command, timeout: timeout) + completion(nil) + } catch let error as RileyLinkDeviceError { + completion(error) + } catch { + assertionFailure() + } + } + } + + func setCustomName(_ name: String, timeout: TimeInterval = expectedMaxBLELatency, completion: ((_ error: RileyLinkDeviceError?) -> Void)? = nil) { + guard let value = name.data(using: .utf8) else { + completion?(.invalidInput(name)) + return + } + + perform { (manager) in + do { + guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.customName) else { + throw PeripheralManagerError.unknownCharacteristic + } + + try manager.writeValue(value, for: characteristic, type: .withResponse, timeout: timeout) + completion?(nil) + } catch let error as PeripheralManagerError { + completion?(.peripheralManagerError(error)) + } catch { + assertionFailure() + } + } + } +} + + + +// MARK: - Synchronous commands +extension PeripheralManager { + enum ResponseType { + case single + case buffered + } + + /// Invokes a command expecting a response + /// + /// - Parameters: + /// - command: The command + /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error + /// - responseType: The BLE response value framing method + /// - Returns: The received response + /// - Throws: + /// - RileyLinkDeviceError.invalidResponse + /// - RileyLinkDeviceError.peripheralManagerError + /// - RileyLinkDeviceError.writeSizeLimitExceeded + func writeCommand(_ command: C, timeout: TimeInterval, responseType: ResponseType) throws -> C.ResponseType { + guard let characteristic = peripheral.getCharacteristicWithUUID(.data) else { + throw RileyLinkDeviceError.peripheralManagerError(.unknownCharacteristic) + } + + let value = try command.writableData() + + log.debug("RL Send: %@", value.hexadecimalString) + + switch responseType { + case .single: + return try writeCommand(value, + for: characteristic, + timeout: timeout + ) + case .buffered: + return try writeLegacyCommand(value, + for: characteristic, + timeout: timeout, + endOfResponseMarker: 0x00 + ) + } + } + + /// Invokes a command without waiting for its response + /// + /// - Parameters: + /// - command: The command + /// - timeout: The amount of time to wait for the peripheral to confirm the write before throwing a timeout error + /// - Throws: + /// - RileyLinkDeviceError.invalidResponse + /// - RileyLinkDeviceError.peripheralManagerError + /// - RileyLinkDeviceError.writeSizeLimitExceeded + fileprivate func writeCommandWithoutResponse(_ command: C, timeout: TimeInterval) throws { + guard let characteristic = peripheral.getCharacteristicWithUUID(.data) else { + throw RileyLinkDeviceError.peripheralManagerError(.unknownCharacteristic) + } + + let value = try command.writableData() + + log.debug("RL Send: %@", value.hexadecimalString) + + do { + try writeValue(value, for: characteristic, type: .withResponse, timeout: timeout) + } catch let error as PeripheralManagerError { + throw RileyLinkDeviceError.peripheralManagerError(error) + } + } + + /// - Throws: + /// - RileyLinkDeviceError.invalidResponse + /// - RileyLinkDeviceError.peripheralManagerError + func readRadioFirmwareVersion(timeout: TimeInterval, responseType: ResponseType) throws -> String { + let response = try writeCommand(GetVersion(), timeout: timeout, responseType: responseType) + return response.version + } + + /// - Throws: + /// - RileyLinkDeviceError.invalidResponse + /// - RileyLinkDeviceError.peripheralManagerError + func readBluetoothFirmwareVersion(timeout: TimeInterval) throws -> String { + guard let characteristic = peripheral.getCharacteristicWithUUID(.firmwareVersion) else { + throw RileyLinkDeviceError.peripheralManagerError(.unknownCharacteristic) + } + + do { + guard let data = try readValue(for: characteristic, timeout: timeout) else { + // TODO: This is an "unknown value" issue, not a timeout + throw RileyLinkDeviceError.peripheralManagerError(.timeout) + } + + guard let version = String(bytes: data, encoding: .utf8) else { + throw RileyLinkDeviceError.invalidResponse(data) + } + + return version + } catch let error as PeripheralManagerError { + throw RileyLinkDeviceError.peripheralManagerError(error) + } + } +} + + +// MARK: - Lower-level helper operations +extension PeripheralManager { + + /// Writes command data expecting a single response + /// + /// - Parameters: + /// - data: The command data + /// - characteristic: The peripheral characteristic to write + /// - type: The type of characteristic write + /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error + /// - Returns: The recieved response + /// - Throws: + /// - RileyLinkDeviceError.invalidResponse + /// - RileyLinkDeviceError.peripheralManagerError + private func writeCommand(_ data: Data, + for characteristic: CBCharacteristic, + type: CBCharacteristicWriteType = .withResponse, + timeout: TimeInterval + ) throws -> R + { + var capturedResponse: R? + + do { + try runCommand(timeout: timeout) { + if case .withResponse = type { + addCondition(.write(characteristic: characteristic)) + } + + addCondition(.valueUpdate(characteristic: characteristic, matching: { value in + guard let value = value else { + return false + } + + log.debug("RL Recv(single): %@", value.hexadecimalString) + + guard let response = R(data: value) else { + // We don't recognize the contents. Keep listening. + return false + } + + switch response.code { + case .rxTimeout, .zeroData, .invalidParam, .unknownCommand: + log.debug("RileyLink response: %{public}@", String(describing: response)) + capturedResponse = response + return true + case .commandInterrupted: + // This is expected in cases where an "Idle" GetPacket command is running + log.debug("RileyLink response: %{public}@", String(describing: response)) + return false + case .success: + capturedResponse = response + return true + } + })) + + peripheral.writeValue(data, for: characteristic, type: type) + } + } catch let error as PeripheralManagerError { + throw RileyLinkDeviceError.peripheralManagerError(error) + } + + guard let response = capturedResponse else { + throw RileyLinkDeviceError.invalidResponse(characteristic.value ?? Data()) + } + + return response + } + + /// Writes command data expecting a bufferred response + /// + /// - Parameters: + /// - data: The command data + /// - characteristic: The peripheral characteristic to write + /// - type: The type of characteristic write + /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error + /// - endOfResponseMarker: The marker delimiting the end of a response in the buffer + /// - Returns: The received response. In the event of multiple responses in the buffer, the first parsable response is returned. + /// - Throws: + /// - RileyLinkDeviceError.invalidResponse + /// - RileyLinkDeviceError.peripheralManagerError + private func writeLegacyCommand(_ data: Data, + for characteristic: CBCharacteristic, + type: CBCharacteristicWriteType = .withResponse, + timeout: TimeInterval, + endOfResponseMarker: UInt8 + ) throws -> R + { + var capturedResponse: R? + var buffer = ResponseBuffer(endMarker: endOfResponseMarker) + + do { + try runCommand(timeout: timeout) { + if case .withResponse = type { + addCondition(.write(characteristic: characteristic)) + } + + addCondition(.valueUpdate(characteristic: characteristic, matching: { value in + guard let value = value else { + return false + } + + log.debug("RL Recv(buffered): %@", value.hexadecimalString) + buffer.append(value) + + for response in buffer.responses { + switch response.code { + case .rxTimeout, .zeroData, .invalidParam, .unknownCommand: + log.debug("RileyLink response: %{public}@", String(describing: response)) + capturedResponse = response + return true + case .commandInterrupted: + // This is expected in cases where an "Idle" GetPacket command is running + log.debug("RileyLink response: %{public}@", String(describing: response)) + case .success: + capturedResponse = response + return true + } + } + + return false + })) + + peripheral.writeValue(data, for: characteristic, type: type) + } + } catch let error as PeripheralManagerError { + throw RileyLinkDeviceError.peripheralManagerError(error) + } + + guard let response = capturedResponse else { + throw RileyLinkDeviceError.invalidResponse(characteristic.value ?? Data()) + } + + return response + } +} diff --git a/RileyLinkBLEKit/PeripheralManager.swift b/RileyLinkBLEKit/PeripheralManager.swift new file mode 100644 index 000000000..adda9993e --- /dev/null +++ b/RileyLinkBLEKit/PeripheralManager.swift @@ -0,0 +1,435 @@ +// +// PeripheralManager.swift +// xDripG5 +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import CoreBluetooth +import Foundation +import os.log + + +class PeripheralManager: NSObject { + + private let log = OSLog(category: "PeripheralManager") + + /// + /// This is mutable, because CBPeripheral instances can seemingly become invalid, and need to be periodically re-fetched from CBCentralManager + var peripheral: CBPeripheral { + didSet { + guard oldValue !== peripheral else { + return + } + + log.error("Replacing peripheral reference %{public}@ -> %{public}@", oldValue, peripheral) + + oldValue.delegate = nil + peripheral.delegate = self + + queue.async { + self.needsConfiguration = true + } + } + } + + /// The dispatch queue used to serialize operations on the peripheral + let queue = DispatchQueue(label: "com.loopkit.PeripheralManager.queue", qos: .utility) + + /// The condition used to signal command completion + private let commandLock = NSCondition() + + /// The required conditions for the operation to complete + private var commandConditions = [CommandCondition]() + + /// Any error surfaced during the active operation + private var commandError: Error? + + unowned let central: CBCentralManager + + let configuration: Configuration + + // Confined to `queue` + private var needsConfiguration = true + + weak var delegate: PeripheralManagerDelegate? + + init(peripheral: CBPeripheral, configuration: Configuration, centralManager: CBCentralManager) { + self.peripheral = peripheral + self.central = centralManager + self.configuration = configuration + + super.init() + + peripheral.delegate = self + + assertConfiguration() + } +} + + +// MARK: - Nested types +extension PeripheralManager { + struct Configuration { + var serviceCharacteristics: [CBUUID: [CBUUID]] = [:] + var notifyingCharacteristics: [CBUUID: [CBUUID]] = [:] + var valueUpdateMacros: [CBUUID: (_ manager: PeripheralManager) -> Void] = [:] + } + + enum CommandCondition { + case notificationStateUpdate(characteristic: CBCharacteristic, enabled: Bool) + case valueUpdate(characteristic: CBCharacteristic, matching: ((Data?) -> Bool)?) + case write(characteristic: CBCharacteristic) + case discoverServices + case discoverCharacteristicsForService(serviceUUID: CBUUID) + } +} + +protocol PeripheralManagerDelegate: class { + func peripheralManager(_ manager: PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic) + + func peripheralManager(_ manager: PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?) + + func peripheralManagerDidUpdateName(_ manager: PeripheralManager) + + func completeConfiguration(for manager: PeripheralManager) throws +} + + +// MARK: - Operation sequence management +extension PeripheralManager { + func configureAndRun(_ block: @escaping (_ manager: PeripheralManager) -> Void) -> (() -> Void) { + return { [unowned self] in + if !self.needsConfiguration && self.peripheral.services == nil { + self.log.error("Configured peripheral has no services. Reconfiguring…") + } + + if self.needsConfiguration || self.peripheral.services == nil { + do { + try self.applyConfiguration() + try self.delegate?.completeConfiguration(for: self) + self.needsConfiguration = false + } catch let error { + self.log.debug("Error applying configuration: %@", String(describing: error)) + // Will retry + } + } + + block(self) + } + } + + func perform(_ block: @escaping (_ manager: PeripheralManager) -> Void) { + queue.async(execute: configureAndRun(block)) + } + + private func assertConfiguration() { + perform { (_) in + // Intentionally empty to trigger configuration if necessary + } + } + + private func applyConfiguration(discoveryTimeout: TimeInterval = 2) throws { + try discoverServices(configuration.serviceCharacteristics.keys.map { $0 }, timeout: discoveryTimeout) + + for service in peripheral.services ?? [] { + guard let characteristics = configuration.serviceCharacteristics[service.uuid] else { + // Not all services may have characteristics + continue + } + + try discoverCharacteristics(characteristics, for: service, timeout: discoveryTimeout) + } + + for (serviceUUID, characteristicUUIDs) in configuration.notifyingCharacteristics { + guard let service = peripheral.services?.itemWithUUID(serviceUUID) else { + throw PeripheralManagerError.unknownCharacteristic + } + + for characteristicUUID in characteristicUUIDs { + guard let characteristic = service.characteristics?.itemWithUUID(characteristicUUID) else { + throw PeripheralManagerError.unknownCharacteristic + } + + guard !characteristic.isNotifying else { + continue + } + + try setNotifyValue(true, for: characteristic, timeout: discoveryTimeout) + } + } + } +} + + +// MARK: - Synchronous Commands +extension PeripheralManager { + /// - Throws: PeripheralManagerError + func runCommand(timeout: TimeInterval, command: () -> Void) throws { + // Prelude + dispatchPrecondition(condition: .onQueue(queue)) + guard central.state == .poweredOn && peripheral.state == .connected else { + throw PeripheralManagerError.notReady + } + + commandLock.lock() + + defer { + commandLock.unlock() + } + + guard commandConditions.isEmpty else { + throw PeripheralManagerError.notReady + } + + // Run + command() + + guard !commandConditions.isEmpty else { + // If the command didn't add any conditions, then finish immediately + return + } + + // Postlude + let signaled = commandLock.wait(until: Date(timeIntervalSinceNow: timeout)) + + defer { + commandError = nil + commandConditions = [] + } + + guard signaled else { + throw PeripheralManagerError.timeout + } + + if let error = commandError { + throw PeripheralManagerError.cbPeripheralError(error) + } + } + + /// It's illegal to call this without first acquiring the commandLock + /// + /// - Parameter condition: The condition to add + func addCondition(_ condition: CommandCondition) { + dispatchPrecondition(condition: .onQueue(queue)) + commandConditions.append(condition) + } + + func discoverServices(_ serviceUUIDs: [CBUUID], timeout: TimeInterval) throws { + let servicesToDiscover = peripheral.servicesToDiscover(from: serviceUUIDs) + + guard servicesToDiscover.count > 0 else { + return + } + + try runCommand(timeout: timeout) { + addCondition(.discoverServices) + + peripheral.discoverServices(serviceUUIDs) + } + } + + func discoverCharacteristics(_ characteristicUUIDs: [CBUUID], for service: CBService, timeout: TimeInterval) throws { + let characteristicsToDiscover = peripheral.characteristicsToDiscover(from: characteristicUUIDs, for: service) + + guard characteristicsToDiscover.count > 0 else { + return + } + + try runCommand(timeout: timeout) { + addCondition(.discoverCharacteristicsForService(serviceUUID: service.uuid)) + + peripheral.discoverCharacteristics(characteristicsToDiscover, for: service) + } + } + + /// - Throws: PeripheralManagerError + func setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic, timeout: TimeInterval) throws { + try runCommand(timeout: timeout) { + addCondition(.notificationStateUpdate(characteristic: characteristic, enabled: enabled)) + + peripheral.setNotifyValue(enabled, for: characteristic) + } + } + + /// - Throws: PeripheralManagerError + func readValue(for characteristic: CBCharacteristic, timeout: TimeInterval) throws -> Data? { + try runCommand(timeout: timeout) { + addCondition(.valueUpdate(characteristic: characteristic, matching: nil)) + + peripheral.readValue(for: characteristic) + } + + return characteristic.value + } + + /// - Throws: PeripheralManagerError + func wait(for characteristic: CBCharacteristic, timeout: TimeInterval) throws -> Data { + try runCommand(timeout: timeout) { + addCondition(.valueUpdate(characteristic: characteristic, matching: nil)) + } + + guard let value = characteristic.value else { + throw PeripheralManagerError.timeout + } + + return value + } + + /// - Throws: PeripheralManagerError + func writeValue(_ value: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType, timeout: TimeInterval) throws { + try runCommand(timeout: timeout) { + if case .withResponse = type { + addCondition(.write(characteristic: characteristic)) + } + + peripheral.writeValue(value, for: characteristic, type: type) + } + } +} + + +// MARK: - Delegate methods executed on the central's queue +extension PeripheralManager: CBPeripheralDelegate { + + func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { + commandLock.lock() + + if let index = commandConditions.index(where: { (condition) -> Bool in + if case .discoverServices = condition { + return true + } else { + return false + } + }) { + commandConditions.remove(at: index) + commandError = error + + if commandConditions.isEmpty { + commandLock.broadcast() + } + } + + commandLock.unlock() + } + + func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { + commandLock.lock() + + if let index = commandConditions.index(where: { (condition) -> Bool in + if case .discoverCharacteristicsForService(serviceUUID: service.uuid) = condition { + return true + } else { + return false + } + }) { + commandConditions.remove(at: index) + commandError = error + + if commandConditions.isEmpty { + commandLock.broadcast() + } + } + + commandLock.unlock() + } + + func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { + commandLock.lock() + + if let index = commandConditions.index(where: { (condition) -> Bool in + if case .notificationStateUpdate(characteristic: characteristic, enabled: characteristic.isNotifying) = condition { + return true + } else { + return false + } + }) { + commandConditions.remove(at: index) + commandError = error + + if commandConditions.isEmpty { + commandLock.broadcast() + } + } + + commandLock.unlock() + } + + func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { + commandLock.lock() + + if let index = commandConditions.index(where: { (condition) -> Bool in + if case .write(characteristic: characteristic) = condition { + return true + } else { + return false + } + }) { + commandConditions.remove(at: index) + commandError = error + + if commandConditions.isEmpty { + commandLock.broadcast() + } + } + + commandLock.unlock() + } + + func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + commandLock.lock() + + if let index = commandConditions.index(where: { (condition) -> Bool in + if case .valueUpdate(characteristic: characteristic, matching: let matching) = condition { + return matching?(characteristic.value) ?? true + } else { + return false + } + }) { + commandConditions.remove(at: index) + commandError = error + + if commandConditions.isEmpty { + commandLock.broadcast() + } + } else if let macro = configuration.valueUpdateMacros[characteristic.uuid] { + macro(self) + } else if commandConditions.isEmpty { + defer { // execute after the unlock + // If we weren't expecting this notification, pass it along to the delegate + delegate?.peripheralManager(self, didUpdateValueFor: characteristic) + } + } + + commandLock.unlock() + } + + func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { + delegate?.peripheralManager(self, didReadRSSI: RSSI, error: error) + } + + func peripheralDidUpdateName(_ peripheral: CBPeripheral) { + delegate?.peripheralManagerDidUpdateName(self) + } +} + + +extension PeripheralManager: CBCentralManagerDelegate { + func centralManagerDidUpdateState(_ central: CBCentralManager) { + switch central.state { + case .poweredOn: + assertConfiguration() + default: + break + } + } + + func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + switch peripheral.state { + case .connected: + assertConfiguration() + default: + break + } + } +} diff --git a/RileyLinkBLEKit/PeripheralManagerError.swift b/RileyLinkBLEKit/PeripheralManagerError.swift new file mode 100644 index 000000000..0c25505c7 --- /dev/null +++ b/RileyLinkBLEKit/PeripheralManagerError.swift @@ -0,0 +1,41 @@ +// +// PeripheralManagerError.swift +// RileyLinkBLEKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import CoreBluetooth + + +enum PeripheralManagerError: Error { + case cbPeripheralError(Error) + case notReady + case timeout + case unknownCharacteristic +} + + +extension PeripheralManagerError: LocalizedError { + var errorDescription: String? { + switch self { + case .cbPeripheralError(let error): + return error.localizedDescription + case .notReady: + return NSLocalizedString("Peripheral isnʼt connected", comment: "Not ready error description") + case .timeout: + return NSLocalizedString("Peripheral did not respond in time", comment: "Timeout error description") + case .unknownCharacteristic: + return NSLocalizedString("Unknown characteristic", comment: "Error description") + } + } + + var failureReason: String? { + switch self { + case .cbPeripheralError(let error as NSError): + return error.localizedFailureReason + default: + return errorDescription + } + } +} diff --git a/RileyLinkBLEKit/RFPacket.h b/RileyLinkBLEKit/RFPacket.h deleted file mode 100644 index 2300bca7d..000000000 --- a/RileyLinkBLEKit/RFPacket.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// RFPacket.h -// RileyLink -// -// Created by Pete Schwamb on 2/28/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -@import Foundation; - -@interface RFPacket : NSObject - -- (nonnull instancetype)initWithData:(nonnull NSData*)data NS_DESIGNATED_INITIALIZER; -- (nullable instancetype)initWithRFSPYResponse:(nonnull NSData*)data NS_DESIGNATED_INITIALIZER; - -- (nonnull NSData*)encodedData; - -@property (nonatomic, nullable, strong) NSData *data; -@property (nonatomic, nullable, strong) NSDate *capturedAt; -@property (nonatomic, assign) int rssi; -@property (nonatomic, assign) int packetNumber; - - -@end diff --git a/RileyLinkBLEKit/RFPacket.m b/RileyLinkBLEKit/RFPacket.m deleted file mode 100644 index 8b079b2b9..000000000 --- a/RileyLinkBLEKit/RFPacket.m +++ /dev/null @@ -1,155 +0,0 @@ -// -// RFPacket.m -// RileyLink -// -// Created by Pete Schwamb on 2/28/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -#import "RFPacket.h" - -@implementation RFPacket - -static const unsigned char crcTable[256] = { 0x0, 0x9B, 0xAD, 0x36, 0xC1, 0x5A, 0x6C, 0xF7, 0x19, 0x82, 0xB4, 0x2F, 0xD8, 0x43, 0x75, 0xEE, 0x32, 0xA9, 0x9F, 0x4, 0xF3, 0x68, 0x5E, 0xC5, 0x2B, 0xB0, 0x86, 0x1D, 0xEA, 0x71, 0x47, 0xDC, 0x64, 0xFF, 0xC9, 0x52, 0xA5, 0x3E, 0x8, 0x93, 0x7D, 0xE6, 0xD0, 0x4B, 0xBC, 0x27, 0x11, 0x8A, 0x56, 0xCD, 0xFB, 0x60, 0x97, 0xC, 0x3A, 0xA1, 0x4F, 0xD4, 0xE2, 0x79, 0x8E, 0x15, 0x23, 0xB8, 0xC8, 0x53, 0x65, 0xFE, 0x9, 0x92, 0xA4, 0x3F, 0xD1, 0x4A, 0x7C, 0xE7, 0x10, 0x8B, 0xBD, 0x26, 0xFA, 0x61, 0x57, 0xCC, 0x3B, 0xA0, 0x96, 0xD, 0xE3, 0x78, 0x4E, 0xD5, 0x22, 0xB9, 0x8F, 0x14, 0xAC, 0x37, 0x1, 0x9A, 0x6D, 0xF6, 0xC0, 0x5B, 0xB5, 0x2E, 0x18, 0x83, 0x74, 0xEF, 0xD9, 0x42, 0x9E, 0x5, 0x33, 0xA8, 0x5F, 0xC4, 0xF2, 0x69, 0x87, 0x1C, 0x2A, 0xB1, 0x46, 0xDD, 0xEB, 0x70, 0xB, 0x90, 0xA6, 0x3D, 0xCA, 0x51, 0x67, 0xFC, 0x12, 0x89, 0xBF, 0x24, 0xD3, 0x48, 0x7E, 0xE5, 0x39, 0xA2, 0x94, 0xF, 0xF8, 0x63, 0x55, 0xCE, 0x20, 0xBB, 0x8D, 0x16, 0xE1, 0x7A, 0x4C, 0xD7, 0x6F, 0xF4, 0xC2, 0x59, 0xAE, 0x35, 0x3, 0x98, 0x76, 0xED, 0xDB, 0x40, 0xB7, 0x2C, 0x1A, 0x81, 0x5D, 0xC6, 0xF0, 0x6B, 0x9C, 0x7, 0x31, 0xAA, 0x44, 0xDF, 0xE9, 0x72, 0x85, 0x1E, 0x28, 0xB3, 0xC3, 0x58, 0x6E, 0xF5, 0x2, 0x99, 0xAF, 0x34, 0xDA, 0x41, 0x77, 0xEC, 0x1B, 0x80, 0xB6, 0x2D, 0xF1, 0x6A, 0x5C, 0xC7, 0x30, 0xAB, 0x9D, 0x6, 0xE8, 0x73, 0x45, 0xDE, 0x29, 0xB2, 0x84, 0x1F, 0xA7, 0x3C, 0xA, 0x91, 0x66, 0xFD, 0xCB, 0x50, 0xBE, 0x25, 0x13, 0x88, 0x7F, 0xE4, 0xD2, 0x49, 0x95, 0xE, 0x38, 0xA3, 0x54, 0xCF, 0xF9, 0x62, 0x8C, 0x17, 0x21, 0xBA, 0x4D, 0xD6, 0xE0, 0x7B }; - - -+ (uint8_t) computeCRC8:(NSData*)data { - uint8_t crc = 0; - const uint8_t *pdata = data.bytes; - unsigned long nbytes = data.length; - /* loop over the buffer data */ - while (nbytes-- > 0) { - crc = crcTable[(crc ^ *pdata++) & 0xff]; - } - return crc; -} - -- (instancetype)init NS_UNAVAILABLE -{ - return nil; -} - -- (instancetype)initWithData:(NSData*)data { - self = [super init]; - if (self) { - _data = data; - } - return self; -} - - -- (instancetype)initWithRFSPYResponse:(NSData*)data -{ - self = [super init]; - if (self) { - if (data.length > 0) { - unsigned char rssiDec = ((const unsigned char*)[data bytes])[0]; - unsigned char rssiOffset = 73; - if (rssiDec >= 128) { - self.rssi = (short)((short)( rssiDec - 256) / 2) - rssiOffset; - } else { - self.rssi = (rssiDec / 2) - rssiOffset; - } - } - if (data.length > 1) { - self.packetNumber = ((const unsigned char*)[data bytes])[1]; - } - - if (data.length > 2) { - NSData *decoded = [self decodeRF:[data subdataWithRange:NSMakeRange(2, data.length - 2)]]; - if (decoded && decoded.length > 2) { - NSData *msgData = [decoded subdataWithRange:NSMakeRange(0, decoded.length - 1)]; - - unsigned char receivedCrc = ((const unsigned char*)[decoded bytes])[decoded.length-1]; - unsigned char computedCrc = [RFPacket computeCRC8:msgData]; - - if (receivedCrc == computedCrc) { - _data = msgData; - } - } - } - } - return self; -} - -- (NSData*)encodedData { - NSMutableData *outData = [NSMutableData data]; - // TODO; we should be able to avoid this copy by accessing the CRC - // in the loop below when i == data.length - NSMutableData *dataPlusCrc = [_data mutableCopy]; - unsigned char crc = [RFPacket computeCRC8:_data]; - [dataPlusCrc appendBytes:&crc length:1]; - char codes[16] = {21,49,50,35,52,37,38,22,26,25,42,11,44,13,14,28}; - const unsigned char *inBytes = [dataPlusCrc bytes]; - unsigned int acc = 0x0; - int bitcount = 0; - for (int i=0; i < dataPlusCrc.length; i++) { - acc <<= 6; - acc |= codes[inBytes[i] >> 4]; - bitcount += 6; - - acc <<= 6; - acc |= codes[inBytes[i] & 0x0f]; - bitcount += 6; - - while (bitcount >= 8) { - unsigned char outByte = acc >> (bitcount-8) & 0xff; - [outData appendBytes:&outByte length:1]; - bitcount -= 8; - acc &= (0xffff >> (16-bitcount)); - } - } - if (bitcount > 0) { - acc <<= (8-bitcount); - unsigned char outByte = acc & 0xff; - [outData appendBytes:&outByte length:1]; - } - return outData; -} - - - -- (NSData*)decodeRF:(NSData*) rawData { - // Converted from ruby using: CODE_SYMBOLS.each{|k,v| puts "@#{Integer("0b"+k)}: @#{Integer("0x"+v)},"};nil - NSDictionary *codes = @{@21: @0, - @49: @1, - @50: @2, - @35: @3, - @52: @4, - @37: @5, - @38: @6, - @22: @7, - @26: @8, - @25: @9, - @42: @10, - @11: @11, - @44: @12, - @13: @13, - @14: @14, - @28: @15}; - NSMutableData *output = [NSMutableData data]; - const unsigned char *bytes = [rawData bytes]; - int availBits = 0; - unsigned int x = 0; - for (int i = 0; i < [rawData length]; i++) - { - x = (x << 8) + bytes[i]; - availBits += 8; - if (availBits >= 12) { - NSNumber *hiNibble = codes[@(x >> (availBits - 6))]; - NSNumber *loNibble = codes[@((x >> (availBits - 12)) & 0b111111)]; - if (hiNibble && loNibble) { - unsigned char decoded = ([hiNibble integerValue] << 4) + [loNibble integerValue]; - [output appendBytes:&decoded length:1]; - } else { - return nil; - } - availBits -= 12; - x = x & (0xffff >> (16-availBits)); - } - } - return output; -} - - -@end diff --git a/RileyLinkBLEKit/RFPacket.swift b/RileyLinkBLEKit/RFPacket.swift new file mode 100644 index 000000000..a69e10ba9 --- /dev/null +++ b/RileyLinkBLEKit/RFPacket.swift @@ -0,0 +1,36 @@ +// +// RFPacket.swift +// RileyLinkBLEKit +// +// Created by Pete Schwamb on 9/16/17. +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import Foundation + +public struct RFPacket { + public let data: Data + let packetCounter: Int + public let rssi: Int + + init?(rfspyResponse: Data) { + guard rfspyResponse.count > 2 else { + return nil + } + + let startIndex = rfspyResponse.startIndex + + let rssiDec = Int(rfspyResponse[startIndex]) + let rssiOffset = 73 + if rssiDec >= 128 { + self.rssi = (rssiDec - 256) / 2 - rssiOffset + } else { + self.rssi = rssiDec / 2 - rssiOffset + } + + self.packetCounter = Int(rfspyResponse[startIndex.advanced(by: 1)]) + + self.data = rfspyResponse.subdata(in: startIndex.advanced(by: 2)..= 2 else { + return false + } + return true + } + + var supportsPreambleExtension: Bool { + return atLeastV2 + } + + var supportsSoftwareEncoding: Bool { + return atLeastV2 + } + + var supportsResetRadioConfig: Bool { + return atLeastV2 + } + + var supports16BitPacketDelay: Bool { + return atLeastV2 + } + + var needsExtraByteForUpdateRegisterCommand: Bool { + return !atLeastV2 + } + +} + diff --git a/RileyLinkBLEKit/ReceivingPacketCmd.h b/RileyLinkBLEKit/ReceivingPacketCmd.h deleted file mode 100644 index 57e8648a3..000000000 --- a/RileyLinkBLEKit/ReceivingPacketCmd.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// ReceivingPacketCmd.h -// RileyLink -// -// Created by Pete Schwamb on 3/3/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -@import Foundation; -#import "CmdBase.h" -#import "RFPacket.h" - -@interface ReceivingPacketCmd : CmdBase - -@property (nonatomic, strong) RFPacket *receivedPacket; -@property (nonatomic, readonly) BOOL didReceiveResponse; -@property (nonatomic, readonly) NSData *rawReceivedData; - -@end diff --git a/RileyLinkBLEKit/ReceivingPacketCmd.m b/RileyLinkBLEKit/ReceivingPacketCmd.m deleted file mode 100644 index bcd4d769e..000000000 --- a/RileyLinkBLEKit/ReceivingPacketCmd.m +++ /dev/null @@ -1,32 +0,0 @@ -// -// ReceivingPacketCmd.m -// RileyLink -// -// Created by Pete Schwamb on 3/3/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -#import "ReceivingPacketCmd.h" - -@implementation ReceivingPacketCmd - -- (RFPacket*) receivedPacket { - if (_receivedPacket == nil && self.response != nil) { - _receivedPacket = [[RFPacket alloc] initWithRFSPYResponse:self.response]; - } - return _receivedPacket; -} - -- (BOOL) didReceiveResponse { - return self.response != nil && self.response.length > 2; -} - -- (NSData*) rawReceivedData { - if (self.didReceiveResponse) { - return [self.response subdataWithRange:NSMakeRange(2, self.response.length - 2)]; - } else { - return nil; - } -} - -@end diff --git a/RileyLinkBLEKit/Response.swift b/RileyLinkBLEKit/Response.swift new file mode 100644 index 000000000..9ec084448 --- /dev/null +++ b/RileyLinkBLEKit/Response.swift @@ -0,0 +1,154 @@ +// +// Response.swift +// RileyLinkBLEKit +// +// Copyright © 2018 Pete Schwamb. All rights reserved. +// + +enum ResponseCode: UInt8 { + case rxTimeout = 0xaa + case commandInterrupted = 0xbb + case zeroData = 0xcc + case success = 0xdd + case invalidParam = 0x11 + case unknownCommand = 0x22 +} + +protocol Response { + var code: ResponseCode { get } + + init?(data: Data) + + init?(legacyData data: Data) +} + +struct CodeResponse: Response { + let code: ResponseCode + + init?(data: Data) { + guard data.count == 1, let code = ResponseCode(rawValue: data[data.startIndex]) else { + return nil + } + + self.code = code + } + + init?(legacyData data: Data) { + guard data.count == 0 else { + return nil + } + + self.code = .success + } +} + +struct UpdateRegisterResponse: Response { + let code: ResponseCode + + init?(data: Data) { + guard data.count > 0, let code = ResponseCode(rawValue: data[data.startIndex]) else { + return nil + } + + self.code = code + } + + private enum LegacyCode: UInt8 { + case success = 1 + case invalidRegister = 2 + + var responseCode: ResponseCode { + switch self { + case .success: + return .success + case .invalidRegister: + return .invalidParam + } + } + } + + init?(legacyData data: Data) { + guard data.count > 0, let code = LegacyCode(rawValue: data[data.startIndex])?.responseCode else { + return nil + } + + self.code = code + } +} + +struct GetVersionResponse: Response { + let code: ResponseCode + let version: String + + init?(data: Data) { + guard data.count > 0, let code = ResponseCode(rawValue: data[data.startIndex]) else { + return nil + } + + self.init(code: code, versionData: data[data.startIndex.advanced(by: 1)...]) + } + + init?(legacyData data: Data) { + self.init(code: .success, versionData: data) + } + + private init?(code: ResponseCode, versionData: Data) { + self.code = code + + guard let version = String(bytes: versionData, encoding: .utf8) else { + return nil + } + + self.version = version + } +} + +struct PacketResponse: Response { + let code: ResponseCode + let packet: RFPacket? + + init?(data: Data) { + guard data.count > 0, let code = ResponseCode(rawValue: data[data.startIndex]) else { + return nil + } + + switch code { + case .success: + guard let packet = RFPacket(rfspyResponse: data[data.startIndex.advanced(by: 1)...]) else { + return nil + } + self.packet = packet + case .rxTimeout, + .commandInterrupted, + .zeroData, + .invalidParam, + .unknownCommand: + self.packet = nil + } + + self.code = code + } + + init?(legacyData data: Data) { + guard data.count > 0 else { + return nil + } + + packet = RFPacket(rfspyResponse: data) + + if packet != nil { + code = .success + } else { + guard let code = ResponseCode(rawValue: data[data.startIndex]) else { + return nil + } + + self.code = code + } + } + + init(code: ResponseCode, packet: RFPacket?) { + self.code = code + self.packet = packet + } +} diff --git a/RileyLinkBLEKit/ResponseBuffer.swift b/RileyLinkBLEKit/ResponseBuffer.swift new file mode 100644 index 000000000..3fa388098 --- /dev/null +++ b/RileyLinkBLEKit/ResponseBuffer.swift @@ -0,0 +1,32 @@ +// +// ResponseBuffer.swift +// RileyLinkBLEKit +// +// Copyright © 2018 Pete Schwamb. All rights reserved. +// + + +/// Represents a data buffer containing one or more responses +struct ResponseBuffer { + let endMarker: UInt8 + private var data = Data() + + init(endMarker: UInt8) { + self.endMarker = endMarker + } + + mutating func append(_ other: Data) { + data.append(other) + } + + var responses: [R] { + let segments = data.split(separator: endMarker, omittingEmptySubsequences: false) + + // If we haven't received at least one endMarker, we don't have a response. + guard segments.count > 1 else { + return [] + } + + return segments.compactMap { R(legacyData: $0) } + } +} diff --git a/RileyLinkBLEKit/RileyLinkBLEDevice.h b/RileyLinkBLEKit/RileyLinkBLEDevice.h deleted file mode 100644 index 9063f6190..000000000 --- a/RileyLinkBLEKit/RileyLinkBLEDevice.h +++ /dev/null @@ -1,106 +0,0 @@ -// -// RileyLinkBLE.h -// RileyLink -// -// Created by Pete Schwamb on 7/28/15. -// Copyright (c) 2015 Pete Schwamb. All rights reserved. -// - -@import Foundation; -@import CoreBluetooth; -#import "CmdBase.h" - -typedef NS_ENUM(NSUInteger, RileyLinkState) { - RileyLinkStateConnecting, - RileyLinkStateConnected, - RileyLinkStateDisconnected -}; - -extern NSString * _Nonnull const SubgRfspyErrorDomain; - -typedef NS_ENUM(NSUInteger, SubgRfspyError) { - SubgRfspyErrorRxTimeout = 0xaa, - SubgRfspyErrorCmdInterrupted = 0xbb, - SubgRfspyErrorZeroData = 0xcc -}; - -typedef NS_ENUM(NSUInteger, SubgRfspyVersionState) { - SubgRfspyVersionStateUnknown = 0, - SubgRfspyVersionStateUpToDate, - SubgRfspyVersionStateOutOfDate, - SubgRfspyVersionStateInvalid -}; - - -#define ERROR_RX_TIMEOUT 0xaa -#define ERROR_CMD_INTERRUPTED 0xbb -#define ERROR_ZERO_DATA 0xcc - -#define RILEYLINK_FREQ_XTAL 24000000 - -#define CC111X_REG_FREQ2 0x09 -#define CC111X_REG_FREQ1 0x0A -#define CC111X_REG_FREQ0 0x0B -#define CC111X_REG_MDMCFG4 0x0C -#define CC111X_REG_MDMCFG3 0x0D -#define CC111X_REG_MDMCFG2 0x0E -#define CC111X_REG_MDMCFG1 0x0F -#define CC111X_REG_MDMCFG0 0x10 -#define CC111X_REG_DEVIATN 0x11 -#define CC111X_REG_AGCCTRL2 0x17 -#define CC111X_REG_AGCCTRL1 0x18 -#define CC111X_REG_AGCCTRL0 0x19 -#define CC111X_REG_FREND1 0x1A -#define CC111X_REG_FREND0 0x1B - - -@interface RileyLinkCmdSession : NSObject -/** - Runs a command synchronously. I.E. this method will not return until the command - finishes, or times out. Returns NO if the command timed out. The command's response - is set if the command did not time out. - */ -- (BOOL) doCmd:(nonnull CmdBase*)cmd withTimeoutMs:(NSInteger)timeoutMS; -@end - -@interface RileyLinkBLEDevice : NSObject - -@property (nonatomic, nullable, readonly) NSString * name; -@property (nonatomic, nullable, strong) NSNumber * RSSI; -@property (nonatomic, nonnull, readonly) NSString * peripheralId; -@property (nonatomic, nonnull, strong) CBPeripheral * peripheral; - -@property (nonatomic, readonly) RileyLinkState state; - -@property (nonatomic, readonly, copy, nonnull) NSString * deviceURI; - -@property (nonatomic, readonly, nullable) NSString *firmwareVersion; - -@property (nonatomic, readonly) SubgRfspyVersionState firmwareState; - -@property (nonatomic, readonly, nullable) NSString *bleFirmwareVersion; - -@property (nonatomic, readonly, nullable) NSDate *lastIdle; - -@property (nonatomic) BOOL timerTickEnabled; - -@property (nonatomic) uint32_t idleTimeoutMS; - -/** - Initializes the device with a specified peripheral - - @param peripheral The peripheral to represent - - @return A newly-initialized device - */ -- (nonnull instancetype)initWithPeripheral:(nonnull CBPeripheral *)peripheral NS_DESIGNATED_INITIALIZER; - -- (void) connectionStateDidChange:(nullable NSError *)error; - -- (void) runSessionWithName:(nonnull NSString*)name usingBlock:(void (^ _Nonnull)(RileyLinkCmdSession* _Nonnull))proc; -- (void) setCustomName:(nonnull NSString*)customName; -- (void) enableIdleListeningOnChannel:(uint8_t)channel; -- (void) disableIdleListening; -- (void) assertIdleListeningForcingRestart:(BOOL)forceRestart; - -@end diff --git a/RileyLinkBLEKit/RileyLinkBLEDevice.m b/RileyLinkBLEKit/RileyLinkBLEDevice.m deleted file mode 100644 index 3a7ddeecc..000000000 --- a/RileyLinkBLEKit/RileyLinkBLEDevice.m +++ /dev/null @@ -1,576 +0,0 @@ -// -// RileyLinkBLE.m -// RileyLink -// -// Created by Pete Schwamb on 7/28/15. -// Copyright (c) 2015 Pete Schwamb. All rights reserved. -// - -@import os.log; - -#import "RileyLinkBLEDevice.h" -#import "RileyLinkBLEManager.h" -#import "NSData+Conversion.h" -#import "SendAndListenCmd.h" -#import "GetPacketCmd.h" -#import "GetVersionCmd.h" -#import "RFPacket.h" - - -NSString * const SubgRfspyErrorDomain = @"SubgRfspyErrorDomain"; - - -// See impl at bottom of file. -@interface RileyLinkCmdSession () -@property (nonatomic, weak) RileyLinkBLEDevice *device; -@end - - -@interface RileyLinkBLEDevice () { - CBCharacteristic *dataCharacteristic; - CBCharacteristic *responseCountCharacteristic; - CBCharacteristic *customNameCharacteristic; - CBCharacteristic *timerTickCharacteristic; - CBCharacteristic *firmwareVersionCharacteristic; - NSMutableArray *incomingPackets; - NSMutableData *inBuf; - NSData *endOfResponseMarker; - BOOL idleListeningEnabled; - uint8_t idleListenChannel; - BOOL fetchingResponse; - CmdBase *currentCommand; - BOOL runningIdle; - BOOL runningSession; - BOOL ready; - BOOL haveResponseCount; - dispatch_group_t cmdDispatchGroup; - dispatch_group_t idleDetectDispatchGroup; -} - -@property (nonatomic, nonnull, strong) dispatch_queue_t serialDispatchQueue; - -@end - - -@implementation RileyLinkBLEDevice - -@synthesize peripheral = _peripheral; -@synthesize lastIdle = _lastIdle; -@synthesize firmwareVersion = _firmwareVersion; -@synthesize bleFirmwareVersion = _bleFirmwareVersion; - -- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral -{ - self = [super init]; - if (self) { - // All processes that interact that run commands on this device should be serialized through - // this queue. - _serialDispatchQueue = dispatch_queue_create("com.rileylink.rlbledevice", DISPATCH_QUEUE_SERIAL); - - cmdDispatchGroup = dispatch_group_create(); - idleDetectDispatchGroup = dispatch_group_create(); - - _idleTimeoutMS = 60 * 1000; - _timerTickEnabled = YES; - - incomingPackets = [NSMutableArray array]; - - inBuf = [NSMutableData data]; - endOfResponseMarker = [NSData dataWithHexadecimalString:@"00"]; - - [self setPeripheral:peripheral]; - } - return self; -} - -- (instancetype)init NS_UNAVAILABLE -{ - return nil; -} - -- (void)setPeripheral:(CBPeripheral *)peripheral { - if (peripheral == _peripheral) { - return; - } - - if (_peripheral != nil) { - os_log_error(OS_LOG_DEFAULT, "RileyLinkBLEDevice: Replacing peripheral reference %{public}@ -> %{public}@", _peripheral.identifier.UUIDString, peripheral.identifier.UUIDString); - } - - _peripheral.delegate = nil; - - _peripheral = peripheral; - _peripheral.delegate = self; - - for (CBService *service in _peripheral.services) { - [self setCharacteristicsFromService:service]; - } -} - -- (NSString *)name -{ - return self.peripheral.name; -} - -- (NSString *)peripheralId -{ - return self.peripheral.identifier.UUIDString; -} - -- (void)setTimerTickEnabled:(BOOL)timerTickEnabled -{ - _timerTickEnabled = timerTickEnabled; - - if (timerTickCharacteristic != nil && - timerTickCharacteristic.isNotifying != timerTickEnabled && - self.peripheral.state == CBPeripheralStateConnected) - { - [self.peripheral setNotifyValue:_timerTickEnabled - forCharacteristic:timerTickCharacteristic]; - } -} - -- (void) runSessionWithName:(nonnull NSString*)name usingBlock:(void (^ _Nonnull)(RileyLinkCmdSession* _Nonnull))proc { - dispatch_group_enter(idleDetectDispatchGroup); - RileyLinkCmdSession *session = [[RileyLinkCmdSession alloc] init]; - session.device = self; - dispatch_async(_serialDispatchQueue, ^{ - runningSession = YES; - NSLog(@"======================== %@ ===========================", name); - proc(session); - NSLog(@"------------------------ %@ ---------------------------", name); - runningSession = NO; - dispatch_group_leave(idleDetectDispatchGroup); - }); - - dispatch_group_notify(idleDetectDispatchGroup, - dispatch_get_main_queue(), ^{ - NSLog(@"idleDetectDispatchGroup empty"); - [self assertIdleListeningForcingRestart:NO]; - }); -} - -- (BOOL) doCmd:(nonnull CmdBase*)cmd withTimeoutMs:(NSInteger)timeoutMS { - dispatch_group_enter(cmdDispatchGroup); - BOOL timedOut = NO; - currentCommand = cmd; - [self issueCommand:cmd]; - dispatch_time_t timeoutAt = dispatch_time(DISPATCH_TIME_NOW, timeoutMS * NSEC_PER_MSEC); - if (dispatch_group_wait(cmdDispatchGroup,timeoutAt) != 0) { - NSLog(@"No response from RileyLink... timing out command."); - if (dataCharacteristic != nil) { - [self.peripheral readValueForCharacteristic:dataCharacteristic]; - } - dispatch_group_leave(cmdDispatchGroup); - currentCommand = nil; - timedOut = YES; - } - return !timedOut; -} - -- (void) issueCommand:(nonnull CmdBase*)cmd { - if (dataCharacteristic == nil) { - NSLog(@"Ignoring command issued before we have discovered characteristics"); - return; - } - NSLog(@"Writing command to data characteristic: %@", [cmd.data hexadecimalString]); - // 255 is the real limit (buf limit in bgscript), but we set the limit at 220, as we need room for escaping special chars. - if (cmd.data.length > 220) { - NSLog(@"********** Warning: packet too large: %zd bytes ************", cmd.data.length); - } else { - uint8_t count = cmd.data.length; - NSMutableData *outBuf = [NSMutableData dataWithBytes:&count length:1]; - [outBuf appendData:cmd.data]; - [self.peripheral writeValue:outBuf forCharacteristic:dataCharacteristic type:CBCharacteristicWriteWithResponse]; - } -} - -- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { - if (error) { - NSLog(@"Could not write characteristic: %@", error); - return; - } - if (characteristic == customNameCharacteristic) { - [[NSNotificationCenter defaultCenter] postNotificationName:RILEYLINK_EVENT_NAME_CHANGED object:nil]; - } - //NSLog(@"Did write characteristic: %@", characteristic.UUID); -} - -- (RileyLinkState) state { - RileyLinkState rval; - switch (self.peripheral.state) { - case CBPeripheralStateConnected: - rval = RileyLinkStateConnected; - break; - case CBPeripheralStateConnecting: - rval = RileyLinkStateConnecting; - break; - default: - rval = RileyLinkStateDisconnected; - break; - } - return rval; -} - -- (void)connectionStateDidChange:(NSError *)error -{ - switch (self.peripheral.state) { - case CBPeripheralStateConnected: - [self assertIdleListeningForcingRestart:NO]; - break; - case CBPeripheralStateDisconnected: - runningIdle = NO; - runningSession = NO; - break; - case CBPeripheralStateConnecting: - case CBPeripheralStateDisconnecting: - break; - } -} - -- (void)setCharacteristicsFromService:(CBService *)service { - for (CBCharacteristic *characteristic in service.characteristics) { - if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:RILEYLINK_RESPONSE_COUNT_UUID]]) { - [self.peripheral setNotifyValue:YES forCharacteristic:characteristic]; - responseCountCharacteristic = characteristic; - [self.peripheral readValueForCharacteristic:characteristic]; - } else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:RILEYLINK_DATA_UUID]]) { - dataCharacteristic = characteristic; - } else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:RILEYLINK_CUSTOM_NAME_UUID]]) { - customNameCharacteristic = characteristic; - } else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:RILEYLINK_TIMER_TICK_UUID]]) { - [self.peripheral setNotifyValue:_timerTickEnabled forCharacteristic:characteristic]; - timerTickCharacteristic = characteristic; - } else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:RILEYLINK_FIRMWARE_VERSION_UUID]]) { - firmwareVersionCharacteristic = characteristic; - [service.peripheral readValueForCharacteristic:firmwareVersionCharacteristic]; - } - } - - NSDictionary *attrs = @{@"peripheral": self.peripheral}; - [[NSNotificationCenter defaultCenter] postNotificationName:RILEYLINK_EVENT_DEVICE_ATTRS_DISCOVERED object:self userInfo:attrs]; -} - -- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error { - if (error) { - NSLog(@"Failure while discovering services: %@", error); - return; - } - //NSLog(@"didDiscoverServices: %@, %@", peripheral, peripheral.services); - for (CBService *service in peripheral.services) { - if ([service.UUID isEqual:[CBUUID UUIDWithString:RILEYLINK_SERVICE_UUID]]) { - [peripheral discoverCharacteristics:[RileyLinkBLEManager UUIDsFromUUIDStrings:@[RILEYLINK_RESPONSE_COUNT_UUID, - RILEYLINK_DATA_UUID, - RILEYLINK_CUSTOM_NAME_UUID, - RILEYLINK_TIMER_TICK_UUID, - RILEYLINK_FIRMWARE_VERSION_UUID] - excludingAttributes:service.characteristics] - forService:service]; - } - } - // Discover other characteristics -} - -- (void)peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)RSSI error:(NSError *)error { - if (error != nil) { - NSLog(@"Error reading RSSI: %@", [error localizedDescription]); - } else { - [[NSNotificationCenter defaultCenter] postNotificationName:RILEYLINK_EVENT_RSSI_CHANGED object:self userInfo:@{@"RSSI": RSSI}]; - self.RSSI = RSSI; - } -} - -- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error { - os_log_debug(OS_LOG_DEFAULT, "%{public}s", __PRETTY_FUNCTION__); - - if (error) { - os_log_error(OS_LOG_DEFAULT, "Error discovering characteristics for service: %{public}@", error); - [self cleanup]; - return; - } - - [self setCharacteristicsFromService:service]; -} - -- (void)peripheralDidUpdateName:(CBPeripheral *)peripheral { - [[NSNotificationCenter defaultCenter] postNotificationName:RILEYLINK_EVENT_NAME_CHANGED object:self userInfo:@{@"Name": peripheral.name}]; -} - -- (void)checkVersion { - [self runSessionWithName:@"Check version" usingBlock:^(RileyLinkCmdSession * _Nonnull s) { - GetVersionCmd *cmd = [[GetVersionCmd alloc] init]; - NSString *foundVersion; - BOOL versionOK = NO; - // We run two commands here, to flush out responses to any old commands - [s doCmd:cmd withTimeoutMs:5000]; - if ([s doCmd:cmd withTimeoutMs:1000]) { - foundVersion = [[NSString alloc] initWithData:cmd.response encoding:NSUTF8StringEncoding]; - NSLog(@"Got version: %@", foundVersion); - - switch ([self firmwareStateForVersionString:foundVersion]) { - case SubgRfspyVersionStateUnknown: - case SubgRfspyVersionStateInvalid: - NSLog(@"Unable to parse version... expecting 0.x, found %@", foundVersion); - break; - case SubgRfspyVersionStateUpToDate: - versionOK = YES; - break; - case SubgRfspyVersionStateOutOfDate: - NSLog(@"The firmware version on this RileyLink is out of date. Found version\"%@\". Please use subg_rfspy version 0.7 or newer.", foundVersion); - break; - } - - _firmwareVersion = foundVersion; - } else { - NSLog(@"Unable to retrieve version from RileyLink. Get version command timed out."); - } - - if (versionOK) { - ready = YES; - [[NSNotificationCenter defaultCenter] postNotificationName:RILEYLINK_EVENT_DEVICE_READY object:self]; - } - }]; -} - -- (SubgRfspyVersionState)firmwareStateForVersionString:(NSString *)firmwareVersion -{ - if (firmwareVersion == nil) { - return SubgRfspyVersionStateUnknown; - } - - NSRange range = [firmwareVersion rangeOfString:@"subg_rfspy"]; - - if (range.location == 0 && firmwareVersion.length > 11) { - NSString *numberPart = [firmwareVersion substringFromIndex:11]; - NSArray *versionComponents = [numberPart componentsSeparatedByString:@"."]; - - if (versionComponents.count > 1) { - NSInteger major = [versionComponents[0] integerValue]; - NSInteger minor = [versionComponents[1] integerValue]; - - if (major <= 0 && minor < 8) { - return SubgRfspyVersionStateOutOfDate; - } else { - return SubgRfspyVersionStateUpToDate; - } - } else { - return SubgRfspyVersionStateInvalid; - } - } else { - return SubgRfspyVersionStateInvalid; - } -} - -- (SubgRfspyVersionState)firmwareState -{ - return [self firmwareStateForVersionString:_firmwareVersion]; -} - -- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { - if (error) { - NSLog(@"Error updating %@: %@", characteristic, error); - return; - } - //NSLog(@"didUpdateValueForCharacteristic: %@", characteristic); - - if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:RILEYLINK_DATA_UUID]]) { - [self dataReceivedFromRL:characteristic.value]; - } else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:RILEYLINK_RESPONSE_COUNT_UUID]]) { - if (!haveResponseCount) { - // The first time we get a notice on this is just from connecting. - haveResponseCount = YES; - [self checkVersion]; - } else { - const unsigned char responseCount = ((const unsigned char*)(characteristic.value).bytes)[0]; - NSLog(@"Updated response count: %d", responseCount); - [peripheral readValueForCharacteristic:dataCharacteristic]; - } - } else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:RILEYLINK_TIMER_TICK_UUID]]) { - const unsigned char timerTick = ((const unsigned char*)(characteristic.value).bytes)[0]; - [self assertIdleListeningForcingRestart:NO]; - [[NSNotificationCenter defaultCenter] postNotificationName:RILEYLINK_EVENT_DEVICE_TIMER_TICK object:self]; - NSLog(@"Updated timer tick: %d", timerTick); - } else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:RILEYLINK_FIRMWARE_VERSION_UUID]]) { - _bleFirmwareVersion = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding]; - } -} - -- (void)dataReceivedFromRL:(NSData*) data { - //NSLog(@"******* New Data: %@", [data hexadecimalString]); - [inBuf appendData:data]; - - while(inBuf.length > 0) { - NSRange endOfResp = [inBuf rangeOfData:endOfResponseMarker options:0 range:NSMakeRange(0, inBuf.length)]; - - NSData *fullResponse; - if (endOfResp.location != NSNotFound) { - fullResponse = [inBuf subdataWithRange:NSMakeRange(0, endOfResp.location)]; - //NSLog(@"******* Full response: %@", [fullResponse hexadecimalString]); - NSInteger remainder = inBuf.length - endOfResp.location - 1; - if (remainder > 0) { - inBuf = [[inBuf subdataWithRange:NSMakeRange(endOfResp.location+1, remainder)] mutableCopy]; - //NSLog(@"******* Remainder: %@", [inBuf hexadecimalString]); - } else { - inBuf = [NSMutableData data]; - } - } else { - //NSLog(@"******* Buffering: %@", [inBuf hexadecimalString]); - } - - if (fullResponse) { - if (runningIdle) { - NSLog(@"Response to idle: %@", [fullResponse hexadecimalString]); - runningIdle = NO; - [self handleIdleListenerResponse:fullResponse error:nil]; - if (!runningSession) { - if (inBuf.length > 0) { - NSLog(@"clearing unexpected buffer data: %@", [inBuf hexadecimalString]); - inBuf = [NSMutableData data]; - } - [self onIdle]; - } - } else if (currentCommand) { - NSLog(@"Response to command: %@", [fullResponse hexadecimalString]); - currentCommand.response = fullResponse; - if (inBuf.length > 0) { - // This happens when connecting to a RL that is still running a command - // from a previous connection. - NSLog(@"Dropping extraneous data: %@", [inBuf hexadecimalString]); - inBuf.length = 0; - } - currentCommand = nil; - dispatch_group_leave(cmdDispatchGroup); - } else { - NSLog(@"Received data but no outstanding command!"); - inBuf.length = 0; - } - } else { - break; - } - } -} - -- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { - NSLog(@"Updated notification state for %@, %@", characteristic, error); -} - -- (void)cleanup { - NSLog(@"Entering cleanup"); - - // See if we are subscribed to a characteristic on the peripheral - for (CBService *service in self.peripheral.services) { - for (CBCharacteristic *characteristic in service.characteristics) { - if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:RILEYLINK_RESPONSE_COUNT_UUID]]) { - if (characteristic.isNotifying) { - [self.peripheral setNotifyValue:NO forCharacteristic:characteristic]; - return; - } - } - } - } - - dataCharacteristic = nil; - responseCountCharacteristic = nil; - customNameCharacteristic = nil; - firmwareVersionCharacteristic = nil; -} - -- (NSString*) deviceURI { - return [@"rileylink://" stringByAppendingString:self.name]; -} - -- (void) setCustomName:(nonnull NSString*)customName { - if (customNameCharacteristic) { - NSData *data = [customName dataUsingEncoding:NSUTF8StringEncoding]; - [self.peripheral writeValue:data forCharacteristic:customNameCharacteristic type:CBCharacteristicWriteWithResponse]; - } else { - NSLog(@"Missing customNameCharacteristic"); - } -} - -- (void) onIdle { - if (idleListeningEnabled && _peripheral.state == CBPeripheralStateConnected) { - runningIdle = YES; - NSLog(@"Starting idle RX"); - GetPacketCmd *cmd = [[GetPacketCmd alloc] init]; - cmd.listenChannel = idleListenChannel; - cmd.timeoutMS = _idleTimeoutMS; - [self issueCommand:cmd]; - - _lastIdle = [NSDate date]; - } -} - -- (void) enableIdleListeningOnChannel:(uint8_t)channel { - idleListeningEnabled = YES; - idleListenChannel = channel; - - [self assertIdleListeningForcingRestart:NO]; -} - -- (void) disableIdleListening { - idleListeningEnabled = NO; - runningIdle = NO; -} - -- (void) assertIdleListeningForcingRestart:(BOOL)forceRestart { - if (idleListeningEnabled && !runningSession && _peripheral.state == CBPeripheralStateConnected) { - NSTimeInterval resetIdleAfterInterval = 2.0 * (float)_idleTimeoutMS / 1000.0; - - if (forceRestart || !runningIdle || ([[NSDate dateWithTimeIntervalSinceNow:-resetIdleAfterInterval] compare:_lastIdle] == NSOrderedDescending)) { - [self onIdle]; - } - } -} - -- (BOOL) handleIdleListenerResponse:(NSData *)response error:(NSError **)errorOut { - if (response.length > 3) { - // This is a response to our idle listen command - RFPacket *packet = [[RFPacket alloc] initWithRFSPYResponse:response]; - if (packet.data) { - packet.capturedAt = [NSDate date]; - NSLog(@"Read packet (%d): %zd bytes", packet.rssi, packet.data.length); - NSDictionary *attrs = @{ - @"packet": packet, - @"peripheral": self.peripheral, - }; - [[NSNotificationCenter defaultCenter] postNotificationName:RILEYLINK_IDLE_RESPONSE_RECEIVED object:self userInfo:attrs]; - } - return YES; - } else if (response.length > 0) { - uint8_t errorCode = ((uint8_t*)response.bytes)[0]; - switch (errorCode) { - case SubgRfspyErrorRxTimeout: - NSLog(@"Idle rx timeout"); - break; - case SubgRfspyErrorCmdInterrupted: - NSLog(@"Idle rx command interrupted."); - break; - case SubgRfspyErrorZeroData: - NSLog(@"Idle rx zero data?!?!"); - break; - default: - NSLog(@"Unexpected response to idle rx command: %@", [response hexadecimalString]); - break; - } - - if (errorOut != NULL) { - *errorOut = [NSError errorWithDomain:SubgRfspyErrorDomain code:errorCode userInfo:nil]; - } - - return NO; - } else { - NSLog(@"Idle command got empty response!!"); - return YES; - } -} - -@end - -@implementation RileyLinkCmdSession -- (BOOL) doCmd:(nonnull CmdBase*)cmd withTimeoutMs:(NSInteger)timeoutMS { - return [_device doCmd:cmd withTimeoutMs:timeoutMS]; -} -@end - - diff --git a/RileyLinkBLEKit/RileyLinkBLEKit.h b/RileyLinkBLEKit/RileyLinkBLEKit.h index 64ce668ea..191e90389 100644 --- a/RileyLinkBLEKit/RileyLinkBLEKit.h +++ b/RileyLinkBLEKit/RileyLinkBLEKit.h @@ -2,11 +2,10 @@ // RileyLinkBLEKit.h // RileyLinkBLEKit // -// Created by Nathan Racklyeft on 4/8/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. +// Copyright © 2017 Pete Schwamb. All rights reserved. // -#import +#import //! Project version number for RileyLinkBLEKit. FOUNDATION_EXPORT double RileyLinkBLEKitVersionNumber; @@ -14,10 +13,6 @@ FOUNDATION_EXPORT double RileyLinkBLEKitVersionNumber; //! Project version string for RileyLinkBLEKit. FOUNDATION_EXPORT const unsigned char RileyLinkBLEKitVersionString[]; -#import -#import -#import -#import -#import -#import -#import +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/RileyLinkBLEKit/RileyLinkBLEManager.h b/RileyLinkBLEKit/RileyLinkBLEManager.h deleted file mode 100644 index aa94fd678..000000000 --- a/RileyLinkBLEKit/RileyLinkBLEManager.h +++ /dev/null @@ -1,55 +0,0 @@ -// -// RileyLink.h -// RileyLink -// -// Created by Pete Schwamb on 8/5/14. -// Copyright (c) 2014 Pete Schwamb. All rights reserved. -// - -@import CoreBluetooth; -@import Foundation; - -#define RILEYLINK_EVENT_DEVICE_CREATED @"RILEYLINK_EVENT_DEVICE_CREATED" -#define RILEYLINK_IDLE_RESPONSE_RECEIVED @"RILEYLINK_IDLE_RESPONSE_RECEIVED" -#define RILEYLINK_EVENT_DEVICE_CONNECTED @"RILEYLINK_EVENT_DEVICE_CONNECTED" -#define RILEYLINK_EVENT_DEVICE_DISCONNECTED @"RILEYLINK_EVENT_DEVICE_DISCONNECTED" -#define RILEYLINK_EVENT_DEVICE_ATTRS_DISCOVERED @"RILEYLINK_EVENT_DEVICE_ATTRS_DISCOVERED" -#define RILEYLINK_EVENT_DEVICE_READY @"RILEYLINK_EVENT_DEVICE_READY" -#define RILEYLINK_EVENT_DEVICE_TIMER_TICK @"RILEYLINK_EVENT_DEVICE_TIMER_TICK" -#define RILEYLINK_EVENT_RSSI_CHANGED @"RILEYLINK_EVENT_RSSI_CHANGED" -#define RILEYLINK_EVENT_NAME_CHANGED @"RILEYLINK_EVENT_NAME_CHANGED" - -#define RILEYLINK_SERVICE_UUID @"0235733b-99c5-4197-b856-69219c2a3845" -#define RILEYLINK_DATA_UUID @"c842e849-5028-42e2-867c-016adada9155" -#define RILEYLINK_RESPONSE_COUNT_UUID @"6e6c7910-b89e-43a5-a0fe-50c5e2b81f4a" -#define RILEYLINK_CUSTOM_NAME_UUID @"d93b2af0-1e28-11e4-8c21-0800200c9a66" -#define RILEYLINK_TIMER_TICK_UUID @"6e6c7910-b89e-43a5-78af-50c5e2b86f7e" -#define RILEYLINK_FIRMWARE_VERSION_UUID @"30d99dc9-7c91-4295-a051-0a104d238cf2" - -@class RileyLinkBLEDevice; - -@interface RileyLinkBLEManager : NSObject - -@property (nonatomic, nonnull, readonly, copy) NSArray *rileyLinkList; - -- (void)connectDevice:(nonnull RileyLinkBLEDevice *)device; -- (void)disconnectDevice:(nonnull RileyLinkBLEDevice *)device; - -- (nonnull instancetype)initWithAutoConnectIDs:(nonnull NSSet *)autoConnectIDs; - -@property (nonatomic, nonnull, readonly) NSSet *autoConnectIDs; - -- (void)setScanningEnabled:(BOOL)scanningEnabled; - -/** - Converts an array of UUID strings to CBUUID objects, excluding those represented in an array of CBAttribute objects. - - @param UUIDStrings An array of UUID string representations to filter - @param attributes An array of CBAttribute objects to exclude - - @return An array of CBUUID objects - */ -+ (nonnull NSArray *)UUIDsFromUUIDStrings:(nonnull NSArray *)UUIDStrings excludingAttributes:(nullable NSArray *)attributes; - -@end - diff --git a/RileyLinkBLEKit/RileyLinkBLEManager.m b/RileyLinkBLEKit/RileyLinkBLEManager.m deleted file mode 100644 index 9f9a99302..000000000 --- a/RileyLinkBLEKit/RileyLinkBLEManager.m +++ /dev/null @@ -1,279 +0,0 @@ -// -// RileyLink.m -// RileyLink -// -// Created by Pete Schwamb on 8/5/14. -// Copyright (c) 2014 Pete Schwamb. All rights reserved. -// - -#import "RileyLinkBLEManager.h" -#import -#import "RileyLinkBLEDevice.h" - -@interface RileyLinkBLEManager () { - NSMutableDictionary *_devicesById; // RileyLinkBLEDevices by UUID - NSMutableSet *_autoConnectIDs; - BOOL _scanningEnabled; -} - -@property (nonnull, strong, nonatomic) CBCentralManager *centralManager; - -@end - - -@implementation RileyLinkBLEManager - -+ (NSArray *)UUIDsFromUUIDStrings:(NSArray *)UUIDStrings - excludingAttributes:(NSArray *)attributes { - NSMutableArray *unmatchedUUIDStrings = [UUIDStrings mutableCopy]; - - for (CBAttribute *attribute in attributes) { - [unmatchedUUIDStrings removeObject:attribute.UUID.UUIDString]; - } - - NSMutableArray *UUIDs = [NSMutableArray array]; - - for (NSString *UUIDString in unmatchedUUIDStrings) { - [UUIDs addObject:[CBUUID UUIDWithString:UUIDString]]; - } - - return [NSArray arrayWithArray:UUIDs]; -} - -- (instancetype)initWithAutoConnectIDs:(NSSet *)autoConnectIDs -{ - self = [super init]; - if (self) { - _centralManager = [[CBCentralManager alloc] initWithDelegate:self - queue:nil - options:@{CBCentralManagerOptionRestoreIdentifierKey: @"com.rileylink.CentralManager"}]; - - _devicesById = [NSMutableDictionary dictionary]; - _autoConnectIDs = [autoConnectIDs mutableCopy]; - _scanningEnabled = NO; - } - return self; -} - -#pragma mark - - -- (NSArray *)rileyLinkList { - return _devicesById.allValues; -} - -- (RileyLinkBLEDevice *)addPeripheralToDeviceList:(CBPeripheral *)peripheral RSSI:(NSNumber *)RSSI { - RileyLinkBLEDevice *device = _devicesById[peripheral.identifier.UUIDString]; - if (device == nil) { - device = [[RileyLinkBLEDevice alloc] initWithPeripheral:peripheral]; - _devicesById[peripheral.identifier.UUIDString] = device; - NSLog(@"RILEYLINK_EVENT_DEVICE_CREATED"); - [[NSNotificationCenter defaultCenter] postNotificationName:RILEYLINK_EVENT_DEVICE_CREATED object:self userInfo:@{@"device": device}]; - } else { - device.peripheral = peripheral; - } - - if (RSSI != nil) { - device.RSSI = RSSI; - } - - if ([_autoConnectIDs containsObject:device.peripheralId]) { - [self connectPeripheral:device.peripheral]; - } - - return device; -} - -- (void)setScanningEnabled:(BOOL)scanningEnabled { - _scanningEnabled = scanningEnabled; - - if (_centralManager.state == CBManagerStatePoweredOn) { - if (_scanningEnabled) { - [self startScan]; - } else if (_centralManager.isScanning) { - [_centralManager stopScan]; - } - } -} - -- (void)addPeripheralToAutoConnectList:(CBPeripheral *)peripheral { - [_autoConnectIDs addObject:peripheral.identifier.UUIDString]; -} - -- (void)removePeripheralFromAutoConnectList:(CBPeripheral *)peripheral { - [_autoConnectIDs removeObject:peripheral.identifier.UUIDString]; -} - -- (BOOL)hasDiscoveredAllAutoConnectPeripherals -{ - return [_autoConnectIDs isSubsetOfSet:[NSSet setWithArray:_devicesById.allKeys]]; -} - -#pragma mark - - -- (void)startScan { - [self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:RILEYLINK_SERVICE_UUID]] options:NULL]; - - NSLog(@"Scanning started (state = %zd)", self.centralManager.state); -} - -- (void)connectDevice:(RileyLinkBLEDevice *)device -{ - CBPeripheral *peripheral = [_centralManager retrievePeripheralsWithIdentifiers:@[device.peripheral.identifier]].firstObject; - - if (peripheral != nil) { - device.peripheral = peripheral; - - [self connectPeripheral:peripheral]; - } -} - -- (void)connectPeripheral:(CBPeripheral *)peripheral -{ - if (_centralManager.state == CBManagerStatePoweredOn) { - if (peripheral.state != CBPeripheralStateConnected) { - NSLog(@"Connecting to peripheral %zd:%@", _centralManager.state, peripheral); - [_centralManager connectPeripheral:peripheral options:nil]; - } else { - NSLog(@"Skipped request to connect to %@:%@", _centralManager, peripheral); - [self centralManager:_centralManager didConnectPeripheral:peripheral]; - } - } - - [self addPeripheralToAutoConnectList:peripheral]; -} - -- (void)disconnectDevice:(RileyLinkBLEDevice *)device -{ - CBPeripheral *peripheral = [_centralManager retrievePeripheralsWithIdentifiers:@[device.peripheral.identifier]].firstObject; - - if (peripheral != nil) { - device.peripheral = peripheral; - - [self disconnectPeripheral:peripheral]; - } -} - -- (void)disconnectPeripheral:(CBPeripheral *)peripheral -{ - if (_centralManager.state == CBManagerStatePoweredOn) { - if (peripheral.state != CBPeripheralStateDisconnected) { - NSLog(@"Disconnecting from peripheral %@", peripheral); - [_centralManager cancelPeripheralConnection:peripheral]; - } else { - NSLog(@"Skipped request to disconnect from %@:%@", _centralManager, peripheral); - [self centralManager:_centralManager didDisconnectPeripheral:peripheral error:nil]; - } - } - - [self removePeripheralFromAutoConnectList:peripheral]; -} - -- (void)attemptReconnectForDisconnectedDevices { - for (RileyLinkBLEDevice *device in _devicesById.allValues) { - if ([_autoConnectIDs containsObject:device.peripheralId]) { - NSLog(@"Attempting reconnect to %@", device); - [self connectDevice:device]; - } - } -} - -#pragma mark - CBCentralManagerDelegate - -- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary *)dict { - NSLog(@"in willRestoreState: awoken from bg to handle ble updates"); - NSArray *peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey]; - - for (CBPeripheral *peripheral in peripherals) { - [self addPeripheralToDeviceList:peripheral RSSI:nil]; - } -} - -- (void)centralManagerDidUpdateState:(CBCentralManager *)central { - if (central.state == CBManagerStatePoweredOn) { - [self attemptReconnectForDisconnectedDevices]; - - if (![self hasDiscoveredAllAutoConnectPeripherals] || _scanningEnabled) { - [self startScan]; - } else if (central.isScanning) { - [central stopScan]; - } - } -} - -- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { - - NSLog(@"Discovered %@ at %@", peripheral.name, RSSI); - - NSString *localName = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey]; - NSLog(@"localName = %@", localName); - - - [self addPeripheralToDeviceList:peripheral RSSI:RSSI]; - - if (!_scanningEnabled && [self hasDiscoveredAllAutoConnectPeripherals] && central.isScanning) { - NSLog(@"All peripherals discovered. Scanning stopped (state = %zd)", self.centralManager.state); - [central stopScan]; - } -} - -- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { - NSLog(@"Failed to connect to peripheral: %@", error); - - RileyLinkBLEDevice *device = _devicesById[peripheral.identifier.UUIDString]; - [device connectionStateDidChange:error]; - - [self attemptReconnectForDisconnectedDevices]; -} - -- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { - NSLog(@"%s", __PRETTY_FUNCTION__); - - NSArray *servicesToDiscover = [[self class] UUIDsFromUUIDStrings:@[RILEYLINK_SERVICE_UUID] - excludingAttributes:peripheral.services]; - if (servicesToDiscover.count) { - NSLog(@"Discovering services"); - [peripheral discoverServices:servicesToDiscover]; - } - - RileyLinkBLEDevice *device = _devicesById[peripheral.identifier.UUIDString]; - - [device connectionStateDidChange:nil]; - - if (device == nil) { - return; - } - - NSDictionary *attrs = @{@"peripheral": device.peripheral}; - [[NSNotificationCenter defaultCenter] postNotificationName:RILEYLINK_EVENT_DEVICE_CONNECTED object:device userInfo:attrs]; - - [device.peripheral readRSSI]; -} - -- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { - - if (error) { - NSLog(@"Disconnection: %@", error); - } - NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; - - RileyLinkBLEDevice *device = _devicesById[peripheral.identifier.UUIDString]; - - [device connectionStateDidChange:error]; - - if (device == nil) { - return; - } - - attrs[@"peripheral"] = device.peripheral; - - if (error) { - attrs[@"error"] = error; - } - - NSLog(@"RILEYLINK_EVENT_DEVICE_DISCONNECTED"); - [[NSNotificationCenter defaultCenter] postNotificationName:RILEYLINK_EVENT_DEVICE_DISCONNECTED object:device userInfo:attrs]; - - [self attemptReconnectForDisconnectedDevices]; -} - -@end diff --git a/RileyLinkBLEKit/RileyLinkDevice.swift b/RileyLinkBLEKit/RileyLinkDevice.swift new file mode 100644 index 000000000..f3dea9ba3 --- /dev/null +++ b/RileyLinkBLEKit/RileyLinkDevice.swift @@ -0,0 +1,382 @@ +// +// RileyLinkDevice.swift +// RileyLinkBLEKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import CoreBluetooth +import os.log + + +/// TODO: Should we be tracking the most recent "pump" RSSI? +public class RileyLinkDevice { + let manager: PeripheralManager + + private let log = OSLog(category: "RileyLinkDevice") + + // Confined to `manager.queue` + private(set) var bleFirmwareVersion: BLEFirmwareVersion? + + // Confined to `manager.queue` + private(set) var radioFirmwareVersion: RadioFirmwareVersion? + + // Confined to `queue` + private var idleListeningState: IdleListeningState = .disabled { + didSet { + switch (oldValue, idleListeningState) { + case (.disabled, .enabled): + assertIdleListening(forceRestart: true) + case (.enabled, .enabled): + assertIdleListening(forceRestart: false) + default: + break + } + } + } + + // Confined to `queue` + private var lastIdle: Date? + + // Confined to `queue` + // TODO: Tidy up this state/preference machine + private var isIdleListeningPending = false + + // Confined to `queue` + private var isTimerTickEnabled = true + + /// Serializes access to device state + private let queue = DispatchQueue(label: "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.queue", qos: .userInitiated) + + /// The queue used to serialize sessions and observe when they've drained + private let sessionQueue: OperationQueue = { + let queue = OperationQueue() + queue.name = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.sessionQueue" + queue.qualityOfService = .utility + queue.maxConcurrentOperationCount = 1 + + return queue + }() + + private var sessionQueueOperationCountObserver: NSKeyValueObservation! + + init(peripheralManager: PeripheralManager) { + self.manager = peripheralManager + sessionQueue.underlyingQueue = peripheralManager.queue + + peripheralManager.delegate = self + + sessionQueueOperationCountObserver = sessionQueue.observe(\.operationCount, options: [.new]) { [unowned self] (queue, change) in + if let newValue = change.newValue, newValue == 0 { + self.log.debug("Session queue operation count is now empty") + self.assertIdleListening(forceRestart: true) + } + } + } +} + + +// MARK: - Peripheral operations. Thread-safe. +extension RileyLinkDevice { + public var name: String? { + return manager.peripheral.name + } + + public var deviceURI: String { + return "rileylink://\(name ?? peripheralIdentifier.uuidString)" + } + + public var peripheralIdentifier: UUID { + return manager.peripheral.identifier + } + + public var peripheralState: CBPeripheralState { + return manager.peripheral.state + } + + public func readRSSI() { + guard case .connected = manager.peripheral.state, case .poweredOn = manager.central.state else { + return + } + manager.peripheral.readRSSI() + } + + public func setCustomName(_ name: String) { + manager.setCustomName(name) + } +} + + +extension RileyLinkDevice: Equatable, Hashable { + public static func ==(lhs: RileyLinkDevice, rhs: RileyLinkDevice) -> Bool { + return lhs === rhs + } + + public var hashValue: Int { + return peripheralIdentifier.hashValue + } +} + + +// MARK: - Status management +extension RileyLinkDevice { + public struct Status { + public let lastIdle: Date? + + public let name: String? + + public let bleFirmwareVersion: BLEFirmwareVersion? + + public let radioFirmwareVersion: RadioFirmwareVersion? + } + + public func getStatus(_ completion: @escaping (_ status: Status) -> Void) { + queue.async { + let lastIdle = self.lastIdle + + self.manager.queue.async { + completion(Status( + lastIdle: lastIdle, + name: self.name, + bleFirmwareVersion: self.bleFirmwareVersion, + radioFirmwareVersion: self.radioFirmwareVersion + )) + } + } + } +} + + +// MARK: - Command session management +extension RileyLinkDevice { + public func runSession(withName name: String, _ block: @escaping (_ session: CommandSession) -> Void) { + sessionQueue.addOperation(manager.configureAndRun({ [weak self] (manager) in + self?.log.debug("======================== %{public}@ ===========================", name) + block(CommandSession(manager: manager, responseType: self?.bleFirmwareVersion?.responseType ?? .buffered, firmwareVersion: self?.radioFirmwareVersion ?? .unknown)) + self?.log.debug("------------------------ %{public}@ ---------------------------", name) + })) + } +} + + +// MARK: - Idle management +extension RileyLinkDevice { + public enum IdleListeningState { + case enabled(timeout: TimeInterval, channel: UInt8) + case disabled + } + + func setIdleListeningState(_ state: IdleListeningState) { + queue.async { + self.idleListeningState = state + } + } + + public func assertIdleListening(forceRestart: Bool = false) { + queue.async { + guard case .enabled(timeout: let timeout, channel: let channel) = self.idleListeningState else { + return + } + + guard case .connected = self.manager.peripheral.state, case .poweredOn = self.manager.central.state else { + return + } + + guard forceRestart || (self.lastIdle ?? .distantPast).timeIntervalSinceNow < -timeout else { + return + } + + guard !self.isIdleListeningPending else { + return + } + + self.isIdleListeningPending = true + self.log.debug("Enqueuing idle listening") + + self.manager.startIdleListening(idleTimeout: timeout, channel: channel) { (error) in + self.queue.async { + if let error = error { + self.log.error("Unable to start idle listening: %@", String(describing: error)) + } else { + self.lastIdle = Date() + NotificationCenter.default.post(name: .DeviceDidStartIdle, object: self) + } + self.isIdleListeningPending = false + } + } + } + } +} + + +// MARK: - Timer tick management +extension RileyLinkDevice { + func setTimerTickEnabled(_ enabled: Bool) { + queue.async { + self.isTimerTickEnabled = enabled + self.assertTimerTick() + } + } + + func assertTimerTick() { + queue.async { + if self.isTimerTickEnabled != self.manager.timerTickEnabled { + self.manager.setTimerTickEnabled(self.isTimerTickEnabled) + } + } + } +} + + +// MARK: - CBCentralManagerDelegate Proxying +extension RileyLinkDevice { + func centralManagerDidUpdateState(_ central: CBCentralManager) { + if case .poweredOn = central.state { + assertIdleListening(forceRestart: false) + assertTimerTick() + } + + manager.centralManagerDidUpdateState(central) + } + + func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + if case .connected = peripheral.state { + assertIdleListening(forceRestart: false) + assertTimerTick() + } + + manager.centralManager(central, didConnect: peripheral) + + NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self) + } + + func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self) + } + + func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { + NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self) + } +} + + +extension RileyLinkDevice: PeripheralManagerDelegate { + // This is called from the central's queue + func peripheralManager(_ manager: PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic) { + switch MainServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString) { + case .data?: + guard let value = characteristic.value, value.count > 0 else { + return + } + + self.manager.queue.async { + if let responseType = self.bleFirmwareVersion?.responseType { + let response: PacketResponse? + + switch responseType { + case .buffered: + var buffer = ResponseBuffer(endMarker: 0x00) + buffer.append(value) + response = buffer.responses.last + case .single: + response = PacketResponse(data: value) + } + + if let response = response { + switch response.code { + case .rxTimeout, .commandInterrupted, .zeroData, .invalidParam, .unknownCommand: + self.log.debug("Idle error received: %@", String(describing: response.code)) + case .success: + if let packet = response.packet { + self.log.debug("Idle packet received: %@", String(describing: value)) + NotificationCenter.default.post( + name: .DevicePacketReceived, + object: self, + userInfo: [RileyLinkDevice.notificationPacketKey: packet] + ) + } + } + } else { + self.log.debug("Unknown idle response: %@", value.hexadecimalString) + } + } + + self.assertIdleListening(forceRestart: true) + } + case .responseCount?: + // PeripheralManager.Configuration.valueUpdateMacros is responsible for handling this response. + break + case .timerTick?: + NotificationCenter.default.post(name: .DeviceTimerDidTick, object: self) + + assertIdleListening(forceRestart: false) + case .customName?, .firmwareVersion?, .none: + break + } + } + + func peripheralManager(_ manager: PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?) { + NotificationCenter.default.post( + name: .DeviceRSSIDidChange, + object: self, + userInfo: [RileyLinkDevice.notificationRSSIKey: RSSI] + ) + } + + func peripheralManagerDidUpdateName(_ manager: PeripheralManager) { + NotificationCenter.default.post( + name: .DeviceNameDidChange, + object: self, + userInfo: nil + ) + } + + func completeConfiguration(for manager: PeripheralManager) throws { + // Read bluetooth version to determine compatibility + let bleVersionString = try manager.readBluetoothFirmwareVersion(timeout: 1) + bleFirmwareVersion = BLEFirmwareVersion(versionString: bleVersionString) + + let radioVersionString = try manager.readRadioFirmwareVersion(timeout: 1, responseType: bleFirmwareVersion?.responseType ?? .buffered) + radioFirmwareVersion = RadioFirmwareVersion(versionString: radioVersionString) + } +} + + +extension RileyLinkDevice: CustomDebugStringConvertible { + public var debugDescription: String { + return [ + "## RileyLinkDevice", + "name: \(name ?? "")", + "lastIdle: \(lastIdle ?? .distantPast)", + "isIdleListeningPending: \(isIdleListeningPending)", + "isTimerTickEnabled: \(isTimerTickEnabled)", + "isTimerTickNotifying: \(manager.timerTickEnabled)", + "radioFirmware: \(String(describing: radioFirmwareVersion))", + "bleFirmware: \(String(describing: bleFirmwareVersion))", + "peripheral: \(manager.peripheral)", + "sessionQueue.operationCount: \(sessionQueue.operationCount)" + ].joined(separator: "\n") + } +} + + +extension RileyLinkDevice { + public static let notificationPacketKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationPacket" + + public static let notificationRSSIKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationRSSI" +} + + +extension Notification.Name { + public static let DeviceConnectionStateDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.ConnectionStateDidChange") + + public static let DeviceDidStartIdle = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.DidStartIdle") + + public static let DeviceNameDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.NameDidChange") + + public static let DevicePacketReceived = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.PacketReceived") + + public static let DeviceRSSIDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.RSSIDidChange") + + public static let DeviceTimerDidTick = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.TimerTickDidChange") +} diff --git a/RileyLinkBLEKit/RileyLinkDeviceError.swift b/RileyLinkBLEKit/RileyLinkDeviceError.swift new file mode 100644 index 000000000..e0e669bc7 --- /dev/null +++ b/RileyLinkBLEKit/RileyLinkDeviceError.swift @@ -0,0 +1,45 @@ +// +// RileyLinkDeviceError.swift +// RileyLinkBLEKit +// +// Copyright © 2018 Pete Schwamb. All rights reserved. +// + + +enum RileyLinkDeviceError: Error { + case peripheralManagerError(PeripheralManagerError) + case invalidInput(String) + case writeSizeLimitExceeded(maxLength: Int) + case invalidResponse(Data) + case responseTimeout + case unsupportedCommand(RileyLinkCommand) +} + + +extension RileyLinkDeviceError: LocalizedError { + var errorDescription: String? { + switch self { + case .peripheralManagerError(let error): + return error.errorDescription + case .invalidInput(let input): + return String(format: NSLocalizedString("Input %@ is invalid", comment: "Invalid input error description (1: input)"), String(describing: input)) + case .invalidResponse(let response): + return String(format: NSLocalizedString("Response %@ is invalid", comment: "Invalid response error description (1: response)"), String(describing: response)) + case .writeSizeLimitExceeded(let maxLength): + return String(format: NSLocalizedString("Data exceededs maximum size of %@ bytes", comment: "Write size limit exceeded error description (1: size limit)"), NumberFormatter.localizedString(from: NSNumber(value: maxLength), number: .none)) + case .responseTimeout: + return NSLocalizedString("Pump did not respond in time", comment: "Response timeout error description") + case .unsupportedCommand(let command): + return String(format: NSLocalizedString("RileyLink firmware does not support the %@ command", comment: "Unsupported command error description"), String(describing: command)) + } + } + + var failureReason: String? { + switch self { + case .peripheralManagerError(let error): + return error.failureReason + default: + return errorDescription + } + } +} diff --git a/RileyLinkBLEKit/RileyLinkDeviceManager.swift b/RileyLinkBLEKit/RileyLinkDeviceManager.swift new file mode 100644 index 000000000..96ef354ba --- /dev/null +++ b/RileyLinkBLEKit/RileyLinkDeviceManager.swift @@ -0,0 +1,311 @@ +// +// RileyLinkDeviceManager.swift +// RileyLinkBLEKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import CoreBluetooth +import os.log + + +public class RileyLinkDeviceManager: NSObject { + private let log = OSLog(category: "RileyLinkDeviceManager") + + private var central: CBCentralManager! + + private let centralQueue = DispatchQueue(label: "com.rileylink.RileyLinkBLEKit.BluetoothManager.centralQueue", qos: .utility) + + // Isolated to centralQueue + private var devices: [RileyLinkDevice] = [] { + didSet { + NotificationCenter.default.post(name: .ManagerDevicesDidChange, object: self) + } + } + + // Isolated to centralQueue + private var autoConnectIDs: Set + + // Isolated to centralQueue + private var isScanningEnabled = false + + public init(autoConnectIDs: Set) { + self.autoConnectIDs = autoConnectIDs + + super.init() + + central = CBCentralManager( + delegate: self, + queue: centralQueue, + options: [ + CBCentralManagerOptionRestoreIdentifierKey: "com.rileylink.CentralManager" + ] + ) + } + + // MARK: - Configuration. Not thread-safe. + + public var idleListeningEnabled: Bool { + if case .disabled = idleListeningState { + return false + } else { + return true + } + } + + public var idleListeningState: RileyLinkDevice.IdleListeningState = .disabled { + didSet { + let newState = self.idleListeningState + + centralQueue.async { + for device in self.devices { + device.setIdleListeningState(newState) + } + } + } + } + + public var timerTickEnabled = true { + didSet { + let newValue = timerTickEnabled + + centralQueue.async { + for device in self.devices { + device.setTimerTickEnabled(newValue) + } + } + } + } +} + + +// MARK: - Connecting +extension RileyLinkDeviceManager { + public func connect(_ device: RileyLinkDevice) { + centralQueue.async { + self.autoConnectIDs.insert(device.manager.peripheral.identifier.uuidString) + + guard let peripheral = self.reloadPeripheral(for: device) else { + return + } + + self.central.connectIfNecessary(peripheral) + } + } + + public func disconnect(_ device: RileyLinkDevice) { + centralQueue.async { + self.autoConnectIDs.remove(device.manager.peripheral.identifier.uuidString) + + guard let peripheral = self.reloadPeripheral(for: device) else { + return + } + + self.central.cancelPeripheralConnectionIfNecessary(peripheral) + } + } + + /// Asks the central manager for its peripheral instance for a given device. + /// It seems to be possible that this reference changes across a bluetooth reset, and not updating the reference can result in API MISUSE warnings + /// + /// - Parameter device: The device to reload + /// - Returns: The peripheral instance returned by the central manager + private func reloadPeripheral(for device: RileyLinkDevice) -> CBPeripheral? { + dispatchPrecondition(condition: .onQueue(centralQueue)) + + guard let peripheral = central.retrievePeripherals(withIdentifiers: [device.manager.peripheral.identifier]).first else { + return nil + } + + device.manager.peripheral = peripheral + return peripheral + } + + private var hasDiscoveredAllAutoConnectDevices: Bool { + dispatchPrecondition(condition: .onQueue(centralQueue)) + + return autoConnectIDs.isSubset(of: devices.map { $0.manager.peripheral.identifier.uuidString }) + } + + private func autoConnectDevices() { + dispatchPrecondition(condition: .onQueue(centralQueue)) + + for device in devices where autoConnectIDs.contains(device.manager.peripheral.identifier.uuidString) { + log.info("Attempting reconnect to %@", device.manager.peripheral) + connect(device) + } + } + + private func addPeripheral(_ peripheral: CBPeripheral) { + dispatchPrecondition(condition: .onQueue(centralQueue)) + + var device: RileyLinkDevice! = devices.first(where: { $0.manager.peripheral.identifier == peripheral.identifier }) + + if let device = device { + device.manager.peripheral = peripheral + } else { + device = RileyLinkDevice(peripheralManager: PeripheralManager(peripheral: peripheral, configuration: .rileyLink, centralManager: central)) + device.setTimerTickEnabled(timerTickEnabled) + device.setIdleListeningState(idleListeningState) + + devices.append(device) + + log.info("Created device for peripheral %@", peripheral) + } + + if autoConnectIDs.contains(peripheral.identifier.uuidString) { + central.connectIfNecessary(peripheral) + } + } +} + + +extension RileyLinkDeviceManager { + public func getDevices(_ completion: @escaping (_ devices: [RileyLinkDevice]) -> Void) { + centralQueue.async { + completion(self.devices) + } + } + + public func deprioritize(_ device: RileyLinkDevice, _ completion: (() -> Void)? = nil) { + centralQueue.async { + self.devices.deprioritize(device) + completion?() + } + } +} + +extension Array where Element == RileyLinkDevice { + public var firstConnected: Element? { + return self.first { (device) -> Bool in + return device.manager.peripheral.state == .connected + } + } + + mutating func deprioritize(_ element: Element) { + if let index = self.index(where: { $0 === element }) { + self.swapAt(index, self.count - 1) + } + } +} + + +// MARK: - Scanning +extension RileyLinkDeviceManager { + public func setScanningEnabled(_ enabled: Bool) { + centralQueue.async { + self.isScanningEnabled = enabled + + if case .poweredOn = self.central.state { + if enabled { + self.central.scanForPeripherals() + } else if self.central.isScanning { + self.central.stopScan() + } + } + } + } + + public func assertIdleListening(forcingRestart: Bool) { + centralQueue.async { + for device in self.devices { + device.assertIdleListening(forceRestart: forcingRestart) + } + } + } +} + + +// MARK: - Delegate methods called on `centralQueue` +extension RileyLinkDeviceManager: CBCentralManagerDelegate { + public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { + log.info("%@", #function) + + guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else { + return + } + + for peripheral in peripherals { + addPeripheral(peripheral) + } + } + + public func centralManagerDidUpdateState(_ central: CBCentralManager) { + log.info("%@: %@", #function, central.state.description) + if case .poweredOn = central.state { + autoConnectDevices() + + if isScanningEnabled || !hasDiscoveredAllAutoConnectDevices { + central.scanForPeripherals() + } else if central.isScanning { + central.stopScan() + } + } + + for device in devices { + device.centralManagerDidUpdateState(central) + } + } + + public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { + log.info("Discovered %@ at %@", peripheral, RSSI) + + addPeripheral(peripheral) + + // TODO: Should we keep scanning? There's no UI to remove a lost RileyLink, which could result in a battery drain due to indefinite scanning. + if !isScanningEnabled && central.isScanning && hasDiscoveredAllAutoConnectDevices { + log.info("All peripherals discovered") + central.stopScan() + } + } + + public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + // Notify the device so it can begin configuration + for device in devices where device.manager.peripheral.identifier == peripheral.identifier { + device.centralManager(central, didConnect: peripheral) + } + } + + public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + for device in devices where device.manager.peripheral.identifier == peripheral.identifier { + device.centralManager(central, didDisconnectPeripheral: peripheral, error: error) + } + + autoConnectDevices() + } + + public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { + log.error("%@: %@: %@", #function, peripheral, String(describing: error)) + + for device in devices where device.manager.peripheral.identifier == peripheral.identifier { + device.centralManager(central, didFailToConnect: peripheral, error: error) + } + + autoConnectDevices() + } +} + + +extension RileyLinkDeviceManager { + public override var debugDescription: String { + var report = [ + "## RileyLinkDeviceManager", + "central: \(central)", + "autoConnectIDs: \(autoConnectIDs)", + "timerTickEnabled: \(timerTickEnabled)", + "idleListeningState: \(idleListeningState)" + ] + + for device in devices { + report.append(String(reflecting: device)) + report.append("") + } + + return report.joined(separator: "\n\n") + } +} + + +extension Notification.Name { + public static let ManagerDevicesDidChange = Notification.Name("com.rileylink.RileyLinkBLEKit.DevicesDidChange") +} diff --git a/RileyLinkBLEKit/SendAndListenCmd.h b/RileyLinkBLEKit/SendAndListenCmd.h deleted file mode 100644 index 9d5eca885..000000000 --- a/RileyLinkBLEKit/SendAndListenCmd.h +++ /dev/null @@ -1,23 +0,0 @@ -// -// SendDataCmd.h -// RileyLink -// -// Created by Pete Schwamb on 8/9/15. -// Copyright (c) 2015 Pete Schwamb. All rights reserved. -// - -@import Foundation; -#import "ReceivingPacketCmd.h" -#import "RFPacket.h" - -@interface SendAndListenCmd : ReceivingPacketCmd - -@property (nonatomic, strong) RFPacket *packet; -@property (nonatomic, assign) uint8_t sendChannel; // In general, 0 = meter, cgm. 2 = pump -@property (nonatomic, assign) uint8_t repeatCount; // 0 = no repeat, i.e. only one packet. 1 repeat = 2 packets sent total. -@property (nonatomic, assign) uint8_t msBetweenPackets; -@property (nonatomic, assign) uint8_t listenChannel; -@property (nonatomic, assign) uint32_t timeoutMS; -@property (nonatomic, assign) uint8_t retryCount; - -@end diff --git a/RileyLinkBLEKit/SendAndListenCmd.m b/RileyLinkBLEKit/SendAndListenCmd.m deleted file mode 100644 index 16756338f..000000000 --- a/RileyLinkBLEKit/SendAndListenCmd.m +++ /dev/null @@ -1,34 +0,0 @@ -// -// SendDataCmd.m -// RileyLink -// -// Created by Pete Schwamb on 8/9/15. -// Copyright (c) 2015 Pete Schwamb. All rights reserved. -// - -#import "SendAndListenCmd.h" -#import "RileyLinkBLEManager.h" - -@implementation SendAndListenCmd - -- (NSData*)data { - uint8_t cmd[10]; - cmd[0] = RILEYLINK_CMD_SEND_AND_LISTEN; - cmd[1] = _sendChannel; - cmd[2] = _repeatCount; - cmd[3] = _msBetweenPackets; - cmd[4] = _listenChannel; - cmd[5] = _timeoutMS >> 24; - cmd[6] = (_timeoutMS >> 16) & 0xff; - cmd[7] = (_timeoutMS >> 8) & 0xff; - cmd[8] = _timeoutMS & 0xff; - cmd[9] = _retryCount; - - NSMutableData *serialized = [NSMutableData dataWithBytes:cmd length:10]; - [serialized appendData:[_packet encodedData]]; - uint8_t nullTerminator = 0; - [serialized appendBytes:&nullTerminator length:1]; - return serialized; -} - -@end diff --git a/RileyLinkBLEKit/SendPacketCmd.h b/RileyLinkBLEKit/SendPacketCmd.h deleted file mode 100644 index c5d50e830..000000000 --- a/RileyLinkBLEKit/SendPacketCmd.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// SendPacketCmd.h -// RileyLink -// -// Created by Pete Schwamb on 12/27/15. -// Copyright © 2015 Pete Schwamb. All rights reserved. -// - -@import Foundation; -#import "CmdBase.h" -#import "RFPacket.h" - -@interface SendPacketCmd : CmdBase - -@property (nonatomic, strong) RFPacket *packet; -@property (nonatomic, assign) uint8_t sendChannel; // In general, 0 = meter, cgm. 2 = pump -@property (nonatomic, assign) uint8_t repeatCount; // 0 = no repeat, i.e. only one packet. 1 repeat = 2 packets sent total. -@property (nonatomic, assign) uint8_t msBetweenPackets; - -@end diff --git a/RileyLinkBLEKit/SendPacketCmd.m b/RileyLinkBLEKit/SendPacketCmd.m deleted file mode 100644 index a53711a20..000000000 --- a/RileyLinkBLEKit/SendPacketCmd.m +++ /dev/null @@ -1,28 +0,0 @@ -// -// SendPacketCmd.m -// RileyLink -// -// Created by Pete Schwamb on 12/27/15. -// Copyright © 2015 Pete Schwamb. All rights reserved. -// - -#import "SendPacketCmd.h" - -@implementation SendPacketCmd - -- (NSData*)data { - uint8_t cmd[4]; - cmd[0] = RILEYLINK_CMD_SEND_PACKET; - cmd[1] = _sendChannel; - cmd[2] = _repeatCount; - cmd[3] = _msBetweenPackets; - - NSMutableData *serialized = [NSMutableData dataWithBytes:cmd length:4]; - [serialized appendData:[_packet encodedData]]; - uint8_t nullTerminator = 0; - [serialized appendBytes:&nullTerminator length:1]; - return serialized; -} - - -@end diff --git a/RileyLinkBLEKit/UpdateRegisterCmd.h b/RileyLinkBLEKit/UpdateRegisterCmd.h deleted file mode 100644 index 61289a45e..000000000 --- a/RileyLinkBLEKit/UpdateRegisterCmd.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// UpdateRegisterCmd.h -// RileyLink -// -// Created by Pete Schwamb on 1/25/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -#import "CmdBase.h" - -@interface UpdateRegisterCmd : CmdBase - -@property (nonatomic, assign) uint8_t addr; -@property (nonatomic, assign) uint8_t value; - -@end diff --git a/RileyLinkBLEKit/UpdateRegisterCmd.m b/RileyLinkBLEKit/UpdateRegisterCmd.m deleted file mode 100644 index de455c346..000000000 --- a/RileyLinkBLEKit/UpdateRegisterCmd.m +++ /dev/null @@ -1,21 +0,0 @@ -// -// UpdateRegisterCmd.m -// RileyLink -// -// Created by Pete Schwamb on 1/25/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -#import "UpdateRegisterCmd.h" - -@implementation UpdateRegisterCmd - -- (NSData*)data { - uint8_t cmd[4]; - cmd[0] = RILEYLINK_CMD_UPDATE_REGISTER; - cmd[1] = _addr; - cmd[2] = _value; - return [NSData dataWithBytes:cmd length:4]; -} - -@end diff --git a/RileyLinkBLEKit/es.lproj/InfoPlist.strings b/RileyLinkBLEKit/es.lproj/InfoPlist.strings new file mode 100644 index 000000000..f8e9a2b43 --- /dev/null +++ b/RileyLinkBLEKit/es.lproj/InfoPlist.strings @@ -0,0 +1,3 @@ +/* (No Comment) */ +"CFBundleName" = "$(PRODUCT_NAME)"; + diff --git a/RileyLinkBLEKit/ru.lproj/InfoPlist.strings b/RileyLinkBLEKit/ru.lproj/InfoPlist.strings new file mode 100644 index 000000000..874e8a453 --- /dev/null +++ b/RileyLinkBLEKit/ru.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* No Localized Strings */ diff --git a/RileyLinkBLEKitTests/Info.plist b/RileyLinkBLEKitTests/Info.plist index e4ede6701..f4285f218 100644 --- a/RileyLinkBLEKitTests/Info.plist +++ b/RileyLinkBLEKitTests/Info.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,10 +15,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.2 - CFBundleSignature - ???? + 2.0.0 CFBundleVersion - $(CURRENT_PROJECT_VERSION) + 1 diff --git a/RileyLinkBLEKitTests/RFPacketTests.swift b/RileyLinkBLEKitTests/RFPacketTests.swift new file mode 100644 index 000000000..116bc17cb --- /dev/null +++ b/RileyLinkBLEKitTests/RFPacketTests.swift @@ -0,0 +1,20 @@ +// +// RFPacketTests.swift +// RileyLinkBLEKitTests +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import XCTest +@testable import RileyLinkBLEKit + +class RFPacketTests: XCTestCase { + + func testDecodeRF() { + let response = Data(hexadecimalString: "4926a965a5d1a8dab0e5635635555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555559a35") + let packet = RFPacket(rfspyResponse: response!)! + XCTAssertEqual("a965a5d1a8dab0e5635635555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555559a35", packet.data.hexadecimalString) + XCTAssertEqual(-37, packet.rssi) + } + +} diff --git a/RileyLinkBLEKitTests/RadioFirmwareVersionTests.swift b/RileyLinkBLEKitTests/RadioFirmwareVersionTests.swift new file mode 100644 index 000000000..001f29029 --- /dev/null +++ b/RileyLinkBLEKitTests/RadioFirmwareVersionTests.swift @@ -0,0 +1,19 @@ +// +// RadioFirmwareVersionTests.swift +// RileyLinkBLEKitTests +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import XCTest +@testable import RileyLinkBLEKit + +class RadioFirmwareVersionTests: XCTestCase { + + func testVersionParsing() { + let version = RadioFirmwareVersion(versionString: "subg_rfspy 0.8")! + + XCTAssertEqual([0, 8], version.components) + } + +} diff --git a/RileyLinkBLEKitTests/ResponseBufferTests.swift b/RileyLinkBLEKitTests/ResponseBufferTests.swift new file mode 100644 index 000000000..eec2d7e7e --- /dev/null +++ b/RileyLinkBLEKitTests/ResponseBufferTests.swift @@ -0,0 +1,23 @@ +// +// ResponseBufferTests.swift +// RileyLinkBLEKitTests +// +// Copyright © 2018 Pete Schwamb. All rights reserved. +// + +import XCTest +@testable import RileyLinkBLEKit + +class ResponseBufferTests: XCTestCase { + + func testSingleError() { + var buffer = ResponseBuffer(endMarker: 0x00) + + buffer.append(Data(hexadecimalString: "bb00")!) + + let responses = buffer.responses + + XCTAssertEqual(1, responses.count) + } + +} diff --git a/RileyLinkBLEKitTests/RileyLinkBLEKitTests.m b/RileyLinkBLEKitTests/RileyLinkBLEKitTests.m deleted file mode 100644 index 18349d80f..000000000 --- a/RileyLinkBLEKitTests/RileyLinkBLEKitTests.m +++ /dev/null @@ -1,58 +0,0 @@ -// -// RileyLinkBLEKitTests.m -// RileyLinkBLEKitTests -// -// Created by Nathan Racklyeft on 4/8/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -#import -#import "RileyLinkBLEDevice.h" -#import "RFPacket.h" -#import "NSData+Conversion.h" - -@interface RileyLinkBLEDevice (_Private) - -- (SubgRfspyVersionState)firmwareStateForVersionString:(NSString *)firmwareVersion; - -@end - -@interface RileyLinkBLEKitTests : XCTestCase - -@end - -@implementation RileyLinkBLEKitTests - -- (void)testVersionParsing { - id peripheral = nil; - - RileyLinkBLEDevice *device = [[RileyLinkBLEDevice alloc] initWithPeripheral:peripheral]; - - SubgRfspyVersionState state = [device firmwareStateForVersionString:@"subg_rfspy 0.8"]; - - XCTAssertEqual(SubgRfspyVersionStateUpToDate, state); -} - -- (void)testDecodeRF { - NSData *response = [NSData dataWithHexadecimalString:@"4926a965a5d1a8dab0e5635635555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555559a35"]; - RFPacket *packet = [[RFPacket alloc] initWithRFSPYResponse:response]; - XCTAssertEqualObjects(@"a7754838ce0303000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", packet.data.hexadecimalString); -} - -- (void)testEncodeData { - NSData *msg = [NSData dataWithHexadecimalString:@"a77548380600a2"]; - RFPacket *packet = [[RFPacket alloc] initWithData:msg]; - - XCTAssertEqualObjects(@"a965a5d1a8da566555ab2555", packet.encodedData.hexadecimalString); -} - -- (void)testDecodeInvalidCRC { - // This data is corrupt in a special way; the data still decodes via tha 4b6b conversion without error, - // but produces a decoded message that doesn't match its CRC. - NSData *response = [NSData dataWithHexadecimalString:@"4926b165a5d1a8dab0e5635635555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555559a35"]; - RFPacket *packet = [[RFPacket alloc] initWithRFSPYResponse:response]; - XCTAssertNil(packet.data); -} - - -@end diff --git a/RileyLinkKit/DeviceState.swift b/RileyLinkKit/DeviceState.swift new file mode 100644 index 000000000..f7d51208a --- /dev/null +++ b/RileyLinkKit/DeviceState.swift @@ -0,0 +1,29 @@ +// +// DeviceState.swift +// RileyLinkKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + + +public struct DeviceState { + public var lastTuned: Date? + + public var lastValidFrequency: Measurement? + + public init(lastTuned: Date? = nil, lastValidFrequency: Measurement? = nil) { + self.lastTuned = lastTuned + self.lastValidFrequency = lastValidFrequency + } +} + + +extension DeviceState: CustomDebugStringConvertible { + public var debugDescription: String { + return [ + "## DeviceState", + "lastValidFrequency: \(String(describing: lastValidFrequency))", + "lastTuned: \(String(describing: lastTuned))", + ].joined(separator: "\n") + } +} diff --git a/RileyLinkKit/Either.swift b/RileyLinkKit/Either.swift deleted file mode 100644 index ab7439602..000000000 --- a/RileyLinkKit/Either.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Either.swift -// RileyLink -// -// Created by Pete Schwamb on 3/19/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -import Foundation - -public enum Either { - case success(T1) - case failure(T2) -} diff --git a/RileyLinkKit/Extensions/BasalProfile.swift b/RileyLinkKit/Extensions/BasalProfile.swift new file mode 100644 index 000000000..0f3df9380 --- /dev/null +++ b/RileyLinkKit/Extensions/BasalProfile.swift @@ -0,0 +1,22 @@ +// +// BasalProfile.swift +// RileyLinkKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import MinimedKit + + +extension BasalProfile { + var readMessageType: MessageType { + switch self { + case .standard: + return .readProfileSTD512 + case .profileA: + return .readProfileA512 + case .profileB: + return .readProfileB512 + } + } +} diff --git a/RileyLinkKit/Extensions/CommandSession.swift b/RileyLinkKit/Extensions/CommandSession.swift new file mode 100644 index 000000000..003c98a3e --- /dev/null +++ b/RileyLinkKit/Extensions/CommandSession.swift @@ -0,0 +1,11 @@ +// +// CommandSession.swift +// RileyLinkKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import RileyLinkBLEKit + + +extension CommandSession: PumpMessageSender { } diff --git a/RileyLinkKit/Extensions/HistoryPage.swift b/RileyLinkKit/Extensions/HistoryPage.swift new file mode 100644 index 000000000..e913e05d5 --- /dev/null +++ b/RileyLinkKit/Extensions/HistoryPage.swift @@ -0,0 +1,70 @@ +// +// HistoryPage.swift +// RileyLinkKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import MinimedKit + + +extension HistoryPage { + /// Returns TimestampedHistoryEvents from this page occuring after a given date + /// + /// - Parameters: + /// - start: The date to filter events occuring on or after + /// - timeZone: The current time zone offset of the pump + /// - model: The pump model + /// - Returns: A tuple containing: + /// - events: The matching events + /// - hasMoreEvents: Whether the next page likely contains events after the specified start date + /// - cancelledEarly: + func timestampedEvents(after start: Date, timeZone: TimeZone, model: PumpModel) -> (events: [TimestampedHistoryEvent], hasMoreEvents: Bool, cancelledEarly: Bool) { + // Start with some time in the future, to account for the condition when the pump's clock is ahead + // of ours by a small amount. + var timeCursor = Date(timeIntervalSinceNow: TimeInterval(minutes: 60)) + var events = [TimestampedHistoryEvent]() + var timeAdjustmentInterval: TimeInterval = 0 + var seenEventData = Set() + var lastEvent: PumpEvent? + + for event in self.events.reversed() { + if let event = event as? TimestampedPumpEvent, !seenEventData.contains(event.rawData) { + seenEventData.insert(event.rawData) + + var timestamp = event.timestamp + timestamp.timeZone = timeZone + + if let date = timestamp.date?.addingTimeInterval(timeAdjustmentInterval) { + + let shouldCheckDateForCompletion = !event.isDelayedAppend(with: model) + + if shouldCheckDateForCompletion { + if date <= start { + // Success, we have all the events we need + //NSLog("Found event at or before startDate(%@)", date as NSDate, String(describing: eventTimestampDeltaAllowance), startDate as NSDate) + return (events: events, hasMoreEvents: false, cancelledEarly: false) + } else if date.timeIntervalSince(timeCursor) > TimeInterval(minutes: 60) { + // Appears that pump lost time; we can't build up a valid timeline from this point back. + // TODO: Convert logging + NSLog("Found event (%@) out of order in history. Ending history fetch.", date as NSDate) + return (events: events, hasMoreEvents: false, cancelledEarly: true) + } + + timeCursor = date + } + + events.insert(TimestampedHistoryEvent(pumpEvent: event, date: date), at: 0) + } + } + + if let changeTimeEvent = event as? ChangeTimePumpEvent, let newTimeEvent = lastEvent as? NewTimePumpEvent { + timeAdjustmentInterval += (newTimeEvent.timestamp.date?.timeIntervalSince(changeTimeEvent.timestamp.date!))! + } + + lastEvent = event + } + + return (events: events, hasMoreEvents: true, cancelledEarly: false) + } +} diff --git a/RileyLinkKit/Extensions/PumpMessage.swift b/RileyLinkKit/Extensions/PumpMessage.swift new file mode 100644 index 000000000..c61bfc561 --- /dev/null +++ b/RileyLinkKit/Extensions/PumpMessage.swift @@ -0,0 +1,21 @@ +// +// PumpMessage.swift +// RileyLinkKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import MinimedKit + + +extension PumpMessage { + /// Initializes a Carelink message using settings and a default body + /// + /// - Parameters: + /// - settings: Pump settings used for determining address + /// - type: The message type + /// - body: The message body, defaulting to a 1-byte empty body + init(settings: PumpSettings, type: MessageType, body: MessageBody = CarelinkShortMessageBody()) { + self.init(packetType: .carelink, address: settings.pumpID, messageType: type, messageBody: body) + } +} diff --git a/RileyLinkKit/Extensions/RileyLinkDevice.swift b/RileyLinkKit/Extensions/RileyLinkDevice.swift new file mode 100644 index 000000000..c5d643de1 --- /dev/null +++ b/RileyLinkKit/Extensions/RileyLinkDevice.swift @@ -0,0 +1,27 @@ +// +// RileyLinkDevice.swift +// RileyLinkKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import RileyLinkBLEKit + +extension RileyLinkDevice.Status { + public var firmwareDescription: String { + let versions = [radioFirmwareVersion, bleFirmwareVersion].compactMap { (version: CustomStringConvertible?) -> String? in + if let version = version { + return String(describing: version) + } else { + return nil + } + } + + return versions.joined(separator: " / ") + } +} + + +extension Notification.Name { + static let DeviceRadioConfigDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkKit.DeviceRadioConfigDidChange") +} diff --git a/RileyLinkKit/Info.plist b/RileyLinkKit/Info.plist index 0eb186c06..7e7479f00 100644 --- a/RileyLinkKit/Info.plist +++ b/RileyLinkKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.2.2 + 2.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/RileyLinkKit/PumpMessageSender.swift b/RileyLinkKit/PumpMessageSender.swift new file mode 100644 index 000000000..208eed346 --- /dev/null +++ b/RileyLinkKit/PumpMessageSender.swift @@ -0,0 +1,153 @@ +// +// PumpMessageSender.swift +// RileyLink +// +// Created by Jaim Zuber on 3/2/17. +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import Foundation +import MinimedKit +import RileyLinkBLEKit +import os.log + +private let standardPumpResponseWindow: TimeInterval = .milliseconds(180) + +private let log = OSLog(category: "PumpMessageSender") + + +protocol PumpMessageSender { + /// - Throws: LocalizedError + func resetRadioConfig() throws + + /// - Throws: LocalizedError + func updateRegister(_ address: CC111XRegister, value: UInt8) throws + + /// - Throws: LocalizedError + func setBaseFrequency(_ frequency: Measurement) throws + + /// Sends data to the pump, listening for a reply + /// + /// - Parameters: + /// - data: The data to send + /// - repeatCount: The number of times to repeat the message before listening begins + /// - timeout: The length of time to listen for a response before timing out + /// - retryCount: The number of times to repeat the send & listen sequence + /// - Returns: The packet reply + /// - Throws: LocalizedError + func sendAndListen(_ data: Data, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> RFPacket? + + /// - Throws: LocalizedError + func listen(onChannel channel: Int, timeout: TimeInterval) throws -> RFPacket? + + /// - Throws: LocalizedError + func send(_ data: Data, onChannel channel: Int, timeout: TimeInterval) throws +} + +extension PumpMessageSender { + /// - Throws: PumpOpsError.deviceError + func send(_ msg: PumpMessage) throws { + do { + try send(MinimedPacket(outgoingData: msg.txData).encodedData(), onChannel: 0, timeout: 0) + } catch let error as LocalizedError { + throw PumpOpsError.deviceError(error) + } + } + + /// Sends a message to the pump, expecting a specific response body + /// + /// - Parameters: + /// - message: The message to send + /// - responseType: The expected response message type + /// - repeatCount: The number of times to repeat the message before listening begins + /// - timeout: The length of time to listen for a pump response + /// - retryCount: The number of times to repeat the send & listen sequence + /// - Returns: The expected response message body + /// - Throws: + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unexpectedResponse + /// - PumpOpsError.unknownResponse + func getResponse(to message: PumpMessage, responseType: MessageType = .pumpAck, repeatCount: Int = 0, timeout: TimeInterval = standardPumpResponseWindow, retryCount: Int = 3) throws -> T { + + log.debug("getResponse(%{public}@, %d, %f, %d)", String(describing: message), repeatCount, timeout, retryCount) + + let response = try sendAndListen(message, repeatCount: repeatCount, timeout: timeout, retryCount: retryCount) + + guard response.messageType == responseType, let body = response.messageBody as? T else { + if let body = response.messageBody as? PumpErrorMessageBody { + switch body.errorCode { + case .known(let code): + throw PumpOpsError.pumpError(code) + case .unknown(let code): + throw PumpOpsError.unknownPumpErrorCode(code) + } + } else { + throw PumpOpsError.unexpectedResponse(response, from: message) + } + } + return body + } + + /// Sends a message to the pump, listening for a message in reply + /// + /// - Parameters: + /// - message: The message to send + /// - repeatCount: The number of times to repeat the message before listening begins + /// - timeout: The length of time to listen for a pump response + /// - retryCount: The number of times to repeat the send & listen sequence + /// - Returns: The message reply + /// - Throws: An error describing a failure in the sending or receiving of a message: + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unknownResponse + func sendAndListen(_ message: PumpMessage, repeatCount: Int = 0, timeout: TimeInterval = standardPumpResponseWindow, retryCount: Int = 3) throws -> PumpMessage { + let rfPacket = try sendAndListenForPacket(message, repeatCount: repeatCount, timeout: timeout, retryCount: retryCount) + + guard let packet = MinimedPacket(encodedData: rfPacket.data) else { + // TODO: Change error to better reflect that this is an encoding or CRC error + throw PumpOpsError.unknownResponse(rx: rfPacket.data, during: message) + } + + guard let response = PumpMessage(rxData: packet.data) else { + // Unknown packet type or message type + throw PumpOpsError.unknownResponse(rx: packet.data, during: message) + } + + guard response.address == message.address else { + throw PumpOpsError.crosstalk(response, during: message) + } + + return response + } + + /// - Throws: + /// - PumpOpsError.noResponse + /// - PumpOpsError.deviceError + func sendAndListenForPacket(_ message: PumpMessage, repeatCount: Int = 0, timeout: TimeInterval = standardPumpResponseWindow, retryCount: Int = 3) throws -> RFPacket { + let packet: RFPacket? + + do { + packet = try sendAndListen(MinimedPacket(outgoingData: message.txData).encodedData(), repeatCount: repeatCount, timeout: timeout, retryCount: retryCount) + } catch let error as LocalizedError { + throw PumpOpsError.deviceError(error) + } + + guard let rfPacket = packet else { + throw PumpOpsError.noResponse(during: message) + } + + return rfPacket + } + + /// - Throws: PumpOpsError.deviceError + func listenForPacket(onChannel channel: Int, timeout: TimeInterval) throws -> RFPacket? { + do { + return try listen(onChannel: channel, timeout: timeout) + } catch let error as LocalizedError { + throw PumpOpsError.deviceError(error) + } + } +} diff --git a/RileyLinkKit/PumpOps.swift b/RileyLinkKit/PumpOps.swift index a7ec11e4b..1c0c4e11f 100644 --- a/RileyLinkKit/PumpOps.swift +++ b/RileyLinkKit/PumpOps.swift @@ -11,368 +11,109 @@ import MinimedKit import RileyLinkBLEKit -public class PumpOps { - - public let pumpState: PumpState - public let device: RileyLinkBLEDevice - - public init(pumpState: PumpState, device: RileyLinkBLEDevice) { - self.pumpState = pumpState - self.device = device - } - - public func pressButton(_ completion: @escaping (Either) -> Void) { - device.runSession(withName: "Press button") { (session) -> Void in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - let message = PumpMessage(packetType: .carelink, address: self.pumpState.pumpID, messageType: .buttonPress, messageBody: ButtonPressCarelinkMessageBody(buttonType: .down)) - do { - _ = try ops.runCommandWithArguments(message) - completion(.success("Success.")) - } catch let error { - DispatchQueue.main.async { () -> Void in - completion(.failure(error)) - } - } - } - } - - public func getBasalSettings(_ completion: @escaping (Either) -> Void) { - device.runSession(withName: "Get Basal Settings") { (session) -> Void in - do { - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - - let basalSettings = try ops.getBasalSchedule() - completion(.success(basalSettings)) - } catch let error { - completion(.failure(error)) - } - } - } - - public func getPumpModel(_ completion: @escaping (Either) -> Void) { - device.runSession(withName: "Get pump model") { (session) -> Void in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - do { - let model = try ops.getPumpModelNumber() +public protocol PumpOpsDelegate: class { + func pumpOps(_ pumpOps: PumpOps, didChange state: PumpState) +} - self.pumpState.pumpModel = PumpModel(rawValue: model) - DispatchQueue.main.async { () -> Void in - completion(.success(model)) - } - } catch let error { - DispatchQueue.main.async { () -> Void in - completion(.failure(error)) - } - } - } - } - - public func readSettings(_ completion: @escaping (Either) -> Void) { - device.runSession(withName: "Read pump settings") { (session) -> Void in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - do { - let response: ReadSettingsCarelinkMessageBody = try ops.messageBody(to: .readSettings) - DispatchQueue.main.async { () -> Void in - completion(.success(response)) - } - } catch let error { - DispatchQueue.main.async { () -> Void in - completion(.failure(error)) - } - } - } - } +public class PumpOps { - - public func getBatteryVoltage(_ completion: @escaping (Either) -> Void) { - device.runSession(withName: "Get battery voltage") { (session) -> Void in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - do { - let response: GetBatteryCarelinkMessageBody = try ops.messageBody(to: .getBattery) - DispatchQueue.main.async { () -> Void in - completion(.success(response)) - } - } catch let error { - DispatchQueue.main.async { () -> Void in - completion(.failure(error)) - } - } + private var pumpSettings: PumpSettings + + private var pumpState: PumpState { + didSet { + delegate.pumpOps(self, didChange: pumpState) } } - /** - Reads the current insulin reservoir volume. - - This operation is performed asynchronously and the completion will be executed on an arbitrary background queue. + private var configuredDevices: Set = Set() - - parameter completion: A closure called after the command is complete. This closure takes a single Result argument: - - success(units, date): The reservoir volume, in units of insulin, and DateCompoments representing the pump's clock - - failure(error): An error describing why the command failed - */ - public func readRemainingInsulin(_ completion: @escaping (Either<(units: Double, date: DateComponents), Error>) -> Void) { - device.runSession(withName: "Read remaining insulin") { (session) in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) + private let sessionQueue = DispatchQueue(label: "com.rileylink.RileyLinkKit.PumpOps", qos: .utility) - do { - let pumpModel = try ops.getPumpModel() - - let clockResp: ReadTimeCarelinkMessageBody = try ops.messageBody(to: .readTime) - - let response: ReadRemainingInsulinMessageBody = try ops.messageBody(to: .readRemainingInsulin) + private unowned let delegate: PumpOpsDelegate + + public init(pumpSettings: PumpSettings, pumpState: PumpState?, delegate: PumpOpsDelegate) { + self.pumpSettings = pumpSettings + self.delegate = delegate - completion(.success((units: response.getUnitsRemainingForStrokes(pumpModel.strokesPerUnit), date: clockResp.dateComponents))) - } catch let error { - completion(.failure(error)) - } + if let pumpState = pumpState { + self.pumpState = pumpState + } else { + self.pumpState = PumpState() + self.delegate.pumpOps(self, didChange: self.pumpState) } } - /** - Fetches history entries which occurred on or after the specified date. - - It is possible for Minimed Pumps to non-atomically append multiple history entries with the same timestamp, for example, `BolusWizardEstimatePumpEvent` may appear and be read before `BolusNormalPumpEvent` is written. Therefore, the `startDate` parameter is used as part of an inclusive range, leaving the client to manage the possibility of duplicates. - - History timestamps are reconciled with UTC based on the `timeZone` property of PumpState, as well as recorded clock change events. + public func updateSettings(_ settings: PumpSettings) { + sessionQueue.async { + let oldSettings = self.pumpSettings + self.pumpSettings = settings - - parameter startDate: The earliest date of events to retrieve - - parameter completion: A closure called after the command is complete. This closure takes a single Result argument: - - success(events): An array of fetched history entries, in ascending order of insertion - - failure(error): An error describing why the command failed - - */ - public func getHistoryEvents(since startDate: Date, completion: @escaping (Either<(events: [TimestampedHistoryEvent], pumpModel: PumpModel), Error>) -> Void) { - device.runSession(withName: "Get history events") { (session) -> Void in - NSLog("History fetching task started.") - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - do { - let (events, pumpModel) = try ops.getHistoryEvents(since: startDate) - DispatchQueue.main.async { () -> Void in - completion(.success((events: events, pumpModel: pumpModel))) - } - } catch let error { - DispatchQueue.main.async { () -> Void in - completion(.failure(error)) - } + if oldSettings.pumpID != settings.pumpID { + self.pumpState = PumpState() } } } - /** - Fetches glucose history entries which occurred on or after the specified date. - - History timestamps are reconciled with UTC based on the `timeZone` property of PumpState, as well as recorded clock change events. - - - parameter startDate: The earliest date of events to retrieve - - parameter completion: A closure called after the command is complete. This closure takes a single Result argument: - - success(events): An array of fetched history entries, in ascending order of insertion - - failure(error): An error describing why the command failed - - */ - public func getGlucoseHistoryEvents(since startDate: Date, completion: @escaping (Either<[TimestampedGlucoseEvent], Error>) -> Void) { - device.runSession(withName: "Get glucose history events") { (session) -> Void in - NSLog("Glucose history fetching task started.") - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - do { - let events = try ops.getGlucoseHistoryEvents(since: startDate) - DispatchQueue.main.async { () -> Void in - completion(.success(events)) + public func runSession(withName name: String, using deviceSelector: @escaping (_ completion: @escaping (_ device: RileyLinkDevice?) -> Void) -> Void, _ block: @escaping (_ session: PumpOpsSession?) -> Void) { + sessionQueue.async { + deviceSelector { (device) in + guard let device = device else { + block(nil) + return } - } catch let error { - DispatchQueue.main.async { () -> Void in - completion(.failure(error)) - } - } - } - } - public func writeGlucoseHistoryTimestamp(completion: @escaping (Either) -> Void) { - device.runSession(withName: "Write glucose history timestamp") { (session) -> Void in - NSLog("Write glucose history timestamp started.") - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - do { - _ = try ops.writeGlucoseHistoryTimestamp() - DispatchQueue.main.async { () -> Void in - completion(.success(true)) - } - } catch let error { - DispatchQueue.main.async { () -> Void in - completion(.failure(error)) - } + self.runSession(withName: name, using: device, block) } } } - /** - Reads the pump's clock - - This operation is performed asynchronously and the completion will be executed on an arbitrary background queue. - - - parameter completion: A closure called after the command is complete. This closure takes a single Result argument: - - success(clock): The pump's clock - - failure(error): An error describing why the command failed - */ - public func readTime(_ completion: @escaping (Either) -> Void) { - device.runSession(withName: "Read pump time") { (session) in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - - do { - let response: ReadTimeCarelinkMessageBody = try ops.messageBody(to: .readTime) + public func runSession(withName name: String, using device: RileyLinkDevice, _ block: @escaping (_ session: PumpOpsSession) -> Void) { + sessionQueue.async { + let semaphore = DispatchSemaphore(value: 0) - completion(.success(response.dateComponents)) - } catch let error { - completion(.failure(error)) + device.runSession(withName: name) { (session) in + let session = PumpOpsSession(settings: self.pumpSettings, pumpState: self.pumpState, session: session, delegate: self) + self.configureDevice(device, with: session) + block(session) + semaphore.signal() } - } - } - - - /** - Reads clock, reservoir, battery, bolusing, and suspended state from pump - - This operation is performed asynchronously and the completion will be executed on an arbitrary background queue. - - parameter completion: A closure called after the command is complete. This closure takes a single Result argument: - - success(status): A structure describing the current status of the pump - - failure(error): An error describing why the command failed - */ - public func readPumpStatus(_ completion: @escaping (Either) -> Void) { - device.runSession(withName: "Read pump status") { (session) in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - - do { - let response: PumpStatus = try ops.readPumpStatus() - completion(.success(response)) - } catch let error { - completion(.failure(error)) - } + semaphore.wait() } } - /** - Sets a bolus - - *Note: Use at your own risk!* - - This operation is performed asynchronously and the completion will be executed on an arbitrary background queue. - - - parameter units: The number of units to deliver - - parameter cancelExistingTemp: If true, additional pump commands will be issued to clear any running temp basal. Defaults to false. - - parameter completion: A closure called after the command is complete. This closure takes a single argument: - - error: An error describing why the command failed - */ - public func setNormalBolus(units: Double, cancelExistingTemp: Bool = false, completion: @escaping (_ error: SetBolusError?) -> Void) { - device.runSession(withName: "Set normal bolus") { (session) in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - completion(ops.setNormalBolus(units: units, cancelExistingTemp: cancelExistingTemp)) - } - } - - /** - Changes the current temporary basal rate - - This operation is performed asynchronously and the completion will be executed on an arbitrary background queue. - - - parameter unitsPerHour: The new basal rate, in Units per hour - - parameter duration: The duration of the rate - - parameter completion: A closure called after the command is complete. This closure takes a single Result argument: - - success(messageBody): The pump message body describing the new basal rate - - failure(error): An error describing why the command failed - */ - public func setTempBasal(rate unitsPerHour: Double, duration: TimeInterval, completion: @escaping (Either) -> Void) { - device.runSession(withName: "Set temp basal") { (session) in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - - do { - let response = try ops.setTempBasal(unitsPerHour, duration: duration) - completion(.success(response)) - } catch let error { - completion(.failure(error)) - } + // Must be called from within the RileyLinkDevice sessionQueue + private func configureDevice(_ device: RileyLinkDevice, with session: PumpOpsSession) { + guard !self.configuredDevices.contains(device) else { + return } - } - - /** - Changes the pump's clock to the specified date components - - This operation is performed asynchronously and the completion will be executed on an arbitrary background queue. - - - parameter generator: A closure which returns the desired date components. An exeception is raised if the date components are not valid. - - parameter completion: A closure called after the command is complete. This closure takes a single argument: - - error: An error describing why the command failed - */ - public func setTime(_ generator: @escaping () -> DateComponents, completion: @escaping (_ error: Error?) -> Void) { - device.runSession(withName: "Set time") { (session) in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - - do { - try ops.changeTime { - PumpMessage(packetType: .carelink, address: self.pumpState.pumpID, messageType: .changeTime, messageBody: ChangeTimeCarelinkMessageBody(dateComponents: generator())!) - } - completion(nil) - } catch let error { - completion(error) - } - } - } - - /** - Pairs the pump with a virtual "watchdog" device to enable it to broadcast periodic status packets. Only pump models x23 and up are supported. - - This operation is performed asynchronously and the completion will be executed on an arbitrary background queue. - - - parameter watchdogID: A 3-byte address for the watchdog device. - - parameter completion: A closure called after the command is complete. This closure takes a single argument: - - error: An error describing why the command failed. - */ - public func changeWatchdogMarriageProfile(toWatchdogID watchdogID: Data, completion: @escaping (_ error: Error?) -> Void) { - device.runSession(withName: "Change watchdog marriage profile") { (session) in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - - var lastError: Error? - for _ in 0..<3 { - do { - try ops.changeWatchdogMarriageProfile(watchdogID) - - lastError = nil - break - } catch let error { - lastError = error - } - } - - completion(lastError) + do { + _ = try session.configureRadio(for: pumpSettings.pumpRegion) + } catch { + // Ignore the error and let the block run anyway + return } + + NotificationCenter.default.post(name: .DeviceRadioConfigDidChange, object: device) + NotificationCenter.default.addObserver(self, selector: #selector(deviceRadioConfigDidChange(_:)), name: .DeviceRadioConfigDidChange, object: device) + configuredDevices.insert(device) } - func tuneRadio(for region: PumpRegion = .northAmerica, completion: @escaping (Either) -> Void) { - device.runSession(withName: "Tune pump") { (session) -> Void in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - do { - try ops.configureRadio(for: region) - let response = try ops.tuneRadio(for: region) - DispatchQueue.main.async { () -> Void in - completion(.success(response)) - } - } catch let error { - DispatchQueue.main.async { () -> Void in - completion(.failure(error)) - } - } + @objc private func deviceRadioConfigDidChange(_ note: Notification) { + guard let device = note.object as? RileyLinkDevice else { + return } + + NotificationCenter.default.removeObserver(self, name: .DeviceRadioConfigDidChange, object: device) + configuredDevices.remove(device) } - - public func setRXFilterMode(_ mode: RXFilterMode, completion: @escaping (_ error: Error?) -> Void) { - device.runSession(withName: "Set RX filter mode") { (session) in - let ops = PumpOpsSynchronous(pumpState: self.pumpState, session: session) - - do { - try ops.setRXFilterMode(mode) - completion(nil) - } catch let error { - completion(error) - } - } +} + + +extension PumpOps: PumpOpsSessionDelegate { + func pumpOpsSession(_ session: PumpOpsSession, didChange state: PumpState) { + self.pumpState = state } } diff --git a/RileyLinkKit/PumpOpsCommunication.swift b/RileyLinkKit/PumpOpsCommunication.swift deleted file mode 100644 index 754c292a8..000000000 --- a/RileyLinkKit/PumpOpsCommunication.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// PumpOpsCommunication.swift -// RileyLink -// -// Created by Jaim Zuber on 3/2/17. -// Copyright © 2017 Pete Schwamb. All rights reserved. -// - -import Foundation -import MinimedKit -import RileyLinkBLEKit - -class PumpOpsCommunication { - private static let standardPumpResponseWindow: UInt32 = 180 - private let expectedMaxBLELatencyMS = 1500 - - let session: RileyLinkCmdSession - - init(session: RileyLinkCmdSession) { - self.session = session - } - - func sendAndListen(_ msg: PumpMessage, timeoutMS: UInt32 = standardPumpResponseWindow, repeatCount: UInt8 = 0, msBetweenPackets: UInt8 = 0, retryCount: UInt8 = 3) throws -> PumpMessage { - let cmd = SendAndListenCmd() - cmd.packet = RFPacket(data: msg.txData) - cmd.timeoutMS = timeoutMS - cmd.repeatCount = repeatCount - cmd.msBetweenPackets = msBetweenPackets - cmd.retryCount = retryCount - cmd.listenChannel = 0 - - let minTimeBetweenPackets = 12 // At least 12 ms between packets for radio to stop/start - - let timeBetweenPackets = max(minTimeBetweenPackets, Int(msBetweenPackets)) - - // 16384 = bitrate, 8 = bits per byte, 6/4 = 4b6 encoding, 1000 = ms in 1s - let singlePacketSendTime = (Double(msg.txData.count * 8) * 6 / 4 / 16384.0) * 1000 - - let totalSendTime = Double(repeatCount) * (singlePacketSendTime + Double(timeBetweenPackets)) - - let totalTimeout = Int(retryCount+1) * (Int(totalSendTime) + Int(timeoutMS)) + expectedMaxBLELatencyMS - - guard session.doCmd(cmd, withTimeoutMs: totalTimeout) else { - throw PumpCommsError.rileyLinkTimeout - } - - guard let data = cmd.receivedPacket.data else { - if cmd.didReceiveResponse { - throw PumpCommsError.unknownResponse(rx: cmd.rawReceivedData.hexadecimalString, during: "Sent \(msg)") - } else { - throw PumpCommsError.noResponse(during: "Sent \(msg)") - } - } - - guard let message = PumpMessage(rxData: data) else { - throw PumpCommsError.unknownResponse(rx: data.hexadecimalString, during: "Sent \(msg)") - } - - guard message.address == msg.address else { - throw PumpCommsError.crosstalk(message, during: "Sent \(msg)") - } - - return message - } -} diff --git a/RileyLinkKit/PumpCommsError.swift b/RileyLinkKit/PumpOpsError.swift similarity index 59% rename from RileyLinkKit/PumpCommsError.swift rename to RileyLinkKit/PumpOpsError.swift index 8f2fb74f5..153de1e07 100644 --- a/RileyLinkKit/PumpCommsError.swift +++ b/RileyLinkKit/PumpOpsError.swift @@ -1,5 +1,5 @@ // -// PumpCommsError.swift +// PumpOpsError.swift // RileyLink // // Created by Pete Schwamb on 5/9/17. @@ -8,6 +8,7 @@ import Foundation import MinimedKit +import RileyLinkBLEKit /// An error that occurs during a command run @@ -15,27 +16,27 @@ import MinimedKit /// - command: The error took place during the command sequence /// - arguments: The error took place during the argument sequence public enum PumpCommandError: Error { - case command(PumpCommsError) - case arguments(PumpCommsError) + case command(PumpOpsError) + case arguments(PumpOpsError) } -public enum PumpCommsError: Error { +public enum PumpOpsError: Error { case bolusInProgress - case crosstalk(PumpMessage, during: String) - case noResponse(during: String) + case crosstalk(PumpMessage, during: CustomStringConvertible) + case deviceError(LocalizedError) + case noResponse(during: CustomStringConvertible) case pumpError(PumpErrorCode) case pumpSuspended case rfCommsFailure(String) - case rileyLinkTimeout case unexpectedResponse(PumpMessage, from: PumpMessage) case unknownPumpErrorCode(UInt8) - case unknownPumpModel - case unknownResponse(rx: String, during: String) + case unknownPumpModel(String) + case unknownResponse(rx: Data, during: CustomStringConvertible) } public enum SetBolusError: Error { - case certain(PumpCommsError) - case uncertain(PumpCommsError) + case certain(PumpOpsError) + case uncertain(PumpOpsError) } @@ -73,7 +74,7 @@ extension SetBolusError: LocalizedError { } -extension PumpCommsError: LocalizedError { +extension PumpOpsError: LocalizedError { public var failureReason: String? { switch self { case .bolusInProgress: @@ -86,19 +87,39 @@ extension PumpCommsError: LocalizedError { return NSLocalizedString("Pump is suspended.", comment: "") case .rfCommsFailure(let msg): return msg - case .rileyLinkTimeout: - return NSLocalizedString("RileyLink timed out.", comment: "") case .unexpectedResponse: return NSLocalizedString("Pump responded unexpectedly.", comment: "") case .unknownPumpErrorCode(let code): - return String(format: NSLocalizedString("Unknown pump error code: %1$@", comment: "The format string description of an unknown pump error code. (1: The specific error code raw value)"),String(describing: code)) - case .unknownPumpModel: - return NSLocalizedString("Unknown pump model.", comment: "") - case .unknownResponse: - return NSLocalizedString("Unknown response from pump.", comment: "") + return String(format: NSLocalizedString("Unknown pump error code: %1$@.", comment: "The format string description of an unknown pump error code. (1: The specific error code raw value)"), String(describing: code)) + case .unknownPumpModel(let model): + return String(format: NSLocalizedString("Unknown pump model: %@.", comment: ""), model) + case .unknownResponse(rx: let data, during: let during): + return String(format: NSLocalizedString("Unknown response during %1$@: %2$@", comment: "Format string for an unknown response. (1: The operation being performed) (2: The response data)"), String(describing: during), String(describing: data)) case .pumpError(let errorCode): - return String(format: NSLocalizedString("Pump error: %1$@", comment: "The format string description of a Pump Error. (1: The specific error code)"),String(describing: errorCode)) + return String(format: NSLocalizedString("Pump error: %1$@.", comment: "The format string description of a Pump Error. (1: The specific error code)"), String(describing: errorCode)) + case .deviceError(let error): + return String(format: NSLocalizedString("Device communication failed: %@.", comment: "Pump comms failure reason for an underlying peripheral error"), error.failureReason ?? "") + } + } + + public var recoverySuggestion: String? { + switch self { + case .pumpError(let errorCode): + return errorCode.recoverySuggestion + default: + return nil } } } + +extension PumpCommandError: LocalizedError { + public var failureReason: String? { + switch self { + case .arguments(let error): + return error.failureReason + case .command(let error): + return error.failureReason + } + } +} diff --git a/RileyLinkKit/PumpOpsSession.swift b/RileyLinkKit/PumpOpsSession.swift new file mode 100644 index 000000000..3915920c1 --- /dev/null +++ b/RileyLinkKit/PumpOpsSession.swift @@ -0,0 +1,990 @@ +// +// PumpOpsSynchronous.swift +// RileyLink +// +// Created by Pete Schwamb on 3/12/16. +// Copyright © 2016 Pete Schwamb. All rights reserved. +// + +import Foundation +import MinimedKit +import RileyLinkBLEKit + + +protocol PumpOpsSessionDelegate: class { + func pumpOpsSession(_ session: PumpOpsSession, didChange state: PumpState) +} + + +public class PumpOpsSession { + + private var pump: PumpState { + didSet { + delegate.pumpOpsSession(self, didChange: pump) + } + } + private let settings: PumpSettings + private let session: PumpMessageSender + + private unowned let delegate: PumpOpsSessionDelegate + + internal init(settings: PumpSettings, pumpState: PumpState, session: PumpMessageSender, delegate: PumpOpsSessionDelegate) { + self.settings = settings + self.pump = pumpState + self.session = session + self.delegate = delegate + } +} + + +// MARK: - Wakeup and power +extension PumpOpsSession { + private static let minimumTimeBetweenWakeAttempts = TimeInterval(minutes: 1) + + /// Attempts to send initial short wakeup message that kicks off the wakeup process. + /// + /// If successful, still does not fully wake up the pump - only alerts it such that the longer wakeup message can be sent next. + /// + /// - Throws: + /// - PumpCommandError.command containing: + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unexpectedResponse + /// - PumpOpsError.unknownResponse + private func sendWakeUpBurst() throws { + // Skip waking up if we recently tried + guard pump.lastWakeAttempt == nil || pump.lastWakeAttempt!.timeIntervalSinceNow <= -PumpOpsSession.minimumTimeBetweenWakeAttempts + else { + return + } + + pump.lastWakeAttempt = Date() + + let shortPowerMessage = PumpMessage(settings: settings, type: .powerOn) + + if pump.pumpModel == nil || !pump.pumpModel!.hasMySentry { + // Older pumps have a longer sleep cycle between wakeups, so send an initial burst + do { + let _: PumpAckMessageBody = try session.getResponse(to: shortPowerMessage, repeatCount: 255, timeout: .milliseconds(1), retryCount: 0) + } + catch { } + } + + do { + let _: PumpAckMessageBody = try session.getResponse(to: shortPowerMessage, repeatCount: 255, timeout: .seconds(12), retryCount: 0) + } catch let error as PumpOpsError { + throw PumpCommandError.command(error) + } + } + + private func isPumpResponding() -> Bool { + do { + let _: GetPumpModelCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .getPumpModel), responseType: .getPumpModel, retryCount: 1) + return true + } catch { + return false + } + } + + /// - Throws: + /// - PumpCommandError.command + /// - PumpCommandError.arguments + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unexpectedResponse + /// - PumpOpsError.unknownResponse + private func wakeup(_ duration: TimeInterval = TimeInterval(minutes: 1)) throws { + guard !pump.isAwake else { + return + } + + // Send a short message to the pump to see if its radio is still powered on + if isPumpResponding() { + // TODO: Convert logging + NSLog("Pump responding despite our wake timer having expired. Extending timer") + // By my observations, the pump stays awake > 1 minute past last comms. Usually + // About 1.5 minutes, but we'll make it a minute to be safe. + pump.awakeUntil = Date(timeIntervalSinceNow: TimeInterval(minutes: 1)) + return + } + + // Command + try sendWakeUpBurst() + + // Arguments + do { + let longPowerMessage = PumpMessage(settings: settings, type: .powerOn, body: PowerOnCarelinkMessageBody(duration: duration)) + let _: PumpAckMessageBody = try session.getResponse(to: longPowerMessage) + } catch let error as PumpOpsError { + throw PumpCommandError.arguments(error) + } catch { + assertionFailure() + } + + // TODO: Convert logging + NSLog("Power on for %.0f minutes", duration.minutes) + pump.awakeUntil = Date(timeIntervalSinceNow: duration) + } +} + +// MARK: - Single reads +extension PumpOpsSession { + /// Retrieves the pump model from either the state or from the + /// + /// - Parameter usingCache: Whether the pump state should be checked first for a known pump model + /// - Returns: The pump model + /// - Throws: + /// - PumpCommandError.command + /// - PumpCommandError.arguments + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unexpectedResponse + /// - PumpOpsError.unknownResponse + public func getPumpModel(usingCache: Bool = true) throws -> PumpModel { + if usingCache, let pumpModel = pump.pumpModel { + return pumpModel + } + + try wakeup() + let body: GetPumpModelCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .getPumpModel), responseType: .getPumpModel) + + guard let pumpModel = PumpModel(rawValue: body.model) else { + throw PumpOpsError.unknownPumpModel(body.model) + } + + pump.pumpModel = pumpModel + + return pumpModel + } + + /// - Throws: + /// - PumpCommandError.command + /// - PumpCommandError.arguments + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unexpectedResponse + /// - PumpOpsError.unknownResponse + public func getBatteryStatus() throws -> GetBatteryCarelinkMessageBody { + try wakeup() + return try session.getResponse(to: PumpMessage(settings: settings, type: .getBattery), responseType: .getBattery) + } + + /// - Throws: + /// - PumpCommandError.command + /// - PumpCommandError.arguments + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unexpectedResponse + /// - PumpOpsError.unknownResponse + internal func getPumpStatus() throws -> ReadPumpStatusMessageBody { + try wakeup() + return try session.getResponse(to: PumpMessage(settings: settings, type: .readPumpStatus), responseType: .readPumpStatus) + } + + /// - Throws: + /// - PumpCommandError.command + /// - PumpCommandError.arguments + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unexpectedResponse + /// - PumpOpsError.unknownResponse + public func getSettings() throws -> ReadSettingsCarelinkMessageBody { + try wakeup() + return try session.getResponse(to: PumpMessage(settings: settings, type: .readSettings), responseType: .readSettings) + } + + /// Reads the pump's time, returning a set of DateComponents in the pump's presumed time zone. + /// + /// - Returns: The pump's time components including timeZone + /// - Throws: + /// - PumpCommandError.command + /// - PumpCommandError.arguments + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unexpectedResponse + /// - PumpOpsError.unknownResponse + public func getTime() throws -> DateComponents { + try wakeup() + let response: ReadTimeCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readTime), responseType: .readTime) + var components = response.dateComponents + components.timeZone = pump.timeZone + return components + } + + /// Reads Basal Schedule from the pump + /// + /// - Returns: The pump's standard basal schedule + /// - Throws: + /// - PumpCommandError.command + /// - PumpCommandError.arguments + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unexpectedResponse + /// - PumpOpsError.unknownResponse + public func getBasalSchedule(for profile: BasalProfile = .standard) throws -> BasalSchedule { + try wakeup() + + var isFinished = false + var message = PumpMessage(settings: settings, type: profile.readMessageType) + var scheduleData = Data() + while (!isFinished) { + let body: DataFrameMessageBody = try session.getResponse(to: message, responseType: profile.readMessageType) + + scheduleData.append(body.contents) + isFinished = body.isLastFrame + message = PumpMessage(settings: settings, type: .pumpAck) + } + + return BasalSchedule(rawValue: scheduleData)! + } +} + + +// MARK: - Aggregate reads +public struct PumpStatus { + // Date components read from the pump, along with PumpState.timeZone + public let clock: DateComponents + public let batteryVolts: Double + public let batteryStatus: BatteryStatus + public let suspended: Bool + public let bolusing: Bool + public let reservoir: Double + public let model: PumpModel + public let pumpID: String +} + + +extension PumpOpsSession { + /// Reads the current insulin reservoir volume and the pump's date + /// + /// - Returns: + /// - The reservoir volume, in units of insulin + /// - DateCompoments representing the pump's clock + /// - Throws: + /// - PumpCommandError.command + /// - PumpCommandError.arguments + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unknownResponse + public func getRemainingInsulin() throws -> (units: Double, clock: DateComponents) { + + let pumpModel = try getPumpModel() + let pumpClock = try getTime() + + let reservoir: ReadRemainingInsulinMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readRemainingInsulin), responseType: .readRemainingInsulin) + + return ( + units: reservoir.getUnitsRemainingForStrokes(pumpModel.strokesPerUnit), + clock: pumpClock + ) + } + + /// Reads clock, reservoir, battery, bolusing, and suspended state from pump + /// + /// - Returns: The pump status + /// - Throws: + /// - PumpCommandError + /// - PumpOpsError + public func getCurrentPumpStatus() throws -> PumpStatus { + let pumpModel = try getPumpModel() + + let battResp = try getBatteryStatus() + + let status = try getPumpStatus() + + let (reservoir, clock) = try getRemainingInsulin() + + return PumpStatus( + clock: clock, + batteryVolts: battResp.volts, + batteryStatus: battResp.status, + suspended: status.suspended, + bolusing: status.bolusing, + reservoir: reservoir, + model: pumpModel, + pumpID: settings.pumpID + ) + } +} + + +// MARK: - Command messages +extension PumpOpsSession { + /// - Throws: `PumpCommandError` specifying the failure sequence + private func runCommandWithArguments(_ message: PumpMessage, responseType: MessageType = .pumpAck) throws -> T { + do { + try wakeup() + + let shortMessage = PumpMessage(packetType: message.packetType, address: message.address.hexadecimalString, messageType: message.messageType, messageBody: CarelinkShortMessageBody()) + let _: PumpAckMessageBody = try session.getResponse(to: shortMessage) + } catch let error as PumpOpsError { + throw PumpCommandError.command(error) + } + + do { + return try session.getResponse(to: message, responseType: responseType) + } catch let error as PumpOpsError { + throw PumpCommandError.arguments(error) + } + } + + /// - Throws: `PumpCommandError` specifying the failure sequence + public func pressButton(_ type: ButtonPressCarelinkMessageBody.ButtonType) throws { + let message = PumpMessage(settings: settings, type: .buttonPress, body: ButtonPressCarelinkMessageBody(buttonType: type)) + + let _: PumpAckMessageBody = try runCommandWithArguments(message) + } + + /// - Throws: PumpCommandError + public func selectBasalProfile(_ profile: BasalProfile) throws { + let message = PumpMessage(settings: settings, type: .selectBasalProfile, body: SelectBasalProfileMessageBody(newProfile: profile)) + + let _: PumpAckMessageBody = try runCommandWithArguments(message) + } + + /// Changes the current temporary basal rate + /// + /// - Parameters: + /// - unitsPerHour: The new basal rate, in Units per hour + /// - duration: The duration of the rate + /// - Returns: The pump message body describing the new basal rate + /// - Throws: PumpCommandError + public func setTempBasal(_ unitsPerHour: Double, duration: TimeInterval) throws -> ReadTempBasalCarelinkMessageBody { + var lastError: Error? + + let message = PumpMessage(settings: settings, type: .changeTempBasal, body: ChangeTempBasalCarelinkMessageBody(unitsPerHour: unitsPerHour, duration: duration)) + + for attempt in 1..<4 { + do { + do { + try wakeup() + + let shortMessage = PumpMessage(packetType: message.packetType, address: message.address.hexadecimalString, messageType: message.messageType, messageBody: CarelinkShortMessageBody()) + let _: PumpAckMessageBody = try session.getResponse(to: shortMessage) + } catch let error as PumpOpsError { + throw PumpCommandError.command(error) + } + + do { + let _: PumpAckMessageBody = try session.getResponse(to: message, retryCount: 0) + } catch PumpOpsError.pumpError(let errorCode) { + lastError = PumpCommandError.arguments(.pumpError(errorCode)) + break // Stop because we have a pump error response + } catch PumpOpsError.unknownPumpErrorCode(let errorCode) { + lastError = PumpCommandError.arguments(.unknownPumpErrorCode(errorCode)) + break // Stop because we have a pump error response + } catch { + // The pump does not ACK a successful temp basal. We'll check manually below if it was successful. + } + + let response: ReadTempBasalCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readTempBasal), responseType: .readTempBasal) + + if response.timeRemaining == duration && response.rateType == .absolute { + return response + } else { + throw PumpCommandError.arguments(PumpOpsError.rfCommsFailure("Could not verify TempBasal on attempt \(attempt). ")) + } + } catch let error { + lastError = error + } + } + + throw lastError ?? PumpOpsError.noResponse(during: "Set temp basal") + } + + /// Changes the pump's clock to the specified date components in the system time zone + /// + /// - Parameter generator: A closure which returns the desired date components. An exeception is raised if the date components are not valid. + /// - Throws: PumpCommandError + public func setTime(_ generator: () -> DateComponents) throws { + try wakeup() + + do { + let shortMessage = PumpMessage(settings: settings, type: .changeTime) + let _: PumpAckMessageBody = try session.getResponse(to: shortMessage) + } catch let error as PumpOpsError { + throw PumpCommandError.command(error) + } + + do { + let message = PumpMessage(settings: settings, type: .changeTime, body: ChangeTimeCarelinkMessageBody(dateComponents: generator())!) + let _: PumpAckMessageBody = try session.getResponse(to: message) + self.pump.timeZone = .currentFixed + } catch let error as PumpOpsError { + throw PumpCommandError.arguments(error) + } + } + + /// Sets a bolus + /// + /// *Note: Use at your own risk!* + /// + /// - Parameters: + /// - units: The number of units to deliver + /// - cancelExistingTemp: If true, additional pump commands will be issued to clear any running temp basal. Defaults to false. + /// - Throws: SetBolusError describing the certainty of the underlying error + public func setNormalBolus(units: Double, cancelExistingTemp: Bool = false) throws { + let pumpModel: PumpModel + + do { + try wakeup() + pumpModel = try getPumpModel() + + let status = try getPumpStatus() + + if status.bolusing { + throw PumpOpsError.bolusInProgress + } + + if status.suspended { + throw PumpOpsError.pumpSuspended + } + + if cancelExistingTemp { + _ = try setTempBasal(0, duration: 0) + } + } catch let error as PumpOpsError { + throw SetBolusError.certain(error) + } catch let error as PumpCommandError { + switch error { + case .command(let error): + throw SetBolusError.certain(error) + case .arguments(let error): + throw SetBolusError.certain(error) + } + } catch { + assertionFailure() + return + } + + do { + let message = PumpMessage(settings: settings, type: .bolus, body: BolusCarelinkMessageBody(units: units, strokesPerUnit: pumpModel.strokesPerUnit)) + + if pumpModel.returnsErrorOnBolus { + // TODO: This isn't working as expected; this logic was probably intended to be in the catch block below + let error: PumpErrorMessageBody = try runCommandWithArguments(message, responseType: .errorResponse) + + switch error.errorCode { + case .known(let errorCode): + if errorCode != .bolusInProgress { + throw PumpOpsError.pumpError(errorCode) + } + case .unknown(let unknownErrorCode): + throw PumpOpsError.unknownPumpErrorCode(unknownErrorCode) + } + } else { + let _: PumpAckMessageBody = try runCommandWithArguments(message) + } + } catch let error as PumpOpsError { + throw SetBolusError.certain(error) + } catch let error as PumpCommandError { + switch error { + case .command(let error): + throw SetBolusError.certain(error) + case .arguments(let error): + throw SetBolusError.uncertain(error) + } + } catch { + assertionFailure() + } + return + } + + /// - Throws: `PumpCommandError` specifying the failure sequence + public func setBasalSchedule(_ basalSchedule: BasalSchedule, for profile: BasalProfile, type: MessageType) throws { + + + let frames = DataFrameMessageBody.dataFramesFromContents(basalSchedule.rawValue) + + guard let firstFrame = frames.first else { + return + } + + NSLog(firstFrame.txData.hexadecimalString) + + let message = PumpMessage(settings: settings, type: type, body: firstFrame) + let _: PumpAckMessageBody = try runCommandWithArguments(message) + + for nextFrame in frames.dropFirst() { + let message = PumpMessage(settings: settings, type: type, body: nextFrame) + NSLog(nextFrame.txData.hexadecimalString) + do { + let _: PumpAckMessageBody = try session.getResponse(to: message) + } catch let error as PumpOpsError { + throw PumpCommandError.arguments(error) + } + } + } + + public func discoverCommands(_ updateHandler: (_ messages: [String]) -> Void) { + let codes: [MessageType] = [ + .PumpExperiment_O103, + ] + + for code in codes { + var messages = [String]() + + do { + messages.append(contentsOf: [ + "## Command \(code)", + ]) + + try wakeup() + + // Try the short command message, without any arguments. + let shortMessage = PumpMessage(settings: settings, type: code) + let _: PumpAckMessageBody = try session.getResponse(to: shortMessage) + + messages.append(contentsOf: [ + "Succeeded", + "" + ]) + + // Check history? + + } catch let error { + messages.append(contentsOf: [ + String(describing: error), + "", + ]) + } + + updateHandler(messages) + + Thread.sleep(until: Date(timeIntervalSinceNow: 2)) + } + } +} + + +// MARK: - MySentry (Watchdog) pairing +extension PumpOpsSession { + /// Pairs the pump with a virtual "watchdog" device to enable it to broadcast periodic status packets. Only pump models x23 and up are supported. + /// + /// - Parameter watchdogID: A 3-byte address for the watchdog device. + /// - Throws: + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unexpectedResponse + /// - PumpOpsError.unknownResponse + public func changeWatchdogMarriageProfile(_ watchdogID: Data) throws { + try setRXFilterMode(.wide) + defer { + do { + try configureRadio(for: settings.pumpRegion) + } catch { + // Best effort resetting radio filter mode + } + } + + let commandTimeout = TimeInterval(seconds: 30) + + // Wait for the pump to start polling + guard let encodedData = try session.listenForPacket(onChannel: 0, timeout: commandTimeout)?.data else { + throw PumpOpsError.noResponse(during: "Watchdog listening") + } + + guard let packet = MinimedPacket(encodedData: encodedData) else { + // TODO: Change error to better reflect that this is an encoding or CRC error + throw PumpOpsError.unknownResponse(rx: encodedData, during: "Watchdog listening") + } + + guard let findMessage = PumpMessage(rxData: packet.data) else { + // Unknown packet type or message type + throw PumpOpsError.unknownResponse(rx: packet.data, during: "Watchdog listening") + } + + guard findMessage.address.hexadecimalString == settings.pumpID && findMessage.packetType == .mySentry, + let findMessageBody = findMessage.messageBody as? FindDeviceMessageBody, let findMessageResponseBody = MySentryAckMessageBody(sequence: findMessageBody.sequence, watchdogID: watchdogID, responseMessageTypes: [findMessage.messageType]) + else { + throw PumpOpsError.unknownResponse(rx: packet.data, during: "Watchdog listening") + } + + // Identify as a MySentry device + let findMessageResponse = PumpMessage(packetType: .mySentry, address: settings.pumpID, messageType: .pumpAck, messageBody: findMessageResponseBody) + + let linkMessage = try session.sendAndListen(findMessageResponse, timeout: commandTimeout) + + guard let + linkMessageBody = linkMessage.messageBody as? DeviceLinkMessageBody, + let linkMessageResponseBody = MySentryAckMessageBody(sequence: linkMessageBody.sequence, watchdogID: watchdogID, responseMessageTypes: [linkMessage.messageType]) + else { + throw PumpOpsError.unexpectedResponse(linkMessage, from: findMessageResponse) + } + + // Acknowledge the pump linked with us + let linkMessageResponse = PumpMessage(packetType: .mySentry, address: settings.pumpID, messageType: .pumpAck, messageBody: linkMessageResponseBody) + + try session.send(linkMessageResponse) + } +} + + +// MARK: - Tuning +private extension PumpRegion { + var scanFrequencies: [Measurement] { + let scanFrequencies: [Double] + + switch self { + case .worldWide: + scanFrequencies = [868.25, 868.30, 868.35, 868.40, 868.45, 868.50, 868.55, 868.60, 868.65] + case .northAmerica: + scanFrequencies = [916.45, 916.50, 916.55, 916.60, 916.65, 916.70, 916.75, 916.80] + } + + return scanFrequencies.map { + return Measurement(value: $0, unit: .megahertz) + } + } +} + +enum RXFilterMode: UInt8 { + case wide = 0x50 // 300KHz + case narrow = 0x90 // 150KHz +} + +public struct FrequencyTrial { + public var tries: Int = 0 + public var successes: Int = 0 + public var avgRSSI: Double = -99 + public var frequency: Measurement + + init(frequency: Measurement) { + self.frequency = frequency + } +} + +public struct FrequencyScanResults { + public var trials: [FrequencyTrial] + public var bestFrequency: Measurement +} + +extension PumpOpsSession { + /// - Throws: + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.rfCommsFailure + public func tuneRadio(current: Measurement?) throws -> FrequencyScanResults { + let region = self.settings.pumpRegion + + do { + let results = try scanForPump(in: region.scanFrequencies, current: current) + + return results + } catch let error as PumpOpsError { + throw error + } catch let error as LocalizedError { + throw PumpOpsError.deviceError(error) + } + } + + /// - Throws: PumpOpsError.deviceError + private func setRXFilterMode(_ mode: RXFilterMode) throws { + let drate_e = UInt8(0x9) // exponent of symbol rate (16kbps) + let chanbw = mode.rawValue + do { + try session.updateRegister(.mdmcfg4, value: chanbw | drate_e) + } catch let error as LocalizedError { + throw PumpOpsError.deviceError(error) + } + } + + /// - Throws: + /// - PumpOpsError.deviceError + /// - RileyLinkDeviceError + func configureRadio(for region: PumpRegion) throws { + try session.resetRadioConfig() + + switch region { + case .worldWide: + //try session.updateRegister(.mdmcfg4, value: 0x59) + try setRXFilterMode(.wide) + //try session.updateRegister(.mdmcfg3, value: 0x66) + //try session.updateRegister(.mdmcfg2, value: 0x33) + try session.updateRegister(.mdmcfg1, value: 0x62) + try session.updateRegister(.mdmcfg0, value: 0x1A) + try session.updateRegister(.deviatn, value: 0x13) + case .northAmerica: + //try session.updateRegister(.mdmcfg4, value: 0x99) + try setRXFilterMode(.narrow) + //try session.updateRegister(.mdmcfg3, value: 0x66) + //try session.updateRegister(.mdmcfg2, value: 0x33) + try session.updateRegister(.mdmcfg1, value: 0x61) + try session.updateRegister(.mdmcfg0, value: 0x7E) + try session.updateRegister(.deviatn, value: 0x15) + } + } + + /// - Throws: + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.rfCommsFailure + private func scanForPump(in frequencies: [Measurement], current: Measurement?) throws -> FrequencyScanResults { + + var trials = [FrequencyTrial]() + + let middleFreq = frequencies[frequencies.count / 2] + + do { + // Needed to put the pump in listen mode + try session.setBaseFrequency(middleFreq) + try wakeup() + } catch { + // Continue anyway; the pump likely heard us, even if we didn't hear it. + } + + for freq in frequencies { + let tries = 3 + var trial = FrequencyTrial(frequency: freq) + + try session.setBaseFrequency(freq) + var sumRSSI = 0 + for _ in 1...tries { + // Ignore failures here + let rfPacket = try? session.sendAndListenForPacket(PumpMessage(settings: settings, type: .getPumpModel)) + if let rfPacket = rfPacket, + let pkt = MinimedPacket(encodedData: rfPacket.data), + let response = PumpMessage(rxData: pkt.data), response.messageType == .getPumpModel + { + sumRSSI += rfPacket.rssi + trial.successes += 1 + } + trial.tries += 1 + } + // Mark each failure as a -99 rssi, so we can use highest rssi as best freq + sumRSSI += -99 * (trial.tries - trial.successes) + trial.avgRSSI = Double(sumRSSI) / Double(trial.tries) + trials.append(trial) + } + let sortedTrials = trials.sorted(by: { (a, b) -> Bool in + return a.avgRSSI > b.avgRSSI + }) + + guard sortedTrials.first!.successes > 0 else { + try session.setBaseFrequency(current ?? middleFreq) + throw PumpOpsError.rfCommsFailure("No pump responses during scan") + } + + let results = FrequencyScanResults( + trials: trials, + bestFrequency: sortedTrials.first!.frequency + ) + + try session.setBaseFrequency(results.bestFrequency) + + return results + } +} + + +// MARK: - Pump history +extension PumpOpsSession { + /// Fetches history entries which occurred on or after the specified date. + /// + /// It is possible for Minimed Pumps to non-atomically append multiple history entries with the same timestamp, for example, `BolusWizardEstimatePumpEvent` may appear and be read before `BolusNormalPumpEvent` is written. Therefore, the `startDate` parameter is used as part of an inclusive range, leaving the client to manage the possibility of duplicates. + /// + /// History timestamps are reconciled with UTC based on the `timeZone` property of PumpState, as well as recorded clock change events. + /// + /// - Parameter startDate: The earliest date of events to retrieve + /// - Returns: + /// - An array of fetched history entries, in ascending order of insertion + /// - The pump model + /// - Throws: + /// - PumpCommandError.command + /// - PumpCommandError.arguments + /// - PumpOpsError.crosstalk + /// - PumpOpsError.deviceError + /// - PumpOpsError.noResponse + /// - PumpOpsError.unknownResponse + public func getHistoryEvents(since startDate: Date) throws -> ([TimestampedHistoryEvent], PumpModel) { + try wakeup() + + let pumpModel = try getPumpModel() + + var events = [TimestampedHistoryEvent]() + + pages: for pageNum in 0..<16 { + // TODO: Convert logging + NSLog("Fetching page %d", pageNum) + let pageData: Data + + do { + pageData = try getHistoryPage(pageNum) + } catch PumpOpsError.pumpError { + break pages + } + + var idx = 0 + let chunkSize = 256 + while idx < pageData.count { + let top = min(idx + chunkSize, pageData.count) + let range = Range(uncheckedBounds: (lower: idx, upper: top)) + // TODO: Convert logging + NSLog(String(format: "HistoryPage %02d - (bytes %03d-%03d): ", pageNum, idx, top-1) + pageData.subdata(in: range).hexadecimalString) + idx = top + } + + let page = try HistoryPage(pageData: pageData, pumpModel: pumpModel) + + let (timestampedEvents, hasMoreEvents, _) = page.timestampedEvents(after: startDate, timeZone: pump.timeZone, model: pumpModel) + + events = timestampedEvents + events + + if !hasMoreEvents { + break + } + } + return (events, pumpModel) + } + + private func getHistoryPage(_ pageNum: Int) throws -> Data { + var frameData = Data() + + let msg = PumpMessage(settings: settings, type: .getHistoryPage, body: GetHistoryPageCarelinkMessageBody(pageNum: pageNum)) + + var curResp: GetHistoryPageCarelinkMessageBody = try runCommandWithArguments(msg, responseType: .getHistoryPage) + + var expectedFrameNum = 1 + + while(expectedFrameNum == curResp.frameNumber) { + frameData.append(curResp.frame) + expectedFrameNum += 1 + let msg = PumpMessage(settings: settings, type: .pumpAck) + if !curResp.lastFrame { + curResp = try session.getResponse(to: msg, responseType: .getHistoryPage) + } else { + try session.send(msg) + break + } + } + + guard frameData.count == 1024 else { + throw PumpOpsError.rfCommsFailure("Short history page: \(frameData.count) bytes. Expected 1024") + } + return frameData + } +} + + +// MARK: - Glucose history +extension PumpOpsSession { + private func logGlucoseHistory(pageData: Data, pageNum: Int) { + var idx = 0 + let chunkSize = 256 + while idx < pageData.count { + let top = min(idx + chunkSize, pageData.count) + let range = Range(uncheckedBounds: (lower: idx, upper: top)) + // TODO: Convert logging + NSLog(String(format: "GlucosePage %02d - (bytes %03d-%03d): ", pageNum, idx, top-1) + pageData.subdata(in: range).hexadecimalString) + idx = top + } + } + + /// Fetches glucose history entries which occurred on or after the specified date. + /// + /// History timestamps are reconciled with UTC based on the `timeZone` property of PumpState, as well as recorded clock change events. + /// + /// - Parameter startDate: The earliest date of events to retrieve + /// - Returns: An array of fetched history entries, in ascending order of insertion + /// - Throws: + public func getGlucoseHistoryEvents(since startDate: Date) throws -> [TimestampedGlucoseEvent] { + try wakeup() + + var events = [TimestampedGlucoseEvent]() + + let currentGlucosePage: ReadCurrentGlucosePageMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readCurrentGlucosePage), responseType: .readCurrentGlucosePage) + let startPage = Int(currentGlucosePage.pageNum) + //max lookback of 15 pages or when page is 0 + let endPage = max(startPage - 15, 0) + + pages: for pageNum in stride(from: startPage, to: endPage - 1, by: -1) { + // TODO: Convert logging + NSLog("Fetching page %d", pageNum) + var pageData: Data + var page: GlucosePage + + do { + pageData = try getGlucosePage(UInt32(pageNum)) + // logGlucoseHistory(pageData: pageData, pageNum: pageNum) + page = try GlucosePage(pageData: pageData) + + if page.needsTimestamp && pageNum == startPage { + // TODO: Convert logging + NSLog(String(format: "GlucosePage %02d needs a new sensor timestamp, writing...", pageNum)) + let _ = try writeGlucoseHistoryTimestamp() + + //fetch page again with new sensor timestamp + pageData = try getGlucosePage(UInt32(pageNum)) + logGlucoseHistory(pageData: pageData, pageNum: pageNum) + page = try GlucosePage(pageData: pageData) + } + } catch PumpOpsError.pumpError { + break pages + } + + for event in page.events.reversed() { + var timestamp = event.timestamp + timestamp.timeZone = pump.timeZone + + if event is UnknownGlucoseEvent { + continue pages + } + + if let date = timestamp.date { + if date < startDate && event is SensorTimestampGlucoseEvent { + // TODO: Convert logging + NSLog("Found reference event at (%@) to be before startDate(%@)", date as NSDate, startDate as NSDate) + break pages + } else { + events.insert(TimestampedGlucoseEvent(glucoseEvent: event, date: date), at: 0) + } + } + } + } + return events + } + + private func getGlucosePage(_ pageNum: UInt32) throws -> Data { + var frameData = Data() + + let msg = PumpMessage(settings: settings, type: .getGlucosePage, body: GetGlucosePageMessageBody(pageNum: pageNum)) + + var curResp: GetGlucosePageMessageBody = try runCommandWithArguments(msg, responseType: .getGlucosePage) + + var expectedFrameNum = 1 + + while(expectedFrameNum == curResp.frameNumber) { + frameData.append(curResp.frame) + expectedFrameNum += 1 + let msg = PumpMessage(settings: settings, type: .pumpAck) + if !curResp.lastFrame { + curResp = try session.getResponse(to: msg, responseType: .getGlucosePage) + } else { + try session.send(msg) + break + } + } + + guard frameData.count == 1024 else { + throw PumpOpsError.rfCommsFailure("Short glucose history page: \(frameData.count) bytes. Expected 1024") + } + return frameData + } + + public func writeGlucoseHistoryTimestamp() throws -> Void { + try wakeup() + + let shortWriteTimestamp = PumpMessage(settings: settings, type: .writeGlucoseHistoryTimestamp) + let _: PumpAckMessageBody = try session.getResponse(to: shortWriteTimestamp, timeout: .seconds(12)) + } +} diff --git a/RileyLinkKit/PumpOpsSynchronous.swift b/RileyLinkKit/PumpOpsSynchronous.swift deleted file mode 100644 index 5d86dde7b..000000000 --- a/RileyLinkKit/PumpOpsSynchronous.swift +++ /dev/null @@ -1,820 +0,0 @@ -// -// PumpOpsSynchronous.swift -// RileyLink -// -// Created by Pete Schwamb on 3/12/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -import Foundation -import MinimedKit -import RileyLinkBLEKit - - -public enum RXFilterMode: UInt8 { - case wide = 0x50 // 300KHz - case narrow = 0x90 // 150KHz -} - -class PumpOpsSynchronous { - - public static let PacketKey = "com.rileylink.RileyLinkKit.PumpOpsSynchronousPacketKey" - - private static let standardPumpResponseWindow: UInt32 = 180 - private let expectedMaxBLELatencyMS = 1500 - - // After - private let minimumTimeBetweenWakeAttempts = TimeInterval(minutes: 1) - - var communication: PumpOpsCommunication - let pump: PumpState - let session: RileyLinkCmdSession - - init(pumpState: PumpState, session: RileyLinkCmdSession) { - self.pump = pumpState - self.session = session - self.communication = PumpOpsCommunication(session: session) - } - - internal func makePumpMessage(to messageType: MessageType, using body: MessageBody = CarelinkShortMessageBody()) -> PumpMessage { - return PumpMessage(packetType: .carelink, address: pump.pumpID, messageType: messageType, messageBody: body) - } - - /// Attempts to send initial short wakeup message that kicks off the wakeup process. - /// - /// If successful, still does not fully wake up the pump - only alerts it such that the longer wakeup message can be sent next. - /// - /// - Throws: - /// - PumpCommsError.rileyLinkTimeout - /// - PumpCommsError.unknownResponse - /// - PumpCommsError.noResponse - /// - PumpCommsError.crosstalk - /// - PumpCommsError.unexpectedResponse - private func sendWakeUpBurst() throws { - var lastError: Error? - - if (pump.lastWakeAttempt != nil && pump.lastWakeAttempt!.timeIntervalSinceNow > -minimumTimeBetweenWakeAttempts) { - return - } - - if pump.pumpModel == nil || !pump.pumpModel!.hasMySentry { - // Older pumps have a longer sleep cycle between wakeups, so send an initial burst - do { - let shortPowerMessage = makePumpMessage(to: .powerOn) - _ = try communication.sendAndListen(shortPowerMessage, timeoutMS: 1, repeatCount: 255, msBetweenPackets: 0, retryCount: 0) - } - catch { } - } - - do { - let shortPowerMessage = makePumpMessage(to: .powerOn) - let shortResponse = try communication.sendAndListen(shortPowerMessage, timeoutMS: 12000, repeatCount: 255, msBetweenPackets: 0, retryCount: 0) - - if shortResponse.messageType == .pumpAck { - // Pump successfully received and responded to short wakeup message! - return - } else { - lastError = PumpCommsError.unexpectedResponse(shortResponse, from: shortPowerMessage) - } - } catch let error { - lastError = error - } - - pump.lastWakeAttempt = Date() - - if let lastError = lastError { - // If all attempts failed, throw the final error - throw lastError - } - } - - private func pumpResponding() -> Bool { - do { - let msg = makePumpMessage(to: .getPumpModel) - let response = try communication.sendAndListen(msg, retryCount: 1) - - if response.messageType == .getPumpModel && response.messageBody is GetPumpModelCarelinkMessageBody { - return true - } - } catch { - } - return false - } - - /// - Throws: - /// - PumpCommsError.rileyLinkTimeout - /// - PumpCommsError.unknownResponse - /// - PumpCommsError.noResponse - /// - PumpCommsError.crosstalk - /// - PumpCommsError.unexpectedResponse - private func wakeup(_ duration: TimeInterval = TimeInterval(minutes: 1)) throws { - guard !pump.isAwake else { - return - } - - if pumpResponding() { - NSLog("Pump responding despite our wake timer having expired. Extending timer") - // By my observations, the pump stays awake > 1 minute past last comms. Usually - // About 1.5 minutes, but we'll make it a minute to be safe. - pump.awakeUntil = Date(timeIntervalSinceNow: TimeInterval(minutes: 1)) - return - } - - try sendWakeUpBurst() - - let longPowerMessage = makePumpMessage(to: .powerOn, using: PowerOnCarelinkMessageBody(duration: duration)) - let longResponse = try communication.sendAndListen(longPowerMessage) - - guard longResponse.messageType == .pumpAck else { - throw PumpCommsError.unexpectedResponse(longResponse, from: longPowerMessage) - } - - NSLog("Power on for %.0f minutes", duration.minutes) - pump.awakeUntil = Date(timeIntervalSinceNow: duration) - } - - /// - Throws: `PumpCommandError` specifying the failure sequence - internal func runCommandWithArguments(_ msg: PumpMessage, responseMessageType: MessageType = .pumpAck) throws -> PumpMessage { - do { - try wakeup() - - let shortMsg = makePumpMessage(to: msg.messageType) - let shortResponse = try communication.sendAndListen(shortMsg) - - guard shortResponse.messageType == .pumpAck else { - throw PumpCommsError.unexpectedResponse(shortResponse, from: shortMsg) - } - } catch let error { - throw PumpCommandError.command(error as! PumpCommsError) - } - - do { - let response = try communication.sendAndListen(msg) - - guard response.messageType == responseMessageType else { - throw PumpCommsError.unexpectedResponse(response, from: msg) - } - - return response - } catch let error { - throw PumpCommandError.arguments(error as! PumpCommsError) - } - } - - /// - Throws: - /// - PumpCommsError.rileyLinkTimeout - /// - PumpCommsError.unknownResponse - /// - PumpCommsError.noResponse - /// - PumpCommsError.crosstalk - /// - PumpCommsError.unexpectedResponse - internal func getPumpModelNumber() throws -> String { - let body: GetPumpModelCarelinkMessageBody = try messageBody(to: .getPumpModel) - return body.model - } - - - /// Reads Basal Schedule from the pump - /// - /// - Returns: Array of Basal Schedule Data - /// - Throws: PumpCommsError - internal func getBasalSchedule() throws -> BasalSchedule { - - try wakeup() - - var finished = false - var msg = makePumpMessage(to: .readProfileSTD512) - var scheduleData = Data() - while (!finished) { - let response = try communication.sendAndListen(msg) - - guard response.messageType == .readProfileSTD512, - let body = response.messageBody as? DataFrameMessageBody else { - throw PumpCommsError.unexpectedResponse(response, from: msg) - } - scheduleData.append(body.contents) - finished = body.lastFrameFlag - msg = makePumpMessage(to: .pumpAck) - } - - return BasalSchedule(data: scheduleData) - } - - - /// Retrieves the pump model from either the state or from the - /// - /// - Returns: The pump model - /// - Throws: `PumpCommsError` - internal func getPumpModel() throws -> PumpModel { - if let pumpModel = pump.pumpModel { - return pumpModel - } - - guard let pumpModel = try PumpModel(rawValue: getPumpModelNumber()) else { - throw PumpCommsError.unknownPumpModel - } - - pump.pumpModel = pumpModel - - return pumpModel - } - - /// - Throws: - /// - PumpCommsError.rileyLinkTimeout - /// - PumpCommsError.unknownResponse - /// - PumpCommsError.noResponse - /// - PumpCommsError.crosstalk - /// - PumpCommsError.unexpectedResponse - internal func messageBody(to messageType: MessageType) throws -> T { - try wakeup() - - let msg = makePumpMessage(to: messageType) - let response = try communication.sendAndListen(msg) - - guard response.messageType == messageType, let body = response.messageBody as? T else { - throw PumpCommsError.unexpectedResponse(response, from: msg) - } - return body - } - - internal func setTempBasal(_ unitsPerHour: Double, duration: TimeInterval) throws -> ReadTempBasalCarelinkMessageBody { - - try wakeup() - var lastError: Error? - - let changeMessage = PumpMessage(packetType: .carelink, address: pump.pumpID, messageType: .changeTempBasal, messageBody: ChangeTempBasalCarelinkMessageBody(unitsPerHour: unitsPerHour, duration: duration)) - - for attempt in 0..<3 { - do { - _ = try communication.sendAndListen(makePumpMessage(to: changeMessage.messageType)) - - do { - let response = try communication.sendAndListen(changeMessage, retryCount: 0) - if let errorMsg = response.messageBody as? PumpErrorMessageBody { - switch errorMsg.errorCode { - case .known(let errorCode): - lastError = PumpCommsError.pumpError(errorCode) - case .unknown(let unknownErrorCode): - lastError = PumpCommsError.unknownPumpErrorCode(unknownErrorCode) - } - break - } - } catch { - // The pump does not ACK a successful temp basal. We'll check manually below if it was successful. - } - - let response: ReadTempBasalCarelinkMessageBody = try messageBody(to: .readTempBasal) - - if response.timeRemaining == duration && response.rateType == .absolute { - return response - } else { - lastError = PumpCommsError.rfCommsFailure("Could not verify TempBasal on attempt \(attempt). ") - } - } catch let error { - lastError = error - } - } - - throw lastError! - } - - internal func changeTime(_ messageGenerator: () -> PumpMessage) throws { - try wakeup() - - let shortMessage = makePumpMessage(to: .changeTime) - let shortResponse = try communication.sendAndListen(shortMessage) - - guard shortResponse.messageType == .pumpAck else { - throw PumpCommsError.unexpectedResponse(shortResponse, from: shortMessage) - } - - let message = messageGenerator() - let response = try communication.sendAndListen(message) - - guard response.messageType == .pumpAck else { - throw PumpCommsError.unexpectedResponse(response, from: message) - } - } - - internal func changeWatchdogMarriageProfile(_ watchdogID: Data) throws { - let commandTimeoutMS: UInt32 = 30_000 - - // Wait for the pump to start polling - let listenForFindMessageCmd = GetPacketCmd() - listenForFindMessageCmd.listenChannel = 0 - listenForFindMessageCmd.timeoutMS = commandTimeoutMS - - guard session.doCmd(listenForFindMessageCmd, withTimeoutMs: Int(commandTimeoutMS) + expectedMaxBLELatencyMS) else { - throw PumpCommsError.rileyLinkTimeout - } - - guard let data = listenForFindMessageCmd.receivedPacket.data else { - throw PumpCommsError.noResponse(during: "Watchdog listening") - } - - guard let findMessage = PumpMessage(rxData: data), findMessage.address.hexadecimalString == pump.pumpID && findMessage.packetType == .mySentry, - let findMessageBody = findMessage.messageBody as? FindDeviceMessageBody, let findMessageResponseBody = MySentryAckMessageBody(sequence: findMessageBody.sequence, watchdogID: watchdogID, responseMessageTypes: [findMessage.messageType]) - else { - throw PumpCommsError.unknownResponse(rx: data.hexadecimalString, during: "Watchdog listening") - } - - // Identify as a MySentry device - let findMessageResponse = PumpMessage(packetType: .mySentry, address: pump.pumpID, messageType: .pumpAck, messageBody: findMessageResponseBody) - - let linkMessage = try communication.sendAndListen(findMessageResponse, timeoutMS: commandTimeoutMS) - - guard let - linkMessageBody = linkMessage.messageBody as? DeviceLinkMessageBody, - let linkMessageResponseBody = MySentryAckMessageBody(sequence: linkMessageBody.sequence, watchdogID: watchdogID, responseMessageTypes: [linkMessage.messageType]) - else { - throw PumpCommsError.unexpectedResponse(linkMessage, from: findMessageResponse) - } - - // Acknowledge the pump linked with us - let linkMessageResponse = PumpMessage(packetType: .mySentry, address: pump.pumpID, messageType: .pumpAck, messageBody: linkMessageResponseBody) - - let cmd = SendPacketCmd() - cmd.packet = RFPacket(data: linkMessageResponse.txData) - session.doCmd(cmd, withTimeoutMs: expectedMaxBLELatencyMS) - } - - internal func setRXFilterMode(_ mode: RXFilterMode) throws { - let drate_e = UInt8(0x9) // exponent of symbol rate (16kbps) - let chanbw = mode.rawValue - try updateRegister(UInt8(CC111X_REG_MDMCFG4), value: chanbw | drate_e) - } - - func configureRadio(for region: PumpRegion) throws { - switch region { - case .worldWide: - try updateRegister(UInt8(CC111X_REG_MDMCFG4), value: 0x59) - //try updateRegister(UInt8(CC111X_REG_MDMCFG3), value: 0x66) - //try updateRegister(UInt8(CC111X_REG_MDMCFG2), value: 0x33) - try updateRegister(UInt8(CC111X_REG_MDMCFG1), value: 0x62) - try updateRegister(UInt8(CC111X_REG_MDMCFG0), value: 0x1A) - try updateRegister(UInt8(CC111X_REG_DEVIATN), value: 0x13) - case .northAmerica: - try updateRegister(UInt8(CC111X_REG_MDMCFG4), value: 0x99) - //try updateRegister(UInt8(CC111X_REG_MDMCFG3), value: 0x66) - //try updateRegister(UInt8(CC111X_REG_MDMCFG2), value: 0x33) - try updateRegister(UInt8(CC111X_REG_MDMCFG1), value: 0x61) - try updateRegister(UInt8(CC111X_REG_MDMCFG0), value: 0x7E) - try updateRegister(UInt8(CC111X_REG_DEVIATN), value: 0x15) - } - } - - private func updateRegister(_ addr: UInt8, value: UInt8) throws { - let cmd = UpdateRegisterCmd() - cmd.addr = addr - cmd.value = value - if !session.doCmd(cmd, withTimeoutMs: expectedMaxBLELatencyMS) { - throw PumpCommsError.rileyLinkTimeout - } - } - - internal func setBaseFrequency(_ freqMHz: Double) throws { - let val = Int((freqMHz * 1000000)/(Double(RILEYLINK_FREQ_XTAL)/pow(2.0,16.0))) - - try updateRegister(UInt8(CC111X_REG_FREQ0), value:UInt8(val & 0xff)) - try updateRegister(UInt8(CC111X_REG_FREQ1), value:UInt8((val >> 8) & 0xff)) - try updateRegister(UInt8(CC111X_REG_FREQ2), value:UInt8((val >> 16) & 0xff)) - NSLog("Set frequency to %f", freqMHz) - } - - internal func tuneRadio(for region: PumpRegion) throws -> FrequencyScanResults { - - let scanFrequencies: [Double] - - switch region { - case .worldWide: - scanFrequencies = [868.25, 868.30, 868.35, 868.40, 868.45, 868.50, 868.55, 868.60, 868.65] - case .northAmerica: - scanFrequencies = [916.45, 916.50, 916.55, 916.60, 916.65, 916.70, 916.75, 916.80] - } - - return try scanForPump(in: scanFrequencies) - } - - internal func scanForPump(in frequencies: [Double]) throws -> FrequencyScanResults { - - var results = FrequencyScanResults() - - let middleFreq = frequencies[frequencies.count / 2] - - do { - // Needed to put the pump in listen mode - try setBaseFrequency(middleFreq) - try wakeup() - } catch { - // Continue anyway; the pump likely heard us, even if we didn't hear it. - } - - for freq in frequencies { - let tries = 3 - var trial = FrequencyTrial() - trial.frequencyMHz = freq - try setBaseFrequency(freq) - var sumRSSI = 0 - for _ in 1...tries { - let msg = makePumpMessage(to: .getPumpModel) - let cmd = SendAndListenCmd() - cmd.packet = RFPacket(data: msg.txData) - cmd.timeoutMS = type(of: self).standardPumpResponseWindow - if session.doCmd(cmd, withTimeoutMs: expectedMaxBLELatencyMS) { - if let data = cmd.receivedPacket.data, - let response = PumpMessage(rxData: data), response.messageType == .getPumpModel { - sumRSSI += Int(cmd.receivedPacket.rssi) - trial.successes += 1 - } - } else { - throw PumpCommsError.rileyLinkTimeout - } - trial.tries += 1 - } - // Mark each failure as a -99 rssi, so we can use highest rssi as best freq - sumRSSI += -99 * (trial.tries - trial.successes) - trial.avgRSSI = Double(sumRSSI) / Double(trial.tries) - results.trials.append(trial) - } - let sortedTrials = results.trials.sorted(by: { (a, b) -> Bool in - return a.avgRSSI > b.avgRSSI - }) - if sortedTrials.first!.successes > 0 { - results.bestFrequency = sortedTrials.first!.frequencyMHz - try setBaseFrequency(results.bestFrequency) - pump.lastValidFrequency = results.bestFrequency - } else { - try setBaseFrequency(pump.lastValidFrequency ?? middleFreq) - throw PumpCommsError.rfCommsFailure("No pump responses during scan") - } - - return results - } - - internal func getHistoryEvents(since startDate: Date) throws -> ([TimestampedHistoryEvent], PumpModel) { - try wakeup() - - let pumpModel = try getPumpModel() - - var events = [TimestampedHistoryEvent]() - - pages: for pageNum in 0..<16 { - NSLog("Fetching page %d", pageNum) - let pageData: Data - - do { - pageData = try getHistoryPage(pageNum) - } catch let error as PumpCommsError { - if case .unexpectedResponse(let response, from: _) = error, response.messageType == .errorResponse { - break pages - } else { - throw error - } - } - - var idx = 0 - let chunkSize = 256 - while idx < pageData.count { - let top = min(idx + chunkSize, pageData.count) - let range = Range(uncheckedBounds: (lower: idx, upper: top)) - NSLog(String(format: "HistoryPage %02d - (bytes %03d-%03d): ", pageNum, idx, top-1) + pageData.subdata(in: range).hexadecimalString) - idx = top - } - - let page = try HistoryPage(pageData: pageData, pumpModel: pumpModel) - - let (timeStampedEvents, hasMoreEvents, _) = convertPumpEventToTimestampedEvents(pumpEvents: page.events.reversed(), startDate: startDate, pumpModel: pumpModel) - - events = timeStampedEvents + events - - if !hasMoreEvents { - break - } - } - return (events, pumpModel) - } - - /// Converts PumpEvents after startDate to TimestampedHistoryEvents - /// hasMoreEvents indicates the caller should continue getting more PumpEvents from the device - /// - Parameters: - /// - pumpEvents: array of pump events ordered from newest to oldest (reversed from normal page order) - /// - startDate: return events from past this date (adjusted for TimestampDeltaAllowance) - /// - pumpModel: pumpModel - /// - Returns: tuple of Timestamped History Events and a Bool indicating if more events can be converted - internal func convertPumpEventToTimestampedEvents(pumpEvents: [PumpEvent], startDate: Date, pumpModel: PumpModel) -> (events: [TimestampedHistoryEvent], hasMoreEvents: Bool, cancelledEarly: Bool) { - - // Start with some time in the future, to account for the condition when the pump's clock is ahead - // of ours by a small amount. - var timeCursor = Date(timeIntervalSinceNow: TimeInterval(minutes: 60)) - var events = [TimestampedHistoryEvent]() - var timeAdjustmentInterval: TimeInterval = 0 - var seenEventData = Set() - var lastEvent: PumpEvent? - - for event in pumpEvents { - if let event = event as? TimestampedPumpEvent, !seenEventData.contains(event.rawData) { - seenEventData.insert(event.rawData) - - var timestamp = event.timestamp - timestamp.timeZone = pump.timeZone - - if let date = timestamp.date?.addingTimeInterval(timeAdjustmentInterval) { - - let shouldCheckDateForCompletion = !event.isDelayedAppend(with: pumpModel) - - if shouldCheckDateForCompletion { - if date <= startDate { - // Success, we have all the events we need - //NSLog("Found event at or before startDate(%@)", date as NSDate, String(describing: eventTimestampDeltaAllowance), startDate as NSDate) - return (events: events, hasMoreEvents: false, cancelledEarly: false) - } else if date.timeIntervalSince(timeCursor) > TimeInterval(minutes: 60) { - // Appears that pump lost time; we can't build up a valid timeline from this point back. - NSLog("Found event (%@) out of order in history. Ending history fetch.", date as NSDate) - return (events: events, hasMoreEvents: false, cancelledEarly: true) - } - - timeCursor = date - } - - events.insert(TimestampedHistoryEvent(pumpEvent: event, date: date), at: 0) - - } - } - - if let changeTimeEvent = event as? ChangeTimePumpEvent, let newTimeEvent = lastEvent as? NewTimePumpEvent { - timeAdjustmentInterval += (newTimeEvent.timestamp.date?.timeIntervalSince(changeTimeEvent.timestamp.date!))! - } - - lastEvent = event - } - - return (events: events, hasMoreEvents: true, cancelledEarly: false) - } - - private func getHistoryPage(_ pageNum: Int) throws -> Data { - var frameData = Data() - - let msg = makePumpMessage(to: .getHistoryPage, using: GetHistoryPageCarelinkMessageBody(pageNum: pageNum)) - - let firstResponse = try runCommandWithArguments(msg, responseMessageType: .getHistoryPage) - - var expectedFrameNum = 1 - var curResp = firstResponse.messageBody as! GetHistoryPageCarelinkMessageBody - - while(expectedFrameNum == curResp.frameNumber) { - frameData.append(curResp.frame) - expectedFrameNum += 1 - let msg = makePumpMessage(to: .pumpAck) - if !curResp.lastFrame { - guard let resp = try? communication.sendAndListen(msg) else { - throw PumpCommsError.rfCommsFailure("Did not receive frame data from pump") - } - guard resp.packetType == .carelink && resp.messageType == .getHistoryPage else { - throw PumpCommsError.rfCommsFailure("Bad packet type or message type. Possible interference.") - } - curResp = resp.messageBody as! GetHistoryPageCarelinkMessageBody - } else { - let cmd = SendPacketCmd() - cmd.packet = RFPacket(data: msg.txData) - session.doCmd(cmd, withTimeoutMs: expectedMaxBLELatencyMS) - break - } - } - - guard frameData.count == 1024 else { - throw PumpCommsError.rfCommsFailure("Short history page: \(frameData.count) bytes. Expected 1024") - } - return frameData as Data - } - - internal func logGlucoseHistory(pageData: Data, pageNum: Int) { - var idx = 0 - let chunkSize = 256 - while idx < pageData.count { - let top = min(idx + chunkSize, pageData.count) - let range = Range(uncheckedBounds: (lower: idx, upper: top)) - NSLog(String(format: "GlucosePage %02d - (bytes %03d-%03d): ", pageNum, idx, top-1) + pageData.subdata(in: range).hexadecimalString) - idx = top - } - } - - internal func getGlucoseHistoryEvents(since startDate: Date) throws -> [TimestampedGlucoseEvent] { - try wakeup() - - var events = [TimestampedGlucoseEvent]() - - let currentGlucosePage = try readCurrentGlucosePage() - let startPage = Int(currentGlucosePage.pageNum) - //max lookback of 15 pages or when page is 0 - let endPage = max(startPage - 15, 0) - - pages: for pageNum in stride(from: startPage, to: endPage - 1, by: -1) { - NSLog("Fetching page %d", pageNum) - var pageData: Data - var page: GlucosePage - - do { - pageData = try getGlucosePage(UInt32(pageNum)) - logGlucoseHistory(pageData: pageData, pageNum: pageNum) - page = try GlucosePage(pageData: pageData) - - if page.needsTimestamp && pageNum == startPage { - NSLog(String(format: "GlucosePage %02d needs a new sensor timestamp, writing...", pageNum)) - let _ = try writeGlucoseHistoryTimestamp() - - //fetch page again with new sensor timestamp - pageData = try getGlucosePage(UInt32(pageNum)) - logGlucoseHistory(pageData: pageData, pageNum: pageNum) - page = try GlucosePage(pageData: pageData) - } - - } catch let error as PumpCommsError { - if case .unexpectedResponse(let response, from: _) = error, response.messageType == .errorResponse { - break pages - } else { - throw error - } - } - - for event in page.events.reversed() { - var timestamp = event.timestamp - timestamp.timeZone = pump.timeZone - - if event is UnknownGlucoseEvent { - continue pages - } - - if let date = timestamp.date { - if date < startDate && event is SensorTimestampGlucoseEvent { - NSLog("Found reference event at (%@) to be before startDate(%@)", date as NSDate, startDate as NSDate) - break pages - } else { - events.insert(TimestampedGlucoseEvent(glucoseEvent: event, date: date), at: 0) - } - } - } - } - return events - } - - private func readCurrentGlucosePage() throws -> ReadCurrentGlucosePageMessageBody { - let readCurrentGlucosePageResponse: ReadCurrentGlucosePageMessageBody = try messageBody(to: .readCurrentGlucosePage) - - return readCurrentGlucosePageResponse - } - - private func getGlucosePage(_ pageNum: UInt32) throws -> Data { - var frameData = Data() - - let msg = makePumpMessage(to: .getGlucosePage, using: GetGlucosePageMessageBody(pageNum: pageNum)) - - let firstResponse = try runCommandWithArguments(msg, responseMessageType: .getGlucosePage) - - var expectedFrameNum = 1 - var curResp = firstResponse.messageBody as! GetGlucosePageMessageBody - - while(expectedFrameNum == curResp.frameNumber) { - frameData.append(curResp.frame) - expectedFrameNum += 1 - let msg = makePumpMessage(to: .pumpAck) - if !curResp.lastFrame { - guard let resp = try? communication.sendAndListen(msg) else { - throw PumpCommsError.rfCommsFailure("Did not receive frame data from pump") - } - guard resp.packetType == .carelink && resp.messageType == .getGlucosePage else { - throw PumpCommsError.rfCommsFailure("Bad packet type or message type. Possible interference.") - } - curResp = resp.messageBody as! GetGlucosePageMessageBody - } else { - let cmd = SendPacketCmd() - cmd.packet = RFPacket(data: msg.txData) - session.doCmd(cmd, withTimeoutMs: expectedMaxBLELatencyMS) - break - } - } - - guard frameData.count == 1024 else { - throw PumpCommsError.rfCommsFailure("Short glucose history page: \(frameData.count) bytes. Expected 1024") - } - return frameData as Data - } - - internal func writeGlucoseHistoryTimestamp() throws -> Void { - let shortWriteTimestamp = makePumpMessage(to: .writeGlucoseHistoryTimestamp) - let shortResponse = try communication.sendAndListen(shortWriteTimestamp, timeoutMS: 12000) - - if shortResponse.messageType == .pumpAck { - return - } else { - throw PumpCommsError.unexpectedResponse(shortResponse, from: shortWriteTimestamp) - } - } - - internal func readPumpStatus() throws -> PumpStatus { - let clockResp: ReadTimeCarelinkMessageBody = try messageBody(to: .readTime) - - let pumpModel = try getPumpModel() - - let resResp: ReadRemainingInsulinMessageBody = try messageBody(to: .readRemainingInsulin) - - let reservoir = resResp.getUnitsRemainingForStrokes(pumpModel.strokesPerUnit) - - let battResp: GetBatteryCarelinkMessageBody = try messageBody(to: .getBattery) - - let statusResp: ReadPumpStatusMessageBody = try messageBody(to: .readPumpStatus) - - return PumpStatus(clock: clockResp.dateComponents, batteryVolts: battResp.volts, batteryStatus: battResp.status, suspended: statusResp.suspended, bolusing: statusResp.bolusing, reservoir: reservoir, model: pumpModel, pumpID: pump.pumpID) - - } - - internal func setNormalBolus(units: Double, cancelExistingTemp: Bool) -> SetBolusError? { - do { - let pumpModel = try getPumpModel() - - let statusResp: ReadPumpStatusMessageBody = try messageBody(to: .readPumpStatus) - - if statusResp.bolusing { - throw PumpCommsError.bolusInProgress - } - - if statusResp.suspended { - throw PumpCommsError.pumpSuspended - } - - if cancelExistingTemp { - do { - _ = try setTempBasal(0, duration: TimeInterval(0)) - } catch let error as PumpCommandError { - switch error { - case .command(let error): - return .certain(error) - case .arguments(let error): - return .certain(error) - } - } - } - - let message = PumpMessage(packetType: .carelink, address: pump.pumpID, messageType: .bolus, messageBody: BolusCarelinkMessageBody(units: units, strokesPerUnit: pumpModel.strokesPerUnit)) - - let expectedResponseType: MessageType - - if pumpModel.returnsErrorOnBolus { - expectedResponseType = .errorResponse - } else { - expectedResponseType = .pumpAck - } - - let cmdResponse = try runCommandWithArguments(message, responseMessageType: expectedResponseType) - - if let errorMsg = cmdResponse.messageBody as? PumpErrorMessageBody { - switch errorMsg.errorCode { - case .known(let errorCode): - if !pumpModel.returnsErrorOnBolus || errorCode != .bolusInProgress { - throw PumpCommsError.pumpError(errorCode) - } - case .unknown(let unknownErrorCode): - throw PumpCommsError.unknownPumpErrorCode(unknownErrorCode) - } - } - - } catch let error as PumpCommsError { - return .certain(error) - } catch let error as PumpCommandError { - switch error { - case .command(let error): - return .certain(error) - case .arguments(let error): - return .uncertain(error) - } - } catch { - assertionFailure() - } - return nil - } -} - -public struct PumpStatus { - public let clock: DateComponents - public let batteryVolts: Double - public let batteryStatus: BatteryStatus - public let suspended: Bool - public let bolusing: Bool - public let reservoir: Double - public let model: PumpModel - public let pumpID: String -} - -public struct FrequencyTrial { - public var tries: Int = 0 - public var successes: Int = 0 - public var avgRSSI: Double = -99 - public var frequencyMHz: Double = 0 -} - -public struct FrequencyScanResults { - public var trials = [FrequencyTrial]() - public var bestFrequency: Double = 0 -} - -extension Notification.Name { - public static let PumpOpsSynchronousDidReceivePacket = NSNotification.Name(rawValue: "com.rileylink.RileyLinkKit.PumpOpsSynchronousDidReceivePacket") -} diff --git a/RileyLinkKit/PumpSettings.swift b/RileyLinkKit/PumpSettings.swift new file mode 100644 index 000000000..024cef51e --- /dev/null +++ b/RileyLinkKit/PumpSettings.swift @@ -0,0 +1,62 @@ +// +// PumpSettings.swift +// RileyLinkKit +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import MinimedKit + + +public struct PumpSettings: RawRepresentable { + public typealias RawValue = [String: Any] + + public var pumpID: String + + public var pumpRegion: PumpRegion = .northAmerica + + public init?(rawValue: RawValue) { + guard let pumpID = rawValue["pumpID"] as? String else { + return nil + } + + self.pumpID = pumpID + + if let pumpRegionRaw = rawValue["pumpRegion"] as? PumpRegion.RawValue, + let pumpRegion = PumpRegion(rawValue: pumpRegionRaw) { + self.pumpRegion = pumpRegion + } + } + + public init(pumpID: String, pumpRegion: PumpRegion? = nil) { + self.pumpID = pumpID + + if let pumpRegion = pumpRegion { + self.pumpRegion = pumpRegion + } + } + + public var rawValue: RawValue { + return [ + "pumpID": pumpID, + "pumpRegion": pumpRegion.rawValue + ] + } +} + + +extension PumpSettings: CustomDebugStringConvertible { + public var debugDescription: String { + return [ + "## PumpSettings", + "pumpID: ✔︎", + "pumpRegion: \(pumpRegion)", + ].joined(separator: "\n") + } +} + +extension PumpSettings: Equatable { + public static func ==(lhs: PumpSettings, rhs: PumpSettings) -> Bool { + return lhs.pumpID == rhs.pumpID && lhs.pumpRegion == rhs.pumpRegion + } +} diff --git a/RileyLinkKit/PumpState.swift b/RileyLinkKit/PumpState.swift index bcf27470d..b2ca14269 100644 --- a/RileyLinkKit/PumpState.swift +++ b/RileyLinkKit/PumpState.swift @@ -10,77 +10,54 @@ import Foundation import MinimedKit -public class PumpState { +public struct PumpState: RawRepresentable { + public typealias RawValue = [String: Any] - /// The key for a string value naming the object property whose value changed - public static let PropertyKey = "com.rileylink.RileyLinkKit.PumpState.PropertyKey" - - /// The key for the previous value of the object property whose value changed. - /// If the value type does not conform to AnyObject, a raw representation will be provided instead. - public static let ValueChangeOldKey = "com.rileylink.RileyLinkKit.PumpState.ValueChangeOldKey" - - public let pumpID: String - - public var timeZone: TimeZone = TimeZone.currentFixed { - didSet { - postChangeNotificationForKey("timeZone", oldValue: oldValue) - } - } + public var timeZone: TimeZone - public var pumpRegion: PumpRegion { - didSet { - postChangeNotificationForKey("pumpRegion", oldValue: oldValue.rawValue) - } - } + public var pumpModel: PumpModel? - public var pumpModel: PumpModel? { - didSet { - postChangeNotificationForKey("pumpModel", oldValue: oldValue?.rawValue) - } - } + public var awakeUntil: Date? - public var lastHistoryDump: Date? { - didSet { - postChangeNotificationForKey("lastHistoryDump", oldValue: oldValue) - } - } - - public var awakeUntil: Date? { - didSet { - postChangeNotificationForKey("awakeUntil", oldValue: awakeUntil) + var isAwake: Bool { + if let awakeUntil = awakeUntil { + return awakeUntil.timeIntervalSinceNow > 0 } + + return false } - public init(pumpID: String, pumpRegion: PumpRegion) { - self.pumpID = pumpID - self.pumpRegion = pumpRegion + var lastWakeAttempt: Date? + + public init() { + self.timeZone = .currentFixed } - - public var isAwake: Bool { - if let awakeUntil = awakeUntil { - return awakeUntil.timeIntervalSinceNow > 0 + + public init?(rawValue: RawValue) { + guard + let timeZoneSeconds = rawValue["timeZone"] as? Int, + let timeZone = TimeZone(secondsFromGMT: timeZoneSeconds) + else { + return nil } - return false + self.timeZone = timeZone + + if let pumpModelNumber = rawValue["pumpModel"] as? PumpModel.RawValue { + pumpModel = PumpModel(rawValue: pumpModelNumber) + } } - public var lastValidFrequency: Double? - - public var lastWakeAttempt: Date? - - private func postChangeNotificationForKey(_ key: String, oldValue: Any?) { - var userInfo: [String: Any] = [ - type(of: self).PropertyKey: key + public var rawValue: RawValue { + var rawValue: RawValue = [ + "timeZone": timeZone.secondsFromGMT(), ] - - if let oldValue = oldValue { - userInfo[type(of: self).ValueChangeOldKey] = oldValue + + if let pumpModel = pumpModel { + rawValue["pumpModel"] = pumpModel.rawValue } - - NotificationCenter.default.post(name: .PumpStateValuesDidChange, - object: self, - userInfo: userInfo - ) + + return rawValue } } @@ -91,18 +68,9 @@ extension PumpState: CustomDebugStringConvertible { return [ "## PumpState", "timeZone: \(timeZone)", - "pumpRegion: \(pumpRegion)", "pumpModel: \(pumpModel?.rawValue ?? "")", - "lastHistoryDump: \(lastHistoryDump ?? .distantPast)", "awakeUntil: \(awakeUntil ?? .distantPast)", - "lastWakeAttempt: \(String(describing: lastWakeAttempt))", + "lastWakeAttempt: \(String(describing: lastWakeAttempt))" ].joined(separator: "\n") } } - - -extension Notification.Name { - /// Posted when values of the properties of the PumpState object have changed. - /// The `userInfo` dictionary contains the following keys: `PropertyKey` and `ValueChangeOldKey` - public static let PumpStateValuesDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkKit.PumpState.ValuesDidChangeNotification") -} diff --git a/RileyLinkKit/RileyLinkDevice.swift b/RileyLinkKit/RileyLinkDevice.swift deleted file mode 100644 index 88344272d..000000000 --- a/RileyLinkKit/RileyLinkDevice.swift +++ /dev/null @@ -1,178 +0,0 @@ -// -// RileyLinkDevice.swift -// Naterade -// -// Created by Nathan Racklyeft on 4/10/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import Foundation -import CoreBluetooth -import MinimedKit -import RileyLinkBLEKit - - -public enum RileyLinkDeviceError: Error { - case configurationError -} - - -public class RileyLinkDevice { - - public static let IdleMessageDataKey = "com.rileylink.RileyLinkKit.RileyLinkDeviceIdleMessageData" - - public internal(set) var pumpState: PumpState? - - public var lastIdle: Date? { - return device.lastIdle - } - - public private(set) var lastTuned: Date? - - public private(set) var radioFrequency: Double? - - public private(set) var pumpRSSI: Int? - - public var firmwareVersion: String? { - var versions = [String]() - if let fwVersion = device.firmwareVersion { - versions.append(fwVersion) - } - if let fwVersion = device.bleFirmwareVersion { - versions.append(fwVersion.replacingOccurrences(of: "RileyLink:", with: "")) - } - if versions.count > 0 { - return versions.joined(separator: " / ") - } else { - return "Unknown" - } - } - - public var deviceURI: String { - return device.deviceURI - } - - public var name: String? { - return device.name - } - - public var RSSI: Int? { - return device.rssi?.intValue - } - - public var peripheral: CBPeripheral { - return device.peripheral - } - - internal init(bleDevice: RileyLinkBLEDevice, pumpState: PumpState?) { - self.device = bleDevice - self.pumpState = pumpState - - NotificationCenter.default.addObserver(self, selector: #selector(receivedDeviceNotification(_:)), name: nil, object: bleDevice) - - NotificationCenter.default.addObserver(self, selector: #selector(receivedPacketNotification(_:)), name: .PumpOpsSynchronousDidReceivePacket, object: nil) - - } - - // MARK: - Device commands - - public func assertIdleListening(force: Bool = false) { - device.assertIdleListeningForcingRestart(force) - } - - public func syncPumpTime(_ resultHandler: @escaping (Error?) -> Void) { - if let ops = ops { - ops.setTime({ () -> DateComponents in - let calendar = Calendar(identifier: Calendar.Identifier.gregorian) - return calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: Date()) - }, - completion: { (error) in - if error == nil { - ops.pumpState.timeZone = TimeZone.currentFixed - } - - resultHandler(error) - } - ) - } else { - resultHandler(RileyLinkDeviceError.configurationError) - } - } - - public func tunePump(_ resultHandler: @escaping (Either) -> Void) { - if let ops = ops { - ops.tuneRadio(for: ops.pumpState.pumpRegion) { (result) in - self.lastTuned = Date() - switch result { - case .success(let scanResults): - self.radioFrequency = scanResults.bestFrequency - case .failure: - break - } - - resultHandler(result) - } - } else { - resultHandler(.failure(RileyLinkDeviceError.configurationError)) - } - } - - public func setCustomName(_ name: String) { - device.setCustomName(name) - } - - public var ops: PumpOps? { - if let pumpState = pumpState { - return PumpOps(pumpState: pumpState, device: device) - } else { - return nil - } - } - - // MARK: - - - internal var device: RileyLinkBLEDevice - - @objc private func receivedDeviceNotification(_ note: Notification) { - switch note.name.rawValue { - case RILEYLINK_IDLE_RESPONSE_RECEIVED: - if let packet = note.userInfo?["packet"] as? RFPacket, let pumpID = pumpState?.pumpID, let data = packet.data, let message = PumpMessage(rxData: data), message.address.hexadecimalString == pumpID { - NotificationCenter.default.post(name: .RileyLinkDeviceDidReceiveIdleMessage, object: self, userInfo: [type(of: self).IdleMessageDataKey: data]) - pumpRSSI = Int(packet.rssi) - } - case RILEYLINK_EVENT_DEVICE_TIMER_TICK: - NotificationCenter.default.post(name: .RileyLinkDeviceDidUpdateTimerTick, object: self) - default: - break - } - } - - @objc private func receivedPacketNotification(_ note: Notification) { - if let packet = note.userInfo?[PumpOpsSynchronous.PacketKey] as? RFPacket { - pumpRSSI = Int(packet.rssi) - } - } -} - - -extension RileyLinkDevice: CustomDebugStringConvertible { - public var debugDescription: String { - return [ - "## RileyLinkDevice", - "name: \(name ?? "")", - "RSSI: \(RSSI ?? 0)", - "lastIdle: \(lastIdle ?? .distantPast)", - "lastTuned: \(lastTuned ?? .distantPast)", - "radioFrequency: \(radioFrequency ?? 0)", - "firmwareVersion: \(firmwareVersion ?? "")", - "state: \(peripheral.state.description)" - ].joined(separator: "\n") - } -} - - -extension Notification.Name { - public static let RileyLinkDeviceDidReceiveIdleMessage = NSNotification.Name(rawValue: "com.rileylink.RileyLinkKit.RileyLinkDeviceDidReceiveIdleMessageNotification") - - public static let RileyLinkDeviceDidUpdateTimerTick = NSNotification.Name(rawValue: "com.rileylink.RileyLinkKit.RileyLinkDeviceDidUpdateTimerTickNotification") -} diff --git a/RileyLinkKit/RileyLinkDeviceManager.swift b/RileyLinkKit/RileyLinkDeviceManager.swift deleted file mode 100644 index 80ccb78a2..000000000 --- a/RileyLinkKit/RileyLinkDeviceManager.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// RileyLinkDeviceManager.swift -// RileyLink -// -// Created by Nathan Racklyeft on 4/10/16. -// Copyright © 2016 Pete Schwamb. All rights reserved. -// - -import Foundation -import RileyLinkBLEKit - - -public class RileyLinkDeviceManager { - - public static let RileyLinkDeviceKey = "com.rileylink.RileyLinkKit.RileyLinkDevice" - public static let RileyLinkRSSIKey = "com.rileylink.RileyLinkKit.RileyLinkRSSI" - public static let RileyLinkNameKey = "com.rileylink.RileyLinkKit.RileyLinkName" - - public var pumpState: PumpState? { - didSet { - for device in devices { - device.pumpState = pumpState - } - } - } - - public init(pumpState: PumpState?, autoConnectIDs: Set) { - self.pumpState = pumpState - - bleManager = RileyLinkBLEManager(autoConnectIDs: autoConnectIDs) - - NotificationCenter.default.addObserver(self, selector: #selector(discoveredBLEDevice(_:)), name: NSNotification.Name(rawValue: RILEYLINK_EVENT_DEVICE_CREATED), object: bleManager) - - NotificationCenter.default.addObserver(self, selector: #selector(connectionStateDidChange(_:)), name: NSNotification.Name(rawValue: RILEYLINK_EVENT_DEVICE_CONNECTED), object: nil) - - NotificationCenter.default.addObserver(self, selector: #selector(connectionStateDidChange(_:)), name: NSNotification.Name(rawValue: RILEYLINK_EVENT_DEVICE_DISCONNECTED), object: nil) - - NotificationCenter.default.addObserver(self, selector: #selector(rssiDidChange(_:)), name: NSNotification.Name(rawValue: RILEYLINK_EVENT_RSSI_CHANGED), object: nil) - - NotificationCenter.default.addObserver(self, selector: #selector(nameDidChange(_:)), name: NSNotification.Name(rawValue: RILEYLINK_EVENT_NAME_CHANGED), object: nil) - } - - public func setDeviceScanningEnabled(_ enabled: Bool) { - bleManager.setScanningEnabled(enabled) - } - - /// Whether to subscribe devices to a timer characteristic changing every ~60s. - /// Provides a reliable, external heartbeat for executing periodic tasks. - public var timerTickEnabled = true { - didSet { - for device in devices { - device.device.timerTickEnabled = timerTickEnabled - } - } - } - - public var idleTimeout = TimeInterval(minutes: 1) { - didSet { - for device in devices { - device.device.idleTimeoutMS = UInt32(idleTimeout.milliseconds) - } - } - } - - /// Whether devices should listen for broadcast packets when not running commands - public var idleListeningEnabled = true { - didSet { - for device in devices { - if idleListeningEnabled { - device.device.enableIdleListening(onChannel: 0) - } else { - device.device.disableIdleListening() - } - } - } - } - - private(set) public var devices: [RileyLinkDevice] = [] - - // When multiple RL's are present, this moves the specified RL to the back of the list - // so a different RL will be selected by firstConnectedDevice() - public func deprioritizeDevice(device: RileyLinkDevice) { - if let index = devices.index(where: { $0.peripheral.identifier == device.peripheral.identifier }) { - devices.remove(at: index) - devices.append(device) - } - } - - public var firstConnectedDevice: RileyLinkDevice? { - if let index = devices.index(where: { $0.peripheral.state == .connected }) { - return devices[index] - } else { - return nil - } - } - - public func connectDevice(_ device: RileyLinkDevice) { - bleManager.connect(device.device) - } - - public func disconnectDevice(_ device: RileyLinkDevice) { - bleManager.disconnectDevice(device.device) - } - - private let bleManager: RileyLinkBLEManager - - // MARK: - RileyLinkBLEManager - - @objc private func discoveredBLEDevice(_ note: Notification) { - if let bleDevice = note.userInfo?["device"] as? RileyLinkBLEDevice { - bleDevice.timerTickEnabled = timerTickEnabled - bleDevice.idleTimeoutMS = UInt32(idleTimeout.milliseconds) - - if idleListeningEnabled { - bleDevice.enableIdleListening(onChannel: 0) - } - - let device = RileyLinkDevice(bleDevice: bleDevice, pumpState: pumpState) - - devices.append(device) - - NotificationCenter.default.post(name: .DeviceManagerDidDiscoverDevice, object: self, userInfo: [type(of: self).RileyLinkDeviceKey: device]) - - } - } - - @objc private func connectionStateDidChange(_ note: Notification) { - if let bleDevice = note.object as? RileyLinkBLEDevice, - let index = devices.index(where: { $0.peripheral.identifier == bleDevice.peripheral.identifier }) { - let device = devices[index] - - NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self, userInfo: [type(of: self).RileyLinkDeviceKey: device]) - } - } - - @objc private func rssiDidChange(_ note: Notification) { - if let bleDevice = note.object as? RileyLinkBLEDevice, - let index = devices.index(where: { $0.peripheral.identifier == bleDevice.peripheral.identifier }) { - let device = devices[index] - - NotificationCenter.default.post(name: .DeviceRSSIDidChange, object: self, userInfo: [type(of: self).RileyLinkDeviceKey: device, type(of: self).RileyLinkRSSIKey: note.userInfo!["RSSI"]!]) - } - } - - @objc private func nameDidChange(_ note: Notification) { - if let bleDevice = note.object as? RileyLinkBLEDevice, - let index = devices.index(where: { $0.peripheral.identifier == bleDevice.peripheral.identifier }) { - let device = devices[index] - - NotificationCenter.default.post(name: .DeviceNameDidChange, object: self, userInfo: [type(of: self).RileyLinkDeviceKey: device, type(of: self).RileyLinkNameKey: note.userInfo!["Name"]!]) - } - } -} - - -extension RileyLinkDeviceManager: CustomDebugStringConvertible { - public var debugDescription: String { - var report = [ - "## RileyLinkDeviceManager", - "timerTickEnabled: \(timerTickEnabled)", - "idleListeningEnabled: \(idleListeningEnabled)", - "idleTimeout: \(idleTimeout)" - ] - - for device in devices { - report.append(String(reflecting: device)) - } - - return report.joined(separator: "\n\n") - } -} - - -extension Notification.Name { - public static let DeviceManagerDidDiscoverDevice = Notification.Name(rawValue: "com.rileylink.RileyLinkKit.DidDiscoverDeviceNotification") - - public static let DeviceConnectionStateDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkKit.ConnectionStateDidChangeNotification") - - public static let DeviceRSSIDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkKit.RSSIDidChangeNotification") - public static let DeviceNameDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkKit.NameDidChangeNotification") -} diff --git a/RileyLinkKit/UI/RileyLinkDeviceTableViewCell.swift b/RileyLinkKit/UI/RileyLinkDeviceTableViewCell.swift deleted file mode 100644 index 4106cd7d2..000000000 --- a/RileyLinkKit/UI/RileyLinkDeviceTableViewCell.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// RileyLinkDeviceTableViewCell.swift -// Naterade -// -// Created by Nathan Racklyeft on 8/29/15. -// Copyright © 2015 Nathan Racklyeft. All rights reserved. -// - -import CoreBluetooth -import UIKit - -public class RileyLinkDeviceTableViewCell: UITableViewCell { - - @IBOutlet public weak var connectSwitch: UISwitch! - - @IBOutlet weak var nameLabel: UILabel! - - @IBOutlet weak var signalLabel: UILabel! - - public static func nib() -> UINib { - return UINib(nibName: className, bundle: Bundle(for: self)) - } - - public func configureCellWithName(_ name: String?, signal: Int?, peripheralState: CBPeripheralState?) { - nameLabel.text = name - signalLabel.text = signal != nil ? "\(signal!) dB" : nil - - if let state = peripheralState { - switch state { - case .connected: - connectSwitch.isOn = true - connectSwitch.isEnabled = true - case .connecting: - connectSwitch.isOn = true - connectSwitch.isEnabled = true - case .disconnected: - connectSwitch.isOn = false - connectSwitch.isEnabled = true - case .disconnecting: - connectSwitch.isOn = false - connectSwitch.isEnabled = false - } - } else { - connectSwitch.isHidden = true - } - - } - - public override func prepareForReuse() { - super.prepareForReuse() - - connectSwitch?.removeTarget(nil, action: nil, for: .valueChanged) - } - -} - - diff --git a/RileyLinkKit/UI/RileyLinkDeviceTableViewCell.xib b/RileyLinkKit/UI/RileyLinkDeviceTableViewCell.xib deleted file mode 100644 index 9dd87b441..000000000 --- a/RileyLinkKit/UI/RileyLinkDeviceTableViewCell.xib +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/RileyLinkKit/UI/RileyLinkDeviceTableViewController.swift b/RileyLinkKit/UI/RileyLinkDeviceTableViewController.swift deleted file mode 100644 index 1eb52b494..000000000 --- a/RileyLinkKit/UI/RileyLinkDeviceTableViewController.swift +++ /dev/null @@ -1,593 +0,0 @@ -// -// RileyLinkDeviceTableViewController.swift -// Naterade -// -// Created by Nathan Racklyeft on 3/5/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import UIKit -import MinimedKit - -let CellIdentifier = "Cell" - -public class RileyLinkDeviceTableViewController: UITableViewController, TextFieldTableViewControllerDelegate { - - public var device: RileyLinkDevice! - - var rssiFetchTimer: Timer? { - willSet { - rssiFetchTimer?.invalidate() - } - } - - private var appeared = false - - convenience init() { - self.init(style: .grouped) - } - - public override func viewDidLoad() { - super.viewDidLoad() - - title = device.name - - self.observe() - } - - @objc func updateRSSI() - { - guard case .connected = device.peripheral.state else { - return - } - device.peripheral.readRSSI() - } - - // References to registered notification center observers - private var notificationObservers: [Any] = [] - - deinit { - for observer in notificationObservers { - NotificationCenter.default.removeObserver(observer) - } - } - - private var deviceObserver: Any? { - willSet { - if let observer = deviceObserver { - NotificationCenter.default.removeObserver(observer) - } - } - } - - private func observe() { - let center = NotificationCenter.default - let mainQueue = OperationQueue.main - - notificationObservers = [ - center.addObserver(forName: .DeviceNameDidChange, object: nil, queue: mainQueue) { [weak self = self] (note) -> Void in - let indexPath = IndexPath(row: DeviceRow.customName.rawValue, section: Section.device.rawValue) - self?.tableView.reloadRows(at: [indexPath], with: .none) - self?.title = self?.device.name - }, - center.addObserver(forName: .DeviceConnectionStateDidChange, object: nil, queue: mainQueue) { [weak self = self] (note) -> Void in - let indexPath = IndexPath(row: DeviceRow.connection.rawValue, section: Section.device.rawValue) - self?.tableView.reloadRows(at: [indexPath], with: .none) - }, - center.addObserver(forName: .DeviceRSSIDidChange, object: nil, queue: mainQueue) { [weak self = self] (note) -> Void in - let indexPath = IndexPath(row: DeviceRow.rssi.rawValue, section: Section.device.rawValue) - self?.tableView.reloadRows(at: [indexPath], with: .none) - } - ] - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - if appeared { - tableView.reloadData() - } - - rssiFetchTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(updateRSSI), userInfo: nil, repeats: true) - - appeared = true - } - - public override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - rssiFetchTimer = nil - } - - - // MARK: - Formatters - - private lazy var dateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - - dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .medium - - return dateFormatter - }() - - private lazy var decimalFormatter: NumberFormatter = { - let decimalFormatter = NumberFormatter() - - decimalFormatter.numberStyle = .decimal - decimalFormatter.minimumSignificantDigits = 5 - - return decimalFormatter - }() - - private lazy var successText = NSLocalizedString("Succeeded", comment: "A message indicating a command succeeded") - - // MARK: - Table view data source - - private enum Section: Int, CaseCountable { - case device - case pump - case commands - } - - private enum DeviceRow: Int, CaseCountable { - case customName - case version - case rssi - case connection - case idleStatus - } - - private enum PumpRow: Int, CaseCountable { - case id - case model - case awake - } - - private enum CommandRow: Int, CaseCountable { - case tune - case changeTime - case mySentryPair - case dumpHistory - case fetchGlucose - case writeGlucoseHistoryTimestamp - case getPumpModel - case pressDownButton - case readPumpStatus - case readBasalSchedule - } - - public override func numberOfSections(in tableView: UITableView) -> Int { - if device.pumpState == nil { - return Section.count - 1 - } else { - return Section.count - } - } - - public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch Section(rawValue: section)! { - case .device: - return DeviceRow.count - case .pump: - return PumpRow.count - case .commands: - return CommandRow.count - } - } - - public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: UITableViewCell - - if let reusableCell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier) { - cell = reusableCell - } else { - cell = UITableViewCell(style: .value1, reuseIdentifier: CellIdentifier) - } - - cell.accessoryType = .none - - switch Section(rawValue: indexPath.section)! { - case .device: - switch DeviceRow(rawValue: indexPath.row)! { - case .customName: - cell.textLabel?.text = NSLocalizedString("Name", comment: "The title of the cell showing device name") - cell.detailTextLabel?.text = device.name - cell.accessoryType = .disclosureIndicator - case .version: - cell.textLabel?.text = NSLocalizedString("Firmware", comment: "The title of the cell showing firmware version") - cell.detailTextLabel?.text = device.firmwareVersion - case .connection: - cell.textLabel?.text = NSLocalizedString("Connection State", comment: "The title of the cell showing BLE connection state") - cell.detailTextLabel?.text = device.peripheral.state.description - case .rssi: - cell.textLabel?.text = NSLocalizedString("Signal Strength", comment: "The title of the cell showing BLE signal strength (RSSI)") - if let RSSI = device.RSSI { - cell.detailTextLabel?.text = "\(RSSI) dB" - } else { - cell.detailTextLabel?.text = "–" - } - case .idleStatus: - cell.textLabel?.text = NSLocalizedString("On Idle", comment: "The title of the cell showing the last idle") - - if let idleDate = device.lastIdle { - cell.detailTextLabel?.text = dateFormatter.string(from: idleDate as Date) - } else { - cell.detailTextLabel?.text = "–" - } - } - case .pump: - switch PumpRow(rawValue: indexPath.row)! { - case .id: - cell.textLabel?.text = NSLocalizedString("Pump ID", comment: "The title of the cell showing pump ID") - if let pumpID = device.pumpState?.pumpID { - cell.detailTextLabel?.text = pumpID - } else { - cell.detailTextLabel?.text = "–" - } - case .model: - cell.textLabel?.text = NSLocalizedString("Pump Model", comment: "The title of the cell showing the pump model number") - if let pumpModel = device.pumpState?.pumpModel { - cell.detailTextLabel?.text = String(describing: pumpModel) - } else { - cell.detailTextLabel?.text = NSLocalizedString("Unknown", comment: "The detail text for an unknown pump model") - } - case .awake: - switch device.pumpState?.awakeUntil { - case let until? where until.timeIntervalSinceNow < 0: - cell.textLabel?.text = NSLocalizedString("Last Awake", comment: "The title of the cell describing an awake radio") - cell.detailTextLabel?.text = dateFormatter.string(from: until as Date) - case let until?: - cell.textLabel?.text = NSLocalizedString("Awake Until", comment: "The title of the cell describing an awake radio") - cell.detailTextLabel?.text = dateFormatter.string(from: until as Date) - default: - cell.textLabel?.text = NSLocalizedString("Listening Off", comment: "The title of the cell describing no radio awake data") - cell.detailTextLabel?.text = nil - } - } - case .commands: - cell.accessoryType = .disclosureIndicator - cell.detailTextLabel?.text = nil - - switch CommandRow(rawValue: indexPath.row)! { - case .tune: - switch (device.radioFrequency, device.lastTuned) { - case (let frequency?, let date?): - cell.textLabel?.text = "\(decimalFormatter.string(from: NSNumber(value: frequency))!) MHz" - cell.detailTextLabel?.text = dateFormatter.string(from: date as Date) - default: - cell.textLabel?.text = NSLocalizedString("Tune Radio Frequency", comment: "The title of the command to re-tune the radio") - } - - case .changeTime: - cell.textLabel?.text = NSLocalizedString("Change Time", comment: "The title of the command to change pump time") - - let localTimeZone = TimeZone.current - let localTimeZoneName = localTimeZone.abbreviation() ?? localTimeZone.identifier - - if let pumpTimeZone = device.pumpState?.timeZone { - let timeZoneDiff = TimeInterval(pumpTimeZone.secondsFromGMT() - localTimeZone.secondsFromGMT()) - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.hour, .minute] - let diffString = timeZoneDiff != 0 ? formatter.string(from: abs(timeZoneDiff)) ?? String(abs(timeZoneDiff)) : "" - - cell.detailTextLabel?.text = String(format: NSLocalizedString("%1$@%2$@%3$@", comment: "The format string for displaying an offset from a time zone: (1: GMT)(2: -)(3: 4:00)"), localTimeZoneName, timeZoneDiff != 0 ? (timeZoneDiff < 0 ? "-" : "+") : "", diffString) - } else { - cell.detailTextLabel?.text = localTimeZoneName - } - case .mySentryPair: - cell.textLabel?.text = NSLocalizedString("MySentry Pair", comment: "The title of the command to pair with mysentry") - - case .dumpHistory: - cell.textLabel?.text = NSLocalizedString("Fetch Recent History", comment: "The title of the command to fetch recent history") - - case .fetchGlucose: - cell.textLabel?.text = NSLocalizedString("Fetch Recent Glucose", comment: "The title of the command to fetch recent glucose") - - case .writeGlucoseHistoryTimestamp: - cell.textLabel?.text = NSLocalizedString("Write Glucose History Timestamp", comment: "The title of the command to write a glucose history timestamp") - - case .getPumpModel: - cell.textLabel?.text = NSLocalizedString("Get Pump Model", comment: "The title of the command to get pump model") - - case .pressDownButton: - cell.textLabel?.text = NSLocalizedString("Send Button Press", comment: "The title of the command to send a button press") - - case .readPumpStatus: - cell.textLabel?.text = NSLocalizedString("Read Pump Status", comment: "The title of the command to read pump status") - - case .readBasalSchedule: - cell.textLabel?.text = NSLocalizedString("Read Basal Schedule", comment: "The title of the command to read basal schedule") -} - } - - return cell - } - - public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - switch Section(rawValue: section)! { - case .device: - return NSLocalizedString("Device", comment: "The title of the section describing the device") - case .pump: - return NSLocalizedString("Pump", comment: "The title of the section describing the pump") - case .commands: - return NSLocalizedString("Commands", comment: "The title of the section describing commands") - } - } - - // MARK: - UITableViewDelegate - - public override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { - switch Section(rawValue: indexPath.section)! { - case .device: - switch DeviceRow(rawValue: indexPath.row)! { - case .customName: - return true - default: - return false - } - case .pump: - return false - case .commands: - return device.peripheral.state == .connected - } - } - - public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - switch Section(rawValue: indexPath.section)! { - case .device: - switch DeviceRow(rawValue: indexPath.row)! { - case .customName: - let vc = TextFieldTableViewController() - if let cell = tableView.cellForRow(at: indexPath) { - vc.title = cell.textLabel?.text - vc.value = device.name - vc.delegate = self - vc.keyboardType = .default - } - - show(vc, sender: indexPath) - default: - break - } - case .commands: - let vc: CommandResponseViewController - - switch CommandRow(rawValue: indexPath.row)! { - case .tune: - vc = CommandResponseViewController(command: { [unowned self] (completionHandler) -> String in - self.device.tunePump { (response) -> Void in - switch response { - case .success(let scanResult): - var resultDict: [String: Any] = [:] - - let intFormatter = NumberFormatter() - - let formatString = NSLocalizedString("%1$@ MHz %2$@/%3$@ %4$@", comment: "The format string for displaying a frequency tune trial. Extra spaces added for emphesis: (1: frequency in MHz)(2: success count)(3: total count)(4: average RSSI)") - - resultDict[NSLocalizedString("Best Frequency", comment: "The label indicating the best radio frequency")] = self.decimalFormatter.string(from: NSNumber(value: scanResult.bestFrequency))! - resultDict[NSLocalizedString("Trials", comment: "The label indicating the results of each frequency trial")] = scanResult.trials.map({ (trial) -> String in - - return String(format: formatString, - self.decimalFormatter.string(from: NSNumber(value: trial.frequencyMHz))!, - intFormatter.string(from: NSNumber(value: trial.successes))!, - intFormatter.string(from: NSNumber(value: trial.tries))!, - intFormatter.string(from: NSNumber(value: trial.avgRSSI))! - ) - }) - - var responseText: String - - if let data = try? JSONSerialization.data(withJSONObject: resultDict, options: .prettyPrinted), let string = String(data: data, encoding: String.Encoding.utf8) { - responseText = string - } else { - responseText = NSLocalizedString("No response", comment: "Message display when no response from tuning pump") - } - - completionHandler(responseText) - case .failure(let error): - completionHandler(String(describing: error)) - } - } - - return NSLocalizedString("Tuning radio…", comment: "Progress message for tuning radio") - }) - case .changeTime: - vc = CommandResponseViewController { [unowned self] (completionHandler) -> String in - self.device.syncPumpTime { (error) -> Void in - DispatchQueue.main.async { - if let error = error { - completionHandler(String(describing: error)) - } else { - completionHandler(self.successText) - } - } - } - - return NSLocalizedString("Changing time…", comment: "Progress message for changing pump time.") - } - case .mySentryPair: - vc = CommandResponseViewController { [unowned self] (completionHandler) -> String in - - self.device.ops?.setRXFilterMode(.wide) { (error) in - if let error = error { - completionHandler(String(format: NSLocalizedString("Error setting filter bandwidth: %@", comment: "The error displayed during MySentry pairing when the RX filter could not be set"), String(describing: error))) - } else { - var byteArray = [UInt8](repeating: 0, count: 16) - (self.device.peripheral.identifier as NSUUID).getBytes(&byteArray) - let watchdogID = Data(bytes: byteArray[0..<3]) - - self.device.ops?.changeWatchdogMarriageProfile(toWatchdogID: watchdogID, completion: { (error) in - DispatchQueue.main.async { - if let error = error { - completionHandler(String(describing: error)) - } else { - completionHandler(self.successText) - } - } - }) - } - } - - return NSLocalizedString( - "On your pump, go to the Find Device screen and select \"Find Device\"." + - "\n" + - "\nMain Menu >" + - "\nUtilities >" + - "\nConnect Devices >" + - "\nOther Devices >" + - "\nOn >" + - "\nFind Device", - comment: "Pump find device instruction" - ) - } - case .dumpHistory: - vc = CommandResponseViewController { [unowned self] (completionHandler) -> String in - let calendar = Calendar(identifier: Calendar.Identifier.gregorian) - let oneDayAgo = calendar.date(byAdding: DateComponents(day: -1), to: Date()) - - self.device.ops?.getHistoryEvents(since: oneDayAgo!) { (response) -> Void in - switch response { - case .success(let (events, _)): - var responseText = String(format:"Found %d events since %@", events.count, oneDayAgo! as NSDate) - for event in events { - responseText += String(format:"\nEvent: %@", event.dictionaryRepresentation) - } - completionHandler(responseText) - case .failure(let error): - completionHandler(String(describing: error)) - } - } - return NSLocalizedString("Fetching history…", comment: "Progress message for fetching pump history.") - } - case .fetchGlucose: - vc = CommandResponseViewController { [unowned self] (completionHandler) -> String in - let calendar = Calendar(identifier: Calendar.Identifier.gregorian) - let oneDayAgo = calendar.date(byAdding: DateComponents(day: -1), to: Date()) - self.device.ops?.getGlucoseHistoryEvents(since: oneDayAgo!) { (response) -> Void in - switch response { - case .success(let events): - var responseText = String(format:"Found %d events since %@", events.count, oneDayAgo! as NSDate) - for event in events { - responseText += String(format:"\nEvent: %@", event.dictionaryRepresentation) - } - completionHandler(responseText) - case .failure(let error): - completionHandler(String(describing: error)) - } - } - return NSLocalizedString("Fetching glucose…", comment: "Progress message for fetching pump glucose.") - } - case .writeGlucoseHistoryTimestamp: - vc = CommandResponseViewController { [unowned self] (completionHandler) -> String in - self.device.ops?.writeGlucoseHistoryTimestamp() { (response) -> Void in - switch response { - case .success(_): - completionHandler("Glucose History timestamp was successfully written to pump.") - case .failure(let error): - completionHandler(String(describing: error)) - } - } - return NSLocalizedString("Writing glucose history timestamp…", comment: "Progress message for writing glucose history timestamp.") - } - case .getPumpModel: - vc = CommandResponseViewController { [unowned self] (completionHandler) -> String in - self.device.ops?.getPumpModel({ (response) in - switch response { - case .success(let model): - completionHandler("Pump Model: \(model)") - case .failure(let error): - completionHandler(String(describing: error)) - } - }) - return NSLocalizedString("Fetching pump model…", comment: "Progress message for fetching pump model.") - } - case .pressDownButton: - vc = CommandResponseViewController { [unowned self] (completionHandler) -> String in - self.device.ops?.pressButton({ (response) in - DispatchQueue.main.async { - switch response { - case .success(let msg): - completionHandler("Result: \(msg)") - case .failure(let error): - completionHandler(String(describing: error)) - } - } - }) - return NSLocalizedString("Sending button press…", comment: "Progress message for sending button press to pump.") - } - case .readPumpStatus: - vc = CommandResponseViewController { - [unowned self] (completionHandler) -> String in - self.device.ops?.readPumpStatus { (result) in - DispatchQueue.main.async { - switch result { - case .success(let status): - var str = String(format: NSLocalizedString("%1$@ Units of insulin remaining\n", comment: "The format string describing units of insulin remaining: (1: number of units)"), self.decimalFormatter.string(from: NSNumber(value: status.reservoir))!) - str += String(format: NSLocalizedString("Battery: %1$@ volts\n", comment: "The format string describing pump battery voltage: (1: battery voltage)"), self.decimalFormatter.string(from: NSNumber(value: status.batteryVolts))!) - str += String(format: NSLocalizedString("Suspended: %1$@\n", comment: "The format string describing pump suspended state: (1: suspended)"), String(describing: status.suspended)) - str += String(format: NSLocalizedString("Bolusing: %1$@\n", comment: "The format string describing pump bolusing state: (1: bolusing)"), String(describing: status.bolusing)) - completionHandler(str) - case .failure(let error): - completionHandler(String(describing: error)) - } - } - } - - return NSLocalizedString("Reading pump status…", comment: "Progress message for reading pump status") - } - case .readBasalSchedule: - vc = CommandResponseViewController { - [unowned self] (completionHandler) -> String in - self.device.ops?.getBasalSettings() { (result) in - DispatchQueue.main.async { - switch result { - case .success(let schedule): - var str = String(format: NSLocalizedString("%1$@ basal schedule entries\n", comment: "The format string describing number of basal schedule entries: (1: number of entries)"), self.decimalFormatter.string(from: NSNumber(value: schedule.entries.count))!) - for entry in schedule.entries { - str += "\(String(describing: entry))\n" - } - completionHandler(str) - case .failure(let error): - completionHandler(String(describing: error)) - } - } - } - - return NSLocalizedString("Reading basal schedule…", comment: "Progress message for reading basal schedule") - } - } - - if let cell = tableView.cellForRow(at: indexPath) { - vc.title = cell.textLabel?.text - } - - show(vc, sender: indexPath) - case .pump: - break - } - } - - // MARK: - TextFieldTableViewControllerDelegate - - func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) { - _ = navigationController?.popViewController(animated: true) - } - - func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) { - if let indexPath = tableView.indexPathForSelectedRow { - switch Section(rawValue: indexPath.section)! { - case .device: - switch DeviceRow(rawValue: indexPath.row)! { - case .customName: - device.setCustomName(controller.value!) - default: - break - } - default: - break - - } - } - } - -} diff --git a/RileyLinkKit/es.lproj/InfoPlist.strings b/RileyLinkKit/es.lproj/InfoPlist.strings new file mode 100644 index 000000000..f8e9a2b43 --- /dev/null +++ b/RileyLinkKit/es.lproj/InfoPlist.strings @@ -0,0 +1,3 @@ +/* (No Comment) */ +"CFBundleName" = "$(PRODUCT_NAME)"; + diff --git a/RileyLinkKit/es.lproj/Localizable.strings b/RileyLinkKit/es.lproj/Localizable.strings new file mode 100644 index 000000000..1381fce96 --- /dev/null +++ b/RileyLinkKit/es.lproj/Localizable.strings @@ -0,0 +1,189 @@ +/* The format string for displaying a frequency tune trial. Extra spaces added for emphesis: (1: frequency in MHz)(2: success count)(3: total count)(4: average RSSI) */ +"%1$@ MHz %2$@/%3$@ %4$@" = "%1$@ MHz %2$@/%3$@ %4$@"; + +/* Describes a certain bolus failure (1: size of the bolus in units) */ +"%1$@ U bolus failed" = "%1$@ U Bolo Falló"; + +/* Describes an uncertain bolus failure (1: size of the bolus in units) */ +"%1$@ U bolus may not have succeeded" = "%1$@ U bolo posiblemente fallado"; + +/* The format string describing units of insulin remaining: (1: number of units) */ +"%1$@ Units of insulin remaining\n" = "%1$@ Unidades de insulina restantes"; + +/* The format string describing number of basal schedule entries: (1: number of entries) */ +"%1$@ basal schedule entries\n" = "%1$@ entradas de prefil basal"; + +/* The format string for displaying an offset from a time zone: (1: GMT)(2: -)(3: 4:00) */ +"%1$@%2$@%3$@" = "%1$@%2$@%3$@"; + +/* Communications error for a bolus currently running */ +"A bolus is already in progress." = "Un bolo ya está en progreso..."; + +/* The title of the cell describing an awake radio */ +"Awake Until" = "Despierto hasta"; + +/* The format string describing pump battery voltage: (1: battery voltage) */ +"Battery: %1$@ volts\n" = "Pila: %1$@ voltios"; + +/* The label indicating the best radio frequency */ +"Best Frequency" = "Mejor Frecuencia"; + +/* The format string describing pump bolusing state: (1: bolusing) */ +"Bolusing: %1$@\n" = "Bolo en progreso: %1$@"; + +/* The title of the command to change pump time */ +"Change Time" = "Cambio de Hora"; + +/* Progress message for changing pump time. */ +"Changing time…" = "Cambio de Hora..."; + +/* Recovery instruction for an uncertain bolus failure */ +"Check your pump before retrying." = "Revisar su microinfusadora antes de intentarlo de nuevo"; + +/* The title of the section describing commands */ +"Commands" = "Comandos"; + +/* No comment provided by engineer. */ +"Comms with another pump detected." = "Comunicación con otra microinfusadora detectado."; + +/* The connected state */ +"Connected" = "Conectado"; + +/* The in-progress connecting state */ +"Connecting" = "Conectando"; + +/* The title of the cell showing BLE connection state */ +"Connection State" = "Estado de Conexión"; + +/* The title of the section describing the device */ +"Device" = "Dispositivo"; + +/* The disconnected state */ +"Disconnected" = "Desconectado"; + +/* The in-progress disconnecting state */ +"Disconnecting" = "Desconectando"; + +/* The error displayed during MySentry pairing when the RX filter could not be set */ +"Error setting filter bandwidth: %@" = "Error al establecer el ancho de banda del filtro: %@"; + +/* The title of the command to fetch recent glucose */ +"Fetch Recent Glucose" = "Obtener Glucosa Reciente"; + +/* The title of the command to fetch recent history */ +"Fetch Recent History" = "Obtener Historia Reciente"; + +/* Progress message for fetching pump glucose. */ +"Fetching glucose…" = "Obteniendo glucosa…"; + +/* Progress message for fetching pump history. */ +"Fetching history…" = "Obteniendo historia…"; + +/* Progress message for fetching pump model. */ +"Fetching pump model…" = "Obteniendo modelo de microinfusadora…"; + +/* The title of the cell showing firmware version */ +"Firmware" = "Firmware"; + +/* The title of the command to get pump model */ +"Get Pump Model" = "Obtener Modelo de Microinfusadora"; + +/* Recovery instruction for a certain bolus failure */ +"It is safe to retry." = "Es seguro intentarlo de nuevo."; + +/* The title of the cell describing an awake radio */ +"Last Awake" = "último despierto"; + +/* The title of the cell describing no radio awake data */ +"Listening Off" = "Escuchando apagado"; + +/* The title of the command to pair with mysentry */ +"MySentry Pair" = "Junta de MySentry"; + +/* The title of the cell showing device name */ +"Name" = "Nombre"; + +/* Message display when no response from tuning pump */ +"No response" = "No respuesta"; + +/* The title of the cell showing the last idle */ +"On Idle" = "En Inactivo"; + +/* The title of the section describing the pump */ +"Pump" = "Microinfusadora"; + +/* The title of the cell showing pump ID */ +"Pump ID" = "ID de Microinfusadora"; + +/* The title of the cell showing the pump model number */ +"Pump Model" = "Modelo de Microinfusadora"; + +/* No comment provided by engineer. */ +"Pump did not respond." = "Microinfusadora no respondió."; + +/* The format string description of a Pump Error. (1: The specific error code) */ +"Pump error: %1$@" = "Error de Microinfusadora: %1$@"; + +/* No comment provided by engineer. */ +"Pump is suspended." = "Micorinfusadora está suspendida"; + +/* No comment provided by engineer. */ +"Pump responded unexpectedly." = "Micorinfusadora respondió inesperadamente."; + +/* The title of the command to read basal schedule */ +"Read Basal Schedule" = "Obtener prefil basal"; + +/* The title of the command to read pump status */ +"Read Pump Status" = "Obtener estada de microinfusadora"; + +/* Progress message for reading basal schedule */ +"Reading basal schedule…" = "Obteniendo perfil basal…"; + +/* Progress message for reading pump status */ +"Reading pump status…" = "Obteniendo estada de microinfusadora…"; + +/* No comment provided by engineer. */ +"RileyLink timed out." = "RileyLink agotó el tiempo."; + +/* The title of the command to send a button press */ +"Send Button Press" = "Enviar presion de botón"; + +/* Progress message for sending button press to pump. */ +"Sending button press…" = "Enviando presion de botón…"; + +/* The title of the cell showing BLE signal strength (RSSI) */ +"Signal Strength" = "Intensidad de señal"; + +/* A message indicating a command succeeded */ +"Succeeded" = "éxito"; + +/* The format string describing pump suspended state: (1: suspended) */ +"Suspended: %1$@\n" = "Suspendida: %1$@"; + +/* The label indicating the results of each frequency trial */ +"Trials" = "Pruebas"; + +/* The title of the command to re-tune the radio */ +"Tune Radio Frequency" = "Sintonizar frecuencia de radio"; + +/* Progress message for tuning radio */ +"Tuning radio…" = "Sintonizando frecuencia de radio…"; + +/* The detail text for an unknown pump model */ +"Unknown" = "Desconocido"; + +/* The format string description of an unknown pump error code. (1: The specific error code raw value) */ +"Unknown pump error code: %1$@" = "Codigo desconocido de microinfusadora: %1$@"; + +/* No comment provided by engineer. */ +"Unknown pump model." = "Desconocido modelo de microinfusadora."; + +/* No comment provided by engineer. */ +"Unknown response from pump." = "Respuesta desconocida de microinfusora"; + +/* The title of the command to write a glucose history timestamp */ +"Write Glucose History Timestamp" = "Recordar la hora de glucosa historia"; + +/* Progress message for writing glucose history timestamp. */ +"Writing glucose history timestamp…" = "Recordando la hora de glucosa historia…"; + diff --git a/RileyLinkKit/ru.lproj/InfoPlist.strings b/RileyLinkKit/ru.lproj/InfoPlist.strings new file mode 100644 index 000000000..874e8a453 --- /dev/null +++ b/RileyLinkKit/ru.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* No Localized Strings */ diff --git a/RileyLinkKit/ru.lproj/Localizable.strings b/RileyLinkKit/ru.lproj/Localizable.strings new file mode 100644 index 000000000..22067c302 --- /dev/null +++ b/RileyLinkKit/ru.lproj/Localizable.strings @@ -0,0 +1,189 @@ +/* The format string for displaying a frequency tune trial. Extra spaces added for emphesis: (1: frequency in MHz)(2: success count)(3: total count)(4: average RSSI) */ +"%1$@ MHz %2$@/%3$@ %4$@" = "%1$@ MHz %2$@/%3$@ %4$@"; + +/* Describes a certain bolus failure (1: size of the bolus in units) */ +"%1$@ U bolus failed" = "Болюс %1$@ не состоялся"; + +/* Describes an uncertain bolus failure (1: size of the bolus in units) */ +"%1$@ U bolus may not have succeeded" = "Болюс %1$@ мог не состояться"; + +/* The format string describing units of insulin remaining: (1: number of units) */ +"%1$@ Units of insulin remaining\n" = "Остается %1$@ ед инсулина"; + +/* The format string describing number of basal schedule entries: (1: number of entries) */ +"%1$@ basal schedule entries\n" = "%1$@ записи графика базала"; + +/* The format string for displaying an offset from a time zone: (1: GMT)(2: -)(3: 4:00) */ +"%1$@%2$@%3$@" = "%1$@%2$@%3$@"; + +/* Communications error for a bolus currently running */ +"A bolus is already in progress." = "Болюс уже подается"; + +/* The title of the cell describing an awake radio */ +"Awake Until" = "Рабочее состояние до"; + +/* The format string describing pump battery voltage: (1: battery voltage) */ +"Battery: %1$@ volts\n" = "Батарея: %1$@ вольт"; + +/* The label indicating the best radio frequency */ +"Best Frequency" = "Лучшая частота"; + +/* The format string describing pump bolusing state: (1: bolusing) */ +"Bolusing: %1$@\n" = "Болюс: %1$@"; + +/* The title of the command to change pump time */ +"Change Time" = "Изменить время"; + +/* Progress message for changing pump time. */ +"Changing time…" = "Изменяется время…"; + +/* Recovery instruction for an uncertain bolus failure */ +"Check your pump before retrying." = "Проверьте помпу прежде чем повторить попытку."; + +/* The title of the section describing commands */ +"Commands" = "Команды"; + +/* No comment provided by engineer. */ +"Comms with another pump detected." = "Обнаружена коммуникация с другой помпой"; + +/* The connected state */ +"Connected" = "Соединение установлено"; + +/* The in-progress connecting state */ +"Connecting" = "Соединяется"; + +/* The title of the cell showing BLE connection state */ +"Connection State" = "Состояние соединения"; + +/* The title of the section describing the device */ +"Device" = "устройство"; + +/* The disconnected state */ +"Disconnected" = "Разъединено"; + +/* The in-progress disconnecting state */ +"Disconnecting" = "Разъединяется"; + +/* The error displayed during MySentry pairing when the RX filter could not be set */ +"Error setting filter bandwidth: %@" = "Ошибка при установке частоты фильтра: %@"; + +/* The title of the command to fetch recent glucose */ +"Fetch Recent Glucose" = "Получить недавние значения гликемии"; + +/* The title of the command to fetch recent history */ +"Fetch Recent History" = "Получить логи недавней истории"; + +/* Progress message for fetching pump glucose. */ +"Fetching glucose…" = "Получаю гликемию…"; + +/* Progress message for fetching pump history. */ +"Fetching history…" = "Получаю логи…"; + +/* Progress message for fetching pump model. */ +"Fetching pump model…" = "Получаю модель помпы…"; + +/* The title of the cell showing firmware version */ +"Firmware" = "Прошивка"; + +/* The title of the command to get pump model */ +"Get Pump Model" = "Получить модель помпы"; + +/* Recovery instruction for a certain bolus failure */ +"It is safe to retry." = "Можно повторить без опасений"; + +/* The title of the cell describing an awake radio */ +"Last Awake" = "Недавнее состояние активности"; + +/* The title of the cell describing no radio awake data */ +"Listening Off" = "Получаю данные от"; + +/* The title of the command to pair with mysentry */ +"MySentry Pair" = "Сопряжение с MySentry"; + +/* The title of the cell showing device name */ +"Name" = "Название"; + +/* Message display when no response from tuning pump */ +"No response" = "Нет ответа"; + +/* The title of the cell showing the last idle */ +"On Idle" = "Бездействие"; + +/* The title of the section describing the pump */ +"Pump" = "помпы"; + +/* The title of the cell showing pump ID */ +"Pump ID" = "Инд № помпы"; + +/* The title of the cell showing the pump model number */ +"Pump Model" = "Модель помпы"; + +/* No comment provided by engineer. */ +"Pump did not respond." = "Нет ответа от помпы"; + +/* The format string description of a Pump Error. (1: The specific error code) */ +"Pump error: %1$@" = "Ошибка помпы: %1$@"; + +/* No comment provided by engineer. */ +"Pump is suspended." = "Помпа приостановлена."; + +/* No comment provided by engineer. */ +"Pump responded unexpectedly." = "Неожиданный ответ помпы."; + +/* The title of the command to read basal schedule */ +"Read Basal Schedule" = "Прочитать график базала"; + +/* The title of the command to read pump status */ +"Read Pump Status" = "Прочитать статус помпы"; + +/* Progress message for reading basal schedule */ +"Reading basal schedule…" = "Чтение графика базала…"; + +/* Progress message for reading pump status */ +"Reading pump status…" = "Чтение статуса помпы…"; + +/* No comment provided by engineer. */ +"RileyLink timed out." = "Тайм-аут RileyLink"; + +/* The title of the command to send a button press */ +"Send Button Press" = "Отправить команду нажать кнопку"; + +/* Progress message for sending button press to pump. */ +"Sending button press…" = "Отправляется команда нажать кнопку…"; + +/* The title of the cell showing BLE signal strength (RSSI) */ +"Signal Strength" = "Уровень сигнала"; + +/* A message indicating a command succeeded */ +"Succeeded" = "Успешно"; + +/* The format string describing pump suspended state: (1: suspended) */ +"Suspended: %1$@\n" = "Приостановлено: %1$@"; + +/* The label indicating the results of each frequency trial */ +"Trials" = "Попытки"; + +/* The title of the command to re-tune the radio */ +"Tune Radio Frequency" = "Настроить радиочастоту"; + +/* Progress message for tuning radio */ +"Tuning radio…" = "Настраивается радиочастота…"; + +/* The detail text for an unknown pump model */ +"Unknown" = "Неизвестно"; + +/* The format string description of an unknown pump error code. (1: The specific error code raw value) */ +"Unknown pump error code: %1$@" = "Неизвестный код ошибки помпы: %1$@"; + +/* No comment provided by engineer. */ +"Unknown pump model." = "Неизвестная модель помпы"; + +/* No comment provided by engineer. */ +"Unknown response from pump." = "Неизвестный ответ помпы"; + +/* The title of the command to write a glucose history timestamp */ +"Write Glucose History Timestamp" = "Проставить временной штамп истории гликемии"; + +/* Progress message for writing glucose history timestamp. */ +"Writing glucose history timestamp…" = "Наносится временной штамп истории гликемии"; + diff --git a/RileyLinkKitTests/Info.plist b/RileyLinkKitTests/Info.plist index e4ede6701..2d0a14240 100644 --- a/RileyLinkKitTests/Info.plist +++ b/RileyLinkKitTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.2 + 2.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/RileyLinkKitTests/PumpOpsSynchronousBuildFromFramesTests.swift b/RileyLinkKitTests/PumpOpsSynchronousBuildFromFramesTests.swift index c6da41ce0..f9ca7348f 100644 --- a/RileyLinkKitTests/PumpOpsSynchronousBuildFromFramesTests.swift +++ b/RileyLinkKitTests/PumpOpsSynchronousBuildFromFramesTests.swift @@ -14,13 +14,13 @@ import RileyLinkBLEKit class PumpOpsSynchronousBuildFromFramesTests: XCTestCase { - var sut: PumpOpsSynchronous! + var sut: PumpOpsSession! + var pumpSettings: PumpSettings! var pumpState: PumpState! var pumpID: String! var pumpRegion: PumpRegion! - var rileyLinkCmdSession: RileyLinkCmdSession! var pumpModel: PumpModel! - var pumpOpsCommunicationStub: PumpOpsCommunicationStub! + var messageSenderStub: PumpMessageSenderStub! var timeZone: TimeZone! override func setUp() { @@ -29,25 +29,24 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase { pumpID = "350535" pumpRegion = .worldWide pumpModel = PumpModel.model523 - - rileyLinkCmdSession = RileyLinkCmdSession() - pumpOpsCommunicationStub = PumpOpsCommunicationStub(session: rileyLinkCmdSession) + + messageSenderStub = PumpMessageSenderStub() timeZone = TimeZone(secondsFromGMT: 0) loadSUT() } func loadSUT() { - pumpState = PumpState(pumpID: pumpID, pumpRegion: pumpRegion) + pumpSettings = PumpSettings(pumpID: pumpID, pumpRegion: pumpRegion) + pumpState = PumpState() pumpState.pumpModel = pumpModel pumpState.awakeUntil = Date(timeIntervalSinceNow: 100) // pump is awake - sut = PumpOpsSynchronous(pumpState: pumpState, session: rileyLinkCmdSession) - sut.communication = pumpOpsCommunicationStub + sut = PumpOpsSession(settings: pumpSettings, pumpState: pumpState, session: messageSenderStub, delegate: messageSenderStub) } func testErrorIsntThrown() { - pumpOpsCommunicationStub.responses = buildResponsesDictionary() + messageSenderStub.responses = buildResponsesDictionary() assertNoThrow(try _ = sut.getHistoryEvents(since: Date())) } @@ -55,10 +54,11 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase { func testUnexpectedResponseThrowsError() { var responseDictionary = buildResponsesDictionary() var pumpAckArray = responseDictionary[.getHistoryPage]! - pumpAckArray.insert(sut.makePumpMessage(to: .buttonPress), at: 0) + let message = PumpMessage(settings: pumpSettings, type: .buttonPress) + pumpAckArray.insert(message, at: 0) responseDictionary[.getHistoryPage]! = pumpAckArray - pumpOpsCommunicationStub.responses = responseDictionary + messageSenderStub.responses = responseDictionary // Didn't receive a .pumpAck short reponse so throw an error assertThrows(try _ = sut.getHistoryEvents(since: Date())) @@ -67,17 +67,18 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase { func testUnexpectedPumpAckResponseThrowsError() { var responseDictionary = buildResponsesDictionary() var pumpAckArray = responseDictionary[.getHistoryPage]! - pumpAckArray.insert(sut.makePumpMessage(to: .buttonPress), at: 1) + let message = PumpMessage(settings: pumpSettings, type: .buttonPress) + pumpAckArray.insert(message, at: 1) responseDictionary[.getHistoryPage]! = pumpAckArray - pumpOpsCommunicationStub.responses = responseDictionary + messageSenderStub.responses = responseDictionary // Didn't receive a .getHistoryPage as 2nd response so throw an error assertThrows(try _ = sut.getHistoryEvents(since: Date())) } func test332EventsReturnedUntilOutOrder() { - pumpOpsCommunicationStub.responses = buildResponsesDictionary() + messageSenderStub.responses = buildResponsesDictionary() let date = Date(timeIntervalSince1970: 0) do { @@ -90,7 +91,7 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase { } func testEventsReturnedAfterTime() { - pumpOpsCommunicationStub.responses = buildResponsesDictionary() + messageSenderStub.responses = buildResponsesDictionary() timeZone = TimeZone.current loadSUT() @@ -108,7 +109,7 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase { } func testGMTEventsAreTheSame() { - pumpOpsCommunicationStub.responses = buildResponsesDictionary() + messageSenderStub.responses = buildResponsesDictionary() timeZone = TimeZone(secondsFromGMT:0) loadSUT() @@ -125,7 +126,7 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase { } func testEventsReturnedAreAscendingOrder() { - pumpOpsCommunicationStub.responses = buildResponsesDictionary() + messageSenderStub.responses = buildResponsesDictionary() //02/11/2017 @ 12:00am (UTC) let date = DateComponents(calendar: Calendar.current, timeZone: pumpState.timeZone, year: 2017, month: 2, day: 11, hour: 0, minute: 0, second: 0).date! @@ -160,9 +161,9 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase { let frameThreeMessages = buildPumpMessagesFromFrameArray(fetchPageThreeFrames) let frameFourMessages = buildPumpMessagesFromFrameArray(fetchPageFourFrames) - let pumpAckMessage = sut.makePumpMessage(to: .pumpAck) + let pumpAckMessage = PumpMessage(settings: pumpSettings, type: .pumpAck, body: PumpAckMessageBody(rxData: Data(count: 1))!) - let errorResponseMessage = sut.makePumpMessage(to: .errorResponse) + let errorResponseMessage = PumpMessage(settings: pumpSettings, type: .errorResponse, body: PumpErrorMessageBody(rxData: Data(count: 1))!) var getHistoryPageArray = [pumpAckMessage, frameZeroMessages[0]] getHistoryPageArray.append(contentsOf: [pumpAckMessage, frameOneMessages[0]]) diff --git a/RileyLinkKitTests/PumpOpsSynchronousTests.swift b/RileyLinkKitTests/PumpOpsSynchronousTests.swift index 978d8682b..f7ddd311e 100644 --- a/RileyLinkKitTests/PumpOpsSynchronousTests.swift +++ b/RileyLinkKitTests/PumpOpsSynchronousTests.swift @@ -9,18 +9,18 @@ import XCTest @testable import RileyLinkKit -import MinimedKit -import RileyLinkBLEKit +@testable import MinimedKit +@testable import RileyLinkBLEKit class PumpOpsSynchronousTests: XCTestCase { - var sut: PumpOpsSynchronous! + var sut: PumpOpsSession! + var pumpSettings: PumpSettings! var pumpState: PumpState! var pumpID: String! var pumpRegion: PumpRegion! - var rileyLinkCmdSession: RileyLinkCmdSession! var pumpModel: PumpModel! - var pumpOpsCommunicationStub: PumpOpsCommunicationStub! + var messageSenderStub: PumpMessageSenderStub! let dateComponents2007 = DateComponents(calendar: Calendar.current, year: 2007, month: 1, day: 1) let dateComponents2017 = DateComponents(calendar: Calendar.current, year: 2017, month: 1, day: 1) @@ -45,21 +45,20 @@ class PumpOpsSynchronousTests: XCTestCase { pumpID = "350535" pumpRegion = .worldWide pumpModel = PumpModel.model523 - - rileyLinkCmdSession = RileyLinkCmdSession() - pumpOpsCommunicationStub = PumpOpsCommunicationStub(session: rileyLinkCmdSession) + + messageSenderStub = PumpMessageSenderStub() setUpSUT() } /// Creates the System Under Test. This is needed because our SUT has dependencies injected through the constructor func setUpSUT() { - pumpState = PumpState(pumpID: pumpID, pumpRegion: pumpRegion) + pumpSettings = PumpSettings(pumpID: pumpID, pumpRegion: pumpRegion) + pumpState = PumpState() pumpState.pumpModel = pumpModel pumpState.awakeUntil = Date(timeIntervalSinceNow: 100) // pump is awake - sut = PumpOpsSynchronous(pumpState: pumpState, session: rileyLinkCmdSession) - sut.communication = pumpOpsCommunicationStub + sut = PumpOpsSession(settings: pumpSettings, pumpState: pumpState, session: messageSenderStub, delegate: messageSenderStub) } /// Duplicates logic in setUp with a new PumpModel @@ -71,20 +70,20 @@ class PumpOpsSynchronousTests: XCTestCase { } func testShouldContinueIfTimestampBeforeStartDateNotEncountered() { - let pumpEvents: [PumpEvent] = [createBatteryEvent()] - - let (_, hasMoreEvents, _) = sut.convertPumpEventToTimestampedEvents(pumpEvents: pumpEvents, startDate: Date.distantPast, pumpModel: pumpModel) + let page = HistoryPage(events: [createBatteryEvent()]) + + let (_, hasMoreEvents, _) = page.timestampedEvents(after: .distantPast, timeZone: pumpState.timeZone, model: pumpModel) XCTAssertTrue(hasMoreEvents) } func testShouldFinishIfTimestampBeforeStartDateEncountered() { let batteryEvent = createBatteryEvent() - let pumpEvents: [PumpEvent] = [batteryEvent] + let page = HistoryPage(events: [batteryEvent]) let afterBatteryEventDate = batteryEvent.timestamp.date!.addingTimeInterval(TimeInterval(hours: 10)) - let (_, hasMoreEvents, _) = sut.convertPumpEventToTimestampedEvents(pumpEvents: pumpEvents, startDate: afterBatteryEventDate, pumpModel: pumpModel) + let (_, hasMoreEvents, _) = page.timestampedEvents(after: afterBatteryEventDate, timeZone: pumpState.timeZone, model: pumpModel) XCTAssertFalse(hasMoreEvents) } @@ -92,9 +91,9 @@ class PumpOpsSynchronousTests: XCTestCase { func testEventsAfterStartDateAreReturned() { let batteryEvent2007 = createBatteryEvent(withDateComponent: dateComponents2007) let batteryEvent2017 = createBatteryEvent(withDateComponent: dateComponents2017) - let pumpEvents: [PumpEvent] = [batteryEvent2017, batteryEvent2007] + let page = HistoryPage(events: [batteryEvent2007, batteryEvent2017]) - let (events, _, _) = sut.convertPumpEventToTimestampedEvents(pumpEvents: pumpEvents, startDate: Date.distantPast, pumpModel: pumpModel) + let (events, _, _) = page.timestampedEvents(after: .distantPast, timeZone: pumpState.timeZone, model: pumpModel) XCTAssertEqual(events.count, 2) } @@ -104,9 +103,9 @@ class PumpOpsSynchronousTests: XCTestCase { let batteryEvent2007 = createBatteryEvent(withDateComponent: dateComponents2007) let batteryEvent2017 = createBatteryEvent(withDateComponent: dateComponents2017) - let pumpEvents: [PumpEvent] = [batteryEvent2017, batteryEvent2007] + let page = HistoryPage(events: [batteryEvent2007, batteryEvent2017]) - let (events, hasMoreEvents, cancelled) = sut.convertPumpEventToTimestampedEvents(pumpEvents: pumpEvents, startDate: datePast2007, pumpModel: pumpModel) + let (events, hasMoreEvents, cancelled) = page.timestampedEvents(after: datePast2007, timeZone: pumpState.timeZone, model: pumpModel) assertArray(events, doesntContainPumpEvent: batteryEvent2007) XCTAssertEqual(events.count, 1) @@ -117,9 +116,9 @@ class PumpOpsSynchronousTests: XCTestCase { func testPumpLostTimeCancelsFetchEarly() { let batteryEvent2007 = createBatteryEvent(withDateComponent: dateComponents2007) let batteryEvent2017 = createBatteryEvent(withDateComponent: dateComponents2017) - let pumpEvents: [PumpEvent] = [batteryEvent2007, batteryEvent2017] + let page = HistoryPage(events: [batteryEvent2017, batteryEvent2007]) - let (events, hasMoreEvents, cancelledEarly) = sut.convertPumpEventToTimestampedEvents(pumpEvents: pumpEvents, startDate: Date.distantPast, pumpModel: pumpModel) + let (events, hasMoreEvents, cancelledEarly) = page.timestampedEvents(after: Date.distantPast, timeZone: pumpState.timeZone, model: pumpModel) XCTAssertTrue(cancelledEarly) XCTAssertFalse(hasMoreEvents) @@ -128,8 +127,8 @@ class PumpOpsSynchronousTests: XCTestCase { } func testEventsWithSameDataArentAddedTwice() { - let pumpEvents: [PumpEvent] = [createBolusEvent2009(), createBolusEvent2009()] - let (events, _, _) = sut.convertPumpEventToTimestampedEvents(pumpEvents: pumpEvents, startDate: Date.distantPast, pumpModel: pumpModel) + let page = HistoryPage(events: [createBolusEvent2009(), createBolusEvent2009()]) + let (events, _, _) = page.timestampedEvents(after: Date.distantPast, timeZone: pumpState.timeZone, model: pumpModel) XCTAssertEqual(events.count, 1) } @@ -140,9 +139,9 @@ class PumpOpsSynchronousTests: XCTestCase { // 120 minute duration let squareWaveBolus = BolusNormalPumpEvent(availableData: Data(hexadecimalString: "010080048000240009a24a1510")!, pumpModel: pumpModel)! - let events:[PumpEvent] = [squareWaveBolus] + let page = HistoryPage(events: [squareWaveBolus]) - let (timeStampedEvents, _, _) = sut.convertPumpEventToTimestampedEvents(pumpEvents: events, startDate: Date.distantPast, pumpModel: pumpModel) + let (timeStampedEvents, _, _) = page.timestampedEvents(after: .distantPast, timeZone: pumpState.timeZone, model: pumpModel) // It should be included XCTAssertTrue(array(timeStampedEvents, containsPumpEvent: squareWaveBolus)) @@ -160,8 +159,8 @@ class PumpOpsSynchronousTests: XCTestCase { let dateComponents = tempEventBasal.timestamp.addingTimeInterval(TimeInterval(hours:-4)) let squareBolusEventFourHoursBefore = createSquareBolusEvent(dateComponents: dateComponents) - let events:[PumpEvent] = [squareBolusEventFourHoursBefore, tempEventBasal] - let (timeStampedEvents, hasMoreEvents, cancelled) = sut.convertPumpEventToTimestampedEvents(pumpEvents: events, startDate: Date.distantPast, pumpModel: pumpModel) + let page = HistoryPage(events: [tempEventBasal, squareBolusEventFourHoursBefore]) + let (timeStampedEvents, hasMoreEvents, cancelled) = page.timestampedEvents(after: .distantPast, timeZone: pumpState.timeZone, model: pumpModel) // Debatable (undefined) whether this should be returned. It is tested to avoid inadvertantly changing behavior assertArray(timeStampedEvents, containsPumpEvent: squareBolusEventFourHoursBefore) @@ -174,10 +173,11 @@ class PumpOpsSynchronousTests: XCTestCase { setUpTestWithPumpModel(.model522) let pumpEvent = createSquareBolusEvent2010() + let page = HistoryPage(events: [pumpEvent]) let startDate = pumpEvent.timestamp.date!.addingTimeInterval(TimeInterval(minutes:9)) - let (timestampedEvents, hasMoreEvents, cancelled) = sut.convertPumpEventToTimestampedEvents(pumpEvents: [pumpEvent], startDate: startDate, pumpModel: self.pumpModel) + let (timestampedEvents, hasMoreEvents, cancelled) = page.timestampedEvents(after: startDate, timeZone: pumpState.timeZone, model: pumpModel) assertArray(timestampedEvents, containsPumpEvent: pumpEvent) //We found an event before the start time but we can't verify the timestamp from the Square Bolus so there could be more valid events @@ -196,9 +196,9 @@ class PumpOpsSynchronousTests: XCTestCase { let batteryEvent = createBatteryEvent(withDateComponent: dateComponents2007) let laterBatteryEvent = createBatteryEvent(atTime: after2007Date) - let events = [batteryEvent, laterBatteryEvent] + let page = HistoryPage(events: [laterBatteryEvent, batteryEvent]) - let (_, _, cancelled) = sut.convertPumpEventToTimestampedEvents(pumpEvents: events, startDate: .distantPast, pumpModel: pumpModel) + let (_, _, cancelled) = page.timestampedEvents(after: .distantPast, timeZone: pumpState.timeZone, model: pumpModel) XCTAssertFalse(cancelled) } @@ -311,7 +311,7 @@ class PumpOpsSynchronousTests: XCTestCase { // from comment at https://gist.github.com/szhernovoy/276e69eb90a0de84dd90 func randomDataString(length:Int) -> String { let charSet = "abcdef0123456789" - var c = charSet.characters.map { String($0) } + var c = charSet.map { String($0) } var s:String = "" for _ in 0.. String { return s } -class PumpOpsCommunicationStub : PumpOpsCommunication { - - var responses = [MessageType: [PumpMessage]]() - - // internal tracking of how many times a response type has been received - private var responsesHaveOccured = [MessageType: Int]() - - override func sendAndListen(_ msg: PumpMessage, timeoutMS: UInt32, repeatCount: UInt8 = 0, msBetweenPackets: UInt8 = 0, retryCount: UInt8 = 3) throws -> PumpMessage { - - if let responseArray = responses[msg.messageType] { +class PumpMessageSenderStub: PumpMessageSender { + func sendAndListen(_ data: Data, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> RFPacket? { + guard let decoded = MinimedPacket(encodedData: data), + let messageType = MessageType(rawValue: decoded.data[4]) + else { + throw PumpOpsError.noResponse(during: "Tests") + } + + let response: PumpMessage + + if let responseArray = responses[messageType] { let numberOfResponsesReceived: Int - - if let someValue = responsesHaveOccured[msg.messageType] { + + if let someValue = responsesHaveOccured[messageType] { numberOfResponsesReceived = someValue } else { numberOfResponsesReceived = 0 } - - let nextNumberOfResponsesReceived = numberOfResponsesReceived+1 - responsesHaveOccured[msg.messageType] = nextNumberOfResponsesReceived - + + let nextNumberOfResponsesReceived = numberOfResponsesReceived + 1 + responsesHaveOccured[messageType] = nextNumberOfResponsesReceived + if numberOfResponsesReceived >= responseArray.count { XCTFail() } - - return responseArray[numberOfResponsesReceived] + + response = responseArray[numberOfResponsesReceived] + } else { + response = PumpMessage(rxData: Data())! } - return PumpMessage(rxData: Data())! + + var encoded = MinimedPacket(outgoingData: response.txData).encodedData() + encoded.insert(contentsOf: [0, 0], at: 0) + return RFPacket(rfspyResponse: encoded) + } + + func listen(onChannel channel: Int, timeout: TimeInterval) throws -> RFPacket? { + throw PumpOpsError.noResponse(during: "Tests") + } + + func send(_ data: Data, onChannel channel: Int, timeout: TimeInterval) throws { + // Do nothing + } + + func updateRegister(_ address: CC111XRegister, value: UInt8) throws { + throw PumpOpsError.noResponse(during: "Tests") + } + + func resetRadioConfig() throws { + throw PumpOpsError.noResponse(during: "Tests") + } + + func setBaseFrequency(_ frequency: Measurement) throws { + throw PumpOpsError.noResponse(during: "Tests") + } + + var responses = [MessageType: [PumpMessage]]() + + // internal tracking of how many times a response type has been received + private var responsesHaveOccured = [MessageType: Int]() +} + +extension PumpMessageSenderStub: PumpOpsSessionDelegate { + func pumpOpsSession(_ session: PumpOpsSession, didChange state: PumpState) { + } } diff --git a/RileyLinkKit/Extensions/CBPeripheralState.swift b/RileyLinkKitUI/CBPeripheralState.swift similarity index 100% rename from RileyLinkKit/Extensions/CBPeripheralState.swift rename to RileyLinkKitUI/CBPeripheralState.swift diff --git a/RileyLinkKit/Extensions/CaseCountable.swift b/RileyLinkKitUI/CaseCountable.swift similarity index 75% rename from RileyLinkKit/Extensions/CaseCountable.swift rename to RileyLinkKitUI/CaseCountable.swift index 9e81dcbcc..05dbbe6fd 100644 --- a/RileyLinkKit/Extensions/CaseCountable.swift +++ b/RileyLinkKitUI/CaseCountable.swift @@ -8,9 +8,9 @@ import Foundation -public protocol CaseCountable: RawRepresentable {} +protocol CaseCountable: RawRepresentable {} -public extension CaseCountable where RawValue == Int { +extension CaseCountable where RawValue == Int { static var count: Int { var i: Int = 0 while let new = Self(rawValue: i) { i = new.rawValue.advanced(by: 1) } diff --git a/RileyLinkKitUI/CommandResponseViewController+RileyLinkDevice.swift b/RileyLinkKitUI/CommandResponseViewController+RileyLinkDevice.swift new file mode 100644 index 000000000..e7c895268 --- /dev/null +++ b/RileyLinkKitUI/CommandResponseViewController+RileyLinkDevice.swift @@ -0,0 +1,277 @@ +// +// CommandResponseViewController+RileyLinkDevice.swift +// RileyLinkKitUI +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import RileyLinkBLEKit +import RileyLinkKit +import MinimedKit + + +extension CommandResponseViewController { + typealias T = CommandResponseViewController + + private static let successText = NSLocalizedString("Succeeded", comment: "A message indicating a command succeeded") + + static func changeTime(ops: PumpOps?, device: RileyLinkDevice) -> T { + return T { (completionHandler) -> String in + ops?.runSession(withName: "Set time", using: device) { (session) in + let response: String + do { + try session.setTime { () -> DateComponents in + let calendar = Calendar(identifier: .gregorian) + return calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: Date()) + } + + response = self.successText + } catch let error { + response = String(describing: error) + } + + DispatchQueue.main.async { + completionHandler(response) + } + } + + return NSLocalizedString("Changing time…", comment: "Progress message for changing pump time.") + } + } + + static func dumpHistory(ops: PumpOps?, device: RileyLinkDevice) -> T { + return T { (completionHandler) -> String in + let calendar = Calendar(identifier: Calendar.Identifier.gregorian) + let oneDayAgo = calendar.date(byAdding: DateComponents(day: -1), to: Date()) + + ops?.runSession(withName: "Get history events", using: device) { (session) in + let response: String + do { + let (events, _) = try session.getHistoryEvents(since: oneDayAgo!) + var responseText = String(format: "Found %d events since %@", events.count, oneDayAgo! as NSDate) + for event in events { + responseText += String(format:"\nEvent: %@", event.dictionaryRepresentation) + } + + response = responseText + } catch let error { + response = String(describing: error) + } + + DispatchQueue.main.async { + completionHandler(response) + } + } + + return NSLocalizedString("Fetching history…", comment: "Progress message for fetching pump history.") + } + } + + static func fetchGlucose(ops: PumpOps?, device: RileyLinkDevice) -> T { + return T { (completionHandler) -> String in + let calendar = Calendar(identifier: Calendar.Identifier.gregorian) + let oneDayAgo = calendar.date(byAdding: DateComponents(day: -1), to: Date()) + + ops?.runSession(withName: "Get glucose history", using: device) { (session) in + let response: String + do { + let events = try session.getGlucoseHistoryEvents(since: oneDayAgo!) + var responseText = String(format: "Found %d events since %@", events.count, oneDayAgo! as NSDate) + for event in events { + responseText += String(format: "\nEvent: %@", event.dictionaryRepresentation) + } + + response = responseText + } catch let error { + response = String(describing: error) + } + + DispatchQueue.main.async { + completionHandler(response) + } + } + + return NSLocalizedString("Fetching glucose…", comment: "Progress message for fetching pump glucose.") + } + } + + static func getPumpModel(ops: PumpOps?, device: RileyLinkDevice) -> T { + return T { (completionHandler) -> String in + ops?.runSession(withName: "Get Pump Model", using: device) { (session) in + let response: String + do { + let model = try session.getPumpModel(usingCache: false) + response = "Pump Model: \(model)" + } catch let error { + response = String(describing: error) + } + + DispatchQueue.main.async { + completionHandler(response) + } + } + + return NSLocalizedString("Fetching pump model…", comment: "Progress message for fetching pump model.") + } + } + + static func mySentryPair(ops: PumpOps?, device: RileyLinkDevice) -> T { + return T { (completionHandler) -> String in + var byteArray = [UInt8](repeating: 0, count: 16) + (device.peripheralIdentifier as NSUUID).getBytes(&byteArray) + let watchdogID = Data(bytes: byteArray[0..<3]) + + ops?.runSession(withName: "Change watchdog marriage profile", using: device) { (session) in + let response: String + do { + try session.changeWatchdogMarriageProfile(watchdogID) + response = self.successText + } catch let error { + response = String(describing: error) + } + + DispatchQueue.main.async { + completionHandler(response) + } + } + + return NSLocalizedString( + "On your pump, go to the Find Device screen and select \"Find Device\"." + + "\n" + + "\nMain Menu >" + + "\nUtilities >" + + "\nConnect Devices >" + + "\nOther Devices >" + + "\nOn >" + + "\nFind Device", + comment: "Pump find device instruction" + ) + } + } + + static func pressDownButton(ops: PumpOps?, device: RileyLinkDevice) -> T { + return T { (completionHandler) -> String in + ops?.runSession(withName: "Press down button", using: device) { (session) in + let response: String + do { + try session.pressButton(.down) + response = self.successText + } catch let error { + response = String(describing: error) + } + + DispatchQueue.main.async { + completionHandler(response) + } + } + + return NSLocalizedString("Sending button press…", comment: "Progress message for sending button press to pump.") + } + } + + static func readBasalSchedule(ops: PumpOps?, device: RileyLinkDevice, integerFormatter: NumberFormatter) -> T { + return T { (completionHandler) -> String in + ops?.runSession(withName: "Get Basal Settings", using: device) { (session) in + let response: String + do { + let schedule = try session.getBasalSchedule(for: .standard) + var str = String(format: NSLocalizedString("%1$@ basal schedule entries\n", comment: "The format string describing number of basal schedule entries: (1: number of entries)"), integerFormatter.string(from: NSNumber(value: schedule.entries.count))!) + for entry in schedule.entries { + str += "\(String(describing: entry))\n" + } + response = str + } catch let error { + response = String(describing: error) + } + + DispatchQueue.main.async { + completionHandler(response) + } + } + + return NSLocalizedString("Reading basal schedule…", comment: "Progress message for reading basal schedule") + } + } + + static func readPumpStatus(ops: PumpOps?, device: RileyLinkDevice, decimalFormatter: NumberFormatter) -> T { + return T { (completionHandler) -> String in + ops?.runSession(withName: "Read pump status", using: device) { (session) in + let response: String + do { + let status = try session.getCurrentPumpStatus() + + var str = String(format: NSLocalizedString("%1$@ Units of insulin remaining\n", comment: "The format string describing units of insulin remaining: (1: number of units)"), decimalFormatter.string(from: NSNumber(value: status.reservoir))!) + str += String(format: NSLocalizedString("Battery: %1$@ volts\n", comment: "The format string describing pump battery voltage: (1: battery voltage)"), decimalFormatter.string(from: NSNumber(value: status.batteryVolts))!) + str += String(format: NSLocalizedString("Suspended: %1$@\n", comment: "The format string describing pump suspended state: (1: suspended)"), String(describing: status.suspended)) + str += String(format: NSLocalizedString("Bolusing: %1$@\n", comment: "The format string describing pump bolusing state: (1: bolusing)"), String(describing: status.bolusing)) + response = str + } catch let error { + response = String(describing: error) + } + + DispatchQueue.main.async { + completionHandler(response) + } + } + + return NSLocalizedString("Reading pump status…", comment: "Progress message for reading pump status") + } + } + + static func tuneRadio(ops: PumpOps?, device: RileyLinkDevice, current: Measurement?, measurementFormatter: MeasurementFormatter) -> T { + return T { (completionHandler) -> String in + ops?.runSession(withName: "Tune pump", using: device) { (session) in + let response: String + do { + let scanResult = try session.tuneRadio(current: nil) + + NotificationCenter.default.post( + name: .DeviceStateDidChange, + object: device, + userInfo: [ + RileyLinkDevice.notificationDeviceStateKey: DeviceState( + lastTuned: Date(), + lastValidFrequency: scanResult.bestFrequency + ) + ] + ) + + var resultDict: [String: Any] = [:] + + let intFormatter = NumberFormatter() + let formatString = NSLocalizedString("%1$@ %2$@/%3$@ %4$@", comment: "The format string for displaying a frequency tune trial. Extra spaces added for emphesis: (1: frequency in MHz)(2: success count)(3: total count)(4: average RSSI)") + + resultDict[NSLocalizedString("Best Frequency", comment: "The label indicating the best radio frequency")] = measurementFormatter.string(from: scanResult.bestFrequency) + resultDict[NSLocalizedString("Trials", comment: "The label indicating the results of each frequency trial")] = scanResult.trials.map({ (trial) -> String in + + return String( + format: formatString, + measurementFormatter.string(from: trial.frequency), + intFormatter.string(from: NSNumber(value: trial.successes))!, + intFormatter.string(from: NSNumber(value: trial.tries))!, + intFormatter.string(from: NSNumber(value: trial.avgRSSI))! + ) + }) + + var responseText: String + + if let data = try? JSONSerialization.data(withJSONObject: resultDict, options: .prettyPrinted), let string = String(data: data, encoding: .utf8) { + responseText = string + } else { + responseText = NSLocalizedString("No response", comment: "Message display when no response from tuning pump") + } + + response = responseText + } catch let error { + response = String(describing: error) + } + + DispatchQueue.main.async { + completionHandler(response) + } + } + + return NSLocalizedString("Tuning radio…", comment: "Progress message for tuning radio") + } + } +} diff --git a/RileyLinkKit/UI/CommandResponseViewController.swift b/RileyLinkKitUI/CommandResponseViewController.swift similarity index 53% rename from RileyLinkKit/UI/CommandResponseViewController.swift rename to RileyLinkKitUI/CommandResponseViewController.swift index f3c71ec62..cee94644d 100644 --- a/RileyLinkKit/UI/CommandResponseViewController.swift +++ b/RileyLinkKitUI/CommandResponseViewController.swift @@ -8,7 +8,8 @@ import UIKit -class CommandResponseViewController: UIViewController, UIActivityItemSource { + +class CommandResponseViewController: UIViewController { typealias Command = (_ completionHandler: @escaping (_ responseText: String) -> Void) -> String init(command: @escaping Command) { @@ -32,9 +33,23 @@ class CommandResponseViewController: UIViewController, UIActivityItemSource { override func viewDidLoad() { super.viewDidLoad() - textView.font = UIFont(name: "Menlo-Regular", size: 14) + if #available(iOS 11.0, *) { + textView.contentInsetAdjustmentBehavior = .always + } + + let font = UIFont(name: "Menlo-Regular", size: 14) + if #available(iOS 11.0, *), let font = font { + let metrics = UIFontMetrics(forTextStyle: .body) + textView.font = metrics.scaledFont(for: font) + } else { + textView.font = font + } + textView.text = command { [weak self] (responseText) -> Void in - self?.textView.text = responseText + var newText = self?.textView.text ?? "" + newText += "\n\n" + newText += responseText + self?.textView.text = newText } textView.isEditable = false @@ -46,18 +61,18 @@ class CommandResponseViewController: UIViewController, UIActivityItemSource { present(activityVC, animated: true, completion: nil) } +} - // MARK: - UIActivityItemSource - - func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { +extension CommandResponseViewController: UIActivityItemSource { + public func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { return title ?? textView.text ?? "" } - func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivityType?) -> Any? { + public func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivityType?) -> Any? { return textView.attributedText ?? "" } - func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivityType?) -> String { - return title ?? textView.text ?? "" + public func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivityType?) -> String { + return title ?? textView.text } } diff --git a/RileyLinkKitUI/Info.plist b/RileyLinkKitUI/Info.plist new file mode 100644 index 000000000..d74989676 --- /dev/null +++ b/RileyLinkKitUI/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 2.0.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/RileyLinkKitUI/PumpOps.swift b/RileyLinkKitUI/PumpOps.swift new file mode 100644 index 000000000..bc2a24671 --- /dev/null +++ b/RileyLinkKitUI/PumpOps.swift @@ -0,0 +1,18 @@ +// +// PumpOps.swift +// RileyLinkKitUI +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import RileyLinkKit + + +/// Provide a notification contract that clients can use to inform RileyLink UI of changes to PumpOps.PumpState +extension PumpOps { + public static let notificationPumpStateKey = "com.rileylink.RileyLinkKit.PumpOps.PumpState" +} + +extension Notification.Name { + public static let PumpOpsStateDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkKit.PumpOpsStateDidChange") +} diff --git a/RileyLinkKitUI/RileyLinkDevice.swift b/RileyLinkKitUI/RileyLinkDevice.swift new file mode 100644 index 000000000..d0efa6f29 --- /dev/null +++ b/RileyLinkKitUI/RileyLinkDevice.swift @@ -0,0 +1,18 @@ +// +// RileyLinkDevice.swift +// RileyLinkKitUI +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +import RileyLinkBLEKit + + +/// Provide a notification contract that clients can use to inform RileyLink UI of changes to DeviceState +extension RileyLinkDevice { + public static let notificationDeviceStateKey = "com.rileylink.RileyLinkKit.RileyLinkDevice.DeviceState" +} + +extension Notification.Name { + public static let DeviceStateDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkKit.DeviceStateDidChange") +} diff --git a/RileyLinkKitUI/RileyLinkDeviceTableViewCell.swift b/RileyLinkKitUI/RileyLinkDeviceTableViewCell.swift new file mode 100644 index 000000000..e0776feca --- /dev/null +++ b/RileyLinkKitUI/RileyLinkDeviceTableViewCell.swift @@ -0,0 +1,91 @@ +// +// RileyLinkDeviceTableViewCell.swift +// Naterade +// +// Created by Nathan Racklyeft on 8/29/15. +// Copyright © 2015 Nathan Racklyeft. All rights reserved. +// + +import CoreBluetooth +import UIKit + +public class RileyLinkDeviceTableViewCell: UITableViewCell { + + public var connectSwitch: UISwitch? + + public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: .value1, reuseIdentifier: reuseIdentifier) + + setup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + public override func awakeFromNib() { + super.awakeFromNib() + + setup() + } + + private func setup() { + // Manually layout the switch with an 8pt left margin + // TODO: Adjust appropriately for RTL + let connectSwitch = UISwitch(frame: .zero) + connectSwitch.sizeToFit() + connectSwitch.frame = connectSwitch.frame.offsetBy(dx: 8, dy: 0) + + var switchFrame = connectSwitch.frame + switchFrame.origin = .zero + switchFrame.size.width += 8 + + let switchContainer = UIView(frame: switchFrame) + switchContainer.addSubview(connectSwitch) + + self.connectSwitch = connectSwitch + + accessoryView = switchContainer + accessoryType = .disclosureIndicator + } + + override public func layoutSubviews() { + super.layoutSubviews() + + contentView.layoutMargins.left = separatorInset.left + contentView.layoutMargins.right = separatorInset.left + } + + public func configureCellWithName(_ name: String?, signal: String?, peripheralState: CBPeripheralState?) { + textLabel?.text = name + detailTextLabel?.text = " \(signal ?? "") " + + if let state = peripheralState { + switch state { + case .connected: + connectSwitch?.isOn = true + connectSwitch?.isEnabled = true + case .connecting: + connectSwitch?.isOn = true + connectSwitch?.isEnabled = true + case .disconnected: + connectSwitch?.isOn = false + connectSwitch?.isEnabled = true + case .disconnecting: + connectSwitch?.isOn = false + connectSwitch?.isEnabled = false + } + } else { + connectSwitch?.isHidden = true + } + } + + public override func prepareForReuse() { + super.prepareForReuse() + + connectSwitch?.removeTarget(nil, action: nil, for: .valueChanged) + } + +} + + diff --git a/RileyLinkKitUI/RileyLinkDeviceTableViewController.swift b/RileyLinkKitUI/RileyLinkDeviceTableViewController.swift new file mode 100644 index 000000000..b29ae6fdc --- /dev/null +++ b/RileyLinkKitUI/RileyLinkDeviceTableViewController.swift @@ -0,0 +1,523 @@ +// +// RileyLinkDeviceTableViewController.swift +// Naterade +// +// Created by Nathan Racklyeft on 3/5/16. +// Copyright © 2016 Nathan Racklyeft. All rights reserved. +// + +import UIKit +import MinimedKit +import RileyLinkBLEKit +import RileyLinkKit + +let CellIdentifier = "Cell" + +public class RileyLinkDeviceTableViewController: UITableViewController { + + public let device: RileyLinkDevice + + private var deviceState: DeviceState + + private let ops: PumpOps? + + private var pumpState: PumpState? { + didSet { + // Update the UI if its visible + guard rssiFetchTimer != nil else { return } + + switch (oldValue, pumpState) { + case (.none, .some): + tableView.insertSections(IndexSet(integer: Section.commands.rawValue), with: .automatic) + case (.some, .none): + tableView.deleteSections(IndexSet(integer: Section.commands.rawValue), with: .automatic) + case (_, let state?): + if let cell = cellForRow(.awake) { + cell.setAwakeUntil(state.awakeUntil, formatter: dateFormatter) + } + + if let cell = cellForRow(.model) { + cell.setPumpModel(state.pumpModel) + } + default: + break + } + } + } + + private let pumpSettings: PumpSettings? + + private var bleRSSI: Int? + + private var firmwareVersion: String? { + didSet { + guard isViewLoaded else { + return + } + + cellForRow(.version)?.detailTextLabel?.text = firmwareVersion + } + } + + private var lastIdle: Date? { + didSet { + guard isViewLoaded else { + return + } + + cellForRow(.idleStatus)?.setDetailDate(lastIdle, formatter: dateFormatter) + } + } + + var rssiFetchTimer: Timer? { + willSet { + rssiFetchTimer?.invalidate() + } + } + + private var appeared = false + + public init(device: RileyLinkDevice, deviceState: DeviceState, pumpSettings: PumpSettings?, pumpState: PumpState?, pumpOps: PumpOps?) { + self.device = device + self.deviceState = deviceState + self.pumpSettings = pumpSettings + self.pumpState = pumpState + self.ops = pumpOps + + super.init(style: .grouped) + + updateDeviceStatus() + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + title = device.name + + self.observe() + } + + @objc func updateRSSI() { + device.readRSSI() + } + + func updateDeviceStatus() { + device.getStatus { (status) in + DispatchQueue.main.async { + self.lastIdle = status.lastIdle + self.firmwareVersion = status.firmwareDescription + } + } + } + + // References to registered notification center observers + private var notificationObservers: [Any] = [] + + deinit { + for observer in notificationObservers { + NotificationCenter.default.removeObserver(observer) + } + } + + private var deviceObserver: Any? { + willSet { + if let observer = deviceObserver { + NotificationCenter.default.removeObserver(observer) + } + } + } + + private func observe() { + let center = NotificationCenter.default + let mainQueue = OperationQueue.main + + notificationObservers = [ + center.addObserver(forName: .DeviceNameDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in + if let cell = self?.cellForRow(.customName) { + cell.detailTextLabel?.text = self?.device.name + } + + self?.title = self?.device.name + }, + center.addObserver(forName: .DeviceConnectionStateDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in + if let cell = self?.cellForRow(.connection) { + cell.detailTextLabel?.text = self?.device.peripheralState.description + } + }, + center.addObserver(forName: .DeviceRSSIDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in + self?.bleRSSI = note.userInfo?[RileyLinkDevice.notificationRSSIKey] as? Int + + if let cell = self?.cellForRow(.rssi), let formatter = self?.integerFormatter { + cell.setDetailRSSI(self?.bleRSSI, formatter: formatter) + } + }, + center.addObserver(forName: .DeviceDidStartIdle, object: device, queue: mainQueue) { [weak self] (note) in + self?.updateDeviceStatus() + }, + center.addObserver(forName: .PumpOpsStateDidChange, object: ops, queue: mainQueue) { [weak self] (note) in + if let state = note.userInfo?[PumpOps.notificationPumpStateKey] as? PumpState { + self?.pumpState = state + } + } + ] + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if appeared { + tableView.reloadData() + } + + rssiFetchTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(updateRSSI), userInfo: nil, repeats: true) + + appeared = true + + updateRSSI() + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + rssiFetchTimer = nil + } + + + // MARK: - Formatters + + private lazy var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + + dateFormatter.dateStyle = .none + dateFormatter.timeStyle = .medium + + return dateFormatter + }() + + private lazy var integerFormatter = NumberFormatter() + + private lazy var measurementFormatter: MeasurementFormatter = { + let formatter = MeasurementFormatter() + + formatter.numberFormatter = decimalFormatter + + return formatter + }() + + private lazy var decimalFormatter: NumberFormatter = { + let decimalFormatter = NumberFormatter() + + decimalFormatter.numberStyle = .decimal + decimalFormatter.minimumSignificantDigits = 5 + + return decimalFormatter + }() + + // MARK: - Table view data source + + private enum Section: Int, CaseCountable { + case device + case pump + case commands + } + + private enum DeviceRow: Int, CaseCountable { + case customName + case version + case rssi + case connection + case idleStatus + } + + private enum PumpRow: Int, CaseCountable { + case id + case model + case awake + } + + private enum CommandRow: Int, CaseCountable { + case tune + case changeTime + case mySentryPair + case dumpHistory + case fetchGlucose + case getPumpModel + case pressDownButton + case readPumpStatus + case readBasalSchedule + } + + private func cellForRow(_ row: DeviceRow) -> UITableViewCell? { + return tableView.cellForRow(at: IndexPath(row: row.rawValue, section: Section.device.rawValue)) + } + + private func cellForRow(_ row: PumpRow) -> UITableViewCell? { + return tableView.cellForRow(at: IndexPath(row: row.rawValue, section: Section.pump.rawValue)) + } + + public override func numberOfSections(in tableView: UITableView) -> Int { + if pumpState == nil { + return Section.count - 1 + } else { + return Section.count + } + } + + public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Section(rawValue: section)! { + case .device: + return DeviceRow.count + case .pump: + return PumpRow.count + case .commands: + return CommandRow.count + } + } + + public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: UITableViewCell + + if let reusableCell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier) { + cell = reusableCell + } else { + cell = UITableViewCell(style: .value1, reuseIdentifier: CellIdentifier) + } + + cell.accessoryType = .none + + switch Section(rawValue: indexPath.section)! { + case .device: + switch DeviceRow(rawValue: indexPath.row)! { + case .customName: + cell.textLabel?.text = NSLocalizedString("Name", comment: "The title of the cell showing device name") + cell.detailTextLabel?.text = device.name + cell.accessoryType = .disclosureIndicator + case .version: + cell.textLabel?.text = NSLocalizedString("Firmware", comment: "The title of the cell showing firmware version") + cell.detailTextLabel?.text = firmwareVersion + case .connection: + cell.textLabel?.text = NSLocalizedString("Connection State", comment: "The title of the cell showing BLE connection state") + cell.detailTextLabel?.text = device.peripheralState.description + case .rssi: + cell.textLabel?.text = NSLocalizedString("Signal Strength", comment: "The title of the cell showing BLE signal strength (RSSI)") + + cell.setDetailRSSI(bleRSSI, formatter: integerFormatter) + case .idleStatus: + cell.textLabel?.text = NSLocalizedString("On Idle", comment: "The title of the cell showing the last idle") + cell.setDetailDate(lastIdle, formatter: dateFormatter) + } + case .pump: + switch PumpRow(rawValue: indexPath.row)! { + case .id: + cell.textLabel?.text = NSLocalizedString("Pump ID", comment: "The title of the cell showing pump ID") + if let pumpID = pumpSettings?.pumpID { + cell.detailTextLabel?.text = pumpID + } else { + cell.detailTextLabel?.text = "–" + } + case .model: + cell.textLabel?.text = NSLocalizedString("Pump Model", comment: "The title of the cell showing the pump model number") + cell.setPumpModel(pumpState?.pumpModel) + case .awake: + cell.setAwakeUntil(pumpState?.awakeUntil, formatter: dateFormatter) + } + case .commands: + cell.accessoryType = .disclosureIndicator + cell.detailTextLabel?.text = nil + + switch CommandRow(rawValue: indexPath.row)! { + case .tune: + switch (deviceState.lastValidFrequency, deviceState.lastTuned) { + case (let frequency?, let date?): + cell.textLabel?.text = measurementFormatter.string(from: frequency) + cell.setDetailDate(date, formatter: dateFormatter) + default: + cell.textLabel?.text = NSLocalizedString("Tune Radio Frequency", comment: "The title of the command to re-tune the radio") + } + + case .changeTime: + cell.textLabel?.text = NSLocalizedString("Change Time", comment: "The title of the command to change pump time") + + let localTimeZone = TimeZone.current + let localTimeZoneName = localTimeZone.abbreviation() ?? localTimeZone.identifier + + if let pumpTimeZone = pumpState?.timeZone { + let timeZoneDiff = TimeInterval(pumpTimeZone.secondsFromGMT() - localTimeZone.secondsFromGMT()) + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.hour, .minute] + let diffString = timeZoneDiff != 0 ? formatter.string(from: abs(timeZoneDiff)) ?? String(abs(timeZoneDiff)) : "" + + cell.detailTextLabel?.text = String(format: NSLocalizedString("%1$@%2$@%3$@", comment: "The format string for displaying an offset from a time zone: (1: GMT)(2: -)(3: 4:00)"), localTimeZoneName, timeZoneDiff != 0 ? (timeZoneDiff < 0 ? "-" : "+") : "", diffString) + } else { + cell.detailTextLabel?.text = localTimeZoneName + } + case .mySentryPair: + cell.textLabel?.text = NSLocalizedString("MySentry Pair", comment: "The title of the command to pair with mysentry") + + case .dumpHistory: + cell.textLabel?.text = NSLocalizedString("Fetch Recent History", comment: "The title of the command to fetch recent history") + + case .fetchGlucose: + cell.textLabel?.text = NSLocalizedString("Fetch Enlite Glucose", comment: "The title of the command to fetch recent glucose") + + case .getPumpModel: + cell.textLabel?.text = NSLocalizedString("Get Pump Model", comment: "The title of the command to get pump model") + + case .pressDownButton: + cell.textLabel?.text = NSLocalizedString("Send Button Press", comment: "The title of the command to send a button press") + + case .readPumpStatus: + cell.textLabel?.text = NSLocalizedString("Read Pump Status", comment: "The title of the command to read pump status") + + case .readBasalSchedule: + cell.textLabel?.text = NSLocalizedString("Read Basal Schedule", comment: "The title of the command to read basal schedule") +} + } + + return cell + } + + public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch Section(rawValue: section)! { + case .device: + return NSLocalizedString("Device", comment: "The title of the section describing the device") + case .pump: + return NSLocalizedString("Pump", comment: "The title of the section describing the pump") + case .commands: + return NSLocalizedString("Commands", comment: "The title of the section describing commands") + } + } + + // MARK: - UITableViewDelegate + + public override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + switch Section(rawValue: indexPath.section)! { + case .device: + switch DeviceRow(rawValue: indexPath.row)! { + case .customName: + return true + default: + return false + } + case .pump: + return false + case .commands: + return device.peripheralState == .connected + } + } + + public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch Section(rawValue: indexPath.section)! { + case .device: + switch DeviceRow(rawValue: indexPath.row)! { + case .customName: + let vc = TextFieldTableViewController() + if let cell = tableView.cellForRow(at: indexPath) { + vc.title = cell.textLabel?.text + vc.value = device.name + vc.delegate = self + vc.keyboardType = .default + } + + show(vc, sender: indexPath) + default: + break + } + case .commands: + let vc: CommandResponseViewController + + switch CommandRow(rawValue: indexPath.row)! { + case .tune: + vc = .tuneRadio(ops: ops, device: device, current: deviceState.lastValidFrequency, measurementFormatter: measurementFormatter) + case .changeTime: + vc = .changeTime(ops: ops, device: device) + case .mySentryPair: + vc = .mySentryPair(ops: ops, device: device) + case .dumpHistory: + vc = .dumpHistory(ops: ops, device: device) + case .fetchGlucose: + vc = .fetchGlucose(ops: ops, device: device) + case .getPumpModel: + vc = .getPumpModel(ops: ops, device: device) + case .pressDownButton: + vc = .pressDownButton(ops: ops, device: device) + case .readPumpStatus: + vc = .readPumpStatus(ops: ops, device: device, decimalFormatter: decimalFormatter) + case .readBasalSchedule: + vc = .readBasalSchedule(ops: ops, device: device, integerFormatter: integerFormatter) + } + + if let cell = tableView.cellForRow(at: indexPath) { + vc.title = cell.textLabel?.text + } + + show(vc, sender: indexPath) + case .pump: + break + } + } +} + + +extension RileyLinkDeviceTableViewController: TextFieldTableViewControllerDelegate { + public func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) { + _ = navigationController?.popViewController(animated: true) + } + + public func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) { + if let indexPath = tableView.indexPathForSelectedRow { + switch Section(rawValue: indexPath.section)! { + case .device: + switch DeviceRow(rawValue: indexPath.row)! { + case .customName: + device.setCustomName(controller.value!) + default: + break + } + default: + break + + } + } + } +} + + +private extension UITableViewCell { + func setDetailDate(_ date: Date?, formatter: DateFormatter) { + if let date = date { + detailTextLabel?.text = formatter.string(from: date) + } else { + detailTextLabel?.text = "-" + } + } + + func setDetailRSSI(_ decibles: Int?, formatter: NumberFormatter) { + detailTextLabel?.text = formatter.decibleString(from: decibles) ?? "-" + } + + func setAwakeUntil(_ awakeUntil: Date?, formatter: DateFormatter) { + switch awakeUntil { + case let until? where until.timeIntervalSinceNow < 0: + textLabel?.text = NSLocalizedString("Last Awake", comment: "The title of the cell describing an awake radio") + setDetailDate(until, formatter: formatter) + case let until?: + textLabel?.text = NSLocalizedString("Awake Until", comment: "The title of the cell describing an awake radio") + setDetailDate(until, formatter: formatter) + default: + textLabel?.text = NSLocalizedString("Listening Off", comment: "The title of the cell describing no radio awake data") + detailTextLabel?.text = nil + } + } + + func setPumpModel(_ pumpModel: PumpModel?) { + if let pumpModel = pumpModel { + detailTextLabel?.text = String(describing: pumpModel) + } else { + detailTextLabel?.text = NSLocalizedString("Unknown", comment: "The detail text for an unknown pump model") + } + } +} diff --git a/RileyLinkKitUI/RileyLinkKitUI.h b/RileyLinkKitUI/RileyLinkKitUI.h new file mode 100644 index 000000000..236c99f79 --- /dev/null +++ b/RileyLinkKitUI/RileyLinkKitUI.h @@ -0,0 +1,18 @@ +// +// RileyLinkKitUI.h +// RileyLinkKitUI +// +// Copyright © 2017 Pete Schwamb. All rights reserved. +// + +#import + +//! Project version number for RileyLinkKitUI. +FOUNDATION_EXPORT double RileyLinkKitUIVersionNumber; + +//! Project version string for RileyLinkKitUI. +FOUNDATION_EXPORT const unsigned char RileyLinkKitUIVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/RileyLinkKit/UI/TextFieldTableViewCell.swift b/RileyLinkKitUI/TextFieldTableViewCell.swift similarity index 100% rename from RileyLinkKit/UI/TextFieldTableViewCell.swift rename to RileyLinkKitUI/TextFieldTableViewCell.swift diff --git a/RileyLinkKit/UI/TextFieldTableViewCell.xib b/RileyLinkKitUI/TextFieldTableViewCell.xib similarity index 81% rename from RileyLinkKit/UI/TextFieldTableViewCell.xib rename to RileyLinkKitUI/TextFieldTableViewCell.xib index 9084ff9a5..07c8aab24 100644 --- a/RileyLinkKit/UI/TextFieldTableViewCell.xib +++ b/RileyLinkKitUI/TextFieldTableViewCell.xib @@ -1,14 +1,18 @@ - - + + + + + - + + - + @@ -16,7 +20,7 @@ - + diff --git a/RileyLinkKit/UI/TextFieldTableViewController.swift b/RileyLinkKitUI/TextFieldTableViewController.swift similarity index 50% rename from RileyLinkKit/UI/TextFieldTableViewController.swift rename to RileyLinkKitUI/TextFieldTableViewController.swift index 5a11525dc..d8639e53a 100644 --- a/RileyLinkKit/UI/TextFieldTableViewController.swift +++ b/RileyLinkKitUI/TextFieldTableViewController.swift @@ -9,42 +9,50 @@ import UIKit -internal protocol TextFieldTableViewControllerDelegate: class { +public protocol TextFieldTableViewControllerDelegate: class { func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) } -internal class TextFieldTableViewController: UITableViewController, UITextFieldDelegate { +public class TextFieldTableViewController: UITableViewController, UITextFieldDelegate { private weak var textField: UITextField? - internal var indexPath: IndexPath? + public var indexPath: IndexPath? - internal var placeholder: String? + public var placeholder: String? - internal var value: String? { + internal var unit: String? + + public var value: String? { didSet { delegate?.textFieldTableViewControllerDidEndEditing(self) } } + internal var contextHelp: String? + internal var keyboardType = UIKeyboardType.default - internal weak var delegate: TextFieldTableViewControllerDelegate? + internal var autocapitalizationType = UITextAutocapitalizationType.words + + public weak var delegate: TextFieldTableViewControllerDelegate? internal convenience init() { self.init(style: .grouped) } - internal override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() + tableView.cellLayoutMarginsFollowReadableWidth = true + tableView.register(TextFieldTableViewCell.nib(), forCellReuseIdentifier: TextFieldTableViewCell.className) } - internal override func viewDidAppear(_ animated: Bool) { + public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) textField?.becomeFirstResponder() @@ -52,33 +60,37 @@ internal class TextFieldTableViewController: UITableViewController, UITextFieldD // MARK: - UITableViewDataSource - internal override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 } - internal override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableViewCell.className, for: indexPath) as! TextFieldTableViewCell textField = cell.textField - cell.textField.delegate = self - cell.textField.text = value - cell.textField.keyboardType = keyboardType - cell.textField.placeholder = placeholder - cell.textField.autocapitalizationType = .words + cell.textField?.delegate = self + cell.textField?.text = value + cell.textField?.keyboardType = keyboardType + cell.textField?.placeholder = placeholder + cell.textField?.autocapitalizationType = autocapitalizationType return cell } + override public func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + return contextHelp + } + // MARK: - UITextFieldDelegate - internal func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { value = textField.text return true } - internal func textFieldShouldReturn(_ textField: UITextField) -> Bool { + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { value = textField.text textField.delegate = nil diff --git a/RileyLinkKit/Extensions/UITableViewCell.swift b/RileyLinkKitUI/UITableViewCell.swift similarity index 100% rename from RileyLinkKit/Extensions/UITableViewCell.swift rename to RileyLinkKitUI/UITableViewCell.swift diff --git a/RileyLinkTests/RileyLinkTests-Info.plist b/RileyLinkTests/RileyLinkTests-Info.plist index 5bd2f9239..20849defa 100644 --- a/RileyLinkTests/RileyLinkTests-Info.plist +++ b/RileyLinkTests/RileyLinkTests-Info.plist @@ -13,7 +13,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.2 + 2.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/RileyLinkTests/es.lproj/InfoPlist.strings b/RileyLinkTests/es.lproj/InfoPlist.strings new file mode 100644 index 000000000..477b28ff8 --- /dev/null +++ b/RileyLinkTests/es.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/RileyLinkTests/ru.lproj/InfoPlist.strings b/RileyLinkTests/ru.lproj/InfoPlist.strings new file mode 100644 index 000000000..477b28ff8 --- /dev/null +++ b/RileyLinkTests/ru.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ +