SpecLog 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- SpecLog/SpecLog.py +277 -0
- SpecLog/SpecLogger.py +128 -0
- SpecLog/__init__.py +5 -0
- SpecLog/config/__init__.py +0 -0
- SpecLog/config/config.py +93 -0
- SpecLog/debugLog.py +36 -0
- SpecLog/device.py +156 -0
- SpecLog/loggerConfig.py +190 -0
- SpecLog/monitor.py +1023 -0
- SpecLog/ui/__init__.py +0 -0
- SpecLog/ui/plotting.py +297 -0
- SpecLog/ui/plotting.ui +557 -0
- SpecLog/version.py +1 -0
- speclog-0.0.1.dist-info/METADATA +19 -0
- speclog-0.0.1.dist-info/RECORD +18 -0
- speclog-0.0.1.dist-info/WHEEL +5 -0
- speclog-0.0.1.dist-info/entry_points.txt +7 -0
- speclog-0.0.1.dist-info/top_level.txt +1 -0
SpecLog/monitor.py
ADDED
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Monitor.py: plotting data in real-time or static
|
|
3
|
+
|
|
4
|
+
1. Read log files
|
|
5
|
+
2. Plot live or static data
|
|
6
|
+
3. Show warning information
|
|
7
|
+
|
|
8
|
+
It use PySide 6, there are some terms you need to know before modifying this script
|
|
9
|
+
|
|
10
|
+
1. 'name': the identification of a curve, and the key word to call the curve line and item. It can be the alias if the alias presents
|
|
11
|
+
2. 'item': the curve items, e.g Legend class from PySide 6
|
|
12
|
+
3. 'line': the data line for plotting, it is a class from PySide 6
|
|
13
|
+
|
|
14
|
+
Author: Yen-Chun Huang
|
|
15
|
+
|
|
16
|
+
Company: Bridge 12 Technologies, Inc
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import csv
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
import numpy as _np
|
|
23
|
+
from PySide6.QtWidgets import QApplication, QFileDialog, QMainWindow, QVBoxLayout, QCheckBox
|
|
24
|
+
from PySide6 import QtCore
|
|
25
|
+
import pyqtgraph as pg
|
|
26
|
+
from .ui.plotting import Ui_MainWindow
|
|
27
|
+
from .loggerConfig import *
|
|
28
|
+
from .debugLog import *
|
|
29
|
+
|
|
30
|
+
red = "QCheckBox::indicator {\nwidth:10px;\nheight:10px;\nborder-radius:7px;\n}\n\nQCheckBox::indicator:unchecked {\nbackground-color:red;\nborder:2px solid white;\n}\n"
|
|
31
|
+
green = "QCheckBox::indiator {\nwidth:10px;\nheight:10px;\nborder-radius:7px;\n}\n\nQCheckBox::indicator:unchecked {\nbackground-color:green;\nborder:2pcx solid white;\n}\n"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MainWindow(QMainWindow, Ui_MainWindow):
|
|
35
|
+
def __init__(self, config_file: str = None):
|
|
36
|
+
super().__init__()
|
|
37
|
+
self.setupUi(self)
|
|
38
|
+
|
|
39
|
+
# configuration file
|
|
40
|
+
config = loggerConfig(config_file)
|
|
41
|
+
self.settings = config.settings
|
|
42
|
+
self.limit_hours = int(self.settings['limit_hours'])
|
|
43
|
+
self.device_config = config.devices
|
|
44
|
+
self.file_dir = self.settings["log_folder_location"] + "/LOG/"
|
|
45
|
+
self.commands = config.commands
|
|
46
|
+
self.getAlias()
|
|
47
|
+
self.getCommandInfoByName()
|
|
48
|
+
self.status_string = ""
|
|
49
|
+
|
|
50
|
+
# debug log
|
|
51
|
+
self.debugLogger = debugLog(config_file).logger
|
|
52
|
+
|
|
53
|
+
# get files from directionary
|
|
54
|
+
self.getFiles()
|
|
55
|
+
self.getData()
|
|
56
|
+
self.setWarningLevelByName()
|
|
57
|
+
self.getPenByName()
|
|
58
|
+
self.getLine() # initialize plot
|
|
59
|
+
|
|
60
|
+
# Shown items
|
|
61
|
+
if not self.loadDisplaySettings():
|
|
62
|
+
self.saveDisplaySettings() # initial saving
|
|
63
|
+
|
|
64
|
+
self.hiddenListWidget.addItems(self.hidden_list)
|
|
65
|
+
self.shownListWidget.addItems(
|
|
66
|
+
self.shown_list
|
|
67
|
+
) # self.shown_list is used for displaying data
|
|
68
|
+
|
|
69
|
+
# Status Indicator
|
|
70
|
+
self.setLEDIndicator()
|
|
71
|
+
self.setStatus()
|
|
72
|
+
|
|
73
|
+
# Buttons and Menu settings
|
|
74
|
+
self.hiddenToShown.clicked.connect(
|
|
75
|
+
self.showItems
|
|
76
|
+
) # button to move item from hidden widget to shown widget
|
|
77
|
+
self.shownToHidden.clicked.connect(
|
|
78
|
+
self.hideItems
|
|
79
|
+
) # button to move item from shown widget to hidden widget
|
|
80
|
+
self.clearWarningText.clicked.connect(
|
|
81
|
+
self.clearWarning
|
|
82
|
+
) # button to clear warning message
|
|
83
|
+
self.setPlotType()
|
|
84
|
+
|
|
85
|
+
self.timer = QtCore.QTimer()
|
|
86
|
+
self.timer.setInterval(300)
|
|
87
|
+
self.timer.timeout.connect(self.setStatus)
|
|
88
|
+
self.timer.timeout.connect(self.printWarning)
|
|
89
|
+
self.timer.timeout.connect(self.updateFiles)
|
|
90
|
+
self.timer.timeout.connect(self.updateData)
|
|
91
|
+
self.timer.timeout.connect(self.plot)
|
|
92
|
+
self.timer.start()
|
|
93
|
+
|
|
94
|
+
### ======================================================= Log Data Files =======================================================
|
|
95
|
+
def getFiles(self):
|
|
96
|
+
"""
|
|
97
|
+
Get files from directory
|
|
98
|
+
"""
|
|
99
|
+
self.all_file_list = [
|
|
100
|
+
file for file in os.listdir(self.file_dir) if "log_" in file and '.csv' in file
|
|
101
|
+
]
|
|
102
|
+
self.file_list = self.all_file_list[-3:]
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
def updateFiles(self):
|
|
106
|
+
"""
|
|
107
|
+
update files from directory
|
|
108
|
+
"""
|
|
109
|
+
file = list(os.listdir(self.file_dir))[-1]
|
|
110
|
+
if file != self.all_file_list[-1]:
|
|
111
|
+
# check if file format is correct
|
|
112
|
+
if "log_" in file and '.csv' in file:
|
|
113
|
+
self.all_file_list.append(file)
|
|
114
|
+
self.file_list = self.file_list[1:]
|
|
115
|
+
self.file_list.append(file)
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
### ======================================================= Data Dictionary =======================================================
|
|
122
|
+
def getData(self):
|
|
123
|
+
"""
|
|
124
|
+
Get data from logger files when monitor starts. It is not used for updating data reading
|
|
125
|
+
"""
|
|
126
|
+
self.all_data_by_name = {"Date": [], "Time": [], "Seconds": []}
|
|
127
|
+
window_length = self.windowLength.value()
|
|
128
|
+
|
|
129
|
+
if not self.file_list:
|
|
130
|
+
self.debugLogger.error("Logged Data Not Found")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
# When the monitor starts or a new file presents
|
|
134
|
+
for file in self.file_list:
|
|
135
|
+
f = open(self.file_dir + file, "r")
|
|
136
|
+
names = f.readline().strip("\n").split(",")
|
|
137
|
+
print(names)
|
|
138
|
+
if names[0]:
|
|
139
|
+
names = self.convertNames(
|
|
140
|
+
names
|
|
141
|
+
) # at this point, the name is used locally
|
|
142
|
+
for data in csv.reader(f, delimiter=","):
|
|
143
|
+
self.all_data_by_name = self.updateDataToDict(
|
|
144
|
+
names, data, self.all_data_by_name, self.limit_hours
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if file != self.file_list[-1]: # close the file
|
|
148
|
+
f.close()
|
|
149
|
+
else: # leave the current file open for further reading
|
|
150
|
+
# after reading all data, put the local names list to global
|
|
151
|
+
self.current_file = file
|
|
152
|
+
self.f = f
|
|
153
|
+
self.names = names
|
|
154
|
+
self.window_length = window_length
|
|
155
|
+
|
|
156
|
+
# for initial data dictionary
|
|
157
|
+
self.all_names = [
|
|
158
|
+
name
|
|
159
|
+
for name in self.all_data_by_name.keys()
|
|
160
|
+
if name not in ["Date", "Time", "Seconds"]
|
|
161
|
+
] # get list of data name except Date, Time and Seconds
|
|
162
|
+
self.all_x = self.all_data_by_name["Seconds"]
|
|
163
|
+
self.data_by_name = {
|
|
164
|
+
name: val[-1 * window_length :]
|
|
165
|
+
for name, val in self.all_data_by_name.items()
|
|
166
|
+
if name not in ["Date", "Time", "Seconds"]
|
|
167
|
+
}
|
|
168
|
+
self.x = self.all_x[-1 * window_length :]
|
|
169
|
+
self.x_ticks = self.getXTicks(
|
|
170
|
+
self.x, self.all_data_by_name["Time"][-1 * window_length :]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def updateData(self):
|
|
174
|
+
"""
|
|
175
|
+
Update data when new line appears or new file presents
|
|
176
|
+
|
|
177
|
+
This is the data checking processing and used for live data
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
if self.current_file != self.file_list[-1]:
|
|
181
|
+
self.f.close() # when new file exist, close the previous file
|
|
182
|
+
self.current_file = self.file_list[-1]
|
|
183
|
+
self.f = open(self.file_dir + self.current_file, "r")
|
|
184
|
+
self.names = self.f.readline().strip("\n").split(",")
|
|
185
|
+
self.names = self.convertNames(self.names) # convert names list from alias
|
|
186
|
+
|
|
187
|
+
self.line = self.f.readline().strip("\n")
|
|
188
|
+
self.latest_data = None
|
|
189
|
+
if self.line:
|
|
190
|
+
self.all_data_by_name = self.updateDataToDict(
|
|
191
|
+
self.names, self.line.strip("\n").split(",") , self.all_data_by_name, self.limit_hours
|
|
192
|
+
)
|
|
193
|
+
# self.all_x doesn't need to be updated because it is the reference to self.all_data_by_name['Seconds']
|
|
194
|
+
|
|
195
|
+
if self.plot_type:
|
|
196
|
+
self.livePlot()
|
|
197
|
+
|
|
198
|
+
else:
|
|
199
|
+
self.staticPlot()
|
|
200
|
+
|
|
201
|
+
def livePlot(self):
|
|
202
|
+
"""
|
|
203
|
+
Choose to plot data in live
|
|
204
|
+
it is always updating
|
|
205
|
+
|
|
206
|
+
Modification Suggestions: might add a condition: '# if self.window_length != window_length or self.line or [change_from_static_to_live]:'
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
window_length = self.windowLength.value()
|
|
210
|
+
if self.window_length != window_length:
|
|
211
|
+
self.saveDisplaySettings() # update the window length
|
|
212
|
+
self.window_length = window_length
|
|
213
|
+
self.data_by_name = {
|
|
214
|
+
name: val[-1 * window_length :]
|
|
215
|
+
for name, val in self.all_data_by_name.items()
|
|
216
|
+
if name not in ["Date", "Time"]
|
|
217
|
+
}
|
|
218
|
+
self.x = self.all_x[-1 * window_length :]
|
|
219
|
+
self.x_ticks = self.getXTicks(
|
|
220
|
+
self.x, self.all_data_by_name["Time"][-1 * window_length :]
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return self.plot_type
|
|
224
|
+
|
|
225
|
+
def staticPlot(self):
|
|
226
|
+
if self.static_update_request:
|
|
227
|
+
if self.selected_data_by_date:
|
|
228
|
+
if (
|
|
229
|
+
self.time_is_valid
|
|
230
|
+
): # only update plot when time is valid, 'OK' button is pressed, and file is not selected
|
|
231
|
+
index = self.static_index
|
|
232
|
+
self.data_by_name = {
|
|
233
|
+
name: val[index[0] : index[1]]
|
|
234
|
+
for name, val in self.all_data_by_name.items()
|
|
235
|
+
if name not in ["Date", "Time"]
|
|
236
|
+
}
|
|
237
|
+
self.x = self.all_x[index[0] : index[1]]
|
|
238
|
+
self.x_ticks = self.getXTicks(
|
|
239
|
+
self.x, self.all_data_by_name["Time"][index[0] : index[1]]
|
|
240
|
+
)
|
|
241
|
+
self.static_update_request = (
|
|
242
|
+
False # just update the static figure once
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
elif (
|
|
246
|
+
self.selected_data_by_file
|
|
247
|
+
): # only update when time when 'OK' button is pressed and file is selected
|
|
248
|
+
self.data_by_name = {
|
|
249
|
+
name: val
|
|
250
|
+
for name, val in self.temp_data_by_name.items()
|
|
251
|
+
if name not in ["Date", "Time"]
|
|
252
|
+
}
|
|
253
|
+
self.x = self.temp_data_by_name["Seconds"]
|
|
254
|
+
self.x_ticks = self.getXTicks(self.x, self.temp_data_by_name["Time"])
|
|
255
|
+
self.static_update_request = False # just update the static figure once
|
|
256
|
+
|
|
257
|
+
return self.plot_type
|
|
258
|
+
|
|
259
|
+
def updateDataToDict(self, names: list, data, d: dict, hours: int = 0):
|
|
260
|
+
"""
|
|
261
|
+
Add data to target dictionary based on name
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
names (list): list of key (name) to add to target dictionary
|
|
265
|
+
data: csv read data
|
|
266
|
+
d (dict): target dictionary to add data
|
|
267
|
+
hours (int): the limited hours saved in this dictionary, greater than 0 means no hour limit
|
|
268
|
+
|
|
269
|
+
Return:
|
|
270
|
+
d (dict): target dictionary with added data
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
# convert name from alias:
|
|
274
|
+
# temporary dictionary
|
|
275
|
+
td = {name.strip(): val.strip() for name, val in zip(names, data)}
|
|
276
|
+
|
|
277
|
+
# remove the overtime data if necessary
|
|
278
|
+
# if hours > 0:
|
|
279
|
+
# if d['Seconds'] != []:
|
|
280
|
+
# # print(td)
|
|
281
|
+
# while self.getXAxisFromTime(td["Date"].strip(), td["Time"].strip()) - d['Seconds'][0] > hours * 60 * 60:
|
|
282
|
+
# for name, value in d.items():
|
|
283
|
+
# value.pop(0)
|
|
284
|
+
|
|
285
|
+
# create empty list if key not exists in dictionary
|
|
286
|
+
for name in td.keys():
|
|
287
|
+
if name not in d.keys():
|
|
288
|
+
d[name] = [_np.nan] * len(d["Date"])
|
|
289
|
+
|
|
290
|
+
for name in d.keys():
|
|
291
|
+
if name in ["Date", "Time"]:
|
|
292
|
+
td[name] = td[name].strip()
|
|
293
|
+
elif name == "Seconds":
|
|
294
|
+
td[name] = self.getXAxisFromTime(td["Date"].strip(), td["Time"].strip())
|
|
295
|
+
elif name in td and td[name] != "nan":
|
|
296
|
+
if self.command_info_by_name[name][3]:
|
|
297
|
+
td[name] = str(td[name])
|
|
298
|
+
else:
|
|
299
|
+
td[name] = float(td[name])
|
|
300
|
+
elif name:
|
|
301
|
+
td[name] = _np.nan
|
|
302
|
+
|
|
303
|
+
d[name].append(td[name])
|
|
304
|
+
self.latest_data = td
|
|
305
|
+
|
|
306
|
+
return d
|
|
307
|
+
|
|
308
|
+
### ======================================================= X-axis Processing =======================================================
|
|
309
|
+
def getXAxisFromTime(self, date: str, time: str):
|
|
310
|
+
"""
|
|
311
|
+
Convert Date and Time string to the seconds from 1970/1/1
|
|
312
|
+
|
|
313
|
+
Use seconds as X axis
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
date (str): in format %Y-%m-%d
|
|
317
|
+
time (str): in format %H:%M:%S
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
seconds (int): seconds from 1970/1/1
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
string = date + " " + time # in format '%Y-%m-%d %H:%M:%S'
|
|
324
|
+
datetime_object = datetime.strptime(string, "%Y-%m-%d %H:%M:%S")
|
|
325
|
+
delta = datetime_object - datetime(1970, 1, 1)
|
|
326
|
+
|
|
327
|
+
return int(delta.total_seconds())
|
|
328
|
+
|
|
329
|
+
def getXTicks(self, seconds: list, time: list, ticks_number: int = 10):
|
|
330
|
+
"""
|
|
331
|
+
Get less dense X ticks from x list and time list
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
seconds (list): the list of seconds in integer
|
|
335
|
+
time (list): the list of time in string
|
|
336
|
+
ticks_number (int): the number of ticks in plotting
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
x_ticks (list): the list of tuple in format (seconds: time)
|
|
340
|
+
|
|
341
|
+
"""
|
|
342
|
+
# if not len(seconds) or len(seconds) != len(time):
|
|
343
|
+
# return False
|
|
344
|
+
|
|
345
|
+
# if len(seconds) <= ticks_number:
|
|
346
|
+
# x_ticks = [(seconds[i], time[i]) for i in range(len(seconds))]
|
|
347
|
+
|
|
348
|
+
# else:
|
|
349
|
+
# x_ticks = [
|
|
350
|
+
# (seconds[i], time[i])
|
|
351
|
+
# for i in _np.linspace(0, len(seconds) - 1, ticks_number, dtype=int)
|
|
352
|
+
# ]
|
|
353
|
+
time = []
|
|
354
|
+
|
|
355
|
+
for i in range(len(seconds)):
|
|
356
|
+
time.append(datetime.utcfromtimestamp(seconds[i]).strftime("%Y-%m-%d\n%H:%M:%S"))
|
|
357
|
+
|
|
358
|
+
if len(seconds) <= ticks_number:
|
|
359
|
+
x_ticks = [(seconds[i], time[i]) for i in range(len(seconds))]
|
|
360
|
+
|
|
361
|
+
else:
|
|
362
|
+
x_ticks = [
|
|
363
|
+
(seconds[i], time[i])
|
|
364
|
+
for i in _np.linspace(0, len(seconds) - 1, ticks_number, dtype=int)
|
|
365
|
+
]
|
|
366
|
+
return x_ticks
|
|
367
|
+
|
|
368
|
+
def getLine(self):
|
|
369
|
+
"""
|
|
370
|
+
Initialize plotting and get line based on name
|
|
371
|
+
"""
|
|
372
|
+
self.line_by_name = {} # {name: line}
|
|
373
|
+
for name, data in self.data_by_name.items(): # loop all names
|
|
374
|
+
pen = self.pen_by_name[name] # set pen
|
|
375
|
+
self.line_by_name[name] = self.graphWidget.plot(
|
|
376
|
+
[], [], name=name, pen=pen, label=self.x_ticks
|
|
377
|
+
) # initialize data plotting and get line for each name
|
|
378
|
+
|
|
379
|
+
self.graphWidget.setBackground("w")
|
|
380
|
+
self.graphWidget.showGrid(x=True, y=True)
|
|
381
|
+
self.legend = self.graphWidget.addLegend()
|
|
382
|
+
self.ax = self.graphWidget.getAxis("bottom")
|
|
383
|
+
self.ax.setStyle(tickFont=pg.QtGui.QFont("Arial", 7))
|
|
384
|
+
|
|
385
|
+
return True
|
|
386
|
+
|
|
387
|
+
### ======================================================= Drawing Pen =======================================================
|
|
388
|
+
def getPenByName(self):
|
|
389
|
+
"""
|
|
390
|
+
Set curve color and line style to pen
|
|
391
|
+
|
|
392
|
+
Idea: loop color list. If color is the same, use different dash line
|
|
393
|
+
1. loop the color list. e.g. 1%4 = 1 and 5%4 = 1, so the second and fifth element use the same color
|
|
394
|
+
2. loop the dash list. e.g. 1//4%4 = 0 and 5//4%4 = 1, so the second element doesn't have dash line but fifth element use dash line [16, 16]
|
|
395
|
+
|
|
396
|
+
"""
|
|
397
|
+
self.pen_by_name = {} # {name: pen}
|
|
398
|
+
|
|
399
|
+
color_list_loop = ["#F37021", "#46812B", "#67AE3E", "#4D4D4F"] # can be extend
|
|
400
|
+
dash_list_loop = [None, None, None, None] # can be extend
|
|
401
|
+
|
|
402
|
+
for index, name in enumerate(self.all_names):
|
|
403
|
+
color = color_list_loop[index % len(color_list_loop)] # loop color list
|
|
404
|
+
dash = dash_list_loop[
|
|
405
|
+
index // len(color_list_loop) % len(dash_list_loop)
|
|
406
|
+
] # loop dash line list if same color
|
|
407
|
+
self.pen_by_name[name] = pg.mkPen(
|
|
408
|
+
color=color, dash=dash, width=2.5
|
|
409
|
+
) # set to pen by name
|
|
410
|
+
|
|
411
|
+
return True
|
|
412
|
+
|
|
413
|
+
### ================================================ Set Static or Live plot ===============================================
|
|
414
|
+
def setPlotType(self):
|
|
415
|
+
"""
|
|
416
|
+
The setToStatic button will set the static plot
|
|
417
|
+
The setToLive button will set the live plot
|
|
418
|
+
By default, the plot is live
|
|
419
|
+
"""
|
|
420
|
+
self.plot_type = True # plot is live
|
|
421
|
+
self.setToStatic.clicked.connect(
|
|
422
|
+
self.setStatic
|
|
423
|
+
) # ok button to set static by date
|
|
424
|
+
self.setToLive.clicked.connect(
|
|
425
|
+
self.setLive
|
|
426
|
+
) # reset button to set plot back to live
|
|
427
|
+
self.loadFile.triggered.connect(
|
|
428
|
+
self.loadStaticFile
|
|
429
|
+
) # menu bar for file selection
|
|
430
|
+
|
|
431
|
+
def setStatic(self):
|
|
432
|
+
"""
|
|
433
|
+
Set plot type to static if the input is valid, or give the error and set plot type back to live
|
|
434
|
+
"""
|
|
435
|
+
self.getSelectedDataRangeByDate()
|
|
436
|
+
self.selected_data_by_file = False
|
|
437
|
+
self.selected_data_by_date = True
|
|
438
|
+
|
|
439
|
+
def setLive(self):
|
|
440
|
+
self.plot_type = True
|
|
441
|
+
self.selected_data_by_file = False
|
|
442
|
+
self.selected_data_by_date = False
|
|
443
|
+
self.static_update_request = False
|
|
444
|
+
|
|
445
|
+
### =============================================== Static Plot by File Name ===============================================
|
|
446
|
+
def loadStaticFile(self):
|
|
447
|
+
"""
|
|
448
|
+
Load file from menu bar
|
|
449
|
+
"""
|
|
450
|
+
filename, ext = QFileDialog.getOpenFileName(
|
|
451
|
+
caption="Import File", dir=self.file_dir, filter="*.csv"
|
|
452
|
+
)
|
|
453
|
+
if filename and "log_" in filename and '.csv' in filename: # if a file is selected and file is valid
|
|
454
|
+
try:
|
|
455
|
+
self.temp_data_by_name = {"Date": [], "Time": [], "Seconds": []}
|
|
456
|
+
f = open(filename, "r")
|
|
457
|
+
names = f.readline().strip("\n").split(",")
|
|
458
|
+
names = self.convertNames(names)
|
|
459
|
+
for data in csv.reader(f, delimiter=","):
|
|
460
|
+
self.temp_data_by_name = self.updateDataToDict(
|
|
461
|
+
names, data, self.temp_data_by_name
|
|
462
|
+
)
|
|
463
|
+
f.close()
|
|
464
|
+
if self.temp_data_by_name["Date"]:
|
|
465
|
+
self.plot_type = False
|
|
466
|
+
self.static_update_request = True
|
|
467
|
+
self.selected_data_by_file = True
|
|
468
|
+
self.selected_data_by_date = False
|
|
469
|
+
|
|
470
|
+
return True
|
|
471
|
+
else:
|
|
472
|
+
self.warningText.appendPlainText("The selected file is empty.")
|
|
473
|
+
|
|
474
|
+
except:
|
|
475
|
+
self.warningText.appendPlainText(
|
|
476
|
+
"The selected file is invalid or compromised."
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
self.static_update_request = False
|
|
480
|
+
self.selected_data_by_file = False
|
|
481
|
+
return False
|
|
482
|
+
|
|
483
|
+
### ================================================= Static Plot by Date ==================================================
|
|
484
|
+
def getSelectedDataRangeByDate(self):
|
|
485
|
+
"""
|
|
486
|
+
Use formatted input dates to calculated the index range, and give the index for staticPlot function
|
|
487
|
+
"""
|
|
488
|
+
self.time_is_valid = True # initial True and force to False if input time is not valid. This value is used for making a new static plot
|
|
489
|
+
|
|
490
|
+
# Get time in seconds in a list [start_time, end_time]
|
|
491
|
+
|
|
492
|
+
# Value explains
|
|
493
|
+
# False: the incorrect format that will give the warning message, and the plot will not be static until the input is correct
|
|
494
|
+
# None: the not given value, eg. [1, None] means 1 s to the moment when user click ok and plot is static
|
|
495
|
+
# seconds (int): the seconds converted from formatted input. eg. [500, 1500] means 500 s to 1500 s
|
|
496
|
+
|
|
497
|
+
# The format is yyyymmddHHMM, where yyyy is complete year, mm is complete month, dd is complete day, HH is hour in 24 hour format, and MM is minutes
|
|
498
|
+
# Hours and minutes are optional, the rest are required
|
|
499
|
+
# If the Input and string 'yyyymmddHHMM' or empty, it will assign None to list
|
|
500
|
+
|
|
501
|
+
self.static_time_range = [
|
|
502
|
+
self.returnSeconds(self.startTime.text().strip()),
|
|
503
|
+
self.returnSeconds(self.endTime.text().strip()),
|
|
504
|
+
]
|
|
505
|
+
|
|
506
|
+
if (
|
|
507
|
+
self.static_time_range[0] == False or self.static_time_range[1] == False
|
|
508
|
+
): # input time format is incorrect
|
|
509
|
+
self.warningText.appendPlainText("Static input time is not valid")
|
|
510
|
+
self.time_is_valid = False
|
|
511
|
+
|
|
512
|
+
else:
|
|
513
|
+
# convert seconds to the index of nearest value in self.all_x (all data x-axis)
|
|
514
|
+
self.static_index = [
|
|
515
|
+
self.binary_search(self.all_x, self.static_time_range[0]),
|
|
516
|
+
self.binary_search(self.all_x, self.static_time_range[1]),
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
if not self.static_index[
|
|
520
|
+
0
|
|
521
|
+
]: # force None to 0 for start time if no input is given
|
|
522
|
+
self.static_index[0] = 0
|
|
523
|
+
if not self.static_index[
|
|
524
|
+
1
|
|
525
|
+
]: # force None to the last index in current data x-axis for end time if no input is given
|
|
526
|
+
self.static_index[1] = len(self.all_x) - 1
|
|
527
|
+
|
|
528
|
+
if (
|
|
529
|
+
self.static_index[1] == 0
|
|
530
|
+
): # the end time error which leads to single points
|
|
531
|
+
self.warningText.appendPlainText(
|
|
532
|
+
"Static input time is not valid: end time is earlier than the logging start time"
|
|
533
|
+
)
|
|
534
|
+
self.time_is_valid = False
|
|
535
|
+
|
|
536
|
+
if (
|
|
537
|
+
self.static_index[0] >= self.static_index[1]
|
|
538
|
+
): # the end time error which leads to indexing issue
|
|
539
|
+
self.warningText.appendPlainText(
|
|
540
|
+
"Static input time is not valid: end time should be later than start time"
|
|
541
|
+
)
|
|
542
|
+
self.time_is_valid = False
|
|
543
|
+
|
|
544
|
+
self.plot_type = False # make plot type to static
|
|
545
|
+
self.static_update_request = (
|
|
546
|
+
True # only update static data when 'OK' button is pressed.
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
return True
|
|
550
|
+
|
|
551
|
+
def returnSeconds(self, time: str):
|
|
552
|
+
"""
|
|
553
|
+
Check if a time string is valid in format yyyymmdd or yyyymmddHH or yyyymmddHHMM or empty and return seconds
|
|
554
|
+
"""
|
|
555
|
+
|
|
556
|
+
# check empty
|
|
557
|
+
if time == "yyyymmddHHMM" or not time: # by default or not given
|
|
558
|
+
return None
|
|
559
|
+
|
|
560
|
+
# check length:
|
|
561
|
+
if len(time) not in [
|
|
562
|
+
len("yyyymmdd"),
|
|
563
|
+
len("yyyymmddHH"),
|
|
564
|
+
len("yyyymmddHHMM"),
|
|
565
|
+
]: # incorrect length
|
|
566
|
+
return False
|
|
567
|
+
|
|
568
|
+
if len(time) == len("yyyymmdd"):
|
|
569
|
+
try:
|
|
570
|
+
datetime_object = datetime.strptime(time, "%Y%m%d")
|
|
571
|
+
except:
|
|
572
|
+
return False
|
|
573
|
+
|
|
574
|
+
elif len(time) == len("yyyymmddHH"):
|
|
575
|
+
try:
|
|
576
|
+
datetime_object = datetime.strptime(time, "%Y%m%d%H")
|
|
577
|
+
except:
|
|
578
|
+
return False
|
|
579
|
+
|
|
580
|
+
elif len(time) == len("yyyymmddHHMM"):
|
|
581
|
+
try:
|
|
582
|
+
datetime_object = datetime.strptime(time, "%Y%m%d%H%M")
|
|
583
|
+
except:
|
|
584
|
+
return False
|
|
585
|
+
else:
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
delta = datetime_object - datetime(1970, 1, 1) # use 1970/1/1 as reference
|
|
589
|
+
|
|
590
|
+
return int(delta.total_seconds())
|
|
591
|
+
|
|
592
|
+
### ======================================================= Plotting =======================================================
|
|
593
|
+
def plot(self):
|
|
594
|
+
"""
|
|
595
|
+
Plotting data
|
|
596
|
+
"""
|
|
597
|
+
for name in self.all_names:
|
|
598
|
+
if name in self.shown_list: # plot line in shown widget
|
|
599
|
+
if not self.legend.getLabel(
|
|
600
|
+
self.line_by_name[name]
|
|
601
|
+
): # if not legend, add legend
|
|
602
|
+
self.legend.addItem(self.line_by_name[name], name)
|
|
603
|
+
|
|
604
|
+
self.line_by_name[name].setData(self.x, self.data_by_name[name])
|
|
605
|
+
self.ax.setTicks([self.x_ticks])
|
|
606
|
+
|
|
607
|
+
else: # hide line in hidden widget
|
|
608
|
+
self.legend.removeItem(name) # remove legend
|
|
609
|
+
self.line_by_name[name].setData([], []) # display empty line
|
|
610
|
+
|
|
611
|
+
### ======================================================= List Widget Interaction =======================================================
|
|
612
|
+
def saveDisplaySettings(self):
|
|
613
|
+
"""
|
|
614
|
+
This will save hidden and shown items and window length in the file called 'display_settings.txt'
|
|
615
|
+
format:
|
|
616
|
+
1st line: 'hidden[, item1][, item2]....'
|
|
617
|
+
2nd line: 'shown[, item3][, item4]....'
|
|
618
|
+
3rd line: 'window length,[window_length]'
|
|
619
|
+
"""
|
|
620
|
+
|
|
621
|
+
hidden_list = ["hidden"]
|
|
622
|
+
hidden_list.extend(self.hidden_list)
|
|
623
|
+
hidden_string = ",".join(hidden_list) + "\n"
|
|
624
|
+
shown_list = ["shown"]
|
|
625
|
+
shown_list.extend(self.shown_list)
|
|
626
|
+
shown_string = ",".join(shown_list) + "\n"
|
|
627
|
+
ingnore_list = ["ignore"]
|
|
628
|
+
ingnore_list.extend(self.ignore_list)
|
|
629
|
+
ignore_string = ",".join(ingnore_list) + "\n"
|
|
630
|
+
window_length_string = "window length," + self.windowLength.text()
|
|
631
|
+
f = open(self.file_dir + "display_settings.txt", "w")
|
|
632
|
+
f.write(hidden_string + shown_string + ignore_string + window_length_string)
|
|
633
|
+
f.close()
|
|
634
|
+
|
|
635
|
+
return True
|
|
636
|
+
|
|
637
|
+
def loadDisplaySettings(self):
|
|
638
|
+
"""
|
|
639
|
+
This will load hidden item and shown item settings
|
|
640
|
+
return True if loading successfully, else False
|
|
641
|
+
"""
|
|
642
|
+
|
|
643
|
+
self.hidden_list = []
|
|
644
|
+
self.shown_list = []
|
|
645
|
+
self.ignore_list =[]
|
|
646
|
+
if "display_settings.txt" in os.listdir(self.file_dir):
|
|
647
|
+
f = open(self.file_dir + "display_settings.txt", "r")
|
|
648
|
+
hidden_list = f.readline().strip("\n").split(",")[1:] # remove 'hidden'
|
|
649
|
+
shown_list = f.readline().strip("\n").split(",")[1:] # remove 'shown'
|
|
650
|
+
ignore_list = f.readline().strip("\n").split(",")[1:] # remove 'ignore'
|
|
651
|
+
window_length = (
|
|
652
|
+
f.readline().strip("\n").split(",")[1]
|
|
653
|
+
) # get window length in str
|
|
654
|
+
self.windowLength.setValue(int(window_length))
|
|
655
|
+
f.close()
|
|
656
|
+
for name in self.all_names:
|
|
657
|
+
if name in shown_list:
|
|
658
|
+
self.shown_list.append(name)
|
|
659
|
+
elif name in hidden_list: # in the case that the item is hidden or hasn't been set up
|
|
660
|
+
self.hidden_list.append(name)
|
|
661
|
+
elif name in ignore_list:
|
|
662
|
+
self.ignore_list.append(name)
|
|
663
|
+
return True
|
|
664
|
+
else: # if the file not exist, copy all names to hidden list
|
|
665
|
+
self.hidden_list = self.all_names.copy()
|
|
666
|
+
for name in self.all_names:
|
|
667
|
+
if self.command_info_by_name[name][3]:
|
|
668
|
+
self.ignore_list.append(self.hidden_list.pop(self.hidden_list.index(name)))
|
|
669
|
+
return False
|
|
670
|
+
|
|
671
|
+
def hideItems(self):
|
|
672
|
+
"""
|
|
673
|
+
Move items from shown list to hidden list, and from shown widget to hidden widget
|
|
674
|
+
"""
|
|
675
|
+
selected = self.shownListWidget.selectedItems() # multi selection
|
|
676
|
+
for item in selected:
|
|
677
|
+
row = self.shownListWidget.row(item) # get row
|
|
678
|
+
self.hiddenListWidget.addItem(
|
|
679
|
+
self.shownListWidget.takeItem(row)
|
|
680
|
+
) # remove item from shown widget to hidden widget
|
|
681
|
+
self.shown_list.remove(item.text()) # add the item from hidden list
|
|
682
|
+
self.hidden_list.append(item.text()) # remove the item to the shown list
|
|
683
|
+
|
|
684
|
+
self.saveDisplaySettings() # save current setting
|
|
685
|
+
return True
|
|
686
|
+
|
|
687
|
+
def showItems(self):
|
|
688
|
+
"""
|
|
689
|
+
Move items from hidden list to shown list, and from hidden widget to shown widget
|
|
690
|
+
"""
|
|
691
|
+
|
|
692
|
+
selected = self.hiddenListWidget.selectedItems() # multi selection
|
|
693
|
+
for item in selected:
|
|
694
|
+
row = self.hiddenListWidget.row(item) # get row
|
|
695
|
+
self.shownListWidget.addItem(
|
|
696
|
+
self.hiddenListWidget.takeItem(row)
|
|
697
|
+
) # remove item from hidden widget to shown widget
|
|
698
|
+
self.hidden_list.remove(item.text()) # remove the item from hidden list
|
|
699
|
+
self.shown_list.append(item.text()) # add the item to the shown list
|
|
700
|
+
|
|
701
|
+
self.saveDisplaySettings() # save current setting
|
|
702
|
+
return True
|
|
703
|
+
|
|
704
|
+
### =======================================================Alias Related=======================================================
|
|
705
|
+
def getAlias(self, name: str = None):
|
|
706
|
+
"""
|
|
707
|
+
Get alias and assign it to the name
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
name (str, option): if name is provided, return the alias, otherwise get alias by name dict
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
alias (str): an alias
|
|
714
|
+
"""
|
|
715
|
+
if not name: # generate alias dictionary
|
|
716
|
+
self.alias_by_name = {} # {name: alias}
|
|
717
|
+
for device in self.device_config:
|
|
718
|
+
for name, info in self.commands[device].items():
|
|
719
|
+
if info["alias"]:
|
|
720
|
+
self.alias_by_name[name] = info["alias"]
|
|
721
|
+
else:
|
|
722
|
+
self.alias_by_name[name] = name
|
|
723
|
+
|
|
724
|
+
return True
|
|
725
|
+
else:
|
|
726
|
+
return self.alias_by_name[
|
|
727
|
+
name
|
|
728
|
+
] # this is the special case that the name is actually variable name
|
|
729
|
+
|
|
730
|
+
def convertNames(self, names: list):
|
|
731
|
+
"""
|
|
732
|
+
Convert a list of name to a list of alias
|
|
733
|
+
Args:
|
|
734
|
+
Names (list): a list of name
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
Names (list): a list of name from alias
|
|
738
|
+
"""
|
|
739
|
+
|
|
740
|
+
for i in range(len(names)):
|
|
741
|
+
if names[i].strip() in [
|
|
742
|
+
"Date",
|
|
743
|
+
"Time",
|
|
744
|
+
"Seconds",
|
|
745
|
+
]: # not consider x-axis labels
|
|
746
|
+
pass
|
|
747
|
+
else:
|
|
748
|
+
names[i] = self.getAlias(names[i].strip()) # strip white space in case
|
|
749
|
+
|
|
750
|
+
return names
|
|
751
|
+
|
|
752
|
+
### =======================================================Status Related=======================================================
|
|
753
|
+
def setLEDIndicator(self):
|
|
754
|
+
"""
|
|
755
|
+
Set indicators based on the number of devices and status of devices
|
|
756
|
+
"""
|
|
757
|
+
self.status_names = {} # the list to store what need to be shown as status
|
|
758
|
+
layout = QVBoxLayout()
|
|
759
|
+
self.indicator_dictionary = {}
|
|
760
|
+
self.status = {False: red, True: green}
|
|
761
|
+
for device in ["Logger"] + list(self.device_config):
|
|
762
|
+
led = QCheckBox(device)
|
|
763
|
+
led.setCheckable(False)
|
|
764
|
+
led.setStyleSheet(self.status[False])
|
|
765
|
+
layout.addWidget(led)
|
|
766
|
+
self.indicator_dictionary[device] = led
|
|
767
|
+
self.status_names[device] = []
|
|
768
|
+
if device != "Logger":
|
|
769
|
+
for name, info in self.commands[device].items():
|
|
770
|
+
if info['indicators']:
|
|
771
|
+
self.status_names[device].append(name)
|
|
772
|
+
for indicator in info['indicators']:
|
|
773
|
+
led = QCheckBox(indicator)
|
|
774
|
+
led.setCheckable(False)
|
|
775
|
+
led.setStyleSheet(self.status[False])
|
|
776
|
+
layout.addWidget(led)
|
|
777
|
+
self.indicator_dictionary[indicator] = led
|
|
778
|
+
self.groupBox_6.setLayout(layout)
|
|
779
|
+
|
|
780
|
+
def setStatus(self):
|
|
781
|
+
"""
|
|
782
|
+
Set indicator values of devices
|
|
783
|
+
"""
|
|
784
|
+
current_exe = (
|
|
785
|
+
os.popen("wmic process get description")
|
|
786
|
+
.read()
|
|
787
|
+
.strip()
|
|
788
|
+
.replace(" ", "")
|
|
789
|
+
.split("\n\n")
|
|
790
|
+
)
|
|
791
|
+
logger_status_change = ("SpecLogger_running.exe" in current_exe) != (self.indicator_dictionary["Logger"].styleSheet() == self.status[True])
|
|
792
|
+
if logger_status_change:
|
|
793
|
+
self.indicator_dictionary["Logger"].setStyleSheet(self.status["SpecLogger_running.exe" in current_exe])
|
|
794
|
+
|
|
795
|
+
for device in self.device_config:
|
|
796
|
+
if "SpecLogger_running.exe" in current_exe:
|
|
797
|
+
change_detected = self.device_config[device]["device_status"] != (self.indicator_dictionary[device].styleSheet() == self.status[True])
|
|
798
|
+
if change_detected:
|
|
799
|
+
self.indicator_dictionary[device].setStyleSheet(self.status[self.device_config[device]["device_status"]])
|
|
800
|
+
|
|
801
|
+
if self.latest_data:
|
|
802
|
+
for name in self.status_names[device]:
|
|
803
|
+
indicators = self.commands[device][name]['indicators']
|
|
804
|
+
indicators_reverse = self.commands[device][name]['indicators_reverse']
|
|
805
|
+
status = self.convertStringtoBit(self.latest_data[self.getAlias('status')], len(indicators), True)
|
|
806
|
+
if status == -1:
|
|
807
|
+
for i, indicator in enumerate(indicators):
|
|
808
|
+
self.indicator_dictionary[indicator].setStyleSheet(self.status[False])
|
|
809
|
+
else:
|
|
810
|
+
for i, indicator in enumerate(indicators):
|
|
811
|
+
res = bool(eval(status[i])) if not indicators_reverse[i] else bool(1 - eval(status[i]))
|
|
812
|
+
self.indicator_dictionary[indicator].setStyleSheet(self.status[res])
|
|
813
|
+
else:
|
|
814
|
+
self.indicator_dictionary[device].setStyleSheet(self.status[False])
|
|
815
|
+
for name in self.status_names[device]:
|
|
816
|
+
indicators = self.commands[device][name]['indicators']
|
|
817
|
+
for indicator in indicators:
|
|
818
|
+
self.indicator_dictionary[indicator].setStyleSheet(self.status[False])
|
|
819
|
+
|
|
820
|
+
self.setSystemStatus()
|
|
821
|
+
|
|
822
|
+
def setSystemStatus(self):
|
|
823
|
+
"""
|
|
824
|
+
Set System Status based on indicator values
|
|
825
|
+
"""
|
|
826
|
+
for indicator in self.indicator_dictionary.values():
|
|
827
|
+
if 'red' in indicator.styleSheet():
|
|
828
|
+
temp_status = self.systemStatus.styleSheet().replace('green', 'red')
|
|
829
|
+
self.systemStatus.setStyleSheet(temp_status)
|
|
830
|
+
|
|
831
|
+
return False
|
|
832
|
+
|
|
833
|
+
for level in self.warning_level_by_name.values():
|
|
834
|
+
if level == 2:
|
|
835
|
+
temp_status = self.systemStatus.styleSheet().replace('green', 'red')
|
|
836
|
+
self.systemStatus.setStyleSheet(temp_status)
|
|
837
|
+
return False
|
|
838
|
+
elif level == 1:
|
|
839
|
+
if 'green' in indicator.styleSheet():
|
|
840
|
+
temp_status = self.systemStatus.styleSheet().replace('green', 'orange')
|
|
841
|
+
self.systemStatus.setStyleSheet(temp_status)
|
|
842
|
+
return False
|
|
843
|
+
|
|
844
|
+
temp_status = self.systemStatus.styleSheet().replace('red', 'green')
|
|
845
|
+
self.systemStatus.setStyleSheet(temp_status)
|
|
846
|
+
|
|
847
|
+
return True
|
|
848
|
+
|
|
849
|
+
### ======================================================= Warning Related =======================================================
|
|
850
|
+
def getCommandInfoByName(self):
|
|
851
|
+
"""
|
|
852
|
+
Get commands returns information: [min, max, static, bit_static]
|
|
853
|
+
"""
|
|
854
|
+
|
|
855
|
+
self.command_info_by_name = {}
|
|
856
|
+
for device in self.device_config:
|
|
857
|
+
for name, info in self.commands[device].items():
|
|
858
|
+
name = self.getAlias(name) # convert to alias
|
|
859
|
+
self.command_info_by_name[name] = [None, None, None, None]
|
|
860
|
+
self.command_info_by_name[name][0] = info["min"]
|
|
861
|
+
self.command_info_by_name[name][1] = info["max"]
|
|
862
|
+
self.command_info_by_name[name][2] = info["static"]
|
|
863
|
+
if info['bit_static']:
|
|
864
|
+
self.command_info_by_name[name][3] = self.convertStringtoBit(info['bit_static'], info['bits'], True)
|
|
865
|
+
|
|
866
|
+
### ======================================================= Warning Related =======================================================
|
|
867
|
+
def setWarningLevelByName(self):
|
|
868
|
+
"""
|
|
869
|
+
Set warning level for system indicator
|
|
870
|
+
"""
|
|
871
|
+
|
|
872
|
+
self.warning_level_by_name = {name: 0 for name in self.data_by_name}
|
|
873
|
+
|
|
874
|
+
def printWarning(self):
|
|
875
|
+
"""
|
|
876
|
+
Print warning if the value is not in range
|
|
877
|
+
"""
|
|
878
|
+
for name in self.all_data_by_name:
|
|
879
|
+
if name not in ["Date", "Time", "Seconds"]:
|
|
880
|
+
warning_level = 0
|
|
881
|
+
minimum = self.command_info_by_name[name][0]
|
|
882
|
+
maximum = self.command_info_by_name[name][1]
|
|
883
|
+
static = self.command_info_by_name[name][2]
|
|
884
|
+
if static:
|
|
885
|
+
if self.all_data_by_name[name][-1] != static:
|
|
886
|
+
warning_level = 2
|
|
887
|
+
|
|
888
|
+
if minimum:
|
|
889
|
+
if self.all_data_by_name[name][-1] < minimum:
|
|
890
|
+
warning_level = 2
|
|
891
|
+
|
|
892
|
+
elif self.all_data_by_name[name][-1] < 1.05 * minimum:
|
|
893
|
+
warning_level = 1
|
|
894
|
+
|
|
895
|
+
if maximum:
|
|
896
|
+
if self.all_data_by_name[name][-1] > maximum:
|
|
897
|
+
warning_level = 2
|
|
898
|
+
elif self.all_data_by_name[name][-1] > 0.95 * maximum:
|
|
899
|
+
warning_level = 1
|
|
900
|
+
|
|
901
|
+
if (
|
|
902
|
+
warning_level > 0 and self.warning_level_by_name[name] == 0
|
|
903
|
+
): # warning begins
|
|
904
|
+
current_time = (
|
|
905
|
+
self.all_data_by_name["Date"][-1]
|
|
906
|
+
+ " "
|
|
907
|
+
+ self.all_data_by_name["Time"][-1]
|
|
908
|
+
)
|
|
909
|
+
self.warning_level_by_name[name] = warning_level
|
|
910
|
+
if warning_level == 1:
|
|
911
|
+
string = current_time + ": " + name + " is reaching limit"
|
|
912
|
+
elif warning_level == 2:
|
|
913
|
+
string = current_time + ": " + name + " exceeds limit"
|
|
914
|
+
self.warningText.appendPlainText(string)
|
|
915
|
+
|
|
916
|
+
elif (
|
|
917
|
+
warning_level == 0 and self.warning_level_by_name[name] > 0
|
|
918
|
+
): # warning ends
|
|
919
|
+
current_time = (
|
|
920
|
+
self.all_data_by_name["Date"][-1]
|
|
921
|
+
+ " "
|
|
922
|
+
+ self.all_data_by_name["Time"][-1]
|
|
923
|
+
)
|
|
924
|
+
self.warning_level_by_name[name] = warning_level
|
|
925
|
+
string = current_time + ": " + name + " warning/error clean."
|
|
926
|
+
self.warningText.appendPlainText(string)
|
|
927
|
+
|
|
928
|
+
def clearWarning(self):
|
|
929
|
+
"""
|
|
930
|
+
Clear warning information
|
|
931
|
+
"""
|
|
932
|
+
for name in self.data_by_name:
|
|
933
|
+
if name not in ["Date", "Time", "Seconds"]:
|
|
934
|
+
self.warning_level_by_name[name] = 0
|
|
935
|
+
self.warningText.clear()
|
|
936
|
+
|
|
937
|
+
return True
|
|
938
|
+
|
|
939
|
+
### ======================================================= Internal Used Functions ====================================================
|
|
940
|
+
def binary_search(self, array, value):
|
|
941
|
+
"""
|
|
942
|
+
It is used for searching the index of closest value in a given array. The method is binary search, faster in large array
|
|
943
|
+
|
|
944
|
+
Please be notified that the array has been sorted already. The return is modified to get the index
|
|
945
|
+
|
|
946
|
+
Reference: https://www.geeksforgeeks.org/find-closest-number-array/
|
|
947
|
+
"""
|
|
948
|
+
if value == None:
|
|
949
|
+
return None
|
|
950
|
+
|
|
951
|
+
n = len(array)
|
|
952
|
+
if value < array[0]:
|
|
953
|
+
return 0
|
|
954
|
+
elif value > array[-1]:
|
|
955
|
+
return n - 1
|
|
956
|
+
|
|
957
|
+
i = 0 # lower limit
|
|
958
|
+
j = n # upper limit
|
|
959
|
+
|
|
960
|
+
while i < j:
|
|
961
|
+
mid = (i + j) // 2
|
|
962
|
+
|
|
963
|
+
if array[mid] == value:
|
|
964
|
+
return mid
|
|
965
|
+
|
|
966
|
+
if value < array[mid]: # in the case value is on the left side of array
|
|
967
|
+
if (
|
|
968
|
+
mid > 0 and value > array[mid - 1]
|
|
969
|
+
): # array[mid] > value > array[mid - 1]
|
|
970
|
+
return self.get_closest(mid - 1, mid, value, array)
|
|
971
|
+
|
|
972
|
+
j = mid # move upper limit to mid
|
|
973
|
+
|
|
974
|
+
else: # in the case value is on the right side of array
|
|
975
|
+
if (
|
|
976
|
+
mid < n - 1 and value < array[mid + 1]
|
|
977
|
+
): # array[mid] < value < array[mid + 1]
|
|
978
|
+
return self.get_closest(1, mid + 1, value, array)
|
|
979
|
+
|
|
980
|
+
i = mid + 1 # move lower limit to mid
|
|
981
|
+
|
|
982
|
+
return mid
|
|
983
|
+
|
|
984
|
+
def get_closest(self, index1, index2, target, array):
|
|
985
|
+
if target - array[index1] >= array[index2] - target:
|
|
986
|
+
return index2
|
|
987
|
+
else:
|
|
988
|
+
return index1
|
|
989
|
+
|
|
990
|
+
def convertStringtoBit(self, string, bits = None, reverse = False):
|
|
991
|
+
'''
|
|
992
|
+
convert string to bit string, and optionally to trim it and reverse it
|
|
993
|
+
eg. if string is 301, the function will return 1100000001,
|
|
994
|
+
if bits is 9 and reverse is False, the function will return 100000001,
|
|
995
|
+
if reverse is True and bits is None, the function will return 1000000011,
|
|
996
|
+
|
|
997
|
+
Trimming will be applied before Reverse.
|
|
998
|
+
|
|
999
|
+
'''
|
|
1000
|
+
if isinstance(string, float): # nan
|
|
1001
|
+
return -1
|
|
1002
|
+
|
|
1003
|
+
if '0x' not in string:
|
|
1004
|
+
string = '0x' + string
|
|
1005
|
+
|
|
1006
|
+
out = bin(eval(string))
|
|
1007
|
+
out = out.replace('0b', '')
|
|
1008
|
+
if bits:
|
|
1009
|
+
out = out[-1 * bits:]
|
|
1010
|
+
if len(out) < bits:
|
|
1011
|
+
out = '0' * (bits - len(out)) + out
|
|
1012
|
+
if reverse:
|
|
1013
|
+
out = out[::-1]
|
|
1014
|
+
|
|
1015
|
+
return out
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
if __name__ == "__main__":
|
|
1020
|
+
app = QApplication([])
|
|
1021
|
+
window = MainWindow()
|
|
1022
|
+
window.show()
|
|
1023
|
+
app.exec()
|