Showing 52 changed files with 3986 additions and 0 deletions
+10 -0
.gitignore
@@ -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/
+11 -0
Readme.rtf
@@ -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}}}
+619 -0
USB Meter.xcodeproj/project.pbxproj
@@ -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
+}
+7 -0
USB Meter.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -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>
+8 -0
USB Meter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -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>
+16 -0
USB Meter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -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
+}
+99 -0
USB Meter/AppDelegate.swift
@@ -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
+
+98 -0
USB Meter/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -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
+}
+6 -0
USB Meter/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
1
+{
2
+  "info" : {
3
+    "version" : 1,
4
+    "author" : "xcode"
5
+  }
6
+}
+25 -0
USB Meter/Base.lproj/LaunchScreen.storyboard
@@ -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>
+36 -0
USB Meter/DataTypes/MacAdress.swift
@@ -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
+}
+137 -0
USB Meter/Extensions/CBManagerState.swift
@@ -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
+}
+28 -0
USB Meter/Extensions/CGPoint.swift
@@ -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
+}
+58 -0
USB Meter/Extensions/Data.swift
@@ -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
+}
+21 -0
USB Meter/Extensions/Date.swift
@@ -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
+}
+19 -0
USB Meter/Extensions/Decimal.swift
@@ -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
+}
+49 -0
USB Meter/Extensions/Double.swift
@@ -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
+}
+19 -0
USB Meter/Extensions/Float.swift
@@ -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
+}
+17 -0
USB Meter/Extensions/Path.swift
@@ -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
+}
+65 -0
USB Meter/Extensions/UIView.swift
@@ -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
+
+33 -0
USB Meter/Extensions/View.swift
@@ -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
+*/
+83 -0
USB Meter/Info.plist
@@ -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>
+65 -0
USB Meter/Model/AppData.swift
@@ -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
+}
+176 -0
USB Meter/Model/BluetoothManager.swift
@@ -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
+46 -0
USB Meter/Model/BluetoothRadio.swift
@@ -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
+}
+293 -0
USB Meter/Model/BluetoothSerial.swift
@@ -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
+
+8 -0
USB Meter/Model/CKModel.xcdatamodeld/.xccurrentversion
@@ -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>
+7 -0
USB Meter/Model/CKModel.xcdatamodeld/USB_Meter.xcdatamodel/contents
@@ -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>
+104 -0
USB Meter/Model/ChartContext.swift
@@ -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
+}
+93 -0
USB Meter/Model/Measurements.swift
@@ -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
+}
+486 -0
USB Meter/Model/Meter.swift
@@ -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
+}
+6 -0
USB Meter/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
1
+{
2
+  "info" : {
3
+    "version" : 1,
4
+    "author" : "xcode"
5
+  }
6
+}
+74 -0
USB Meter/SceneDelegate.swift
@@ -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
+
+29 -0
USB Meter/Templates/ICloudDefault.swift
@@ -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
+}
+43 -0
USB Meter/Templates/UserDefault.swift
@@ -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
+}
+14 -0
USB Meter/USB Meter.entitlements
@@ -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>
+32 -0
USB Meter/Views/BorderView.swift
@@ -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
+}
+49 -0
USB Meter/Views/ContentView.swift
@@ -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
+
+20 -0
USB Meter/Views/DeviceHelpView.swift
@@ -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
+}
+27 -0
USB Meter/Views/Meter/ChevronView.swift
@@ -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
+
+35 -0
USB Meter/Views/Meter/ControlView.swift
@@ -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
+}
+46 -0
USB Meter/Views/Meter/Data Groups/DataGroupRowView.swift
@@ -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
+}
+58 -0
USB Meter/Views/Meter/Data Groups/DataGroupsView.swift
@@ -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
+}
+58 -0
USB Meter/Views/Meter/LiveView.swift
@@ -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
+}
+226 -0
USB Meter/Views/Meter/Measurements/Chart/MeasurementChartView.swift
@@ -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
+}
+58 -0
USB Meter/Views/Meter/Measurements/MeasurementPointView.swift
@@ -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
+}
+58 -0
USB Meter/Views/Meter/Measurements/MeasurementsView.swift
@@ -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
+}
+139 -0
USB Meter/Views/Meter/MeterSettingsView.swift
@@ -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
+}
+123 -0
USB Meter/Views/Meter/MeterView.swift
@@ -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
+}
+81 -0
USB Meter/Views/Meter/RSSIView.swift
@@ -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
+}
+46 -0
USB Meter/Views/Meter/RecordingView.swift
@@ -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
+}
+22 -0
USB Meter/Views/MeterRowView.swift
@@ -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
+}