@@ -0,0 +1,10 @@ |
||
| 1 |
+.DS_Store |
|
| 2 |
+ |
|
| 3 |
+# Xcode user-specific data |
|
| 4 |
+*.xcuserstate |
|
| 5 |
+*.xcworkspace/xcuserdata/ |
|
| 6 |
+*.xcodeproj/xcuserdata/ |
|
| 7 |
+ |
|
| 8 |
+# Build products |
|
| 9 |
+DerivedData/ |
|
| 10 |
+build/ |
|
@@ -0,0 +1,11 @@ |
||
| 1 |
+{\rtf1\ansi\ansicpg1252\cocoartf2511
|
|
| 2 |
+\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\froman\fcharset0 Times-Roman;\f1\fswiss\fcharset0 ArialMT;}
|
|
| 3 |
+{\colortbl;\red255\green255\blue255;}
|
|
| 4 |
+{\*\expandedcolortbl;;}
|
|
| 5 |
+\paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0 |
|
| 6 |
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 |
|
| 7 |
+ |
|
| 8 |
+\f0\fs24 \cf0 Documenting code\ |
|
| 9 |
+ {\field{\*\fldinst{HYPERLINK "https://sarunw.com/posts/swift-documentation/"}}{\fldrslt https://sarunw.com/posts/swift-documentation/}}\
|
|
| 10 |
+{\field{\*\fldinst{HYPERLINK "https://developer.apple.com/library/content/documentation/Xcode/Reference/xcode_markup_formatting_ref/Links.html#//apple_ref/doc/uid/TP40016497-CH18-SW1"}}{\fldrslt
|
|
| 11 |
+\f1\fs30 https://developer.apple.com/library/content/documentation/Xcode/Reference/xcode_markup_formatting_ref/Links.html#//apple_ref/doc/uid/TP40016497-CH18-SW1}}} |
|
@@ -0,0 +1,619 @@ |
||
| 1 |
+// !$*UTF8*$! |
|
| 2 |
+{
|
|
| 3 |
+ archiveVersion = 1; |
|
| 4 |
+ classes = {
|
|
| 5 |
+ }; |
|
| 6 |
+ objectVersion = 54; |
|
| 7 |
+ objects = {
|
|
| 8 |
+ |
|
| 9 |
+/* Begin PBXBuildFile section */ |
|
| 10 |
+ 4308CF8624176CAB0002E80B /* DataGroupRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4308CF8524176CAB0002E80B /* DataGroupRowView.swift */; };
|
|
| 11 |
+ 4308CF882417770D0002E80B /* DataGroupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4308CF872417770D0002E80B /* DataGroupsView.swift */; };
|
|
| 12 |
+ 430CB4FC245E07EB006525C2 /* ChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430CB4FB245E07EB006525C2 /* ChevronView.swift */; };
|
|
| 13 |
+ 4311E63A241384960080EA59 /* DeviceHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4311E639241384960080EA59 /* DeviceHelpView.swift */; };
|
|
| 14 |
+ 4327461B24619CED0009BE4B /* MeterRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4327461A24619CED0009BE4B /* MeterRowView.swift */; };
|
|
| 15 |
+ 432EA6442445A559006FC905 /* ChartContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432EA6432445A559006FC905 /* ChartContext.swift */; };
|
|
| 16 |
+ 4347F01D28D717C1007EE7B1 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 4347F01C28D717C1007EE7B1 /* CryptoSwift */; };
|
|
| 17 |
+ 4351E7BB24685ACD00E798A3 /* CGPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4351E7BA24685ACD00E798A3 /* CGPoint.swift */; };
|
|
| 18 |
+ 43554B2F24443939004E66F5 /* MeasurementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43554B2E24443939004E66F5 /* MeasurementsView.swift */; };
|
|
| 19 |
+ 43554B32244449B5004E66F5 /* MeasurementPointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43554B31244449B5004E66F5 /* MeasurementPointView.swift */; };
|
|
| 20 |
+ 43554B3424444B0E004E66F5 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43554B3324444B0E004E66F5 /* Date.swift */; };
|
|
| 21 |
+ 43567FE92443AD7C00000282 /* ICloudDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43567FE82443AD7C00000282 /* ICloudDefault.swift */; };
|
|
| 22 |
+ 4360A34D241CBB3800B464F9 /* RSSIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4360A34C241CBB3800B464F9 /* RSSIView.swift */; };
|
|
| 23 |
+ 4360A34F241D5CF100B464F9 /* MeterSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4360A34E241D5CF100B464F9 /* MeterSettingsView.swift */; };
|
|
| 24 |
+ 437AEE1524249AAA0025C373 /* Readme.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 437AEE1424249AAA0025C373 /* Readme.rtf */; };
|
|
| 25 |
+ 437D47D12415F91B00B7768E /* LiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437D47D02415F91B00B7768E /* LiveView.swift */; };
|
|
| 26 |
+ 437D47D32415FB7E00B7768E /* Decimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437D47D22415FB7E00B7768E /* Decimal.swift */; };
|
|
| 27 |
+ 437D47D52415FD8C00B7768E /* RecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437D47D42415FD8C00B7768E /* RecordingView.swift */; };
|
|
| 28 |
+ 437D47D72415FDF300B7768E /* ControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437D47D62415FDF300B7768E /* ControlView.swift */; };
|
|
| 29 |
+ 437F0AB72463108F005DEBEC /* MeasurementChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437F0AB62463108F005DEBEC /* MeasurementChartView.swift */; };
|
|
| 30 |
+ 4383B460240EB2D000DAAEBF /* Meter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B45F240EB2D000DAAEBF /* Meter.swift */; };
|
|
| 31 |
+ 4383B462240EB5E400DAAEBF /* AppData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B461240EB5E400DAAEBF /* AppData.swift */; };
|
|
| 32 |
+ 4383B465240EB6B200DAAEBF /* UserDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B464240EB6B200DAAEBF /* UserDefault.swift */; };
|
|
| 33 |
+ 4383B468240F845500DAAEBF /* MacAdress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B467240F845500DAAEBF /* MacAdress.swift */; };
|
|
| 34 |
+ 4383B46A240FE4A600DAAEBF /* MeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B469240FE4A600DAAEBF /* MeterView.swift */; };
|
|
| 35 |
+ 438695892463F062008855A9 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438695882463F062008855A9 /* Measurements.swift */; };
|
|
| 36 |
+ 43874C7F2414F3F400525397 /* Float.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43874C7E2414F3F400525397 /* Float.swift */; };
|
|
| 37 |
+ 43874C83241533AD00525397 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43874C82241533AD00525397 /* Data.swift */; };
|
|
| 38 |
+ 43874C852415611200525397 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43874C842415611200525397 /* Double.swift */; };
|
|
| 39 |
+ 438B9555246D2D7500E61AE7 /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438B9554246D2D7500E61AE7 /* Path.swift */; };
|
|
| 40 |
+ 439D996524234B98008DE3AA /* BluetoothRadio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439D996424234B98008DE3AA /* BluetoothRadio.swift */; };
|
|
| 41 |
+ 43CBF660240BF3EB00255B8B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF65F240BF3EB00255B8B /* AppDelegate.swift */; };
|
|
| 42 |
+ 43CBF662240BF3EB00255B8B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF661240BF3EB00255B8B /* SceneDelegate.swift */; };
|
|
| 43 |
+ 43CBF665240BF3EB00255B8B /* CKModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF663240BF3EB00255B8B /* CKModel.xcdatamodeld */; };
|
|
| 44 |
+ 43CBF667240BF3EB00255B8B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF666240BF3EB00255B8B /* ContentView.swift */; };
|
|
| 45 |
+ 43CBF669240BF3ED00255B8B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43CBF668240BF3ED00255B8B /* Assets.xcassets */; };
|
|
| 46 |
+ 43CBF66C240BF3ED00255B8B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43CBF66B240BF3ED00255B8B /* Preview Assets.xcassets */; };
|
|
| 47 |
+ 43CBF66F240BF3ED00255B8B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43CBF66D240BF3ED00255B8B /* LaunchScreen.storyboard */; };
|
|
| 48 |
+ 43CBF677240C043E00255B8B /* BluetoothManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF676240C043E00255B8B /* BluetoothManager.swift */; };
|
|
| 49 |
+ 43CBF681240D153000255B8B /* CBManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF680240D153000255B8B /* CBManagerState.swift */; };
|
|
| 50 |
+ 43DFBE402441A37B004A47EA /* BorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DFBE3F2441A37B004A47EA /* BorderView.swift */; };
|
|
| 51 |
+ 43ED78AE2420A0BE00974487 /* BluetoothSerial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43ED78AD2420A0BE00974487 /* BluetoothSerial.swift */; };
|
|
| 52 |
+ 43F7792B2465AE1600745DF4 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F7792A2465AE1600745DF4 /* UIView.swift */; };
|
|
| 53 |
+/* End PBXBuildFile section */ |
|
| 54 |
+ |
|
| 55 |
+/* Begin PBXFileReference section */ |
|
| 56 |
+ 4308CF8524176CAB0002E80B /* DataGroupRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGroupRowView.swift; sourceTree = "<group>"; };
|
|
| 57 |
+ 4308CF872417770D0002E80B /* DataGroupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGroupsView.swift; sourceTree = "<group>"; };
|
|
| 58 |
+ 430CB4FB245E07EB006525C2 /* ChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronView.swift; sourceTree = "<group>"; };
|
|
| 59 |
+ 4311E639241384960080EA59 /* DeviceHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceHelpView.swift; sourceTree = "<group>"; };
|
|
| 60 |
+ 4327461A24619CED0009BE4B /* MeterRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterRowView.swift; sourceTree = "<group>"; };
|
|
| 61 |
+ 432EA6432445A559006FC905 /* ChartContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartContext.swift; sourceTree = "<group>"; };
|
|
| 62 |
+ 4351E7BA24685ACD00E798A3 /* CGPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPoint.swift; sourceTree = "<group>"; };
|
|
| 63 |
+ 43554B2E24443939004E66F5 /* MeasurementsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementsView.swift; sourceTree = "<group>"; };
|
|
| 64 |
+ 43554B31244449B5004E66F5 /* MeasurementPointView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementPointView.swift; sourceTree = "<group>"; };
|
|
| 65 |
+ 43554B3324444B0E004E66F5 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
|
|
| 66 |
+ 43567FE82443AD7C00000282 /* ICloudDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICloudDefault.swift; sourceTree = "<group>"; };
|
|
| 67 |
+ 4360A34C241CBB3800B464F9 /* RSSIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSSIView.swift; sourceTree = "<group>"; };
|
|
| 68 |
+ 4360A34E241D5CF100B464F9 /* MeterSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterSettingsView.swift; sourceTree = "<group>"; };
|
|
| 69 |
+ 437AEE1424249AAA0025C373 /* Readme.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Readme.rtf; sourceTree = "<group>"; };
|
|
| 70 |
+ 437D47D02415F91B00B7768E /* LiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveView.swift; sourceTree = "<group>"; };
|
|
| 71 |
+ 437D47D22415FB7E00B7768E /* Decimal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Decimal.swift; sourceTree = "<group>"; };
|
|
| 72 |
+ 437D47D42415FD8C00B7768E /* RecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingView.swift; sourceTree = "<group>"; };
|
|
| 73 |
+ 437D47D62415FDF300B7768E /* ControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlView.swift; sourceTree = "<group>"; };
|
|
| 74 |
+ 437F0AB62463108F005DEBEC /* MeasurementChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementChartView.swift; sourceTree = "<group>"; };
|
|
| 75 |
+ 4383B45F240EB2D000DAAEBF /* Meter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Meter.swift; sourceTree = "<group>"; };
|
|
| 76 |
+ 4383B461240EB5E400DAAEBF /* AppData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppData.swift; sourceTree = "<group>"; };
|
|
| 77 |
+ 4383B464240EB6B200DAAEBF /* UserDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = "<group>"; };
|
|
| 78 |
+ 4383B467240F845500DAAEBF /* MacAdress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAdress.swift; sourceTree = "<group>"; };
|
|
| 79 |
+ 4383B469240FE4A600DAAEBF /* MeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterView.swift; sourceTree = "<group>"; };
|
|
| 80 |
+ 438695882463F062008855A9 /* Measurements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurements.swift; sourceTree = "<group>"; };
|
|
| 81 |
+ 43874C7E2414F3F400525397 /* Float.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Float.swift; sourceTree = "<group>"; };
|
|
| 82 |
+ 43874C82241533AD00525397 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
|
|
| 83 |
+ 43874C842415611200525397 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
|
| 84 |
+ 438B9554246D2D7500E61AE7 /* Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Path.swift; sourceTree = "<group>"; };
|
|
| 85 |
+ 439D996424234B98008DE3AA /* BluetoothRadio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothRadio.swift; sourceTree = "<group>"; };
|
|
| 86 |
+ 43CBF65C240BF3EB00255B8B /* USB Meter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "USB Meter.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
| 87 |
+ 43CBF65F240BF3EB00255B8B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
|
| 88 |
+ 43CBF661240BF3EB00255B8B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
|
| 89 |
+ 43CBF664240BF3EB00255B8B /* USB_Meter.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = USB_Meter.xcdatamodel; sourceTree = "<group>"; };
|
|
| 90 |
+ 43CBF666240BF3EB00255B8B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
|
| 91 |
+ 43CBF668240BF3ED00255B8B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
|
| 92 |
+ 43CBF66B240BF3ED00255B8B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
|
| 93 |
+ 43CBF66E240BF3ED00255B8B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
|
| 94 |
+ 43CBF670240BF3ED00255B8B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
|
| 95 |
+ 43CBF676240C043E00255B8B /* BluetoothManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothManager.swift; sourceTree = "<group>"; };
|
|
| 96 |
+ 43CBF67A240C0D8A00255B8B /* USB Meter.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "USB Meter.entitlements"; sourceTree = "<group>"; };
|
|
| 97 |
+ 43CBF680240D153000255B8B /* CBManagerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBManagerState.swift; sourceTree = "<group>"; };
|
|
| 98 |
+ 43DFBE3F2441A37B004A47EA /* BorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderView.swift; sourceTree = "<group>"; };
|
|
| 99 |
+ 43ED78AD2420A0BE00974487 /* BluetoothSerial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothSerial.swift; sourceTree = "<group>"; };
|
|
| 100 |
+ 43F7792A2465AE1600745DF4 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
|
|
| 101 |
+/* End PBXFileReference section */ |
|
| 102 |
+ |
|
| 103 |
+/* Begin PBXFrameworksBuildPhase section */ |
|
| 104 |
+ 43CBF659240BF3EB00255B8B /* Frameworks */ = {
|
|
| 105 |
+ isa = PBXFrameworksBuildPhase; |
|
| 106 |
+ buildActionMask = 2147483647; |
|
| 107 |
+ files = ( |
|
| 108 |
+ 4347F01D28D717C1007EE7B1 /* CryptoSwift in Frameworks */, |
|
| 109 |
+ ); |
|
| 110 |
+ runOnlyForDeploymentPostprocessing = 0; |
|
| 111 |
+ }; |
|
| 112 |
+/* End PBXFrameworksBuildPhase section */ |
|
| 113 |
+ |
|
| 114 |
+/* Begin PBXGroup section */ |
|
| 115 |
+ 4308CF89241777130002E80B /* Data Groups */ = {
|
|
| 116 |
+ isa = PBXGroup; |
|
| 117 |
+ children = ( |
|
| 118 |
+ 4308CF872417770D0002E80B /* DataGroupsView.swift */, |
|
| 119 |
+ 4308CF8524176CAB0002E80B /* DataGroupRowView.swift */, |
|
| 120 |
+ ); |
|
| 121 |
+ path = "Data Groups"; |
|
| 122 |
+ sourceTree = "<group>"; |
|
| 123 |
+ }; |
|
| 124 |
+ 432F6ED8246684060043912E /* Chart */ = {
|
|
| 125 |
+ isa = PBXGroup; |
|
| 126 |
+ children = ( |
|
| 127 |
+ 437F0AB62463108F005DEBEC /* MeasurementChartView.swift */, |
|
| 128 |
+ ); |
|
| 129 |
+ path = Chart; |
|
| 130 |
+ sourceTree = "<group>"; |
|
| 131 |
+ }; |
|
| 132 |
+ 4347F01B28D717C1007EE7B1 /* Frameworks */ = {
|
|
| 133 |
+ isa = PBXGroup; |
|
| 134 |
+ children = ( |
|
| 135 |
+ ); |
|
| 136 |
+ name = Frameworks; |
|
| 137 |
+ sourceTree = "<group>"; |
|
| 138 |
+ }; |
|
| 139 |
+ 43554B3024444983004E66F5 /* Measurements */ = {
|
|
| 140 |
+ isa = PBXGroup; |
|
| 141 |
+ children = ( |
|
| 142 |
+ 43554B2E24443939004E66F5 /* MeasurementsView.swift */, |
|
| 143 |
+ 43554B31244449B5004E66F5 /* MeasurementPointView.swift */, |
|
| 144 |
+ 432F6ED8246684060043912E /* Chart */, |
|
| 145 |
+ ); |
|
| 146 |
+ path = Measurements; |
|
| 147 |
+ sourceTree = "<group>"; |
|
| 148 |
+ }; |
|
| 149 |
+ 437D47CF2415F8CF00B7768E /* Meter */ = {
|
|
| 150 |
+ isa = PBXGroup; |
|
| 151 |
+ children = ( |
|
| 152 |
+ 4383B469240FE4A600DAAEBF /* MeterView.swift */, |
|
| 153 |
+ 4360A34E241D5CF100B464F9 /* MeterSettingsView.swift */, |
|
| 154 |
+ 437D47D02415F91B00B7768E /* LiveView.swift */, |
|
| 155 |
+ 437D47D42415FD8C00B7768E /* RecordingView.swift */, |
|
| 156 |
+ 437D47D62415FDF300B7768E /* ControlView.swift */, |
|
| 157 |
+ 4308CF89241777130002E80B /* Data Groups */, |
|
| 158 |
+ 4360A34C241CBB3800B464F9 /* RSSIView.swift */, |
|
| 159 |
+ 430CB4FB245E07EB006525C2 /* ChevronView.swift */, |
|
| 160 |
+ 43554B3024444983004E66F5 /* Measurements */, |
|
| 161 |
+ ); |
|
| 162 |
+ path = Meter; |
|
| 163 |
+ sourceTree = "<group>"; |
|
| 164 |
+ }; |
|
| 165 |
+ 4383B463240EB66400DAAEBF /* Templates */ = {
|
|
| 166 |
+ isa = PBXGroup; |
|
| 167 |
+ children = ( |
|
| 168 |
+ 4383B464240EB6B200DAAEBF /* UserDefault.swift */, |
|
| 169 |
+ 43567FE82443AD7C00000282 /* ICloudDefault.swift */, |
|
| 170 |
+ ); |
|
| 171 |
+ path = Templates; |
|
| 172 |
+ sourceTree = "<group>"; |
|
| 173 |
+ }; |
|
| 174 |
+ 4383B466240F842700DAAEBF /* DataTypes */ = {
|
|
| 175 |
+ isa = PBXGroup; |
|
| 176 |
+ children = ( |
|
| 177 |
+ 4383B467240F845500DAAEBF /* MacAdress.swift */, |
|
| 178 |
+ ); |
|
| 179 |
+ path = DataTypes; |
|
| 180 |
+ sourceTree = "<group>"; |
|
| 181 |
+ }; |
|
| 182 |
+ 43CBF653240BF3EB00255B8B = {
|
|
| 183 |
+ isa = PBXGroup; |
|
| 184 |
+ children = ( |
|
| 185 |
+ 437AEE1424249AAA0025C373 /* Readme.rtf */, |
|
| 186 |
+ 43CBF65E240BF3EB00255B8B /* USB Meter */, |
|
| 187 |
+ 43CBF65D240BF3EB00255B8B /* Products */, |
|
| 188 |
+ 4347F01B28D717C1007EE7B1 /* Frameworks */, |
|
| 189 |
+ ); |
|
| 190 |
+ sourceTree = "<group>"; |
|
| 191 |
+ }; |
|
| 192 |
+ 43CBF65D240BF3EB00255B8B /* Products */ = {
|
|
| 193 |
+ isa = PBXGroup; |
|
| 194 |
+ children = ( |
|
| 195 |
+ 43CBF65C240BF3EB00255B8B /* USB Meter.app */, |
|
| 196 |
+ ); |
|
| 197 |
+ name = Products; |
|
| 198 |
+ sourceTree = "<group>"; |
|
| 199 |
+ }; |
|
| 200 |
+ 43CBF65E240BF3EB00255B8B /* USB Meter */ = {
|
|
| 201 |
+ isa = PBXGroup; |
|
| 202 |
+ children = ( |
|
| 203 |
+ 43CBF67A240C0D8A00255B8B /* USB Meter.entitlements */, |
|
| 204 |
+ 43CBF65F240BF3EB00255B8B /* AppDelegate.swift */, |
|
| 205 |
+ 43CBF661240BF3EB00255B8B /* SceneDelegate.swift */, |
|
| 206 |
+ 43CBF678240C047D00255B8B /* Model */, |
|
| 207 |
+ 43CBF679240C08C600255B8B /* Views */, |
|
| 208 |
+ 43CBF67F240D14AC00255B8B /* Extensions */, |
|
| 209 |
+ 4383B463240EB66400DAAEBF /* Templates */, |
|
| 210 |
+ 4383B466240F842700DAAEBF /* DataTypes */, |
|
| 211 |
+ 43CBF668240BF3ED00255B8B /* Assets.xcassets */, |
|
| 212 |
+ 43CBF66D240BF3ED00255B8B /* LaunchScreen.storyboard */, |
|
| 213 |
+ 43CBF670240BF3ED00255B8B /* Info.plist */, |
|
| 214 |
+ 43CBF66A240BF3ED00255B8B /* Preview Content */, |
|
| 215 |
+ ); |
|
| 216 |
+ path = "USB Meter"; |
|
| 217 |
+ sourceTree = "<group>"; |
|
| 218 |
+ }; |
|
| 219 |
+ 43CBF66A240BF3ED00255B8B /* Preview Content */ = {
|
|
| 220 |
+ isa = PBXGroup; |
|
| 221 |
+ children = ( |
|
| 222 |
+ 43CBF66B240BF3ED00255B8B /* Preview Assets.xcassets */, |
|
| 223 |
+ ); |
|
| 224 |
+ path = "Preview Content"; |
|
| 225 |
+ sourceTree = "<group>"; |
|
| 226 |
+ }; |
|
| 227 |
+ 43CBF678240C047D00255B8B /* Model */ = {
|
|
| 228 |
+ isa = PBXGroup; |
|
| 229 |
+ children = ( |
|
| 230 |
+ 4383B461240EB5E400DAAEBF /* AppData.swift */, |
|
| 231 |
+ 43CBF676240C043E00255B8B /* BluetoothManager.swift */, |
|
| 232 |
+ 4383B45F240EB2D000DAAEBF /* Meter.swift */, |
|
| 233 |
+ 43ED78AD2420A0BE00974487 /* BluetoothSerial.swift */, |
|
| 234 |
+ 439D996424234B98008DE3AA /* BluetoothRadio.swift */, |
|
| 235 |
+ 43CBF663240BF3EB00255B8B /* CKModel.xcdatamodeld */, |
|
| 236 |
+ 438695882463F062008855A9 /* Measurements.swift */, |
|
| 237 |
+ 432EA6432445A559006FC905 /* ChartContext.swift */, |
|
| 238 |
+ ); |
|
| 239 |
+ path = Model; |
|
| 240 |
+ sourceTree = "<group>"; |
|
| 241 |
+ }; |
|
| 242 |
+ 43CBF679240C08C600255B8B /* Views */ = {
|
|
| 243 |
+ isa = PBXGroup; |
|
| 244 |
+ children = ( |
|
| 245 |
+ 43CBF666240BF3EB00255B8B /* ContentView.swift */, |
|
| 246 |
+ 4327461A24619CED0009BE4B /* MeterRowView.swift */, |
|
| 247 |
+ 437D47CF2415F8CF00B7768E /* Meter */, |
|
| 248 |
+ 4311E639241384960080EA59 /* DeviceHelpView.swift */, |
|
| 249 |
+ 43DFBE3F2441A37B004A47EA /* BorderView.swift */, |
|
| 250 |
+ ); |
|
| 251 |
+ path = Views; |
|
| 252 |
+ sourceTree = "<group>"; |
|
| 253 |
+ }; |
|
| 254 |
+ 43CBF67F240D14AC00255B8B /* Extensions */ = {
|
|
| 255 |
+ isa = PBXGroup; |
|
| 256 |
+ children = ( |
|
| 257 |
+ 43CBF680240D153000255B8B /* CBManagerState.swift */, |
|
| 258 |
+ 4351E7BA24685ACD00E798A3 /* CGPoint.swift */, |
|
| 259 |
+ 43874C7E2414F3F400525397 /* Float.swift */, |
|
| 260 |
+ 43F7792A2465AE1600745DF4 /* UIView.swift */, |
|
| 261 |
+ 43874C842415611200525397 /* Double.swift */, |
|
| 262 |
+ 43874C82241533AD00525397 /* Data.swift */, |
|
| 263 |
+ 437D47D22415FB7E00B7768E /* Decimal.swift */, |
|
| 264 |
+ 43554B3324444B0E004E66F5 /* Date.swift */, |
|
| 265 |
+ 438B9554246D2D7500E61AE7 /* Path.swift */, |
|
| 266 |
+ ); |
|
| 267 |
+ path = Extensions; |
|
| 268 |
+ sourceTree = "<group>"; |
|
| 269 |
+ }; |
|
| 270 |
+/* End PBXGroup section */ |
|
| 271 |
+ |
|
| 272 |
+/* Begin PBXNativeTarget section */ |
|
| 273 |
+ 43CBF65B240BF3EB00255B8B /* USB Meter */ = {
|
|
| 274 |
+ isa = PBXNativeTarget; |
|
| 275 |
+ buildConfigurationList = 43CBF673240BF3ED00255B8B /* Build configuration list for PBXNativeTarget "USB Meter" */; |
|
| 276 |
+ buildPhases = ( |
|
| 277 |
+ 43CBF658240BF3EB00255B8B /* Sources */, |
|
| 278 |
+ 43CBF659240BF3EB00255B8B /* Frameworks */, |
|
| 279 |
+ 43CBF65A240BF3EB00255B8B /* Resources */, |
|
| 280 |
+ ); |
|
| 281 |
+ buildRules = ( |
|
| 282 |
+ ); |
|
| 283 |
+ dependencies = ( |
|
| 284 |
+ ); |
|
| 285 |
+ name = "USB Meter"; |
|
| 286 |
+ packageProductDependencies = ( |
|
| 287 |
+ 4347F01C28D717C1007EE7B1 /* CryptoSwift */, |
|
| 288 |
+ ); |
|
| 289 |
+ productName = "USB Meter"; |
|
| 290 |
+ productReference = 43CBF65C240BF3EB00255B8B /* USB Meter.app */; |
|
| 291 |
+ productType = "com.apple.product-type.application"; |
|
| 292 |
+ }; |
|
| 293 |
+/* End PBXNativeTarget section */ |
|
| 294 |
+ |
|
| 295 |
+/* Begin PBXProject section */ |
|
| 296 |
+ 43CBF654240BF3EB00255B8B /* Project object */ = {
|
|
| 297 |
+ isa = PBXProject; |
|
| 298 |
+ attributes = {
|
|
| 299 |
+ LastSwiftUpdateCheck = 1130; |
|
| 300 |
+ LastUpgradeCheck = 1200; |
|
| 301 |
+ ORGANIZATIONNAME = "Bogdan Timofte"; |
|
| 302 |
+ TargetAttributes = {
|
|
| 303 |
+ 43CBF65B240BF3EB00255B8B = {
|
|
| 304 |
+ CreatedOnToolsVersion = 11.3.1; |
|
| 305 |
+ }; |
|
| 306 |
+ }; |
|
| 307 |
+ }; |
|
| 308 |
+ buildConfigurationList = 43CBF657240BF3EB00255B8B /* Build configuration list for PBXProject "USB Meter" */; |
|
| 309 |
+ compatibilityVersion = "Xcode 9.3"; |
|
| 310 |
+ developmentRegion = en; |
|
| 311 |
+ hasScannedForEncodings = 0; |
|
| 312 |
+ knownRegions = ( |
|
| 313 |
+ en, |
|
| 314 |
+ Base, |
|
| 315 |
+ ); |
|
| 316 |
+ mainGroup = 43CBF653240BF3EB00255B8B; |
|
| 317 |
+ packageReferences = ( |
|
| 318 |
+ 437AEE162424AC3F0025C373 /* XCRemoteSwiftPackageReference "CryptoSwift" */, |
|
| 319 |
+ ); |
|
| 320 |
+ productRefGroup = 43CBF65D240BF3EB00255B8B /* Products */; |
|
| 321 |
+ projectDirPath = ""; |
|
| 322 |
+ projectRoot = ""; |
|
| 323 |
+ targets = ( |
|
| 324 |
+ 43CBF65B240BF3EB00255B8B /* USB Meter */, |
|
| 325 |
+ ); |
|
| 326 |
+ }; |
|
| 327 |
+/* End PBXProject section */ |
|
| 328 |
+ |
|
| 329 |
+/* Begin PBXResourcesBuildPhase section */ |
|
| 330 |
+ 43CBF65A240BF3EB00255B8B /* Resources */ = {
|
|
| 331 |
+ isa = PBXResourcesBuildPhase; |
|
| 332 |
+ buildActionMask = 2147483647; |
|
| 333 |
+ files = ( |
|
| 334 |
+ 43CBF66F240BF3ED00255B8B /* LaunchScreen.storyboard in Resources */, |
|
| 335 |
+ 43CBF66C240BF3ED00255B8B /* Preview Assets.xcassets in Resources */, |
|
| 336 |
+ 437AEE1524249AAA0025C373 /* Readme.rtf in Resources */, |
|
| 337 |
+ 43CBF669240BF3ED00255B8B /* Assets.xcassets in Resources */, |
|
| 338 |
+ ); |
|
| 339 |
+ runOnlyForDeploymentPostprocessing = 0; |
|
| 340 |
+ }; |
|
| 341 |
+/* End PBXResourcesBuildPhase section */ |
|
| 342 |
+ |
|
| 343 |
+/* Begin PBXSourcesBuildPhase section */ |
|
| 344 |
+ 43CBF658240BF3EB00255B8B /* Sources */ = {
|
|
| 345 |
+ isa = PBXSourcesBuildPhase; |
|
| 346 |
+ buildActionMask = 2147483647; |
|
| 347 |
+ files = ( |
|
| 348 |
+ 43874C852415611200525397 /* Double.swift in Sources */, |
|
| 349 |
+ 437D47D72415FDF300B7768E /* ControlView.swift in Sources */, |
|
| 350 |
+ 4308CF882417770D0002E80B /* DataGroupsView.swift in Sources */, |
|
| 351 |
+ 43567FE92443AD7C00000282 /* ICloudDefault.swift in Sources */, |
|
| 352 |
+ 4383B468240F845500DAAEBF /* MacAdress.swift in Sources */, |
|
| 353 |
+ 43CBF681240D153000255B8B /* CBManagerState.swift in Sources */, |
|
| 354 |
+ 4383B46A240FE4A600DAAEBF /* MeterView.swift in Sources */, |
|
| 355 |
+ 4360A34D241CBB3800B464F9 /* RSSIView.swift in Sources */, |
|
| 356 |
+ 437D47D12415F91B00B7768E /* LiveView.swift in Sources */, |
|
| 357 |
+ 43CBF665240BF3EB00255B8B /* CKModel.xcdatamodeld in Sources */, |
|
| 358 |
+ 4360A34F241D5CF100B464F9 /* MeterSettingsView.swift in Sources */, |
|
| 359 |
+ 4383B465240EB6B200DAAEBF /* UserDefault.swift in Sources */, |
|
| 360 |
+ 43CBF677240C043E00255B8B /* BluetoothManager.swift in Sources */, |
|
| 361 |
+ 43CBF660240BF3EB00255B8B /* AppDelegate.swift in Sources */, |
|
| 362 |
+ 438B9555246D2D7500E61AE7 /* Path.swift in Sources */, |
|
| 363 |
+ 4383B460240EB2D000DAAEBF /* Meter.swift in Sources */, |
|
| 364 |
+ 43CBF667240BF3EB00255B8B /* ContentView.swift in Sources */, |
|
| 365 |
+ 43DFBE402441A37B004A47EA /* BorderView.swift in Sources */, |
|
| 366 |
+ 437F0AB72463108F005DEBEC /* MeasurementChartView.swift in Sources */, |
|
| 367 |
+ 437D47D32415FB7E00B7768E /* Decimal.swift in Sources */, |
|
| 368 |
+ 43874C7F2414F3F400525397 /* Float.swift in Sources */, |
|
| 369 |
+ 4383B462240EB5E400DAAEBF /* AppData.swift in Sources */, |
|
| 370 |
+ 437D47D52415FD8C00B7768E /* RecordingView.swift in Sources */, |
|
| 371 |
+ 432EA6442445A559006FC905 /* ChartContext.swift in Sources */, |
|
| 372 |
+ 4308CF8624176CAB0002E80B /* DataGroupRowView.swift in Sources */, |
|
| 373 |
+ 43554B32244449B5004E66F5 /* MeasurementPointView.swift in Sources */, |
|
| 374 |
+ 43F7792B2465AE1600745DF4 /* UIView.swift in Sources */, |
|
| 375 |
+ 43ED78AE2420A0BE00974487 /* BluetoothSerial.swift in Sources */, |
|
| 376 |
+ 43CBF662240BF3EB00255B8B /* SceneDelegate.swift in Sources */, |
|
| 377 |
+ 4351E7BB24685ACD00E798A3 /* CGPoint.swift in Sources */, |
|
| 378 |
+ 4327461B24619CED0009BE4B /* MeterRowView.swift in Sources */, |
|
| 379 |
+ 43554B2F24443939004E66F5 /* MeasurementsView.swift in Sources */, |
|
| 380 |
+ 430CB4FC245E07EB006525C2 /* ChevronView.swift in Sources */, |
|
| 381 |
+ 43554B3424444B0E004E66F5 /* Date.swift in Sources */, |
|
| 382 |
+ 4311E63A241384960080EA59 /* DeviceHelpView.swift in Sources */, |
|
| 383 |
+ 439D996524234B98008DE3AA /* BluetoothRadio.swift in Sources */, |
|
| 384 |
+ 438695892463F062008855A9 /* Measurements.swift in Sources */, |
|
| 385 |
+ 43874C83241533AD00525397 /* Data.swift in Sources */, |
|
| 386 |
+ ); |
|
| 387 |
+ runOnlyForDeploymentPostprocessing = 0; |
|
| 388 |
+ }; |
|
| 389 |
+/* End PBXSourcesBuildPhase section */ |
|
| 390 |
+ |
|
| 391 |
+/* Begin PBXVariantGroup section */ |
|
| 392 |
+ 43CBF66D240BF3ED00255B8B /* LaunchScreen.storyboard */ = {
|
|
| 393 |
+ isa = PBXVariantGroup; |
|
| 394 |
+ children = ( |
|
| 395 |
+ 43CBF66E240BF3ED00255B8B /* Base */, |
|
| 396 |
+ ); |
|
| 397 |
+ name = LaunchScreen.storyboard; |
|
| 398 |
+ sourceTree = "<group>"; |
|
| 399 |
+ }; |
|
| 400 |
+/* End PBXVariantGroup section */ |
|
| 401 |
+ |
|
| 402 |
+/* Begin XCBuildConfiguration section */ |
|
| 403 |
+ 43CBF671240BF3ED00255B8B /* Debug */ = {
|
|
| 404 |
+ isa = XCBuildConfiguration; |
|
| 405 |
+ buildSettings = {
|
|
| 406 |
+ ALWAYS_SEARCH_USER_PATHS = NO; |
|
| 407 |
+ CLANG_ANALYZER_NONNULL = YES; |
|
| 408 |
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; |
|
| 409 |
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; |
|
| 410 |
+ CLANG_CXX_LIBRARY = "libc++"; |
|
| 411 |
+ CLANG_ENABLE_MODULES = YES; |
|
| 412 |
+ CLANG_ENABLE_OBJC_ARC = YES; |
|
| 413 |
+ CLANG_ENABLE_OBJC_WEAK = YES; |
|
| 414 |
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; |
|
| 415 |
+ CLANG_WARN_BOOL_CONVERSION = YES; |
|
| 416 |
+ CLANG_WARN_COMMA = YES; |
|
| 417 |
+ CLANG_WARN_CONSTANT_CONVERSION = YES; |
|
| 418 |
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; |
|
| 419 |
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; |
|
| 420 |
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; |
|
| 421 |
+ CLANG_WARN_EMPTY_BODY = YES; |
|
| 422 |
+ CLANG_WARN_ENUM_CONVERSION = YES; |
|
| 423 |
+ CLANG_WARN_INFINITE_RECURSION = YES; |
|
| 424 |
+ CLANG_WARN_INT_CONVERSION = YES; |
|
| 425 |
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; |
|
| 426 |
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; |
|
| 427 |
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; |
|
| 428 |
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; |
|
| 429 |
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; |
|
| 430 |
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; |
|
| 431 |
+ CLANG_WARN_STRICT_PROTOTYPES = YES; |
|
| 432 |
+ CLANG_WARN_SUSPICIOUS_MOVE = YES; |
|
| 433 |
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; |
|
| 434 |
+ CLANG_WARN_UNREACHABLE_CODE = YES; |
|
| 435 |
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; |
|
| 436 |
+ COPY_PHASE_STRIP = NO; |
|
| 437 |
+ DEBUG_INFORMATION_FORMAT = dwarf; |
|
| 438 |
+ ENABLE_STRICT_OBJC_MSGSEND = YES; |
|
| 439 |
+ ENABLE_TESTABILITY = YES; |
|
| 440 |
+ GCC_C_LANGUAGE_STANDARD = gnu11; |
|
| 441 |
+ GCC_DYNAMIC_NO_PIC = NO; |
|
| 442 |
+ GCC_NO_COMMON_BLOCKS = YES; |
|
| 443 |
+ GCC_OPTIMIZATION_LEVEL = 0; |
|
| 444 |
+ GCC_PREPROCESSOR_DEFINITIONS = ( |
|
| 445 |
+ "DEBUG=1", |
|
| 446 |
+ "$(inherited)", |
|
| 447 |
+ ); |
|
| 448 |
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; |
|
| 449 |
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; |
|
| 450 |
+ GCC_WARN_UNDECLARED_SELECTOR = YES; |
|
| 451 |
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
|
| 452 |
+ GCC_WARN_UNUSED_FUNCTION = YES; |
|
| 453 |
+ GCC_WARN_UNUSED_VARIABLE = YES; |
|
| 454 |
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0; |
|
| 455 |
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; |
|
| 456 |
+ MTL_FAST_MATH = YES; |
|
| 457 |
+ ONLY_ACTIVE_ARCH = YES; |
|
| 458 |
+ SDKROOT = iphoneos; |
|
| 459 |
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; |
|
| 460 |
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; |
|
| 461 |
+ }; |
|
| 462 |
+ name = Debug; |
|
| 463 |
+ }; |
|
| 464 |
+ 43CBF672240BF3ED00255B8B /* Release */ = {
|
|
| 465 |
+ isa = XCBuildConfiguration; |
|
| 466 |
+ buildSettings = {
|
|
| 467 |
+ ALWAYS_SEARCH_USER_PATHS = NO; |
|
| 468 |
+ CLANG_ANALYZER_NONNULL = YES; |
|
| 469 |
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; |
|
| 470 |
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; |
|
| 471 |
+ CLANG_CXX_LIBRARY = "libc++"; |
|
| 472 |
+ CLANG_ENABLE_MODULES = YES; |
|
| 473 |
+ CLANG_ENABLE_OBJC_ARC = YES; |
|
| 474 |
+ CLANG_ENABLE_OBJC_WEAK = YES; |
|
| 475 |
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; |
|
| 476 |
+ CLANG_WARN_BOOL_CONVERSION = YES; |
|
| 477 |
+ CLANG_WARN_COMMA = YES; |
|
| 478 |
+ CLANG_WARN_CONSTANT_CONVERSION = YES; |
|
| 479 |
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; |
|
| 480 |
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; |
|
| 481 |
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; |
|
| 482 |
+ CLANG_WARN_EMPTY_BODY = YES; |
|
| 483 |
+ CLANG_WARN_ENUM_CONVERSION = YES; |
|
| 484 |
+ CLANG_WARN_INFINITE_RECURSION = YES; |
|
| 485 |
+ CLANG_WARN_INT_CONVERSION = YES; |
|
| 486 |
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; |
|
| 487 |
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; |
|
| 488 |
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; |
|
| 489 |
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; |
|
| 490 |
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; |
|
| 491 |
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; |
|
| 492 |
+ CLANG_WARN_STRICT_PROTOTYPES = YES; |
|
| 493 |
+ CLANG_WARN_SUSPICIOUS_MOVE = YES; |
|
| 494 |
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; |
|
| 495 |
+ CLANG_WARN_UNREACHABLE_CODE = YES; |
|
| 496 |
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; |
|
| 497 |
+ COPY_PHASE_STRIP = NO; |
|
| 498 |
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; |
|
| 499 |
+ ENABLE_NS_ASSERTIONS = NO; |
|
| 500 |
+ ENABLE_STRICT_OBJC_MSGSEND = YES; |
|
| 501 |
+ GCC_C_LANGUAGE_STANDARD = gnu11; |
|
| 502 |
+ GCC_NO_COMMON_BLOCKS = YES; |
|
| 503 |
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; |
|
| 504 |
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; |
|
| 505 |
+ GCC_WARN_UNDECLARED_SELECTOR = YES; |
|
| 506 |
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
|
| 507 |
+ GCC_WARN_UNUSED_FUNCTION = YES; |
|
| 508 |
+ GCC_WARN_UNUSED_VARIABLE = YES; |
|
| 509 |
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0; |
|
| 510 |
+ MTL_ENABLE_DEBUG_INFO = NO; |
|
| 511 |
+ MTL_FAST_MATH = YES; |
|
| 512 |
+ SDKROOT = iphoneos; |
|
| 513 |
+ SWIFT_COMPILATION_MODE = wholemodule; |
|
| 514 |
+ SWIFT_OPTIMIZATION_LEVEL = "-O"; |
|
| 515 |
+ VALIDATE_PRODUCT = YES; |
|
| 516 |
+ }; |
|
| 517 |
+ name = Release; |
|
| 518 |
+ }; |
|
| 519 |
+ 43CBF674240BF3ED00255B8B /* Debug */ = {
|
|
| 520 |
+ isa = XCBuildConfiguration; |
|
| 521 |
+ buildSettings = {
|
|
| 522 |
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
|
| 523 |
+ CODE_SIGN_ENTITLEMENTS = "USB Meter/USB Meter.entitlements"; |
|
| 524 |
+ CODE_SIGN_STYLE = Automatic; |
|
| 525 |
+ DEVELOPMENT_ASSET_PATHS = "\"USB Meter/Preview Content\""; |
|
| 526 |
+ DEVELOPMENT_TEAM = 9K2U3V9GZF; |
|
| 527 |
+ ENABLE_PREVIEWS = YES; |
|
| 528 |
+ INFOPLIST_FILE = "USB Meter/Info.plist"; |
|
| 529 |
+ LD_RUNPATH_SEARCH_PATHS = ( |
|
| 530 |
+ "$(inherited)", |
|
| 531 |
+ "@executable_path/Frameworks", |
|
| 532 |
+ ); |
|
| 533 |
+ PRODUCT_BUNDLE_IDENTIFIER = "ro.xdev.USB-Meter"; |
|
| 534 |
+ PRODUCT_NAME = "$(TARGET_NAME)"; |
|
| 535 |
+ SUPPORTS_MACCATALYST = YES; |
|
| 536 |
+ SWIFT_VERSION = 5.0; |
|
| 537 |
+ TARGETED_DEVICE_FAMILY = "1,2"; |
|
| 538 |
+ }; |
|
| 539 |
+ name = Debug; |
|
| 540 |
+ }; |
|
| 541 |
+ 43CBF675240BF3ED00255B8B /* Release */ = {
|
|
| 542 |
+ isa = XCBuildConfiguration; |
|
| 543 |
+ buildSettings = {
|
|
| 544 |
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
|
| 545 |
+ CODE_SIGN_ENTITLEMENTS = "USB Meter/USB Meter.entitlements"; |
|
| 546 |
+ CODE_SIGN_STYLE = Automatic; |
|
| 547 |
+ DEVELOPMENT_ASSET_PATHS = "\"USB Meter/Preview Content\""; |
|
| 548 |
+ DEVELOPMENT_TEAM = 9K2U3V9GZF; |
|
| 549 |
+ ENABLE_PREVIEWS = YES; |
|
| 550 |
+ INFOPLIST_FILE = "USB Meter/Info.plist"; |
|
| 551 |
+ LD_RUNPATH_SEARCH_PATHS = ( |
|
| 552 |
+ "$(inherited)", |
|
| 553 |
+ "@executable_path/Frameworks", |
|
| 554 |
+ ); |
|
| 555 |
+ PRODUCT_BUNDLE_IDENTIFIER = "ro.xdev.USB-Meter"; |
|
| 556 |
+ PRODUCT_NAME = "$(TARGET_NAME)"; |
|
| 557 |
+ SUPPORTS_MACCATALYST = YES; |
|
| 558 |
+ SWIFT_VERSION = 5.0; |
|
| 559 |
+ TARGETED_DEVICE_FAMILY = "1,2"; |
|
| 560 |
+ }; |
|
| 561 |
+ name = Release; |
|
| 562 |
+ }; |
|
| 563 |
+/* End XCBuildConfiguration section */ |
|
| 564 |
+ |
|
| 565 |
+/* Begin XCConfigurationList section */ |
|
| 566 |
+ 43CBF657240BF3EB00255B8B /* Build configuration list for PBXProject "USB Meter" */ = {
|
|
| 567 |
+ isa = XCConfigurationList; |
|
| 568 |
+ buildConfigurations = ( |
|
| 569 |
+ 43CBF671240BF3ED00255B8B /* Debug */, |
|
| 570 |
+ 43CBF672240BF3ED00255B8B /* Release */, |
|
| 571 |
+ ); |
|
| 572 |
+ defaultConfigurationIsVisible = 0; |
|
| 573 |
+ defaultConfigurationName = Release; |
|
| 574 |
+ }; |
|
| 575 |
+ 43CBF673240BF3ED00255B8B /* Build configuration list for PBXNativeTarget "USB Meter" */ = {
|
|
| 576 |
+ isa = XCConfigurationList; |
|
| 577 |
+ buildConfigurations = ( |
|
| 578 |
+ 43CBF674240BF3ED00255B8B /* Debug */, |
|
| 579 |
+ 43CBF675240BF3ED00255B8B /* Release */, |
|
| 580 |
+ ); |
|
| 581 |
+ defaultConfigurationIsVisible = 0; |
|
| 582 |
+ defaultConfigurationName = Release; |
|
| 583 |
+ }; |
|
| 584 |
+/* End XCConfigurationList section */ |
|
| 585 |
+ |
|
| 586 |
+/* Begin XCRemoteSwiftPackageReference section */ |
|
| 587 |
+ 437AEE162424AC3F0025C373 /* XCRemoteSwiftPackageReference "CryptoSwift" */ = {
|
|
| 588 |
+ isa = XCRemoteSwiftPackageReference; |
|
| 589 |
+ repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git"; |
|
| 590 |
+ requirement = {
|
|
| 591 |
+ branch = master; |
|
| 592 |
+ kind = branch; |
|
| 593 |
+ }; |
|
| 594 |
+ }; |
|
| 595 |
+/* End XCRemoteSwiftPackageReference section */ |
|
| 596 |
+ |
|
| 597 |
+/* Begin XCSwiftPackageProductDependency section */ |
|
| 598 |
+ 4347F01C28D717C1007EE7B1 /* CryptoSwift */ = {
|
|
| 599 |
+ isa = XCSwiftPackageProductDependency; |
|
| 600 |
+ package = 437AEE162424AC3F0025C373 /* XCRemoteSwiftPackageReference "CryptoSwift" */; |
|
| 601 |
+ productName = CryptoSwift; |
|
| 602 |
+ }; |
|
| 603 |
+/* End XCSwiftPackageProductDependency section */ |
|
| 604 |
+ |
|
| 605 |
+/* Begin XCVersionGroup section */ |
|
| 606 |
+ 43CBF663240BF3EB00255B8B /* CKModel.xcdatamodeld */ = {
|
|
| 607 |
+ isa = XCVersionGroup; |
|
| 608 |
+ children = ( |
|
| 609 |
+ 43CBF664240BF3EB00255B8B /* USB_Meter.xcdatamodel */, |
|
| 610 |
+ ); |
|
| 611 |
+ currentVersion = 43CBF664240BF3EB00255B8B /* USB_Meter.xcdatamodel */; |
|
| 612 |
+ path = CKModel.xcdatamodeld; |
|
| 613 |
+ sourceTree = "<group>"; |
|
| 614 |
+ versionGroupType = wrapper.xcdatamodel; |
|
| 615 |
+ }; |
|
| 616 |
+/* End XCVersionGroup section */ |
|
| 617 |
+ }; |
|
| 618 |
+ rootObject = 43CBF654240BF3EB00255B8B /* Project object */; |
|
| 619 |
+} |
|
@@ -0,0 +1,7 @@ |
||
| 1 |
+<?xml version="1.0" encoding="UTF-8"?> |
|
| 2 |
+<Workspace |
|
| 3 |
+ version = "1.0"> |
|
| 4 |
+ <FileRef |
|
| 5 |
+ location = "self:"> |
|
| 6 |
+ </FileRef> |
|
| 7 |
+</Workspace> |
|
@@ -0,0 +1,8 @@ |
||
| 1 |
+<?xml version="1.0" encoding="UTF-8"?> |
|
| 2 |
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|
| 3 |
+<plist version="1.0"> |
|
| 4 |
+<dict> |
|
| 5 |
+ <key>IDEDidComputeMac32BitWarning</key> |
|
| 6 |
+ <true/> |
|
| 7 |
+</dict> |
|
| 8 |
+</plist> |
|
@@ -0,0 +1,16 @@ |
||
| 1 |
+{
|
|
| 2 |
+ "object": {
|
|
| 3 |
+ "pins": [ |
|
| 4 |
+ {
|
|
| 5 |
+ "package": "CryptoSwift", |
|
| 6 |
+ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", |
|
| 7 |
+ "state": {
|
|
| 8 |
+ "branch": "master", |
|
| 9 |
+ "revision": "bdfc481d79e3204480d1a9bee4c12f66b84a4b5d", |
|
| 10 |
+ "version": null |
|
| 11 |
+ } |
|
| 12 |
+ } |
|
| 13 |
+ ] |
|
| 14 |
+ }, |
|
| 15 |
+ "version": 1 |
|
| 16 |
+} |
|
@@ -0,0 +1,99 @@ |
||
| 1 |
+// |
|
| 2 |
+// AppDelegate.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 01/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import UIKit |
|
| 10 |
+import CoreData |
|
| 11 |
+ |
|
| 12 |
+//let btSerial = BluetoothSerial(delegate: BSD()) |
|
| 13 |
+let appData = AppData() |
|
| 14 |
+enum Constants {
|
|
| 15 |
+ static let chartUnderscan: CGFloat = 0.5 |
|
| 16 |
+ static let chartOverscan: CGFloat = 1 - chartUnderscan |
|
| 17 |
+} |
|
| 18 |
+// MARK: Clock |
|
| 19 |
+ |
|
| 20 |
+// MARK: Debug |
|
| 21 |
+public func track(_ message: String = "", file: String = #file, function: String = #function, line: Int = #line ) {
|
|
| 22 |
+ let date = Date() |
|
| 23 |
+ let calendar = Calendar.current |
|
| 24 |
+ let hour = calendar.component(.hour, from: date) |
|
| 25 |
+ let minutes = calendar.component(.minute, from: date) |
|
| 26 |
+ let seconds = calendar.component(.second, from: date) |
|
| 27 |
+ print("\(hour):\(minutes):\(seconds) - \(file):\(line) - \(function) \(message)")
|
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+@UIApplicationMain |
|
| 31 |
+class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
| 32 |
+ |
|
| 33 |
+ |
|
| 34 |
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
| 35 |
+ // Override point for customization after application launch. |
|
| 36 |
+ return true |
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ // MARK: UISceneSession Lifecycle |
|
| 40 |
+ |
|
| 41 |
+ func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
|
| 42 |
+ // Called when a new scene session is being created. |
|
| 43 |
+ // Use this method to select a configuration to create the new scene with. |
|
| 44 |
+ return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) |
|
| 45 |
+ } |
|
| 46 |
+ |
|
| 47 |
+ func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
|
| 48 |
+ // Called when the user discards a scene session. |
|
| 49 |
+ // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. |
|
| 50 |
+ // Use this method to release any resources that were specific to the discarded scenes, as they will not return. |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ // MARK: - Core Data stack |
|
| 54 |
+ |
|
| 55 |
+ lazy var persistentContainer: NSPersistentCloudKitContainer = {
|
|
| 56 |
+ /* |
|
| 57 |
+ The persistent container for the application. This implementation |
|
| 58 |
+ creates and returns a container, having loaded the store for the |
|
| 59 |
+ application to it. This property is optional since there are legitimate |
|
| 60 |
+ error conditions that could cause the creation of the store to fail. |
|
| 61 |
+ */ |
|
| 62 |
+ let container = NSPersistentCloudKitContainer(name: "CKModel") |
|
| 63 |
+ container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
|
| 64 |
+ if let error = error as NSError? {
|
|
| 65 |
+ // Replace this implementation with code to handle the error appropriately. |
|
| 66 |
+ // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. |
|
| 67 |
+ |
|
| 68 |
+ /* |
|
| 69 |
+ Typical reasons for an error here include: |
|
| 70 |
+ * The parent directory does not exist, cannot be created, or disallows writing. |
|
| 71 |
+ * The persistent store is not accessible, due to permissions or data protection when the device is locked. |
|
| 72 |
+ * The device is out of space. |
|
| 73 |
+ * The store could not be migrated to the current model version. |
|
| 74 |
+ Check the error message to determine what the actual problem was. |
|
| 75 |
+ */ |
|
| 76 |
+ fatalError("Unresolved error \(error), \(error.userInfo)")
|
|
| 77 |
+ } |
|
| 78 |
+ }) |
|
| 79 |
+ return container |
|
| 80 |
+ }() |
|
| 81 |
+ |
|
| 82 |
+ // MARK: - Core Data Saving support |
|
| 83 |
+ |
|
| 84 |
+ func saveContext () {
|
|
| 85 |
+ let context = persistentContainer.viewContext |
|
| 86 |
+ if context.hasChanges {
|
|
| 87 |
+ do {
|
|
| 88 |
+ try context.save() |
|
| 89 |
+ } catch {
|
|
| 90 |
+ // Replace this implementation with code to handle the error appropriately. |
|
| 91 |
+ // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. |
|
| 92 |
+ let nserror = error as NSError |
|
| 93 |
+ fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
|
|
| 94 |
+ } |
|
| 95 |
+ } |
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+} |
|
| 99 |
+ |
|
@@ -0,0 +1,98 @@ |
||
| 1 |
+{
|
|
| 2 |
+ "images" : [ |
|
| 3 |
+ {
|
|
| 4 |
+ "idiom" : "iphone", |
|
| 5 |
+ "size" : "20x20", |
|
| 6 |
+ "scale" : "2x" |
|
| 7 |
+ }, |
|
| 8 |
+ {
|
|
| 9 |
+ "idiom" : "iphone", |
|
| 10 |
+ "size" : "20x20", |
|
| 11 |
+ "scale" : "3x" |
|
| 12 |
+ }, |
|
| 13 |
+ {
|
|
| 14 |
+ "idiom" : "iphone", |
|
| 15 |
+ "size" : "29x29", |
|
| 16 |
+ "scale" : "2x" |
|
| 17 |
+ }, |
|
| 18 |
+ {
|
|
| 19 |
+ "idiom" : "iphone", |
|
| 20 |
+ "size" : "29x29", |
|
| 21 |
+ "scale" : "3x" |
|
| 22 |
+ }, |
|
| 23 |
+ {
|
|
| 24 |
+ "idiom" : "iphone", |
|
| 25 |
+ "size" : "40x40", |
|
| 26 |
+ "scale" : "2x" |
|
| 27 |
+ }, |
|
| 28 |
+ {
|
|
| 29 |
+ "idiom" : "iphone", |
|
| 30 |
+ "size" : "40x40", |
|
| 31 |
+ "scale" : "3x" |
|
| 32 |
+ }, |
|
| 33 |
+ {
|
|
| 34 |
+ "idiom" : "iphone", |
|
| 35 |
+ "size" : "60x60", |
|
| 36 |
+ "scale" : "2x" |
|
| 37 |
+ }, |
|
| 38 |
+ {
|
|
| 39 |
+ "idiom" : "iphone", |
|
| 40 |
+ "size" : "60x60", |
|
| 41 |
+ "scale" : "3x" |
|
| 42 |
+ }, |
|
| 43 |
+ {
|
|
| 44 |
+ "idiom" : "ipad", |
|
| 45 |
+ "size" : "20x20", |
|
| 46 |
+ "scale" : "1x" |
|
| 47 |
+ }, |
|
| 48 |
+ {
|
|
| 49 |
+ "idiom" : "ipad", |
|
| 50 |
+ "size" : "20x20", |
|
| 51 |
+ "scale" : "2x" |
|
| 52 |
+ }, |
|
| 53 |
+ {
|
|
| 54 |
+ "idiom" : "ipad", |
|
| 55 |
+ "size" : "29x29", |
|
| 56 |
+ "scale" : "1x" |
|
| 57 |
+ }, |
|
| 58 |
+ {
|
|
| 59 |
+ "idiom" : "ipad", |
|
| 60 |
+ "size" : "29x29", |
|
| 61 |
+ "scale" : "2x" |
|
| 62 |
+ }, |
|
| 63 |
+ {
|
|
| 64 |
+ "idiom" : "ipad", |
|
| 65 |
+ "size" : "40x40", |
|
| 66 |
+ "scale" : "1x" |
|
| 67 |
+ }, |
|
| 68 |
+ {
|
|
| 69 |
+ "idiom" : "ipad", |
|
| 70 |
+ "size" : "40x40", |
|
| 71 |
+ "scale" : "2x" |
|
| 72 |
+ }, |
|
| 73 |
+ {
|
|
| 74 |
+ "idiom" : "ipad", |
|
| 75 |
+ "size" : "76x76", |
|
| 76 |
+ "scale" : "1x" |
|
| 77 |
+ }, |
|
| 78 |
+ {
|
|
| 79 |
+ "idiom" : "ipad", |
|
| 80 |
+ "size" : "76x76", |
|
| 81 |
+ "scale" : "2x" |
|
| 82 |
+ }, |
|
| 83 |
+ {
|
|
| 84 |
+ "idiom" : "ipad", |
|
| 85 |
+ "size" : "83.5x83.5", |
|
| 86 |
+ "scale" : "2x" |
|
| 87 |
+ }, |
|
| 88 |
+ {
|
|
| 89 |
+ "idiom" : "ios-marketing", |
|
| 90 |
+ "size" : "1024x1024", |
|
| 91 |
+ "scale" : "1x" |
|
| 92 |
+ } |
|
| 93 |
+ ], |
|
| 94 |
+ "info" : {
|
|
| 95 |
+ "version" : 1, |
|
| 96 |
+ "author" : "xcode" |
|
| 97 |
+ } |
|
| 98 |
+} |
|
@@ -0,0 +1,6 @@ |
||
| 1 |
+{
|
|
| 2 |
+ "info" : {
|
|
| 3 |
+ "version" : 1, |
|
| 4 |
+ "author" : "xcode" |
|
| 5 |
+ } |
|
| 6 |
+} |
|
@@ -0,0 +1,25 @@ |
||
| 1 |
+<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
|
| 2 |
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM"> |
|
| 3 |
+ <dependencies> |
|
| 4 |
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/> |
|
| 5 |
+ <capability name="Safe area layout guides" minToolsVersion="9.0"/> |
|
| 6 |
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> |
|
| 7 |
+ </dependencies> |
|
| 8 |
+ <scenes> |
|
| 9 |
+ <!--View Controller--> |
|
| 10 |
+ <scene sceneID="EHf-IW-A2E"> |
|
| 11 |
+ <objects> |
|
| 12 |
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController"> |
|
| 13 |
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> |
|
| 14 |
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> |
|
| 15 |
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> |
|
| 16 |
+ <color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> |
|
| 17 |
+ <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/> |
|
| 18 |
+ </view> |
|
| 19 |
+ </viewController> |
|
| 20 |
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/> |
|
| 21 |
+ </objects> |
|
| 22 |
+ <point key="canvasLocation" x="53" y="375"/> |
|
| 23 |
+ </scene> |
|
| 24 |
+ </scenes> |
|
| 25 |
+</document> |
|
@@ -0,0 +1,36 @@ |
||
| 1 |
+// |
|
| 2 |
+// MacAdress.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 04/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import Foundation |
|
| 10 |
+ |
|
| 11 |
+class MACAddress {
|
|
| 12 |
+ var bytes: [UInt8] = [0,0,0,0,0,0] |
|
| 13 |
+ |
|
| 14 |
+ init(from data: Data) {
|
|
| 15 |
+ data.copyBytes(to: &bytes, count: bytes.count) |
|
| 16 |
+ } |
|
| 17 |
+ |
|
| 18 |
+ init(from str: String) {
|
|
| 19 |
+ let macAddressParts = str.split(separator: ":") |
|
| 20 |
+ for index in stride(from: 0, to: 5, by: 1){
|
|
| 21 |
+ bytes[index] = UInt8(macAddressParts[index], radix: 16) ?? 0xff; |
|
| 22 |
+ } |
|
| 23 |
+ } |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+extension MACAddress : CustomStringConvertible {
|
|
| 27 |
+ var description: String {
|
|
| 28 |
+ var retval = String(format: "%02X", bytes[0] ) |
|
| 29 |
+ retval += ":" + String(format: "%02X", bytes[1] ) |
|
| 30 |
+ retval += ":" + String(format: "%02X", bytes[2] ) |
|
| 31 |
+ retval += ":" + String(format: "%02X", bytes[3] ) |
|
| 32 |
+ retval += ":" + String(format: "%02X", bytes[4] ) |
|
| 33 |
+ retval += ":" + String(format: "%02X", bytes[5] ) |
|
| 34 |
+ return retval; |
|
| 35 |
+ } |
|
| 36 |
+} |
|
@@ -0,0 +1,137 @@ |
||
| 1 |
+// |
|
| 2 |
+// CBManagerState.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 02/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+//import Foundation |
|
| 10 |
+ |
|
| 11 |
+import CoreBluetooth |
|
| 12 |
+import SwiftUI |
|
| 13 |
+ |
|
| 14 |
+//Manager States |
|
| 15 |
+//.poweredOff A state that indicates Bluetooth is currently powered off. |
|
| 16 |
+//.poweredOn A state that indicates Bluetooth is currently powered on and available to use. |
|
| 17 |
+//.resetting A state that indicates the connection with the system service was momentarily lost. |
|
| 18 |
+//.unauthorized A state that indicates the application isn’t authorized to use the Bluetooth low energy role. |
|
| 19 |
+//.unknown The manager’s state is unknown. |
|
| 20 |
+//.unsupported A state that indicates this device doesn’t support the Bluetooth low energy central or client role. |
|
| 21 |
+ |
|
| 22 |
+extension CBManagerState {
|
|
| 23 |
+ var description: String {
|
|
| 24 |
+ switch self {
|
|
| 25 |
+ case .poweredOff: |
|
| 26 |
+ return "CBManagerState.poweredOff" |
|
| 27 |
+ case .poweredOn: |
|
| 28 |
+ return "CBManagerState.poweredOn" |
|
| 29 |
+ case .resetting: |
|
| 30 |
+ return "CBManagerState.resetting" |
|
| 31 |
+ case .unauthorized: |
|
| 32 |
+ return "CBManagerState.unauthorized" |
|
| 33 |
+ case .unknown: |
|
| 34 |
+ return "CBManagerState.unknown" |
|
| 35 |
+ case .unsupported: |
|
| 36 |
+ return "CBManagerState.unsupported" |
|
| 37 |
+ default: |
|
| 38 |
+ return "CBManagerState.other" |
|
| 39 |
+ } |
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ var color: Color {
|
|
| 43 |
+ switch self {
|
|
| 44 |
+ case .poweredOff: |
|
| 45 |
+ return Color.red |
|
| 46 |
+ case .poweredOn: |
|
| 47 |
+ return Color.blue |
|
| 48 |
+ case .resetting: |
|
| 49 |
+ return Color.green |
|
| 50 |
+ case .unauthorized: |
|
| 51 |
+ return Color.orange |
|
| 52 |
+ case .unknown: |
|
| 53 |
+ return Color.secondary |
|
| 54 |
+ case .unsupported: |
|
| 55 |
+ return Color.gray |
|
| 56 |
+ default: |
|
| 57 |
+ return Color.yellow |
|
| 58 |
+ } |
|
| 59 |
+ } |
|
| 60 |
+ |
|
| 61 |
+ var helpView: AnyView {
|
|
| 62 |
+ switch self {
|
|
| 63 |
+ case .poweredOff: |
|
| 64 |
+ return AnyView(poweredOffHelperView()) |
|
| 65 |
+ case .poweredOn: |
|
| 66 |
+ return AnyView(poweredOnHelperView()) |
|
| 67 |
+ case .resetting: |
|
| 68 |
+ return AnyView(resettingHelperView()) |
|
| 69 |
+ case .unauthorized: |
|
| 70 |
+ return AnyView(unauthorizedHelperView()) |
|
| 71 |
+ case .unknown: |
|
| 72 |
+ return AnyView(unknownHelperView()) |
|
| 73 |
+ case .unsupported: |
|
| 74 |
+ return AnyView(unsupportedHelperView()) |
|
| 75 |
+ default: |
|
| 76 |
+ return AnyView(defaultHelperView()) |
|
| 77 |
+ } |
|
| 78 |
+ } |
|
| 79 |
+ |
|
| 80 |
+ private struct poweredOffHelperView: View {
|
|
| 81 |
+ var body: some View {
|
|
| 82 |
+ Text("Bluetooth is turned off on this device. Tou can turn it on in Settings->Bluetooth")
|
|
| 83 |
+ } |
|
| 84 |
+ } |
|
| 85 |
+ private struct poweredOnHelperView: View {
|
|
| 86 |
+ var body: some View {
|
|
| 87 |
+ Text("Bluetooth is up an running")
|
|
| 88 |
+ } |
|
| 89 |
+ } |
|
| 90 |
+ |
|
| 91 |
+ private struct resettingHelperView: View {
|
|
| 92 |
+ var body: some View {
|
|
| 93 |
+ VStack {
|
|
| 94 |
+ Text("Bluetooth is resetting")
|
|
| 95 |
+ Text("Maybe wait for a while...")
|
|
| 96 |
+ } |
|
| 97 |
+ } |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ private struct unauthorizedHelperView: View {
|
|
| 101 |
+ var body: some View {
|
|
| 102 |
+ HStack {
|
|
| 103 |
+ Text("This application does not have permission to access Bluetooth. You can give it in ")
|
|
| 104 |
+ Button(action: { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) }) {
|
|
| 105 |
+ Text("Settings")
|
|
| 106 |
+ } |
|
| 107 |
+ } |
|
| 108 |
+ } |
|
| 109 |
+ } |
|
| 110 |
+ |
|
| 111 |
+ private struct unknownHelperView: View {
|
|
| 112 |
+ var body: some View {
|
|
| 113 |
+ VStack {
|
|
| 114 |
+ Text("Bluetooth state is unknown!")
|
|
| 115 |
+ Text("There is no help available for this situation.")
|
|
| 116 |
+ } |
|
| 117 |
+ } |
|
| 118 |
+ } |
|
| 119 |
+ |
|
| 120 |
+ private struct unsupportedHelperView: View {
|
|
| 121 |
+ var body: some View {
|
|
| 122 |
+ VStack {
|
|
| 123 |
+ Text("Your device does not have required capabilities to establish Bluetooth connections with USB Meters")
|
|
| 124 |
+ Text("There is no help available for this situation.")
|
|
| 125 |
+ } |
|
| 126 |
+ } |
|
| 127 |
+ } |
|
| 128 |
+ |
|
| 129 |
+ private struct defaultHelperView: View {
|
|
| 130 |
+ var body: some View {
|
|
| 131 |
+ VStack {
|
|
| 132 |
+ Text("Unknown Bluetooth state.")
|
|
| 133 |
+ Text("You may contact develloper.")
|
|
| 134 |
+ } |
|
| 135 |
+ } |
|
| 136 |
+ } |
|
| 137 |
+} |
|
@@ -0,0 +1,28 @@ |
||
| 1 |
+// |
|
| 2 |
+// CGPoint.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 10/05/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import CoreGraphics |
|
| 10 |
+ |
|
| 11 |
+extension CGPoint {
|
|
| 12 |
+ func moveDown(by: CGFloat) -> CGPoint {
|
|
| 13 |
+ return CGPoint(x: self.x, y: self.y + by) |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ func moveUp(by: CGFloat) -> CGPoint {
|
|
| 17 |
+ return CGPoint(x: self.x, y: self.y - by) |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ func putInBounds (minX: CGFloat, maxX: CGFloat, minY: CGFloat, maxY: CGFloat) -> CGPoint {
|
|
| 21 |
+ let xRange = maxX - minX |
|
| 22 |
+ let yRange = maxY - minY |
|
| 23 |
+ let xValue = self.x - minX |
|
| 24 |
+ let yValue = self.y - minY |
|
| 25 |
+ print ("self: \(self) xRange:\(xRange) yRange:\(yRange) xValue:\(xValue) yValue:\(yValue) ")
|
|
| 26 |
+ return CGPoint(x: xValue / xRange, y: yValue / yRange) |
|
| 27 |
+ } |
|
| 28 |
+} |
|
@@ -0,0 +1,58 @@ |
||
| 1 |
+// |
|
| 2 |
+// Data.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 08/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import Foundation |
|
| 10 |
+ |
|
| 11 |
+extension Data {
|
|
| 12 |
+ |
|
| 13 |
+ var hexEncodedStringValue : String {
|
|
| 14 |
+ get{
|
|
| 15 |
+ return map { String(format: "%02hhx", $0) }.joined()
|
|
| 16 |
+ } |
|
| 17 |
+ } |
|
| 18 |
+//Ceva mostenit |
|
| 19 |
+// init<T>(from value: T) {
|
|
| 20 |
+// var value = value |
|
| 21 |
+// self.init(buffer: UnsafeBufferPointer(start: &value, count: 1)) |
|
| 22 |
+// } |
|
| 23 |
+ |
|
| 24 |
+ func value<T>(from: Int) -> T {
|
|
| 25 |
+ let to = from + MemoryLayout<T>.size |
|
| 26 |
+ //track("size: \(self.count) from:\(from) to:\(to)")
|
|
| 27 |
+ return self.subdata(in: from..<to).withUnsafeBytes { $0.load(as: T.self) }
|
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ func value<T>() -> T {
|
|
| 31 |
+ return self.withUnsafeBytes { $0.load(as: T.self) }
|
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ func subdata(in range: ClosedRange<Index>) -> Data {
|
|
| 35 |
+ return subdata(in: range.lowerBound ..< range.upperBound) |
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ func subdata(from: Int, length: Int) -> Data {
|
|
| 39 |
+ return subdata(in: self.startIndex + from ..< self.startIndex.advanced(by: from + length)) |
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ func subdata(from: Int, to: Int) -> Data {
|
|
| 43 |
+ return subdata(in: self.startIndex + from ... self.startIndex.advanced(by: to)) |
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ |
|
| 47 |
+ var utf8String: String {
|
|
| 48 |
+ return string(as: .utf8) |
|
| 49 |
+ } |
|
| 50 |
+ |
|
| 51 |
+ var asciiString: String {
|
|
| 52 |
+ return string(as: .ascii) |
|
| 53 |
+ } |
|
| 54 |
+ |
|
| 55 |
+ func string(as encoding: String.Encoding) -> String {
|
|
| 56 |
+ return String(data: self, encoding: encoding) ?? "" |
|
| 57 |
+ } |
|
| 58 |
+} |
|
@@ -0,0 +1,21 @@ |
||
| 1 |
+// |
|
| 2 |
+// Date.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 13/04/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import Foundation |
|
| 10 |
+ |
|
| 11 |
+extension Date {
|
|
| 12 |
+ |
|
| 13 |
+ // https://nsdateformatter.com |
|
| 14 |
+ func format( as format: String = "yyyy-MM-dd HH:mm") -> String {
|
|
| 15 |
+ let formatter = DateFormatter() |
|
| 16 |
+ formatter.dateStyle = .short |
|
| 17 |
+ formatter.dateFormat = format |
|
| 18 |
+ return formatter.string(from: self) |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+} |
|
@@ -0,0 +1,19 @@ |
||
| 1 |
+// |
|
| 2 |
+// Decimal.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 09/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import Foundation |
|
| 10 |
+ |
|
| 11 |
+import Foundation |
|
| 12 |
+ |
|
| 13 |
+extension Decimal {
|
|
| 14 |
+ |
|
| 15 |
+ var doubleValue:Double {
|
|
| 16 |
+ return NSDecimalNumber(decimal:self).doubleValue |
|
| 17 |
+ } |
|
| 18 |
+ |
|
| 19 |
+} |
|
@@ -0,0 +1,49 @@ |
||
| 1 |
+// |
|
| 2 |
+// Double.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 08/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import Foundation |
|
| 10 |
+import CoreGraphics |
|
| 11 |
+ |
|
| 12 |
+extension Double {
|
|
| 13 |
+ |
|
| 14 |
+ func format(f: String) -> String {
|
|
| 15 |
+ return String(format: "%\(f)", self) |
|
| 16 |
+ } |
|
| 17 |
+ |
|
| 18 |
+ func format(decimalDigits: Int) -> String {
|
|
| 19 |
+ return String(format: "%.\(decimalDigits)f", self) |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ func format(digits: Int) -> String {
|
|
| 23 |
+ for d in 0...digits {
|
|
| 24 |
+ if self < pow(10, d).doubleValue {
|
|
| 25 |
+ return self.format(f: ".\(digits - d)f") |
|
| 26 |
+ } |
|
| 27 |
+ } |
|
| 28 |
+ return self.format(f: ".0f") |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ var uInt8Value: UInt8 {
|
|
| 32 |
+ return UInt8(self) |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ var intValue: Int {
|
|
| 36 |
+ return Int(self) |
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ var CGFloatValue: CGFloat {
|
|
| 40 |
+ return CGFloat(self) |
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ func format(fractionDigits: Int) ->String {
|
|
| 44 |
+ let nf = NumberFormatter() |
|
| 45 |
+ nf.minimumFractionDigits = fractionDigits |
|
| 46 |
+ nf.maximumFractionDigits = fractionDigits |
|
| 47 |
+ return nf.string(from: NSNumber(value: self))! |
|
| 48 |
+ } |
|
| 49 |
+} |
|
@@ -0,0 +1,19 @@ |
||
| 1 |
+// |
|
| 2 |
+// Float.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 08/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import Foundation |
|
| 10 |
+ |
|
| 11 |
+extension Float {
|
|
| 12 |
+ var uInt8Value: UInt8 {
|
|
| 13 |
+ return UInt8(self) |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ var doubleValue: Double {
|
|
| 17 |
+ return Double(self) |
|
| 18 |
+ } |
|
| 19 |
+} |
|
@@ -0,0 +1,17 @@ |
||
| 1 |
+// |
|
| 2 |
+// Path.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 14/05/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import CoreGraphics |
|
| 10 |
+import SwiftUI |
|
| 11 |
+ |
|
| 12 |
+extension Path {
|
|
| 13 |
+ mutating func addLine(from startPoint: CGPoint, to endPoint: CGPoint) {
|
|
| 14 |
+ self.move(to: startPoint) |
|
| 15 |
+ self.addLine(to: endPoint) |
|
| 16 |
+ } |
|
| 17 |
+} |
|
@@ -0,0 +1,65 @@ |
||
| 1 |
+// |
|
| 2 |
+// File.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 08/05/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+struct RoundedRectangleBackground: ViewModifier {
|
|
| 12 |
+ |
|
| 13 |
+ var cornerRadius: CGFloat |
|
| 14 |
+ var foregroundColor: Color |
|
| 15 |
+ var opacity: Double |
|
| 16 |
+ var blurRadius: CGFloat |
|
| 17 |
+ |
|
| 18 |
+ func body(content: Content) -> some View {
|
|
| 19 |
+ content.background( |
|
| 20 |
+ RoundedRectangle(cornerRadius: cornerRadius) |
|
| 21 |
+ .foregroundColor(foregroundColor) |
|
| 22 |
+ .opacity(opacity) |
|
| 23 |
+ .blur(radius: blurRadius) |
|
| 24 |
+ ) |
|
| 25 |
+ } |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+struct RoundedRectangleBorder: ViewModifier {
|
|
| 29 |
+ |
|
| 30 |
+ var cornerRadius: CGFloat |
|
| 31 |
+ var foregroundColor: Color |
|
| 32 |
+ var lineWidth: CGFloat |
|
| 33 |
+ var blurRadius: CGFloat |
|
| 34 |
+ |
|
| 35 |
+ func body(content: Content) -> some View {
|
|
| 36 |
+ content.background( |
|
| 37 |
+ RoundedRectangle(cornerRadius: cornerRadius) |
|
| 38 |
+ .stroke(lineWidth: lineWidth) |
|
| 39 |
+ .foregroundColor(foregroundColor) |
|
| 40 |
+ .blur(radius: blurRadius) |
|
| 41 |
+ ) |
|
| 42 |
+ } |
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+extension View {
|
|
| 46 |
+ func withRoundedRectangleBackground( cornerRadius: CGFloat, foregroundColor: Color, opacity: Double, blurRadius: CGFloat = 0 ) -> some View {
|
|
| 47 |
+ self.modifier(RoundedRectangleBackground(cornerRadius: cornerRadius, foregroundColor: foregroundColor, opacity: opacity, blurRadius: blurRadius)) |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ func withRoundedRectangleBorder( cornerRadius: CGFloat, foregroundColor: Color, lineWidth: CGFloat, blurRadius: CGFloat = 0 ) -> some View {
|
|
| 51 |
+ self.modifier(RoundedRectangleBorder(cornerRadius: cornerRadius, foregroundColor: foregroundColor, lineWidth: lineWidth, blurRadius: blurRadius )) |
|
| 52 |
+ } |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+// MARK: Local |
|
| 56 |
+extension Button {
|
|
| 57 |
+ func asEnableFeatureButton(state: Bool) -> some View {
|
|
| 58 |
+ self |
|
| 59 |
+ .foregroundColor( state ? .primary : .blue ) |
|
| 60 |
+ .padding(5) |
|
| 61 |
+ .withRoundedRectangleBackground(cornerRadius: 15, foregroundColor: state ? .blue : .clear, opacity: 0.5) |
|
| 62 |
+ .withRoundedRectangleBorder(cornerRadius: 15, foregroundColor: .blue, lineWidth: 1, blurRadius: 0.1) |
|
| 63 |
+ } |
|
| 64 |
+} |
|
| 65 |
+ |
|
@@ -0,0 +1,33 @@ |
||
| 1 |
+// |
|
| 2 |
+// View.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 04/05/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+/* MARK: Iusless... |
|
| 12 |
+enum XNavigationViewStyle {
|
|
| 13 |
+ case auto |
|
| 14 |
+ case doubleColumn |
|
| 15 |
+ case stack |
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+extension View {
|
|
| 19 |
+ func xNavigationViewStyle(_ style: XNavigationViewStyle) -> some View {
|
|
| 20 |
+ switch style {
|
|
| 21 |
+ case .auto: |
|
| 22 |
+ track("auto")
|
|
| 23 |
+ return AnyView(self.navigationViewStyle(DefaultNavigationViewStyle())) |
|
| 24 |
+ case .doubleColumn: |
|
| 25 |
+ track("doubleColumn")
|
|
| 26 |
+ return AnyView(self.navigationViewStyle(DoubleColumnNavigationViewStyle())) |
|
| 27 |
+ case .stack: |
|
| 28 |
+ track("stack")
|
|
| 29 |
+ return AnyView(self.navigationViewStyle(StackNavigationViewStyle())) |
|
| 30 |
+ } |
|
| 31 |
+ } |
|
| 32 |
+} |
|
| 33 |
+*/ |
|
@@ -0,0 +1,83 @@ |
||
| 1 |
+<?xml version="1.0" encoding="UTF-8"?> |
|
| 2 |
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|
| 3 |
+<plist version="1.0"> |
|
| 4 |
+<dict> |
|
| 5 |
+ <key>CFBundleDevelopmentRegion</key> |
|
| 6 |
+ <string>$(DEVELOPMENT_LANGUAGE)</string> |
|
| 7 |
+ <key>CFBundleDisplayName</key> |
|
| 8 |
+ <string></string> |
|
| 9 |
+ <key>CFBundleExecutable</key> |
|
| 10 |
+ <string>$(EXECUTABLE_NAME)</string> |
|
| 11 |
+ <key>CFBundleIdentifier</key> |
|
| 12 |
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> |
|
| 13 |
+ <key>CFBundleInfoDictionaryVersion</key> |
|
| 14 |
+ <string>6.0</string> |
|
| 15 |
+ <key>CFBundleName</key> |
|
| 16 |
+ <string>$(PRODUCT_NAME)</string> |
|
| 17 |
+ <key>CFBundlePackageType</key> |
|
| 18 |
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> |
|
| 19 |
+ <key>CFBundleShortVersionString</key> |
|
| 20 |
+ <string>1.0</string> |
|
| 21 |
+ <key>CFBundleURLTypes</key> |
|
| 22 |
+ <array> |
|
| 23 |
+ <dict> |
|
| 24 |
+ <key>CFBundleTypeRole</key> |
|
| 25 |
+ <string>Editor</string> |
|
| 26 |
+ <key>CFBundleURLName</key> |
|
| 27 |
+ <string>@ro.xdev.USB-Meter</string> |
|
| 28 |
+ <key>CFBundleURLSchemes</key> |
|
| 29 |
+ <array> |
|
| 30 |
+ <string>USB-Meter</string> |
|
| 31 |
+ </array> |
|
| 32 |
+ </dict> |
|
| 33 |
+ </array> |
|
| 34 |
+ <key>CFBundleVersion</key> |
|
| 35 |
+ <string>1</string> |
|
| 36 |
+ <key>LSApplicationCategoryType</key> |
|
| 37 |
+ <string>public.app-category.utilities</string> |
|
| 38 |
+ <key>LSRequiresIPhoneOS</key> |
|
| 39 |
+ <true/> |
|
| 40 |
+ <key>NSBluetoothAlwaysUsageDescription</key> |
|
| 41 |
+ <string>This app needs to use Bluetooth to connect with USB Meter</string> |
|
| 42 |
+ <key>UIApplicationSceneManifest</key> |
|
| 43 |
+ <dict> |
|
| 44 |
+ <key>UIApplicationSupportsMultipleScenes</key> |
|
| 45 |
+ <false/> |
|
| 46 |
+ <key>UISceneConfigurations</key> |
|
| 47 |
+ <dict> |
|
| 48 |
+ <key>UIWindowSceneSessionRoleApplication</key> |
|
| 49 |
+ <array> |
|
| 50 |
+ <dict> |
|
| 51 |
+ <key>UISceneConfigurationName</key> |
|
| 52 |
+ <string>Default Configuration</string> |
|
| 53 |
+ <key>UISceneDelegateClassName</key> |
|
| 54 |
+ <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string> |
|
| 55 |
+ </dict> |
|
| 56 |
+ </array> |
|
| 57 |
+ </dict> |
|
| 58 |
+ </dict> |
|
| 59 |
+ <key>UILaunchStoryboardName</key> |
|
| 60 |
+ <string>LaunchScreen</string> |
|
| 61 |
+ <key>UIRequiredDeviceCapabilities</key> |
|
| 62 |
+ <array> |
|
| 63 |
+ <string>armv7</string> |
|
| 64 |
+ </array> |
|
| 65 |
+ <key>UIRequiresFullScreen</key> |
|
| 66 |
+ <false/> |
|
| 67 |
+ <key>UIStatusBarHidden</key> |
|
| 68 |
+ <false/> |
|
| 69 |
+ <key>UISupportedInterfaceOrientations</key> |
|
| 70 |
+ <array> |
|
| 71 |
+ <string>UIInterfaceOrientationPortrait</string> |
|
| 72 |
+ <string>UIInterfaceOrientationLandscapeLeft</string> |
|
| 73 |
+ <string>UIInterfaceOrientationLandscapeRight</string> |
|
| 74 |
+ </array> |
|
| 75 |
+ <key>UISupportedInterfaceOrientations~ipad</key> |
|
| 76 |
+ <array> |
|
| 77 |
+ <string>UIInterfaceOrientationPortrait</string> |
|
| 78 |
+ <string>UIInterfaceOrientationPortraitUpsideDown</string> |
|
| 79 |
+ <string>UIInterfaceOrientationLandscapeLeft</string> |
|
| 80 |
+ <string>UIInterfaceOrientationLandscapeRight</string> |
|
| 81 |
+ </array> |
|
| 82 |
+</dict> |
|
| 83 |
+</plist> |
|
@@ -0,0 +1,65 @@ |
||
| 1 |
+// |
|
| 2 |
+// DataStore.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 03/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+import Combine |
|
| 11 |
+import CoreBluetooth |
|
| 12 |
+ |
|
| 13 |
+final class AppData : ObservableObject {
|
|
| 14 |
+ |
|
| 15 |
+ |
|
| 16 |
+ let objectWillChange = ObservableObjectPublisher() |
|
| 17 |
+ private var userDefaultsNotification: AnyCancellable? |
|
| 18 |
+ private var icloudGefaultsNotification: AnyCancellable? |
|
| 19 |
+ init() {
|
|
| 20 |
+ userDefaultsNotification = NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification).sink { _ in
|
|
| 21 |
+ self.objectWillChange.send() |
|
| 22 |
+ } |
|
| 23 |
+ icloudGefaultsNotification = NotificationCenter.default.publisher(for: NSUbiquitousKeyValueStore.didChangeExternallyNotification).sink(receiveValue: test) |
|
| 24 |
+ //NotificationCenter.default.publisher(for: NSUbiquitousKeyValueStore.didChangeExternallyNotification).sink(receiveValue: { notification in
|
|
| 25 |
+ |
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ let bluetoothManager = BluetoothManager() |
|
| 29 |
+ |
|
| 30 |
+ @Published var enableRecordFeature: Bool = true |
|
| 31 |
+ |
|
| 32 |
+ @Published var meters: [UUID:Meter] = [UUID:Meter]() {
|
|
| 33 |
+ willSet {
|
|
| 34 |
+ self.objectWillChange.send() |
|
| 35 |
+ } |
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ @ICloudDefault(key: "MeterNames", defaultValue: [:]) var meterNames: [String:String] |
|
| 39 |
+ func test(notification: NotificationCenter.Publisher.Output) -> Void {
|
|
| 40 |
+ if let changedKeys = notification.userInfo?["NSUbiquitousKeyValueStoreChangedKeysKey"] as? [String] {
|
|
| 41 |
+ var somethingChanged = false |
|
| 42 |
+ for changedKey in changedKeys {
|
|
| 43 |
+ switch changedKey {
|
|
| 44 |
+ case "MeterNames": |
|
| 45 |
+ for meter in self.meters.values {
|
|
| 46 |
+ if let newName = self.meterNames[meter.btSerial.macAddress.description] {
|
|
| 47 |
+ if meter.name != newName {
|
|
| 48 |
+ meter.name = newName |
|
| 49 |
+ somethingChanged = true |
|
| 50 |
+ } |
|
| 51 |
+ } |
|
| 52 |
+ } |
|
| 53 |
+ default: |
|
| 54 |
+ track("Unknown key: '\(changedKey)' changed in iCloud)")
|
|
| 55 |
+ } |
|
| 56 |
+ if changedKey == "MeterNames" {
|
|
| 57 |
+ |
|
| 58 |
+ } |
|
| 59 |
+ } |
|
| 60 |
+ if somethingChanged {
|
|
| 61 |
+ self.objectWillChange.send() |
|
| 62 |
+ } |
|
| 63 |
+ } |
|
| 64 |
+ } |
|
| 65 |
+} |
|
@@ -0,0 +1,176 @@ |
||
| 1 |
+// |
|
| 2 |
+// BTManager.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 01/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import CoreBluetooth |
|
| 10 |
+ |
|
| 11 |
+class BluetoothManager : NSObject, ObservableObject {
|
|
| 12 |
+ private var manager: CBCentralManager! |
|
| 13 |
+ // MARK: MacOS split advertisementData generating multiple discoveries with partial data: https://stackoverflow.com/questions/41628114/cbperipheral-advertisementdata-is-different-when-discovering-peripherals-on-osx. |
|
| 14 |
+ #if targetEnvironment(macCatalyst) |
|
| 15 |
+ private var advertisementDataCache = AdvertisementDataCache() |
|
| 16 |
+ #endif |
|
| 17 |
+ @Published var managerState = CBManagerState.unknown |
|
| 18 |
+ |
|
| 19 |
+ override init () {
|
|
| 20 |
+ super.init() |
|
| 21 |
+ manager = CBCentralManager(delegate: self, queue: nil) |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ |
|
| 25 |
+ private func scanForMeters() {
|
|
| 26 |
+ guard manager.state == .poweredOn else {
|
|
| 27 |
+ track( "Scan requested but Bluetooth state is \(manager.state)") |
|
| 28 |
+ return |
|
| 29 |
+ } |
|
| 30 |
+ //manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ]) |
|
| 31 |
+ manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ]) |
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ func discoveredMeter(peripheral: CBPeripheral, advertising advertismentData: [String : Any], rssi RSSI: NSNumber) {
|
|
| 35 |
+ //track("discovered new USB Meter: (\(peripheral), advertsing \(advertismentData)")
|
|
| 36 |
+ if let peripheralName = peripheral.name?.trimmingCharacters(in: .whitespacesAndNewlines) {
|
|
| 37 |
+ if let kCBAdvDataManufacturerData = advertismentData["kCBAdvDataManufacturerData"] as? Data {
|
|
| 38 |
+ // MARK: MAC Address |
|
| 39 |
+ let macAddress = MACAddress(from: kCBAdvDataManufacturerData.suffix(from: 2)) |
|
| 40 |
+ // MARK: Model |
|
| 41 |
+ if let model = ModelByPeriferalName[peripheralName] {
|
|
| 42 |
+ //track("Tetermided model for peripheral name: '\(peripheralName)'")
|
|
| 43 |
+ // MARK: Known Meters Lookup |
|
| 44 |
+ if appData.meters[peripheral.identifier] == nil {
|
|
| 45 |
+ track("adding new USB Meter named '\(peripheralName)' with MAC Address: '\(macAddress)'")
|
|
| 46 |
+ let btSerial = BluetoothSerial(peripheral: peripheral, radio: modelRadios[model] ?? .UNKNOWN, with: macAddress, managedBy: manager, RSSI: RSSI.intValue) |
|
| 47 |
+ var m = appData.meters |
|
| 48 |
+ m[peripheral.identifier] = Meter(model: model, with: btSerial) |
|
| 49 |
+ appData.meters = m |
|
| 50 |
+ } else {
|
|
| 51 |
+// track("Updating USB Meter: \(peripheral.identifier) ")
|
|
| 52 |
+ peripheral.delegate?.peripheral?(peripheral, didReadRSSI: RSSI, error: nil) |
|
| 53 |
+ } |
|
| 54 |
+ } else {
|
|
| 55 |
+ track("Unable to determine model for peripheral name: '\(peripheralName)'")
|
|
| 56 |
+ } |
|
| 57 |
+ } else {
|
|
| 58 |
+ track("Insuficient data to use device!")
|
|
| 59 |
+ } |
|
| 60 |
+ } |
|
| 61 |
+ else{
|
|
| 62 |
+ track("Periferal: \(peripheral.identifier) does not have a name")
|
|
| 63 |
+ } |
|
| 64 |
+ } |
|
| 65 |
+} |
|
| 66 |
+ |
|
| 67 |
+extension BluetoothManager : CBCentralManagerDelegate {
|
|
| 68 |
+ // MARK: CBCentralManager state Changed |
|
| 69 |
+ func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
| 70 |
+ managerState = central.state; |
|
| 71 |
+ track("\(central.state)")
|
|
| 72 |
+ |
|
| 73 |
+ switch central.state {
|
|
| 74 |
+ case .poweredOff: |
|
| 75 |
+ track("Bluetooth is Off. How should I behave?")
|
|
| 76 |
+ case .poweredOn: |
|
| 77 |
+ track("Bluetooth is On... Start scanning...")
|
|
| 78 |
+ // note that "didDisconnectPeripheral" won't be called if BLE is turned off while connected |
|
| 79 |
+ // connectedPeripheral = nil |
|
| 80 |
+ // pendingPeripheral = nil |
|
| 81 |
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
| 82 |
+ self?.scanForMeters() |
|
| 83 |
+ } |
|
| 84 |
+ case .resetting: |
|
| 85 |
+ track("Bluetooth is reseting... . Whatever that means.")
|
|
| 86 |
+ case .unauthorized: |
|
| 87 |
+ track("Bluetooth is not authorized.")
|
|
| 88 |
+ case .unknown: |
|
| 89 |
+ track("Bluetooth is in an unknown state.")
|
|
| 90 |
+ case .unsupported: |
|
| 91 |
+ track("Bluetooth not supported by device")
|
|
| 92 |
+ default: |
|
| 93 |
+ track("Bluetooth is in a state never seen before!")
|
|
| 94 |
+ } |
|
| 95 |
+ } |
|
| 96 |
+ |
|
| 97 |
+ // MARK: MacOS multiple discoveries advertisementData caching |
|
| 98 |
+ #if targetEnvironment(macCatalyst) |
|
| 99 |
+ private class AdvertisementDataCache {
|
|
| 100 |
+ |
|
| 101 |
+ fileprivate var map = [UUID: [String: Any]]() |
|
| 102 |
+ |
|
| 103 |
+ func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
|
|
| 104 |
+ var ad = (map[peripheral.identifier]) ?? [String: Any]() |
|
| 105 |
+ for (key, value) in advertisementData {
|
|
| 106 |
+ ad[key] = value |
|
| 107 |
+ } |
|
| 108 |
+ map[peripheral.identifier] = ad |
|
| 109 |
+ return ad |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ func clear() {
|
|
| 113 |
+ map.removeAll() |
|
| 114 |
+ } |
|
| 115 |
+ } |
|
| 116 |
+ #endif |
|
| 117 |
+ |
|
| 118 |
+ // MARK: CBCentralManager didDiscover peripheral |
|
| 119 |
+ func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
|
| 120 |
+ #if targetEnvironment(macCatalyst) |
|
| 121 |
+// MARK: MacOS probably assumes that if "kCBAdvDataIsConnectable" is not present in parial advertisment data it nust be 0 |
|
| 122 |
+// var ad = advertisementData |
|
| 123 |
+// if ( ad["kCBAdvDataManufacturerData"] == nil ) {
|
|
| 124 |
+// ad.removeValue(forKey: "kCBAdvDataIsConnectable") |
|
| 125 |
+// } |
|
| 126 |
+ let completeAdvertisementData = self.advertisementDataCache.append(peripheral: peripheral, advertisementData: advertisementData) |
|
| 127 |
+ #else |
|
| 128 |
+ let completeAdvertisementData = advertisementData |
|
| 129 |
+ #endif |
|
| 130 |
+ //track("Device discoverded UUID: '\(peripheral.identifier)' named '\(peripheral.name ?? "Unknown")'); RSSI: \(RSSI) dBm; Advertisment data: \(advertisementData)")
|
|
| 131 |
+ discoveredMeter(peripheral: peripheral, advertising: completeAdvertisementData, rssi: RSSI ) |
|
| 132 |
+ } |
|
| 133 |
+ |
|
| 134 |
+ // MARK: CBCentralManager didConnect peripheral |
|
| 135 |
+ internal func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
| 136 |
+ //track("Connected to peripheral: '\(peripheral.identifier)'")
|
|
| 137 |
+ if let usbMeter = appData.meters[peripheral.identifier] {
|
|
| 138 |
+ usbMeter.btSerial.connectionEstablished() |
|
| 139 |
+ } |
|
| 140 |
+ else {
|
|
| 141 |
+ track("Connected to unknown meter with UUID: '\(peripheral.identifier)'")
|
|
| 142 |
+ } |
|
| 143 |
+ } |
|
| 144 |
+ |
|
| 145 |
+ // MARK: CBCentralManager didDisconnectPeripheral peripheral |
|
| 146 |
+ internal func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
| 147 |
+ track("Disconnected from peripheral: '\(peripheral.identifier)' with error: \(error.debugDescription)")
|
|
| 148 |
+ if let usbMeter = appData.meters[peripheral.identifier] {
|
|
| 149 |
+ usbMeter.btSerial.connectionClosed() |
|
| 150 |
+ } |
|
| 151 |
+ else {
|
|
| 152 |
+ track("Disconnected from unknown meter with UUID: '\(peripheral.identifier)'")
|
|
| 153 |
+ } |
|
| 154 |
+ } |
|
| 155 |
+} |
|
| 156 |
+ |
|
| 157 |
+// MARK: MacOS multiple discoveries advertisementData caching |
|
| 158 |
+#if targetEnvironment(macCatalyst) |
|
| 159 |
+private class AdvertisementDataCache {
|
|
| 160 |
+ |
|
| 161 |
+ fileprivate var map = [UUID: [String: Any]]() |
|
| 162 |
+ |
|
| 163 |
+ func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
|
|
| 164 |
+ var ad = (map[peripheral.identifier]) ?? [String: Any]() |
|
| 165 |
+ for (key, value) in advertisementData {
|
|
| 166 |
+ ad[key] = value |
|
| 167 |
+ } |
|
| 168 |
+ map[peripheral.identifier] = ad |
|
| 169 |
+ return ad |
|
| 170 |
+ } |
|
| 171 |
+ |
|
| 172 |
+ func clear() {
|
|
| 173 |
+ map.removeAll() |
|
| 174 |
+ } |
|
| 175 |
+} |
|
| 176 |
+#endif |
|
@@ -0,0 +1,46 @@ |
||
| 1 |
+// |
|
| 2 |
+// BluetoothRadio.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 19/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import CoreBluetooth |
|
| 10 |
+ |
|
| 11 |
+/** |
|
| 12 |
+ Bluetooth Radio Modules |
|
| 13 |
+ # DX-BT18 (HM-10) |
|
| 14 |
+ - Documentation [DX-BT18 User Manual](https:fccid.io/2AKS8DX-BT18/User-Manual/Users-Manual-4216091) |
|
| 15 |
+ - Code [HM10 Bluetooth Serial iOS](https://github.com/hoiberg/HM10-BluetoothSerial-iOS) |
|
| 16 |
+ # PW0316 |
|
| 17 |
+ - Documentation [PW0316 BLE4.0 User Manual](http://www.phangwei.com/o/PW0316_User_Manual_V2.9.pdf) |
|
| 18 |
+ */ |
|
| 19 |
+enum BluetoothRadio : CaseIterable {
|
|
| 20 |
+ case BT18 |
|
| 21 |
+ case PW0316 |
|
| 22 |
+ case UNKNOWN |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 25 |
+/** |
|
| 26 |
+ Dictionary containing services used in our communication for each radio |
|
| 27 |
+ */ |
|
| 28 |
+var BluetoothRadioServicesUUIDS: [BluetoothRadio:[CBUUID]] = [ |
|
| 29 |
+ .BT18 : [CBUUID(string: "FFE0")], |
|
| 30 |
+ .PW0316 : [CBUUID(string: "FFE0"), CBUUID(string: "FFE5")] |
|
| 31 |
+] |
|
| 32 |
+ |
|
| 33 |
+/** |
|
| 34 |
+ Returns an array containing all service UUIDs used by radios |
|
| 35 |
+ */ |
|
| 36 |
+func allBluetoothRadioServices () -> [CBUUID] {
|
|
| 37 |
+ var retval: [CBUUID] = [] |
|
| 38 |
+ for radio in BluetoothRadio.allCases {
|
|
| 39 |
+ for serviceUUID in BluetoothRadioServicesUUIDS[radio] ?? [] {
|
|
| 40 |
+ if !retval.contains(serviceUUID) {
|
|
| 41 |
+ retval.append(serviceUUID) |
|
| 42 |
+ } |
|
| 43 |
+ } |
|
| 44 |
+ } |
|
| 45 |
+ return retval |
|
| 46 |
+} |
|
@@ -0,0 +1,293 @@ |
||
| 1 |
+// |
|
| 2 |
+// bluetoothSerial.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 17/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+// https://github.com/hoiberg/HM10-BluetoothSerial-iOS |
|
| 9 |
+import CoreBluetooth |
|
| 10 |
+ |
|
| 11 |
+final class BluetoothSerial : NSObject, ObservableObject {
|
|
| 12 |
+ |
|
| 13 |
+ enum AdministrativeState {
|
|
| 14 |
+ case down |
|
| 15 |
+ case up |
|
| 16 |
+ } |
|
| 17 |
+ enum OperationalState: Int, Comparable {
|
|
| 18 |
+ case peripheralNotConnected |
|
| 19 |
+ case peripheralConnectionPending |
|
| 20 |
+ case peripheralConnected |
|
| 21 |
+ case peripheralReady |
|
| 22 |
+ |
|
| 23 |
+ static func < (lhs: OperationalState, rhs: OperationalState) -> Bool {
|
|
| 24 |
+ return lhs.rawValue < rhs.rawValue |
|
| 25 |
+ } |
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ private var administrativeState = AdministrativeState.down |
|
| 29 |
+ private var operationalState = OperationalState.peripheralNotConnected {
|
|
| 30 |
+ didSet {
|
|
| 31 |
+ delegate?.opertionalStateChanged(to: operationalState) |
|
| 32 |
+ } |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ var macAddress: MACAddress |
|
| 36 |
+ private var manager: CBCentralManager |
|
| 37 |
+ private var radio: BluetoothRadio |
|
| 38 |
+ @Published var RSSI: Int |
|
| 39 |
+ |
|
| 40 |
+ private var expectedResponseLength = 0 |
|
| 41 |
+ private var wdTimer: Timer? |
|
| 42 |
+ |
|
| 43 |
+ var peripheral: CBPeripheral |
|
| 44 |
+ |
|
| 45 |
+ /// The characteristic 0xFFE1 we need to write to, of the connectedPeripheral |
|
| 46 |
+ private var writeCharacteristic: CBCharacteristic? |
|
| 47 |
+ |
|
| 48 |
+ private var buffer = Data() |
|
| 49 |
+ |
|
| 50 |
+ weak var delegate: SerialPortDelegate? |
|
| 51 |
+ |
|
| 52 |
+ init( peripheral: CBPeripheral, radio: BluetoothRadio, with macAddress: MACAddress, managedBy manager: CBCentralManager, RSSI: Int ) {
|
|
| 53 |
+ |
|
| 54 |
+ self.peripheral = peripheral |
|
| 55 |
+ self.macAddress = macAddress |
|
| 56 |
+ self.radio = radio |
|
| 57 |
+ self.manager = manager |
|
| 58 |
+ self.RSSI = RSSI |
|
| 59 |
+ Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: {_ in
|
|
| 60 |
+ if peripheral.state == .connected {
|
|
| 61 |
+ peripheral.readRSSI() |
|
| 62 |
+ } |
|
| 63 |
+ }) |
|
| 64 |
+ super.init() |
|
| 65 |
+ peripheral.delegate = self |
|
| 66 |
+ } |
|
| 67 |
+ |
|
| 68 |
+ func connect() {
|
|
| 69 |
+ administrativeState = .up |
|
| 70 |
+ if operationalState < .peripheralConnected {
|
|
| 71 |
+ operationalState = .peripheralConnectionPending |
|
| 72 |
+ track("Connect caled")
|
|
| 73 |
+ manager.connect(peripheral, options: nil) |
|
| 74 |
+ } else {
|
|
| 75 |
+ track("Peripheral allready connected: \(operationalState)")
|
|
| 76 |
+ } |
|
| 77 |
+ } |
|
| 78 |
+ |
|
| 79 |
+ func disconnect() {
|
|
| 80 |
+ if operationalState >= .peripheralConnected {
|
|
| 81 |
+ manager.cancelPeripheralConnection(peripheral) |
|
| 82 |
+ buffer.removeAll() |
|
| 83 |
+ } |
|
| 84 |
+ } |
|
| 85 |
+ |
|
| 86 |
+ /** |
|
| 87 |
+ Send data |
|
| 88 |
+ |
|
| 89 |
+ - parameter data: Data to be sent. |
|
| 90 |
+ - parameter expectedResponseLength: Optional If message sent require a respnse the length for that response must be provideed. Incomming data will be buffered before calling delegate.didReceiveData |
|
| 91 |
+ */ |
|
| 92 |
+ func write(_ data: Data, expectedResponseLength: Int = 0) {
|
|
| 93 |
+ //track("\(self.expectedResponseLength)")
|
|
| 94 |
+ //track(data.hexEncodedStringValue) |
|
| 95 |
+ guard operationalState == .peripheralReady else {
|
|
| 96 |
+ track("Guard: \(operationalState)")
|
|
| 97 |
+ return |
|
| 98 |
+ } |
|
| 99 |
+ guard self.expectedResponseLength == 0 else {
|
|
| 100 |
+ track("Guard: \(self.expectedResponseLength)")
|
|
| 101 |
+ return |
|
| 102 |
+ } |
|
| 103 |
+ |
|
| 104 |
+ self.expectedResponseLength = expectedResponseLength |
|
| 105 |
+ |
|
| 106 |
+// track("Sending...")
|
|
| 107 |
+ switch radio {
|
|
| 108 |
+ case .BT18 : |
|
| 109 |
+ peripheral.writeValue(data, for: writeCharacteristic!, type: .withoutResponse) |
|
| 110 |
+ case .PW0316 : |
|
| 111 |
+ peripheral.writeValue(data, for: writeCharacteristic!, type: .withResponse) |
|
| 112 |
+ default: |
|
| 113 |
+ track("Radio \(radio) Not Implemented!")
|
|
| 114 |
+ } |
|
| 115 |
+// track("Sent!")
|
|
| 116 |
+ if self.expectedResponseLength != 0 {
|
|
| 117 |
+ setWDT() |
|
| 118 |
+ } |
|
| 119 |
+ } |
|
| 120 |
+ |
|
| 121 |
+ func connectionEstablished () {
|
|
| 122 |
+ track("")
|
|
| 123 |
+ operationalState = .peripheralConnected |
|
| 124 |
+ peripheral.discoverServices(BluetoothRadioServicesUUIDS[radio]) |
|
| 125 |
+ } |
|
| 126 |
+ |
|
| 127 |
+ func connectionClosed () {
|
|
| 128 |
+ track("")
|
|
| 129 |
+ operationalState = .peripheralNotConnected |
|
| 130 |
+ expectedResponseLength = 0 |
|
| 131 |
+ writeCharacteristic = nil |
|
| 132 |
+ } |
|
| 133 |
+ |
|
| 134 |
+ func setWDT() {
|
|
| 135 |
+ wdTimer?.invalidate() |
|
| 136 |
+ wdTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false, block: {_ in
|
|
| 137 |
+ track("Response timeout. Expected: \(self.expectedResponseLength) - buffer: \(self.buffer.count)")
|
|
| 138 |
+ self.expectedResponseLength = 0 |
|
| 139 |
+ self.disconnect() |
|
| 140 |
+ }) |
|
| 141 |
+ } |
|
| 142 |
+ |
|
| 143 |
+} |
|
| 144 |
+ |
|
| 145 |
+// MARK: CBPeripheralDelegate |
|
| 146 |
+extension BluetoothSerial : CBPeripheralDelegate {
|
|
| 147 |
+ |
|
| 148 |
+ // MARK: didReadRSSI |
|
| 149 |
+ func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
|
| 150 |
+ if error != nil {
|
|
| 151 |
+ track( "Error: \(error!)" ) |
|
| 152 |
+ } |
|
| 153 |
+ self.RSSI = RSSI.intValue |
|
| 154 |
+ } |
|
| 155 |
+ |
|
| 156 |
+ // MARK: didDiscoverServices |
|
| 157 |
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
| 158 |
+ track("\(String(describing: peripheral.services))")
|
|
| 159 |
+ if error != nil {
|
|
| 160 |
+ track( "Error: \(error!)" ) |
|
| 161 |
+ } |
|
| 162 |
+ switch radio {
|
|
| 163 |
+ case .BT18: |
|
| 164 |
+ // discover the 0xFFE1 characteristic for all services (though there should only be one(service)) |
|
| 165 |
+ for service in peripheral.services! {
|
|
| 166 |
+ switch service.uuid {
|
|
| 167 |
+ case CBUUID(string: "FFE0"): |
|
| 168 |
+ // check whether the characteristic we're looking for (0xFFE1) is present - just to be sure |
|
| 169 |
+ peripheral.discoverCharacteristics([CBUUID(string: "FFE1")], for: service) |
|
| 170 |
+ default: |
|
| 171 |
+ track ("Unexpected service discovered: '\(service)'")
|
|
| 172 |
+ } |
|
| 173 |
+ } |
|
| 174 |
+ case .PW0316: |
|
| 175 |
+ for service in peripheral.services! {
|
|
| 176 |
+ //track("\(service.uuid)")
|
|
| 177 |
+ switch service.uuid {
|
|
| 178 |
+ case CBUUID(string: "FFE0"): |
|
| 179 |
+ //track("\(service.uuid)")
|
|
| 180 |
+ // check whether the characteristic we're looking for (0xFFE4) is present - just to be sure |
|
| 181 |
+ peripheral.discoverCharacteristics([CBUUID(string: "FFE4")], for: service) |
|
| 182 |
+ break |
|
| 183 |
+ case CBUUID(string: "FFE5"): |
|
| 184 |
+ //track("\(service.uuid)")
|
|
| 185 |
+ // check whether the characteristic we're looking for (0xFFE9) is present - just to be sure |
|
| 186 |
+ peripheral.discoverCharacteristics([CBUUID(string: "FFE9")], for: service) |
|
| 187 |
+ default: |
|
| 188 |
+ track ("Unexpected service discovered: '\(service)'")
|
|
| 189 |
+ } |
|
| 190 |
+ } |
|
| 191 |
+ break; |
|
| 192 |
+ default: |
|
| 193 |
+ track("Radio \(radio) Not Implemented!")
|
|
| 194 |
+ } |
|
| 195 |
+ } |
|
| 196 |
+ |
|
| 197 |
+ // MARK: didDiscoverCharacteristicsFor |
|
| 198 |
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
|
| 199 |
+ if error != nil {
|
|
| 200 |
+ track( "Error: \(error!)" ) |
|
| 201 |
+ } |
|
| 202 |
+ track("\(String(describing: service.characteristics))")
|
|
| 203 |
+ switch radio {
|
|
| 204 |
+ case .BT18: |
|
| 205 |
+ // check whether the characteristic we're looking for (0xFFE1) is present - just to be sure |
|
| 206 |
+ for characteristic in service.characteristics! {
|
|
| 207 |
+ //track(characteristic.debugDescription) |
|
| 208 |
+ switch characteristic.uuid {
|
|
| 209 |
+ case CBUUID(string: "FFE1"): |
|
| 210 |
+ // subscribe to this value (so we'll get notified when there is serial data for us..) |
|
| 211 |
+ peripheral.setNotifyValue(true, for: characteristic) |
|
| 212 |
+ // keep a reference to this characteristic so we can write to it |
|
| 213 |
+ writeCharacteristic = characteristic |
|
| 214 |
+ // Change State |
|
| 215 |
+ operationalState = .peripheralReady |
|
| 216 |
+ default: |
|
| 217 |
+ track ("Unexpected characteristic discovered: '\(characteristic)'")
|
|
| 218 |
+ } |
|
| 219 |
+ } |
|
| 220 |
+ case .PW0316: |
|
| 221 |
+ for characteristic in service.characteristics! {
|
|
| 222 |
+ switch characteristic.uuid {
|
|
| 223 |
+ case CBUUID(string: "FFE9"): //TX |
|
| 224 |
+ //track("characteristic FFE9: \(characteristic.properties & CBCharacteristicProperties.write)")
|
|
| 225 |
+ writeCharacteristic = characteristic |
|
| 226 |
+ operationalState = .peripheralReady |
|
| 227 |
+ case CBUUID(string: "FFE4"): //RX |
|
| 228 |
+ peripheral.setNotifyValue(true, for: characteristic) |
|
| 229 |
+ //track("characteristic FFE4: \(characteristic.properties)")
|
|
| 230 |
+ default: |
|
| 231 |
+ track ("Unexpected characteristic discovered: '\(characteristic)'")
|
|
| 232 |
+ } |
|
| 233 |
+ |
|
| 234 |
+ } |
|
| 235 |
+ break; |
|
| 236 |
+ default: |
|
| 237 |
+ track("Radio \(radio) Not Implemented!")
|
|
| 238 |
+ } |
|
| 239 |
+ } |
|
| 240 |
+ |
|
| 241 |
+ // MARK: didUpdateValueFor |
|
| 242 |
+ func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
| 243 |
+// track("")
|
|
| 244 |
+ if error != nil {
|
|
| 245 |
+ track( "Error: \(error!)" ) |
|
| 246 |
+ } |
|
| 247 |
+ buffer.append( characteristic.value ?? Data() ) |
|
| 248 |
+// track("\n\(buffer.hexEncodedStringValue)")
|
|
| 249 |
+ switch buffer.count {
|
|
| 250 |
+ case let x where x < expectedResponseLength: |
|
| 251 |
+ setWDT() |
|
| 252 |
+ //track("buffering")
|
|
| 253 |
+ break; |
|
| 254 |
+ case let x where x == expectedResponseLength: |
|
| 255 |
+ //track("buffer ready")
|
|
| 256 |
+ wdTimer?.invalidate() |
|
| 257 |
+ expectedResponseLength = 0 |
|
| 258 |
+ delegate?.didReceiveData(buffer) |
|
| 259 |
+ buffer.removeAll() |
|
| 260 |
+ case let x where x > expectedResponseLength: |
|
| 261 |
+ // MARK: De unde stim că asta a fost tot? Probabil o deconectare ar rezolva problema |
|
| 262 |
+ wdTimer?.invalidate() |
|
| 263 |
+ expectedResponseLength = 0 |
|
| 264 |
+ buffer.removeAll() |
|
| 265 |
+ track("Buffer Overflow")
|
|
| 266 |
+ default: |
|
| 267 |
+ track("This is not possible!")
|
|
| 268 |
+ } |
|
| 269 |
+ } |
|
| 270 |
+ |
|
| 271 |
+ func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
| 272 |
+ if error != nil { track( "Error: \(error!)" ) }
|
|
| 273 |
+ } |
|
| 274 |
+ |
|
| 275 |
+ func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
|
|
| 276 |
+ //track("")
|
|
| 277 |
+ } |
|
| 278 |
+ |
|
| 279 |
+} |
|
| 280 |
+ |
|
| 281 |
+// MARK: SerialPortDelegate |
|
| 282 |
+protocol SerialPortDelegate: AnyObject {
|
|
| 283 |
+ // MARK: State Changed |
|
| 284 |
+ func opertionalStateChanged( to newOperationalState: BluetoothSerial.OperationalState ) |
|
| 285 |
+ // MARK: Data was received |
|
| 286 |
+ func didReceiveData(_ data: Data) |
|
| 287 |
+} |
|
| 288 |
+ |
|
| 289 |
+// MARK: SerialPortDelegate Optionals |
|
| 290 |
+extension SerialPortDelegate {
|
|
| 291 |
+} |
|
| 292 |
+ |
|
| 293 |
+ |
|
@@ -0,0 +1,8 @@ |
||
| 1 |
+<?xml version="1.0" encoding="UTF-8"?> |
|
| 2 |
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|
| 3 |
+<plist version="1.0"> |
|
| 4 |
+<dict> |
|
| 5 |
+ <key>_XCCurrentVersionName</key> |
|
| 6 |
+ <string>USB_Meter.xcdatamodel</string> |
|
| 7 |
+</dict> |
|
| 8 |
+</plist> |
|
@@ -0,0 +1,7 @@ |
||
| 1 |
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
|
| 2 |
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19E287" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier=""> |
|
| 3 |
+ <entity name="Entity" representedClassName="Entity" syncable="YES" codeGenerationType="class"/> |
|
| 4 |
+ <elements> |
|
| 5 |
+ <element name="Entity" positionX="-63" positionY="-18" width="128" height="43"/> |
|
| 6 |
+ </elements> |
|
| 7 |
+</model> |
|
@@ -0,0 +1,104 @@ |
||
| 1 |
+// |
|
| 2 |
+// ChartContext.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 14/04/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import CoreGraphics |
|
| 10 |
+import SwiftUI |
|
| 11 |
+ |
|
| 12 |
+class ChartContext {
|
|
| 13 |
+ private var rect : CGRect? |
|
| 14 |
+ private var pad: CGFloat = 0 |
|
| 15 |
+ |
|
| 16 |
+ var isValid: Bool {
|
|
| 17 |
+ get {
|
|
| 18 |
+ return rect != nil && rect!.width > 0 && rect!.height > 0 |
|
| 19 |
+ } |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ var size: CGSize {
|
|
| 23 |
+ get {
|
|
| 24 |
+ guard rect != nil else {
|
|
| 25 |
+ track("Invalid Context")
|
|
| 26 |
+ fatalError() |
|
| 27 |
+ } |
|
| 28 |
+ return rect!.size |
|
| 29 |
+ } |
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ var origin: CGPoint {
|
|
| 33 |
+ get {
|
|
| 34 |
+ guard rect != nil else {
|
|
| 35 |
+ track("Invalid Context")
|
|
| 36 |
+ fatalError() |
|
| 37 |
+ } |
|
| 38 |
+ return rect!.origin |
|
| 39 |
+ } |
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ var minValue: Double {
|
|
| 43 |
+ return rect == nil ? .nan : Double(rect!.minY) |
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ var maxValue: Double {
|
|
| 47 |
+ get {
|
|
| 48 |
+ return rect == nil ? .nan : Double(rect!.maxY) |
|
| 49 |
+ } |
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ func reset() {
|
|
| 53 |
+ rect = nil |
|
| 54 |
+ padding() |
|
| 55 |
+ } |
|
| 56 |
+ func include( point: CGPoint ) {
|
|
| 57 |
+ if rect == nil {
|
|
| 58 |
+ rect = CGRect(origin: point, size: .zero) |
|
| 59 |
+ padding() |
|
| 60 |
+ } else {
|
|
| 61 |
+ rect = rect!.union(CGRect(origin: point, size: .zero)) |
|
| 62 |
+ padding() |
|
| 63 |
+ } |
|
| 64 |
+ } |
|
| 65 |
+ |
|
| 66 |
+ func padding() {
|
|
| 67 |
+ guard rect != nil else {
|
|
| 68 |
+ track("Invalid Context")
|
|
| 69 |
+ pad = 0 |
|
| 70 |
+ fatalError() |
|
| 71 |
+ } |
|
| 72 |
+ pad = rect!.size.height * Constants.chartUnderscan |
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ func yAxisLabel( for itemNo: Int, of items: Int ) -> Double {
|
|
| 76 |
+ let labelSpace = Double(rect!.height) / Double(items - 1) |
|
| 77 |
+ let labelRelativeValue = labelSpace * Double(itemNo - 1) |
|
| 78 |
+ return minValue + labelRelativeValue |
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ // MARK: Conversii dubioase |
|
| 82 |
+ func xAxisLabel( for itemNo: Int, of items: Int ) -> Double {
|
|
| 83 |
+ let labelSpace = Double(rect!.width) / Double(items - 1) |
|
| 84 |
+ let labelRelativeValue = labelSpace * Double(itemNo - 1) |
|
| 85 |
+ return Double(rect!.origin.x) + labelRelativeValue |
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ func placeInRect (point: CGPoint) -> CGPoint {
|
|
| 89 |
+ guard rect != nil else {
|
|
| 90 |
+ track("Invalid Context")
|
|
| 91 |
+ fatalError() |
|
| 92 |
+ } |
|
| 93 |
+ guard rect!.width != 0 else {
|
|
| 94 |
+ fatalError() |
|
| 95 |
+ } |
|
| 96 |
+ guard rect!.height != 0 else {
|
|
| 97 |
+ fatalError() |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ let x = (point.x - rect!.origin.x)/rect!.width |
|
| 101 |
+ let y = (pad + point.y - rect!.origin.y)/rect!.height |
|
| 102 |
+ return CGPoint(x: x, y: 1 - y * Constants.chartOverscan) |
|
| 103 |
+ } |
|
| 104 |
+} |
|
@@ -0,0 +1,93 @@ |
||
| 1 |
+// |
|
| 2 |
+// Measurements.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 07/05/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import Foundation |
|
| 10 |
+import CoreGraphics |
|
| 11 |
+ |
|
| 12 |
+class Measurements : ObservableObject {
|
|
| 13 |
+ |
|
| 14 |
+ class Measurement : ObservableObject {
|
|
| 15 |
+ struct Point : Identifiable , Hashable {
|
|
| 16 |
+ var id : Int |
|
| 17 |
+ var timestamp: Date |
|
| 18 |
+ var value: Double |
|
| 19 |
+ func point() -> CGPoint {
|
|
| 20 |
+ return CGPoint(x: timestamp.timeIntervalSince1970, y: value) |
|
| 21 |
+ } |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ var points: [Point] = [] |
|
| 25 |
+ var context = ChartContext() |
|
| 26 |
+ |
|
| 27 |
+ func removeValue(index: Int) {
|
|
| 28 |
+ points.remove(at: index) |
|
| 29 |
+ context.reset() |
|
| 30 |
+ for point in points {
|
|
| 31 |
+ context.include( point: point.point() ) |
|
| 32 |
+ } |
|
| 33 |
+ self.objectWillChange.send() |
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ func addPoint(timestamp: Date, value: Double) {
|
|
| 37 |
+ let newPoint = Measurement.Point(id: points.count, timestamp: timestamp, value: value) |
|
| 38 |
+ points.append(newPoint) |
|
| 39 |
+ context.include( point: newPoint.point() ) |
|
| 40 |
+ self.objectWillChange.send() |
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ func reset() {
|
|
| 44 |
+ points.removeAll() |
|
| 45 |
+ context.reset() |
|
| 46 |
+ self.objectWillChange.send() |
|
| 47 |
+ } |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ @Published var power = Measurement() |
|
| 51 |
+ @Published var voltage = Measurement() |
|
| 52 |
+ @Published var current = Measurement() |
|
| 53 |
+ |
|
| 54 |
+ private var lastPointTimestamp = 0 |
|
| 55 |
+ |
|
| 56 |
+ private var itemsInSum: Double = 0 |
|
| 57 |
+ private var powerSum: Double = 0 |
|
| 58 |
+ private var voltageSum: Double = 0 |
|
| 59 |
+ private var currentSum: Double = 0 |
|
| 60 |
+ |
|
| 61 |
+ func remove(at idx: Int) {
|
|
| 62 |
+ power.removeValue(index: idx) |
|
| 63 |
+ voltage.removeValue(index: idx) |
|
| 64 |
+ current.removeValue(index: idx) |
|
| 65 |
+ self.objectWillChange.send() |
|
| 66 |
+ } |
|
| 67 |
+ |
|
| 68 |
+ |
|
| 69 |
+ |
|
| 70 |
+ func addValues(timestamp: Date, power: Double, voltage: Double, current: Double) {
|
|
| 71 |
+ let valuesTimestamp = timestamp.timeIntervalSinceReferenceDate.intValue |
|
| 72 |
+ if lastPointTimestamp == 0 {
|
|
| 73 |
+ lastPointTimestamp = valuesTimestamp |
|
| 74 |
+ } |
|
| 75 |
+ if lastPointTimestamp == valuesTimestamp {
|
|
| 76 |
+ itemsInSum += 1 |
|
| 77 |
+ powerSum += voltage |
|
| 78 |
+ voltageSum += voltage |
|
| 79 |
+ currentSum += current |
|
| 80 |
+ } |
|
| 81 |
+ else {
|
|
| 82 |
+ self.power.addPoint( timestamp: timestamp, value: powerSum / itemsInSum ) |
|
| 83 |
+ self.voltage.addPoint( timestamp: timestamp, value: voltageSum / itemsInSum ) |
|
| 84 |
+ self.current.addPoint( timestamp: timestamp, value: currentSum / itemsInSum ) |
|
| 85 |
+ lastPointTimestamp = valuesTimestamp |
|
| 86 |
+ itemsInSum = 1 |
|
| 87 |
+ powerSum = power |
|
| 88 |
+ voltageSum = voltage |
|
| 89 |
+ currentSum = current |
|
| 90 |
+ } |
|
| 91 |
+ self.objectWillChange.send() |
|
| 92 |
+ } |
|
| 93 |
+} |
|
@@ -0,0 +1,486 @@ |
||
| 1 |
+// |
|
| 2 |
+// File.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 03/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+//MARK: Store and documentation: https://www.aliexpress.com/item/32968303350.html |
|
| 9 |
+//MARK: Protocol: https://sigrok.org/wiki/RDTech_UM_series |
|
| 10 |
+//MARK: Pithon Code: https://github.com/rfinnie/rdserialtool |
|
| 11 |
+//MARK: HM-10 Code: https://github.com/hoiberg/HM10-BluetoothSerial-iOS |
|
| 12 |
+//MARK: Package dependency https://github.com/krzyzanowskim/CryptoSwift |
|
| 13 |
+ |
|
| 14 |
+import CoreBluetooth |
|
| 15 |
+import CryptoSwift |
|
| 16 |
+import SwiftUI |
|
| 17 |
+ |
|
| 18 |
+/** |
|
| 19 |
+ Supprted USB Meters |
|
| 20 |
+ # UM25C |
|
| 21 |
+ # TC66 |
|
| 22 |
+ * Reverse Engineering |
|
| 23 |
+ [UM Series](https://sigrok.org/wiki/RDTech_UM_series) |
|
| 24 |
+ [TC66C](https://sigrok.org/wiki/RDTech_TC66C) |
|
| 25 |
+ */ |
|
| 26 |
+enum Model {
|
|
| 27 |
+ case UM25C |
|
| 28 |
+ case UM34C |
|
| 29 |
+ case TC66C |
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+var modelRadios: [Model : BluetoothRadio] = [ |
|
| 33 |
+ .UM25C : .BT18, |
|
| 34 |
+ .UM34C : .BT18, |
|
| 35 |
+ .TC66C : .PW0316 |
|
| 36 |
+] |
|
| 37 |
+ |
|
| 38 |
+var ModelByPeriferalName: [String : Model] = [ |
|
| 39 |
+ "UM25C" : .UM25C, |
|
| 40 |
+ "UM34C" : .UM34C, |
|
| 41 |
+ "TC66C" : .TC66C, |
|
| 42 |
+ "PW0316" : .TC66C |
|
| 43 |
+] |
|
| 44 |
+ |
|
| 45 |
+var colorForModel: [Model : Color] = [ |
|
| 46 |
+ .UM25C : .blue, |
|
| 47 |
+ .UM34C : .yellow, |
|
| 48 |
+ .TC66C : .black |
|
| 49 |
+] |
|
| 50 |
+ |
|
| 51 |
+ |
|
| 52 |
+class Meter : NSObject, ObservableObject, Identifiable {
|
|
| 53 |
+ |
|
| 54 |
+ enum OperationalState: Int, Comparable {
|
|
| 55 |
+ case notPresent |
|
| 56 |
+ case peripheralNotConnected |
|
| 57 |
+ case peripheralConnectionPending |
|
| 58 |
+ case peripheralConnected |
|
| 59 |
+ case peripheralReady |
|
| 60 |
+ case comunicating |
|
| 61 |
+ case dataIsAvailable |
|
| 62 |
+ |
|
| 63 |
+ static func < (lhs: OperationalState, rhs: OperationalState) -> Bool {
|
|
| 64 |
+ return lhs.rawValue < rhs.rawValue |
|
| 65 |
+ } |
|
| 66 |
+ } |
|
| 67 |
+ |
|
| 68 |
+ @Published var operationalState = OperationalState.peripheralNotConnected {
|
|
| 69 |
+ didSet {
|
|
| 70 |
+ switch operationalState {
|
|
| 71 |
+ case .notPresent: |
|
| 72 |
+ break |
|
| 73 |
+ case .peripheralNotConnected: |
|
| 74 |
+ if enableAutoConnect {
|
|
| 75 |
+ track("\(name) - Reconnecting...")
|
|
| 76 |
+ btSerial.connect() |
|
| 77 |
+ } |
|
| 78 |
+ case .peripheralConnectionPending: |
|
| 79 |
+ break |
|
| 80 |
+ case .peripheralConnected: |
|
| 81 |
+ break |
|
| 82 |
+ case .peripheralReady: |
|
| 83 |
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Change `2.0` to the desired number of seconds.
|
|
| 84 |
+ self.dataDumpRequest() |
|
| 85 |
+ } |
|
| 86 |
+ case .comunicating: |
|
| 87 |
+ break |
|
| 88 |
+ case .dataIsAvailable: |
|
| 89 |
+ break |
|
| 90 |
+ } |
|
| 91 |
+ } |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ static func operationalColor(for state: OperationalState) -> Color {
|
|
| 95 |
+ switch state {
|
|
| 96 |
+ case .notPresent: |
|
| 97 |
+ return .red |
|
| 98 |
+ case .peripheralNotConnected: |
|
| 99 |
+ return .blue |
|
| 100 |
+ case .peripheralConnectionPending: |
|
| 101 |
+ return .yellow |
|
| 102 |
+ case .peripheralConnected: |
|
| 103 |
+ return .yellow |
|
| 104 |
+ case .peripheralReady: |
|
| 105 |
+ return .orange |
|
| 106 |
+ case .comunicating: |
|
| 107 |
+ return .orange |
|
| 108 |
+ case .dataIsAvailable: |
|
| 109 |
+ return .green |
|
| 110 |
+ } |
|
| 111 |
+ } |
|
| 112 |
+ |
|
| 113 |
+ private var wdTimer: Timer? |
|
| 114 |
+ |
|
| 115 |
+ @Published var lastSeen = Date() {
|
|
| 116 |
+ didSet {
|
|
| 117 |
+ wdTimer?.invalidate() |
|
| 118 |
+ if operationalState == .peripheralNotConnected {
|
|
| 119 |
+ wdTimer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false, block: {_ in
|
|
| 120 |
+ track("\(self.name) - Lost advertisments...")
|
|
| 121 |
+ self.operationalState = .notPresent |
|
| 122 |
+ }) |
|
| 123 |
+ } else if operationalState == .notPresent {
|
|
| 124 |
+ operationalState = .peripheralNotConnected |
|
| 125 |
+ } |
|
| 126 |
+ } |
|
| 127 |
+ } |
|
| 128 |
+ |
|
| 129 |
+ var uuid: UUID |
|
| 130 |
+ var model: Model |
|
| 131 |
+ var modelString: String |
|
| 132 |
+ |
|
| 133 |
+ var name: String {
|
|
| 134 |
+ didSet {
|
|
| 135 |
+ appData.meterNames[btSerial.macAddress.description] = name |
|
| 136 |
+ } |
|
| 137 |
+ } |
|
| 138 |
+ |
|
| 139 |
+ var color : Color {
|
|
| 140 |
+ get {
|
|
| 141 |
+ return colorForModel[model]! |
|
| 142 |
+ } |
|
| 143 |
+ } |
|
| 144 |
+ |
|
| 145 |
+ @Published var btSerial: BluetoothSerial |
|
| 146 |
+ |
|
| 147 |
+ @Published var measurements = Measurements() |
|
| 148 |
+ |
|
| 149 |
+ private var commandQueue: [Data] = [] |
|
| 150 |
+ private var dataDumpRequestTimestamp = Date() |
|
| 151 |
+ |
|
| 152 |
+ class DataGroupRecord {
|
|
| 153 |
+ @Published var ah: Double |
|
| 154 |
+ @Published var wh: Double |
|
| 155 |
+ init(ah: Double, wh: Double) {
|
|
| 156 |
+ self.ah = ah |
|
| 157 |
+ self.wh = wh |
|
| 158 |
+ } |
|
| 159 |
+ } |
|
| 160 |
+ @Published var selectedDataGroup: UInt8 = 0 |
|
| 161 |
+ @Published var dataGroupRecords: [Int : DataGroupRecord] = [:] |
|
| 162 |
+ |
|
| 163 |
+ @Published var screenBrightness: Int = -1 {
|
|
| 164 |
+ didSet {
|
|
| 165 |
+ if oldValue != screenBrightness {
|
|
| 166 |
+ screenBrightnessTimestamp = Date() |
|
| 167 |
+ if oldValue != -1 {
|
|
| 168 |
+ setSceeenBrightness(to: UInt8(screenBrightness)) |
|
| 169 |
+ } |
|
| 170 |
+ } |
|
| 171 |
+ } |
|
| 172 |
+ } |
|
| 173 |
+ private var screenBrightnessTimestamp = Date() |
|
| 174 |
+ |
|
| 175 |
+ @Published var screenTimeout: Int = -1 {
|
|
| 176 |
+ didSet {
|
|
| 177 |
+ if oldValue != screenTimeout {
|
|
| 178 |
+ screenTimeoutTimestamp = Date() |
|
| 179 |
+ if oldValue != -1 {
|
|
| 180 |
+ setScreenSaverTimeout(to: UInt8(screenTimeout)) |
|
| 181 |
+ } |
|
| 182 |
+ } |
|
| 183 |
+ } |
|
| 184 |
+ } |
|
| 185 |
+ private var screenTimeoutTimestamp = Date() |
|
| 186 |
+ |
|
| 187 |
+ @Published var voltage: Double = 0 |
|
| 188 |
+ @Published var current: Double = 0 |
|
| 189 |
+ @Published var power: Double = 0 |
|
| 190 |
+ @Published var temperatureCelsius: Double = 0 |
|
| 191 |
+ @Published var temperatureFahrenheit: Double = 0 |
|
| 192 |
+ @Published var usbPlusVoltage: Double = 0 |
|
| 193 |
+ @Published var usbMinusVoltage: Double = 0 |
|
| 194 |
+ @Published var recordedAH: Double = 0 |
|
| 195 |
+ @Published var recordedWH: Double = 0 |
|
| 196 |
+ @Published var recording: Bool = false |
|
| 197 |
+ @Published var recordingTreshold: Double = 0 /* MARK: Seteaza inutil la pornire {
|
|
| 198 |
+ didSet {
|
|
| 199 |
+ if recordingTreshold != oldValue {
|
|
| 200 |
+ setrecordingTreshold(to: (recordingTreshold*100).uInt8Value) |
|
| 201 |
+ } |
|
| 202 |
+ } |
|
| 203 |
+ } */ |
|
| 204 |
+ @Published var currentScreen: UInt16 = 0 |
|
| 205 |
+ @Published var recordingDuration: UInt32 = 0 |
|
| 206 |
+ @Published var loadResistance: Double = 0 |
|
| 207 |
+ @Published var modelNumber: UInt16 = 0 |
|
| 208 |
+ @Published var chargerTypeIndex: UInt16 = 0 |
|
| 209 |
+ private var enableAutoConnect: Bool = false |
|
| 210 |
+ |
|
| 211 |
+ init ( model: Model, with serialPort: BluetoothSerial ) {
|
|
| 212 |
+ uuid = serialPort.peripheral.identifier |
|
| 213 |
+ //dataStore.meterUUIDS.append(serialPort.peripheral.identifier) |
|
| 214 |
+ modelString = serialPort.peripheral.name! |
|
| 215 |
+ self.model = model |
|
| 216 |
+ btSerial = serialPort |
|
| 217 |
+ name = appData.meterNames[serialPort.macAddress.description] ?? serialPort.macAddress.description |
|
| 218 |
+ super.init() |
|
| 219 |
+ btSerial.delegate = self |
|
| 220 |
+ //name = dataStore.meterNames[macAddress.description] ?? peripheral.name! |
|
| 221 |
+ for index in stride(from: 0, through: 9, by: 1) {
|
|
| 222 |
+ dataGroupRecords[index] = DataGroupRecord(ah:0, wh: 0) |
|
| 223 |
+ } |
|
| 224 |
+ } |
|
| 225 |
+ |
|
| 226 |
+ func dataDumpRequest() {
|
|
| 227 |
+ if commandQueue.isEmpty {
|
|
| 228 |
+ switch model {
|
|
| 229 |
+ case .UM25C: |
|
| 230 |
+ btSerial.write( Data([0xF0]), expectedResponseLength: 130) |
|
| 231 |
+ case .UM34C: |
|
| 232 |
+ btSerial.write( Data([0xF0]), expectedResponseLength: 130) |
|
| 233 |
+ case .TC66C: |
|
| 234 |
+ btSerial.write( "bgetva\r\n".data(using: String.Encoding.ascii)!, expectedResponseLength: 192) |
|
| 235 |
+ } |
|
| 236 |
+ dataDumpRequestTimestamp = Date() |
|
| 237 |
+ // track("\(name) - Request sent!")
|
|
| 238 |
+ } else {
|
|
| 239 |
+ track("Request delayed for: \(commandQueue.first!.hexEncodedStringValue)")
|
|
| 240 |
+ btSerial.write( commandQueue.first! ) |
|
| 241 |
+ commandQueue.removeFirst() |
|
| 242 |
+ DispatchQueue.main.asyncAfter( deadline: .now() + 1 ) {
|
|
| 243 |
+ self.dataDumpRequest() |
|
| 244 |
+ } |
|
| 245 |
+ } |
|
| 246 |
+ } |
|
| 247 |
+ |
|
| 248 |
+ /** |
|
| 249 |
+ received data parser |
|
| 250 |
+ - parameter buffer cotains response for data dump request |
|
| 251 |
+ - Decription metod for TC66C AES ECB response found [here](https:github.com/krzyzanowskim/CryptoSwift/issues/693) |
|
| 252 |
+ */ |
|
| 253 |
+ func parseData ( from buffer: Data) {
|
|
| 254 |
+ //track("\(name)")
|
|
| 255 |
+ switch model {
|
|
| 256 |
+ case .UM25C: |
|
| 257 |
+ parseUMData(from: buffer) |
|
| 258 |
+ case .UM34C: |
|
| 259 |
+ parseUMData(from: buffer) |
|
| 260 |
+ case .TC66C: |
|
| 261 |
+ parseTCData( from: buffer ) |
|
| 262 |
+ } |
|
| 263 |
+ measurements.addValues(timestamp: dataDumpRequestTimestamp, power: power, voltage: voltage, current: current) |
|
| 264 |
+// DispatchQueue.global(qos: .userInitiated).asyncAfter( deadline: .now() + 0.33 ) {
|
|
| 265 |
+// //track("\(name) - Scheduled new request.")
|
|
| 266 |
+// } |
|
| 267 |
+ operationalState = .dataIsAvailable |
|
| 268 |
+ dataDumpRequest() |
|
| 269 |
+ } |
|
| 270 |
+ |
|
| 271 |
+ func parseUMData(from buffer: Data) {
|
|
| 272 |
+ modelNumber = UInt16( bigEndian: buffer.value( from: 0 ) ) |
|
| 273 |
+ switch model {
|
|
| 274 |
+ case .UM25C: |
|
| 275 |
+ voltage = Double( UInt16( bigEndian: buffer.value( from: 2) ) )/1000 |
|
| 276 |
+ current = Double( UInt16( bigEndian: buffer.value( from: 4) ) )/10000 |
|
| 277 |
+ case .UM34C: |
|
| 278 |
+ voltage = Double( UInt16( bigEndian: buffer.value( from: 2) ) )/100 |
|
| 279 |
+ current = Double( UInt16( bigEndian: buffer.value( from: 4) ) )/1000 |
|
| 280 |
+ case .TC66C: |
|
| 281 |
+ track("\(name) - This is not possible!")
|
|
| 282 |
+ |
|
| 283 |
+ } |
|
| 284 |
+ power = Double( UInt32( bigEndian: buffer.value( from: 6) ) )/1000 |
|
| 285 |
+ temperatureCelsius = Double( UInt16( bigEndian: buffer.value( from: 10) ) ) |
|
| 286 |
+ temperatureFahrenheit = Double( UInt16( bigEndian: buffer.value( from: 12) ) ) |
|
| 287 |
+ selectedDataGroup = UInt8(UInt16( bigEndian: buffer.value( from: 14) ) ) |
|
| 288 |
+ for index in stride(from: 0, through: 9, by: 1) {
|
|
| 289 |
+ let offset = 16 + index * 8 |
|
| 290 |
+ dataGroupRecords[index] = DataGroupRecord(ah: Double (UInt32( bigEndian: buffer.value( from: offset ) ) )/1000, wh:Double (UInt32( bigEndian: buffer.value( from: offset + 4 ) ) )/1000) |
|
| 291 |
+ } |
|
| 292 |
+ usbPlusVoltage = Double( UInt16( bigEndian: buffer.value( from: 96) ) )/100 |
|
| 293 |
+ usbMinusVoltage = Double( UInt16( bigEndian: buffer.value( from: 98) ) )/100 |
|
| 294 |
+ chargerTypeIndex = UInt16( bigEndian: buffer.value( from: 100) ) |
|
| 295 |
+ recordedAH = Double (UInt32( bigEndian: buffer.value( from: 102 ) ) )/1000 |
|
| 296 |
+ recordedWH = Double (UInt32( bigEndian: buffer.value( from: 106 ) ) )/1000 |
|
| 297 |
+ recordingTreshold = Double (UInt16( bigEndian: buffer.value( from: 110 ) ) )/100 |
|
| 298 |
+ recordingDuration = UInt32( bigEndian: buffer.value( from: 112 ) ) |
|
| 299 |
+ recording = UInt16( bigEndian: buffer.value( from: 116 ) ) == 1 |
|
| 300 |
+ if screenTimeoutTimestamp < dataDumpRequestTimestamp {
|
|
| 301 |
+ let tmpValue = Int (UInt16( bigEndian: buffer.value( from: 118 ) ) ) |
|
| 302 |
+ if screenTimeout != tmpValue {
|
|
| 303 |
+ screenTimeout = tmpValue |
|
| 304 |
+ } |
|
| 305 |
+ } else {
|
|
| 306 |
+ track("\(name) - Skip updating screenTimeout (changed after request).")
|
|
| 307 |
+ } |
|
| 308 |
+ |
|
| 309 |
+ if screenBrightnessTimestamp < dataDumpRequestTimestamp {
|
|
| 310 |
+ let tmpValue = Int (UInt16( bigEndian: buffer.value( from: 120 ) ) ) |
|
| 311 |
+ if screenBrightness != tmpValue {
|
|
| 312 |
+ screenBrightness = tmpValue |
|
| 313 |
+ } |
|
| 314 |
+ } else {
|
|
| 315 |
+ track("\(name) - Skip updating screenBrightness (changed after request).")
|
|
| 316 |
+ } |
|
| 317 |
+ |
|
| 318 |
+ currentScreen = UInt16(bigEndian: buffer.value( from: 126 ) ) |
|
| 319 |
+ loadResistance = Double ( UInt32( bigEndian: buffer.value( from: 122 ) ) )/10 |
|
| 320 |
+ // track("\(name) - Model Number = \(modelNumber)")
|
|
| 321 |
+ // track("\(name) - chargerTypeIndex = \(chargerTypeIndex)")
|
|
| 322 |
+ } |
|
| 323 |
+ |
|
| 324 |
+ private func validatePac ( id: UInt8, pac: Data ) -> Bool {
|
|
| 325 |
+ //track("\(name) - \(id) - \(pac.hexEncodedStringValue)")
|
|
| 326 |
+ let expectedHeader = "pac\(id)".data(using: String.Encoding.ascii) |
|
| 327 |
+ let pacHeader = pac.subdata(from: 0, length: 4) |
|
| 328 |
+ let expectedCRC = UInt16( bigEndian: pac.subdata(from: 0, length: 60).crc16(seed: 0xFFFF).value( from: 0 ) ) |
|
| 329 |
+ let pacCRC = UInt16( littleEndian: pac.value(from: 60) ) |
|
| 330 |
+ return expectedHeader == pacHeader && expectedCRC == pacCRC |
|
| 331 |
+ } |
|
| 332 |
+ |
|
| 333 |
+ func parseTCData(from buffer: Data) {
|
|
| 334 |
+ do {
|
|
| 335 |
+ let key: [UInt8] = [ |
|
| 336 |
+ 0x58, 0x21, 0xfa, 0x56, 0x01, 0xb2, 0xf0, 0x26, |
|
| 337 |
+ 0x87, 0xff, 0x12, 0x04, 0x62, 0x2a, 0x4f, 0xb0, |
|
| 338 |
+ 0x86, 0xf4, 0x02, 0x60, 0x81, 0x6f, 0x9a, 0x0b, |
|
| 339 |
+ 0xa7, 0xf1, 0x06, 0x61, 0x9a, 0xb8, 0x72, 0x88 |
|
| 340 |
+ ] |
|
| 341 |
+ let cipher = try! AES(key: key, blockMode: ECB()) |
|
| 342 |
+ let decryptedBuffer = Data( try cipher.decrypt(buffer.bytes) ) |
|
| 343 |
+ let pac1: Data = decryptedBuffer.subdata( from: 0, length: 64 ) |
|
| 344 |
+ if validatePac(id: 1, pac: pac1) {
|
|
| 345 |
+ let pac2: Data = decryptedBuffer.subdata( from: 64, length: 64 ) |
|
| 346 |
+ if validatePac(id: 2, pac: pac2) {
|
|
| 347 |
+ let pac3: Data = decryptedBuffer.subdata( from: 128, length: 64 ) |
|
| 348 |
+ if validatePac(id: 3, pac: pac3) {
|
|
| 349 |
+ // let modelName = pac1.subdata(from: 4, length: 4).asciiString |
|
| 350 |
+ // track("\(name) - Model: \(modelName)")
|
|
| 351 |
+ // let firmwareVersion = pac1.subdata(from: 8, length: 4).asciiString |
|
| 352 |
+ // track("\(name) - Firmware Version: \(firmwareVersion)")
|
|
| 353 |
+ // let serialNumber = UInt32( littleEndian: pac1.value( from: 12 ) ) |
|
| 354 |
+ // track("\(name) - Serial Number: \(serialNumber)")
|
|
| 355 |
+ // let powerCycleCount = UInt32( littleEndian: pac1.value( from: 44 ) ) |
|
| 356 |
+ // track("\(name) - Power Cycle Count: \(powerCycleCount)")
|
|
| 357 |
+ voltage = Double( UInt32( littleEndian: pac1.value( from: 48) ) )/10000 |
|
| 358 |
+ current = Double( UInt32( littleEndian: pac1.value( from: 52) ) )/100000 |
|
| 359 |
+ power = Double( UInt32( littleEndian: pac1.value( from: 56) ) )/10000 |
|
| 360 |
+ loadResistance = Double( UInt32( littleEndian: pac2.value( from: 4) ) )/10 |
|
| 361 |
+ for index in stride(from: 0, through: 1, by: 1) {
|
|
| 362 |
+ let offset = 8 + index * 8 |
|
| 363 |
+ dataGroupRecords[index] = DataGroupRecord(ah: Double (UInt32( littleEndian: pac2.value( from: offset ) ) )/1000, wh:Double (UInt32( littleEndian: pac2.value( from: offset + 40 ) ) )/1000) |
|
| 364 |
+ } |
|
| 365 |
+ temperatureCelsius = Double( UInt32( littleEndian: pac2.value( from: 28 ) )) * ( UInt32( littleEndian: pac2.value( from: 24 ) ) == 1 ? -1 : 1 ) |
|
| 366 |
+ usbPlusVoltage = Double( UInt32( littleEndian: pac2.value( from: 32) ) )/100 |
|
| 367 |
+ usbMinusVoltage = Double( UInt32( littleEndian: pac2.value( from: 36) ) )/100 |
|
| 368 |
+ return |
|
| 369 |
+ } |
|
| 370 |
+ } |
|
| 371 |
+ } |
|
| 372 |
+ track("\(name) - Invalid data")
|
|
| 373 |
+ |
|
| 374 |
+ } catch {
|
|
| 375 |
+ track("\(name) - Error: \(error)")
|
|
| 376 |
+ } |
|
| 377 |
+ } |
|
| 378 |
+ |
|
| 379 |
+ func nextScreen() {
|
|
| 380 |
+ switch model {
|
|
| 381 |
+ case .UM25C: |
|
| 382 |
+ commandQueue.append( Data( [0xF1] ) ) |
|
| 383 |
+ case .UM34C: |
|
| 384 |
+ commandQueue.append( Data( [0xF1] ) ) |
|
| 385 |
+ case .TC66C: |
|
| 386 |
+ commandQueue.append( "bnextp\r\n".data(using: String.Encoding.ascii)! ) |
|
| 387 |
+ } |
|
| 388 |
+ } |
|
| 389 |
+ |
|
| 390 |
+ func rotateScreen() {
|
|
| 391 |
+ switch model {
|
|
| 392 |
+ case .UM25C: |
|
| 393 |
+ commandQueue.append( Data( [0xF2] ) ) |
|
| 394 |
+ case .UM34C: |
|
| 395 |
+ commandQueue.append( Data( [0xF2] ) ) |
|
| 396 |
+ case .TC66C: |
|
| 397 |
+ commandQueue.append( "brotat\r\n".data(using: String.Encoding.ascii)! ) |
|
| 398 |
+ } |
|
| 399 |
+ } |
|
| 400 |
+ |
|
| 401 |
+ func previousScreen() {
|
|
| 402 |
+ switch model {
|
|
| 403 |
+ case .UM25C: |
|
| 404 |
+ commandQueue.append( Data( [0xF3] ) ) |
|
| 405 |
+ case .UM34C: |
|
| 406 |
+ commandQueue.append( Data( [0xF3] ) ) |
|
| 407 |
+ case .TC66C: |
|
| 408 |
+ commandQueue.append( "blastp\r\n".data(using: String.Encoding.ascii)! ) |
|
| 409 |
+ } |
|
| 410 |
+ } |
|
| 411 |
+ |
|
| 412 |
+ func clear() {
|
|
| 413 |
+ guard model != .TC66C else { return }
|
|
| 414 |
+ commandQueue.append( Data( [0xF4] ) ) |
|
| 415 |
+ } |
|
| 416 |
+ |
|
| 417 |
+ func clear(group id: UInt8) {
|
|
| 418 |
+ guard model != .TC66C else { return }
|
|
| 419 |
+ commandQueue.append( Data( [0xA0 | id] ) ) |
|
| 420 |
+ clear() |
|
| 421 |
+ commandQueue.append( Data( [0xA0 | selectedDataGroup] ) ) |
|
| 422 |
+ } |
|
| 423 |
+ |
|
| 424 |
+ func selectDataGroup ( id: UInt8) {
|
|
| 425 |
+ track("\(name) - \(id)")
|
|
| 426 |
+ selectedDataGroup = id |
|
| 427 |
+ commandQueue.append( Data( [0xA0 | selectedDataGroup] ) ) |
|
| 428 |
+ } |
|
| 429 |
+ |
|
| 430 |
+ private func setSceeenBrightness ( to value: UInt8) {
|
|
| 431 |
+ track("\(name) - \(value)")
|
|
| 432 |
+ guard model != .TC66C else { return }
|
|
| 433 |
+ commandQueue.append( Data( [0xD0 | value] ) ) |
|
| 434 |
+ } |
|
| 435 |
+ private func setScreenSaverTimeout ( to value: UInt8) {
|
|
| 436 |
+ track("\(name) - \(value)")
|
|
| 437 |
+ guard model != .TC66C else { return }
|
|
| 438 |
+ commandQueue.append( Data( [0xE0 | value]) ) |
|
| 439 |
+ } |
|
| 440 |
+ func setrecordingTreshold ( to value: UInt8) {
|
|
| 441 |
+ guard model != .TC66C else { return }
|
|
| 442 |
+ commandQueue.append( Data( [0xB0 + value] ) ) |
|
| 443 |
+ } |
|
| 444 |
+ |
|
| 445 |
+ /** |
|
| 446 |
+ Connect to meter. |
|
| 447 |
+ 1. It calls BluetoothSerial.connect |
|
| 448 |
+ */ |
|
| 449 |
+ func connect() {
|
|
| 450 |
+ enableAutoConnect = true |
|
| 451 |
+ btSerial.connect() |
|
| 452 |
+ } |
|
| 453 |
+ |
|
| 454 |
+ /** |
|
| 455 |
+ Disconnect from meter. |
|
| 456 |
+ It calls BluetoothSerial.disconnect |
|
| 457 |
+ */ |
|
| 458 |
+ func disconnect() {
|
|
| 459 |
+ enableAutoConnect = false |
|
| 460 |
+ btSerial.disconnect() |
|
| 461 |
+ } |
|
| 462 |
+} |
|
| 463 |
+ |
|
| 464 |
+extension Meter : SerialPortDelegate {
|
|
| 465 |
+ |
|
| 466 |
+ func opertionalStateChanged(to serialPortOperationalState: BluetoothSerial.OperationalState) {
|
|
| 467 |
+ lastSeen = Date() |
|
| 468 |
+ //track("\(name) - \(serialPortOperationalState)")
|
|
| 469 |
+ switch serialPortOperationalState {
|
|
| 470 |
+ case .peripheralNotConnected: |
|
| 471 |
+ operationalState = .peripheralNotConnected |
|
| 472 |
+ case .peripheralConnectionPending: |
|
| 473 |
+ operationalState = .peripheralConnectionPending |
|
| 474 |
+ case .peripheralConnected: |
|
| 475 |
+ operationalState = .peripheralConnected |
|
| 476 |
+ case .peripheralReady: |
|
| 477 |
+ operationalState = .peripheralReady |
|
| 478 |
+ } |
|
| 479 |
+ } |
|
| 480 |
+ |
|
| 481 |
+ func didReceiveData(_ data: Data) {
|
|
| 482 |
+ lastSeen = Date() |
|
| 483 |
+ operationalState = .comunicating |
|
| 484 |
+ parseData(from: data) |
|
| 485 |
+ } |
|
| 486 |
+} |
|
@@ -0,0 +1,6 @@ |
||
| 1 |
+{
|
|
| 2 |
+ "info" : {
|
|
| 3 |
+ "version" : 1, |
|
| 4 |
+ "author" : "xcode" |
|
| 5 |
+ } |
|
| 6 |
+} |
|
@@ -0,0 +1,74 @@ |
||
| 1 |
+// |
|
| 2 |
+// SceneDelegate.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 01/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import UIKit |
|
| 10 |
+import SwiftUI |
|
| 11 |
+ |
|
| 12 |
+class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
| 13 |
+ |
|
| 14 |
+ var window: UIWindow? |
|
| 15 |
+ |
|
| 16 |
+ |
|
| 17 |
+ func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
|
| 18 |
+ |
|
| 19 |
+ // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. |
|
| 20 |
+ // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. |
|
| 21 |
+ // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). |
|
| 22 |
+ |
|
| 23 |
+ // Get the managed object context from the shared persistent container. |
|
| 24 |
+ let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext |
|
| 25 |
+ |
|
| 26 |
+ // Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath. |
|
| 27 |
+ // Add `@Environment(\.managedObjectContext)` in the views that will need the context. |
|
| 28 |
+ let contentView = ContentView() |
|
| 29 |
+ .environment(\.managedObjectContext, context) |
|
| 30 |
+ .environmentObject(appData) |
|
| 31 |
+ |
|
| 32 |
+ // Use a UIHostingController as window root view controller. |
|
| 33 |
+ if let windowScene = scene as? UIWindowScene {
|
|
| 34 |
+ let window = UIWindow(windowScene: windowScene) |
|
| 35 |
+ window.rootViewController = UIHostingController(rootView: contentView) |
|
| 36 |
+ self.window = window |
|
| 37 |
+ window.makeKeyAndVisible() |
|
| 38 |
+ } |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ func sceneDidDisconnect(_ scene: UIScene) {
|
|
| 42 |
+ // Called as the scene is being released by the system. |
|
| 43 |
+ // This occurs shortly after the scene enters the background, or when its session is discarded. |
|
| 44 |
+ // Release any resources associated with this scene that can be re-created the next time the scene connects. |
|
| 45 |
+ // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). |
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 48 |
+ func sceneDidBecomeActive(_ scene: UIScene) {
|
|
| 49 |
+ // Called when the scene has moved from an inactive state to an active state. |
|
| 50 |
+ // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ func sceneWillResignActive(_ scene: UIScene) {
|
|
| 54 |
+ // Called when the scene will move from an active state to an inactive state. |
|
| 55 |
+ // This may occur due to temporary interruptions (ex. an incoming phone call). |
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ func sceneWillEnterForeground(_ scene: UIScene) {
|
|
| 59 |
+ // Called as the scene transitions from the background to the foreground. |
|
| 60 |
+ // Use this method to undo the changes made on entering the background. |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ func sceneDidEnterBackground(_ scene: UIScene) {
|
|
| 64 |
+ // Called as the scene transitions from the foreground to the background. |
|
| 65 |
+ // Use this method to save data, release shared resources, and store enough scene-specific state information |
|
| 66 |
+ // to restore the scene back to its current state. |
|
| 67 |
+ |
|
| 68 |
+ // Save changes in the application's managed object context when the application transitions to the background. |
|
| 69 |
+ (UIApplication.shared.delegate as? AppDelegate)?.saveContext() |
|
| 70 |
+ } |
|
| 71 |
+ |
|
| 72 |
+ |
|
| 73 |
+} |
|
| 74 |
+ |
|
@@ -0,0 +1,29 @@ |
||
| 1 |
+// |
|
| 2 |
+// ICloudDefault.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 12/04/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import Foundation |
|
| 10 |
+// https://github.com/lobodpav/Xcode11.4Issues/blob/master/Sources/Xcode11.4Test/CloudListener.swift |
|
| 11 |
+// https://medium.com/@craiggrummitt/boss-level-property-wrappers-and-user-defaults-6a28c7527cf |
|
| 12 |
+@propertyWrapper struct ICloudDefault<T> {
|
|
| 13 |
+ let key: String |
|
| 14 |
+ let defaultValue: T |
|
| 15 |
+ |
|
| 16 |
+ var wrappedValue: T {
|
|
| 17 |
+ get {
|
|
| 18 |
+ return NSUbiquitousKeyValueStore.default.object(forKey: key) as? T ?? defaultValue |
|
| 19 |
+ } |
|
| 20 |
+ set {
|
|
| 21 |
+ NSUbiquitousKeyValueStore.default.set(newValue, forKey: key) |
|
| 22 |
+ /* MARK: Sincronizarea forțată |
|
| 23 |
+ Face ca sincronizarea intre dispozitive mai repidă dar există o limita de update-uri catre iloud |
|
| 24 |
+ */ |
|
| 25 |
+ NSUbiquitousKeyValueStore.default.synchronize() |
|
| 26 |
+ track("Pushed into iCloud value: '\(newValue)' for key: '\(key)'")
|
|
| 27 |
+ } |
|
| 28 |
+ } |
|
| 29 |
+} |
|
@@ -0,0 +1,43 @@ |
||
| 1 |
+// |
|
| 2 |
+// UserDefault.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 03/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import Foundation |
|
| 10 |
+ |
|
| 11 |
+@propertyWrapper struct UserDefault<T> {
|
|
| 12 |
+ let key: String |
|
| 13 |
+ let defaultValue: T |
|
| 14 |
+ |
|
| 15 |
+ var wrappedValue: T {
|
|
| 16 |
+ get {
|
|
| 17 |
+ return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue |
|
| 18 |
+ } |
|
| 19 |
+ set {
|
|
| 20 |
+ UserDefaults.standard.set(newValue, forKey: key) |
|
| 21 |
+ } |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+// static func getValue(forKey: String) -> T? {
|
|
| 25 |
+// return UserDefaults.standard.object(forKey: forKey) as? T |
|
| 26 |
+// } |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+// Asta cel mai probabil face figuri pentru ca nu pare sa aiba observer |
|
| 30 |
+@propertyWrapper struct UserRuntime<T> {
|
|
| 31 |
+ |
|
| 32 |
+ var value: T |
|
| 33 |
+ |
|
| 34 |
+ var wrappedValue: T {
|
|
| 35 |
+ get {
|
|
| 36 |
+ return value |
|
| 37 |
+ } |
|
| 38 |
+ set {
|
|
| 39 |
+ value = newValue |
|
| 40 |
+ } |
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+} |
|
@@ -0,0 +1,14 @@ |
||
| 1 |
+<?xml version="1.0" encoding="UTF-8"?> |
|
| 2 |
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|
| 3 |
+<plist version="1.0"> |
|
| 4 |
+<dict> |
|
| 5 |
+ <key>com.apple.developer.icloud-container-identifiers</key> |
|
| 6 |
+ <array/> |
|
| 7 |
+ <key>com.apple.developer.ubiquity-kvstore-identifier</key> |
|
| 8 |
+ <string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string> |
|
| 9 |
+ <key>com.apple.security.app-sandbox</key> |
|
| 10 |
+ <true/> |
|
| 11 |
+ <key>com.apple.security.device.bluetooth</key> |
|
| 12 |
+ <true/> |
|
| 13 |
+</dict> |
|
| 14 |
+</plist> |
|
@@ -0,0 +1,32 @@ |
||
| 1 |
+// |
|
| 2 |
+// BorderView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 11/04/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+struct BorderView: View {
|
|
| 12 |
+ let show: Bool |
|
| 13 |
+ var fillColor: Color = .clear |
|
| 14 |
+ var opacity = 0.5 |
|
| 15 |
+ |
|
| 16 |
+ var body: some View {
|
|
| 17 |
+ ZStack {
|
|
| 18 |
+ RoundedRectangle(cornerRadius: 10) |
|
| 19 |
+ .foregroundColor(fillColor).opacity(opacity) |
|
| 20 |
+ |
|
| 21 |
+ RoundedRectangle(cornerRadius: 10) |
|
| 22 |
+ .stroke(lineWidth: 3.0).foregroundColor(show ? fillColor : Color.clear) |
|
| 23 |
+ .animation(.linear(duration: 0.1)) |
|
| 24 |
+ } |
|
| 25 |
+ } |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+struct BorderView_Previews: PreviewProvider {
|
|
| 29 |
+ static var previews: some View {
|
|
| 30 |
+ BorderView(show: true) |
|
| 31 |
+ } |
|
| 32 |
+} |
|
@@ -0,0 +1,49 @@ |
||
| 1 |
+// |
|
| 2 |
+// ContentView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 01/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+//MARK: Bluetooth Icon: https://upload.wikimedia.org/wikipedia/commons/d/da/Bluetooth.svg |
|
| 10 |
+ |
|
| 11 |
+import SwiftUI |
|
| 12 |
+ |
|
| 13 |
+struct ContentView: View {
|
|
| 14 |
+ |
|
| 15 |
+ @EnvironmentObject private var appData: AppData |
|
| 16 |
+ |
|
| 17 |
+ var body: some View {
|
|
| 18 |
+ NavigationView {
|
|
| 19 |
+ List {
|
|
| 20 |
+ Section(header: Text("Help")) {
|
|
| 21 |
+ //VStack {
|
|
| 22 |
+ NavigationLink(destination: appData.bluetoothManager.managerState.helpView ) {
|
|
| 23 |
+ Text("Bluetooth")
|
|
| 24 |
+ .foregroundColor(appData.bluetoothManager.managerState.color) |
|
| 25 |
+ } |
|
| 26 |
+ NavigationLink(destination: DeviceHelpView()) {
|
|
| 27 |
+ Text("Device")
|
|
| 28 |
+ .foregroundColor(appData.bluetoothManager.managerState.color) |
|
| 29 |
+ } |
|
| 30 |
+ } |
|
| 31 |
+ Section(header: Text("Discovered Devices")) {
|
|
| 32 |
+ ForEach( [Meter](appData.meters.values), id: \.self ) { meter in
|
|
| 33 |
+ NavigationLink(destination: MeterView() |
|
| 34 |
+ .environmentObject(meter) ) {
|
|
| 35 |
+ MeterRowView() |
|
| 36 |
+ .environmentObject( meter ) |
|
| 37 |
+ } |
|
| 38 |
+ } |
|
| 39 |
+ } |
|
| 40 |
+ } |
|
| 41 |
+ .navigationBarTitle(Text("USB Meters"), displayMode: .inline)
|
|
| 42 |
+ .navigationBarItems(trailing: |
|
| 43 |
+ Button("Help") {
|
|
| 44 |
+ print("Help tapped!")
|
|
| 45 |
+ }) |
|
| 46 |
+ } |
|
| 47 |
+ } |
|
| 48 |
+} |
|
| 49 |
+ |
|
@@ -0,0 +1,20 @@ |
||
| 1 |
+// |
|
| 2 |
+// BluetoothHelperView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 07/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+import CoreBluetooth |
|
| 11 |
+ |
|
| 12 |
+struct DeviceHelpView: View {
|
|
| 13 |
+ var body: some View {
|
|
| 14 |
+ List {
|
|
| 15 |
+ Text("Device ON?")
|
|
| 16 |
+ Text("Device has Bluetooth enabled?")
|
|
| 17 |
+ Text("Device allerady connected?")
|
|
| 18 |
+ } |
|
| 19 |
+ } |
|
| 20 |
+} |
|
@@ -0,0 +1,27 @@ |
||
| 1 |
+// |
|
| 2 |
+// SwiftUIView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 02/05/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+struct ChevronView: View {
|
|
| 12 |
+ |
|
| 13 |
+ @Binding var rotate: Bool |
|
| 14 |
+ |
|
| 15 |
+ var body: some View {
|
|
| 16 |
+ Button(action: {
|
|
| 17 |
+ self.rotate.toggle() |
|
| 18 |
+ }) {
|
|
| 19 |
+ Image(systemName: "chevron.right.circle") |
|
| 20 |
+ .imageScale(.large) |
|
| 21 |
+ .rotationEffect(.degrees(rotate ? 270 : 90)) |
|
| 22 |
+ .animation(.easeInOut) |
|
| 23 |
+ .padding(.vertical) |
|
| 24 |
+ } |
|
| 25 |
+ } |
|
| 26 |
+} |
|
| 27 |
+ |
|
@@ -0,0 +1,35 @@ |
||
| 1 |
+// |
|
| 2 |
+// ControlView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 09/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+struct ControlView: View {
|
|
| 12 |
+ |
|
| 13 |
+ @EnvironmentObject private var meter: Meter |
|
| 14 |
+ |
|
| 15 |
+ var body: some View {
|
|
| 16 |
+ HStack {
|
|
| 17 |
+ VStack {
|
|
| 18 |
+ HStack {
|
|
| 19 |
+ Button(action: { self.meter.rotateScreen() }, label: { Image(systemName: "arrow.clockwise") })
|
|
| 20 |
+ } |
|
| 21 |
+ HStack {
|
|
| 22 |
+ Button(action: { self.meter.previousScreen() }, label: { Image(systemName: "arrowtriangle.left") })
|
|
| 23 |
+ Text("Current Screen: \(meter.currentScreen)")
|
|
| 24 |
+ Button(action: { self.meter.nextScreen() }, label: { Image(systemName: "arrowtriangle.right") })
|
|
| 25 |
+ } |
|
| 26 |
+ } |
|
| 27 |
+ } |
|
| 28 |
+ } |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+struct ControlView_Previews: PreviewProvider {
|
|
| 32 |
+ static var previews: some View {
|
|
| 33 |
+ ControlView() |
|
| 34 |
+ } |
|
| 35 |
+} |
|
@@ -0,0 +1,46 @@ |
||
| 1 |
+// |
|
| 2 |
+// DataGroupView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 10/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+// https://www.youtube.com/watch?v=9q209mNDV_4 |
|
| 9 |
+ |
|
| 10 |
+import SwiftUI |
|
| 11 |
+ |
|
| 12 |
+struct DataGroupRowView: View {
|
|
| 13 |
+ |
|
| 14 |
+ var id: UInt8 |
|
| 15 |
+ var width: CGFloat |
|
| 16 |
+ var opacity: Double |
|
| 17 |
+ |
|
| 18 |
+ @EnvironmentObject private var usbMeter: Meter |
|
| 19 |
+ |
|
| 20 |
+ var body: some View {
|
|
| 21 |
+ HStack (spacing: 1) {
|
|
| 22 |
+ ZStack {
|
|
| 23 |
+ Button(action: { self.usbMeter.selectDataGroup( id: self.id ) }, label: { Image(systemName: "\(id).circle") })//.font(.title)
|
|
| 24 |
+ Rectangle().opacity( opacity ) |
|
| 25 |
+ }.frame(width: width) |
|
| 26 |
+ |
|
| 27 |
+ ZStack {
|
|
| 28 |
+ Text("\(usbMeter.dataGroupRecords[Int(id)]!.ah.format(decimalDigits: 3))")
|
|
| 29 |
+ Rectangle().opacity( opacity ) |
|
| 30 |
+ }.frame(width: width) |
|
| 31 |
+ |
|
| 32 |
+ ZStack {
|
|
| 33 |
+ Text("\(usbMeter.dataGroupRecords[Int(id)]!.wh.format(decimalDigits: 3))")
|
|
| 34 |
+ Rectangle().opacity( opacity ) |
|
| 35 |
+ }.frame(width: width) |
|
| 36 |
+ |
|
| 37 |
+ ZStack {
|
|
| 38 |
+ Button(action: { self.usbMeter.clear(group: self.id) }, label: { Image(systemName: "bin.xmark") })
|
|
| 39 |
+// .font(.title) |
|
| 40 |
+// .foregroundColor(.red) |
|
| 41 |
+ Rectangle().opacity( opacity ) |
|
| 42 |
+ }.frame(width: width) |
|
| 43 |
+ } |
|
| 44 |
+ .background(BorderView(show: usbMeter.selectedDataGroup == id)) |
|
| 45 |
+ } |
|
| 46 |
+} |
|
@@ -0,0 +1,58 @@ |
||
| 1 |
+// |
|
| 2 |
+// DataGroupsView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 10/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+// MARK: GeometryReader: https://stackoverflow.com/questions/57577462/get-width-of-a-view-using-in-swiftui/57591483 Pe iPhone dimensiunea shetului pare mai mare decat cea afisata fara GeometryReader |
|
| 9 |
+ |
|
| 10 |
+import SwiftUI |
|
| 11 |
+ |
|
| 12 |
+struct DataGroupsView: View {
|
|
| 13 |
+ |
|
| 14 |
+ @Binding var visibility: Bool |
|
| 15 |
+ @EnvironmentObject private var usbMeter: Meter |
|
| 16 |
+ |
|
| 17 |
+ var body: some View {
|
|
| 18 |
+ GeometryReader { box in
|
|
| 19 |
+ VStack (spacing: 1) {
|
|
| 20 |
+ HStack {
|
|
| 21 |
+ Text("Data Groups")
|
|
| 22 |
+ .bold() |
|
| 23 |
+ Spacer() |
|
| 24 |
+ Button(action: {self.visibility.toggle()}) {
|
|
| 25 |
+ Text("ⓧ")
|
|
| 26 |
+ .foregroundColor(.red) |
|
| 27 |
+ } |
|
| 28 |
+ } |
|
| 29 |
+ .font(.title) |
|
| 30 |
+ |
|
| 31 |
+ Spacer() |
|
| 32 |
+ |
|
| 33 |
+ HStack (spacing: 1) {
|
|
| 34 |
+ ForEach (["Group", "Ah", "Wh", "Clear"], id: \.self ) { text in
|
|
| 35 |
+ self.THView( text: text, width: (box.size.width-25)/4 ) |
|
| 36 |
+ } |
|
| 37 |
+ } |
|
| 38 |
+ .frame(height: ( box.size.height - 100 ) / 11) |
|
| 39 |
+ ForEach((0...9), id: \.self) { groupId in
|
|
| 40 |
+ DataGroupRowView( id: UInt8(groupId), width: ((box.size.width-25)/4 ), opacity : groupId.isMultiple(of: 2) ? 0.1 : 0.2 ) |
|
| 41 |
+ } |
|
| 42 |
+ .frame(height: ( box.size.height - 100 ) / 11) |
|
| 43 |
+ } |
|
| 44 |
+ .padding() |
|
| 45 |
+ } |
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 48 |
+ fileprivate func THView(text: String, width: CGFloat) -> some View {
|
|
| 49 |
+ return ZStack {
|
|
| 50 |
+ Rectangle() |
|
| 51 |
+ .opacity(0.4) |
|
| 52 |
+ Text(text) |
|
| 53 |
+ .bold() |
|
| 54 |
+ } |
|
| 55 |
+ .frame(width: width) |
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+} |
|
@@ -0,0 +1,58 @@ |
||
| 1 |
+// |
|
| 2 |
+// LiveView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 09/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+struct LiveView: View {
|
|
| 12 |
+ |
|
| 13 |
+ @EnvironmentObject private var meter: Meter |
|
| 14 |
+ |
|
| 15 |
+ var body: some View {
|
|
| 16 |
+ VStack {
|
|
| 17 |
+ Text("Live Data")
|
|
| 18 |
+ .font(.headline) |
|
| 19 |
+ HStack {
|
|
| 20 |
+ VStack (alignment: .leading) {
|
|
| 21 |
+ Text("Voltage:")
|
|
| 22 |
+ Text("Current:")
|
|
| 23 |
+ Text("Power:")
|
|
| 24 |
+ Text("Load")
|
|
| 25 |
+ Text("Temperature:")
|
|
| 26 |
+ Text("")
|
|
| 27 |
+ Text("USB Data+:")
|
|
| 28 |
+ Text("USB Data-:")
|
|
| 29 |
+ Text("Charger:")
|
|
| 30 |
+ } |
|
| 31 |
+ VStack (alignment: .trailing) {
|
|
| 32 |
+ HStack {
|
|
| 33 |
+ Text("\(meter.measurements.voltage.context.minValue.format(decimalDigits: 3))V")
|
|
| 34 |
+ Text("\(meter.voltage.format(decimalDigits: 3))V")
|
|
| 35 |
+ Text("\(meter.measurements.voltage.context.maxValue.format(decimalDigits: 3))V")
|
|
| 36 |
+ } |
|
| 37 |
+ HStack {
|
|
| 38 |
+ Text("\(meter.measurements.current.context.minValue.format(decimalDigits: 3))A")
|
|
| 39 |
+ Text("\(meter.current.format(decimalDigits: 3))A")
|
|
| 40 |
+ Text("\(meter.measurements.current.context.maxValue.format(decimalDigits: 3))A")
|
|
| 41 |
+ } |
|
| 42 |
+ HStack {
|
|
| 43 |
+ Text("\(meter.measurements.power.context.minValue.format(decimalDigits: 3))W")
|
|
| 44 |
+ Text("\(meter.power.format(decimalDigits: 3))W")
|
|
| 45 |
+ Text("\(meter.measurements.power.context.maxValue.format(decimalDigits: 3))W")
|
|
| 46 |
+ } |
|
| 47 |
+ Text("\(meter.loadResistance.format(decimalDigits: 1))Ω")
|
|
| 48 |
+ Text("\(meter.temperatureCelsius)℃")
|
|
| 49 |
+ Text("\(meter.temperatureFahrenheit)℉")
|
|
| 50 |
+ Text("\(meter.usbPlusVoltage.format(decimalDigits: 2))V")
|
|
| 51 |
+ Text("\(meter.usbMinusVoltage.format(decimalDigits: 2))V")
|
|
| 52 |
+ Text("\(meter.chargerTypeIndex)")
|
|
| 53 |
+ } |
|
| 54 |
+ } |
|
| 55 |
+ .font(.footnote) |
|
| 56 |
+ } |
|
| 57 |
+ } |
|
| 58 |
+} |
|
@@ -0,0 +1,226 @@ |
||
| 1 |
+// |
|
| 2 |
+// MeasurementChartView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 06/05/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+struct MeasurementChartView: View {
|
|
| 12 |
+ |
|
| 13 |
+ @EnvironmentObject private var measurements: Measurements |
|
| 14 |
+ |
|
| 15 |
+ @State var displayVoltage: Bool = false |
|
| 16 |
+ @State var displayCurrent: Bool = false |
|
| 17 |
+ @State var displayPower: Bool = true |
|
| 18 |
+ let xLabels: Int = 4 |
|
| 19 |
+ let yLabels: Int = 4 |
|
| 20 |
+ |
|
| 21 |
+ var body: some View {
|
|
| 22 |
+ Group {
|
|
| 23 |
+ //if measurements.power.points.count > 0 {
|
|
| 24 |
+ VStack {
|
|
| 25 |
+ HStack {
|
|
| 26 |
+ Button( action: {
|
|
| 27 |
+ self.displayVoltage.toggle() |
|
| 28 |
+ if self.displayVoltage {
|
|
| 29 |
+ self.displayPower = false |
|
| 30 |
+ } |
|
| 31 |
+ } ) { Text("Voltage") }
|
|
| 32 |
+ .asEnableFeatureButton(state: displayVoltage) |
|
| 33 |
+ Button( action: {
|
|
| 34 |
+ self.displayCurrent.toggle() |
|
| 35 |
+ if self.displayCurrent {
|
|
| 36 |
+ self.displayPower = false |
|
| 37 |
+ } |
|
| 38 |
+ } ) { Text("Current") }
|
|
| 39 |
+ .asEnableFeatureButton(state: displayCurrent) |
|
| 40 |
+ Button( action: {
|
|
| 41 |
+ self.displayPower.toggle() |
|
| 42 |
+ if self.displayPower {
|
|
| 43 |
+ self.displayCurrent = false |
|
| 44 |
+ self.displayVoltage = false |
|
| 45 |
+ } |
|
| 46 |
+ } ) { Text("Power") }
|
|
| 47 |
+ .asEnableFeatureButton(state: displayPower) |
|
| 48 |
+ } |
|
| 49 |
+ .padding(.bottom, 5) |
|
| 50 |
+ if measurements.current.context.isValid {
|
|
| 51 |
+ VStack {
|
|
| 52 |
+ GeometryReader { geometry in
|
|
| 53 |
+ HStack {
|
|
| 54 |
+ Group { // MARK: Left Legend
|
|
| 55 |
+ if self.displayPower {
|
|
| 56 |
+ self.yAxisLabelsView(geometry: geometry, context: self.measurements.power.context, measurementUnit: "W") |
|
| 57 |
+ .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .red, opacity: 0.5) |
|
| 58 |
+ } else if self.displayVoltage {
|
|
| 59 |
+ self.yAxisLabelsView(geometry: geometry, context: self.measurements.voltage.context, measurementUnit: "V") |
|
| 60 |
+ .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .green, opacity: 0.5) |
|
| 61 |
+ } |
|
| 62 |
+ else if self.displayCurrent {
|
|
| 63 |
+ self.yAxisLabelsView(geometry: geometry, context: self.measurements.current.context, measurementUnit: "A") |
|
| 64 |
+ .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .blue, opacity: 0.5) |
|
| 65 |
+ } |
|
| 66 |
+ } |
|
| 67 |
+ ZStack { // MARK: Graph
|
|
| 68 |
+ if self.displayPower {
|
|
| 69 |
+ Chart(strokeColor: .red) |
|
| 70 |
+ .environmentObject(self.measurements.power) |
|
| 71 |
+ .opacity(0.5) |
|
| 72 |
+ } else {
|
|
| 73 |
+ if self.displayVoltage{
|
|
| 74 |
+ Chart(strokeColor: .green) |
|
| 75 |
+ .environmentObject(self.measurements.voltage) |
|
| 76 |
+ .opacity(0.5) |
|
| 77 |
+ } |
|
| 78 |
+ if self.displayCurrent{
|
|
| 79 |
+ Chart(strokeColor: .blue) |
|
| 80 |
+ .environmentObject(self.measurements.current) |
|
| 81 |
+ .opacity(0.5) |
|
| 82 |
+ } |
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ // MARK: Grid |
|
| 86 |
+ self.horizontalGuides() |
|
| 87 |
+ self.verticalGuides() |
|
| 88 |
+ } |
|
| 89 |
+ .withRoundedRectangleBackground( cornerRadius: 0, foregroundColor: .primary, opacity: 0.06 ) |
|
| 90 |
+ Group { // MARK: Right Legend
|
|
| 91 |
+ self.yAxisLabelsView(geometry: geometry, context: self.measurements.current.context, measurementUnit: "A") |
|
| 92 |
+ .foregroundColor(self.displayVoltage && self.displayCurrent ? .primary : .clear) |
|
| 93 |
+ .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .blue, opacity: 0.5) |
|
| 94 |
+ } |
|
| 95 |
+ } |
|
| 96 |
+ } |
|
| 97 |
+ xAxisLabelsView(context: self.measurements.current.context) |
|
| 98 |
+ .padding(.horizontal, 10) |
|
| 99 |
+ |
|
| 100 |
+ } |
|
| 101 |
+ } |
|
| 102 |
+ else {
|
|
| 103 |
+ Text("Nothing to show!")
|
|
| 104 |
+ } |
|
| 105 |
+ |
|
| 106 |
+ } |
|
| 107 |
+ .padding(10) |
|
| 108 |
+ .font(.footnote) |
|
| 109 |
+ .frame(maxWidth: .greatestFiniteMagnitude) |
|
| 110 |
+ .withRoundedRectangleBackground( cornerRadius: 15, foregroundColor: .primary, opacity: 0.03 ) |
|
| 111 |
+ .padding() |
|
| 112 |
+ } |
|
| 113 |
+ } |
|
| 114 |
+ |
|
| 115 |
+ // MARK: Cu iteratie nu functioneaza deoarece view-ul din bucla nu este reimprospatat la modificare obiectului observat |
|
| 116 |
+ fileprivate func xAxisLabelsView(context: ChartContext) -> some View {
|
|
| 117 |
+ var timeFormat: String? |
|
| 118 |
+ switch context.size.width {
|
|
| 119 |
+ case 0..<3600: timeFormat = "HH:mm:ss" |
|
| 120 |
+ case 3600...86400: timeFormat = "HH:mm" |
|
| 121 |
+ default: timeFormat = "E:HH:MM" |
|
| 122 |
+ } |
|
| 123 |
+ return HStack {
|
|
| 124 |
+ ForEach (1...xLabels, id: \.self) { i in
|
|
| 125 |
+ Group {
|
|
| 126 |
+ Text( "\(Date(timeIntervalSince1970: context.xAxisLabel(for: i, of: self.yLabels)).format(as: timeFormat!))" ) |
|
| 127 |
+ .fontWeight(.semibold) |
|
| 128 |
+ if i < self.xLabels {
|
|
| 129 |
+ Spacer() |
|
| 130 |
+ } |
|
| 131 |
+ } |
|
| 132 |
+ } |
|
| 133 |
+ } |
|
| 134 |
+ } |
|
| 135 |
+ |
|
| 136 |
+ fileprivate func yAxisLabelsView(geometry: GeometryProxy, context: ChartContext, measurementUnit: String) -> some View {
|
|
| 137 |
+ return ZStack {
|
|
| 138 |
+ VStack {
|
|
| 139 |
+ Text("\(context.yAxisLabel(for: 4, of: 4).format(fractionDigits: 2))")
|
|
| 140 |
+ .fontWeight(.semibold) |
|
| 141 |
+ .padding(.top, geometry.size.height*Constants.chartUnderscan/2 ) |
|
| 142 |
+ Spacer() |
|
| 143 |
+ ForEach (1..<yLabels-1, id: \.self) { i in
|
|
| 144 |
+ Group {
|
|
| 145 |
+ Text("\(context.yAxisLabel(for: self.yLabels-i, of: self.yLabels).format(fractionDigits: 2))")
|
|
| 146 |
+ .fontWeight(.semibold) |
|
| 147 |
+ Spacer() |
|
| 148 |
+ } |
|
| 149 |
+ } |
|
| 150 |
+ Text("\(context.yAxisLabel(for: 1, of: yLabels).format(fractionDigits: 2))")
|
|
| 151 |
+ .fontWeight(.semibold) |
|
| 152 |
+ .padding(.bottom, geometry.size.height*Constants.chartUnderscan/2 ) |
|
| 153 |
+ } |
|
| 154 |
+ VStack {
|
|
| 155 |
+ Text(measurementUnit) |
|
| 156 |
+ .fontWeight(.bold) |
|
| 157 |
+ .padding(.top, 5) |
|
| 158 |
+ Spacer() |
|
| 159 |
+ } |
|
| 160 |
+ } |
|
| 161 |
+ } |
|
| 162 |
+ |
|
| 163 |
+ fileprivate func horizontalGuides() -> some View {
|
|
| 164 |
+ GeometryReader { geometry in
|
|
| 165 |
+ Path { path in
|
|
| 166 |
+ let pading = geometry.size.height*Constants.chartUnderscan |
|
| 167 |
+ let height = geometry.size.height - pading |
|
| 168 |
+ let border = pading/2 |
|
| 169 |
+ for i: CGFloat in stride(from: 0, through: CGFloat(self.yLabels-1), by: 1) {
|
|
| 170 |
+ path.addLine(from: CGPoint(x: 0, y: border + height*i/CGFloat(self.yLabels-1 )), to: CGPoint(x: geometry.size.width, y: border + height*i/CGFloat(self.yLabels-1))) |
|
| 171 |
+ } |
|
| 172 |
+ }.stroke(lineWidth: 0.25) |
|
| 173 |
+ } |
|
| 174 |
+ } |
|
| 175 |
+ |
|
| 176 |
+ fileprivate func verticalGuides() -> some View {
|
|
| 177 |
+ GeometryReader { geometry in
|
|
| 178 |
+ Path { path in
|
|
| 179 |
+ |
|
| 180 |
+ for i: CGFloat in stride(from: 1, through: CGFloat(self.xLabels-1), by: 1) {
|
|
| 181 |
+ path.move(to: CGPoint(x: geometry.size.width*i/CGFloat(self.xLabels-1), y: 0) ) |
|
| 182 |
+ path.addLine(to: CGPoint(x: path.currentPoint!.x, y: geometry.size.height) ) |
|
| 183 |
+ } |
|
| 184 |
+ }.stroke(lineWidth: 0.25) |
|
| 185 |
+ } |
|
| 186 |
+ } |
|
| 187 |
+ |
|
| 188 |
+} |
|
| 189 |
+ |
|
| 190 |
+struct Chart : View {
|
|
| 191 |
+ |
|
| 192 |
+ @EnvironmentObject private var measurement: Measurements.Measurement |
|
| 193 |
+ var areaChart: Bool = false |
|
| 194 |
+ var strokeColor: Color = .black |
|
| 195 |
+ |
|
| 196 |
+ var body : some View {
|
|
| 197 |
+ GeometryReader { geometry in
|
|
| 198 |
+ if self.areaChart {
|
|
| 199 |
+ self.path( geometry: geometry ) |
|
| 200 |
+ .fill(LinearGradient( gradient: .init(colors: [Color.red, Color.green]), startPoint: .init(x: 0.5, y: 0.1), endPoint: .init(x: 0.5, y: 0.9))) |
|
| 201 |
+ } else {
|
|
| 202 |
+ self.path( geometry: geometry ) |
|
| 203 |
+ .stroke(self.strokeColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) |
|
| 204 |
+ } |
|
| 205 |
+ } |
|
| 206 |
+ } |
|
| 207 |
+ |
|
| 208 |
+ fileprivate func path(geometry: GeometryProxy) -> Path {
|
|
| 209 |
+ return Path { path in
|
|
| 210 |
+ let firstPoint = measurement.context.placeInRect(point: measurement.points.first!.point()) |
|
| 211 |
+ path.move(to: CGPoint(x: firstPoint.x * geometry.size.width, y: firstPoint.y * geometry.size.height ) ) |
|
| 212 |
+ for item in measurement.points.map({ measurement.context.placeInRect(point: $0.point()) }) {
|
|
| 213 |
+ path.addLine(to: CGPoint(x: item.x * geometry.size.width, y: item.y * geometry.size.height ) ) |
|
| 214 |
+ } |
|
| 215 |
+ if self.areaChart {
|
|
| 216 |
+ let lastPointX = measurement.context.placeInRect(point: CGPoint(x: measurement.points.last!.point().x, y: measurement.context.origin.y )) |
|
| 217 |
+ let firstPointX = measurement.context.placeInRect(point: CGPoint(x: measurement.points.first!.point().x, y: measurement.context.origin.y )) |
|
| 218 |
+ path.addLine(to: CGPoint(x: lastPointX.x * geometry.size.width, y: lastPointX.y * geometry.size.height ) ) |
|
| 219 |
+ path.addLine(to: CGPoint(x: firstPointX.x * geometry.size.width, y: firstPointX.y * geometry.size.height ) ) |
|
| 220 |
+ // MARK: Nu e nevoie. Fill inchide automat calea |
|
| 221 |
+ // path.closeSubpath() |
|
| 222 |
+ } |
|
| 223 |
+ } |
|
| 224 |
+ } |
|
| 225 |
+ |
|
| 226 |
+} |
|
@@ -0,0 +1,58 @@ |
||
| 1 |
+// |
|
| 2 |
+// MeasurementView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 13/04/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+struct MeasurementPointView: View {
|
|
| 12 |
+ |
|
| 13 |
+ var power: Measurements.Measurement.Point |
|
| 14 |
+ var voltage: Measurements.Measurement.Point |
|
| 15 |
+ var current: Measurements.Measurement.Point |
|
| 16 |
+ |
|
| 17 |
+ @State var showDetail: Bool = false |
|
| 18 |
+ |
|
| 19 |
+ var body: some View {
|
|
| 20 |
+ VStack {
|
|
| 21 |
+ HStack {
|
|
| 22 |
+ Image(systemName: "pencil.and.ellipsis.rectangle") |
|
| 23 |
+ .imageScale(.large) |
|
| 24 |
+ Text ("\(voltage.timestamp.format(as: "yyyy-MM-dd hh:mm:ss"))")
|
|
| 25 |
+ Spacer() |
|
| 26 |
+ Button(action: {
|
|
| 27 |
+ self.showDetail.toggle() |
|
| 28 |
+ }) {
|
|
| 29 |
+ Image(systemName: "chevron.right.circle") |
|
| 30 |
+ .imageScale(.large) |
|
| 31 |
+ .rotationEffect(.degrees(showDetail ? 90 : 0)) |
|
| 32 |
+ .animation(.easeInOut(duration: 0.25)) |
|
| 33 |
+ } |
|
| 34 |
+ } |
|
| 35 |
+ if showDetail {
|
|
| 36 |
+ VStack {
|
|
| 37 |
+ HStack {
|
|
| 38 |
+ Text("Power:")
|
|
| 39 |
+ Spacer() |
|
| 40 |
+ Text("\(power.value.format(fractionDigits: 4))")
|
|
| 41 |
+ } |
|
| 42 |
+ HStack {
|
|
| 43 |
+ Text("Voltage:")
|
|
| 44 |
+ Spacer() |
|
| 45 |
+ Text("\(voltage.value.format(fractionDigits: 4))")
|
|
| 46 |
+ } |
|
| 47 |
+ HStack {
|
|
| 48 |
+ Text("Current:")
|
|
| 49 |
+ Spacer() |
|
| 50 |
+ Text("\(current.value.format(fractionDigits: 4))")
|
|
| 51 |
+ } |
|
| 52 |
+ } |
|
| 53 |
+ .padding() |
|
| 54 |
+ } |
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ } |
|
| 58 |
+} |
|
@@ -0,0 +1,58 @@ |
||
| 1 |
+// |
|
| 2 |
+// MeasurementView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 13/04/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+struct MeasurementsView: View {
|
|
| 12 |
+ |
|
| 13 |
+ @EnvironmentObject private var measurements: Measurements |
|
| 14 |
+ |
|
| 15 |
+ @Binding var visibility: Bool |
|
| 16 |
+ |
|
| 17 |
+ var body: some View {
|
|
| 18 |
+ NavigationView {
|
|
| 19 |
+ VStack {
|
|
| 20 |
+ List {
|
|
| 21 |
+ ForEach (measurements.power.points) { point in
|
|
| 22 |
+ // MARK: Crapa la stergere daca lista incape in fereastra: Fatal error: Index out of range |
|
| 23 |
+ MeasurementPointView(power: point, voltage: self.measurements.voltage.points[point.id], current: self.measurements.current.points[point.id]) |
|
| 24 |
+ }.onDelete { (indexSet) in
|
|
| 25 |
+ for idx in indexSet {
|
|
| 26 |
+ self.measurements.remove(at: idx) |
|
| 27 |
+ } |
|
| 28 |
+ } |
|
| 29 |
+ } |
|
| 30 |
+ } |
|
| 31 |
+ .navigationBarItems(leading: HStack{
|
|
| 32 |
+ #if targetEnvironment(macCatalyst) |
|
| 33 |
+ Button(action: {self.visibility.toggle()}) {
|
|
| 34 |
+ Text("ⓧ")
|
|
| 35 |
+ .foregroundColor(.red) |
|
| 36 |
+ } |
|
| 37 |
+ #else |
|
| 38 |
+ Spacer() |
|
| 39 |
+ #endif |
|
| 40 |
+ }, |
|
| 41 |
+ trailing: HStack{
|
|
| 42 |
+ #if targetEnvironment(macCatalyst) |
|
| 43 |
+ EditButton() |
|
| 44 |
+ #endif |
|
| 45 |
+ Button(action: {
|
|
| 46 |
+ self.measurements.power.reset() |
|
| 47 |
+ self.measurements.voltage.reset() |
|
| 48 |
+ self.measurements.current.reset() |
|
| 49 |
+ }) {
|
|
| 50 |
+ Text("🗑")
|
|
| 51 |
+ .foregroundColor(.red) |
|
| 52 |
+ } |
|
| 53 |
+ }) |
|
| 54 |
+ .navigationBarTitle("Measurements", displayMode: .inline)
|
|
| 55 |
+ } |
|
| 56 |
+ .navigationViewStyle(StackNavigationViewStyle()) |
|
| 57 |
+ } |
|
| 58 |
+} |
|
@@ -0,0 +1,139 @@ |
||
| 1 |
+// |
|
| 2 |
+// SettingsView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 14/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+struct MeterSettingsView: View {
|
|
| 12 |
+ |
|
| 13 |
+ @EnvironmentObject private var meter: Meter |
|
| 14 |
+ |
|
| 15 |
+ @State private var editingName = false |
|
| 16 |
+ @State private var editingScreenTimeout = false |
|
| 17 |
+ @State private var editingScreenBrightness = false |
|
| 18 |
+ |
|
| 19 |
+ var body: some View {
|
|
| 20 |
+ ScrollView {
|
|
| 21 |
+ VStack (spacing: 10) {
|
|
| 22 |
+ // MARK: Name |
|
| 23 |
+ VStack {
|
|
| 24 |
+ HStack {
|
|
| 25 |
+ Text ("Name").fontWeight(.semibold)
|
|
| 26 |
+ Spacer() |
|
| 27 |
+ if !editingName {
|
|
| 28 |
+ Text(meter.name) |
|
| 29 |
+ } |
|
| 30 |
+ ChevronView( rotate: $editingName ) |
|
| 31 |
+ } |
|
| 32 |
+ if editingName {
|
|
| 33 |
+ EditNameView(editingName: $editingName, newName: meter.name) |
|
| 34 |
+ } |
|
| 35 |
+ } |
|
| 36 |
+ .padding() |
|
| 37 |
+ .background(RoundedRectangle(cornerRadius: 15).foregroundColor(.secondary).opacity(0.1)) |
|
| 38 |
+ if meter.operationalState == .dataIsAvailable {
|
|
| 39 |
+ // MARK: Screen Timeout |
|
| 40 |
+ // Ar trebui separat enabled/disabled de valorile in minute eventual stocata valoarea in iCloud la dezactivare pentru restaurare |
|
| 41 |
+ VStack{
|
|
| 42 |
+ HStack {
|
|
| 43 |
+ Text ("Screen Timeout").fontWeight(.semibold)
|
|
| 44 |
+ Spacer() |
|
| 45 |
+ if !editingScreenTimeout {
|
|
| 46 |
+ Text ( meter.screenTimeout != 0 ? "\(meter.screenTimeout) Minutes" : "Off" ) |
|
| 47 |
+ } |
|
| 48 |
+ ChevronView( rotate: $editingScreenTimeout ) |
|
| 49 |
+ } |
|
| 50 |
+ if editingScreenTimeout {
|
|
| 51 |
+ EditScreenTimeoutView() |
|
| 52 |
+ } |
|
| 53 |
+ } |
|
| 54 |
+ .padding() |
|
| 55 |
+ .background(RoundedRectangle(cornerRadius: 15).foregroundColor(.secondary).opacity(0.1)) |
|
| 56 |
+ // MARK: Screen Brightness |
|
| 57 |
+ VStack{
|
|
| 58 |
+ HStack {
|
|
| 59 |
+ Text ("Screen Brightness").fontWeight(.semibold)
|
|
| 60 |
+ Spacer() |
|
| 61 |
+ if !editingScreenBrightness {
|
|
| 62 |
+ Text ( "\(meter.screenBrightness)" ) |
|
| 63 |
+ } |
|
| 64 |
+ ChevronView( rotate: $editingScreenBrightness ) |
|
| 65 |
+ } |
|
| 66 |
+ if editingScreenBrightness {
|
|
| 67 |
+ EditScreenBrightnessView() |
|
| 68 |
+ } |
|
| 69 |
+ } |
|
| 70 |
+ .padding() |
|
| 71 |
+ .background(RoundedRectangle(cornerRadius: 15).foregroundColor(.secondary).opacity(0.1)) |
|
| 72 |
+ } |
|
| 73 |
+ } |
|
| 74 |
+ } |
|
| 75 |
+ .padding() |
|
| 76 |
+ .navigationBarTitle("Meter Settings")
|
|
| 77 |
+ .navigationBarItems( trailing: RSSIView( RSSI: meter.btSerial.RSSI ).frame( width: 24 ) ) |
|
| 78 |
+ } |
|
| 79 |
+} |
|
| 80 |
+ |
|
| 81 |
+struct EditNameView: View {
|
|
| 82 |
+ |
|
| 83 |
+ @EnvironmentObject private var meter: Meter |
|
| 84 |
+ |
|
| 85 |
+ @Binding var editingName: Bool |
|
| 86 |
+ @State var newName: String |
|
| 87 |
+ |
|
| 88 |
+ var body: some View {
|
|
| 89 |
+ TextField("Name", text: self.$newName, onCommit: {
|
|
| 90 |
+ self.meter.name = self.newName |
|
| 91 |
+ self.editingName = false |
|
| 92 |
+ }) |
|
| 93 |
+ .textFieldStyle(RoundedBorderTextFieldStyle()) |
|
| 94 |
+ .lineLimit(1) |
|
| 95 |
+ .disableAutocorrection(true) |
|
| 96 |
+ .multilineTextAlignment(.center) |
|
| 97 |
+ .padding(.horizontal) |
|
| 98 |
+ } |
|
| 99 |
+} |
|
| 100 |
+ |
|
| 101 |
+struct EditScreenTimeoutView: View {
|
|
| 102 |
+ |
|
| 103 |
+ @EnvironmentObject private var meter: Meter |
|
| 104 |
+ |
|
| 105 |
+ var body: some View {
|
|
| 106 |
+ Picker("", selection: self.$meter.screenTimeout ) {
|
|
| 107 |
+ Text("1").tag(1)
|
|
| 108 |
+ Text("2").tag(2)
|
|
| 109 |
+ Text("3").tag(3)
|
|
| 110 |
+ Text("4").tag(4)
|
|
| 111 |
+ Text("5").tag(5)
|
|
| 112 |
+ Text("6").tag(6)
|
|
| 113 |
+ Text("7").tag(7)
|
|
| 114 |
+ Text("8").tag(8)
|
|
| 115 |
+ Text("9").tag(9)
|
|
| 116 |
+ Text("Off").tag(0)
|
|
| 117 |
+ } |
|
| 118 |
+ .pickerStyle( SegmentedPickerStyle() ) |
|
| 119 |
+ .padding(.horizontal) |
|
| 120 |
+ } |
|
| 121 |
+} |
|
| 122 |
+ |
|
| 123 |
+struct EditScreenBrightnessView: View {
|
|
| 124 |
+ |
|
| 125 |
+ @EnvironmentObject private var meter: Meter |
|
| 126 |
+ |
|
| 127 |
+ var body: some View {
|
|
| 128 |
+ Picker("", selection: self.$meter.screenBrightness ) {
|
|
| 129 |
+ Text("0").tag(0)
|
|
| 130 |
+ Text("1").tag(1)
|
|
| 131 |
+ Text("2").tag(2)
|
|
| 132 |
+ Text("3").tag(3)
|
|
| 133 |
+ Text("4").tag(4)
|
|
| 134 |
+ Text("5").tag(5)
|
|
| 135 |
+ } |
|
| 136 |
+ .pickerStyle( SegmentedPickerStyle() ) |
|
| 137 |
+ .padding(.horizontal) |
|
| 138 |
+ } |
|
| 139 |
+} |
|
@@ -0,0 +1,123 @@ |
||
| 1 |
+// |
|
| 2 |
+// MeterView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 04/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+// MARK: Parent frame https://stackoverflow.com/questions/56832865/how-to-access-parents-frame-in-swiftui |
|
| 9 |
+ |
|
| 10 |
+import SwiftUI |
|
| 11 |
+import CoreBluetooth |
|
| 12 |
+ |
|
| 13 |
+struct MeterView: View {
|
|
| 14 |
+ |
|
| 15 |
+ @EnvironmentObject private var meter: Meter |
|
| 16 |
+ |
|
| 17 |
+ @State var dataGroupsViewVisibility: Bool = false |
|
| 18 |
+ @State var measurementsViewVisibility: Bool = false |
|
| 19 |
+ private var myBounds: CGRect { UIScreen.main.bounds }
|
|
| 20 |
+ |
|
| 21 |
+ var body: some View {
|
|
| 22 |
+ ScrollView {
|
|
| 23 |
+ VStack (alignment: .center, spacing: 10) {
|
|
| 24 |
+ // MARK: Name |
|
| 25 |
+ VStack {
|
|
| 26 |
+ Text("Meter")
|
|
| 27 |
+ .font(.headline) |
|
| 28 |
+ Text("\(meter.name)")
|
|
| 29 |
+ } |
|
| 30 |
+ .padding() |
|
| 31 |
+ .background(RoundedRectangle(cornerRadius: 20).foregroundColor(meter.color).opacity(0.1)) |
|
| 32 |
+ // MARK: Mac |
|
| 33 |
+ VStack {
|
|
| 34 |
+ Text("MAC")
|
|
| 35 |
+ .font(.headline) |
|
| 36 |
+ Text("\(meter.btSerial.macAddress.description)")
|
|
| 37 |
+ } |
|
| 38 |
+ .padding() |
|
| 39 |
+ .background(RoundedRectangle(cornerRadius: 20).foregroundColor(.secondary).opacity(0.1)) |
|
| 40 |
+ } |
|
| 41 |
+ // MARK: Connect/Disconnect |
|
| 42 |
+ connectionControlButton() |
|
| 43 |
+ // MARK: Show Data |
|
| 44 |
+ if ( meter.operationalState == .dataIsAvailable) {
|
|
| 45 |
+ Text("Model: \(meter.modelString) - (\(meter.modelNumber))")
|
|
| 46 |
+ HStack {
|
|
| 47 |
+ Button(action: {self.dataGroupsViewVisibility.toggle()}) {
|
|
| 48 |
+ VStack {
|
|
| 49 |
+ Image(systemName: "map") |
|
| 50 |
+ .sheet(isPresented: self.$dataGroupsViewVisibility) {
|
|
| 51 |
+ DataGroupsView(visibility: self.$dataGroupsViewVisibility) |
|
| 52 |
+ .environmentObject(self.meter) |
|
| 53 |
+ } |
|
| 54 |
+ Text("ceva")
|
|
| 55 |
+ } |
|
| 56 |
+ } |
|
| 57 |
+ Button(action: {self.measurementsViewVisibility.toggle()}) {
|
|
| 58 |
+ VStack {
|
|
| 59 |
+ Image(systemName: "recordingtape") |
|
| 60 |
+ .sheet(isPresented: self.$measurementsViewVisibility) {
|
|
| 61 |
+ MeasurementsView(visibility: self.$measurementsViewVisibility) |
|
| 62 |
+ .environmentObject(self.meter.measurements) |
|
| 63 |
+ } |
|
| 64 |
+ Text("altceva")
|
|
| 65 |
+ } |
|
| 66 |
+ } |
|
| 67 |
+ } |
|
| 68 |
+ if self.meter.measurements.power.context.isValid {
|
|
| 69 |
+ MeasurementChartView() |
|
| 70 |
+ .environmentObject(self.meter.measurements) |
|
| 71 |
+ .frame(minHeight: myBounds.height/3) |
|
| 72 |
+ } |
|
| 73 |
+ ControlView() |
|
| 74 |
+ LiveView() |
|
| 75 |
+ } |
|
| 76 |
+ } |
|
| 77 |
+ //.frame(minWidth: 0, maxWidth: .greatestFiniteMagnitude, minHeight: 0, maxHeight: .greatestFiniteMagnitude) |
|
| 78 |
+ .navigationBarTitle("Meter")
|
|
| 79 |
+ .navigationBarItems(trailing: HStack (spacing: 0) {
|
|
| 80 |
+ if meter.operationalState > .notPresent {
|
|
| 81 |
+ RSSIView(RSSI: meter.btSerial.RSSI) |
|
| 82 |
+ .frame(width: 24) |
|
| 83 |
+ .padding(.vertical) |
|
| 84 |
+ } |
|
| 85 |
+ NavigationLink(destination: MeterSettingsView().environmentObject(meter)) {
|
|
| 86 |
+ Image( systemName: "gear" ) |
|
| 87 |
+ //.imageScale(.large) |
|
| 88 |
+ .padding(.vertical) |
|
| 89 |
+ .padding(.leading) |
|
| 90 |
+ //.background(RoundedRectangle(cornerRadius: 20)).opacity(0.05) |
|
| 91 |
+ } |
|
| 92 |
+ }) |
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ fileprivate func connectionControlButton() -> some View {
|
|
| 96 |
+ /* |
|
| 97 |
+ MARK: De adaugat si celelalte situatii |
|
| 98 |
+ case peripheralNotConnected |
|
| 99 |
+ case peripheralConnectionPending |
|
| 100 |
+ case peripheralConnected |
|
| 101 |
+ case ready |
|
| 102 |
+ */ |
|
| 103 |
+ let buttonColor = meter.operationalState > .peripheralNotConnected ? Color.red : Color.green |
|
| 104 |
+ return Group {
|
|
| 105 |
+ if meter.operationalState == .notPresent {
|
|
| 106 |
+ Text("Not found at this time.").foregroundColor(.red)
|
|
| 107 |
+ } else {
|
|
| 108 |
+ |
|
| 109 |
+ HStack {
|
|
| 110 |
+ if meter.operationalState < .peripheralConnectionPending {
|
|
| 111 |
+ Button (action: { self.meter.connect() } ) { Text("Connect") }
|
|
| 112 |
+ } else {
|
|
| 113 |
+ Button (action: { self.meter.disconnect() } ) { Text("Disconnect") }
|
|
| 114 |
+ } |
|
| 115 |
+ } |
|
| 116 |
+ .padding() |
|
| 117 |
+ .background(RoundedRectangle(cornerRadius: 20).foregroundColor(buttonColor).opacity(0.1)) |
|
| 118 |
+ .frame(maxWidth: .greatestFiniteMagnitude) |
|
| 119 |
+ } |
|
| 120 |
+ } |
|
| 121 |
+ |
|
| 122 |
+ } |
|
| 123 |
+} |
|
@@ -0,0 +1,81 @@ |
||
| 1 |
+// |
|
| 2 |
+// SwiftUIView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 14/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+ |
|
| 12 |
+struct RSSIView: View {
|
|
| 13 |
+ |
|
| 14 |
+ var RSSI: Int |
|
| 15 |
+ |
|
| 16 |
+ var body: some View {
|
|
| 17 |
+ GeometryReader { box in
|
|
| 18 |
+ HStack (alignment: VerticalAlignment.bottom, spacing: 3) {
|
|
| 19 |
+ ZStack {
|
|
| 20 |
+ Rectangle().stroke() |
|
| 21 |
+ if self.RSSI >= -110 {
|
|
| 22 |
+ Rectangle() |
|
| 23 |
+ .foregroundColor(self.clr()) |
|
| 24 |
+ } |
|
| 25 |
+ } |
|
| 26 |
+ .frame(height: box.size.height * 0.2) |
|
| 27 |
+ ZStack {
|
|
| 28 |
+ Rectangle().stroke() |
|
| 29 |
+ if self.RSSI >= -100 {
|
|
| 30 |
+ Rectangle() |
|
| 31 |
+ .foregroundColor(self.clr()) |
|
| 32 |
+ } |
|
| 33 |
+ } |
|
| 34 |
+ .frame(height: box.size.height * 0.4) |
|
| 35 |
+ ZStack {
|
|
| 36 |
+ Rectangle().stroke() |
|
| 37 |
+ if self.RSSI >= -80 {
|
|
| 38 |
+ Rectangle() |
|
| 39 |
+ .foregroundColor(self.clr()) |
|
| 40 |
+ } |
|
| 41 |
+ } |
|
| 42 |
+ .frame(height: box.size.height * 0.6) |
|
| 43 |
+ ZStack {
|
|
| 44 |
+ Rectangle().stroke() |
|
| 45 |
+ if self.RSSI >= -60 {
|
|
| 46 |
+ Rectangle() |
|
| 47 |
+ .foregroundColor(self.clr()) |
|
| 48 |
+ } |
|
| 49 |
+ } |
|
| 50 |
+ .frame(height: box.size.height * 0.8) |
|
| 51 |
+ ZStack {
|
|
| 52 |
+ Rectangle().stroke() |
|
| 53 |
+ if self.RSSI >= -50 {
|
|
| 54 |
+ Rectangle() |
|
| 55 |
+ .foregroundColor(self.clr()) |
|
| 56 |
+ } |
|
| 57 |
+ } |
|
| 58 |
+ .frame(height: box.size.height/1) |
|
| 59 |
+ } |
|
| 60 |
+ } |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ private func clr() -> Color {
|
|
| 64 |
+ switch RSSI {
|
|
| 65 |
+ case let x where x < -100: |
|
| 66 |
+ return .red |
|
| 67 |
+ case let x where x < -80: |
|
| 68 |
+ return .orange |
|
| 69 |
+ default: |
|
| 70 |
+ return .green |
|
| 71 |
+ } |
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+} |
|
| 75 |
+ |
|
| 76 |
+ |
|
| 77 |
+struct SwiftUIView_Previews: PreviewProvider {
|
|
| 78 |
+ static var previews: some View {
|
|
| 79 |
+ RSSIView(RSSI: -80).frame(width: 64, height: 64, alignment: .center) |
|
| 80 |
+ } |
|
| 81 |
+} |
|
@@ -0,0 +1,46 @@ |
||
| 1 |
+// |
|
| 2 |
+// RecordingView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 09/03/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+struct RecordingView: View {
|
|
| 12 |
+ |
|
| 13 |
+ @EnvironmentObject private var usbMeter: Meter |
|
| 14 |
+ |
|
| 15 |
+ var body: some View {
|
|
| 16 |
+ VStack {
|
|
| 17 |
+ Text ("Recorded Data")
|
|
| 18 |
+ Text ("REC")
|
|
| 19 |
+ .foregroundColor(usbMeter.recording ? .red : .green) |
|
| 20 |
+ HStack {
|
|
| 21 |
+ VStack {
|
|
| 22 |
+ Text ("Capacity")
|
|
| 23 |
+ Text ("Energy")
|
|
| 24 |
+ Text ("Duration")
|
|
| 25 |
+ Text ("Treshold")
|
|
| 26 |
+ } |
|
| 27 |
+ VStack {
|
|
| 28 |
+ Text("\(usbMeter.recordedAH.format(decimalDigits: 3)) Ah")
|
|
| 29 |
+ Text("\(usbMeter.recordedWH.format(decimalDigits: 3)) Wh")
|
|
| 30 |
+ Text("\(usbMeter.recordingDuration) Sec")
|
|
| 31 |
+ HStack {
|
|
| 32 |
+ Slider(value: $usbMeter.recordingTreshold, in: 0...0.30, step: 0.01) |
|
| 33 |
+ //.frame(width: 300) |
|
| 34 |
+ Text("\(usbMeter.recordingTreshold.format(decimalDigits: 2)) A")
|
|
| 35 |
+ }.padding() |
|
| 36 |
+ }.padding() |
|
| 37 |
+ } |
|
| 38 |
+ } |
|
| 39 |
+ } |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+struct RecordingView_Previews: PreviewProvider {
|
|
| 43 |
+ static var previews: some View {
|
|
| 44 |
+ RecordingView() |
|
| 45 |
+ } |
|
| 46 |
+} |
|
@@ -0,0 +1,22 @@ |
||
| 1 |
+// |
|
| 2 |
+// MeterComunicationView.swift |
|
| 3 |
+// USB Meter |
|
| 4 |
+// |
|
| 5 |
+// Created by Bogdan Timofte on 05/05/2020. |
|
| 6 |
+// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
+// |
|
| 8 |
+ |
|
| 9 |
+import SwiftUI |
|
| 10 |
+ |
|
| 11 |
+struct MeterRowView: View {
|
|
| 12 |
+ |
|
| 13 |
+ @EnvironmentObject private var meter: Meter |
|
| 14 |
+ |
|
| 15 |
+ var body: some View {
|
|
| 16 |
+ HStack {
|
|
| 17 |
+ Image( systemName: "antenna.radiowaves.left.and.right" ).foregroundColor(Meter.operationalColor(for: meter.operationalState) ) |
|
| 18 |
+ Text("\(meter.name)")
|
|
| 19 |
+// Spacer() |
|
| 20 |
+ } |
|
| 21 |
+ } |
|
| 22 |
+} |
|