forked from zietzm/Helmholtz_Test_Bench
added data logging functionality
This commit is contained in:
@@ -102,3 +102,4 @@ ENV/
|
||||
.idea/misc.xml
|
||||
config.ini
|
||||
*.ini
|
||||
log.csv
|
||||
|
||||
+87
-4
@@ -9,11 +9,13 @@ import numpy as np
|
||||
import os
|
||||
from os.path import exists
|
||||
import threading
|
||||
from datetime import datetime
|
||||
|
||||
import globals as g
|
||||
import cage_func as func
|
||||
import csv_threading as csv
|
||||
import config_handling as config
|
||||
import csv_logging as log
|
||||
|
||||
NORM_FONT = ()
|
||||
HEADER_FONT = ("Arial", 13, "bold")
|
||||
@@ -40,7 +42,7 @@ class HelmholtzGUI(Tk):
|
||||
|
||||
self.pages = {} # dictionary for storing all pages
|
||||
|
||||
for P in [ManualMode, Configuration, ExecuteCSVMode]:
|
||||
for P in [ManualMode, Configuration, ExecuteCSVMode, ConfigureLogging]:
|
||||
page = P(mainArea, self)
|
||||
self.pages[P] = page
|
||||
page.grid(row=0, column=0, sticky="nsew")
|
||||
@@ -73,6 +75,8 @@ class TopMenu:
|
||||
menu.add_cascade(label="Mode", menu=ModeSelector)
|
||||
ModeSelector.add_command(label="Static Manual Input", command=lambda: self.manual_mode(window))
|
||||
ModeSelector.add_command(label="Execute CSV Sequence", command=lambda: self.execute_csv_mode(window))
|
||||
ModeSelector.add_separator()
|
||||
ModeSelector.add_command(label="Configure Data Logging", command=lambda: self.logging(window))
|
||||
ModeSelector.add_command(label="Settings...", command=lambda: self.configuration(window))
|
||||
|
||||
@staticmethod
|
||||
@@ -87,6 +91,10 @@ class TopMenu:
|
||||
def execute_csv_mode(window):
|
||||
window.show_frame(ExecuteCSVMode)
|
||||
|
||||
@staticmethod
|
||||
def logging(window):
|
||||
window.show_frame(ConfigureLogging)
|
||||
|
||||
|
||||
class ManualMode(Frame):
|
||||
|
||||
@@ -598,7 +606,7 @@ class ExecuteCSVMode(Frame):
|
||||
pass
|
||||
|
||||
def load_csv(self): # load in csv file to be executed
|
||||
directory = os.path.abspath(os.getcwd()) # get directory of current config file
|
||||
directory = os.path.abspath(os.getcwd()) # get project directory
|
||||
# open file selection dialogue and save path of selected file
|
||||
filename = filedialog.askopenfilename(initialdir=directory, title="Select CSV File",
|
||||
filetypes=(("Comma Separated Values", "*.csv*"), ("All Files", "*.*")))
|
||||
@@ -662,11 +670,86 @@ class ConfigureLogging(Frame):
|
||||
self.parent = parent
|
||||
self.controller = controller # object on which mainloop() is running, usually main window
|
||||
|
||||
self.log_file = None # string containing path of log file
|
||||
self.regular_logging = False # True if data should be logged regularly
|
||||
|
||||
self.grid_rowconfigure(ALL, weight=1)
|
||||
self.grid_columnconfigure(ALL, weight=1)
|
||||
|
||||
row_counter = 0
|
||||
|
||||
# Create and place buttons
|
||||
# Setup frame to house buttons:
|
||||
self.top_buttons_frame = Frame(self)
|
||||
self.top_buttons_frame.grid_rowconfigure(ALL, weight=1)
|
||||
self.top_buttons_frame.grid_columnconfigure(ALL, weight=1)
|
||||
self.top_buttons_frame.grid(row=row_counter, column=0, sticky=W, padx=20)
|
||||
|
||||
self.stop_logging_button = Button(self.top_buttons_frame, text="Stop Logging", command=self.stop_logging,
|
||||
pady=5, padx=5, font=SMALL_BUTTON_FONT)
|
||||
self.stop_logging_button.grid(row=0, column=0, padx=5)
|
||||
self.start_logging_button = Button(self.top_buttons_frame, text="Start Logging", command=self.start_logging,
|
||||
pady=5, padx=5, font=SMALL_BUTTON_FONT)
|
||||
self.start_logging_button.grid(row=0, column=0, padx=5)
|
||||
self.write_to_file_button = Button(self.top_buttons_frame, text="Write data to file", font=SMALL_BUTTON_FONT,
|
||||
command=self.write_to_file, pady=5, padx=5, state="disabled")
|
||||
self.write_to_file_button.grid(row=0, column=1, padx=5)
|
||||
|
||||
row_counter += 1
|
||||
|
||||
# Create checkboxes to select what data to log
|
||||
self.checkbox_frame = Frame(self)
|
||||
self.checkbox_frame.grid_rowconfigure(ALL, weight=1)
|
||||
self.checkbox_frame.grid_columnconfigure(ALL, weight=1)
|
||||
self.checkbox_frame.grid(row=row_counter, column=0, sticky=W, padx=10, pady=10)
|
||||
|
||||
self.checkbox_vars = {} # dictionary containing the bool variables changed by the checkboxes
|
||||
self.active_keys = [] # list with all the keys relating to the currently ticked checkboxes
|
||||
|
||||
# generate and place all the checkboxes:
|
||||
row = 0
|
||||
for key in log.axis_data_dict.keys():
|
||||
self.checkbox_vars[key] = BooleanVar(value=True) # create variable for checkbox and put it in dictionary
|
||||
checkbox = Checkbutton(self.checkbox_frame, text=key, # generate checkbox
|
||||
variable=self.checkbox_vars[key], onvalue=True, offvalue=False)
|
||||
checkbox.grid(row=row, column=0, sticky=W) # place checkbox in UI
|
||||
row += 1
|
||||
|
||||
def page_switch(self): # function that is called when switching to this window
|
||||
# every class in the UI needs this, even if it doesn't do anything
|
||||
pass
|
||||
|
||||
def start_logging(self):
|
||||
ui_print("Started data logging.")
|
||||
self.update_choices() # update list with ticked checkboxes
|
||||
self.regular_logging = True
|
||||
log.zero_time = datetime.now()
|
||||
self.periodic_log(1000) # ToDo: get interval from entry field
|
||||
self.write_to_file_button["state"] = "disabled"
|
||||
self.stop_logging_button.tkraise() # switch button to stop
|
||||
|
||||
def stop_logging(self):
|
||||
ui_print("Stopped data logging. Remember to save data to file!")
|
||||
self.regular_logging = False
|
||||
self.write_to_file_button["state"] = "normal"
|
||||
self.start_logging_button.tkraise() # switch button to start
|
||||
|
||||
@staticmethod
|
||||
def write_to_file():
|
||||
filepath = log.select_file() # select a file to write to
|
||||
log.write_to_file(log.log_data, filepath) # write logged data to the file
|
||||
|
||||
def update_choices(self):
|
||||
self.active_keys = []
|
||||
for key in self.checkbox_vars.keys():
|
||||
if self.checkbox_vars[key].get(): # box is ticked
|
||||
self.active_keys.append(key)
|
||||
|
||||
def periodic_log(self, interval): # logs data in regular intervals (ms)
|
||||
if self.regular_logging: # logging in intervals is active
|
||||
log.log_datapoint(self.active_keys) # add datapoint with active keys to log data frame
|
||||
self.controller.after(interval, lambda: self.periodic_log(interval)) # call function again after interval
|
||||
|
||||
|
||||
class StatusDisplay(Frame):
|
||||
|
||||
@@ -716,7 +799,7 @@ class StatusDisplay(Frame):
|
||||
|
||||
self.update_labels()
|
||||
|
||||
def continuous_label_update(self, controller, interval): # update display values in regular intervals
|
||||
def continuous_label_update(self, controller, interval): # update display values in regular intervals (ms)
|
||||
self.update_labels()
|
||||
if g.app is not None: # app ist still running
|
||||
# ToDo (optional): prevent call after program close
|
||||
@@ -766,7 +849,7 @@ class OutputConsole(Frame): # console to print stuff in, similar to standard py
|
||||
def ui_print(*content): # prints text to built in console
|
||||
output = ""
|
||||
for text in content:
|
||||
output = " ".join((output, str(text))) # append content
|
||||
output = " ".join((output, str(text))) # merge all contents into one string
|
||||
if not g.exitFlag:
|
||||
output = "".join(("\n", output)) # begin new line each time
|
||||
g.app.OutputConsole.console.insert(END, output) # print to console
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# This file contains functions related to logging data from the program to a CSV file
|
||||
# They are mainly but not only called by the ConfigureLogging class in User_Interface.py
|
||||
|
||||
import pandas as pd
|
||||
import globals as g
|
||||
from datetime import datetime
|
||||
import os
|
||||
from tkinter import filedialog
|
||||
from tkinter import messagebox
|
||||
import User_Interface as ui
|
||||
|
||||
log_data = pd.DataFrame() # pandas data frame containing logged data
|
||||
logging = False # Bool to indicate if data should be logged at the moment
|
||||
unsaved_data = False # Bool to indicate if there is unsaved data, set to True each time a datapoint is logged
|
||||
zero_time = datetime.now()
|
||||
|
||||
# create dictionary with all value handles that could be logged
|
||||
# Key: String that is displayed in UI and column headers. Also serves as handle to access dictionary elements.
|
||||
# Keys are the same as the rows in the status display ToDo (optional): use this for the status display
|
||||
# Content: name of the corresponding attribute in the Axis class (in cage_func.py).
|
||||
# Important: attribute handle must match definition in Axis class exactly, used with axis.getattr() to get values.
|
||||
axis_data_dict = {
|
||||
'PSU Status': 'connected',
|
||||
'Voltage Setpoint': 'voltage_setpoint',
|
||||
'Actual Voltage': 'voltage',
|
||||
'Current Setpoint': 'current_setpoint',
|
||||
'Actual Current': 'current',
|
||||
'Target Field': 'target_field_comp',
|
||||
'Trgt. Field Raw': 'target_field_comp',
|
||||
'Target Current': 'target_current',
|
||||
'Inverted': 'polarity_switched'
|
||||
}
|
||||
|
||||
|
||||
def triple_list(key_list): # creates list with each entry of key_list tripled with axis names before it
|
||||
new_list = [] # initialize list
|
||||
for key in key_list: # go through the given list
|
||||
for axis_name in ['X', 'Y', 'Z']: # per given list entry create three, one for each axis
|
||||
new_list.append(' '.join((axis_name, key))) # put axis_name before the given entry and append to new list
|
||||
return new_list
|
||||
|
||||
|
||||
def init_log_dataframe(key_list): # probably not needed, ToDo: remove
|
||||
global log_data
|
||||
columns = triple_list(key_list)
|
||||
log_data = pd.DataFrame(columns=columns)
|
||||
|
||||
|
||||
def log_datapoint(key_list): # ToDo: comments
|
||||
global log_data
|
||||
global unsaved_data
|
||||
date = datetime.now().date()
|
||||
time = datetime.now().strftime("%H:%M:%S,%f")
|
||||
t = (datetime.now() - zero_time).total_seconds()
|
||||
data = [[date, time, t]]
|
||||
for key in key_list:
|
||||
for axis in g.AXES:
|
||||
data[0].append(getattr(axis, axis_data_dict[key])) # get value
|
||||
column_names = ["Date", "Time", "t (s)", *triple_list(key_list)]
|
||||
new_row = pd.DataFrame(data, columns=column_names)
|
||||
log_data = log_data.append(new_row, ignore_index=True)
|
||||
unsaved_data = True
|
||||
|
||||
|
||||
def select_file(): # select a file to write logs to
|
||||
directory = os.path.abspath(os.getcwd()) # get project directory
|
||||
# open file selection dialogue and save path of selected file
|
||||
filepath = filedialog.asksaveasfilename(initialdir=directory, title="Set log file",
|
||||
filetypes=([("Comma Separated Values", "*.csv*")]),
|
||||
defaultextension=[("Comma Separated Values", "*.csv*")])
|
||||
|
||||
if filepath == '': # this happens when file selection window is closed without selecting a file
|
||||
ui.ui_print("No file selected, can not save logged data.")
|
||||
return None
|
||||
else: # a valid file name was entered
|
||||
return filepath
|
||||
|
||||
|
||||
def write_to_file(dataframe, filepath):
|
||||
# get global variables for use in this function:
|
||||
global unsaved_data
|
||||
if filepath is not None: # user has selected a file and no errors occurred
|
||||
ui.ui_print("Writing logged data to file", filepath)
|
||||
try:
|
||||
# write data collected in log_data DataFrame to csv file in german excel format:
|
||||
dataframe.to_csv(filepath, index=False, sep=';', decimal=',')
|
||||
except PermissionError:
|
||||
message = "No permission to write to: \n%s. \nFile may be open in another program." % filepath
|
||||
messagebox.showerror("Permission Error", message)
|
||||
except BaseException as e:
|
||||
message = "Error while trying to write to file \n%s.\n%s" % (filepath, e)
|
||||
messagebox.showerror("Error!", message)
|
||||
else: # no exceptions occurred
|
||||
unsaved_data = False # data has been saved, so no unsaved data remains
|
||||
|
||||
|
||||
def clear_logged_data(): # clears all logged data from data frame
|
||||
global log_data # get global variable
|
||||
log_data = pd.DataFrame() # reset to an empty data frame, i.e. clear all logged data
|
||||
+2
-1
@@ -74,7 +74,8 @@ class ExecCSVThread(Thread):
|
||||
time.sleep(delay)
|
||||
|
||||
# check again if everything is connected before starting next loop run:
|
||||
all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), parent.arduino_override.get())
|
||||
all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(),
|
||||
parent.arduino_override.get())
|
||||
|
||||
if not self.stopped() and not g.exitFlag and all_connected: # sequence ended without interruption
|
||||
ui.ui_print("Sequence executed, powering down channels.")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from os.path import exists
|
||||
import traceback
|
||||
from tkinter import messagebox
|
||||
import time
|
||||
|
||||
import cage_func as func
|
||||
from User_Interface import HelmholtzGUI
|
||||
@@ -9,18 +8,23 @@ from User_Interface import ui_print
|
||||
import User_Interface as ui
|
||||
import globals as g
|
||||
import config_handling as config
|
||||
import csv_logging as log
|
||||
|
||||
|
||||
def program_end():
|
||||
def program_end(): # called on exception or when user closes application
|
||||
g.exitFlag = True # tell everything else the application has been closed
|
||||
if g.app is not None:
|
||||
if g.app.pages[ui.ExecuteCSVMode].csv_thread is not None: # end possible csv execution thread
|
||||
g.app.pages[ui.ExecuteCSVMode].csv_thread.stop() # stop thread
|
||||
# g.app.pages[ui.ExecuteCSVMode].csv_thread.join() # wait for thread to finish
|
||||
# ToDo: figure out why this doesn't work with join()
|
||||
time.sleep(0.2) # give the thread time to finish, workaround to avoid join()
|
||||
# g.app = None # reset to None so nothing tries to print in the UI output
|
||||
func.shut_down_all() # shut down devices
|
||||
|
||||
if log.unsaved_data: # There is logged data that has not been saved yet
|
||||
# open pop-up to ask user if he wants to save the data:
|
||||
save_log = messagebox.askquestion("Save log data?", "There seems to be unsaved logging data. "
|
||||
"Do you wish to write it to a file now?")
|
||||
if save_log == 'yes': # user has chosen yes
|
||||
filepath = log.select_file() # let user select a file to write to
|
||||
log.write_to_file(log.log_data, filepath) # write the data to the chosen file
|
||||
if g.app is not None:
|
||||
g.app.destroy() # close application
|
||||
|
||||
@@ -51,7 +55,7 @@ try: # start normal operations
|
||||
ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once
|
||||
func.setup_all() # initiate communication, set handles
|
||||
|
||||
g.app.protocol("WM_DELETE_WINDOW", program_end) # call program end function if user closes the application
|
||||
g.app.protocol("WM_DELETE_WINDOW", program_end) # call program_end function if user closes the application
|
||||
|
||||
g.app.mainloop()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user