SpecLog 0.0.1__tar.gz

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-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: SpecLog
3
+ Version: 0.0.1
4
+ Summary: A Python Package for logging data from instrumentations
5
+ Author-email: Yen-Chun Huang <yen-chun.huang@bruker.com>
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: Microsoft :: Windows
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: matplotlib>=3.7.1
12
+ Requires-Dist: numpy>=1.24.3
13
+ Requires-Dist: PySide6>=6.9.0
14
+ Requires-Dist: pyqtgraph>=0.13.3
15
+ Requires-Dist: spinlab>=1.1.3
16
+ Requires-Dist: pyserial>=3.5
17
+
18
+ # SpecLog
19
+ Python Package for Spectrometer Logging
@@ -0,0 +1,2 @@
1
+ # SpecLog
2
+ Python Package for Spectrometer Logging
@@ -0,0 +1,277 @@
1
+ """
2
+ SpecLog: The logging program for instrumentations
3
+
4
+ SpecLog: read configurations, get available devices, send command and save return.
5
+
6
+ Author: Yen-Chun Huang
7
+
8
+ Company: Bridge 12 Technologies, Inc
9
+ """
10
+
11
+ from .device import *
12
+ import time
13
+ import datetime
14
+ import os
15
+ from .loggerConfig import *
16
+ from .debugLog import *
17
+
18
+
19
+ class SpecLog:
20
+ def __init__(self, config_file: str = None):
21
+ self.config = loggerConfig(config_file)
22
+ self.debugLogger = debugLog(config_file).logger
23
+ self.settings = self.config.settings
24
+ self.commands = (
25
+ self.config.commands
26
+ ) # dictionary {model: {variable: {command, alias, min, max, static}}}
27
+ self.device_config = self.config.devices
28
+
29
+ self.log_dir = self.settings["log_folder_location"] + "/LOG/"
30
+ self._checkDirectory()
31
+
32
+ self.delay = int(self.settings["log_interval"])
33
+ self.max_size = int(self.settings["save_file_size_kb"])
34
+
35
+ self.connectDevices()
36
+ self.reconnectDevices()
37
+
38
+ self.data_by_variable = self._getDataDictByVariable(
39
+ self.commands
40
+ ) # initial empty dictionary for storing data
41
+
42
+ self.header = self._makeLogHeader()
43
+ self.last_query_time = None
44
+ self.current_log_file = None
45
+ self.warning = 0
46
+
47
+ def log(self):
48
+ """
49
+ Send command to a valid device, analyze return and save data to log file
50
+
51
+ It runs one time only.
52
+ """
53
+ # if not self.current_log_file:
54
+ # self.debugLogger.info("new log file created")
55
+ # self._createNewLog()
56
+
57
+ now = time.time()
58
+ if self.reconnectDevices(): # check the device connections
59
+ self.debugLogger.info("A device is reconnected")
60
+ if not self.last_query_time or now - self.last_query_time > self.delay:
61
+ warning_level = 0
62
+ self._setTimeInDataDictByVariable() # update time
63
+ devices_info = (
64
+ self.devices.devices_info
65
+ ) # dictionary: {model: {status, config_status, device, id_command}}
66
+ for name, info in self.commands.items():
67
+ delimiter = self.device_config[name]["delimiter"]
68
+ index = self.device_config[name]["index"]
69
+ device = devices_info[name]["device"]
70
+ termination = self.device_config[name]["termination"]
71
+ for variable in info.keys():
72
+ info[variable]['min'], info[variable]['max'], info[variable]['static']
73
+ if self.devices.checkDeviceStatus(
74
+ name
75
+ ): # check the connection of a device
76
+ try:
77
+ command = info[variable]["command"]
78
+ device.write((command+termination).encode()) # send command to device
79
+ data_string = device.read_until(termination.encode()).decode()
80
+ data = self._returnStringConverter(
81
+ data_string, delimiter, index
82
+ )
83
+ except Exception as err:
84
+ self.devices.device_info["status"] = False
85
+ data = "nan"
86
+
87
+ else:
88
+ data = "nan" # write nan to not available data
89
+ self.data_by_variable[variable] = data
90
+
91
+ # check warning
92
+ if data == "nan":
93
+ warning_level = 2
94
+
95
+ elif info[variable]['static'] and float(data) != info[variable]['static']:
96
+ warning_level = 2
97
+
98
+ else:
99
+ if info[variable]['min']:
100
+ if float(data) <= info[variable]['min'] * 1.05:
101
+ warning_level = max(warning_level, 1)
102
+ elif float(data) < info[variable]['min']:
103
+ warning_level = 2
104
+
105
+
106
+ if info[variable]['max']:
107
+ if float(data) >= info[variable]['max'] * 0.95:
108
+ warning_level = max(warning_level, 1)
109
+ elif float(data) > info[variable]['max']:
110
+ warning_level = 2
111
+ self.warning = warning_level
112
+
113
+ self.last_query_time = now
114
+
115
+ # if self._checkFileSize(): # exceed the maximum file size
116
+ # if self._createNewLog(): # create log with header
117
+ # self.debugLogger.info("File size exceed, new log file created")
118
+
119
+ self._saveData()
120
+
121
+ def connectDevices(self):
122
+ """
123
+ Call DEVICE
124
+ """
125
+ self.devices = DEVICE(self.config, self.debugLogger) # establish communication
126
+ self.devices._getPorts()
127
+ self.available_addresses = self.devices.deviceAddresses
128
+ return True
129
+
130
+ def reconnectDevices(self):
131
+ restart_DEVICE = False
132
+ self.devices._getPorts()
133
+ for name in self.device_config.keys():
134
+ address = self.device_config[name]["address"]
135
+
136
+ if (
137
+ address not in self.devices.deviceAddresses
138
+ and address in self.available_addresses
139
+ ): # when a connection is loss
140
+ # This step is to prevent the moment when the device is shown in the resource manager but connection fails
141
+ self.available_addresses.remove(address)
142
+
143
+ if (
144
+ address in self.devices.deviceAddresses
145
+ and address not in self.available_addresses
146
+ and not self.devices.devices_info[name]["status"]
147
+ ):
148
+ restart_DEVICE = True
149
+
150
+ if restart_DEVICE:
151
+ del self.devices # delete the DEVICE class
152
+ return self.connectDevices()
153
+
154
+ else:
155
+ return False
156
+
157
+ def _checkDirectory(self):
158
+ if not os.path.exists(self.log_dir):
159
+ os.mkdir(self.log_dir)
160
+ return False
161
+ return True
162
+
163
+ def _findLog(self):
164
+ """
165
+ Find current log file
166
+
167
+ """
168
+ today = datetime.datetime.now().strftime("%Y%m%d") # YYYYMMDD
169
+ self.current_log_file = self.log_dir + "/log_" + today + ".csv"
170
+
171
+ return "log_" + today + ".csv" in os.listdir(self.log_dir)
172
+
173
+ def _createNewLog(self):
174
+ """
175
+ Create new log file in log directory
176
+ """
177
+ # now = datetime.datetime.now().strftime("%Y%m%d%H%M%S") # YYYYMMDDHMS
178
+
179
+ with open(self.current_log_file, "w") as f:
180
+ f.write(self.header)
181
+
182
+ return True
183
+
184
+ def _makeLogHeader(self):
185
+ """
186
+ Make a log header base on the data by variable dictionary
187
+
188
+ Return:
189
+ header (str) format: 'Date, Time, variable 1, variable 2.....'
190
+ """
191
+ list_of_items = list(self.data_by_variable.keys())
192
+ header = ", ".join(list_of_items) + "\n"
193
+
194
+ return header
195
+
196
+ def _saveData(self):
197
+ """
198
+ Save current data to current log file
199
+
200
+ """
201
+ list_of_data = list(self.data_by_variable.values())
202
+ data_string = ", ".join(list_of_data) + "\n"
203
+
204
+ if not self._findLog():
205
+ self._createNewLog()
206
+ with open(self.current_log_file, "a") as f:
207
+ f.write(data_string)
208
+
209
+ return True
210
+
211
+ # def _checkFileSize(self):
212
+ # """
213
+ # Check the size of file
214
+
215
+ # Return:
216
+ # bool: True if file is oversize
217
+ # """
218
+ # if os.path.getsize(self.current_log_file) > self.max_size * 1024:
219
+ # return True
220
+ # else:
221
+ # return False
222
+
223
+ def _getDataDictByVariable(self, command_dict: dict):
224
+ """
225
+ Get the variable list based on the model and it's command dictionary
226
+ Args:
227
+ command_dict (dict): the command list for a device, {variable: {command, alias, min, max, static}}
228
+
229
+ Returns:
230
+ data_dict (dict): the data dictionary in format {variable: reading}
231
+
232
+ """
233
+ today = str(datetime.date.today())
234
+ now = str(datetime.datetime.now().strftime("%H:%M:%S"))
235
+ data_dict = {"Date": today, "Time": now}
236
+ for info in command_dict.values():
237
+ temp_dict = {key: None for key in info}
238
+ data_dict = {**data_dict, **temp_dict}
239
+
240
+ return data_dict
241
+
242
+ def _returnStringConverter(self, string: str, delimiter: str, index: int):
243
+ """
244
+ Convert returned string and acquire data from it
245
+ Args:
246
+ delimiter (str): the delimiter for string analysis
247
+ index (int): the data index
248
+
249
+ Returns:
250
+ data (string): return data in str
251
+ """
252
+ try:
253
+ if delimiter:
254
+ data = string.split(delimiter)[index]
255
+ else:
256
+ data = string.split()[index]
257
+ data = data.replace(" ", "") # remove white space
258
+ data = data.replace("\n", "")
259
+ data = data.replace("\t", "")
260
+ data = data.replace("\r", "")
261
+ data = data.strip()
262
+ except:
263
+ self.debugLogger.error("String Convert Fail: recived: %s, delimiter: %s, index: %s" %(string, delimiter, index))
264
+ return data
265
+
266
+ def _setTimeInDataDictByVariable(self):
267
+ """
268
+ Update the time in data dictionary by variable
269
+ """
270
+ today = str(datetime.date.today())
271
+ now = str(datetime.datetime.now().strftime("%H:%M:%S"))
272
+ self.data_by_variable["Date"] = today
273
+ self.data_by_variable["Time"] = now
274
+ return True
275
+
276
+ # if __name__ == '__main__':
277
+ # SpecLog()
@@ -0,0 +1,128 @@
1
+ """
2
+ This is the python program to control pyB12logger
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import argparse
8
+ import shutil
9
+ import subprocess
10
+ from collections import Counter
11
+ from .SpecLog import *
12
+
13
+ # auto start and adding icon to desktop (public)
14
+ startup_folder = os.path.join(
15
+ os.environ["APPDATA"],
16
+ r"Microsoft\Windows\Start Menu\Programs\Startup"
17
+ )
18
+ desktop_folder = os.path.join(os.environ["USERPROFILE"], "Desktop")
19
+
20
+ source_running_logger = os.path.join(
21
+ os.path.dirname(sys.executable), "scripts", "SpecLogger_running.exe"
22
+ )
23
+
24
+ source_monitor = os.path.join(
25
+ os.path.dirname(sys.executable), "scripts", "pymonitor.exe"
26
+ )
27
+
28
+ def main_func():
29
+ parser = argparse.ArgumentParser(prog="SpecLogger")
30
+ parser.add_argument(
31
+ "status",
32
+ type=str,
33
+ nargs="?",
34
+ default=None,
35
+ choices=["start", "stop"],
36
+ help="To start/stop SpecLogger. If no argument, the SpecLogger will start by default",
37
+ )
38
+ parser.add_argument(
39
+ "-desktop",
40
+ type=str,
41
+ default=False,
42
+ choices=["True", "False"],
43
+ help="To create desktop icons",
44
+ )
45
+ parser.add_argument(
46
+ "-startup",
47
+ type=str,
48
+ default=None,
49
+ choices=["True", "False"],
50
+ help="To enable/disable SpecLogger at startup.",
51
+ )
52
+ parser.add_argument(
53
+ "-debug",
54
+ type=str,
55
+ default="False",
56
+ choices=["True", "False"],
57
+ help="To start debug console SpecLogger.",
58
+ )
59
+ args = parser.parse_args()
60
+
61
+ if args.startup == "True":
62
+ target = os.path.join(startup_folder, "SpecLogger_running.exe")
63
+ if not os.path.exists(target):
64
+ shutil.copy(source_running_logger, target)
65
+ print("SpecLogger will run on startup.")
66
+ elif args.startup == "False":
67
+ if not os.path.exists(startup_folder + "/SpecLogger_running.exe"):
68
+ print(startup_folder + "SpecLogger_running.exe")
69
+ print("SpecLogger does not run on startup.")
70
+ else:
71
+ os.remove(startup_folder + "/SpecLogger_running.exe")
72
+ print("SpecLogger will not run on startup.")
73
+
74
+ if args.desktop == "True":
75
+ target_logger = os.path.join(desktop_folder, "SpecLogger_running.exe")
76
+ target_monitor = os.path.join(desktop_folder, "pymonitor.exe")
77
+
78
+ if not os.path.exists(target_logger):
79
+ shutil.copy(source_running_logger, target_logger)
80
+ print("Create SpecLogger_running.exe on the desktop.")
81
+ else:
82
+ print("SpecLogger_running.exe is on desktop already.")
83
+
84
+ if not os.path.exists(target_monitor):
85
+ shutil.copy(source_monitor, target_monitor)
86
+ print("Create pymonitor.exe on the desktop.")
87
+ else:
88
+ print("pymonitor.exe is on desktop already.")
89
+
90
+ if not args.startup and not args.desktop and not args.status: # not arguments
91
+ args.status = "start"
92
+
93
+ if args.status == "start":
94
+ current_exe = (
95
+ os.popen("wmic process get description")
96
+ .read()
97
+ .strip()
98
+ .replace(" ", "")
99
+ .split("\n\n")
100
+ )
101
+ hashDict = Counter(current_exe)
102
+
103
+ if (
104
+ "SpecLogger_running.exe" in hashDict
105
+ and hashDict["SpecLogger_running.exe"] > 0
106
+ ):
107
+ print("SpecLogger has started already.")
108
+ return
109
+
110
+ else:
111
+ if args.debug == "False":
112
+ subprocess.Popen(
113
+ "SpecLogger_running.exe", creationflags=subprocess.CREATE_NO_WINDOW
114
+ )
115
+ print("SpecLogger started")
116
+ elif args.debug == "True":
117
+ os.startfile("SpecLogger_running.exe")
118
+ print("SpecLogger debug mode started")
119
+
120
+ elif args.status == "stop":
121
+ os.system("taskkill /im SpecLogger_running.exe /F /t")
122
+ print("SpecLogger stopped")
123
+ else: # ignore
124
+ return
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main_func()
@@ -0,0 +1,5 @@
1
+ from .SpecLog import *
2
+ from .device import *
3
+ from .monitor import *
4
+ from .version import *
5
+ from .ui import plotting
File without changes
@@ -0,0 +1,93 @@
1
+ """
2
+ global config
3
+ """
4
+ import configparser
5
+ from pathlib import Path
6
+ import warnings
7
+ import os
8
+ import shutil
9
+
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def _escape_split(s, delim=",", escape="\\"):
16
+ tokens = []
17
+ previous_escape = False
18
+ subtoken = ""
19
+ for k in range(len(s)):
20
+ if s[k] == delim and (not previous_escape):
21
+ if len(subtoken) > 0:
22
+ tokens.append(subtoken)
23
+ subtoken = "" # reset subtoken
24
+ else:
25
+ # ESCAPE DELIM -> DELIM
26
+ if previous_escape and s[k] != escape and s[k] == delim:
27
+ subtoken = subtoken[:-1] + s[k]
28
+ else:
29
+ # add to subtoken
30
+ subtoken += s[k]
31
+ # set previous_escape flag:
32
+ # If for current char is escape (True and s[k]=='\\') and previous_escape is False -> set it to True
33
+ # If for current char is escape (True and s[k]=='\\') and previous_escape is True -> case of '\\\\' -> escaping an escape character -> set it back to False
34
+ # if current char is no escape character -> set it to false
35
+ previous_escape = (not previous_escape) and (s[k] == escape)
36
+ if len(subtoken) > 0:
37
+ tokens.append(subtoken)
38
+ return tokens
39
+
40
+
41
+ def _kwarg_converter(s: str):
42
+ tokens = _escape_split(s, ",", escape="\\")
43
+ args = []
44
+ kwargs = {}
45
+ for k in tokens:
46
+ subtokens = _escape_split(k, "=", escape="\\")
47
+ if len(subtokens) == 1:
48
+ args.append(subtokens[0])
49
+ else:
50
+ kwargs[subtokens[0].strip()] = subtokens[1].strip()
51
+ return args, kwargs
52
+
53
+
54
+ def _get_log_config(configname, key=None):
55
+ config = configparser.ConfigParser(
56
+ converters={
57
+ "list": lambda x: list(x.strip("[").strip("]").split(",")),
58
+ "args_kwargs": _kwarg_converter,
59
+ }
60
+ )
61
+ # define three possible locations:
62
+ log_current_config = Path.cwd() / configname
63
+ log_home_config = Path.home() / configname
64
+
65
+ log_cfg_folder = str(Path(__file__).parent) # / configname #.with_name("config"))
66
+
67
+ log_global_config = Path(log_cfg_folder) / configname
68
+
69
+ if key == "public":
70
+ # copy command to public location
71
+ # check if command location
72
+ log_public = "C:/Users/Public/"
73
+ list_dir = os.listdir(log_public)
74
+ log_dir = log_public + "LOG_Config"
75
+ if "LOG_Config" not in list_dir:
76
+ os.mkdir(log_dir)
77
+
78
+ log_public_config = log_dir + "/" + configname
79
+
80
+ if configname not in os.listdir(log_dir):
81
+ shutil.copy(log_global_config, log_dir + "/" + configname)
82
+
83
+ config.read(log_public_config)
84
+ else:
85
+ config_read_list = [log_global_config, log_home_config, log_current_config]
86
+
87
+ # user defined takes precedence
88
+ config.read(config_read_list)
89
+
90
+ return config
91
+
92
+
93
+ CONFIG = _get_log_config("config.cfg", key="public")
@@ -0,0 +1,36 @@
1
+ """
2
+ This is debug log class for logger
3
+
4
+ Author: Yen-Chun Huang
5
+
6
+ Company: Bridge 12 Technologies. Inc
7
+ """
8
+
9
+ import logging
10
+ from .loggerConfig import *
11
+ import os
12
+
13
+
14
+ class debugLog:
15
+ def __init__(self, config_file: str = None):
16
+ config = loggerConfig(config_file)
17
+ settings = config.settings
18
+ log_dir = settings["log_folder_location"] + "/LOG/"
19
+
20
+ if not os.path.exists(log_dir):
21
+ os.mkdir(log_dir)
22
+
23
+ logpath = log_dir + "/debug_log.txt"
24
+ self.logger = logging.getLogger(__name__)
25
+ self.logger.setLevel(logging.DEBUG)
26
+ ch = logging.FileHandler(str(logpath))
27
+ ch.setLevel(logging.INFO)
28
+ ch2 = logging.StreamHandler()
29
+ ch2.setLevel(logging.DEBUG)
30
+ formatter = logging.Formatter(
31
+ "%(asctime)s - [%(filename)s:%(lineno)d] - %(levelname)s - %(message)s"
32
+ )
33
+ ch.setFormatter(formatter)
34
+ ch2.setFormatter(formatter)
35
+ self.logger.addHandler(ch)
36
+ self.logger.addHandler(ch2)