From 556ade3e9dff8ba74adea46efcd324ba253e6ca5 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 22 Mar 2018 09:01:53 -0500 Subject: [PATCH 1/2] Observe backfill messages --- {xDripG5 => Common}/HKUnit.swift | 4 +- xDripG5.xcodeproj/project.pbxproj | 18 ++- xDripG5/BluetoothManager.swift | 25 +++- xDripG5/BluetoothServices.swift | 5 +- xDripG5/CalibrationState.swift | 103 ++++++++++----- xDripG5/Glucose.swift | 51 ++++++-- xDripG5/Messages/GlucoseBackfillMessage.swift | 117 ++++++++++++++++++ xDripG5/Messages/GlucoseRxMessage.swift | 65 ++++++++-- xDripG5/Opcode.swift | 28 ++++- xDripG5/Transmitter.swift | 96 ++++++++++++-- .../GlucoseBackfillMessageTests.swift | 65 ++++++++++ xDripG5Tests/GlucoseRxMessageTests.swift | 52 +++++--- xDripG5Tests/GlucoseTests.swift | 28 ++--- .../TransmitterTimeRxMessageTests.swift | 10 +- 14 files changed, 542 insertions(+), 125 deletions(-) rename {xDripG5 => Common}/HKUnit.swift (82%) create mode 100644 xDripG5/Messages/GlucoseBackfillMessage.swift create mode 100644 xDripG5Tests/GlucoseBackfillMessageTests.swift diff --git a/xDripG5/HKUnit.swift b/Common/HKUnit.swift similarity index 82% rename from xDripG5/HKUnit.swift rename to Common/HKUnit.swift index 34196bf7..e3f0dded 100644 --- a/xDripG5/HKUnit.swift +++ b/Common/HKUnit.swift @@ -10,7 +10,7 @@ import HealthKit extension HKUnit { - static func milligramsPerDeciliter() -> HKUnit { + static let milligramsPerDeciliter: HKUnit = { return HKUnit.gramUnit(with: .milli).unitDivided(by: HKUnit.literUnit(with: .deci)) - } + }() } diff --git a/xDripG5.xcodeproj/project.pbxproj b/xDripG5.xcodeproj/project.pbxproj index bf0d6f7f..62b33750 100644 --- a/xDripG5.xcodeproj/project.pbxproj +++ b/xDripG5.xcodeproj/project.pbxproj @@ -11,13 +11,14 @@ 431CE7631F8EEF6D00255374 /* CBPeripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7621F8EEF6D00255374 /* CBPeripheral.swift */; }; 431CE7671F91D0B300255374 /* PeripheralManager+G5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7661F91D0B300255374 /* PeripheralManager+G5.swift */; }; 4323115F1EFC870300B95E62 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4323115E1EFC870300B95E62 /* OSLog.swift */; }; + 433BC81B205CB64A000B1200 /* GlucoseBackfillMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433BC81A205CB64A000B1200 /* GlucoseBackfillMessage.swift */; }; + 433BC81D205CBB16000B1200 /* GlucoseBackfillMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433BC81C205CBB16000B1200 /* GlucoseBackfillMessageTests.swift */; }; 43460F88200B30D10030C0E3 /* TransmitterIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43460F87200B30D10030C0E3 /* TransmitterIDTests.swift */; }; 435535D41FB2C1B000CE5A23 /* PeripheralManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435535D31FB2C1B000CE5A23 /* PeripheralManagerError.swift */; }; 437AFEFA2038EC43008C4892 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437AFEF92038EC43008C4892 /* AppDelegate.swift */; }; 437AFEFC2038EC43008C4892 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437AFEFB2038EC43008C4892 /* ViewController.swift */; }; 437AFF012038EC43008C4892 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 437AFF002038EC43008C4892 /* Assets.xcassets */; }; 437AFF042038EC43008C4892 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 437AFF022038EC43008C4892 /* LaunchScreen.storyboard */; }; - 437AFF0F2038ED71008C4892 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437AFF0E2038ED71008C4892 /* HKUnit.swift */; }; 437AFF122038EDEC008C4892 /* xDripG5.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43CABDF31C3506F100005705 /* xDripG5.framework */; }; 437AFF132038EDEC008C4892 /* xDripG5.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 43CABDF31C3506F100005705 /* xDripG5.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 437AFF182038EDF9008C4892 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43E3978C1D566AEA0028E321 /* HealthKit.framework */; }; @@ -61,6 +62,8 @@ 43E397911D5692080028E321 /* GlucoseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E397901D5692080028E321 /* GlucoseTests.swift */; }; 43E397931D56950C0028E321 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E397921D56950C0028E321 /* HKUnit.swift */; }; 43E4B1F21F8AF9790038823E /* PeripheralManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E4B1F11F8AF9790038823E /* PeripheralManager.swift */; }; + 43E5292C2060C4FA00ACEB3B /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E397921D56950C0028E321 /* HKUnit.swift */; }; + 43E5292D2060C50800ACEB3B /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E397921D56950C0028E321 /* HKUnit.swift */; }; 43EEA7111D14DC0800CBBDA0 /* AESCrypt.h in Headers */ = {isa = PBXBuildFile; fileRef = 43EEA70F1D14DC0800CBBDA0 /* AESCrypt.h */; settings = {ATTRIBUTES = (Public, ); }; }; 43EEA7121D14DC0800CBBDA0 /* AESCrypt.m in Sources */ = {isa = PBXBuildFile; fileRef = 43EEA7101D14DC0800CBBDA0 /* AESCrypt.m */; }; 43F82BCC1D035AA4006F5DD7 /* TransmitterTimeRxMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F82BCB1D035AA4006F5DD7 /* TransmitterTimeRxMessageTests.swift */; }; @@ -117,6 +120,8 @@ 431CE7621F8EEF6D00255374 /* CBPeripheral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBPeripheral.swift; sourceTree = ""; }; 431CE7661F91D0B300255374 /* PeripheralManager+G5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PeripheralManager+G5.swift"; sourceTree = ""; }; 4323115E1EFC870300B95E62 /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; + 433BC81A205CB64A000B1200 /* GlucoseBackfillMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseBackfillMessage.swift; sourceTree = ""; }; + 433BC81C205CBB16000B1200 /* GlucoseBackfillMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseBackfillMessageTests.swift; sourceTree = ""; }; 43460F87200B30D10030C0E3 /* TransmitterIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransmitterIDTests.swift; sourceTree = ""; }; 435535D31FB2C1B000CE5A23 /* PeripheralManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralManagerError.swift; sourceTree = ""; }; 437AFEF72038EC43008C4892 /* CGMBLEKit Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "CGMBLEKit Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -125,7 +130,6 @@ 437AFF002038EC43008C4892 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 437AFF032038EC43008C4892 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 437AFF052038EC43008C4892 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 437AFF0E2038ED71008C4892 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HKUnit.swift; path = xDripG5/HKUnit.swift; sourceTree = SOURCE_ROOT; }; 43846AC51D8F896C00799272 /* CalibrationDataRxMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationDataRxMessage.swift; sourceTree = ""; }; 43846AC71D8F89BE00799272 /* CalibrationDataRxMessageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationDataRxMessageTests.swift; sourceTree = ""; }; 43880F971D9E19FC009061A8 /* TransmitterVersionRxMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransmitterVersionRxMessage.swift; sourceTree = ""; }; @@ -212,7 +216,6 @@ children = ( C1CD8B0A203931AD00A8F498 /* NSUserDefaults.swift */, 437AFEF92038EC43008C4892 /* AppDelegate.swift */, - 437AFF0E2038ED71008C4892 /* HKUnit.swift */, 437AFEFB2038EC43008C4892 /* ViewController.swift */, 437AFF002038EC43008C4892 /* Assets.xcassets */, C19084B9203932BD00AA47F3 /* Main.storyboard */, @@ -262,7 +265,6 @@ 431CE7621F8EEF6D00255374 /* CBPeripheral.swift */, 43E3978A1D5668BD0028E321 /* CalibrationState.swift */, 43E3978E1D566B170028E321 /* Glucose.swift */, - 43E397921D56950C0028E321 /* HKUnit.swift */, 43CABDF81C3506F100005705 /* Info.plist */, 430D64C41CB7846A00FCA750 /* NSData+CRC.swift */, 4323115E1EFC870300B95E62 /* OSLog.swift */, @@ -282,6 +284,7 @@ isa = PBXGroup; children = ( 43846AC71D8F89BE00799272 /* CalibrationDataRxMessageTests.swift */, + 433BC81C205CBB16000B1200 /* GlucoseBackfillMessageTests.swift */, 43DC87C11C8B520F005BC30D /* GlucoseRxMessageTests.swift */, 43E397901D5692080028E321 /* GlucoseTests.swift */, 43CABE041C3506F100005705 /* Info.plist */, @@ -307,6 +310,7 @@ 43846AC51D8F896C00799272 /* CalibrationDataRxMessage.swift */, 43CABE1C1C350B3D00005705 /* DisconnectTxMessage.swift */, 43CE7CC71CA73AEB003CC1B0 /* FirmwareVersionTxMessage.swift */, + 433BC81A205CB64A000B1200 /* GlucoseBackfillMessage.swift */, 43CE7CD31CA73CE8003CC1B0 /* GlucoseHistoryTxMessage.swift */, 43CABE1D1C350B3D00005705 /* GlucoseRxMessage.swift */, 43CABE1E1C350B3D00005705 /* GlucoseTxMessage.swift */, @@ -328,6 +332,7 @@ isa = PBXGroup; children = ( 43DC87BF1C8B509B005BC30D /* Data.swift */, + 43E397921D56950C0028E321 /* HKUnit.swift */, ); path = Common; sourceTree = ""; @@ -484,11 +489,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 43E5292C2060C4FA00ACEB3B /* HKUnit.swift in Sources */, C1CD8B0C203931AD00A8F498 /* NSUserDefaults.swift in Sources */, 43D140CE2047AA930032346D /* Data.swift in Sources */, 437AFEFC2038EC43008C4892 /* ViewController.swift in Sources */, 437AFEFA2038EC43008C4892 /* AppDelegate.swift in Sources */, - 437AFF0F2038ED71008C4892 /* HKUnit.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -518,6 +523,7 @@ 43CE7CCC1CA73BCC003CC1B0 /* BatteryStatusTxMessage.swift in Sources */, 43EEA7121D14DC0800CBBDA0 /* AESCrypt.m in Sources */, 43CE7CCE1CA73C22003CC1B0 /* SessionStartTxMessage.swift in Sources */, + 433BC81B205CB64A000B1200 /* GlucoseBackfillMessage.swift in Sources */, 43CABE2E1C350B3D00005705 /* TransmitterTimeTxMessage.swift in Sources */, 43880F981D9E19FC009061A8 /* TransmitterVersionRxMessage.swift in Sources */, 43CABE2C1C350B3D00005705 /* TransmitterMessage.swift in Sources */, @@ -544,6 +550,8 @@ files = ( 43460F88200B30D10030C0E3 /* TransmitterIDTests.swift in Sources */, 43F82BCC1D035AA4006F5DD7 /* TransmitterTimeRxMessageTests.swift in Sources */, + 433BC81D205CBB16000B1200 /* GlucoseBackfillMessageTests.swift in Sources */, + 43E5292D2060C50800ACEB3B /* HKUnit.swift in Sources */, 43F82BD41D037227006F5DD7 /* SessionStartRxMessageTests.swift in Sources */, 43846AC81D8F89BE00799272 /* CalibrationDataRxMessageTests.swift in Sources */, 43DC87C01C8B509B005BC30D /* Data.swift in Sources */, diff --git a/xDripG5/BluetoothManager.swift b/xDripG5/BluetoothManager.swift index d65bddad..e5f876e0 100644 --- a/xDripG5/BluetoothManager.swift +++ b/xDripG5/BluetoothManager.swift @@ -31,11 +31,19 @@ protocol BluetoothManagerDelegate: class { */ func bluetoothManager(_ manager: BluetoothManager, shouldConnectPeripheral peripheral: CBPeripheral) -> Bool - /// Tells the delegate that the bluetooth manager received new data in the control characteristic. + /// Informs the delegate that the bluetooth manager received new data in the control characteristic /// - /// - parameter manager: The bluetooth manager - /// - parameter didReceiveControlResponse: The data received on the control characteristic + /// - Parameters: + /// - manager: The bluetooth manager + /// - response: The data received on the control characteristic func bluetoothManager(_ manager: BluetoothManager, didReceiveControlResponse response: Data) + + /// Informs the delegate that the bluetooth manager received new data in the backfill characteristic + /// + /// - Parameters: + /// - manager: The bluetooth manager + /// - response: The data received on the backfill characteristic + func bluetoothManager(_ manager: BluetoothManager, didReceiveBackfillResponse response: Data) } @@ -96,7 +104,7 @@ class BluetoothManager: NSObject { } let currentState = peripheral?.state ?? .disconnected - guard currentState == .disconnected else { + guard currentState != .connected else { return } @@ -255,6 +263,13 @@ extension BluetoothManager: PeripheralManagerDelegate { return } - self.delegate?.bluetoothManager(self, didReceiveControlResponse: value) + switch CGMServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString.uppercased()) { + case .none, .communication?, .authentication?: + return + case .control?: + self.delegate?.bluetoothManager(self, didReceiveControlResponse: value) + case .backfill?: + self.delegate?.bluetoothManager(self, didReceiveBackfillResponse: value) + } } } diff --git a/xDripG5/BluetoothServices.swift b/xDripG5/BluetoothServices.swift index 62971632..4733bc77 100644 --- a/xDripG5/BluetoothServices.swift +++ b/xDripG5/BluetoothServices.swift @@ -48,7 +48,7 @@ enum CGMServiceCharacteristicUUID: String, CBUUIDRawValue { case authentication = "F8083535-849E-531C-C594-30F1F86A4EA5" // Read/Write/Notify - case probablyBackfill = "F8083536-849E-531C-C594-30F1F86A4EA5" + case backfill = "F8083536-849E-531C-C594-30F1F86A4EA5" } @@ -67,7 +67,8 @@ extension PeripheralManager.Configuration { TransmitterServiceUUID.cgmService.cbUUID: [ CGMServiceCharacteristicUUID.communication.cbUUID, CGMServiceCharacteristicUUID.authentication.cbUUID, - CGMServiceCharacteristicUUID.control.cbUUID + CGMServiceCharacteristicUUID.control.cbUUID, + CGMServiceCharacteristicUUID.backfill.cbUUID, ] ], notifyingCharacteristics: [:], diff --git a/xDripG5/CalibrationState.swift b/xDripG5/CalibrationState.swift index f27c3250..b7a7b775 100644 --- a/xDripG5/CalibrationState.swift +++ b/xDripG5/CalibrationState.swift @@ -9,50 +9,87 @@ import Foundation -public enum CalibrationState { +public enum CalibrationState: RawRepresentable { public typealias RawValue = UInt8 - case stopped - case warmup - case needFirstInitialCalibration - case needSecondInitialCalibration - case ok - case needCalibration + public enum State: RawValue { + case stopped = 1 + case warmup = 2 + + case needFirstInitialCalibration = 4 + case needSecondInitialCalibration = 5 + case ok = 6 + case needCalibration7 = 7 + case calibrationError8 = 8 + case calibrationError9 = 9 + case calibrationError10 = 10 + case sensorFailure11 = 11 + case sensorFailure12 = 12 + case calibrationError13 = 13 + case needCalibration14 = 14 + case sessionFailure15 = 15 + case sessionFailure16 = 16 + case sessionFailure17 = 17 + case questionMarks = 18 + } + + case known(State) case unknown(RawValue) - init(rawValue: UInt8) { - switch rawValue { - case 1: - self = .stopped - case 2: - self = .warmup - case 4: - self = .needFirstInitialCalibration - case 5: - self = .needSecondInitialCalibration - case 6: - self = .ok - case 7: - self = .needCalibration - default: + public init(rawValue: RawValue) { + guard let state = State(rawValue: rawValue) else { self = .unknown(rawValue) + return + } + + self = .known(state) + } + + public var rawValue: RawValue { + switch self { + case .known(let state): + return state.rawValue + case .unknown(let rawValue): + return rawValue } } public var hasReliableGlucose: Bool { - return self == .ok || self == .needCalibration + guard case .known(let state) = self else { + return false + } + + switch state { + case .stopped, + .warmup, + .needFirstInitialCalibration, + .needSecondInitialCalibration, + .calibrationError8, + .calibrationError9, + .calibrationError10, + .sensorFailure11, + .sensorFailure12, + .calibrationError13, + .sessionFailure15, + .sessionFailure16, + .sessionFailure17, + .questionMarks: + return false + case .ok, .needCalibration7, .needCalibration14: + return true + } } } -extension CalibrationState: Equatable { } - -public func ==(lhs: CalibrationState, rhs: CalibrationState) -> Bool { - switch (lhs, rhs) { - case (.stopped, .stopped), (.warmup, .warmup), (.needFirstInitialCalibration, .needFirstInitialCalibration), (.needSecondInitialCalibration, .needSecondInitialCalibration), (.ok, .ok), (.needCalibration, .needCalibration): - return true - case let (.unknown(lhsRaw), .unknown(rhsRaw)): - return lhsRaw == rhsRaw - default: - return false +extension CalibrationState: Equatable { + public static func ==(lhs: CalibrationState, rhs: CalibrationState) -> Bool { + switch (lhs, rhs) { + case (.known(let lhs), .known(let rhs)): + return lhs == rhs + case (.unknown(let lhs), .unknown(let rhs)): + return lhs == rhs + default: + return false + } } } diff --git a/xDripG5/Glucose.swift b/xDripG5/Glucose.swift index a4a50e64..54f1b892 100644 --- a/xDripG5/Glucose.swift +++ b/xDripG5/Glucose.swift @@ -11,25 +11,46 @@ import HealthKit public struct Glucose { - public let glucoseMessage: GlucoseRxMessage + let glucoseMessage: GlucoseSubMessage let timeMessage: TransmitterTimeRxMessage - init(glucoseMessage: GlucoseRxMessage, timeMessage: TransmitterTimeRxMessage, activationDate: Date) { + init( + transmitterID: String, + glucoseMessage: GlucoseRxMessage, + timeMessage: TransmitterTimeRxMessage, + activationDate: Date + ) { + self.init( + transmitterID: transmitterID, + status: glucoseMessage.status, + glucoseMessage: glucoseMessage.glucose, + timeMessage: timeMessage, + activationDate: activationDate + ) + } + + init( + transmitterID: String, + status: UInt8, + glucoseMessage: GlucoseSubMessage, + timeMessage: TransmitterTimeRxMessage, + activationDate: Date + ) { + self.transmitterID = transmitterID self.glucoseMessage = glucoseMessage self.timeMessage = timeMessage + self.status = TransmitterStatus(rawValue: status) - status = TransmitterStatus(rawValue: glucoseMessage.status) - state = CalibrationState(rawValue: glucoseMessage.state) sessionStartDate = activationDate.addingTimeInterval(TimeInterval(timeMessage.sessionStartTime)) readDate = activationDate.addingTimeInterval(TimeInterval(glucoseMessage.timestamp)) } // MARK: - Transmitter Info + public let transmitterID: String public let status: TransmitterStatus public let sessionStartDate: Date // MARK: - Glucose Info - public let state: CalibrationState public let readDate: Date public var isDisplayOnly: Bool { @@ -41,20 +62,28 @@ public struct Glucose { return nil } - let unit = HKUnit.milligramsPerDeciliter() + let unit = HKUnit.milligramsPerDeciliter return HKQuantity(unit: unit, doubleValue: Double(glucoseMessage.glucose)) } + public var state: CalibrationState { + return CalibrationState(rawValue: glucoseMessage.state) + } + public var trend: Int { return Int(glucoseMessage.trend) } -} - -extension Glucose: Equatable { } + // An identifier for this reading thatʼs consistent between backfill/live data + public var syncIdentifier: String { + return "\(transmitterID) \(glucoseMessage.timestamp)" + } +} -public func ==(lhs: Glucose, rhs: Glucose) -> Bool { - return lhs.glucoseMessage == rhs.glucoseMessage && lhs.timeMessage == rhs.timeMessage +extension Glucose: Equatable { + public static func ==(lhs: Glucose, rhs: Glucose) -> Bool { + return lhs.glucoseMessage == rhs.glucoseMessage && lhs.syncIdentifier == rhs.syncIdentifier + } } diff --git a/xDripG5/Messages/GlucoseBackfillMessage.swift b/xDripG5/Messages/GlucoseBackfillMessage.swift new file mode 100644 index 00000000..58ad8411 --- /dev/null +++ b/xDripG5/Messages/GlucoseBackfillMessage.swift @@ -0,0 +1,117 @@ +// +// GlucoseBackfillMessage.swift +// xDripG5 +// +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation + +// 50 05 02 00 b7ff5200 66045300 00000000 0000 7138 + +struct GlucoseBackfillTxMessage: RespondableMessage { + typealias Response = GlucoseBackfillRxMessage + + let byte1: UInt8 + let byte2: UInt8 + let identifier: UInt8 + + let startTime: UInt32 + let endTime: UInt32 + + let length: UInt32 = 0 + let backfillCRC: UInt16 = 0 + + var data: Data { + var data = Data(for: .glucoseBackfillTx) + data.append(contentsOf: [byte1, byte2, identifier]) + data.append(startTime) + data.append(endTime) + data.append(length) + data.append(backfillCRC) + + return data.appendingCRC() + } +} + +// 51 00 01 00 b7ff5200 66045300 32000000 e6cb 9805 + +struct GlucoseBackfillRxMessage: TransmitterRxMessage { + let status: UInt8 + let backfillStatus: UInt8 + let identifier: UInt8 + let startTime: UInt32 + let endTime: UInt32 + let bufferLength: UInt32 + let bufferCRC: UInt16 + + init?(data: Data) { + guard data.count == 20, + data.isCRCValid, + data.starts(with: .glucoseBackfillRx) + else { + return nil + } + + status = data[1] + backfillStatus = data[2] + identifier = data[3] + startTime = data[4..<8].toInt() + endTime = data[8..<12].toInt() + bufferLength = data[12..<16].toInt() + bufferCRC = data[16..<18].toInt() + } +} + +// 0100bc460000b7ff52008b0006eee30053008500 +// 020006eb0f025300800006ee3a0353007e0006f5 +// 030066045300790006f8 + +struct GlucoseBackfillFrameBuffer { + let identifier: UInt8 + private var frames: [Data] = [] + + init(identifier: UInt8) { + self.identifier = identifier + } + + mutating func append(_ frame: Data) { + // Byte 0 is the frame index + // Byte 1 is the identifier + guard frame.count > 2, + frame[0] == frames.count + 1, + frame[1] == identifier else { + return + } + + frames.append(frame) + } + + var count: Int { + return frames.reduce(0, { $0 + $1.count }) + } + + var crc16: UInt16 { + return frames.reduce(into: Data(), { $0.append($1) }).crc16 + } + + var glucose: [GlucoseSubMessage] { + // Drop the first 2 bytes from each frame + let data = frames.reduce(into: Data(), { $0.append($1.dropFirst(2)) }) + + // Drop the first 4 bytes from the combined message + // Byte 0: ?? + // Byte 1: ?? + // Byte 2: ?? (only seen 0 so far) + // Byte 3: ?? (only seen 0 so far) + let glucoseData = data.dropFirst(4) + + return stride( + from: glucoseData.startIndex, + to: glucoseData.endIndex, + by: GlucoseSubMessage.size + ).map { + return GlucoseSubMessage(data: glucoseData[$0..<$0.advanced(by: GlucoseSubMessage.size)])! + } + } +} diff --git a/xDripG5/Messages/GlucoseRxMessage.swift b/xDripG5/Messages/GlucoseRxMessage.swift index c10a72ba..f540352c 100644 --- a/xDripG5/Messages/GlucoseRxMessage.swift +++ b/xDripG5/Messages/GlucoseRxMessage.swift @@ -9,15 +9,46 @@ import Foundation -public struct GlucoseRxMessage: TransmitterRxMessage { - public let status: UInt8 - public let sequence: UInt32 +public struct GlucoseSubMessage: TransmitterRxMessage { + static let size = 8 + public let timestamp: UInt32 public let glucoseIsDisplayOnly: Bool public let glucose: UInt16 public let state: UInt8 public let trend: Int8 + init?(data: Data) { + guard data.count == GlucoseSubMessage.size else { + return nil + } + + var start = data.startIndex + var end = start.advanced(by: 4) + timestamp = data[start.. 0 + glucose = glucoseBytes & 0xfff + + start = end + end = start.advanced(by: 1) + state = data[start] + + start = end + end = start.advanced(by: 1) + trend = Int8(bitPattern: data[start]) + } +} + + +public struct GlucoseRxMessage: TransmitterRxMessage { + public let status: UInt8 + public let sequence: UInt32 + public let glucose: GlucoseSubMessage + init?(data: Data) { guard data.count == 16 && data.isCRCValid else { return nil @@ -29,21 +60,29 @@ public struct GlucoseRxMessage: TransmitterRxMessage { status = data[1] sequence = data[2..<6].toInt() - timestamp = data[6..<10].toInt() - let glucoseBytes = data[10..<12].to(UInt16.self) - glucoseIsDisplayOnly = (glucoseBytes & 0xf000) > 0 - glucose = glucoseBytes & 0xfff + guard let glucose = GlucoseSubMessage(data: data[6..<14]) else { + return nil + } + self.glucose = glucose + } +} - state = data[12] - trend = Int8(bitPattern: data[13]) +extension GlucoseSubMessage: Equatable { + public static func ==(lhs: GlucoseSubMessage, rhs: GlucoseSubMessage) -> Bool { + return lhs.timestamp == rhs.timestamp && + lhs.glucoseIsDisplayOnly == rhs.glucoseIsDisplayOnly && + lhs.glucose == rhs.glucose && + lhs.state == rhs.state && + lhs.trend == rhs.trend } } extension GlucoseRxMessage: Equatable { -} - -public func ==(lhs: GlucoseRxMessage, rhs: GlucoseRxMessage) -> Bool { - return lhs.sequence == rhs.sequence && lhs.timestamp == rhs.timestamp + public static func ==(lhs: GlucoseRxMessage, rhs: GlucoseRxMessage) -> Bool { + return lhs.status == rhs.status && + lhs.sequence == rhs.sequence && + lhs.glucose == rhs.glucose + } } diff --git a/xDripG5/Opcode.swift b/xDripG5/Opcode.swift index 0e4a4084..780b091f 100644 --- a/xDripG5/Opcode.swift +++ b/xDripG5/Opcode.swift @@ -7,36 +7,54 @@ enum Opcode: UInt8 { + // Auth case authRequestTx = 0x01 + case authRequestRx = 0x03 case authChallengeTx = 0x04 case authChallengeRx = 0x05 case keepAlive = 0x06 case bondRequest = 0x07 + // Control case disconnectTx = 0x09 - case firmwareVersionTx = 0x20 - case batteryStatusTx = 0x22 + + + + case firmwareVersionTx = 0x20 + case firmwareVersionRx = 0x21 + case batteryStatusTx = 0x22 + case batteryStatusRx = 0x23 case transmitterTimeTx = 0x24 case transmitterTimeRx = 0x25 case sessionStartTx = 0x26 case sessionStartRx = 0x27 - case sessionStopTx = 0x28 case sessionStopRx = 0x29 + + + + case glucoseTx = 0x30 case glucoseRx = 0x31 - + case calibrationDataTx = 0x32 case calibrationDataRx = 0x33 case calibrateGlucoseTx = 0x34 + case calibrateGlucoseRx = 0x35 - case glucoseHistoryTx = 0x3e + case eraseTx = 0x42 + case eraseRx = 0x43 case transmitterVersionTx = 0x4a case transmitterVersionRx = 0x4b + + case glucoseBackfillTx = 0x50 + case glucoseBackfillRx = 0x51 + + } diff --git a/xDripG5/Transmitter.swift b/xDripG5/Transmitter.swift index d5497c55..a073a3db 100644 --- a/xDripG5/Transmitter.swift +++ b/xDripG5/Transmitter.swift @@ -16,6 +16,8 @@ public protocol TransmitterDelegate: class { func transmitter(_ transmitter: Transmitter, didRead glucose: Glucose) + func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose]) + func transmitter(_ transmitter: Transmitter, didReadUnknownData data: Data) } @@ -35,20 +37,36 @@ public final class Transmitter: BluetoothManagerDelegate { private var id: TransmitterID + public var passiveModeEnabled: Bool + + public weak var delegate: TransmitterDelegate? + + // MARK: - Passive observation state, confined to `bluetoothManager.managerQueue` + /// The initial activation date of the transmitter - public private(set) var activationDate: Date? + private var activationDate: Date? - private var lastTimeMessage: TransmitterTimeRxMessage? + /// The last-seen time message + private var lastTimeMessage: TransmitterTimeRxMessage? { + didSet { + if let time = lastTimeMessage { + activationDate = Date(timeIntervalSinceNow: -TimeInterval(time.currentTime)) + } else { + activationDate = nil + } + } + } - public var passiveModeEnabled: Bool + /// The backfill data buffer + private var backfillBuffer: GlucoseBackfillFrameBuffer? - public weak var delegate: TransmitterDelegate? + // MARK: - private let log = OSLog(category: "Transmitter") private let bluetoothManager = BluetoothManager() - private var delegateQueue = DispatchQueue(label: "com.loudnate.xDripG5.delegateQueue", qos: .utility) + private let delegateQueue = DispatchQueue(label: "com.loudnate.xDripG5.delegateQueue", qos: .utility) public init(id: String, passiveModeEnabled: Bool = false) { self.id = TransmitterID(id: id) @@ -112,7 +130,13 @@ public final class Transmitter: BluetoothManagerDelegate { self.log.info("Bonding request sent. Waiting user to respond.") } - let glucose = try peripheral.control(shouldWaitForBond: status.bonded != 0x1) + let (timeMessage, activationDate, glucoseMessage) = try peripheral.control(shouldWaitForBond: status.bonded != 0x1) + let glucose = Glucose( + transmitterID: self.id.id, + glucoseMessage: glucoseMessage, + timeMessage: timeMessage, + activationDate: activationDate + ) self.delegateQueue.async { self.delegate?.transmitter(self, didRead: glucose) } @@ -148,24 +172,71 @@ public final class Transmitter: BluetoothManagerDelegate { let activationDate = activationDate { delegateQueue.async { - self.delegate?.transmitter(self, didRead: Glucose(glucoseMessage: glucoseMessage, timeMessage: timeMessage, activationDate: activationDate)) + self.delegate?.transmitter(self, didRead: Glucose(transmitterID: self.id.id, glucoseMessage: glucoseMessage, timeMessage: timeMessage, activationDate: activationDate)) } } case .transmitterTimeRx?: if let timeMessage = TransmitterTimeRxMessage(data: response) { - self.activationDate = Date(timeIntervalSinceNow: -TimeInterval(timeMessage.currentTime)) self.lastTimeMessage = timeMessage return } + case .glucoseBackfillRx?: + guard let backfillMessage = GlucoseBackfillRxMessage(data: response) else { + break + } + + guard let backfillBuffer = backfillBuffer else { + log.error("Received GlucoseBackfillRxMessage %{public}@ but backfillBuffer is nil", String(describing: backfillMessage)) + break + } + + guard let timeMessage = lastTimeMessage, let activationDate = activationDate else { + log.error("Received GlucoseBackfillRxMessage %{public}@ but activationDate is unknown", String(describing: backfillMessage)) + break + } + + guard backfillMessage.bufferLength == backfillBuffer.count else { + log.error("GlucoseBackfillRxMessage expected buffer length %d, but was %d", backfillMessage.bufferLength, backfillBuffer.count) + break + } + + guard backfillMessage.bufferCRC == backfillBuffer.crc16 else { + log.error("GlucoseBackfillRxMessage expected CRC %04x, but was %04x", backfillMessage.bufferCRC, backfillBuffer.crc16) + break + } + + let glucose = backfillBuffer.glucose.map { + Glucose(transmitterID: id.id, status: backfillMessage.status, glucoseMessage: $0, timeMessage: timeMessage, activationDate: activationDate) + } + + if glucose.count > 0 { + delegateQueue.async { + self.delegate?.transmitter(self, didReadBackfill: glucose) + } + } case .none: delegateQueue.async { - self.delegate?.transmitter(self, didReadUnknownData: response) + self.delegate?.transmitter(self, didReadUnknownData: response) } default: // We ignore all other known opcodes break } } + + func bluetoothManager(_ manager: BluetoothManager, didReceiveBackfillResponse response: Data) { + guard response.count > 2 else { + return + } + + if response[0] == 1 { + log.info("Starting new backfill buffer with ID %d", response[1]) + + self.backfillBuffer = GlucoseBackfillFrameBuffer(identifier: response[1]) + } + + self.backfillBuffer?.append(response) + } } @@ -224,7 +295,6 @@ fileprivate extension PeripheralManager { throw TransmitterError.authenticationError("Failed to compute challenge hash for transmitter ID") } - do { try writeMessage(AuthChallengeTxMessage(challengeHash: challengeHash), for: .authentication) } catch let error { @@ -259,7 +329,7 @@ fileprivate extension PeripheralManager { } } - fileprivate func control(shouldWaitForBond: Bool = false) throws -> Glucose { + fileprivate func control(shouldWaitForBond: Bool = false) throws -> (time: TransmitterTimeRxMessage, activationDate: Date, glucose: GlucoseRxMessage) { do { if shouldWaitForBond { try setNotifyValue(true, for: .control, timeout: 15) @@ -294,13 +364,13 @@ fileprivate extension PeripheralManager { } } - // Update and notify - return Glucose(glucoseMessage: glucoseMessage, timeMessage: timeMessage, activationDate: activationDate) + return (time: timeMessage, activationDate: activationDate, glucose: glucoseMessage) } fileprivate func listenToControl() throws { do { try setNotifyValue(true, for: .control) + try setNotifyValue(true, for: .backfill) } catch let error { throw TransmitterError.controlError("Error enabling notification: \(error)") } diff --git a/xDripG5Tests/GlucoseBackfillMessageTests.swift b/xDripG5Tests/GlucoseBackfillMessageTests.swift new file mode 100644 index 00000000..4eb2ae71 --- /dev/null +++ b/xDripG5Tests/GlucoseBackfillMessageTests.swift @@ -0,0 +1,65 @@ +// +// GlucoseBackfillMessageTests.swift +// xDripG5Tests +// +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import XCTest +@testable import xDripG5 + +class GlucoseBackfillMessageTests: XCTestCase { + + func testTxMessage() { + let message = GlucoseBackfillTxMessage(byte1: 5, byte2: 2, identifier: 0, startTime: 5439415, endTime: 5440614) // 20 minutes + + XCTAssertEqual(Data(hexadecimalString: "50050200b7ff5200660453000000000000007138")!, message.data) + } + + func testRxMessage() { + let message = GlucoseBackfillRxMessage(data: Data(hexadecimalString: "51000100b7ff52006604530032000000e6cb9805")!)! + + XCTAssertEqual(.ok, TransmitterStatus(rawValue: message.status)) + XCTAssertEqual(1, message.backfillStatus) + XCTAssertEqual(0, message.identifier) + XCTAssertEqual(5439415, message.startTime) + XCTAssertEqual(5440614, message.endTime) + XCTAssertEqual(50, message.bufferLength) + XCTAssertEqual(0xcbe6, message.bufferCRC) + + var buffer = GlucoseBackfillFrameBuffer(identifier: message.identifier) + buffer.append(Data(hexadecimalString: "0100bc460000b7ff52008b0006eee30053008500")!) + buffer.append(Data(hexadecimalString: "020006eb0f025300800006ee3a0353007e0006f5")!) + buffer.append(Data(hexadecimalString: "030066045300790006f8")!) + + XCTAssertEqual(Int(message.bufferLength), buffer.count) + XCTAssertEqual(message.bufferCRC, buffer.crc16) + + let messages = buffer.glucose + + XCTAssertEqual(139, messages[0].glucose) + XCTAssertEqual(5439415, messages[0].timestamp) + XCTAssertEqual(.known(.ok), CalibrationState(rawValue: messages[0].state)) + XCTAssertEqual(-18, messages[0].trend) + + XCTAssertEqual(133, messages[1].glucose) + XCTAssertEqual(5439715, messages[1].timestamp) + XCTAssertEqual(.known(.ok), CalibrationState(rawValue: messages[1].state)) + XCTAssertEqual(-21, messages[1].trend) + + XCTAssertEqual(128, messages[2].glucose) + XCTAssertEqual(5440015, messages[2].timestamp) + XCTAssertEqual(.known(.ok), CalibrationState(rawValue: messages[2].state)) + XCTAssertEqual(-18, messages[2].trend) + + XCTAssertEqual(126, messages[3].glucose) + XCTAssertEqual(5440314, messages[3].timestamp) + XCTAssertEqual(.known(.ok), CalibrationState(rawValue: messages[3].state)) + XCTAssertEqual(-11, messages[3].trend) + + XCTAssertEqual(121, messages[4].glucose) + XCTAssertEqual(5440614, messages[4].timestamp) + XCTAssertEqual(.known(.ok), CalibrationState(rawValue: messages[4].state)) + XCTAssertEqual(-08, messages[4].trend) + } +} diff --git a/xDripG5Tests/GlucoseRxMessageTests.swift b/xDripG5Tests/GlucoseRxMessageTests.swift index 89a8ce9a..9c11d6c0 100644 --- a/xDripG5Tests/GlucoseRxMessageTests.swift +++ b/xDripG5Tests/GlucoseRxMessageTests.swift @@ -18,11 +18,11 @@ class GlucoseRxMessageTests: XCTestCase { XCTAssertEqual(0, message.status) XCTAssertEqual(2664, message.sequence) - XCTAssertEqual(5730698, message.timestamp) - XCTAssertFalse(message.glucoseIsDisplayOnly) - XCTAssertEqual(204, message.glucose) - XCTAssertEqual(6, message.state) - XCTAssertEqual(-1, message.trend) + XCTAssertEqual(5730698, message.glucose.timestamp) + XCTAssertFalse(message.glucose.glucoseIsDisplayOnly) + XCTAssertEqual(204, message.glucose.glucose) + XCTAssertEqual(6, message.glucose.state) + XCTAssertEqual(-1, message.glucose.trend) } func testNegativeTrend() { @@ -31,11 +31,11 @@ class GlucoseRxMessageTests: XCTestCase { XCTAssertEqual(0, message.status) XCTAssertEqual(2671, message.sequence) - XCTAssertEqual(5732798, message.timestamp) - XCTAssertFalse(message.glucoseIsDisplayOnly) - XCTAssertEqual(122, message.glucose) - XCTAssertEqual(6, message.state) - XCTAssertEqual(-28, message.trend) + XCTAssertEqual(5732798, message.glucose.timestamp) + XCTAssertFalse(message.glucose.glucoseIsDisplayOnly) + XCTAssertEqual(122, message.glucose.glucose) + XCTAssertEqual(6, message.glucose.state) + XCTAssertEqual(-28, message.glucose.trend) } func testDisplayOnly() { @@ -44,11 +44,11 @@ class GlucoseRxMessageTests: XCTestCase { XCTAssertEqual(0, message.status) XCTAssertEqual(2672, message.sequence) - XCTAssertEqual(5733105, message.timestamp) - XCTAssertTrue(message.glucoseIsDisplayOnly) - XCTAssertEqual(88, message.glucose) - XCTAssertEqual(6, message.state) - XCTAssertEqual(-29, message.trend) + XCTAssertEqual(5733105, message.glucose.timestamp) + XCTAssertTrue(message.glucose.glucoseIsDisplayOnly) + XCTAssertEqual(88, message.glucose.glucose) + XCTAssertEqual(6, message.glucose.state) + XCTAssertEqual(-29, message.glucose.trend) } func testOldTransmitter() { @@ -57,11 +57,23 @@ class GlucoseRxMessageTests: XCTestCase { XCTAssertEqual(0, message.status) XCTAssertEqual(170, message.sequence) - XCTAssertEqual(7905429, message.timestamp) // 90 days, status is still OK - XCTAssertFalse(message.glucoseIsDisplayOnly) - XCTAssertEqual(139, message.glucose) - XCTAssertEqual(6, message.state) - XCTAssertEqual(10, message.trend) + XCTAssertEqual(7905429, message.glucose.timestamp) // 90 days, status is still OK + XCTAssertFalse(message.glucose.glucoseIsDisplayOnly) + XCTAssertEqual(139, message.glucose.glucose) + XCTAssertEqual(6, message.glucose.state) + XCTAssertEqual(10, message.glucose.trend) } + func testZeroSequence() { + let data = Data(hexadecimalString: "3100000000008eb14d00820006f6a038")! + let message = GlucoseRxMessage(data: data)! + + XCTAssertEqual(0, message.status) + XCTAssertEqual(0, message.sequence) + XCTAssertEqual(5091726, message.glucose.timestamp) + XCTAssertFalse(message.glucose.glucoseIsDisplayOnly) + XCTAssertEqual(130, message.glucose.glucose) + XCTAssertEqual(6, message.glucose.state) + XCTAssertEqual(-10, message.glucose.trend) + } } diff --git a/xDripG5Tests/GlucoseTests.swift b/xDripG5Tests/GlucoseTests.swift index 43d634fa..9756c220 100644 --- a/xDripG5Tests/GlucoseTests.swift +++ b/xDripG5Tests/GlucoseTests.swift @@ -30,54 +30,54 @@ class GlucoseTests: XCTestCase { func testMessageData() { let data = Data(hexadecimalString: "3100680a00008a715700cc0006ffc42a")! let message = GlucoseRxMessage(data: data)! - let glucose = Glucose(glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) + let glucose = Glucose(transmitterID: "123456", glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) XCTAssertEqual(TransmitterStatus.ok, glucose.status) XCTAssertEqual(calendar.date(from: DateComponents(year: 2016, month: 12, day: 6, hour: 7, minute: 51, second: 38))!, glucose.readDate) XCTAssertEqual(calendar.date(from: DateComponents(year: 2016, month: 12, day: 26, hour: 11, minute: 16, second: 12))!, glucose.sessionStartDate) XCTAssertFalse(glucose.isDisplayOnly) - XCTAssertEqual(204, glucose.glucose?.doubleValue(for: HKUnit.milligramsPerDeciliter())) - XCTAssertEqual(CalibrationState.ok, glucose.state) + XCTAssertEqual(204, glucose.glucose?.doubleValue(for: .milligramsPerDeciliter)) + XCTAssertEqual(.known(.ok), glucose.state) XCTAssertEqual(-1, glucose.trend) } func testNegativeTrend() { let data = Data(hexadecimalString: "31006f0a0000be7957007a0006e4818d")! let message = GlucoseRxMessage(data: data)! - let glucose = Glucose(glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) + let glucose = Glucose(transmitterID: "123456", glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) XCTAssertEqual(TransmitterStatus.ok, glucose.status) XCTAssertEqual(calendar.date(from: DateComponents(year: 2016, month: 12, day: 6, hour: 8, minute: 26, second: 38))!, glucose.readDate) XCTAssertFalse(glucose.isDisplayOnly) - XCTAssertEqual(122, glucose.glucose?.doubleValue(for: HKUnit.milligramsPerDeciliter())) - XCTAssertEqual(CalibrationState.ok, glucose.state) + XCTAssertEqual(122, glucose.glucose?.doubleValue(for: .milligramsPerDeciliter)) + XCTAssertEqual(.known(.ok), glucose.state) XCTAssertEqual(-28, glucose.trend) } func testDisplayOnly() { let data = Data(hexadecimalString: "3100700a0000f17a5700584006e3cee9")! let message = GlucoseRxMessage(data: data)! - let glucose = Glucose(glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) + let glucose = Glucose(transmitterID: "123456", glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) XCTAssertEqual(TransmitterStatus.ok, glucose.status) XCTAssertEqual(calendar.date(from: DateComponents(year: 2016, month: 12, day: 6, hour: 8, minute: 31, second: 45))!, glucose.readDate) XCTAssertTrue(glucose.isDisplayOnly) - XCTAssertEqual(88, glucose.glucose?.doubleValue(for: HKUnit.milligramsPerDeciliter())) - XCTAssertEqual(CalibrationState.ok, glucose.state) - XCTAssertEqual(-29, message.trend) + XCTAssertEqual(88, glucose.glucose?.doubleValue(for: .milligramsPerDeciliter)) + XCTAssertEqual(.known(.ok), glucose.state) + XCTAssertEqual(-29, message.glucose.trend) } func testOldTransmitter() { let data = Data(hexadecimalString: "3100aa00000095a078008b00060a8b34")! let message = GlucoseRxMessage(data: data)! - let glucose = Glucose(glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) + let glucose = Glucose(transmitterID: "123456", glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) XCTAssertEqual(TransmitterStatus.ok, glucose.status) XCTAssertEqual(calendar.date(from: DateComponents(year: 2016, month: 12, day: 31, hour: 11, minute: 57, second: 09))!, glucose.readDate) // 90 days, status is still OK XCTAssertFalse(glucose.isDisplayOnly) - XCTAssertEqual(139, glucose.glucose?.doubleValue(for: HKUnit.milligramsPerDeciliter())) - XCTAssertEqual(CalibrationState.ok, glucose.state) - XCTAssertEqual(10, message.trend) + XCTAssertEqual(139, glucose.glucose?.doubleValue(for: .milligramsPerDeciliter)) + XCTAssertEqual(.known(.ok), glucose.state) + XCTAssertEqual(10, message.glucose.trend) } } diff --git a/xDripG5Tests/TransmitterTimeRxMessageTests.swift b/xDripG5Tests/TransmitterTimeRxMessageTests.swift index cca72c71..cddeb188 100644 --- a/xDripG5Tests/TransmitterTimeRxMessageTests.swift +++ b/xDripG5Tests/TransmitterTimeRxMessageTests.swift @@ -36,12 +36,18 @@ class TransmitterTimeRxMessageTests: XCTestCase { } func testInSession() { - let data = Data(hexadecimalString: "2500470272007cff710001000000fa1d")! - let message = TransmitterTimeRxMessage(data: data)! + var data = Data(hexadecimalString: "2500470272007cff710001000000fa1d")! + var message = TransmitterTimeRxMessage(data: data)! XCTAssertEqual(0, message.status) XCTAssertEqual(7471687, message.currentTime) XCTAssertEqual(7470972, message.sessionStartTime) + data = Data(hexadecimalString: "2500beb24d00f22d4d000100000083c0")! + message = TransmitterTimeRxMessage(data: data)! + + XCTAssertEqual(0, message.status) + XCTAssertEqual(5092030, message.currentTime) + XCTAssertEqual(5058034, message.sessionStartTime) } } From a9f9b9af1c78e7cfdf2722aacee5172569c94188 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 22 Mar 2018 17:54:04 -0500 Subject: [PATCH 2/2] Add glucoseHistoryTx back in. Update example app. --- CGMBLEKit Example/AppDelegate.swift | 10 +++++++++- CGMBLEKit Example/ViewController.swift | 9 ++++++++- xDripG5/Opcode.swift | 6 ++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CGMBLEKit Example/AppDelegate.swift b/CGMBLEKit Example/AppDelegate.swift index b14a60f4..3f8028cc 100644 --- a/CGMBLEKit Example/AppDelegate.swift +++ b/CGMBLEKit Example/AppDelegate.swift @@ -12,7 +12,7 @@ import CoreBluetooth @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, TransmitterDelegate { - + var window: UIWindow? static var sharedDelegate: AppDelegate { @@ -105,4 +105,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, TransmitterDelegate { } } } + + func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose]) { + DispatchQueue.main.async { + if let vc = self.window?.rootViewController as? TransmitterDelegate { + vc.transmitter(transmitter, didReadBackfill: glucose) + } + } + } } diff --git a/CGMBLEKit Example/ViewController.swift b/CGMBLEKit Example/ViewController.swift index 57ebdc19..642d31b5 100644 --- a/CGMBLEKit Example/ViewController.swift +++ b/CGMBLEKit Example/ViewController.swift @@ -107,7 +107,7 @@ class ViewController: UIViewController, TransmitterDelegate, UITextFieldDelegate } func transmitter(_ transmitter: Transmitter, didRead glucose: Glucose) { - let unit = HKUnit.milligramsPerDeciliter() + let unit = HKUnit.milligramsPerDeciliter if let value = glucose.glucose?.doubleValue(for: unit) { titleLabel.text = "\(value) \(unit.unitString)" } else { @@ -123,6 +123,13 @@ class ViewController: UIViewController, TransmitterDelegate, UITextFieldDelegate titleLabel.text = NSLocalizedString("Unknown Data", comment: "Title displayed during unknown data response") subtitleLabel.text = data.hexadecimalString } + + func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose]) { + titleLabel.text = NSLocalizedString("Backfill", comment: "Title displayed during backfill response") + subtitleLabel.text = String(describing: glucose.map { $0.glucose }) + } + + } diff --git a/xDripG5/Opcode.swift b/xDripG5/Opcode.swift index 780b091f..9bd8eab3 100644 --- a/xDripG5/Opcode.swift +++ b/xDripG5/Opcode.swift @@ -15,7 +15,7 @@ enum Opcode: UInt8 { case authChallengeRx = 0x05 case keepAlive = 0x06 case bondRequest = 0x07 - + // Control case disconnectTx = 0x09 @@ -44,10 +44,12 @@ enum Opcode: UInt8 { case calibrationDataRx = 0x33 case calibrateGlucoseTx = 0x34 case calibrateGlucoseRx = 0x35 + + case glucoseHistoryTx = 0x3e case eraseTx = 0x42 case eraseRx = 0x43 - + case transmitterVersionTx = 0x4a case transmitterVersionRx = 0x4b