redesign Libre Utilities
This commit is contained in:
parent
9e9d0d190d
commit
f034133488
|
@ -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";
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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?
|
||||
|
|
|
@ -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?
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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)"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ public class BgReading: NSManagedObject {
|
|||
c = 0
|
||||
ra = 0
|
||||
rb = 0
|
||||
rc = 0
|
||||
rc = 0
|
||||
hideSlope = false
|
||||
id = UniqueId.createEventId()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -95,6 +95,10 @@ extension Data {
|
|||
append(UnsafeBufferPointer(start: &element, count: 1))
|
||||
}
|
||||
|
||||
func getByteAt(position:Int) -> Int {
|
||||
return Int(self[position])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -32,3 +32,4 @@
|
|||
"firmware" = "Firmware";
|
||||
"hardware" = "Hardware";
|
||||
"unknown" = "Unknown";
|
||||
"sensorStatus" = "Sensor status";
|
||||
|
|
|
@ -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")
|
||||
}()
|
||||
|
|
|
@ -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")
|
||||
}()
|
||||
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
///
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{
|
||||
|
||||
|
|
Loading…
Reference in New Issue