redesign Libre Utilities

This commit is contained in:
Johan Degraeve 2020-05-26 14:08:06 +02:00
parent 9e9d0d190d
commit f034133488
42 changed files with 1402 additions and 1400 deletions

View File

@ -59,6 +59,10 @@
F816E12C2439DFBA009EE65B /* DexcomG4+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816E12B2439DFBA009EE65B /* DexcomG4+CoreDataProperties.swift */; };
F816E12E2439E06E009EE65B /* BluetoothPeripheralManager+CGMDexcomG4TransmitterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816E12D2439E06E009EE65B /* BluetoothPeripheralManager+CGMDexcomG4TransmitterDelegate.swift */; };
F816E1312439E2DD009EE65B /* DexcomG4BluetoothPeripheralViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816E1302439E2DD009EE65B /* DexcomG4BluetoothPeripheralViewModel.swift */; };
F8177023248CF78300AA3600 /* LibreSensorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8177022248CF78300AA3600 /* LibreSensorType.swift */; };
F8177025248ED4DE00AA3600 /* Libre1DerivedAlgorithmParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8177024248ED4DE00AA3600 /* Libre1DerivedAlgorithmParameters.swift */; };
F8177027248ED57000AA3600 /* LibreRawGlucoseOOPA2Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8177026248ED57000AA3600 /* LibreRawGlucoseOOPA2Data.swift */; };
F8177029248ED6E600AA3600 /* LibreRawGlucoseOOPData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8177028248ED6E600AA3600 /* LibreRawGlucoseOOPData.swift */; };
F81D6D4822BD5F62005EFAE2 /* DexcomShareUploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81D6D4722BD5F62005EFAE2 /* DexcomShareUploadManager.swift */; };
F81D6D4E22BFC762005EFAE2 /* TextsDexcomShareTestResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81D6D4D22BFC762005EFAE2 /* TextsDexcomShareTestResult.swift */; };
F81D6D5222C27F18005EFAE2 /* BgReading+DexcomShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81D6D5122C27F18005EFAE2 /* BgReading+DexcomShare.swift */; };
@ -119,6 +123,11 @@
F878FA7D2405B3CF00BC6DA6 /* BluetoothPeripheralManager+CGMBubbleTransmitterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F878FA7C2405B3CF00BC6DA6 /* BluetoothPeripheralManager+CGMBubbleTransmitterDelegate.swift */; };
F889CB6F236D84AC00A81068 /* M5StackView.strings in Resources */ = {isa = PBXBuildFile; fileRef = F889CB71236D84AC00A81068 /* M5StackView.strings */; };
F890E07A247687AE008FB2EC /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = F890E079247687AE008FB2EC /* URL.swift */; };
F89467EE249EBBDD00F18424 /* LibreRawGlucoseWeb.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89467ED249EBBDD00F18424 /* LibreRawGlucoseWeb.swift */; };
F89467F0249FFF5900F18424 /* LibreRawGlucoseOOPGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89467EF249FFF5900F18424 /* LibreRawGlucoseOOPGlucose.swift */; };
F89467F2249FFF8700F18424 /* LibreRawGlucoseOOPA2List.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89467F1249FFF8700F18424 /* LibreRawGlucoseOOPA2List.swift */; };
F89467F4249FFFA700F18424 /* LibreRawGlucoseOOPA2Cotent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89467F3249FFFA700F18424 /* LibreRawGlucoseOOPA2Cotent.swift */; };
F89467F824A7C94C00F18424 /* LibreHistoricGlucoseA2.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89467F724A7C94C00F18424 /* LibreHistoricGlucoseA2.swift */; };
F897AAF92200F2D200CDDD10 /* CBPeripheralState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897AAF82200F2D200CDDD10 /* CBPeripheralState.swift */; };
F897AAFB2201018800CDDD10 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897AAFA2201018800CDDD10 /* String.swift */; };
F897E24B23FC86CF0075E0E8 /* CGMG5TransmitterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897E24A23FC86CF0075E0E8 /* CGMG5TransmitterDelegate.swift */; };
@ -293,7 +302,6 @@
F8F9721A23A5915900C3F17D /* CGMBubbleTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F971DE23A5915900C3F17D /* CGMBubbleTransmitter.swift */; };
F8F9721B23A5915900C3F17D /* CGMMiaoMiaoTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F971E023A5915900C3F17D /* CGMMiaoMiaoTransmitter.swift */; };
F8F9721C23A5915900C3F17D /* LibreOOPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F971E223A5915900C3F17D /* LibreOOPResponse.swift */; };
F8F9721D23A5915900C3F17D /* LibreDerivedAlgorithmRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F971E323A5915900C3F17D /* LibreDerivedAlgorithmRunner.swift */; };
F8F9721E23A5915900C3F17D /* LibreSensorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F971E423A5915900C3F17D /* LibreSensorState.swift */; };
F8F9721F23A5915900C3F17D /* LibreOOPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F971E523A5915900C3F17D /* LibreOOPClient.swift */; };
F8F9722123A5915900C3F17D /* LibreSensorSerialNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F971E723A5915900C3F17D /* LibreSensorSerialNumber.swift */; };
@ -383,6 +391,10 @@
F816E12B2439DFBA009EE65B /* DexcomG4+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DexcomG4+CoreDataProperties.swift"; sourceTree = "<group>"; };
F816E12D2439E06E009EE65B /* BluetoothPeripheralManager+CGMDexcomG4TransmitterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BluetoothPeripheralManager+CGMDexcomG4TransmitterDelegate.swift"; sourceTree = "<group>"; };
F816E1302439E2DD009EE65B /* DexcomG4BluetoothPeripheralViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomG4BluetoothPeripheralViewModel.swift; sourceTree = "<group>"; };
F8177022248CF78300AA3600 /* LibreSensorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreSensorType.swift; sourceTree = "<group>"; };
F8177024248ED4DE00AA3600 /* Libre1DerivedAlgorithmParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Libre1DerivedAlgorithmParameters.swift; sourceTree = "<group>"; };
F8177026248ED57000AA3600 /* LibreRawGlucoseOOPA2Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreRawGlucoseOOPA2Data.swift; sourceTree = "<group>"; };
F8177028248ED6E600AA3600 /* LibreRawGlucoseOOPData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreRawGlucoseOOPData.swift; sourceTree = "<group>"; };
F81D6D4522B67F55005EFAE2 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/SpeakReading.strings; sourceTree = "<group>"; };
F81D6D4722BD5F62005EFAE2 /* DexcomShareUploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomShareUploadManager.swift; sourceTree = "<group>"; };
F81D6D4D22BFC762005EFAE2 /* TextsDexcomShareTestResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextsDexcomShareTestResult.swift; sourceTree = "<group>"; };
@ -471,6 +483,11 @@
F889CB97236D89C800A81068 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
F889CB9D236D8EEC00A81068 /* xdrip v4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "xdrip v4.xcdatamodel"; sourceTree = "<group>"; };
F890E079247687AE008FB2EC /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
F89467ED249EBBDD00F18424 /* LibreRawGlucoseWeb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreRawGlucoseWeb.swift; sourceTree = "<group>"; };
F89467EF249FFF5900F18424 /* LibreRawGlucoseOOPGlucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreRawGlucoseOOPGlucose.swift; sourceTree = "<group>"; };
F89467F1249FFF8700F18424 /* LibreRawGlucoseOOPA2List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreRawGlucoseOOPA2List.swift; sourceTree = "<group>"; };
F89467F3249FFFA700F18424 /* LibreRawGlucoseOOPA2Cotent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreRawGlucoseOOPA2Cotent.swift; sourceTree = "<group>"; };
F89467F724A7C94C00F18424 /* LibreHistoricGlucoseA2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibreHistoricGlucoseA2.swift; sourceTree = "<group>"; };
F897AAF82200F2D200CDDD10 /* CBPeripheralState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBPeripheralState.swift; sourceTree = "<group>"; };
F897AAFA2201018800CDDD10 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
F897E24A23FC86CF0075E0E8 /* CGMG5TransmitterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMG5TransmitterDelegate.swift; sourceTree = "<group>"; };
@ -725,7 +742,6 @@
F8F971DE23A5915900C3F17D /* CGMBubbleTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMBubbleTransmitter.swift; sourceTree = "<group>"; };
F8F971E023A5915900C3F17D /* CGMMiaoMiaoTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMMiaoMiaoTransmitter.swift; sourceTree = "<group>"; };
F8F971E223A5915900C3F17D /* LibreOOPResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibreOOPResponse.swift; sourceTree = "<group>"; };
F8F971E323A5915900C3F17D /* LibreDerivedAlgorithmRunner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibreDerivedAlgorithmRunner.swift; sourceTree = "<group>"; };
F8F971E423A5915900C3F17D /* LibreSensorState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibreSensorState.swift; sourceTree = "<group>"; };
F8F971E523A5915900C3F17D /* LibreOOPClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibreOOPClient.swift; sourceTree = "<group>"; };
F8F971E723A5915900C3F17D /* LibreSensorSerialNumber.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibreSensorSerialNumber.swift; sourceTree = "<group>"; };
@ -1844,15 +1860,23 @@
F8F971E123A5915900C3F17D /* Utilities */ = {
isa = PBXGroup;
children = (
F8F971E223A5915900C3F17D /* LibreOOPResponse.swift */,
F8F971E323A5915900C3F17D /* LibreDerivedAlgorithmRunner.swift */,
F8F971E423A5915900C3F17D /* LibreSensorState.swift */,
F8F971E523A5915900C3F17D /* LibreOOPClient.swift */,
F8F971E723A5915900C3F17D /* LibreSensorSerialNumber.swift */,
F8F971E823A5915900C3F17D /* CRC.swift */,
F8177024248ED4DE00AA3600 /* Libre1DerivedAlgorithmParameters.swift */,
F8F971E923A5915900C3F17D /* LibreDataParser.swift */,
F89467F724A7C94C00F18424 /* LibreHistoricGlucoseA2.swift */,
F8F971EA23A5915900C3F17D /* LibreMeasurement.swift */,
F8F971E523A5915900C3F17D /* LibreOOPClient.swift */,
F8F971E223A5915900C3F17D /* LibreOOPResponse.swift */,
F8F971EB23A5915900C3F17D /* LibreRawGlucoseData.swift */,
F89467F3249FFFA700F18424 /* LibreRawGlucoseOOPA2Cotent.swift */,
F8177026248ED57000AA3600 /* LibreRawGlucoseOOPA2Data.swift */,
F89467F1249FFF8700F18424 /* LibreRawGlucoseOOPA2List.swift */,
F8177028248ED6E600AA3600 /* LibreRawGlucoseOOPData.swift */,
F89467EF249FFF5900F18424 /* LibreRawGlucoseOOPGlucose.swift */,
F89467ED249EBBDD00F18424 /* LibreRawGlucoseWeb.swift */,
F8F971E723A5915900C3F17D /* LibreSensorSerialNumber.swift */,
F8F971E423A5915900C3F17D /* LibreSensorState.swift */,
F8177022248CF78300AA3600 /* LibreSensorType.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -2190,17 +2214,20 @@
F8F9721623A5915900C3F17D /* CGMG4xDripTransmitter.swift in Sources */,
F821CF8122A5C814005C1E43 /* RepeatingTimer.swift in Sources */,
F8F9722223A5915900C3F17D /* CRC.swift in Sources */,
F89467F2249FFF8700F18424 /* LibreRawGlucoseOOPA2List.swift in Sources */,
F821CF6F229FC280005C1E43 /* Endpoint+NightScout.swift in Sources */,
F8F9722A23A5915900C3F17D /* TransmitterBatteryInfo.swift in Sources */,
F878FA7D2405B3CF00BC6DA6 /* BluetoothPeripheralManager+CGMBubbleTransmitterDelegate.swift in Sources */,
F821CF5D229BF43A005C1E43 /* NSDateFormatter.swift in Sources */,
F8F9721B23A5915900C3F17D /* CGMMiaoMiaoTransmitter.swift in Sources */,
F8F9721A23A5915900C3F17D /* CGMBubbleTransmitter.swift in Sources */,
F89467F824A7C94C00F18424 /* LibreHistoricGlucoseA2.swift in Sources */,
F8AC42A121B31F170078C348 /* xdrip.xcdatamodeld in Sources */,
F8EA6CA921BBE3010082976B /* UniqueId.swift in Sources */,
F8A1585122EDB597007F5B5D /* ConstantsBGGraphBuilder.swift in Sources */,
F81D6D4822BD5F62005EFAE2 /* DexcomShareUploadManager.swift in Sources */,
F8C9784D2428052E00A09483 /* CGMSensorType.swift in Sources */,
F89467EE249EBBDD00F18424 /* LibreRawGlucoseWeb.swift in Sources */,
F8A1586B22EDB967007F5B5D /* ConstantsMaster.swift in Sources */,
F8F9723923A5928D00C3F17D /* M5StackBluetoothPeripheralViewModel.swift in Sources */,
F8F9722B23A5915900C3F17D /* CGMTransmitterDelegate.swift in Sources */,
@ -2236,6 +2263,7 @@
F8297F4E238DCAD800D74D66 /* BluetoothPeripheralsViewController.swift in Sources */,
F8B3A848227F090E004BA588 /* SettingsViewHealthKitSettingsViewModel.swift in Sources */,
F8025E5021EE746400ECF0C0 /* Calibrator.swift in Sources */,
F89467F4249FFFA700F18424 /* LibreRawGlucoseOOPA2Cotent.swift in Sources */,
F85DC2F421CFE3D400B9F74A /* Sensor+CoreDataClass.swift in Sources */,
F8B3A844227F090E004BA588 /* SettingsViewAlertSettingsViewModel.swift in Sources */,
F8A1586322EDB86E007F5B5D /* ConstantsSounds.swift in Sources */,
@ -2261,6 +2289,7 @@
F898EDF4234A8A3200BFB79B /* UInt16.swift in Sources */,
F8BDD452221DEAB2006EAB84 /* TextsSettingsView.swift in Sources */,
F8EEDD6423020FAD00D2D610 /* NoCalibrator.swift in Sources */,
F8177023248CF78300AA3600 /* LibreSensorType.swift in Sources */,
F8C97859242AAE7B00A09483 /* MiaoMiao+BluetoothPeripheral.swift in Sources */,
F8EEDD5422FF685400D2D610 /* NSMutableURLRequest.swift in Sources */,
F897AAFB2201018800CDDD10 /* String.swift in Sources */,
@ -2298,6 +2327,7 @@
F8E3A2A923D906C200E5E98A /* WatchManager.swift in Sources */,
F8B3A80A227A3D11004BA588 /* TextsAlertTypeSettings.swift in Sources */,
F8F9721823A5915900C3F17D /* CGMBlueReaderTransmitter.swift in Sources */,
F8177029248ED6E600AA3600 /* LibreRawGlucoseOOPData.swift in Sources */,
F8252867243E50FE0067AF77 /* ConstantsLibre.swift in Sources */,
F8A1586D22EDB9BE007F5B5D /* ConstantsDexcomFollower.swift in Sources */,
F8F9720923A5915900C3F17D /* AESCrypt.m in Sources */,
@ -2310,7 +2340,6 @@
F8E3C3AB21FE17B700907A04 /* StringProtocol.swift in Sources */,
F8F9720723A5915900C3F17D /* AuthChallengeTxMessage.swift in Sources */,
F8B3A78E22622954004BA588 /* AlertType+CoreDataClass.swift in Sources */,
F8F9721D23A5915900C3F17D /* LibreDerivedAlgorithmRunner.swift in Sources */,
F821CF5A229BF43A005C1E43 /* CoreDataManager.swift in Sources */,
F8E51D6924549E2C001C9E5A /* SettingsViewTraceSettingsViewModel.swift in Sources */,
F85DC2F321CFE3D400B9F74A /* Calibration+CoreDataClass.swift in Sources */,
@ -2335,8 +2364,10 @@
F8B48A9422B2A705009BCC01 /* TextsSpeakReading.swift in Sources */,
F821CF5F229BF43A005C1E43 /* ApplicationManager.swift in Sources */,
F8B3A834227F08AC004BA588 /* PickerViewData.swift in Sources */,
F8177025248ED4DE00AA3600 /* Libre1DerivedAlgorithmParameters.swift in Sources */,
F808592D23677D6A00F3829D /* ChartPoint.swift in Sources */,
F8F9722823A5915900C3F17D /* BluconUtilities.swift in Sources */,
F8177027248ED57000AA3600 /* LibreRawGlucoseOOPA2Data.swift in Sources */,
F8F9724723A69A8000C3F17D /* BluetoothPeripheralCategory.swift in Sources */,
F8B3A79522635A25004BA588 /* AlertType+CoreDataProperties.swift in Sources */,
F8B3A84C227F090E004BA588 /* SettingsViewController.swift in Sources */,
@ -2351,6 +2382,7 @@
F897E25323FC871C0075E0E8 /* BluetoothPeripheralManager+WatlaaBluetoothTransmitterDelegate.swift in Sources */,
F81D6D5222C27F18005EFAE2 /* BgReading+DexcomShare.swift in Sources */,
F8F9723023A5915900C3F17D /* M5StackUtilities.swift in Sources */,
F89467F0249FFF5900F18424 /* LibreRawGlucoseOOPGlucose.swift in Sources */,
F8297F52238ECA3200D74D66 /* BluetoothPeripheralViewController.swift in Sources */,
F816E1312439E2DD009EE65B /* DexcomG4BluetoothPeripheralViewModel.swift in Sources */,
F821CF66229EE68B005C1E43 /* NightScoutFollowManager.swift in Sources */,
@ -2783,7 +2815,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.0.1;
PRODUCT_BUNDLE_IDENTIFIER = net.johandegraeve.xdripswift;
PRODUCT_BUNDLE_IDENTIFIER = net.johandegraeve.xdriptest;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "xdrip/xdrip-Bridging-Header.h";
@ -2812,7 +2844,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.0.1;
PRODUCT_BUNDLE_IDENTIFIER = net.johandegraeve.xdripswift;
PRODUCT_BUNDLE_IDENTIFIER = net.johandegraeve.xdriptest;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "xdrip/xdrip-Bridging-Header.h";

View File

@ -243,7 +243,7 @@ enum BluetoothPeripheralType: String, CaseIterable {
}
/// is it web oop enabled or not
/// is it web oop supported or not.
func canWebOOP() -> Bool {
switch self {

View File

@ -10,21 +10,25 @@ public class GlucoseData {
var glucoseLevelFiltered:Double
init(timeStamp:Date, glucoseLevelRaw:Double, glucoseLevelFiltered:Double) {
self.timeStamp = timeStamp
self.glucoseLevelRaw = glucoseLevelRaw
self.glucoseLevelFiltered = glucoseLevelFiltered
}
convenience init(timeStamp:Date, glucoseLevelRaw:Double) {
self.init(timeStamp: timeStamp, glucoseLevelRaw: glucoseLevelRaw, glucoseLevelFiltered: glucoseLevelRaw)
}
convenience init(timeStamp:Date) {
self.init(timeStamp: timeStamp, glucoseLevelRaw: 0.0, glucoseLevelFiltered: 0.0)
}
var description: String {
return "timeStamp = " + timeStamp.description(with: .current) + ", glucoseLevelRaw = " + glucoseLevelRaw.description
return "timeStamp = " + timeStamp.description(with: .current) + ", glucoseLevelRaw = " + glucoseLevelRaw.description + ", glucoseLevelFiltered = " + glucoseLevelFiltered.description
}
}

View File

@ -172,7 +172,8 @@ class CGMBluconTransmitter: BluetoothTransmitter {
}
//get readings from buffer and send to cGMTransmitterDelegate
var result = LibreDataParser.parse(libreData: rxBuffer, timeStampLastBgReading: timeStampLastBgReading)
// TODO: use LibreDataParser.libreDataProcessor and make parseLibre1DataWithoutCalibration private to LibreDataParser
var result = LibreDataParser.parseLibre1DataWithoutCalibration(libreData: rxBuffer, timeStampLastBgReading: timeStampLastBgReading)
//TODO: sort glucosedata before calling newReadingsReceived
cgmTransmitterDelegate?.cgmTransmitterInfoReceived(glucoseData: &result.glucoseData, transmitterBatteryInfo: nil, sensorTimeInMinutes: result.sensorTimeInMinutes)

View File

@ -43,11 +43,10 @@ class CGMBubbleTransmitter:BluetoothTransmitter, CGMTransmitter {
/// used as parameter in call to cgmTransmitterDelegate.cgmTransmitterInfoReceived, when there's no glucosedata to send
var emptyArray: [GlucoseData] = []
// current sensor serial number, if nil then it's not known yet
/// current sensor serial number, if nil then it's not known yet
private var sensorSerialNumber:String?
private var patchUid: String?
/// gives information about type of sensor (Libre1, Libre2, etc..)
private var patchInfo: String?
/// oop website url to use in case oop web would be enabled
@ -133,6 +132,9 @@ class CGMBubbleTransmitter:BluetoothTransmitter, CGMTransmitter {
if let value = characteristic.value {
// trace the contents, as we'll probably do much troubleshooting still
trace("in didUpdateValueFor, received data as hexstring : %{public}@", log: log, category: ConstantsLog.categoryCGMBubble, type: .info, value.hexEncodedString())
//check if buffer needs to be reset
if (Date() > startDate.addingTimeInterval(CGMBubbleTransmitter.maxWaitForpacketInSeconds - 1)) {
trace("in peripheral didUpdateValueFor, more than %{public}d seconds since last update - or first update since app launch, resetting buffer", log: log, category: ConstantsLog.categoryCGMBubble, type: .info, CGMBubbleTransmitter.maxWaitForpacketInSeconds)
@ -142,6 +144,7 @@ class CGMBubbleTransmitter:BluetoothTransmitter, CGMTransmitter {
if let firstByte = value.first {
if let bubbleResponseState = BubbleResponseType(rawValue: firstByte) {
switch bubbleResponseState {
case .dataInfo:
// get hardware, firmware and batteryPercentage
@ -161,58 +164,82 @@ class CGMBubbleTransmitter:BluetoothTransmitter, CGMTransmitter {
_ = writeDataToPeripheral(data: Data([0x02, 0x00, 0x00, 0x00, 0x00, 0x2B]), type: .withoutResponse)
case .serialNumber:
guard value.count >= 10 else { return }
rxBuffer.append(value.subdata(in: 2..<10))
patchUid = value.subdata(in: 2 ..< 10).hexEncodedString().uppercased()
case .dataPacket:
rxBuffer.append(value.suffix(from: 4))
if rxBuffer.count >= 352 {
guard let patchInfo = patchInfo else {
resetRxBuffer()
guard let libreSensorSerialNumber = LibreSensorSerialNumber(withUID: Data(rxBuffer.subdata(in: 0..<8))) else {
trace(" could not create libreSensorSerialNumber", log: self.log, category: ConstantsLog.categoryCGMBubble, type: .info)
return
}
if let libreSensorSerialNumber = LibreSensorSerialNumber(withUID: Data(rxBuffer.subdata(in: 0..<8))) {
// verify serial number and if changed inform delegate
if libreSensorSerialNumber.serialNumber != sensorSerialNumber {
// verify serial number and if changed inform delegate
if libreSensorSerialNumber.serialNumber != sensorSerialNumber {
sensorSerialNumber = libreSensorSerialNumber.serialNumber
trace(" new sensor detected : %{public}@", log: log, category: ConstantsLog.categoryCGMBubble, type: .info, libreSensorSerialNumber.serialNumber)
// inform delegate about new sensor detected
cgmTransmitterDelegate?.newSensorDetected()
cGMBubbleTransmitterDelegate?.received(serialNumber: libreSensorSerialNumber.serialNumber, from: self)
// also reset timestamp last reading, to be sure that if new sensor is started, we get historic data
timeStampLastBgReading = Date(timeIntervalSince1970: 0)
sensorSerialNumber = libreSensorSerialNumber.serialNumber
trace(" new sensor detected : %{public}@", log: log, category: ConstantsLog.categoryCGMBubble, type: .info, libreSensorSerialNumber.serialNumber)
// inform delegate about new sensor detected
cgmTransmitterDelegate?.newSensorDetected()
cGMBubbleTransmitterDelegate?.received(serialNumber: libreSensorSerialNumber.serialNumber, from: self)
// also reset timestamp last reading, to be sure that if new sensor is started, we get historic data
timeStampLastBgReading = Date(timeIntervalSince1970: 0)
}
// function used two times in next statements
let checkCrc = { guard Crc.LibreCrc(data: &self.rxBuffer, headerOffset: self.bubbleHeaderLength) else {
trace(" Libre 1 sensor, CRC check failed", log: self.log, category: ConstantsLog.categoryCGMBubble, type: .info)
return
} }
// if patchInfo != nil, then this is a Bubble that sends patchInfo.
// libre1 patchinfo first byte is "70" or "E5" - other case no CRC check required
if let patchInfo = patchInfo, patchInfo.contains(find: "70") || patchInfo.contains(find: "E5") {
checkCrc()
} else if patchInfo == nil {
// must be a Bubble that doesn't support patchInfo, so it must be a Libre1, CRC check required
checkCrc()
}
LibreDataParser.libreDataProcessor(libreSensorSerialNumber: libreSensorSerialNumber, patchInfo: patchInfo, webOOPEnabled: webOOPEnabled, oopWebSite: oopWebSite, oopWebToken: oopWebToken, libreData: (rxBuffer.subdata(in: bubbleHeaderLength..<(344 + bubbleHeaderLength))), cgmTransmitterDelegate: cgmTransmitterDelegate, timeStampLastBgReading: timeStampLastBgReading, completionHandler: { (timeStampLastBgReading: Date?, sensorState: LibreSensorState?) in
if let timeStampLastBgReading = timeStampLastBgReading {
self.timeStampLastBgReading = timeStampLastBgReading
}
}
// libre1 patchinfo first byte is "70" or "E5"
if patchInfo.contains(find: "70") || patchInfo.contains(find: "E5") {
guard Crc.LibreCrc(data: &rxBuffer, headerOffset: bubbleHeaderLength) else { return }
}
LibreDataParser.libreDataProcessor(sensorSerialNumber: sensorSerialNumber, patchUid: patchUid, patchInfo: patchInfo, webOOPEnabled: webOOPEnabled, oopWebSite: oopWebSite, oopWebToken: oopWebToken, libreData: (rxBuffer.subdata(in: bubbleHeaderLength..<(344 + bubbleHeaderLength))), cgmTransmitterDelegate: cgmTransmitterDelegate, transmitterBatteryInfo: nil, firmware: nil, hardware: nil, hardwareSerialNumber: nil, bootloader: nil, timeStampLastBgReading: timeStampLastBgReading, completionHandler: {(timeStampLastBgReading:Date) in
self.timeStampLastBgReading = timeStampLastBgReading
if let sensorState = sensorState {
self.cGMBubbleTransmitterDelegate?.received(sensorStatus: sensorState, from: self)
}
})
//reset the buffer
resetRxBuffer()
}
case .noSensor:
cgmTransmitterDelegate?.sensorNotDetected()
case .patchInfo:
if value.count >= 10 {
patchInfo = value.subdata(in: 5 ..< 11).hexEncodedString().uppercased()
}
// send libreSensorType to delegate
if let libreSensorType = LibreSensorType.type(patchInfo: patchInfo) {
cGMBubbleTransmitterDelegate?.received(libreSensorType: libreSensorType, from: self)
}
}
}
}
@ -275,7 +302,7 @@ fileprivate enum BubbleResponseType: UInt8 {
case dataInfo = 128 //0x80
case noSensor = 191 //0xBF
case serialNumber = 192 //0xC0
case patchInfo = 193
case patchInfo = 193 //0xC1
}
extension BubbleResponseType: CustomStringConvertible {

View File

@ -11,8 +11,14 @@ protocol CGMBubbleTransmitterDelegate: AnyObject {
/// received sensor Serial Number
func received(serialNumber: String, from cGMBubbleTransmitter: CGMBubbleTransmitter)
/// M5Stack is sending batteryLevel
/// Bubble is sending batteryLevel
func received(batteryLevel: Int, from cGMBubbleTransmitter: CGMBubbleTransmitter)
/// Bubble is sending type of transmitter
func received(libreSensorType: LibreSensorType, from cGMBubbleTransmitter: CGMBubbleTransmitter)
/// bubble is sending sensorStatus
func received(sensorStatus: LibreSensorState, from cGMBubbleTransmitter: CGMBubbleTransmitter)
}

View File

@ -188,11 +188,13 @@ class CGMMiaoMiaoTransmitter:BluetoothTransmitter, CGMTransmitter {
// send battery level to delegate
cGMMiaoMiaoTransmitterDelegate?.received(batteryLevel: batteryPercentage, from: self)
LibreDataParser.libreDataProcessor(sensorSerialNumber: LibreSensorSerialNumber(withUID: Data(rxBuffer.subdata(in: 5..<13)))?.serialNumber, webOOPEnabled: webOOPEnabled, oopWebSite: oopWebSite, oopWebToken: oopWebToken, libreData: (rxBuffer.subdata(in: miaoMiaoHeaderLength..<(344 + miaoMiaoHeaderLength))), cgmTransmitterDelegate: cgmTransmitterDelegate, transmitterBatteryInfo: TransmitterBatteryInfo.percentage(percentage: batteryPercentage), firmware: firmware, hardware: hardware, hardwareSerialNumber: nil, bootloader: nil, timeStampLastBgReading: timeStampLastBgReading, completionHandler: {(timeStampLastBgReading:Date) in
self.timeStampLastBgReading = timeStampLastBgReading
LibreDataParser.libreDataProcessor(libreSensorSerialNumber: LibreSensorSerialNumber(withUID: Data(rxBuffer.subdata(in: 5..<13))), patchInfo: nil, webOOPEnabled: webOOPEnabled, oopWebSite: oopWebSite, oopWebToken: oopWebToken, libreData: (rxBuffer.subdata(in: miaoMiaoHeaderLength..<(344 + miaoMiaoHeaderLength))), cgmTransmitterDelegate: cgmTransmitterDelegate, timeStampLastBgReading: timeStampLastBgReading, completionHandler: { (timeStampLastBgReading: Date?, sensorState: LibreSensorState?) in
if let timeStampLastBgReading = timeStampLastBgReading {
self.timeStampLastBgReading = timeStampLastBgReading
}
})
//reset the buffer
resetRxBuffer()

View File

@ -10,7 +10,7 @@ import Foundation
/// local algorithm use this
public struct LibreDerivedAlgorithmParameters: Codable, CustomStringConvertible {
public struct Libre1DerivedAlgorithmParameters: Codable, CustomStringConvertible {
public var slope_slope: Double
public var slope_offset: Double
public var offset_slope: Double
@ -20,19 +20,18 @@ public struct LibreDerivedAlgorithmParameters: Codable, CustomStringConvertible
public var extraOffset: Double = 0
public var serialNumber: String
/// if values all 0, is wrong parameters
var isErrorParameters: Bool {
return slope_slope == 0 &&
slope_offset == 0 &&
offset_slope == 0 &&
offset_offset == 0
}
public var description: String {
return "LibreDerivedAlgorithmParameters:: slopeslope: \(slope_slope), slopeoffset: \(slope_offset), offsetoffset: \(offset_offset), offsetSlope: \(offset_slope), extraSlope: \(extraSlope), extraOffset: \(extraOffset), isValidForFooterWithReverseCRCs: \(isValidForFooterWithReverseCRCs)"
}
public init(slope_slope: Double, slope_offset:Double, offset_slope: Double, offset_offset: Double, isValidForFooterWithReverseCRCs: Int, extraSlope: Double, extraOffset: Double, sensorSerialNumber:String) {
/// init will fail if slope_slope == 0 && slope_offset == 0 && offset_slope == 0 && offset_offset == 0
public init?(slope_slope: Double, slope_offset:Double, offset_slope: Double, offset_offset: Double, isValidForFooterWithReverseCRCs: Int, extraSlope: Double, extraOffset: Double, sensorSerialNumber:String) {
guard slope_slope == 0 &&
slope_offset == 0 &&
offset_slope == 0 &&
offset_offset == 0 else {return nil}
self.slope_slope = slope_slope
self.slope_offset = slope_offset
self.offset_slope = offset_slope
@ -41,6 +40,7 @@ public struct LibreDerivedAlgorithmParameters: Codable, CustomStringConvertible
self.extraSlope = extraSlope
self.extraOffset = extraOffset
self.serialNumber = sensorSerialNumber
}
}

View File

@ -1,8 +1,14 @@
import Foundation
import os
/// for trace
fileprivate let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryLibreDataParser)
class LibreDataParser {
/// parses libre block
// MARK: - public functions
/// parses libre1 block, without oop web.
/// - parameters:
/// - libreData: the 344 bytes block from Libre
/// - timeStampLastBgReading: this is of the timestamp of the latest reading we already received during previous session
@ -10,22 +16,21 @@ class LibreDataParser {
/// - array of GlucoseData, first is the most recent. Only returns recent readings, ie not the ones that are older than timeStampLastBgReading. 30 seconds are added here, meaning, new reading should be at least 30 seconds more recent than timeStampLastBgReading
/// - sensorState: status of the sensor
/// - sensorTimeInMinutes: age of sensor in minutes
public static func parse(libreData: Data, timeStampLastBgReading:Date) -> (glucoseData:[GlucoseData], sensorState:LibreSensorState, sensorTimeInMinutes:Int) {
public static func parseLibre1DataWithoutCalibration(libreData: Data, timeStampLastBgReading:Date) -> (glucoseData:[GlucoseData], sensorState:LibreSensorState, sensorTimeInMinutes:Int) {
var i:Int
var glucoseData:GlucoseData
var byte:Data
var timeInMinutes:Double
let ourTime:Date = Date()
let indexTrend:Int = getByteAt(buffer: libreData, position: 26) & 0xFF
let indexHistory:Int = getByteAt(buffer: libreData, position: 27) & 0xFF
let sensorTimeInMinutes:Int = 256 * (getByteAt(buffer:libreData, position: 317) & 0xFF) + (getByteAt(buffer:libreData, position: 316) & 0xFF)
let indexTrend:Int = libreData.getByteAt(position: 26) & 0xFF
let indexHistory:Int = libreData.getByteAt(position: 27) & 0xFF
let sensorTimeInMinutes:Int = 256 * (libreData.getByteAt(position: 317) & 0xFF) + (libreData.getByteAt(position: 316) & 0xFF)
let sensorStartTimeInMilliseconds:Double = ourTime.toMillisecondsAsDouble() - (Double)(sensorTimeInMinutes * 60 * 1000)
var returnValue:Array<GlucoseData> = []
let sensorState = LibreSensorState(stateByte: libreData[4])
/////// loads trend values
// we will add the most recent readings, but then we'll only add the readings that are at least 5 minutes apart (giving 10 seconds spare)
// we will add the most recent readings, but then we'll only add the readings that are at least 5 minutes apart (giving 10 seconds spare)
// for that variable timeStampLastAddedGlucoseData is used. It's initially set to now + 5 minutes
var timeStampLastAddedGlucoseData = Date().toMillisecondsAsDouble() + 5 * 60 * 1000
@ -81,81 +86,316 @@ class LibreDataParser {
}
return (returnValue, sensorState, sensorTimeInMinutes)
}
/// Function which groups common functionality used for transmitters that support the 344 Libre block. It checks if webOOP is enabled, if yes tries to use the webOOP, response is processed and delegate is called. If webOOP is not enabled, then local parsing is done.
/// Process Libre block for all types of Libre sensors, and for both with and without web oop (without only for Libre 1). It checks if webOOP is enabled, if yes tries to use the webOOP, response is processed and delegate is called. If webOOP not enabled, and if Libre1, then local processing is done, in that case glucose values are not calibrated
/// - parameters:
/// - sensorSerialNumber : if nil, then webOOP will not be used and local parsing will be done
/// - patchUid : sensor sn hex string
/// - patchInfo : will be used by server to out the glucose data
/// - libreSensorSerialNumber : if nil, then webOOP will not be used and local parsing will be done, but only for Libre 1
/// - patchInfo : will be used by server to out the glucose data, corresponds to type of sensor. Nil if not known which is used for Bubble or MM older firmware versions and also Watlaa
/// - libreData : the 344 bytes from Libre sensor
/// - timeStampLastBgReading : timestamp of last reading, older readings will be ignored
/// - webOOPEnabled : is webOOP enabled or not, if not enabled, local parsing is used
/// - timeStampLastBgReading : timestamp of last reading, older readings will be ignored. This is only used to save some processing time. If not known, set it to Date(timeIntervalSince1970: 0)
/// - webOOPEnabled : is webOOP enabled or not, if not enabled, local parsing is used. This can only be the case for Libre1
/// - oopWebSite : the site url to use if oop web would be enabled
/// - oopWebToken : the token to use if oop web would be enabled
/// - cgmTransmitterDelegate : the cgmTransmitterDelegate
/// - transmitterBatteryInfo : not mandatory, if nil then delegate will simply not receive it, possibly the delegate already received it before, and if not
/// - firmware : not mandatory, if nil then delegate will simply not receive it, possibly the delegate already received it before, and if not or maybe it doesn't exist for the specific type of transmitter
/// - hardware : not mandatory, if nil then delegate will simply not receive it, possibly the delegate already received it before, and if not or maybe it doesn't exist for the specific type of transmitter
/// - hardwareSerialNumber : not mandatory, if nil then delegate will simply not receive it, possibly the delegate already received it before, and if not or maybe it doesn't exist for the specific type of transmitter
/// - bootloader : not mandatory, if nil then delegate will simply not receive it, possibly the delegate already received it before, and if not or maybe it doesn't exist for the specific type of transmitter
/// - completionHandler : will be called when glucose data is read with as parameter the timestamp of the last reading. Goal is that caller an set timeStampLastBgReading to the new value
///
/// parameter values that are not known, simply ignore them, if they are not known then they are probably not important, or they've already been passed to the delegate before.
public static func libreDataProcessor(sensorSerialNumber: String?, patchUid: String? = nil, patchInfo: String? = nil, webOOPEnabled: Bool, oopWebSite: String?, oopWebToken: String?, libreData: Data, cgmTransmitterDelegate : CGMTransmitterDelegate?, transmitterBatteryInfo:TransmitterBatteryInfo?, firmware: String?, hardware: String?, hardwareSerialNumber: String?, bootloader:String?, timeStampLastBgReading: Date, completionHandler:@escaping ((_ timeStampLastBgReading: Date) -> ())) {
/// - cgmTransmitterDelegate : the cgmTransmitterDelegate, will be used to send the resultin glucose data and sensorTime (function cgmTransmitterInfoReceived)
/// - completionHandler : will be called when glucose data is read with as parameter the timestamp of the last reading. Goal is that caller can set timeStampLastBgReading to the new value. Also the LibreSensorState if found.
public static func libreDataProcessor(libreSensorSerialNumber: LibreSensorSerialNumber?, patchInfo: String?, webOOPEnabled: Bool, oopWebSite: String?, oopWebToken: String?, libreData: Data, cgmTransmitterDelegate : CGMTransmitterDelegate?, timeStampLastBgReading: Date, completionHandler:@escaping ((_ timeStampLastBgReading: Date?, _ sensorState: LibreSensorState?) -> ())) {
if let sensorSerialNumber = sensorSerialNumber, let oopWebSite = oopWebSite, let oopWebToken = oopWebToken, webOOPEnabled {
LibreOOPClient.handleLibreData(libreData: libreData, patchUid: patchUid, patchInfo: patchInfo, timeStampLastBgReading: timeStampLastBgReading, serialNumber: sensorSerialNumber, oopWebSite: oopWebSite, oopWebToken: oopWebToken) { (result) in
handleGlucoseData(result: result, cgmTransmitterDelegate: cgmTransmitterDelegate, transmitterBatteryInfo: transmitterBatteryInfo, firmware: firmware, hardware: hardware, hardwareSerialNumber: hardwareSerialNumber, bootloader: bootloader, sensorSerialNumber: sensorSerialNumber, completionHandler: completionHandler)
// get libreSensorType, if this fails then it must be an unknown Libre sensor type in which case we don't proceed
guard let libreSensorType = LibreSensorType.type(patchInfo: patchInfo) else {
// unwrap patchInfo, although it can't be nil here because LibreSensorType.type would have returned .libre1 otherwise
if let patchInfo = patchInfo {
// as we failed to create libreSensorType, patchInfo should not be nil
trace("in libreDataProcessor, failed to create libreSensorType, patchInfo = %{public}@", log: log, category: ConstantsLog.categoryLibreDataParser, type: .info, patchInfo)
}
return
}
// unwrap patchInfo, patchInfo must be non nill as we've already tested that
guard let patchInfo = patchInfo else {return}
// let's see if we can and must use web oop
if let libreSensorSerialNumber = libreSensorSerialNumber, let oopWebSite = oopWebSite, let oopWebToken = oopWebToken, webOOPEnabled {
switch libreSensorType {
case .libre1, .libreUS, .libreProH:// these types are all Libre 1
// get LibreDerivedAlgorithmParameters and parse using the libre1DerivedAlgorithmParameters
LibreOOPClient.getLibre1DerivedAlgorithmParameters(bytes: libreData, libreSensorSerialNumber: libreSensorSerialNumber, oopWebSite: oopWebSite, oopWebToken: oopWebToken) { (libre1DerivedAlgorithmParameters) in
// parse the data using oop web algorithm
let parsedResult = parseLibre1DataWithOOPWebCalibration(libreData: libreData, libre1DerivedAlgorithmParameters: libre1DerivedAlgorithmParameters, timeStampLastBgReading: timeStampLastBgReading)
handleGlucoseData(result: (parsedResult.libreRawGlucoseData.map { $0 as GlucoseData }, parsedResult.sensorTimeInMinutes, parsedResult.sensorState, nil), cgmTransmitterDelegate: cgmTransmitterDelegate, libreSensorSerialNumber: libreSensorSerialNumber, completionHandler: completionHandler)
}
case .libre1A2:
LibreOOPClient.getLibreRawGlucoseOOPOA2Data(libreData: libreData, oopWebSite: oopWebSite) { (libreRawGlucoseOOPA2Data) in
// TODO : check if debug tracing is enabled, to avoid to parse for description when it's not needed
trace("in libreDataProcessor, received libreRawGlucoseOOPA2Data = %{public}@", log: log, category: ConstantsLog.categoryLibreDataParser, type: .debug, libreRawGlucoseOOPA2Data.description)
// convert libreRawGlucoseOOPA2Data to (libreRawGlucoseData:[LibreRawGlucoseData], sensorState:LibreSensorState, sensorTimeInMinutes:Int?)
let parsedResult = libreRawGlucoseOOPA2Data.glucoseData(timeStampLastBgReading: timeStampLastBgReading)
handleGlucoseData(result: (parsedResult.libreRawGlucoseData.map { $0 as GlucoseData }, parsedResult.sensorTimeInMinutes, parsedResult.sensorState, nil), cgmTransmitterDelegate: cgmTransmitterDelegate, libreSensorSerialNumber: libreSensorSerialNumber, completionHandler: completionHandler)
}
case .libre2:
LibreOOPClient.getLibreRawGlucoseOOPData(libreData: libreData, libreSensorSerialNumber: libreSensorSerialNumber, patchInfo: patchInfo, oopWebSite: oopWebSite, oopWebToken: oopWebToken) { (libreRawGlucoseOOPData) in
// TODO : check if debug tracing is enabled, to avoid to parse for description when it's not needed
trace("in libreDataProcessor, received libreRawGlucoseOOPData = %{public}@", log: log, category: ConstantsLog.categoryLibreDataParser, type: .debug, libreRawGlucoseOOPData.description)
// convert libreRawGlucoseOOPData to (libreRawGlucoseData:[LibreRawGlucoseData], sensorState:LibreSensorState, sensorTimeInMinutes:Int?)
let parsedResult = libreRawGlucoseOOPData.glucoseData(timeStampLastBgReading: timeStampLastBgReading)
debuglogging("in libreDataProcessor, parsedResult.gluosedata size = " + parsedResult.libreRawGlucoseData.count.description)
handleGlucoseData(result: (parsedResult.libreRawGlucoseData.map { $0 as GlucoseData }, parsedResult.sensorTimeInMinutes, parsedResult.sensorState, nil), cgmTransmitterDelegate: cgmTransmitterDelegate, libreSensorSerialNumber: libreSensorSerialNumber, completionHandler: completionHandler)
}
}
} else if !webOOPEnabled {
// use local parser
process(libreData: libreData, timeStampLastBgReading: timeStampLastBgReading, cgmTransmitterDelegate: cgmTransmitterDelegate, transmitterBatteryInfo: transmitterBatteryInfo, firmware: firmware, hardware: hardware, hardwareSerialNumber: hardwareSerialNumber, bootloader: bootloader, sensorSerialNumber: sensorSerialNumber, completionHandler: completionHandler)
//get readings from buffer using local Libre 1 parser
let parsedLibre1Data = LibreDataParser.parseLibre1DataWithoutCalibration(libreData: libreData, timeStampLastBgReading: timeStampLastBgReading)
// handle the result
handleGlucoseData(result: (parsedLibre1Data.glucoseData, parsedLibre1Data.sensorTimeInMinutes, parsedLibre1Data.sensorState, nil), cgmTransmitterDelegate: cgmTransmitterDelegate, libreSensorSerialNumber: libreSensorSerialNumber, completionHandler: completionHandler)
} else {
// it's not a libre 1 and oop web is enabled, so there's nothing we can do
trace("in libreDataProcessor, can not continue - web oop is enabled, but there's missing info in the request", log: log, category: ConstantsLog.categoryLibreDataParser, type: .info)
}
}
}
fileprivate func getByteAt(buffer:Data, position:Int) -> Int {
// TODO: move to extension data
return Int(buffer[position])
}
fileprivate func getGlucoseRaw(bytes:Data) -> Int {
return ((256 * (getByteAt(buffer: bytes, position: 0) & 0xFF) + (getByteAt(buffer: bytes, position: 1) & 0xFF)) & 0x1FFF)
return ((256 * (bytes.getByteAt(position: 0) & 0xFF) + (bytes.getByteAt(position: 1) & 0xFF)) & 0x1FFF)
}
/// calls LibreDataParser.parse - calls handleGlucoseData
fileprivate func process(libreData: Data, timeStampLastBgReading: Date, cgmTransmitterDelegate : CGMTransmitterDelegate?, transmitterBatteryInfo:TransmitterBatteryInfo?, firmware: String?, hardware: String?, hardwareSerialNumber: String?, bootloader:String?, sensorSerialNumber:String?, completionHandler:((_ timeStampLastBgReading: Date) -> ())) {
fileprivate func trendMeasurements(bytes: Data, mostRecentReadingDate: Date, timeStampLastBgReading: Date, _ offset: Double = 0.0, slope: Double = 0.1, libre1DerivedAlgorithmParameters: Libre1DerivedAlgorithmParameters?) -> [LibreMeasurement] {
//get readings from buffer
let result = LibreDataParser.parse(libreData: libreData, timeStampLastBgReading: timeStampLastBgReading)
// let headerRange = 0..<24 // 24 bytes, i.e. 3 blocks a 8 bytes
let bodyRange = 24..<320 // 296 bytes, i.e. 37 blocks a 8 bytes
// let footerRange = 320..<344 // 24 bytes, i.e. 3 blocks a 8 bytes
// add errordescription nil, needed by handleGlucoseData
let resultWithErrorDescription: ([GlucoseData], LibreSensorState, Int, String?) = (result.glucoseData, result.sensorState, result.sensorTimeInMinutes, nil)
let body = Array(bytes[bodyRange])
let nextTrendBlock = Int(body[2])
var measurements = [LibreMeasurement]()
// Trend data is stored in body from byte 4 to byte 4+96=100 in units of 6 bytes. Index on data such that most recent block is first.
for blockIndex in 0...15 {
var index = 4 + (nextTrendBlock - 1 - blockIndex) * 6 // runs backwards
if index < 4 {
index = index + 96 // if end of ring buffer is reached shift to beginning of ring buffer
}
let range = index..<index+6
let measurementBytes = Array(body[range])
let measurementDate = mostRecentReadingDate.addingTimeInterval(Double(-60 * blockIndex))
if measurementDate > timeStampLastBgReading {
let measurement = LibreMeasurement(bytes: measurementBytes, slope: slope, offset: offset, date: measurementDate, libre1DerivedAlgorithmParameters: libre1DerivedAlgorithmParameters)
measurements.append(measurement)
}
}
return measurements
}
fileprivate func historyMeasurements(bytes: Data, timeStampLastBgReading: Date, _ offset: Double = 0.0, slope: Double = 0.1, libre1DerivedAlgorithmParameters: Libre1DerivedAlgorithmParameters?) -> [LibreMeasurement] {
// let headerRange = 0..<24 // 24 bytes, i.e. 3 blocks a 8 bytes
let bodyRange = 24..<320 // 296 bytes, i.e. 37 blocks a 8 bytes
// let footerRange = 320..<344 // 24 bytes, i.e. 3 blocks a 8 bytes
let body = Array(bytes[bodyRange])
let nextHistoryBlock = Int(body[3])
let minutesSinceStart = Int(body[293]) << 8 + Int(body[292])
let sensorStartTimeInMilliseconds:Double = Date().toMillisecondsAsDouble() - (Double)(minutesSinceStart * 60 * 1000)
var measurements = [LibreMeasurement]()
// History data is stored in body from byte 100 to byte 100+192-1=291 in units of 6 bytes. Index on data such that most recent block is first.
for blockIndex in 0..<32 {
let timeInMinutes = max(0,(Double)(abs(minutesSinceStart - 3)/15)*15 - (Double)(blockIndex*15))
var index = 100 + (nextHistoryBlock - 1 - blockIndex) * 6 // runs backwards
if index < 100 {
index = index + 192 // if end of ring buffer is reached shift to beginning of ring buffer
}
let range = index..<index+6
let measurementBytes = Array(body[range])
let measurementDate = Date(timeIntervalSince1970: sensorStartTimeInMilliseconds/1000 + timeInMinutes * 60)
if measurementDate > timeStampLastBgReading {
let measurement = LibreMeasurement(bytes: measurementBytes, slope: slope, offset: offset, minuteCounter: Int(timeInMinutes.rawValue), date: measurementDate, libre1DerivedAlgorithmParameters: libre1DerivedAlgorithmParameters)
measurements.append(measurement)
} else {
break
}
}
return measurements
handleGlucoseData(result: resultWithErrorDescription, cgmTransmitterDelegate: cgmTransmitterDelegate, transmitterBatteryInfo: transmitterBatteryInfo, firmware: firmware, hardware: hardware, hardwareSerialNumber: hardwareSerialNumber, bootloader: bootloader, sensorSerialNumber: sensorSerialNumber, completionHandler: completionHandler)
}
/// calls delegate with parameters from result
/// - parameters:
/// - result
/// - glucoseData : array of GlucoseData
/// - sensorTimeInMinutes: int
/// - errorDescription: optional
/// - sensorState: LibreSensorState
/// - cgmTransmitterDelegate: instance of CGMTransmitterDelegate, which will be called with result
/// - libreSensorSerialNumber
/// - callback which takes a data as parameter, being timeStampLastBgReading
///
/// if result.errorDescription not nil, then delegate function error will be called
fileprivate func handleGlucoseData(result: (glucoseData:[GlucoseData], sensorState: LibreSensorState, sensorTimeInMinutes:Int, errorDescription: String?), cgmTransmitterDelegate : CGMTransmitterDelegate?, transmitterBatteryInfo:TransmitterBatteryInfo?, firmware:String?, hardware:String?, hardwareSerialNumber:String?, bootloader:String?, sensorSerialNumber:String?, completionHandler:((_ timeStampLastBgReading: Date) -> ())) {
fileprivate func handleGlucoseData(result: (glucoseData:[GlucoseData], sensorTimeInMinutes:Int?, sensorState: LibreSensorState?, errorDescription: String?), cgmTransmitterDelegate : CGMTransmitterDelegate?, libreSensorSerialNumber:LibreSensorSerialNumber?, completionHandler:((_ timeStampLastBgReading: Date?, _ sensorState: LibreSensorState?) -> ())) {
if let errorDescription = result.errorDescription {
cgmTransmitterDelegate?.error(message: "Web OOP : " + errorDescription)
} else {
var result = result
cgmTransmitterDelegate?.cgmTransmitterInfoReceived(glucoseData: &result.glucoseData, transmitterBatteryInfo: transmitterBatteryInfo, sensorTimeInMinutes: result.sensorTimeInMinutes)
// if result.errorDescription not nil, then send it to the delegate
guard result.errorDescription == nil else {
//set timeStampLastBgReading to timestamp of latest reading in the response so that next time we parse only the more recent readings
if result.glucoseData.count > 0 {
completionHandler(result.glucoseData[0].timeStamp)
}
// in case weboop is not used then result.errorDescription is always nil, that's how it's code here. So we can
cgmTransmitterDelegate?.error(message: "Web OOP : " + result.errorDescription!)
return
}
// if sensor time < 60, return an empty glucose data array
if let sensorTimeInMinutes = result.sensorTimeInMinutes {
guard sensorTimeInMinutes >= 60 else {
var emptyArray = [GlucoseData]()
cgmTransmitterDelegate?.cgmTransmitterInfoReceived(glucoseData: &emptyArray, transmitterBatteryInfo: nil, sensorTimeInMinutes: result.sensorTimeInMinutes)
return
}
}
// call delegate with result
var result = result
cgmTransmitterDelegate?.cgmTransmitterInfoReceived(glucoseData: &result.glucoseData, transmitterBatteryInfo: nil, sensorTimeInMinutes: result.sensorTimeInMinutes)
//set timeStampLastBgReading to timestamp of latest reading in the response so that next time we parse only the more recent readings
completionHandler(result.glucoseData.count > 0 ? result.glucoseData[0].timeStamp : nil, result.sensorState)
}
/// to glucose data
/// - Parameter measurements: array of LibreMeasurement
/// - Returns: array of LibreRawGlucoseData
fileprivate func trendToLibreGlucose(_ measurements: [LibreMeasurement]) -> [LibreRawGlucoseData] {
var origarr = [LibreRawGlucoseData]()
for trend in measurements {
let glucose = LibreRawGlucoseData.init(timeStamp: trend.date, glucoseLevelRaw: trend.temperatureAlgorithmGlucose)
origarr.append(glucose)
}
return origarr
}
fileprivate func historyToLibreGlucose(_ measurements: [LibreMeasurement]) -> [LibreRawGlucoseData] {
var origarr = [LibreRawGlucoseData]()
for history in measurements {
let glucose = LibreRawGlucoseData(timeStamp: history.date, unsmoothedGlucose: history.temperatureAlgorithmGlucose)
origarr.append(glucose)
}
return origarr
}
/// parses libre 1 block with OOP WEB.
/// - parameters:
/// - libreData: the 344 bytes block from Libre
/// - timeStampLastBgReading: this is of the timestamp of the latest reading we already received during previous session
/// - returns:
/// - array of libreRawGlucoseData, first is the most recent. Only returns recent readings, ie not the ones that are older than timeStampLastBgReading. 30 seconds are added here, meaning, new reading should be at least 30 seconds more recent than timeStampLastBgReading
/// - sensorState: status of the sensor
/// - sensorTimeInMinutes: age of sensor in minutes
fileprivate func parseLibre1DataWithOOPWebCalibration(libreData: Data, libre1DerivedAlgorithmParameters: Libre1DerivedAlgorithmParameters, timeStampLastBgReading: Date) -> (libreRawGlucoseData:[LibreRawGlucoseData], sensorState:LibreSensorState, sensorTimeInMinutes:Int) {
// initialise returnvalue, array of LibreRawGlucoseData
var finalResult:[LibreRawGlucoseData] = []
// calculate sensorState
let sensorState = LibreSensorState(stateByte: libreData[4])
let sensorTimeInMinutes:Int = 256 * (Int)(libreData.uint8(position: 317) & 0xFF) + (Int)(libreData.uint8(position: 316) & 0xFF)
// iterates through glucoseData, compares timestamp, if still higher than timeStampLastBgReading (+ 30 seconds) then adds it to finalResult
let processGlucoseData = { (glucoseData: [LibreRawGlucoseData], timeStampLastAddedGlucoseData: Date) in
var timeStampLastAddedGlucoseDataAsDouble = timeStampLastAddedGlucoseData.toMillisecondsAsDouble()
for glucose in glucoseData {
let timeStampOfNewGlucoseData = glucose.timeStamp
if timeStampOfNewGlucoseData.toMillisecondsAsDouble() > (timeStampLastBgReading.toMillisecondsAsDouble() + 30000.0) {
// return only readings that are at least 5 minutes away from each other, except the first, same approach as in LibreDataParser.parse
if timeStampOfNewGlucoseData.toMillisecondsAsDouble() < timeStampLastAddedGlucoseDataAsDouble - (5 * 60 * 1000 - 10000) {
timeStampLastAddedGlucoseDataAsDouble = timeStampOfNewGlucoseData.toMillisecondsAsDouble()
finalResult.append(glucose)
}
} else {
break
}
}
}
// get last16 from trend data
// latest reading will get date of now
let last16 = trendMeasurements(bytes: libreData, mostRecentReadingDate: Date(), timeStampLastBgReading: timeStampLastBgReading, libre1DerivedAlgorithmParameters: libre1DerivedAlgorithmParameters)
// process last16, new readings should be smaller than now + 5 minutes
processGlucoseData(trendToLibreGlucose(last16), Date(timeIntervalSinceNow: 5 * 60))
// get last32 from history data
let last32 = historyMeasurements(bytes: libreData, timeStampLastBgReading: timeStampLastBgReading, libre1DerivedAlgorithmParameters: libre1DerivedAlgorithmParameters)
// process last 32 with date earlier than the earliest in last16
var timeStampLastAddedGlucoseData = Date()
if last16.count > 0, let last = last16.last {
timeStampLastAddedGlucoseData = last.date
}
processGlucoseData(historyToLibreGlucose(last32), timeStampLastAddedGlucoseData)
return (finalResult, sensorState, sensorTimeInMinutes)
}

View File

@ -0,0 +1,21 @@
import Foundation
class LibreHistoricGlucoseA2: NSObject, Codable {
/// if quality != 0, it means the value is error
let quality : Int?
/// the value's sensor time
let time: Int?
/// glucose value
let bg : Double?
/// description
override var description: String {
return "LibreHistoricGlucoseA2 = \nquality = " + (quality != nil ? quality!.description : "nil") + ", time = " + (time != nil ? time!.description : "nil") + ", bg = " + (bg != nil ? bg!.description : "nil")
}
}

View File

@ -11,67 +11,65 @@ import Foundation
/// Structure for one glucose measurement including value, date and raw data bytes
struct LibreMeasurement {
/// The date for this measurement
let date: Date
/// The minute counter for this measurement
let counter: Int
/// The bytes as read from the sensor. All data is derived from this \"raw data"
let bytes: [UInt8]
/// The bytes as String
let byteString: String
private let minuteCounter: Int
/// The raw glucose as read from the sensor
let rawGlucose: Int
/// The raw temperature as read from the sensor
let rawTemperature: Int
/// slope to calculate glucose from raw value in (mg/dl)/raw
let slope: Double
/// glucose offset to be added in mg/dl
let offset: Double
/// The glucose value in mg/dl
let glucose: Double
/// Initialize a new glucose measurement
// let slope_slope: Double = 0.0
// let slope_offset: Double = 0.0
// let offset_slope: Double = 0.0
// let offset_offset: Double = 0.0
private let glucose: Double
let temperatureAlgorithmGlucose: Double
// {"status":"complete","slope_slope":0.00001816666666666667,"slope_offset":-0.00016666666666666666,"offset_offset":-21.5,"offset_slope":0.007499999999999993,"uuid":"calibrationmetadata-e61686dd-1305-44f0-a675-df98aabce67f","isValidForFooterWithReverseCRCs":61141}
let oopSlope: Double
let oopOffset: Double
///
let temperatureAlgorithmParameterSet: LibreDerivedAlgorithmParameters?
private let oopSlope: Double
private let oopOffset: Double
private let temperatureAlgorithmParameterSet: Libre1DerivedAlgorithmParameters?
///
/// - parameter bytes: raw data bytes as read from the sensor
/// - parameter slope: slope to calculate glucose from raw value in (mg/dl)/raw
/// - parameter offset: glucose offset to be added in mg/dl
/// - parameter date: date of the measurement
///
/// - returns: Measurement
init(bytes: [UInt8], slope: Double = 0.1, offset: Double = 0.0, minuteCounter: Int = 0, date: Date, LibreDerivedAlgorithmParameterSet: LibreDerivedAlgorithmParameters? = nil) {
self.bytes = bytes
self.byteString = bytes.reduce("", {$0 + String(format: "%02X", arguments: [$1])})
/// - parameters :
/// - bytes: raw data bytes as read from the sensor
/// - slope: slope to calculate glucose from raw value in (mg/dl)/raw
/// - offset: glucose offset to be added in mg/dl
/// - date: date of the measurement
/// - minuteCounter : minute counter of this measurement
/// - libre1DerivedAlgorithmParameters : Libre1DerivedAlgorithmParameters for the sensor
init(bytes: [UInt8], slope: Double = 0.1, offset: Double = 0.0, minuteCounter: Int = 0, date: Date, libre1DerivedAlgorithmParameters: Libre1DerivedAlgorithmParameters? = nil) {
self.rawGlucose = (Int(bytes[1] & 0x1F) << 8) + Int(bytes[0]) // switched to 13 bit mask on 2018-03-15
self.rawTemperature = (Int(bytes[4] & 0x3F) << 8) + Int(bytes[3]) // 14 bit-mask for raw temperature
self.slope = slope
self.offset = offset
self.glucose = offset + slope * Double(rawGlucose)
self.date = date
self.counter = minuteCounter
self.minuteCounter = minuteCounter
// local algorithm
self.temperatureAlgorithmParameterSet = LibreDerivedAlgorithmParameterSet
if let LibreDerivedAlgorithmParameterSet = self.temperatureAlgorithmParameterSet {
self.oopSlope = LibreDerivedAlgorithmParameterSet.slope_slope * Double(rawTemperature) + LibreDerivedAlgorithmParameterSet.offset_slope
self.oopOffset = LibreDerivedAlgorithmParameterSet.slope_offset * Double(rawTemperature) + LibreDerivedAlgorithmParameterSet.offset_offset
self.temperatureAlgorithmParameterSet = libre1DerivedAlgorithmParameters
if let libreDerivedAlgorithmParameterSet = self.temperatureAlgorithmParameterSet {
self.oopSlope = libreDerivedAlgorithmParameterSet.slope_slope * Double(rawTemperature) + libreDerivedAlgorithmParameterSet.offset_slope
self.oopOffset = libreDerivedAlgorithmParameterSet.slope_offset * Double(rawTemperature) + libreDerivedAlgorithmParameterSet.offset_offset
// self.oopSlope = slope_slope * Double(rawTemperature) + slope_offset
// self.oopOffset = offset_slope * Double(rawTemperature) + offset_offset
let oopGlucose = oopSlope * Double(rawGlucose) + oopOffset
//self.temperatureAlgorithmGlucose = oopGlucose
// Final correction, if sensor values are very low and need to be compensated
self.temperatureAlgorithmGlucose = oopGlucose * LibreDerivedAlgorithmParameterSet.extraSlope + LibreDerivedAlgorithmParameterSet.extraOffset
self.temperatureAlgorithmGlucose = oopGlucose * libreDerivedAlgorithmParameterSet.extraSlope + libreDerivedAlgorithmParameterSet.extraOffset
} else {
self.oopSlope = 0
self.oopOffset = 0
@ -79,14 +77,4 @@ struct LibreMeasurement {
}
}
var description: String {
var aString = String("Glucose: \(glucose) (mg/dl), date: \(date), slope: \(slope), offset: \(offset), rawGlucose: \(rawGlucose), rawTemperature: \(rawTemperature), bytes: \(bytes) \n")
aString.append("OOP: slope_slope: \(String(describing: temperatureAlgorithmParameterSet?.slope_slope)), slope_offset: \(String(describing: temperatureAlgorithmParameterSet?.slope_offset)), offset_slope: \(String(describing: temperatureAlgorithmParameterSet?.offset_slope)), offset_offset: \(String(describing: temperatureAlgorithmParameterSet?.offset_offset))\n")
aString.append("OOP: slope: \(oopSlope), \noffset: \(oopOffset)")
aString.append("oopGlucose: \(temperatureAlgorithmGlucose) (mg/dl)" )
return aString
}
}

View File

@ -18,483 +18,253 @@
import Foundation
import os
import SpriteKit
import UserNotifications
public class LibreOOPClient {
private static let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryLibreOOPClient)
class LibreOOPClient {
// MARK: - properties
/// for trace
private static let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryLibreOOPClient)
/// get the libre glucose data by server
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - patchUid : sensor sn hex string
/// - libreSensorSerialNumber : sensor sn
/// - patchInfo : will be used by server to out the glucose data
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - callback: server data that contains the 344 bytes details
static func webOOP(libreData: [UInt8], patchUid: String, patchInfo: String, oopWebSite: String, oopWebToken: String, callback: ((LibreRawGlucoseOOPData?) -> Void)?) {
let bytesAsData = Data(bytes: libreData, count: libreData.count)
/// - callback: server data that contains the 344 bytes details, is called in DispatchQueue.main.async
static func getLibreRawGlucoseOOPData(libreData: Data, libreSensorSerialNumber: LibreSensorSerialNumber, patchInfo: String, oopWebSite: String, oopWebToken: String, callback:@escaping (LibreRawGlucoseOOPData) -> Void) {
let item = URLQueryItem(name: "accesstoken", value: oopWebToken)
let item1 = URLQueryItem(name: "patchUid", value: patchUid)
let item1 = URLQueryItem(name: "patchUid", value: libreSensorSerialNumber.uidString.uppercased())
let item2 = URLQueryItem(name: "patchInfo", value: patchInfo)
let item3 = URLQueryItem(name: "content", value: bytesAsData.hexEncodedString())
let item3 = URLQueryItem(name: "content", value: libreData.hexEncodedString())
var urlComponents = URLComponents(string: "\(oopWebSite)/libreoop2")!
urlComponents.queryItems = [item, item1, item2, item3]
if let uploadURL = URL.init(string: urlComponents.url?.absoluteString.removingPercentEncoding ?? "") {
let request = NSMutableURLRequest(url: uploadURL)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
DispatchQueue.main.async {
guard let data = data else {
callback?(nil)
trace("in getLibreRawGlucoseOOPData, data is nil", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error)
return
}
let decoder = JSONDecoder.init()
if let oopValue = try? decoder.decode(LibreRawGlucoseOOPData.self, from: data) {
callback?(oopValue)
} else {
callback?(nil)
// trace the data as String, level debug
if let dataAsString = String(bytes: data, encoding: .utf8) {
trace("in getLibreRawGlucoseOOPData, data received frop oop web server = %{public}@", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .debug, dataAsString)
}
let decoder = JSONDecoder()
if let oopValue = try? decoder.decode(LibreRawGlucoseOOPData.self, from: data) {
callback(oopValue)
} else {
// json parsing failed
trace("in getLibreRawGlucoseOOPData, could not do json parsing", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error)
// if response is not nil then trace
if let response = String(data: data, encoding: String.Encoding.utf8) {
trace("in getLibreRawGlucoseOOPData, data as string = %{public}@", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error, response)
}
return
}
}
}
task.resume()
} else {
callback?(nil)
}
}
/// if server failed, will parse the 344 bytes by `LibreMeasurement`
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - params: local algorithm use this
/// - timeStampLastBgReading: timestamp of last reading, older readings will be ignored
/// - Returns: glucose data
static func oopParams(libreData: [UInt8], params: LibreDerivedAlgorithmParameters, timeStampLastBgReading: Date) -> [LibreRawGlucoseData] {
// get current glucose from 344 bytes
let last16 = trendMeasurements(bytes: libreData, date: Date(), timeStampLastBgReading: timeStampLastBgReading, LibreDerivedAlgorithmParameterSet: params)
if let glucoseData = trendToLibreGlucose(last16), let first = glucoseData.first {
// get histories from 344 bytes
let last32 = historyMeasurements(bytes: libreData, date: first.timeStamp, LibreDerivedAlgorithmParameterSet: params)
// every 15 minutes apart
let glucose32 = trendToLibreGlucose(last32) ?? []
// every 5 minutes apart, fill data by `SKKeyframeSequence`
let last96 = split(current: first, glucoseData: glucose32.reversed())
return last96
} else {
return []
}
}
/// get the parameters and local parse
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - serialNumber: sensor serial number
/// - timeStampLastBgReading: timestamp of last reading, older readings will be ignored
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - callback: will be called when glucose data is read with as parameter the timestamp of the last reading.
static func oop(libreData: [UInt8], serialNumber: String, timeStampLastBgReading: Date, oopWebSite: String, oopWebToken: String, _ callback: @escaping ((glucoseData: [GlucoseData], sensorState: LibreSensorState, sensorTimeInMinutes: Int, errorDescription: String?)) -> Void) {
let sensorState = LibreSensorState(stateByte: libreData[4])
let body = Array(libreData[24 ..< 320])
let sensorTime = Int(body[293]) << 8 + Int(body[292])
// if sensor time < 60, it can not get the parameters from the server
guard sensorTime >= 60 else {
callback(([], .starting, sensorTime, nil))
return
}
// get LibreDerivedAlgorithmParameters
calibrateSensor(bytes: [UInt8](libreData), serialNumber: serialNumber, oopWebSite: oopWebSite, oopWebToken: oopWebToken) {
(calibrationparams) in
callback((oopParams(libreData: [UInt8](libreData), params: calibrationparams, timeStampLastBgReading: timeStampLastBgReading),
sensorState,
sensorTime, nil))
}
}
/// if `patchInfo.hasPrefix("A2")`, server uses another arithmetic to handle the 344 bytes
/// if `patchInfo.hasPrefix("A2") 'Libre 1 A2', server uses another arithmetic to handle the 344 bytes
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - libreData: the 344 bytes from Libre sensor
/// - oopWebSite: the site url to use if oop web would be enabled
/// - callback: server data that contains the 344 bytes details
static func handleLibreA2Data(libreData: [UInt8], oopWebSite: String, callback: ((LibreRawGlucoseOOPA2Data?) -> Void)?) {
let bytesAsData = Data(bytes: libreData, count: libreData.count)
if let uploadURL = URL.init(string: "\(oopWebSite)/callnox") {
static func getLibreRawGlucoseOOPOA2Data (libreData: Data, oopWebSite: String, callback:@escaping (LibreRawGlucoseOOPA2Data) -> Void) {
if let uploadURL = URL(string: "\(oopWebSite)/callnox") {
do {
var request = URLRequest(url: uploadURL)
request.httpMethod = "POST"
let data = try JSONSerialization.data(withJSONObject: [["timestamp": "\(Int(Date().timeIntervalSince1970 * 1000))",
"content": bytesAsData.hexEncodedString()]], options: [])
"content": libreData.hexEncodedString()]], options: [])
let string = String.init(data: data, encoding: .utf8)
let json: [String: String] = ["userId": "1",
"list": string!]
request.setBodyContent(contentMap: json)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
do {
guard let data = data else {
callback?(nil)
DispatchQueue.main.async {
if let error = error {
trace("in getLibreRawGlucoseOOPOA2Data, error is not nil, error = %{public}@", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error, error.localizedDescription)
return
}
let decoder = JSONDecoder.init()
let oopValue = try decoder.decode(LibreRawGlucoseOOPA2Data.self, from: data)
callback?(oopValue)
} catch {
callback?(nil)
}
}
task.resume()
} catch {
callback?(nil)
}
}
}
/// handle server data from two functions `webOOP` and `handleLibreA2Data`, parse the data to glucse data
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - patchInfo: sensor sn hex string
/// - oopValue: parsed value
/// - timeStampLastBgReading: timestamp of last reading, older readings will be ignored
/// - serialNumber: serial number
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - callback: will be called when glucose data is read with as parameter the timestamp of the last reading.
static func handleGlucose(libreData: [UInt8], patchInfo: String, oopValue: LibreRawGlucoseWeb?, timeStampLastBgReading: Date, serialNumber: String, oopWebSite: String, oopWebToken: String, _ callback: @escaping ((glucoseData: [GlucoseData], sensorState: LibreSensorState, sensorTimeInMinutes: Int, errorDescription: String?)) -> Void) {
if let oopValue = oopValue, !oopValue.isError {
if oopValue.valueError {
if oopValue.sensorState == .notYetStarted {
callback(([], .notYetStarted, 0, nil))
} else {
callback(([], .failure, 0, nil))
}
} else {
if oopValue.canGetParameters {
if UserDefaults.standard.algorithmParameters?.serialNumber != serialNumber {
calibrateSensor(bytes: [UInt8](libreData), serialNumber: serialNumber, oopWebSite: oopWebSite, oopWebToken: oopWebToken, callback: {_ in })
guard let data = data else {
trace("in getLibreRawGlucoseOOPOA2Data, data is nil", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error)
return
}
let decoder = JSONDecoder()
do {
let oopValue = try decoder.decode(LibreRawGlucoseOOPA2Data.self, from: data)
callback(oopValue)
} catch {
// json parsing failed
trace("in getLibreRawGlucoseOOPOA2Data, could not do json parsing", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error)
// if response is not nil then trace
if let response = String(data: data, encoding: String.Encoding.utf8) {
trace("in getLibreRawGlucoseOOPOA2Data, data as string = %{public}@", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error, response)
}
return
}
}
}
if let time = oopValue.sensorTime {
var last96 = [LibreRawGlucoseData]()
let value = oopValue.glucoseData(date: Date())
last96 = split(current: value.0, glucoseData: value.1)
task.resume()
} catch let error {
trace(" failed to upload, error = %{public}@", log: log, category: ConstantsLog.categoryDexcomShareUploadManager, type: .info, error.localizedDescription)
return
if time < 20880 {
callback((last96, oopValue.sensorState, time, nil))
} else {
callback(([], .expired, time, nil))
}
} else {
callback(([], oopValue.sensorState, 0, nil))
}
}
} else {
// only patchInfo `hasPrefix` "70" and "E5" can use the local parse
if patchInfo.hasPrefix("70") || patchInfo.hasPrefix("E5") {
oop(libreData: libreData, serialNumber: serialNumber, timeStampLastBgReading: timeStampLastBgReading, oopWebSite: oopWebSite, oopWebToken: oopWebToken, callback)
} else {
callback(([], .failure, 0, nil))
}
}
}
/// handle 344 bytes
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - patchUid: sensor sn hex string
/// - patchInfo: will be used by server to out the glucose data
/// - timeStampLastBgReading: timestamp of last reading, older readings will be ignored
/// - serialNumber: serial number
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - callback: will be called when glucose data is read with as parameter the timestamp of the last reading.
static func handleLibreData(libreData: Data, patchUid: String?, patchInfo: String?, timeStampLastBgReading: Date, serialNumber: String, oopWebSite: String, oopWebToken: String, _ callback: @escaping ((glucoseData: [GlucoseData], sensorState: LibreSensorState, sensorTimeInMinutes: Int, errorDescription: String?)) -> Void) {
let bytes = [UInt8](libreData)
guard let patchUid = patchUid, let patchInfo = patchInfo else {
oop(libreData: bytes, serialNumber: serialNumber, timeStampLastBgReading: timeStampLastBgReading, oopWebSite: oopWebSite, oopWebToken: oopWebToken, callback)
return
}
// if patchInfo.hasPrefix("A2"), server uses another arithmetic to handle the 344 bytes
if patchInfo.hasPrefix("A2") {
handleLibreA2Data(libreData: bytes, oopWebSite: oopWebSite) { (data) in
DispatchQueue.main.async {
handleGlucose(libreData: bytes, patchInfo: patchInfo, oopValue: data, timeStampLastBgReading: timeStampLastBgReading, serialNumber: serialNumber, oopWebSite: oopWebSite, oopWebToken: oopWebToken, callback)
}
}
} else {
DispatchQueue.main.async {
webOOP(libreData: bytes, patchUid: patchUid, patchInfo: patchInfo, oopWebSite: oopWebSite, oopWebToken: oopWebToken) { (data) in
handleGlucose(libreData: bytes, patchInfo: patchInfo, oopValue: data, timeStampLastBgReading: timeStampLastBgReading, serialNumber: serialNumber, oopWebSite: oopWebSite, oopWebToken: oopWebToken, callback)
}
}
}
}
/// 15 minutes apart to 5 minutes apart
/// - Parameters:
/// - current: current glucose
/// - glucoseData: histories
/// - Returns: contains current glucose and histories, 5 minutes apart
static func split(current: LibreRawGlucoseData?, glucoseData: [LibreRawGlucoseData]) -> [LibreRawGlucoseData] {
var x = [Double]()
var y = [Double]()
if let current = current {
let timeInterval = current.timeStamp.timeIntervalSince1970 * 1000
x.append(timeInterval)
y.append(current.glucoseLevelRaw)
}
for glucose in glucoseData.reversed() {
let time = glucose.timeStamp.timeIntervalSince1970 * 1000
x.insert(time, at: 0)
y.insert(glucose.glucoseLevelRaw, at: 0)
}
let startTime = x.first ?? 0
let endTime = x.last ?? 0
// add glucoses to `SKKeyframeSequence`
let frameS = SKKeyframeSequence.init(keyframeValues: y, times: x as [NSNumber])
frameS.interpolationMode = .spline
var items = [LibreRawGlucoseData]()
var ptime = endTime
while ptime >= startTime {
// get value from SKKeyframeSequence
let value = (frameS.sample(atTime: CGFloat(ptime)) as? Double) ?? 0
let item = LibreRawGlucoseData.init(timeStamp: Date.init(timeIntervalSince1970: ptime / 1000), glucoseLevelRaw: value)
items.append(item)
ptime -= 300000
}
return items
}
/// get the `LibreDerivedAlgorithmParameters`
/// get the `Libre1DerivedAlgorithmParameters for Libre1 Sensor, either from UserDefaults (if already fetched earlier for that sensor), or from oopWeb. If oopWeb fetch fails, then default values are used
/// - Parameters:
/// - bytes: the 344 bytes from Libre sensor
/// - serialNumber: serial number
/// - libreSensorSerialNumber: LibreSensorSerialNumber is a structure that hold the serial number
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - callback: return `LibreDerivedAlgorithmParameters`
public static func calibrateSensor(bytes: [UInt8], serialNumber: String, oopWebSite: String, oopWebToken: String, callback: @escaping (LibreDerivedAlgorithmParameters) -> Void) {
// the parameters of one sensor will not be changed, if have cached it, get it from userdefaults
if let parameters = UserDefaults.standard.algorithmParameters {
if parameters.serialNumber == serialNumber {
callback(parameters)
/// - callback: takes LibreDerivedAlgorithmParameters`as parameter, will not be called if there's no result for instance because oop web server can not be reached
static func getLibre1DerivedAlgorithmParameters(bytes: Data, libreSensorSerialNumber: LibreSensorSerialNumber, oopWebSite: String, oopWebToken: String, callback: @escaping (Libre1DerivedAlgorithmParameters) -> Void) {
// the parameters of one sensor will not be changed, if the values are already available in userdefaults , then use those values
if let libre1DerivedAlgorithmParameters = UserDefaults.standard.libre1DerivedAlgorithmParameters {
if libre1DerivedAlgorithmParameters.serialNumber == libreSensorSerialNumber.serialNumber {
callback(libre1DerivedAlgorithmParameters)
return
}
}
/// default parameters
let params = LibreDerivedAlgorithmParameters.init(slope_slope: 0.00001816666666666667,
slope_offset: -0.00016666666666666666,
offset_slope: 0.007499999999999993,
offset_offset: -21.5,
isValidForFooterWithReverseCRCs: 1,
extraSlope: 1.0,
extraOffset: 0.0,
sensorSerialNumber: serialNumber)
post(bytes: bytes, oopWebSite: oopWebSite, oopWebToken: oopWebToken, { (data, str, can) in
let decoder = JSONDecoder()
do {
let response = try decoder.decode(GetCalibrationStatus.self, from: data)
if let slope = response.slope {
var libreDerivedAlgorithmParameters = LibreDerivedAlgorithmParameters.init(slope_slope: slope.slopeSlope ?? 0,
slope_offset: slope.slopeOffset ?? 0,
offset_slope: slope.offsetSlope ?? 0,
offset_offset: slope.offsetOffset ?? 0,
isValidForFooterWithReverseCRCs: Int(slope.isValidForFooterWithReverseCRCs ?? 1),
extraSlope: 1.0,
extraOffset: 0.0,
sensorSerialNumber: serialNumber)
libreDerivedAlgorithmParameters.serialNumber = serialNumber
if !libreDerivedAlgorithmParameters.isErrorParameters {
UserDefaults.standard.algorithmParameters = libreDerivedAlgorithmParameters
callback(libreDerivedAlgorithmParameters)
} else {
// server values all 0, `isErrorParameters` is true
// return the default parameters
callback(params)
}
} else {
// encoding data failed, no need to handle as an error, it means probably next time a new post will be done to the oop web server
trace("in calibrateSensor, slope is nil", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error)
// return the default parameters
callback(params)
}
} catch {
// encoding data failed, return the default parameters
callback(params)
trace("in calibrateSensor, error while encoding data : %{public}@", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error, error.localizedDescription)
}
})
}
/// get `LibreDerivedAlgorithmParameters` from server
/// - Parameters:
/// - bytes: the 344 bytes from Libre sensor
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - completion: network result
static func post(bytes: [UInt8], oopWebSite: String, oopWebToken: String,_ completion:@escaping (( _ data_: Data, _ response: String, _ success: Bool ) -> Void)) {
let date = Date().toMillisecondsAsInt64()
let bytesAsData = Data(bytes: bytes, count: bytes.count)
// calibration parameters not available yet, get them from oopWebSite
let json: [String: String] = [
"token": oopWebToken,
"content": "\(bytesAsData.hexEncodedString())",
"timestamp": "\(date)",
"content": "\(bytes.hexEncodedString())",
"timestamp": "\(Date().toMillisecondsAsInt64())",
]
if let uploadURL = URL.init(string: "\(oopWebSite)/calibrateSensor") {
if let uploadURL = URL(string: "\(oopWebSite)/calibrateSensor") {
var request = URLRequest(url: uploadURL)
request.httpMethod = "POST"
request.setBodyContent(contentMap: json)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in
guard let data = data else {
DispatchQueue.main.sync {
completion("network error".data(using: .utf8)!, "network error", false)
}
return
DispatchQueue.main.async {
}
if let response = String(data: data, encoding: String.Encoding.utf8) {
DispatchQueue.main.sync {
completion(data, response, true)
if let error = error {
trace("in getOopWebCalibrationStatus, received error : %{public}@", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error, error.localizedDescription)
return
}
return
}
DispatchQueue.main.sync {
completion("response error".data(using: .utf8)!, "response error", false)
guard let data = data else {
trace("in getOopWebCalibrationStatus, data is nil", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error)
return
}
// data is not nil, let's try to do json decoding
let decoder = JSONDecoder()
do {
let response = try decoder.decode(OopWebCalibrationStatus.self, from: data)
if let slope = response.slope {
if let libreDerivedAlgorithmParameters = Libre1DerivedAlgorithmParameters(slope_slope: slope.slopeSlope ?? 0, slope_offset: slope.slopeOffset ?? 0, offset_slope: slope.offsetSlope ?? 0, offset_offset: slope.offsetOffset ?? 0, isValidForFooterWithReverseCRCs: Int(slope.isValidForFooterWithReverseCRCs ?? 1), extraSlope: 1.0, extraOffset: 0.0, sensorSerialNumber: libreSensorSerialNumber.serialNumber) {
// store result in UserDefaults, next time, server will not be used anymore, we will use the stored value
UserDefaults.standard.libre1DerivedAlgorithmParameters = libreDerivedAlgorithmParameters
callback(libreDerivedAlgorithmParameters)
}
}
} catch {
// json parsing failed
trace("in calibrateSensor, could not do json parsing", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error)
// if response is not nil then trace
if let response = String(data: data, encoding: String.Encoding.utf8) {
trace(" data as string = %{public}@", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error, response)
}
}
}
}
task.resume()
}
}
/// current glucose value from 344 bytes
/// - Parameters:
/// - bytes: the 344 bytes from Libre sensor
/// - date: the current date
/// - timeStampLastBgReading: timestamp of last reading, older readings will be ignored
/// - offset: glucose offset to be added in mg/dl
/// - slope: slope to calculate glucose from raw value in (mg/dl)/raw
/// - LibreDerivedAlgorithmParameterSet: algorithm parameters
/// - Returns: return parsed values
static func trendMeasurements(bytes: [UInt8], date: Date, timeStampLastBgReading: Date, _ offset: Double = 0.0, slope: Double = 0.1, LibreDerivedAlgorithmParameterSet: LibreDerivedAlgorithmParameters?) -> [LibreMeasurement] {
guard bytes.count >= 320 else { return [] }
// let headerRange = 0..<24 // 24 bytes, i.e. 3 blocks a 8 bytes
let bodyRange = 24 ..< 320 // 296 bytes, i.e. 37 blocks a 8 bytes
// let footerRange = 320..<344 // 24 bytes, i.e. 3 blocks a 8 bytes
let body = Array(bytes[bodyRange])
let nextTrendBlock = Int(body[2])
var measurements = [LibreMeasurement]()
// Trend data is stored in body from byte 4 to byte 4+96=100 in units of 6 bytes. Index on data such that most recent block is first.
for blockIndex in 0 ... 15 {
var index = 4 + (nextTrendBlock - 1 - blockIndex) * 6 // runs backwards
if index < 4 {
index = index + 96 // if end of ring buffer is reached shift to beginning of ring buffer
}
guard index + 6 < body.count else { break }
let range = index ..< index + 6
let measurementBytes = Array(body[range])
let measurementDate = date.addingTimeInterval(Double(-60 * blockIndex))
if measurementDate > timeStampLastBgReading {
let measurement = LibreMeasurement(bytes: measurementBytes, slope: slope, offset: offset, date: measurementDate, LibreDerivedAlgorithmParameterSet: LibreDerivedAlgorithmParameterSet)
measurements.append(measurement)
}
task.resume()
}
return measurements
}
/// histories for 344 bytes
/// - Parameters:
/// - bytes: the 344 bytes from Libre sensor
/// - date: the current date
/// - offset: glucose offset to be added in mg/dl
/// - slope: slope to calculate glucose from raw value in (mg/dl)/raw
/// - LibreDerivedAlgorithmParameterSet: algorithm parameters
/// - Returns: return parsed values
static func historyMeasurements(bytes: [UInt8], date: Date, _ offset: Double = 0.0, slope: Double = 0.1, LibreDerivedAlgorithmParameterSet: LibreDerivedAlgorithmParameters?) -> [LibreMeasurement] {
guard bytes.count >= 320 else { return [] }
let bodyRange = 24..<320 // 296 bytes, i.e. 37 blocks a 8 bytes
let body = Array(bytes[bodyRange])
let nextHistoryBlock = Int(body[3])
let minutesSinceStart = Int(body[293]) << 8 + Int(body[292])
var measurements = [LibreMeasurement]()
// History data is stored in body from byte 100 to byte 100+192-1=291 in units of 6 bytes. Index on data such that most recent block is first.
for blockIndex in 0..<32 {
var index = 100 + (nextHistoryBlock - 1 - blockIndex) * 6 // runs backwards
if index < 100 {
index = index + 192 // if end of ring buffer is reached shift to beginning of ring buffer
}
guard index + 6 < body.count else { break }
let range = index..<index+6
let measurementBytes = Array(body[range])
let (date, counter) = dateOfMostRecentHistoryValue(minutesSinceStart: minutesSinceStart, nextHistoryBlock: nextHistoryBlock, date: date)
let final = date.addingTimeInterval(Double(-900 * blockIndex))
let measurement = LibreMeasurement(bytes: measurementBytes,
slope: slope,
offset: offset,
minuteCounter: counter - blockIndex * 15,
date: final,
LibreDerivedAlgorithmParameterSet: LibreDerivedAlgorithmParameterSet)
measurements.append(measurement)
}
return measurements
}
/// Get date of most recent history value.
/// History values are updated every 15 minutes. Their corresponding time from start of the sensor in minutes is 15, 30, 45, 60, ..., but the value is delivered three minutes later, i.e. at the minutes 18, 33, 48, 63, ... and so on. So for instance if the current time in minutes (since start of sensor) is 67, the most recent value is 7 minutes old. This can be calculated from the minutes since start. Unfortunately sometimes the history index is incremented earlier than the minutes counter and they are not in sync. This has to be corrected.
/// - Parameters:
/// - minutesSinceStart: /// Minutes (approx) since start of sensor
/// - nextHistoryBlock: /// Index on the next block of trend data that the sensor will measure and store
/// - date: the current date
/// - Returns: the date of the most recent history value and the corresponding minute counter
static func dateOfMostRecentHistoryValue(minutesSinceStart: Int, nextHistoryBlock: Int, date: Date) -> (date: Date, counter: Int) {
let nextHistoryIndexCalculatedFromMinutesCounter = ( (minutesSinceStart - 3) / 15 ) % 32
let delay = (minutesSinceStart - 3) % 15 + 3 // in minutes
if nextHistoryIndexCalculatedFromMinutesCounter == nextHistoryBlock {
return (date: date.addingTimeInterval( 60.0 * -Double(delay) ), counter: minutesSinceStart - delay)
} else {
return (date: date.addingTimeInterval( 60.0 * -Double(delay - 15)), counter: minutesSinceStart - delay)
}
}
/// to glucose data
/// - Parameter measurements: measurements
/// - Returns: glucose data
static func trendToLibreGlucose(_ measurements: [LibreMeasurement]) -> [LibreRawGlucoseData]?{
var origarr = [LibreRawGlucoseData]()
for trend in measurements {
let glucose = LibreRawGlucoseData.init(timeStamp: trend.date, glucoseLevelRaw: trend.temperatureAlgorithmGlucose)
origarr.append(glucose)
}
return origarr
}
}

View File

@ -1,508 +0,0 @@
////
//// RemoteBG.swift
//// SwitftOOPWeb
////
//// Created by Bjørn Inge Berg on 08.04.2018.
//// Copyright © 2018 Bjørn Inge Berg. All rights reserved.
////
//
//
// LibreOOPClient.swift
// SwitftOOPWeb
//
// Created by Bjørn Inge Berg on 08.04.2018.
// Copyright © 2018 Bjørn Inge Berg. All rights reserved.
//
//
// adapted by Johan Degraeve for xdrip ios
import Foundation
import os
import SpriteKit
import UserNotifications
public class LibreOOPClient {
private static let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryLibreOOPClient)
/// get the libre glucose data by server
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - patchUid : sensor sn hex string
/// - patchInfo : will be used by server to out the glucose data
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - callback: server data that contains the 344 bytes details
static func webOOP(libreData: [UInt8], patchUid: String, patchInfo: String, oopWebSite: String, oopWebToken: String, callback: ((LibreRawGlucoseOOPData?) -> Void)?) {
let bytesAsData = Data(bytes: libreData, count: libreData.count)
let item = URLQueryItem(name: "accesstoken", value: oopWebToken)
let item1 = URLQueryItem(name: "patchUid", value: patchUid)
let item2 = URLQueryItem(name: "patchInfo", value: patchInfo)
let item3 = URLQueryItem(name: "content", value: bytesAsData.hexEncodedString())
var urlComponents = URLComponents(string: "\(oopWebSite)/libreoop2")!
urlComponents.queryItems = [item, item1, item2, item3]
if let uploadURL = URL.init(string: urlComponents.url?.absoluteString.removingPercentEncoding ?? "") {
let request = NSMutableURLRequest(url: uploadURL)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
DispatchQueue.main.async {
guard let data = data else {
callback?(nil)
return
}
let decoder = JSONDecoder.init()
if let oopValue = try? decoder.decode(LibreRawGlucoseOOPData.self, from: data) {
callback?(oopValue)
} else {
callback?(nil)
}
}
}
task.resume()
} else {
callback?(nil)
}
}
/// if server failed, will parse the 344 bytes by `LibreMeasurement`
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - params: local algorithm use this
/// - timeStampLastBgReading: timestamp of last reading, older readings will be ignored
/// - Returns: glucose data
static func oopParams(libreData: [UInt8], params: LibreDerivedAlgorithmParameters, timeStampLastBgReading: Date) -> [LibreRawGlucoseData] {
// get current glucose from 344 bytes
let last16 = trendMeasurements(bytes: libreData, date: Date(), timeStampLastBgReading: timeStampLastBgReading, LibreDerivedAlgorithmParameterSet: params)
if let glucoseData = trendToLibreGlucose(last16), let first = glucoseData.first {
// get histories from 344 bytes
let last32 = historyMeasurements(bytes: libreData, date: first.timeStamp, LibreDerivedAlgorithmParameterSet: params)
// every 15 minutes apart
let glucose32 = trendToLibreGlucose(last32) ?? []
// every 5 minutes apart, fill data by `SKKeyframeSequence`
let last96 = split(current: first, glucoseData: glucose32.reversed())
return last96
} else {
return []
}
}
/// get the parameters and local parse
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - serialNumber: sensor serial number
/// - timeStampLastBgReading: timestamp of last reading, older readings will be ignored
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - callback: will be called when glucose data is read with as parameter the timestamp of the last reading.
static func oop(libreData: [UInt8], serialNumber: String, timeStampLastBgReading: Date, oopWebSite: String, oopWebToken: String, _ callback: @escaping ((glucoseData: [GlucoseData], sensorState: LibreSensorState, sensorTimeInMinutes: Int, errorDescription: String?)) -> Void) {
let sensorState = LibreSensorState(stateByte: libreData[4])
let body = Array(libreData[24 ..< 320])
let sensorTime = Int(body[293]) << 8 + Int(body[292])
// if sensor time < 60, it can not get the parameters from the server
guard sensorTime >= 60 else {
callback(([], .starting, sensorTime, nil))
return
}
// get LibreDerivedAlgorithmParameters
calibrateSensor(bytes: [UInt8](libreData), serialNumber: serialNumber, oopWebSite: oopWebSite, oopWebToken: oopWebToken) {
(calibrationparams) in
callback((oopParams(libreData: [UInt8](libreData), params: calibrationparams, timeStampLastBgReading: timeStampLastBgReading),
sensorState,
sensorTime, nil))
}
}
/// if `patchInfo.hasPrefix("A2")`, server uses another arithmetic to handle the 344 bytes
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - oopWebSite: the site url to use if oop web would be enabled
/// - callback: server data that contains the 344 bytes details
static func handleLibreA2Data(libreData: [UInt8], oopWebSite: String, callback: ((LibreRawGlucoseOOPA2Data?) -> Void)?) {
let bytesAsData = Data(bytes: libreData, count: libreData.count)
if let uploadURL = URL.init(string: "\(oopWebSite)/callnox") {
do {
var request = URLRequest(url: uploadURL)
request.httpMethod = "POST"
let data = try JSONSerialization.data(withJSONObject: [["timestamp": "\(Int(Date().timeIntervalSince1970 * 1000))",
"content": bytesAsData.hexEncodedString()]], options: [])
let string = String.init(data: data, encoding: .utf8)
let json: [String: String] = ["userId": "1",
"list": string!]
request.setBodyContent(contentMap: json)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
do {
guard let data = data else {
callback?(nil)
return
}
let decoder = JSONDecoder.init()
let oopValue = try decoder.decode(LibreRawGlucoseOOPA2Data.self, from: data)
callback?(oopValue)
} catch {
callback?(nil)
}
}
task.resume()
} catch {
callback?(nil)
}
}
}
/// handle server data from two functions `webOOP` and `handleLibreA2Data`, parse the data to glucse data
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - patchInfo: sensor sn hex string
/// - oopValue: parsed value
/// - timeStampLastBgReading: timestamp of last reading, older readings will be ignored
/// - serialNumber: serial number
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - callback: will be called when glucose data is read with as parameter the timestamp of the last reading.
static func handleGlucose(libreData: [UInt8], patchInfo: String, oopValue: LibreRawGlucoseWeb?, timeStampLastBgReading: Date, serialNumber: String, oopWebSite: String, oopWebToken: String, _ callback: @escaping ((glucoseData: [GlucoseData], sensorState: LibreSensorState, sensorTimeInMinutes: Int, errorDescription: String?)) -> Void) {
if let oopValue = oopValue, !oopValue.isError {
if oopValue.valueError {
if oopValue.sensorState == .notYetStarted {
callback(([], .notYetStarted, 0, nil))
} else {
callback(([], .failure, 0, nil))
}
} else {
if oopValue.canGetParameters {
if UserDefaults.standard.algorithmParameters?.serialNumber != serialNumber {
calibrateSensor(bytes: [UInt8](libreData), serialNumber: serialNumber, oopWebSite: oopWebSite, oopWebToken: oopWebToken, callback: {_ in })
}
}
if let time = oopValue.sensorTime {
var last96 = [LibreRawGlucoseData]()
let value = oopValue.glucoseData(date: Date())
last96 = split(current: value.0, glucoseData: value.1)
if time < 20880 {
callback((last96, oopValue.sensorState, time, nil))
} else {
callback(([], .expired, time, nil))
}
} else {
callback(([], oopValue.sensorState, 0, nil))
}
}
} else {
// only patchInfo `hasPrefix` "70" and "E5" can use the local parse
if patchInfo.hasPrefix("70") || patchInfo.hasPrefix("E5") {
oop(libreData: libreData, serialNumber: serialNumber, timeStampLastBgReading: timeStampLastBgReading, oopWebSite: oopWebSite, oopWebToken: oopWebToken, callback)
} else {
callback(([], .failure, 0, nil))
}
}
}
/// handle 344 bytes
/// - Parameters:
/// - libreData: the 344 bytes from Libre sensor
/// - patchUid: sensor sn hex string
/// - patchInfo: will be used by server to out the glucose data
/// - timeStampLastBgReading: timestamp of last reading, older readings will be ignored
/// - serialNumber: serial number
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - callback: will be called when glucose data is read with as parameter the timestamp of the last reading.
static func handleLibreData(libreData: Data, patchUid: String?, patchInfo: String?, timeStampLastBgReading: Date, serialNumber: String, oopWebSite: String, oopWebToken: String, _ callback: @escaping ((glucoseData: [GlucoseData], sensorState: LibreSensorState, sensorTimeInMinutes: Int, errorDescription: String?)) -> Void) {
let bytes = [UInt8](libreData)
guard let patchUid = patchUid, let patchInfo = patchInfo else {
oop(libreData: bytes, serialNumber: serialNumber, timeStampLastBgReading: timeStampLastBgReading, oopWebSite: oopWebSite, oopWebToken: oopWebToken, callback)
return
}
// if patchInfo.hasPrefix("A2"), server uses another arithmetic to handle the 344 bytes
if patchInfo.hasPrefix("A2") {
handleLibreA2Data(libreData: bytes, oopWebSite: oopWebSite) { (data) in
DispatchQueue.main.async {
handleGlucose(libreData: bytes, patchInfo: patchInfo, oopValue: data, timeStampLastBgReading: timeStampLastBgReading, serialNumber: serialNumber, oopWebSite: oopWebSite, oopWebToken: oopWebToken, callback)
}
}
} else {
DispatchQueue.main.async {
webOOP(libreData: bytes, patchUid: patchUid, patchInfo: patchInfo, oopWebSite: oopWebSite, oopWebToken: oopWebToken) { (data) in
handleGlucose(libreData: bytes, patchInfo: patchInfo, oopValue: data, timeStampLastBgReading: timeStampLastBgReading, serialNumber: serialNumber, oopWebSite: oopWebSite, oopWebToken: oopWebToken, callback)
}
}
}
}
/// 15 minutes apart to 5 minutes apart
/// - Parameters:
/// - current: current glucose
/// - glucoseData: histories
/// - Returns: contains current glucose and histories, 5 minutes apart
static func split(current: LibreRawGlucoseData?, glucoseData: [LibreRawGlucoseData]) -> [LibreRawGlucoseData] {
var x = [Double]()
var y = [Double]()
if let current = current {
let timeInterval = current.timeStamp.timeIntervalSince1970 * 1000
x.append(timeInterval)
y.append(current.glucoseLevelRaw)
}
for glucose in glucoseData.reversed() {
let time = glucose.timeStamp.timeIntervalSince1970 * 1000
x.insert(time, at: 0)
y.insert(glucose.glucoseLevelRaw, at: 0)
}
let startTime = x.first ?? 0
let endTime = x.last ?? 0
// add glucoses to `SKKeyframeSequence`
let frameS = SKKeyframeSequence.init(keyframeValues: y, times: x as [NSNumber])
frameS.interpolationMode = .spline
var items = [LibreRawGlucoseData]()
var ptime = endTime
while ptime >= startTime {
// get value from SKKeyframeSequence
let value = (frameS.sample(atTime: CGFloat(ptime)) as? Double) ?? 0
let item = LibreRawGlucoseData.init(timeStamp: Date.init(timeIntervalSince1970: ptime / 1000), glucoseLevelRaw: value)
items.append(item)
ptime -= 300000
}
return items
}
/// get the `LibreDerivedAlgorithmParameters`
/// - Parameters:
/// - bytes: the 344 bytes from Libre sensor
/// - serialNumber: serial number
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - callback: return `LibreDerivedAlgorithmParameters`
public static func calibrateSensor(bytes: [UInt8], serialNumber: String, oopWebSite: String, oopWebToken: String, callback: @escaping (LibreDerivedAlgorithmParameters) -> Void) {
// the parameters of one sensor will not be changed, if have cached it, get it from userdefaults
if let parameters = UserDefaults.standard.algorithmParameters {
if parameters.serialNumber == serialNumber {
callback(parameters)
return
}
}
<<<<<<< HEAD
// default parameters
let params = LibreDerivedAlgorithmParameters.init(slope_slope: 0.00001729,
slope_offset: -0.0006316,
offset_slope: 0.002080,
offset_offset: -20.15,
=======
/// default parameters
let params = LibreDerivedAlgorithmParameters.init(slope_slope: 0.00001816666666666667,
slope_offset: -0.00016666666666666666,
offset_slope: 0.007499999999999993,
offset_offset: -21.5,
>>>>>>> 74e797ad31f6fadf1504e2ab9c1a54d9489f3c85
isValidForFooterWithReverseCRCs: 1,
extraSlope: 1.0,
extraOffset: 0.0,
sensorSerialNumber: serialNumber)
post(bytes: bytes, oopWebSite: oopWebSite, oopWebToken: oopWebToken, { (data, str, can) in
let decoder = JSONDecoder()
do {
let response = try decoder.decode(GetCalibrationStatus.self, from: data)
if let slope = response.slope {
var libreDerivedAlgorithmParameters = LibreDerivedAlgorithmParameters.init(slope_slope: slope.slopeSlope ?? 0,
slope_offset: slope.slopeOffset ?? 0,
offset_slope: slope.offsetSlope ?? 0,
offset_offset: slope.offsetOffset ?? 0,
isValidForFooterWithReverseCRCs: Int(slope.isValidForFooterWithReverseCRCs ?? 1),
extraSlope: 1.0,
extraOffset: 0.0,
sensorSerialNumber: serialNumber)
libreDerivedAlgorithmParameters.serialNumber = serialNumber
if !libreDerivedAlgorithmParameters.isErrorParameters {
UserDefaults.standard.algorithmParameters = libreDerivedAlgorithmParameters
callback(libreDerivedAlgorithmParameters)
} else {
// server values all 0, `isErrorParameters` is true
// return the default parameters
callback(params)
}
} else {
// encoding data failed, no need to handle as an error, it means probably next time a new post will be done to the oop web server
trace("in calibrateSensor, slope is nil", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error)
// return the default parameters
callback(params)
}
} catch {
// encoding data failed, return the default parameters
callback(params)
trace("in calibrateSensor, error while encoding data : %{public}@", log: log, category: ConstantsLog.categoryLibreOOPClient, type: .error, error.localizedDescription)
}
})
}
/// get `LibreDerivedAlgorithmParameters` from server
/// - Parameters:
/// - bytes: the 344 bytes from Libre sensor
/// - oopWebSite: the site url to use if oop web would be enabled
/// - oopWebToken: the token to use if oop web would be enabled
/// - completion: network result
static func post(bytes: [UInt8], oopWebSite: String, oopWebToken: String,_ completion:@escaping (( _ data_: Data, _ response: String, _ success: Bool ) -> Void)) {
let date = Date().toMillisecondsAsInt64()
let bytesAsData = Data(bytes: bytes, count: bytes.count)
let json: [String: String] = [
"token": oopWebToken,
"content": "\(bytesAsData.hexEncodedString())",
"timestamp": "\(date)",
]
if let uploadURL = URL.init(string: "\(oopWebSite)/calibrateSensor") {
var request = URLRequest(url: uploadURL)
request.httpMethod = "POST"
request.setBodyContent(contentMap: json)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
guard let data = data else {
DispatchQueue.main.sync {
completion("network error".data(using: .utf8)!, "network error", false)
}
return
}
if let response = String(data: data, encoding: String.Encoding.utf8) {
DispatchQueue.main.sync {
completion(data, response, true)
}
return
}
DispatchQueue.main.sync {
completion("response error".data(using: .utf8)!, "response error", false)
}
}
task.resume()
}
}
/// current glucose value from 344 bytes
/// - Parameters:
/// - bytes: the 344 bytes from Libre sensor
/// - date: the current date
/// - timeStampLastBgReading: timestamp of last reading, older readings will be ignored
/// - offset: glucose offset to be added in mg/dl
/// - slope: slope to calculate glucose from raw value in (mg/dl)/raw
/// - LibreDerivedAlgorithmParameterSet: algorithm parameters
/// - Returns: return parsed values
static func trendMeasurements(bytes: [UInt8], date: Date, timeStampLastBgReading: Date, _ offset: Double = 0.0, slope: Double = 0.1, LibreDerivedAlgorithmParameterSet: LibreDerivedAlgorithmParameters?) -> [LibreMeasurement] {
guard bytes.count >= 320 else { return [] }
// let headerRange = 0..<24 // 24 bytes, i.e. 3 blocks a 8 bytes
let bodyRange = 24 ..< 320 // 296 bytes, i.e. 37 blocks a 8 bytes
// let footerRange = 320..<344 // 24 bytes, i.e. 3 blocks a 8 bytes
let body = Array(bytes[bodyRange])
let nextTrendBlock = Int(body[2])
var measurements = [LibreMeasurement]()
// Trend data is stored in body from byte 4 to byte 4+96=100 in units of 6 bytes. Index on data such that most recent block is first.
for blockIndex in 0 ... 15 {
var index = 4 + (nextTrendBlock - 1 - blockIndex) * 6 // runs backwards
if index < 4 {
index = index + 96 // if end of ring buffer is reached shift to beginning of ring buffer
}
guard index + 6 < body.count else { break }
let range = index ..< index + 6
let measurementBytes = Array(body[range])
let measurementDate = date.addingTimeInterval(Double(-60 * blockIndex))
if measurementDate > timeStampLastBgReading {
let measurement = LibreMeasurement(bytes: measurementBytes, slope: slope, offset: offset, date: measurementDate, LibreDerivedAlgorithmParameterSet: LibreDerivedAlgorithmParameterSet)
measurements.append(measurement)
}
}
return measurements
}
/// histories for 344 bytes
/// - Parameters:
/// - bytes: the 344 bytes from Libre sensor
/// - date: the current date
/// - offset: glucose offset to be added in mg/dl
/// - slope: slope to calculate glucose from raw value in (mg/dl)/raw
/// - LibreDerivedAlgorithmParameterSet: algorithm parameters
/// - Returns: return parsed values
static func historyMeasurements(bytes: [UInt8], date: Date, _ offset: Double = 0.0, slope: Double = 0.1, LibreDerivedAlgorithmParameterSet: LibreDerivedAlgorithmParameters?) -> [LibreMeasurement] {
guard bytes.count >= 320 else { return [] }
let bodyRange = 24..<320 // 296 bytes, i.e. 37 blocks a 8 bytes
let body = Array(bytes[bodyRange])
let nextHistoryBlock = Int(body[3])
let minutesSinceStart = Int(body[293]) << 8 + Int(body[292])
var measurements = [LibreMeasurement]()
// History data is stored in body from byte 100 to byte 100+192-1=291 in units of 6 bytes. Index on data such that most recent block is first.
for blockIndex in 0..<32 {
var index = 100 + (nextHistoryBlock - 1 - blockIndex) * 6 // runs backwards
if index < 100 {
index = index + 192 // if end of ring buffer is reached shift to beginning of ring buffer
}
guard index + 6 < body.count else { break }
let range = index..<index+6
let measurementBytes = Array(body[range])
let (date, counter) = dateOfMostRecentHistoryValue(minutesSinceStart: minutesSinceStart, nextHistoryBlock: nextHistoryBlock, date: date)
let final = date.addingTimeInterval(Double(-900 * blockIndex))
let measurement = LibreMeasurement(bytes: measurementBytes,
slope: slope,
offset: offset,
minuteCounter: counter - blockIndex * 15,
date: final,
LibreDerivedAlgorithmParameterSet: LibreDerivedAlgorithmParameterSet)
measurements.append(measurement)
}
return measurements
}
/// Get date of most recent history value.
/// History values are updated every 15 minutes. Their corresponding time from start of the sensor in minutes is 15, 30, 45, 60, ..., but the value is delivered three minutes later, i.e. at the minutes 18, 33, 48, 63, ... and so on. So for instance if the current time in minutes (since start of sensor) is 67, the most recent value is 7 minutes old. This can be calculated from the minutes since start. Unfortunately sometimes the history index is incremented earlier than the minutes counter and they are not in sync. This has to be corrected.
/// - Parameters:
/// - minutesSinceStart: /// Minutes (approx) since start of sensor
/// - nextHistoryBlock: /// Index on the next block of trend data that the sensor will measure and store
/// - date: the current date
/// - Returns: the date of the most recent history value and the corresponding minute counter
static func dateOfMostRecentHistoryValue(minutesSinceStart: Int, nextHistoryBlock: Int, date: Date) -> (date: Date, counter: Int) {
let nextHistoryIndexCalculatedFromMinutesCounter = ( (minutesSinceStart - 3) / 15 ) % 32
let delay = (minutesSinceStart - 3) % 15 + 3 // in minutes
if nextHistoryIndexCalculatedFromMinutesCounter == nextHistoryBlock {
return (date: date.addingTimeInterval( 60.0 * -Double(delay) ), counter: minutesSinceStart - delay)
} else {
return (date: date.addingTimeInterval( 60.0 * -Double(delay - 15)), counter: minutesSinceStart - delay)
}
}
/// to glucose data
/// - Parameter measurements: measurements
/// - Returns: glucose data
static func trendToLibreGlucose(_ measurements: [LibreMeasurement]) -> [LibreRawGlucoseData]?{
var origarr = [LibreRawGlucoseData]()
for trend in measurements {
let glucose = LibreRawGlucoseData.init(timeStamp: trend.date, glucoseLevelRaw: trend.temperatureAlgorithmGlucose)
origarr.append(glucose)
}
return origarr
}
}

View File

@ -12,10 +12,10 @@ import Foundation
/// the parameters from server
struct GetCalibrationStatus: Codable, CustomStringConvertible {
struct OopWebCalibrationStatus: Codable, CustomStringConvertible {
var error: Bool?
var command: String?
var slope: GetCalibrationStatusResult?
var slope: OopWebCalibrationStatusResult?
var description: String {
return """
@ -27,7 +27,7 @@ struct GetCalibrationStatus: Codable, CustomStringConvertible {
}
}
struct GetCalibrationStatusResult: Codable {
struct OopWebCalibrationStatusResult: Codable {
var status: String?
var slopeSlope: Double?
var slopeOffset: Double?

View File

@ -12,6 +12,7 @@ class LibreRawGlucoseData: GlucoseData {
}
convenience init(timeStamp:Date, glucoseLevelRaw:Double) {
debuglogging("creating LibreRawGlucoseData with timestamp " + timeStamp.description(with: .current) + " and glucoseLevelRaw = " + glucoseLevelRaw.description)
self.init(timeStamp: timeStamp, glucoseLevelRaw: glucoseLevelRaw, glucoseLevelFiltered: glucoseLevelRaw)
}
@ -19,294 +20,11 @@ class LibreRawGlucoseData: GlucoseData {
self.init(timeStamp: timeStamp, glucoseLevelRaw: unsmoothedGlucose, glucoseLevelFiltered: unsmoothedGlucose, unsmoothedGlucose: unsmoothedGlucose)
}
}
protocol LibreRawGlucoseWeb {
/// if the server value is error return true
var isError: Bool { get }
/// sensor time
var sensorTime: Int? { get }
/// if `false`, it means current 344 bytes can not get the parameters from server
var canGetParameters: Bool { get }
/// sensor state
var sensorState: LibreSensorState { get }
/// when sensor return error 344 bytes, server will return wrong glucose data
var valueError: Bool { get }
/// get glucoses from server data
func glucoseData(date: Date) ->(LibreRawGlucoseData?, [LibreRawGlucoseData])
}
public class LibreRawGlucoseOOPData: NSObject, Codable, LibreRawGlucoseWeb {
/// histories by server
var historicGlucose : [LibreRawGlucoseOOPGlucose]?
/// current glucose
var realTimeGlucose : LibreRawGlucoseOOPGlucose?
/// trend arrow by server
var trendArrow : String?
/// sensor message
var msg: String?
var errcode: String?
/// if endTime != 0, the sensor expired
var endTime: Int?
enum Error: String {
typealias RawValue = String
case RESCAN_SENSOR_BAD_CRC // crc failed
// sensor terminate
case TERMINATE_SENSOR_NORMAL_TERMINATED_STATE
case TERMINATE_SENSOR_ERROR_TERMINATED_STATE
case TERMINATE_SENSOR_CORRUPT_PAYLOAD
// http request bad arguments
case FATAL_ERROR_BAD_ARGUMENTS
// the follow messages is sensor state
case TYPE_SENSOR_NOT_STARTED
case TYPE_SENSOR_STARTING
case TYPE_SENSOR_Expired
case TYPE_SENSOR_END
case TYPE_SENSOR_ERROR
case TYPE_SENSOR_OK
case TYPE_SENSOR_DETERMINED
}
/// if the server value is error return true
var isError: Bool {
if let msg = msg {
switch Error(rawValue: msg) { // sensor terminate
case .TERMINATE_SENSOR_CORRUPT_PAYLOAD,
.TERMINATE_SENSOR_NORMAL_TERMINATED_STATE,
.TERMINATE_SENSOR_ERROR_TERMINATED_STATE:
return false
default:
break
}
}
// if parse the 344 failed, historicGlucose will be nil, return is error
return historicGlucose?.isEmpty ?? true
}
/// sensor time
var sensorTime: Int? {
// if endTime != 0, the sensor expired
if let endTime = endTime, endTime != 0 {
return 24 * 6 * 149
}
return realTimeGlucose?.id
}
/// if `false`, it means current 344 bytes can not get the parameters from server
var canGetParameters: Bool {
if let dataQuality = realTimeGlucose?.dataQuality, let id = realTimeGlucose?.id {
if dataQuality == 0 && id >= 60 {
return true
}
}
return false
}
var sensorState: LibreSensorState {
/// if sensor time < 60, sensor state is starting
if let dataQuality = realTimeGlucose?.dataQuality, let id = realTimeGlucose?.id {
if dataQuality != 0 && id < 60 {
return LibreSensorState.starting
}
}
/// description
override var description: String {
var state = LibreSensorState.ready
// parse the sensor state from msg
if let msg = msg {
switch Error(rawValue: msg) {
case .TYPE_SENSOR_NOT_STARTED:
state = .notYetStarted
break;
case .TYPE_SENSOR_STARTING:
state = .starting
break;
case .TYPE_SENSOR_Expired,
.TERMINATE_SENSOR_CORRUPT_PAYLOAD,
.TERMINATE_SENSOR_NORMAL_TERMINATED_STATE,
.TERMINATE_SENSOR_ERROR_TERMINATED_STATE:
state = .expired
break;
case .TYPE_SENSOR_END:
state = .expired
break;
case .TYPE_SENSOR_ERROR:
state = .failure
break;
case .TYPE_SENSOR_OK:
state = .ready
case .TYPE_SENSOR_DETERMINED:
state = .unknown
break
default:
break;
}
}
return "\nLibreRawGlucoseData\nunsmoothedGlucose = " + unsmoothedGlucose.description + "\n" + "GlucoseData = \n" + super.description
// if endTime != 0, the sensor expired
if let endTime = endTime, endTime != 0 {
state = .expired
}
return state
}
/// get glucoses from server data
/// - Parameter date: timestamp of last reading
/// - Returns: return current glucose and histories
func glucoseData(date: Date) ->(LibreRawGlucoseData?, [LibreRawGlucoseData]) {
if endTime != 0 {
return (nil, [])
}
var current: LibreRawGlucoseData?
guard let g = realTimeGlucose, g.dataQuality == 0 else { return(nil, []) }
current = LibreRawGlucoseData.init(timeStamp: date, glucoseLevelRaw: g.value ?? 0)
var array = [LibreRawGlucoseData]()
// every 15 minutes apart
let gap: TimeInterval = 60 * 15
var date = date
if var history = historicGlucose {
if (history.first?.id ?? 0) < (history.last?.id ?? 0) {
history = history.reversed()
}
for g in history {
date = date.addingTimeInterval(-gap)
// if dataQuality != 0, the value is error
if g.dataQuality != 0 { continue }
let glucose = LibreRawGlucoseData.init(timeStamp: date, glucoseLevelRaw: g.value ?? 0)
array.insert(glucose, at: 0)
}
}
return (current ,array)
}
/// when sensor return error 344 bytes, server will return wrong glucose data
var valueError: Bool {
// sensor time < 60, the sensor is starting
if let id = realTimeGlucose?.id, id < 60 {
return false
}
// current glucose is error, this parse failed, can not be use
if let g = realTimeGlucose, let value = g.dataQuality {
return value != 0
}
return false
}
}
/// glucose value
class LibreRawGlucoseOOPGlucose: NSObject, Codable {
/// if dataQuality != 0, it means the value is error
let dataQuality : Int?
/// the value's sensor time
let id: Int?
/// glucose value
let value : Double?
}
public class LibreRawGlucoseOOPA2Data: NSObject, Codable, LibreRawGlucoseWeb {
var errcode: Int?
var list: [LibreRawGlucoseOOPA2List]?
/// server parse value
var content: LibreRawGlucoseOOPA2Cotent? {
return list?.first?.content
}
/// if the server value is error return true
var isError: Bool {
if content?.currentBg ?? 0 <= 10 {
return true
}
return list?.first?.content?.historicBg?.isEmpty ?? true
}
/// sensor time
var sensorTime: Int? {
return content?.currentTime
}
/// if `false`, it means current 344 bytes can not get the parameters from server
var canGetParameters: Bool {
if let id = content?.currentTime {
if id >= 60 {
return true
}
}
return false
}
/// sensor state
var sensorState: LibreSensorState {
if let id = content?.currentTime {
if id < 60 { // if sensor time < 60, the sensor is starting
return LibreSensorState.starting
} else if id >= 20880 { // if sensor time >= 20880, the sensor expired
return LibreSensorState.expired
}
}
let state = LibreSensorState.ready
return state
}
/// get glucoses from server data
/// - Parameter date: timestamp of last reading
/// - Returns: return current glucose and histories
func glucoseData(date: Date) ->(LibreRawGlucoseData?, [LibreRawGlucoseData]) {
var current: LibreRawGlucoseData?
guard !isError else { return(nil, []) }
current = LibreRawGlucoseData.init(timeStamp: date, glucoseLevelRaw: content?.currentBg ?? 0)
var array = [LibreRawGlucoseData]()
// every 15 minutes apart
let gap: TimeInterval = 60 * 15
var date = date
if var history = content?.historicBg {
if (history.first?.time ?? 0) < (history.last?.time ?? 0) {
history = history.reversed()
}
for g in history {
date = date.addingTimeInterval(-gap)
// if dataQuality != 0, the value is error
if g.quality != 0 { continue }
let glucose = LibreRawGlucoseData.init(timeStamp: date, glucoseLevelRaw: g.bg ?? 0)
array.insert(glucose, at: 0)
}
}
return (current ,array)
}
/// when sensor return error 344 bytes, server will return wrong glucose data
var valueError: Bool {
// sensor time < 60, the sensor is starting
if let id = content?.currentTime, id < 60 {
return false
}
// current glucose is error
if content?.currentBg ?? 0 <= 10 {
return true
}
return false
}
}
class LibreRawGlucoseOOPA2List: NSObject, Codable {
var content: LibreRawGlucoseOOPA2Cotent?
}
class LibreRawGlucoseOOPA2Cotent: NSObject, Codable {
/// current sensor time
var currentTime: Int?
/// histories
var historicBg: [HistoricGlucoseA2]?
/// current glucose value
var currentBg: Double?
}
class HistoricGlucoseA2: NSObject, Codable {
/// if quality != 0, it means the value is error
let quality : Int?
/// the value's sensor time
let time: Int?
/// glucose value
let bg : Double?
}

View File

@ -0,0 +1,35 @@
import Foundation
class LibreRawGlucoseOOPA2Cotent: NSObject, Codable {
/// current sensor time
var currentTime: Int?
/// histories
var historicBg: [LibreHistoricGlucoseA2]?
/// current glucose value
var currentBg: Double?
/// description
override var description: String {
var returnValue = "LibreRawGlucoseOOPA2Cotent = \ncurrentTime = " + (currentTime != nil ? currentTime!.description : "nil") + "\n"
if let historicBg = historicBg {
for bg in historicBg {
returnValue = returnValue + bg.description + "\n"
}
}
if let currentBg = currentBg {returnValue = returnValue + currentBg.description}
return returnValue
}
}

View File

@ -0,0 +1,134 @@
//
// LibreRawGlucoseOOPA2Data.swift
// xdrip
//
// Created by Johan Degraeve on 08/06/2020.
// Copyright © 2020 Johan Degraeve. All rights reserved.
//
// source https://github.com/JohanDegraeve/xdripswift/blob/bd5b3060f3a7d4c68dce767b5c86306239d06d14/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/LibreRawGlucoseData.swift#L208
import Foundation
public class LibreRawGlucoseOOPA2Data: NSObject, Decodable, LibreRawGlucoseWeb {
var errcode: Int?
var list: [LibreRawGlucoseOOPA2List]?
/// - time when instance of LibreRawGlucoseOOPData was created
/// - this can be created to calculate the timestamp of realtimeLibreRawGlucoseData
let creationTimeStamp = Date()
/// server parse value
var content: LibreRawGlucoseOOPA2Cotent? {
return list?.first?.content
}
/// if the server value is error return true
var isError: Bool {
if content?.currentBg ?? 0 <= 10 {
return true
}
return list?.first?.content?.historicBg?.isEmpty ?? true
}
/// sensor state
var sensorState: LibreSensorState {
if let id = content?.currentTime {
if id < 60 { // if sensor time < 60, the sensor is starting
return LibreSensorState.starting
} else if id >= 20880 { // if sensor time >= 20880, the sensor expired
return LibreSensorState.expired
}
}
let state = LibreSensorState.ready
return state
}
func glucoseData(timeStampLastBgReading: Date?) -> (libreRawGlucoseData:[LibreRawGlucoseData], sensorState:LibreSensorState, sensorTimeInMinutes:Int?) {
// initialize returnvalue, empty glucoseData array, sensorState, and nil as sensorTimeInMinutes
var returnValue: ([LibreRawGlucoseData], LibreSensorState, Int?) = ([LibreRawGlucoseData](), sensorState, nil)
// if isError function returns true, then return empty array
guard !isError else { return returnValue }
// if sensorState is not .ready, then return empty array
if sensorState != .ready { return returnValue }
// content should be non nil, content.currentBg not nil and currentBg != 0, content.currentTime (sensorTimeInMinutes) not nil
guard let content = content, let currentBg = content.currentBg, currentBg != 0, let sensorTimeInMinutes = content.currentTime else { return returnValue }
// set senorTimeInMinutes in returnValue
returnValue.2 = sensorTimeInMinutes
// create realtimeLibreRawGlucoseData, which is the current glucose data
let realtimeLibreRawGlucoseData = LibreRawGlucoseData(timeStamp: creationTimeStamp, glucoseLevelRaw: currentBg)
// add first element
returnValue.0.append(realtimeLibreRawGlucoseData)
// history should be non nil, otherwise return only the first value
guard var history = content.historicBg else { return returnValue }
// check the order, first should be the highest value, time is sensor time in minutes, means first should be the most recent or the highest sensor time
// if not, reverse it
if (history.first?.time ?? 0) < (history.last?.time ?? 0) {
history = history.reversed()
}
// iterate through history, only add readings with timetamp > timeStampLastBgReading
for libreHistoricGlucoseA2 in history {
// if quality != 0, the value is error, don't add it
if libreHistoricGlucoseA2.quality != 0 {continue}
// if time is nil, (which is sensorTimeInMinutes at the moment this reading was created), then we can't calculate the timestamp, don't add it
if libreHistoricGlucoseA2.time == nil {continue}
// create timestamp of the reading
let readingTimeStamp = creationTimeStamp.addingTimeInterval(-60 * Double(sensorTimeInMinutes - libreHistoricGlucoseA2.time!))
// only add the new reading if at least 30 seconds older than timeStampLastBgReading (if not nil) - when nil then 0.0 is used, readingTimeStamp will never be < 0.0
// as soon as a reading is found which is too old to process then break the loop
if readingTimeStamp.toMillisecondsAsDouble() < (timeStampLastBgReading != nil ? (timeStampLastBgReading!.toMillisecondsAsDouble() + 30000) : 0.0) {break}
// only add readings that are at least 5 minutes away from each other, same approach as in LibreDataParser.parse
if let lastElement = returnValue.0.last {
if lastElement.timeStamp.toMillisecondsAsDouble() - readingTimeStamp.toMillisecondsAsDouble() < (5 * 60 * 1000 - 10000) {continue}
}
// bg value should be non nil and > 0.0
if libreHistoricGlucoseA2.bg == nil {continue}
if libreHistoricGlucoseA2.bg! == 0.0 {continue}
let libreRawGlucoseData = LibreRawGlucoseData(timeStamp: readingTimeStamp, glucoseLevelRaw: libreHistoricGlucoseA2.bg!)
returnValue.0.append(libreRawGlucoseData)
}
return (returnValue)
}
override public var description: String {
var returnValue = "LibreRawGlucoseOOPA2Data =\n"
// a description created by LibreRawGlucoseWeb
returnValue = returnValue + (self as LibreRawGlucoseWeb).description
if let errcode = errcode {
returnValue = returnValue + " errcode = " + errcode.description + "\n"
}
return returnValue
}
}

View File

@ -0,0 +1,16 @@
import Foundation
class LibreRawGlucoseOOPA2List: NSObject, Codable {
var content: LibreRawGlucoseOOPA2Cotent?
/// description
override var description: String {
guard let content = content else {return "LibreRawGlucoseOOPA2List = \ncontent = nil (type LibreRawGlucoseOOPA2List)"}
return "LibreRawGlucoseOOPA2List = \ncontent = " + content.description
}
}

View File

@ -0,0 +1,197 @@
// https://github.com/JohanDegraeve/xdripswift/blob/bd5b3060f3a7d4c68dce767b5c86306239d06d14/xdrip/BluetoothTransmitter/CGM/Libre/Utilities/LibreRawGlucoseData.swift#L208
import Foundation
public class LibreRawGlucoseOOPData: NSObject, Decodable, LibreRawGlucoseWeb {
/// histories by server
var historicGlucose : [LibreRawGlucoseOOPGlucose]?
/// current glucose
var realTimeGlucose : LibreRawGlucoseOOPGlucose?
/// trend arrow by server
var trendArrow : String?
/// sensor message
var msg: String?
var errcode: String?
/// if endTime != 0, the sensor expired
var endTime: Int?
/// - time when instance of LibreRawGlucoseOOPData was created
/// - this can be created to calculate the timestamp of realTimeGlucose
let creationTimeStamp = Date()
enum Error: String {
typealias RawValue = String
case RESCAN_SENSOR_BAD_CRC // crc failed
// sensor terminate
case TERMINATE_SENSOR_NORMAL_TERMINATED_STATE
case TERMINATE_SENSOR_ERROR_TERMINATED_STATE
case TERMINATE_SENSOR_CORRUPT_PAYLOAD
// http request bad arguments
case FATAL_ERROR_BAD_ARGUMENTS
// the follow messages is sensor state
case TYPE_SENSOR_NOT_STARTED
case TYPE_SENSOR_STARTING
case TYPE_SENSOR_Expired
case TYPE_SENSOR_END
case TYPE_SENSOR_ERROR
case TYPE_SENSOR_OK
case TYPE_SENSOR_DETERMINED
}
/// if the server value is error return true
var isError: Bool {
if let msg = msg {
switch Error(rawValue: msg) { // sensor terminate
case .TERMINATE_SENSOR_CORRUPT_PAYLOAD,
.TERMINATE_SENSOR_NORMAL_TERMINATED_STATE,
.TERMINATE_SENSOR_ERROR_TERMINATED_STATE:
return false
default:
break
}
}
// if parse the 344 failed, historicGlucose will be nil, return is error
return historicGlucose?.isEmpty ?? true
}
var sensorState: LibreSensorState {
/// if sensor time < 60, sensor state is starting
if let dataQuality = realTimeGlucose?.dataQuality, let id = realTimeGlucose?.id {
if dataQuality != 0 && id < 60 {
return LibreSensorState.starting
}
}
var state = LibreSensorState.ready
// parse the sensor state from msg
if let msg = msg {
switch Error(rawValue: msg) {
case .TYPE_SENSOR_NOT_STARTED:
state = .notYetStarted
break;
case .TYPE_SENSOR_STARTING:
state = .starting
break;
case .TYPE_SENSOR_Expired,
.TERMINATE_SENSOR_CORRUPT_PAYLOAD,
.TERMINATE_SENSOR_NORMAL_TERMINATED_STATE,
.TERMINATE_SENSOR_ERROR_TERMINATED_STATE:
state = .expired
break;
case .TYPE_SENSOR_END:
state = .expired
break;
case .TYPE_SENSOR_ERROR:
state = .failure
break;
case .TYPE_SENSOR_OK:
state = .ready
case .TYPE_SENSOR_DETERMINED:
state = .unknown
break
default:
break;
}
}
// if endTime != 0, the sensor expired
if let endTime = endTime, endTime != 0 {
state = .expired
}
return state
}
func glucoseData(timeStampLastBgReading: Date?) -> (libreRawGlucoseData:[LibreRawGlucoseData], sensorState:LibreSensorState, sensorTimeInMinutes:Int?) {
// initialize returnvalue, empty glucoseData array, sensorState, and nil as sensorTimeInMinutes
var returnValue: ([LibreRawGlucoseData], LibreSensorState, Int?) = ([LibreRawGlucoseData](), sensorState, nil)
// if isError function returns true, then return empty array
guard !isError else { return returnValue }
// if sensorState is not .ready, then return empty array
if sensorState != .ready { return returnValue }
// realTimeGlucose must be non-nil and realTimeGlucose.dataQuality must be 0, id (sensor time) must be non nil, otherwise return empty array
guard let realTimeGlucose = realTimeGlucose, realTimeGlucose.dataQuality == 0, let value = realTimeGlucose.value, let sensorTimeInMinutes = realTimeGlucose.id else { return returnValue }
// set senorTimeInMinutes in returnValue
returnValue.2 = sensorTimeInMinutes
// get realtimeLibreRawGlucoseData which is the first element to add, with timestamp the time this instance of LibreRawGlucoseOOPData was created
let realtimeLibreRawGlucoseData = LibreRawGlucoseData(timeStamp: creationTimeStamp, glucoseLevelRaw: value)
// add first element to returnValue
returnValue.0.append(realtimeLibreRawGlucoseData)
// if historicGlucose is nil then return currentLibreRawGlucoseData and an empty array
guard var history = historicGlucose else {return returnValue}
// check the order, first should be the highest value, time is sensor time in minutes, means first should be the most recent or the highest sensor time
// if not, reverse it
if (history.first?.id ?? 0) < (history.last?.id ?? 0) {
history = history.reversed()
}
// go through history
for realTimeGlucose in history {
// if dataQuality != 0, the value is error, don't add it
if realTimeGlucose.dataQuality != 0 { continue }
// if id is nil, (which is sensorTimeInMinutes at the moment this reading was created), then we can't calculate the timestamp, don't add it
if realTimeGlucose.id == nil {continue}
// create timestamp of the reading
let readingTimeStamp = creationTimeStamp.addingTimeInterval(-60 * Double(sensorTimeInMinutes - realTimeGlucose.id!))
// only add the new reading if at least 30 seconds older than timeStampLastBgReading (if not nil) - when nil then 0.0 is used, readingTimeStamp will never be < 0.0
// as soon as a reading is found which is too old to process then break the loop
if readingTimeStamp.toMillisecondsAsDouble() < (timeStampLastBgReading != nil ? (timeStampLastBgReading!.toMillisecondsAsDouble() + 30000) : 0.0) {break}
// only add readings that are at least 5 minutes away from each other, same approach as in LibreDataParser.parse
if let lastElement = returnValue.0.last {
if lastElement.timeStamp.toMillisecondsAsDouble() - readingTimeStamp.toMillisecondsAsDouble() < (5 * 60 * 1000 - 10000) {continue}
}
// realTimeGlucose.value should be non nil and not 0
if realTimeGlucose.value == nil {continue}
if realTimeGlucose.value! == 0.0 {continue}
let libreRawGlucoseData = LibreRawGlucoseData(timeStamp: readingTimeStamp, glucoseLevelRaw: realTimeGlucose.value!)
returnValue.0.append(libreRawGlucoseData)
}
return (returnValue)
}
public override var description: String {
var returnValue = "LibreRawGlucoseWeb =\n"
// a description created by LibreRawGlucoseWeb
returnValue = returnValue + (self as LibreRawGlucoseWeb).description
if let errcode = errcode {
returnValue = returnValue + " errcode = " + errcode.description + "\n"
}
return returnValue
}
}

View File

@ -0,0 +1,24 @@
import Foundation
/// a readings value, id (time in minutes since sensor start) and dataQuality
class LibreRawGlucoseOOPGlucose: NSObject, Codable {
/// if dataQuality != 0, it means the value is error
let dataQuality : Int?
/// the time of this reading, in minutes, since sensor start
let id: Int?
/// glucose value
let value : Double?
/// description
override var description: String {
return "LibreRawGlucoseOOPGlucose = \ndataQuality = " + (dataQuality != nil ? dataQuality!.description : "nil") + ", id = " + (id != nil ? id!.description : "nil") + ", value = " + (value != nil ? value!.description : "nil")
}
}

View File

@ -0,0 +1,56 @@
import Foundation
protocol LibreRawGlucoseWeb {
/// if the server value is error return true
var isError: Bool { get }
/// sensor state
var sensorState: LibreSensorState { get }
/// - parameters:
/// - timeStampLastBgReading: this is of the timestamp of the latest reading we already received during previous session, optional
/// - returns:
/// - array of libreRawGlucoseData, first is the most recent. Only returns recent readings, ie not the ones that are older than timeStampLastBgReading. 30 seconds are added here, meaning, new reading should be at least 30 seconds more recent than timeStampLastBgReading
/// - sensorState: status of the sensor
/// - sensorTimeInMinutes: age of sensor in minutes, optional
func glucoseData(timeStampLastBgReading: Date?) -> (libreRawGlucoseData:[LibreRawGlucoseData], sensorState:LibreSensorState, sensorTimeInMinutes:Int?)
}
// to implement a var description
extension LibreRawGlucoseWeb {
var description: String {
var returnValue = " isError = " + isError.description + "\n"
returnValue = returnValue + " sensorState = " + sensorState.description + "\n"
let libreGlucoseData = glucoseData(timeStampLastBgReading: nil)
returnValue = returnValue + "\nSize of [LibreRawGlucoseData] = " + libreGlucoseData.libreRawGlucoseData.count.description + "\n"
if libreGlucoseData.libreRawGlucoseData.count > 0 {
returnValue = returnValue + "list = \n"
for glucoseData in libreGlucoseData.libreRawGlucoseData {
returnValue = returnValue + glucoseData.description + "\n"
}
}
if let sensorTimeInMinutes = libreGlucoseData.sensorTimeInMinutes {
returnValue = returnValue + "sensor time in minutes = " + sensorTimeInMinutes.description + "\n"
} else {
returnValue = returnValue + "sensor time in minutes is unknown\n"
}
return returnValue
}
}

View File

@ -8,7 +8,7 @@
import Foundation
struct LibreSensorSerialNumber: CustomStringConvertible {
public struct LibreSensorSerialNumber: CustomStringConvertible {
let uid: Data
@ -102,8 +102,10 @@ struct LibreSensorSerialNumber: CustomStringConvertible {
return stringArray.dropFirst().reduce(stringArray.first!, {$0 + ":" + $1} )
}
// MARK: - CustomStringConvertible Protocoll
var description: String {
// MARK: - CustomStringConvertible Protocol
public var description: String {
return "Uid is \(prettyUidString) and derived serial number is \(serialNumber)"
}
}

View File

@ -17,7 +17,7 @@ import Foundation
/// - shutdown: 0x05 sensor stops operation after 15d after start
/// - failure: 0x06 sensor has an error
/// - unknown: any other state
enum LibreSensorState {
public enum LibreSensorState {
case notYetStarted
case starting
case ready

View File

@ -0,0 +1,95 @@
import Foundation
// sourcre https://github.com/gui-dos/DiaBLE
public enum LibreSensorType: String {
// TODO: can this be improved ? raw type UInt8 iso String ? in which case all patchInfo variables would need to become of type LibreSensorType in stead of a String. In the bluetooth transmitters, then when assigning a value, immediately set to LibreSensorType iso patchInfo
// Libre 1
case libre1 = "DF"
case libre1A2 = "A2"
case libre2 = "9D"
case libreUS = "E5"
case libreProH = "70"
var description: String {
switch self {
case .libre1:
return "Libre 1"
case .libre1A2:
return "Libre 1"
case .libre2:
return "Libre 2"
case .libreUS:
return "Libre US"
case .libreProH:
return "Libre PRO H"
}
}
/// some of the Libre types can not work without webOOP. In case returnvalue is true, then user can not change the value
func needsWebOOP() -> Bool {
switch self {
case .libre1:
return false
case .libre1A2, .libre2, .libreUS, .libreProH :
return true
}
}
/// - reads the first byte in patchInfo and dependent on that value, returns type of sensor
/// - if patchInfo = nil, then returnvalue is Libre1
/// - if first byte is unknown, then returns nil
static func type(patchInfo: String?) -> LibreSensorType? {
guard let patchInfo = patchInfo else {return .libre1}
guard patchInfo.count > 1 else {return nil}
let firstTwoChars = patchInfo[0..<2]
switch firstTwoChars {
case "DF":
return .libre1
case "A2":
return .libre1A2
case "9D":
return .libre2
case "E5":
return .libreUS
case "70":
return .libreProH
default:
return nil
}
}
}

View File

@ -119,6 +119,9 @@ final class WatlaaBluetoothTransmitter: BluetoothTransmitter {
// current sensor serial number, if nil then it's not known yet
private var sensorSerialNumber:String?
/// used as parameter in call to cgmTransmitterDelegate.cgmTransmitterInfoReceived, when there's no glucosedata to send
var emptyArray: [GlucoseData] = []
// MARK: - public functions
/// - parameters:
@ -288,10 +291,8 @@ final class WatlaaBluetoothTransmitter: BluetoothTransmitter {
trace("in peripheral didUpdateValueFor, Buffer complete", log: log, category: ConstantsLog.categoryWatlaa, type: .info)
if (Crc.LibreCrc(data: &rxBuffer, headerOffset: miaoMiaoHeaderLength)) {
//get MiaoMiao info from MiaoMiao header
let firmware = String(describing: rxBuffer[14...15].hexEncodedString())
let hardware = String(describing: rxBuffer[16...17].hexEncodedString())
// get batteryPercentage
let batteryPercentage = Int(rxBuffer[13])
// get sensor serialNumber and if changed inform delegate
@ -320,11 +321,17 @@ final class WatlaaBluetoothTransmitter: BluetoothTransmitter {
// send battery level to delegate
watlaaBluetoothTransmitterDelegate?.received(transmitterBatteryLevel: batteryPercentage, watlaaBluetoothTransmitter: self)
LibreDataParser.libreDataProcessor(sensorSerialNumber: LibreSensorSerialNumber(withUID: Data(rxBuffer.subdata(in: 5..<13)))?.serialNumber, webOOPEnabled: webOOPEnabled, oopWebSite: oopWebSite, oopWebToken: oopWebToken, libreData: (rxBuffer.subdata(in: miaoMiaoHeaderLength..<(344 + miaoMiaoHeaderLength))), cgmTransmitterDelegate: cgmTransmitterDelegate, transmitterBatteryInfo: TransmitterBatteryInfo.percentage(percentage: batteryPercentage), firmware: firmware, hardware: hardware, hardwareSerialNumber: nil, bootloader: nil, timeStampLastBgReading: timeStampLastBgReading, completionHandler: {(timeStampLastBgReading:Date) in
self.timeStampLastBgReading = timeStampLastBgReading
})
// send batteryPercentage to delegate
cgmTransmitterDelegate?.cgmTransmitterInfoReceived(glucoseData: &emptyArray, transmitterBatteryInfo: TransmitterBatteryInfo.percentage(percentage: batteryPercentage), sensorTimeInMinutes: nil)
LibreDataParser.libreDataProcessor(libreSensorSerialNumber: LibreSensorSerialNumber(withUID: Data(rxBuffer.subdata(in: 5..<13))), patchInfo: nil, webOOPEnabled: webOOPEnabled, oopWebSite: oopWebSite, oopWebToken: oopWebToken, libreData: (rxBuffer.subdata(in: miaoMiaoHeaderLength..<(344 + miaoMiaoHeaderLength))), cgmTransmitterDelegate: cgmTransmitterDelegate, timeStampLastBgReading: timeStampLastBgReading, completionHandler: { (timeStampLastBgReading: Date?, sensorState: LibreSensorState?) in
if let timeStampLastBgReading = timeStampLastBgReading {
self.timeStampLastBgReading = timeStampLastBgReading
}
}
)
//reset the buffer
resetRxBuffer()

View File

@ -5,7 +5,7 @@ enum ConstantsLibre {
static let defaultWebOOPEnabled = false
/// site for libreOOP client
static let site = "http://www.glucose.space/calibrateSensor"
static let site = "http://www.glucose.space"
/// token to use to access site
static let token = "bubble-201907"

View File

@ -106,6 +106,9 @@ enum ConstantsLog {
/// LibreOOPClient
static let categoryLibreOOPClient = "LibreOOPClient "
/// libreDataParser
static let categoryLibreDataParser = "LibreDataParser "
/// for use in M5Stack
static let categoryM5StackBluetoothTransmitter = "M5StackBluetoothTransmitter "
@ -124,8 +127,10 @@ enum ConstantsLog {
/// bluetoothPeripheralViewController
static let categoryBluetoothPeripheralViewController = "blePeripheralViewController "
/// nightscout view model
static let categoryNightScoutSettingsViewModel = "nightScoutSettingsViewModel "
/// trace
static let categoryTraceSettingsViewModel = "TraceSettingsViewModel"
}

View File

@ -4,7 +4,12 @@ import CoreData
/// - has attributes which are common to all kinds of BluetoothPeripheral : address, name, shouldconnect, alias, parameterUpdateNeededAtNextConnect.
/// - the name is very close the the protocol BluetoothPeripheral, bad luck that's all
public class BLEPeripheral: NSManagedObject {
/// - libre sensor type, not stored in coreData, will only be available after having received it from the Bubble
/// - if nil, then it's not known yet
/// - will only be used for BLEPeripherals that support Libre
public var libreSensorType: LibreSensorType?
/// create BLEPeripheral, shouldconnect default value = true
init(address: String, name: String, alias: String?, nsManagedObjectContext:NSManagedObjectContext) {

View File

@ -35,7 +35,7 @@ public class BgReading: NSManagedObject {
c = 0
ra = 0
rb = 0
rc = 0
rc = 0
hideSlope = false
id = UniqueId.createEventId()
}

View File

@ -3,9 +3,12 @@ import CoreData
public class Bubble: NSManagedObject {
/// batterylevel, not stored in coreData, will only be available after having received it from the M5Stack
/// batterylevel, not stored in coreData, will only be available after having received it from the Bubble
public var batteryLevel: Int = 0
// sensorState
public var sensorState: LibreSensorState = .unknown
/// create Bubble
/// - parameters:
init(address: String, name: String, alias: String?, nsManagedObjectContext:NSManagedObjectContext) {

View File

@ -95,6 +95,10 @@ extension Data {
append(UnsafeBufferPointer(start: &element, count: 1))
}
func getByteAt(position:Int) -> Int {
return Int(self[position])
}
}

View File

@ -178,8 +178,8 @@ extension UserDefaults {
/// to merge from 3.x to 4.x, can be deleted once 3.x is not used anymore
case cgmTransmitterDeviceAddress = "cgmTransmitterDeviceAddress"
/// web oop parameters, only for bubble
case algorithmParameters = "algorithmParameters"
/// web oop parameters, only for Libre 1
case libre1DerivedAlgorithmParameters = "algorithmParameters"
}
@ -837,21 +837,23 @@ extension UserDefaults {
set(newValue, forKey: Key.cgmTransmitterDeviceAddress.rawValue)
}
}
/// web oop parameters, only for bubble
var algorithmParameters: LibreDerivedAlgorithmParameters? {
var libre1DerivedAlgorithmParameters: Libre1DerivedAlgorithmParameters? {
get {
guard let jsonString = string(forKey: Key.algorithmParameters.rawValue) else { return nil }
guard let jsonString = string(forKey: Key.libre1DerivedAlgorithmParameters.rawValue) else { return nil }
guard let jsonData = jsonString.data(using: .utf8) else { return nil }
guard let value = try? JSONDecoder().decode(LibreDerivedAlgorithmParameters.self, from: jsonData) else { return nil }
guard let value = try? JSONDecoder().decode(Libre1DerivedAlgorithmParameters.self, from: jsonData) else { return nil }
return value
}
set {
let encoder = JSONEncoder()
guard let jsonData = try? encoder.encode(newValue) else { return }
let jsonString = String(bytes: jsonData, encoding: .utf8)
set(jsonString, forKey: Key.algorithmParameters.rawValue)
set(jsonString, forKey: Key.libre1DerivedAlgorithmParameters.rawValue)
}
}
}

View File

@ -13,6 +13,33 @@ extension BluetoothPeripheralManager: CGMBubbleTransmitterDelegate {
}
func received(sensorStatus: LibreSensorState, from cGMBubbleTransmitter: CGMBubbleTransmitter) {
guard let bubble = findTransmitter(cGMBubbleTransmitter: cGMBubbleTransmitter) else {return}
// store serial number in bubble object
bubble.sensorState = sensorStatus
// no coredatamanager savechanges needed because batterylevel is not stored in coredata
}
func received(libreSensorType: LibreSensorType, from cGMBubbleTransmitter: CGMBubbleTransmitter) {
guard let bubble = findTransmitter(cGMBubbleTransmitter: cGMBubbleTransmitter) else {return}
// store serial number in bubble.blePeripheral object
bubble.blePeripheral.libreSensorType = libreSensorType
// if the libreSensorType needs oopweb, then enable oopweb. (User may have set it to false, but if it's one that requires oopweb, then we force to true)
if libreSensorType.needsWebOOP() {
bubble.blePeripheral.webOOPEnabled = true
}
// coredatamanager savechanges needed because webOOPEnabled is stored in coredata
coreDataManager.saveChanges()
}
func received(serialNumber: String, from cGMBubbleTransmitter: CGMBubbleTransmitter) {

View File

@ -521,7 +521,7 @@ class BluetoothPeripheralManager: NSObject {
if let watlaa = bluetoothPeripheral as? Watlaa {
newTransmitter = WatlaaBluetoothTransmitter(address: watlaa.blePeripheral.address, name: watlaa.blePeripheral.name, cgmTransmitterDelegate: cgmTransmitterDelegate, bluetoothTransmitterDelegate: self, watlaaBluetoothTransmitterDelegate: self, timeStampLastBgReading: nil, sensorSerialNumber: watlaa.blePeripheral.sensorSerialNumber, webOOPEnabled: watlaa.blePeripheral.webOOPEnabled, oopWebSite: watlaa.blePeripheral.oopWebSite, oopWebToken: watlaa.blePeripheral.oopWebSite)
newTransmitter = WatlaaBluetoothTransmitter(address: watlaa.blePeripheral.address, name: watlaa.blePeripheral.name, cgmTransmitterDelegate: cgmTransmitterDelegate, bluetoothTransmitterDelegate: self, watlaaBluetoothTransmitterDelegate: self, timeStampLastBgReading: nil, sensorSerialNumber: watlaa.blePeripheral.sensorSerialNumber, webOOPEnabled: watlaa.blePeripheral.webOOPEnabled, oopWebSite: watlaa.blePeripheral.oopWebSite, oopWebToken: watlaa.blePeripheral.oopWebToken)
}
@ -570,7 +570,7 @@ class BluetoothPeripheralManager: NSObject {
if let cgmTransmitterDelegate = cgmTransmitterDelegate {
newTransmitter = CGMBubbleTransmitter(address: bubble.blePeripheral.address, name: bubble.blePeripheral.name, bluetoothTransmitterDelegate: self, cGMBubbleTransmitterDelegate: self, cGMTransmitterDelegate: cgmTransmitterDelegate, timeStampLastBgReading: nil, sensorSerialNumber: bubble.blePeripheral.sensorSerialNumber, webOOPEnabled: bubble.blePeripheral.webOOPEnabled, oopWebSite: bubble.blePeripheral.oopWebSite, oopWebToken: bubble.blePeripheral.oopWebSite)
newTransmitter = CGMBubbleTransmitter(address: bubble.blePeripheral.address, name: bubble.blePeripheral.name, bluetoothTransmitterDelegate: self, cGMBubbleTransmitterDelegate: self, cGMTransmitterDelegate: cgmTransmitterDelegate, timeStampLastBgReading: nil, sensorSerialNumber: bubble.blePeripheral.sensorSerialNumber, webOOPEnabled: bubble.blePeripheral.webOOPEnabled, oopWebSite: bubble.blePeripheral.oopWebSite, oopWebToken: bubble.blePeripheral.oopWebToken)
} else {
@ -585,7 +585,7 @@ class BluetoothPeripheralManager: NSObject {
if let cgmTransmitterDelegate = cgmTransmitterDelegate {
newTransmitter = CGMMiaoMiaoTransmitter(address: miaoMiao.blePeripheral.address, name: miaoMiao.blePeripheral.name, bluetoothTransmitterDelegate: self, cGMMiaoMiaoTransmitterDelegate: self, cGMTransmitterDelegate: cgmTransmitterDelegate, timeStampLastBgReading: nil, sensorSerialNumber: miaoMiao.blePeripheral.sensorSerialNumber, webOOPEnabled: miaoMiao.blePeripheral.webOOPEnabled, oopWebSite: miaoMiao.blePeripheral.oopWebSite, oopWebToken: miaoMiao.blePeripheral.oopWebSite)
newTransmitter = CGMMiaoMiaoTransmitter(address: miaoMiao.blePeripheral.address, name: miaoMiao.blePeripheral.name, bluetoothTransmitterDelegate: self, cGMMiaoMiaoTransmitterDelegate: self, cGMTransmitterDelegate: cgmTransmitterDelegate, timeStampLastBgReading: nil, sensorSerialNumber: miaoMiao.blePeripheral.sensorSerialNumber, webOOPEnabled: miaoMiao.blePeripheral.webOOPEnabled, oopWebSite: miaoMiao.blePeripheral.oopWebSite, oopWebToken: miaoMiao.blePeripheral.oopWebToken)
} else {

View File

@ -8,6 +8,7 @@
"confirmDeletionPeripheral" = "Do you want to delete bluetooth device: ";
"bluetoothPeripheralAlias" = "Alias:";
"SensorSerialNumber" = "Sensor Serial Number:";
"sensorType" = "Sensor Type:";
"serialNumber" = "Serial Number:";
"battery" = "Battery:";
"needsTransmitterId" = "Missing Transmitter ID";

View File

@ -32,3 +32,4 @@
"firmware" = "Firmware";
"hardware" = "Hardware";
"unknown" = "Unknown";
"sensorStatus" = "Sensor status";

View File

@ -40,6 +40,10 @@ class Texts_BluetoothPeripheralView {
return NSLocalizedString("SensorSerialNumber", tableName: filename, bundle: Bundle.main, value: "Sensor Serial Number:", comment: "BluetoothPeripheral view, text of the cell with the sensor serial number")
}()
static let sensorType: String = {
return NSLocalizedString("sensorType", tableName: filename, bundle: Bundle.main, value: "Sensor Type:", comment: "BluetoothPeripheral view, text of the cell with the sensor type (only used for Libre)")
}()
static let serialNumber: String = {
return NSLocalizedString("serialNumber", tableName: filename, bundle: Bundle.main, value: "Serial Number:", comment: "BluetoothPeripheral view, text of the cell with the serial number (this is not the sensor serial number")
}()

View File

@ -145,4 +145,8 @@ class Texts_Common {
return NSLocalizedString("unknown", tableName: filename, bundle: Bundle.main, value: "Unknown", comment: "general usage")
}()
static let sensorStatus = {
return NSLocalizedString("sensorStatus", tableName: filename, bundle: Bundle.main, value: "Sensor status", comment: "to show the sensor status")
}()
}

View File

@ -23,8 +23,6 @@ fileprivate enum Setting:Int, CaseIterable {
/// timestamp when connection changed to connected or not connected
case connectOrDisconnectTimeStamp = 4
/// ANY NEW SETTINGS SHOULD BE INSERTED HERE
/// transmitterID, only for devices that need it
case transmitterId = 5
@ -859,15 +857,36 @@ extension BluetoothPeripheralViewController: UITableViewDataSource, UITableViewD
case .webOOPEnabled:
// set row text and set default row label to nil
cell.textLabel?.text = Texts_SettingsView.labelWebOOPTransmitter
cell.detailTextLabel?.text = nil
var currentStatus = false
// get current value of webOOPEnabled, default false
var currentWebOOPEnabledValue = false
if let bluetoothPeripheral = bluetoothPeripheral {
currentStatus = bluetoothPeripheral.blePeripheral.webOOPEnabled
currentWebOOPEnabledValue = bluetoothPeripheral.blePeripheral.webOOPEnabled
}
cell.accessoryView = UISwitch(isOn: currentStatus, action: { (isOn:Bool) in
// set accessoryView to UISwitch with currentWebOOPEnabledValue assigned
let test = UISwitch(isOn: currentWebOOPEnabledValue, action: { (isOn:Bool) in
self.bluetoothPeripheral?.blePeripheral.webOOPEnabled = isOn
// send info to bluetoothPeripheralManager
if let bluetoothPeripheral = self.bluetoothPeripheral {
bluetoothPeripheralManager.receivedNewValue(webOOPEnabled: isOn, for: bluetoothPeripheral)
tableView.reloadSections(IndexSet(integer: self.webOOPSettingsSectionNumber), with: .none)
}
})
cell.accessoryView = UISwitch(isOn: currentWebOOPEnabledValue, action: { (isOn:Bool) in
self.bluetoothPeripheral?.blePeripheral.webOOPEnabled = isOn
@ -882,6 +901,15 @@ extension BluetoothPeripheralViewController: UITableViewDataSource, UITableViewD
})
// if it's a bluetoothPeripheral that supports libre and if it's a libre sensor type that needs oopWeb, then value can not be changed,
if let bluetoothPeripheral = self.bluetoothPeripheral, let libreSensorType = bluetoothPeripheral.blePeripheral.libreSensorType {
if libreSensorType.needsWebOOP() {
cell.accessoryView?.isUserInteractionEnabled = false
}
}
cell.accessoryType = .none
case .webOOPsite:

View File

@ -36,6 +36,4 @@ protocol BluetoothPeripheralViewModel {
/// how many sections does this viewmodel define, in addition to the section already defined in BluetoothPeripheralViewController
func numberOfSections() -> Int
/// as weboop is a general setting, but only applicable to specific types of transmitter, the model will need to be able to tell if it's supported or not
///
}

View File

@ -7,17 +7,23 @@ class BubbleBluetoothPeripheralViewModel {
/// settings specific for bubble
private enum Settings:Int, CaseIterable {
/// battery level
case batteryLevel = 0
/// firmware version
case firmWare = 1
/// hardware version
case hardWare = 2
/// Libre sensor type
case sensorType = 0
/// Sensor serial number
case sensorSerialNumber = 3
case sensorSerialNumber = 1
/// sensor State
case sensorState = 2
/// battery level
case batteryLevel = 3
/// firmware version
case firmWare = 4
/// hardware version
case hardWare = 5
}
@ -155,6 +161,29 @@ extension BubbleBluetoothPeripheralViewModel: BluetoothPeripheralViewModel {
}
case .sensorType:
cell.accessoryType = .none
cell.textLabel?.text = Texts_BluetoothPeripheralView.sensorType
if let libreSensorType = bubble.blePeripheral.libreSensorType {
cell.detailTextLabel?.text = libreSensorType.description
} else {
cell.detailTextLabel?.text = nil
}
case .sensorState:
cell.accessoryType = .none
cell.textLabel?.text = Texts_Common.sensorStatus
cell.detailTextLabel?.text = bubble.sensorState.description
}
}
@ -170,7 +199,7 @@ extension BubbleBluetoothPeripheralViewModel: BluetoothPeripheralViewModel {
switch setting {
case .batteryLevel:
case .batteryLevel, .sensorType, .sensorState:
return .nothing
case .firmWare:
@ -225,6 +254,16 @@ extension BubbleBluetoothPeripheralViewModel: CGMBubbleTransmitterDelegate {
}
func received(sensorStatus: LibreSensorState, from cGMBubbleTransmitter: CGMBubbleTransmitter) {
// inform also bluetoothPeripheralManager
(bluetoothPeripheralManager as? CGMBubbleTransmitterDelegate)?.received(sensorStatus: sensorStatus, from: cGMBubbleTransmitter)
// here's the trigger to update the table
reloadRow(row: Settings.sensorState.rawValue)
}
func received(serialNumber: String, from cGMBubbleTransmitter: CGMBubbleTransmitter) {
// inform also bluetoothPeripheralManager
@ -247,7 +286,7 @@ extension BubbleBluetoothPeripheralViewModel: CGMBubbleTransmitterDelegate {
func received(hardware: String, from cGMBubbleTransmitter: CGMBubbleTransmitter) {
// inform also bluetoothPeripheralManager
// inform bluetoothPeripheralManager, bluetoothPeripheralManager will store the hardware in the bubble object
(bluetoothPeripheralManager as? CGMBubbleTransmitterDelegate)?.received(hardware: hardware, from: cGMBubbleTransmitter)
// here's the trigger to update the table
@ -255,6 +294,18 @@ extension BubbleBluetoothPeripheralViewModel: CGMBubbleTransmitterDelegate {
}
func received(libreSensorType: LibreSensorType, from cGMBubbleTransmitter: CGMBubbleTransmitter) {
// inform bluetoothPeripheralManager, bluetoothPeripheralManager will store the libreSensorType in the bubble object
(bluetoothPeripheralManager as? CGMBubbleTransmitterDelegate)?.received(libreSensorType: libreSensorType, from: cGMBubbleTransmitter)
// here's the trigger to update the table row for sensorType
reloadRow(row: Settings.sensorType.rawValue)
// ideally we should also reload the row which allows to change the value of OOP web enabled, because it may have been set by the BluetoothPeripheralManager to true, but that's in the class BluetoothPeripheralViewController, tant pis. Might be confusing, user may see it as disabled but still it's enabled
}
private func reloadRow(row: Int) {
if let bluetoothPeripheralViewController = bluetoothPeripheralViewController {

View File

@ -489,6 +489,8 @@ final class RootViewController: UIViewController {
// also for cases where calibration is not needed, we go through this code
if let activeSensor = activeSensor, let calibrator = calibrator, let bgReadingsAccessor = bgReadingsAccessor {
// TODO : check on sensorage should be done in another way, because 10 day sensors will now also be supported
// assuming it's a 14 day sensor (as 10 day sensor is not supported), if it's a Libre sensor and age > 14.5 days, then don't process the readings
if cgmTransmitter.cgmTransmitterType().sensorType() == .Libre{