-
Notifications
You must be signed in to change notification settings - Fork 0
/
serlogger.py
332 lines (265 loc) · 10.8 KB
/
serlogger.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
__author__ = "mark-IV-II"
__version__ = "2.1.2-qt"
__name__ = "Serial Datalogger Module"
from io import TextIOWrapper
import os # For writing to temp file
import sys # To identify platform
import time # To set a wait time
import json # to write to json file
import serial # pip install pyserial
import logging # for logging to console and file
from logging import DEBUG, INFO, WARN
# from icecream import ic
from datetime import datetime # to save timestamp
from tempfile import gettempdir
class logger:
"""Serial data Logger class. Variable: log, save_dir.
Functions: find_all_ports, capture, save_capture, stop_capture, ."""
# Initialise class parameters
def __init__(self, log=True, save_dir=None):
self.mod_logger = logging.getLogger("Module logger")
self.mod_logger.setLevel(DEBUG)
fh = logging.FileHandler(
filename=f"{__name__} v{__version__}.log", mode="a"
)
fh.setLevel(WARN)
ch = logging.StreamHandler()
ch.setLevel(INFO)
formatter1 = logging.Formatter(
"%(asctime)s: %(name)s - %(levelname)s - %(message)s"
)
formatter2 = logging.Formatter("%(levelname)s - %(message)s")
# Add formatters
fh.setFormatter(formatter1)
ch.setFormatter(formatter2)
# add the handlers to the logger
self.mod_logger.addHandler(fh)
self.mod_logger.addHandler(ch)
# self.mod_logger.basicConfig(level=logging.INFO)
self.file_name = f"Log-{self._get_time(file=True)}"
self.json_warn = False
self.serial_data = None
self.out_format_select = {
"txt": self._write_to_txt,
"csv": self._write_to_csv,
"json": self._write_to_json,
}
self.file_names = {
"txt": f"{self.file_name}.txt",
"csv": f"{self.file_name}.csv",
"json": f"{self.file_name}.json",
}
self.full_file_name = ""
try:
self.dir_name = os.path.normpath(save_dir)
logfile = open(
os.path.join(self.dir_name, f"{self.file_name}.temp"), "w"
)
self.mod_logger.info(
f"File write permissions checked for: {self.dir_name}"
)
logfile.close()
os.remove(logfile.name)
self.is_temp = False
except Exception as e:
self.dir_name = os.path.normpath(gettempdir())
self.is_temp = True
self.mod_logger.warning(f"Error setting up given directory: {e}.")
self.mod_logger.warning("Using temporary directory instead")
def _get_time(self, file=False):
"""Get current time in required format"""
if file:
return datetime.now().strftime("%Y-%m-%d %H-%M-%S")
else:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def _write_to_txt(self, string, timestamp=0):
"""Write output to a plain text file"""
self.full_file_name = os.path.join(
self.dir_name, self.file_names['txt']
)
if timestamp:
string = f"{self._get_time()}: {string}"
with open(self.full_file_name, "a+") as logfile:
logfile.write(string)
self.mod_logger.info(string)
def _write_to_csv(self, string, timestamp=0):
"""Write output to a comma seperated file"""
self.full_file_name = os.path.join(
self.dir_name, self.file_names["csv"]
)
if timestamp:
string = f"{self._get_time()},{string}"
with open(self.full_file_name, "a+") as logfile:
logfile.write(string)
self.mod_logger.info(string)
def _write_to_json(self, string, timestamp=0):
"""Write output to a JSON file.
Experimental, could be slow or in wrong format in certian situations"""
self.full_file_name = os.path.join(
self.dir_name, self.file_names["json"])
log_dict = {}
if not self.json_warn:
self.mod_logger.warning(
"Saving to JSON adds timestamp regardless of timestamp flag"
)
self.json_warn = True
log_dict[self._get_time()] = string
try:
with open(self.full_file_name, "ab+") as logfile:
logfile.seek(0, 2)
if logfile.tell() == 0:
json_string = json.dumps([log_dict])
logfile.write(json_string.encode())
else:
json_string = json.dumps(log_dict, indent=8)
logfile.seek(-1, 2)
logfile.truncate()
logfile.write(" , ".encode())
logfile.write(json_string.encode())
logfile.write("]".encode())
self.mod_logger.info(json_string)
except Exception as e:
self.mod_logger.error(f"Error with json file: {e}")
def set_out_path(self, new_path: str):
"""Set the output path for saving files. Arguments - new_path"""
self.dir_name = os.path.normpath(new_path)
self.full_file_name = os.path.join(self.dir_name, self.file_name)
self.is_temp = False
def get_supported_file_formats(self) -> dict:
"""Return a dict of supported file types and their extensions"""
ext_dict = {
"Text File (.txt)": "txt",
"Comma Seperated Values (.csv)": "csv",
"JavaScript Object Notation (.json)": "json",
}
return ext_dict
def get_default_baud_rates(self) -> tuple:
"""Return a tuple of predefined baud rates"""
baud_rates = (
"300",
"1200",
"2400",
"4800",
"9600",
"19200",
"38400",
"57600",
"74880",
"115200",
"230400",
)
return baud_rates
# Function to list all available serial ports
def find_all_ports(self):
"""Return a list of port names found. OS independent in nature.
Unsupported OS raises OSError exception.
Support for Linux & MacOS is experimental"""
port_list = [] # List to return ports information
# Find all ports for Windows
if sys.platform.startswith("win"):
import serial.tools.list_ports_windows as sertoolswin
ports = sertoolswin.comports()
# Find all ports for Linux based OSes
elif sys.platform.startswith("linux") or sys.platform.startswith("cygwin"):
import serial.tools.list_ports_linux as sertoolslin
ports = sertoolslin.comports()
# Find all ports for MacOSX
elif sys.platform.startswith("darwin"):
import serial.tools.list_ports_osx as sertoolsosx
ports = sertoolsosx.comports()
# Raise exception for unsupported OS
else:
raise OSError("Unsupported Platform/OS")
# Fill the return list with information found
if ports:
self.mod_logger.info("Available ports are:")
for port, desc, hwid in sorted(ports):
port_list.append(port)
self.mod_logger.info(f"{port}: {desc} with id: {hwid}")
else:
self.mod_logger.warning(
"No serial ports detected. Please make sure the device is connected properly"
)
port_list.append("No ports found")
return port_list
# Main function that captures the data from serial port
def capture(
self,
port_name,
baud_rate=9600,
raw_mode=False,
format_ext="txt",
timestamp=False,
decoder="utf-8",
non_blocking=1,
):
"""Capture data coming through serial port of the computer.
Arguments include port name, baud rate, raw mode flag,
Boolean flag to add timestamp to files,
File formats - .txt (default), .csv, .json,
Decoder - default utf-8"""
self.mod_logger.info("Capturing")
try:
# Intialise function parameters
port = str(port_name)
baud_rate = int(baud_rate)
data = self.serial_data = serial.Serial(
port, baud_rate, timeout=0.1, rtscts=non_blocking)
while self.log:
# ic(self.full_file_name)
# print('logging') #-- can be used for debugging
if raw_mode:
line = data.read(data.in_waiting)
else:
line = data.readline()
line = line.decode(decoder)
# Skip if line is empty
if line == "":
continue
self.out_format_select[format_ext](
string=line, timestamp=timestamp)
# Catch exception, print the error and stop logging
except Exception as e:
self.mod_logger.error(f"Error: {str(e)}")
self.log = False # Set flag to false to stop logging
def save_capture(self, result_file: TextIOWrapper):
"""Save captured data to a desired file.
Parameter: result_file - full path to save file"""
try:
self.stop_capture() # Stop logging before saving file
# Copy the contents of the temp file to result file
with open(self.full_file_name, "rb") as file1:
with open(result_file, "wb") as file2:
for line in file1:
file2.write(line)
self.file_name = result_file.name
self.mod_logger.info(f"File {self.file_name} saved")
# Open a new temp file since old one is closed
self.file_name = f"Log-{self._get_time(file=True)}.txt"
except Exception as error:
self.mod_logger.error(
f"{error}. Temp file name: {self.full_file_name}. Output file name: {result_file}"
)
def stop_capture(self):
"""Stop execution of serial logger. Sets internal log flag to False"""
self.mod_logger.info("Stopping. All data while paused is not logged")
self.log = False # Set flag to false to stop logging
time.sleep(1)
try:
if self.serial_data.is_open:
self.serial_data.close()
except Exception as err:
self.mod_logger.error(
f"Error while saving file at stop: {str(err)}")
def new_file(self):
"""Create a new file object with new file name"""
self.stop_capture()
self.file_name = f'Log-{self._get_time(file=True)}'
self.file_names = {
'txt': f'{self.file_name}.txt',
'csv': f'{self.file_name}.csv',
'json': f'{self.file_name}.json'
}
self.mod_logger.info(
"New file name generated. Start capturing to save to new file"
)