# This file contains classes to build all elements of the graphical user interface. # These classes also contain the methods that are executed when UI elements are activated. from tkinter import * from tkinter import ttk from tkinter import messagebox from tkinter import filedialog from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg 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 # define font styles: HEADER_FONT = ("Arial", 13, "bold") SUB_HEADER_FONT = ("Arial", 9, "bold") BIG_BUTTON_FONT = ("Arial", 11, "bold") SMALL_BUTTON_FONT = ("Arial", 9) class HelmholtzGUI(Tk): # main application window, almost everything else here es called from this class # Inherited base class: Tk(), main application window class def __init__(self): Tk.__init__(self) Tk.wm_title(self, "Helmholtz Cage Control") # set title of the window Tk.wm_iconbitmap(self, "Helmholtz.ico") # set application icon self.Menu = TopMenu(self) # display dropdown menu bar at the top (see TopMenu class for details) mainArea = Frame(self, padx=10, pady=10) # create main area Frame where controls of each mode are displayed mainArea.pack(side="top", fill="both", expand=True) # pack main area at the top of the window mainArea.grid_rowconfigure(0, weight=1) # configure rows and columns of the Tkinter grid to expand with window mainArea.grid_columnconfigure(0, weight=1) # initialize the GUI pages for the different modes and setup switching between them # see https://pythonprogramming.net/change-show-new-frame-tkinter/ for explanation # switching between pages is done with show_frame() method self.pages = {} # dictionary for storing all pages (different modes, displayed in main area) for P in [ManualMode, Configuration, ExecuteCSVMode, ConfigureLogging]: # do this for every mode page page = P(mainArea, self) # initialize the page with the mainArea frame as the parent self.pages[P] = page # add the page to the dictionary page.grid(row=0, column=0, sticky="nsew") # place all pages in the same place in the GUI # setup status display and output console status_frame = Frame(self) # create frame to house them status_frame.pack(side="bottom", fill="x", expand=False) # place at bottom of main window, expand to full width status_frame.grid_columnconfigure(1, weight=1) # make column 1, (output console), expand to fill full width # initialize and place status display: self.StatusDisplay = StatusDisplay(status_frame, self) self.StatusDisplay.grid(row=0, column=0, sticky="nesw") # initialize and place output console: self.OutputConsole = OutputConsole(status_frame) self.OutputConsole.grid(row=0, column=1, sticky="nesw") self.show_frame(ManualMode) # show manual mode to start with def show_frame(self, key): # method to switch between pages in the main area frame = self.pages[key] # get correct page from the dictionary frame.page_switch() # update displays in this page with window-specific update function frame.tkraise() # bring this frame to the front class TopMenu: # the menu bar at the top of the window def __init__(self, window): menu = Menu(window) # initialize Menu object window.config(menu=menu) # put menu at the top of the window ModeSelector = Menu(menu) # create a submenu object menu.add_cascade(label="Mode", menu=ModeSelector) # add a dropdown with the submenu object # create the different options in the dropdown: 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 def manual_mode(window): # switch to the manual mode page window.show_frame(ManualMode) @staticmethod def configuration(window): # switch to the settings page window.show_frame(Configuration) @staticmethod def execute_csv_mode(window): # switch to the CSV execution page window.show_frame(ExecuteCSVMode) @staticmethod def logging(window): # switch to the logging settings page window.show_frame(ConfigureLogging) class ManualMode(Frame): # Mode for manually setting currents and fields on the test stand. # Inherits the Frame object from Tkinter and is placed in the mainArea of the application window. def __init__(self, parent, controller): Frame.__init__(self, parent) # initialize the frame object self.controller = controller # object on which mainloop() is running, usually main window self.grid_rowconfigure(ALL, weight=1) # configure rows and columns of the Tkinter grid to expand with window self.grid_columnconfigure(ALL, weight=1) row_counter = 0 # keep track of which row in the main grid we are in # setup title text header = Label(self, text="Manual Input Mode", font=HEADER_FONT, pady=3) header.grid(row=row_counter, column=0) row_counter += 1 # Setup Dropdown Menu for input mode dropdown_frame = Frame(self) # create frame to house dropdown dropdown_frame.grid_rowconfigure(ALL, weight=1) dropdown_frame.grid_columnconfigure(ALL, weight=1) dropdown_frame.grid(row=row_counter, column=0) # place frame on the page self.input_mode = StringVar() # variable that is changed by the dropdown selection # make dictionary with information on all modes. # content: [function to call on button press, unit text to be displayed, function to call on dropdown change] self.modes = {"Magnetic Field": [self.execute_field, "\u03BCT", self.switch_to_field_mode], "Current": [self.execute_current, "A", self.switch_to_current_mode]} self.unit = StringVar() # variable to store the unit of the current mode, used to update a label with the unit default_mode = list(self.modes.keys())[0] # setup which mode to show at the beginning # create the dropdown. parameters: (frame to place it in, variable changed, starting selection, all options) input_mode_selector = ttk.OptionMenu(dropdown_frame, self.input_mode, default_mode, *self.modes.keys()) input_mode_selector.grid(row=0, column=1, sticky=W) # place dropdown on the grid dropdown_frame.grid_columnconfigure(1, minsize=115) # set size of column with dropdown to keep it from moving # Add a description before the dropdown: selector_label = Label(dropdown_frame, text="Select Input Mode:", padx=10, pady=10) selector_label.grid(row=0, column=0) row_counter = row_counter + 1 # Setup Entry fields for field/current values # create and configure frame to house fields: self.entries_frame = Frame(self) self.entries_frame.grid_rowconfigure(ALL, weight=1) self.entries_frame.grid_columnconfigure(ALL, weight=1) self.entries_frame.grid_columnconfigure(2, weight=1, minsize=20) # set column width so it doesn't move around self.entries_frame.grid_columnconfigure(3, weight=1, minsize=110) self.entries_frame.grid(row=row_counter, column=0) entry_texts = ["X-Axis:", "Y-Axis:", "Z-Axis:"] # row labels self.entry_vars = [DoubleVar() for _ in range(3)] # variables that are changed by entries into the fields self.max_value_vars = [StringVar() for _ in range(3)] # variables for labels showing the min/max values # Build up the entry field table: row = 0 for text in entry_texts: # go through x,y,z axis rows field = ttk.Entry(self.entries_frame, textvariable=self.entry_vars[row]) # create entry field self.entry_vars[row].set(0) # set its value to 0 to start field.grid(row=row, column=1, sticky=W) axis_label = Label(self.entries_frame, text=text, padx=5, pady=10) # create label showing the axis axis_label.grid(row=row, column=0, sticky=W) unit_label = Label(self.entries_frame, textvariable=self.unit) # create updatable label showing the unit unit_label.grid(row=row, column=2, sticky=W) # create updatable label showing the min/max achievable values: max_value_label = Label(self.entries_frame, textvariable=self.max_value_vars[row]) max_value_label.grid(row=row, column=3, sticky=W) row = row + 1 row_counter += 1 # setup checkbox for compensating ambient field checkbox_frame = Frame(self, padx=20) # create frame to house it checkbox_frame.grid(row=row_counter, column=0, sticky=W) self.compensate = BooleanVar(value=True) # create variable to be changed by the checkbox # create checkbox: self.compensate_checkbox = Checkbutton(checkbox_frame, text="Compensate ambient field", variable=self.compensate, onvalue=True, offvalue=False) self.compensate_checkbox.pack(side="left") row_counter += 1 # Setup buttons self.buttons_frame = Frame(self) self.buttons_frame.grid_rowconfigure(ALL, weight=1) self.buttons_frame.grid_columnconfigure(ALL, weight=1) self.buttons_frame.grid(row=row_counter, column=0) Label(self.buttons_frame, text="").grid(row=0, column=0) # add spacer # add button for executing the current entries execute_button = Button(self.buttons_frame, text="Execute!", command=self.execute, pady=5, padx=5, font=BIG_BUTTON_FONT) execute_button.grid(row=row_counter, column=0, padx=5) # add button for quick power_down power_down_button = Button(self.buttons_frame, text="Power Down All", command=self.power_down, pady=5, padx=5, font=BIG_BUTTON_FONT) power_down_button.grid(row=row_counter, column=1, padx=5) # add button for reinitialization of devices reinit_button = Button(self.buttons_frame, text="Reinitialize", command=self.reinitialize, pady=5, padx=5, font=BIG_BUTTON_FONT) reinit_button.grid(row=row_counter, column=2, padx=5) row_counter = row_counter + 1 Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer self.input_mode.trace_add('write', self.change_mode_callback) # call mode change function on dropdown change self.input_mode.set(default_mode) # call up default mode at the start self.compensate.trace_add('write', self.change_mode_callback) # call mode change function on checkbox change def page_switch(self): # function that is called when switching to this page in the UI self.modes[self.input_mode.get()][2]() # update max values and units, e.g. calls update_max_fields function # noinspection PyUnusedLocal # not sure what the parameters are for, but it doesn't work without them def change_mode_callback(self, var, index, mode): # called whenever input mode dropdown or checkbox is changed self.unit.set(self.modes[self.input_mode.get()][1]) # change unit text self.modes[self.input_mode.get()][2]() # update max values, e.g. calls update_max_fields function def switch_to_field_mode(self): # called when switching to magnetic field entry mode self.compensate_checkbox.config(state=NORMAL) # enable the compensate ambient field checkbox # update the labels showing the min/max achievable values compensate = self.compensate.get() # read out if compensate field checkbox is checked (True or False) i = 0 for var in self.max_value_vars: # go through the max value labels for each axis if not compensate: # ambient field should not be compensated field = g.AXES[i].max_field * 1e6 # get max values from the axis object elif compensate: # ambient field should be compensated field = g.AXES[i].max_comp_field * 1e6 else: # this really should never happen field = [0, 0] ui_print("Unexpected value encountered: compensate =", compensate) messagebox.showerror("Unexpected Value!", ("Unexpected value encountered: compensate =", compensate)) var.set("(%0.1f to %0.1f \u03BCT)" % (field[0], field[1])) # update the label text with the new values i += 1 def switch_to_current_mode(self): # called when switching to the input current mode self.compensate_checkbox.config(state=DISABLED) # disable the compensate ambient field checkbox # update the labels showing the min/max achievable values i = 0 for var in self.max_value_vars: # go through the max value labels for each axis var.set("(%0.2f to %0.2f A)" % (-g.AXES[i].max_amps, g.AXES[i].max_amps)) # update the label i += 1 def reinitialize(self): # called on "Reinitialize!" button press func.setup_all() # reinitialize all PSUs and the Arduino # log change to the log file if user has selected event logging in the Configure Logging window logger = self.controller.pages[ConfigureLogging] # get object of logging configurator if logger.event_logging: # data should be logged when test stand is commanded logger.log_datapoint() # log data def power_down(self): # called on "power down" button press func.power_down_all() # power down outputs on all PSUs and the Arduino # log change to the log file if user has selected event logging in the Configure Logging window logger = self.controller.pages[ConfigureLogging] # get object of logging configurator if logger.event_logging: # data should be logged when test stand is commanded logger.log_datapoint() # log data def execute(self): # called on "Execute!" button press # reads values from the entry fields and commands the test stand accordingly vector = np.array([0, 0, 0], dtype=float) # initialize vector to later send to test stand i = 0 try: # try to read values from the entry fields for var in self.entry_vars: vector[i] = var.get() # write read out value to correct position in the vector i += 1 except TclError as e: # user did not enter correct format somewhere messagebox.showwarning("Invalid Entry", "Invalid entry:\n%s" % e) # show warning message else: # no issues while reading entries (user entered correct format) function_to_call = self.modes[self.input_mode.get()][0] # get function of appropriate mode function_to_call(vector) # call function (self.execute_field() or self.execute_current()) self.controller.StatusDisplay.update_labels() # update status display after change # log change to the log file if user has selected event logging in the Configure Logging window logger = self.controller.pages[ConfigureLogging] # get object of logging configurator if logger.event_logging: # data should be logged when test stand is commanded logger.log_datapoint() # log data def execute_field(self, vector): # convert magnetic field vector and send to test stand ui_print("Field executing:", vector, "\u03BCT") compensate = self.compensate.get() # read out if compensate ambient field checkbox is ticked if compensate: # ambient field should be compensated func.set_field(vector * 1e-6) # convert to Tesla and send to test stand elif not compensate: # ambient field should not be compensated func.set_field_simple(vector * 1e-6) # convert to Tesla and send to test stand else: # this really should never happen ui_print("Unexpected value encountered: compensate =", compensate) messagebox.showerror("Unexpected Value!", ("Unexpected value encountered: compensate =", compensate)) @staticmethod def execute_current(vector): # send current vector to the test stand ui_print("Current executing:", vector, "A") func.set_current_vec(vector) # command test stand class ExecuteCSVMode(Frame): # generate configuration window to set program constants def __init__(self, parent, controller): Frame.__init__(self, parent) self.parent = parent self.controller = controller # object on which mainloop() is running, usually main window # Functional init: self.csv_thread = None # the thread object for executing csv self.sequence_array = None # array containing the values from the csv file # Build UI: self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(ALL, weight=1) row_counter = 0 self.row_elements = [] # make list of elements in rows to calculate height available for plot # setup heading header = Label(self, text="Execute CSV Mode", font=HEADER_FONT, pady=3) header.grid(row=row_counter, column=0, padx=100, sticky=W) self.row_elements.append(header) row_counter += 1 # Setup 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.row_elements.append(self.top_buttons_frame) # Create and place buttons self.select_file_button = Button(self.top_buttons_frame, text="Select csv file...", command=self.load_csv, pady=5, padx=5, font=SMALL_BUTTON_FONT) self.select_file_button.grid(row=0, column=0, padx=5) self.execute_button = Button(self.top_buttons_frame, text="Run Sequence", command=self.run_sequence, pady=5, padx=5, font=SMALL_BUTTON_FONT, state="disabled") self.execute_button.grid(row=0, column=1, padx=5) self.stop_button = Button(self.top_buttons_frame, text="Stop Run", command=self.stop_run, pady=5, padx=5, font=SMALL_BUTTON_FONT, state="disabled") self.stop_button.grid(row=0, column=2, padx=5) # add button for reinitialization self.reinit_button = Button(self.top_buttons_frame, text="Reinitialize Devices", command=self.reinitialize, pady=5, padx=5, font=SMALL_BUTTON_FONT) self.reinit_button.grid(row=0, column=3, padx=5) row_counter += 1 # setup testing checkboxes 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=20) self.row_elements.append(self.checkbox_frame) checkbox_label = Label(self.checkbox_frame, text="Disable device connection checks:") checkbox_label.grid(row=0, column=0, sticky=W, padx=3) self.xy_override = BooleanVar(value=False) self.z_override = BooleanVar(value=False) self.arduino_override = BooleanVar(value=False) xy_checkbox = Checkbutton(self.checkbox_frame, text="XY PSU", variable=self.xy_override, onvalue=True, offvalue=False) xy_checkbox.grid(row=0, column=1, padx=3) z_checkbox = Checkbutton(self.checkbox_frame, text="Z PSU", variable=self.z_override, onvalue=True, offvalue=False) z_checkbox.grid(row=0, column=2, padx=3) arduino_checkbox = Checkbutton(self.checkbox_frame, text="Arduino", variable=self.arduino_override, onvalue=True, offvalue=False) arduino_checkbox.grid(row=0, column=3, padx=3) row_counter += 1 # make frame for plot of csv values self.plotFrame = Frame(self) self.plotFrame.grid_rowconfigure(0, weight=1) self.plotFrame.grid_columnconfigure(0, weight=1) self.plotFrame.grid(row=row_counter, column=0, sticky="nsw", padx=10, pady=10) 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 load_csv(self): # load in csv file to be executed 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", "*.*"))) if exists(filename): # does the file exist? ui_print("File selected:", filename) try: self.sequence_array = csv.read_csv_to_array(filename) # read array from csv except BaseException as e: ui_print("Error while opening file:", e) messagebox.showerror("Error!", "Error while opening file: \n%s" % e) try: csv.check_array_ok(self.sequence_array) # check for values exceeding limits self.display_plot() # plot data and display except BaseException as e: ui_print("Error while processing data from file:", e) messagebox.showerror("Error!", "Error while processing data from file: \n%s" % e) else: self.execute_button["state"] = "normal" # activate run button elif filename == '': # this happens when file selection window is closed without selecting a file ui_print("No file selected, could not load.") else: ui_print("Selected file", filename, "does not exist, could not load.") def run_sequence(self): # (de)activate buttons as needed: self.select_file_button["state"] = "disabled" self.execute_button["state"] = "disabled" self.stop_button["state"] = "normal" self.reinit_button["state"] = "disabled" g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later # create separate thread to run sequence execution in: self.csv_thread = csv.ExecCSVThread(self.sequence_array, self, self.controller) self.csv_thread.start() # start thread def stop_run(self): self.csv_thread.stop() # this will cause the csv loop to end # (de)activate buttons as needed: self.select_file_button["state"] = "normal" self.execute_button["state"] = "normal" self.stop_button["state"] = "disabled" self.reinit_button["state"] = "normal" # log change to the log file if user has selected event logging in the Configure Logging window logger = self.controller.pages[ConfigureLogging] # get object of logging configurator if logger.event_logging: # data should be logged when test stand is commanded logger.log_datapoint() # log data def reinitialize(self): # called on "Reinitialize devices" button press func.setup_all() # reinitialize all PSUs and the Arduino # log change to the log file if user has selected event logging in the Configure Logging window logger = self.controller.pages[ConfigureLogging] # get object of logging configurator if logger.event_logging: # data should be logged when test stand is commanded logger.log_datapoint() # log data def display_plot(self): # calculate available height for plot (in pixels): height_others = 0 for element in self.row_elements: # go through all rows in the widget except the plot frame height_others += element.winfo_height() # add up heights height = self.parent.winfo_height() - height_others - 50 # set to height of parent frame - other rows - margin width = min(self.parent.winfo_width() - 100, 1100) # set width to available space but max. 1100 figure = csv.plot_field_sequence(self.sequence_array, width, height) # create figure to be displayed plotCanvas = FigureCanvasTkAgg(figure, self.plotFrame) # create canvas to draw figure on plotCanvas.draw() # equivalent to matplotlib.show() plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in UI class Configuration(Frame): # generate configuration window to set program constants def __init__(self, parent, controller): Frame.__init__(self, parent) self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(ALL, weight=1) row_counter = 0 header = Label(self, text="Configuration Window", font=HEADER_FONT, pady=3) header.grid(row=row_counter, column=0, padx=100, sticky=W) row_counter += 1 # Setup buttons to select config file # Setup frame to house buttons: self.file_select_frame = Frame(self) self.file_select_frame.grid_rowconfigure(ALL, weight=1) self.file_select_frame.grid_columnconfigure(ALL, weight=1) self.file_select_frame.grid(row=row_counter, column=0, sticky=W, padx=20) # Create and place buttons load_file_button = Button(self.file_select_frame, text="Load config file...", command=self.load_config, pady=5, padx=5, font=SMALL_BUTTON_FONT) load_file_button.grid(row=0, column=0, padx=5) save_button = Button(self.file_select_frame, text="Save current config", command=self.save_config, pady=5, padx=5, font=SMALL_BUTTON_FONT) save_button.grid(row=0, column=1, padx=5) save_as_button = Button(self.file_select_frame, text="Save current config as...", command=self.save_config_as, pady=5, padx=5, font=SMALL_BUTTON_FONT) save_as_button.grid(row=0, column=2, padx=5) row_counter += 1 # Serial port settings frame: port_frame = Frame(self) port_frame.grid_rowconfigure(ALL, weight=1) port_frame.grid_columnconfigure(ALL, weight=1) port_frame.grid(row=row_counter, column=0, sticky=W) entry_texts = ["XY PSU Serial Port:", "Z PSU Serial Port:"] self.XY_port = StringVar(value=g.XY_PORT) # create variables to store the port names and set to current names self.Z_port = StringVar(value=g.Z_PORT) port_vars = [self.XY_port, self.Z_port] row = 0 for text in entry_texts: field = Entry(port_frame, textvariable=port_vars[row]) # create entry field field.grid(row=row, column=1, sticky=W) axis_label = Label(port_frame, text=text, padx=5, pady=10) axis_label.grid(row=row, column=0, sticky=W) info_label = Label(port_frame, text="e.g. COM10") info_label.grid(row=row, column=2, sticky=W) row += 1 row_counter += 1 Label(self, text="", pady=0).grid(row=row_counter, column=0) # add spacer row_counter += 1 value_frame = Frame(self) value_frame.grid_rowconfigure(ALL, weight=1) value_frame.grid_columnconfigure(ALL, weight=1) value_frame.grid(row=row_counter, column=0) # Setup dictionary to generate entry table from # {Key: [[x-value,y-value,z-value], unit, description, config file key, unit conversion factor]} self.entries = { "Coil Constants:": [[DoubleVar() for _ in range(3)], "\u03BCT/A", "", "coil_const", 1e6], "Ambient Field:": [[DoubleVar() for _ in range(3)], "\u03BCT", "Field to be compensated", "ambient_field", 1e6], "Resistances:": [[DoubleVar() for _ in range(3)], "\u03A9", "Resistance of coils + equipment", "resistance", 1], "Max. Current:": [[DoubleVar() for _ in range(3)], "A", "Max. allowed current", "max_amps", 1], "Max. Voltage:": [[DoubleVar() for _ in range(3)], "V", "Max. allowed voltage, must not exceed 16V!", "max_volts", 1], "Arduino Pins:": [[IntVar() for _ in range(3)], "-", "Should be 15, 16, 17", "relay_pin", 1] } self.fields = {} # Fill in header (axis names): col = 1 for text in ["X-Axis", "Y-Axis", "Z-Axis"]: label = Label(value_frame, text=text, font=SUB_HEADER_FONT) label.grid(row=0, column=col, sticky="ew") col += 1 # generate table with entries, unit labels and descriptions: row = 1 for key in self.entries.keys(): self.fields[key] = [] for axis in range(3): # generate entry fields field = Entry(value_frame, textvariable=self.entries[key][0][axis], width=10) field.grid(row=row, column=axis + 1, sticky=W, padx=2) self.fields[key].append(field) # safe access to field for use elsewhere axis_label = Label(value_frame, text=key, padx=5, pady=5) axis_label.grid(row=row, column=0, sticky=W) unit_label = Label(value_frame, text=self.entries[key][1]) unit_label.grid(row=row, column=4, sticky=W) description_label = Label(value_frame, text=self.entries[key][2]) description_label.grid(row=row, column=5, sticky=W) row = row + 1 row_counter += 1 self.update_fields() # set current values from config file Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer row_counter += 1 # Setup buttons # Setup frame to house buttons: self.buttons_frame = Frame(self) self.buttons_frame.grid_rowconfigure(ALL, weight=1) self.buttons_frame.grid_columnconfigure(ALL, weight=1) self.buttons_frame.grid(row=row_counter, column=0, sticky=W, padx=20) # Create and place buttons implement_button = Button(self.buttons_frame, text="Update and Reinitialize", command=self.implement, pady=5, padx=5, font=BIG_BUTTON_FONT) implement_button.grid(row=0, column=0, padx=5) restore_button = Button(self.buttons_frame, text="Restore Defaults", command=self.restore_defaults, pady=5, padx=5, font=BIG_BUTTON_FONT) restore_button.grid(row=0, column=1, padx=5) row_counter += 1 Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer def page_switch(self): # function that is called when switching to this window self.update_fields() def restore_defaults(self): # restore all default settings config.reset_config_to_default() # overwrite config file with default ui_print("\nReinitializing devices...") func.setup_all() # setup everything with the defaults self.update_fields() # update fields in config window def update_fields(self): # set current values for all entry variables from config file self.XY_port.set(g.XY_PORT) self.Z_port.set(g.Z_PORT) for key in self.entries.keys(): for i in [0, 1, 2]: value = config.read_from_config(g.AXIS_NAMES[i], self.entries[key][3], config.CONFIG_OBJECT) # get value from config file self.entries[key][0][i].set(value) # set initial value on variable type_value = self.entries[key][0][i].get() # get value with correct data type factor = self.entries[key][4] # get unit conversion factor self.entries[key][0][i].set(round(type_value * factor, 3)) # set value with correct unit conversion # check if values are within safe limits: value_check = func.value_in_limits(g.AXIS_NAMES[i], self.entries[key][3], value) if value_check == 'OK': # value is acceptable self.fields[key][i].config(background="White") # set colour of this entry to white else: # value exceeds limits self.fields[key][i].config(background="Red") # set colour of this entry to red to show problem def write_values(self): # update config file with user inputs into entry fields and reinitialize # set serial ports for PSUs: config.edit_config("PORTS", "xy_port", self.XY_port.get()) config.edit_config("PORTS", "z_port", self.Z_port.get()) # set numeric values for all axes for key in self.entries.keys(): # go through rows of entry table for i in [0, 1, 2]: # go through columns of entry table (axes) try: value = self.entries[key][0][i].get() # get value from field except TclError as e: # wrong format entered, e.g. text in number fields ui_print("Invalid entry for %s %s %s" % (g.AXIS_NAMES[i], key, e)) else: # format is ok factor = self.entries[key][4] # get unit conversion factor if factor not in [0, 1]: # prevent div/0 and conversion of int variables to float value = value / factor # do unit conversion # Check if value is within safe limits config_key = self.entries[key][3] # handle by which value is indexed in config file value_ok = func.value_in_limits(g.AXIS_NAMES[i], config_key, value) unit = self.entries[key][1] # get unit string for error messages axis = g.AXIS_NAMES[i] # get axis name for error messages if value_ok == 'OK': config.edit_config(g.AXIS_NAMES[i], config_key, value) # write new value to config file else: # value is not within limits if value_ok == 'HIGH': max_value = g.default_arrays[config_key][1][i] # get max value message = "Attempted to set too high value for {s} {k}\n" \ "{v} {unit}, max. {mv} {unit} allowed.\n" \ "Excessive values may damage equipment!\n" \ "Do you really want to use this value?" \ .format(s=axis, k=key, v=value * factor, mv=round(max_value * factor, 1), unit=unit) elif value_ok == 'LOW': min_value = g.default_arrays[config_key][2][i] # get min value message = "Attempted to set too low value for {s} {k}\n" \ "{v} {unit}, min. {mv} {unit} allowed.\n" \ "Excessive values may damage equipment!\n" \ "Do you really want to use this value?" \ .format(s=axis, k=key, v=value * factor, mv=round(min_value * factor, 1), unit=unit) else: message = "Unknown case, this should not happen." # display pop-up message to ask user if he really wants the value answer = messagebox.askquestion("Value out of Bounds", message) # becomes 'yes' or 'no' depending on user choice if answer == 'yes': # user really wants the value # call function to write new value to config file with override=True config.edit_config(g.AXIS_NAMES[i], config_key, value, True) # if user chooses 'no' nothing happens, old value is kept def implement(self): # executed on button press self.write_values() # write current values from entry fields to config object ui_print("\nReinitializing devices...") func.setup_all() # reinitialize devices and program with new values self.update_fields() # update entry fields to show new values def load_config(self): # load configuration from some config file directory = os.path.dirname(os.path.abspath(config.CONFIG_FILE)) # get directory of current config file # open file selection dialogue and save path of selected file filename = filedialog.askopenfilename(initialdir=directory, title="Select Config File", filetypes=(("Config File", "*.ini*"), ("All Files", "*.*"))) if exists(filename): # does the file exist? config.CONFIG_FILE = filename # set global config file to the new file config.CONFIG_OBJECT = config.get_config_from_file(filename) # load from config file to config object config.check_config(config.CONFIG_OBJECT) # check and display warnings if values are out of bounds ui_print("\nReinitializing devices...") func.setup_all() # reinitialize devices and program with new values self.update_fields() # update entry fields to show new values elif filename == '': # this happens when file selection window is closed without selecting a file ui_print("No file selected, could not load config.") else: ui_print("Selected file", filename, "does not exist, could not load config.") def save_config_as(self): # save current configuration to a new config file directory = os.path.dirname(os.path.abspath(config.CONFIG_FILE)) # get directory of current config file # open file selection dialogue and save path of selected file filename = filedialog.asksaveasfilename(initialdir=directory, title="Save config to file", filetypes=([("Config File", "*.ini*")]), defaultextension=[("Config File", "*.ini*")]) if filename == '': # this happens when file selection window is closed without selecting a file ui_print("No file selected, could not save config.") else: # a file name was entered config.CONFIG_FILE = filename # set global config file to the new file self.write_values() # write current entry field values to the config object config.write_config_to_file(config.CONFIG_OBJECT) # write contents of config object to file func.setup_all() # reinitialize devices and program with new values self.update_fields() # update entry fields to show values as they are in the config def save_config(self): # same as save_config_as() but with the current config file self.write_values() config.write_config_to_file(config.CONFIG_OBJECT) func.setup_all() # reinitialize devices and program with new values self.update_fields() # update entry fields to show values as they are in the config class ConfigureLogging(Frame): # generate window to configure data logging to csv # ToDo: support logging of axis-independent info like Arduino status def __init__(self, parent, controller): Frame.__init__(self, parent) 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.event_logging = False # True if data should be logged every time a command is sent to the test stand # log_datapoint() has to be called wherever a command is sent to the test stand and data should be logged # it does not happen automatically whenever something is sent to the test stand # It is done mainly in the functions for UI buttons, but rather inconsistently ToDo(optional): make consistent self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(ALL, weight=1) row_counter = 0 # setup heading header = Label(self, text="Configure Data Logging", font=HEADER_FONT, pady=3) header.grid(row=row_counter, column=0, padx=100, sticky=W) row_counter += 1 # 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, pady=5) 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) self.clear_data_button = Button(self.top_buttons_frame, text="Clear logged data", font=SMALL_BUTTON_FONT, command=self.clear_data, pady=5, padx=5, state="disabled") self.clear_data_button.grid(row=0, column=2, padx=5) row_counter += 1 # Create label showing how many datapoints have been logged self.log_label_frame = Frame(self) self.log_label_frame.grid_rowconfigure(ALL, weight=1) self.log_label_frame.grid_columnconfigure(ALL, weight=1) self.log_label_frame.grid(row=row_counter, column=0, sticky=W, padx=20) self.logged_datapoints = IntVar() # create variable to store number of logged datapoints # Add description label: datapoints_description = Label(self.log_label_frame, text="Datapoints logged:") datapoints_description.grid(row=0, column=0, sticky=W) # Add updatable label to show how much data has been logged datapoints_label = Label(self.log_label_frame, textvariable=self.logged_datapoints) datapoints_label.grid(row=0, column=1, sticky=W) row_counter += 1 # create checkboxes and entries to set how often data should be logged self.settings_frame = Frame(self) self.settings_frame.grid_rowconfigure(ALL, weight=1) self.settings_frame.grid_columnconfigure(ALL, weight=1) self.settings_frame.grid(row=row_counter, column=0, sticky=W, padx=20) self.regular_logging_var = BooleanVar(value=True) # create variable for the regular logging checkbox self.event_logging_var = BooleanVar(value=True) # create variable for the logging on command checkbox self.log_interval = DoubleVar(value=1) # create variable for logging interval entry field # create checkboxes for regular and event logging: self.regular_logging_checkbox = Checkbutton(self.settings_frame, text="Log in regular intervals", variable=self.regular_logging_var, onvalue=True, offvalue=False) self.event_logging_checkbox = Checkbutton(self.settings_frame, text="Log whenever test stand is commanded", variable=self.event_logging_var, onvalue=True, offvalue=False) self.regular_logging_checkbox.grid(row=0, column=0, sticky=W) self.event_logging_checkbox.grid(row=1, column=0, sticky=W, columnspan=3) # Set up entry field for setting logging interval # Add description label for logging interval entry: interval_label = Label(self.settings_frame, text=" Interval (s):") interval_label.grid(row=0, column=1, sticky=W) # Add entry field to set interval self.interval_entry = Entry(self.settings_frame, textvariable=self.log_interval) self.interval_entry.grid(row=0, column=2, sticky=W) 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.checkboxes = [] # list containing all the checkbox objects, used to enable/disable all of them self.active_keys = [] # list with all the keys relating to the currently ticked checkboxes # generate and place all the checkboxes: checkbox_label = Label(self.checkbox_frame, text="Select which data to log:") checkbox_label.grid(row=0, column=0, columnspan=2) # ToDo (optional): Add option to select which axes to log data from row = 1 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 self.checkboxes.append(checkbox) # add created checkbox to list 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 = self.regular_logging_var.get() # check if regular logging checkbox is ticked self.event_logging = self.event_logging_var.get() # check if event logging checkbox is ticked self.update_datapoint_count() # start regular update of label showing how many datapoints have been collected if self.logged_datapoints.get() == 0: # no data has been logged so far # (if condition is here to keep timestamps consistent when repeatedly starting/stopping) log.zero_time = datetime.now() # set reference time for timestamps in log error = False if self.regular_logging: try: # try to get log interval interval_ms = int(self.log_interval.get() * 1000) except TclError as e: # invalid entry for log interval messagebox.showwarning("Wrong entry format!", "Invalid entry for log interval:\n%s" % e) self.event_logging = False # don't start logging if there is a problem error = True else: self.periodic_log(interval_ms) # start periodic logging if (self.regular_logging or self.event_logging) and not error: # logging is active and no error during setup # lock/unlock buttons and checkboxes: self.write_to_file_button["state"] = "disabled" self.clear_data_button["state"] = "normal" self.lock_checkboxes() 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 # tell everything its time to stop logging self.event_logging = False # tell everything its time to stop logging self.write_to_file_button["state"] = "normal" # enable button self.unlock_checkboxes() # enable checkboxes self.start_logging_button.tkraise() # switch start/stop button to start def write_to_file(self): # lets user select a file and writes logged data to it filepath = log.select_file() # select a file to write to if filepath is None: # no valid file was selected # ask user if he wants to try again: try_again = messagebox.askquestion("No file selected", "No valid file was selected. Try again?") if try_again == 'yes': # user wants to try again self.write_to_file() # call same function again so user can retry else: log.write_to_file(log.log_data, filepath) # write logged data to the file def clear_data(self): # called on button press, asks user if he want to save logged data and then deletes it if log.unsaved_data: # there is logged data that has not been written to a file 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 before deleting?") if save_log == 'yes': # user has chosen yes self.write_to_file() # run write to file function to save data log.clear_logged_data() # delete the logged data log.unsaved_data = False # tell everything that there is no unsaved data remaining self.logged_datapoints.set(len(log.log_data)) # update the label showing how much data has been logged ui_print("Log data cleared.") def update_choices(self): # updates the list storing which checkboxes are currently ticked # (this is passed to logging functions and determines which data is logged) self.active_keys = [] # initialize the list for key in self.checkbox_vars.keys(): # go through all checkboxes if self.checkbox_vars[key].get(): # box is ticked self.active_keys.append(key) # add corresponding item to the list def lock_checkboxes(self): # lock all checkboxes, so they can not be modified while logging for checkbox in [*self.checkboxes, self.event_logging_checkbox, self.regular_logging_checkbox]: checkbox.config(state=DISABLED) self.interval_entry.config(state=DISABLED) def unlock_checkboxes(self): for checkbox in [*self.checkboxes, self.event_logging_checkbox, self.regular_logging_checkbox]: checkbox.config(state=NORMAL) self.interval_entry.config(state=NORMAL) def periodic_log(self, interval): # logs data in regular intervals (ms) if self.regular_logging: # logging in intervals is active self.log_datapoint() self.controller.after(interval, lambda: self.periodic_log(interval)) # call again after time interval def log_datapoint(self): # log a single datapoint based on which checkboxes are ticked try: log.log_datapoint(self.active_keys) # add datapoint with active checkboxes to log data frame except Exception as e: messagebox.showerror("Error!", "Error while logging data: \n%s" % e) def update_datapoint_count(self): if self.regular_logging or self.event_logging: # logging is active self.logged_datapoints.set(len(log.log_data)) # update label with number of rows in log_data self.controller.after(1000, self.update_datapoint_count) # call function again after 1 second class StatusDisplay(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent, relief=SUNKEN, bd=1) self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(ALL, weight=1) rowCounter = 0 # keep track of which row we are at in the grid layout x_pad = 10 # centrally set padding col = 0 for header in ["", "X-Axis", "Y-Axis", "Z-Axis"]: # create Column headers headLabel = Label(self, text=header, font=SUB_HEADER_FONT, borderwidth=1, relief="flat", anchor="w", padx=x_pad) headLabel.grid(row=rowCounter, column=col, sticky="ew") col = col + 1 # move to next column rowCounter = rowCounter + 1 # increase row counter to place future stuff below header # define content of row entries TextLabels = ["PSU Serial Port:", "PSU Channel:", "PSU Status:", "Arduino Status:", "", "Output:", "Remote Control:", "Voltage Setpoint:", "Actual Voltage:", "Current Setpoint:", "Actual Current:", "", "Target Field:", "Trgt. Field Raw:", "Target Current:", "Inverted:"] self.rowNo = len(TextLabels) # get number of label rows self.columnNo = 4 # number of label columns # prepare list of lists to contain all labels for row entries in all columns: self.Labels = [[] for _ in range(self.columnNo)] self.label_dict = {} for name in TextLabels: self.label_dict[name] = [StringVar() for _ in range(self.columnNo - 1)] # add labels for row titles self.Labels[0].append(Label(self, text=name, borderwidth=1, relief="flat", anchor="w", padx=x_pad)) for col in range(self.columnNo - 1): # add labels vor values self.Labels[col + 1].append(Label(self, textvariable=self.label_dict[name][col], borderwidth=1, relief="flat", anchor="w", padx=x_pad)) col = 0 for LabelCol in self.Labels: # place row entries in grid layout for all columns for row in range(self.rowNo): # place row entries LabelCol[row].grid(row=row + rowCounter, column=col, sticky="nsew") col = col + 1 # rowCounter = rowCounter + self.rowNo # increase row counter to place future stuff below this self.update_labels() def continuous_label_update(self, controller, interval): # update display values in regular intervals (ms) if not g.exitFlag: # app ist still running self.update_labels() controller.after(interval, lambda: self.continuous_label_update(controller, interval)) def update_labels(self): # update all values in the status display g.ARDUINO.update_status_info() i = 0 for axis in g.AXES: if axis.device is not None: axis.update_status_info() # update all label variables with current values: self.label_dict["PSU Serial Port:"][i].set(g.PORTS[i]) self.label_dict["PSU Channel:"][i].set(axis.channel) self.label_dict["PSU Status:"][i].set(axis.connected) self.label_dict["Arduino Status:"][i].set(g.ARDUINO.connected) # ToDo (optional): make this multicolumn self.label_dict["Output:"][i].set(axis.output_active) self.label_dict["Remote Control:"][i].set(axis.remote_ctrl_active) self.label_dict["Voltage Setpoint:"][i].set("%0.3f V" % axis.voltage_setpoint) self.label_dict["Actual Voltage:"][i].set("%0.3f V" % axis.voltage) self.label_dict["Current Setpoint:"][i].set("%0.3f A" % axis.current_setpoint) self.label_dict["Actual Current:"][i].set("%0.3f A" % axis.current) self.label_dict["Target Field:"][i].set("%0.3f \u03BCT" % (axis.target_field * 1e6)) self.label_dict["Trgt. Field Raw:"][i].set("%0.3f \u03BCT" % (axis.target_field_comp * 1e6)) self.label_dict["Target Current:"][i].set("%0.3f A" % axis.target_current) self.label_dict["Inverted:"][i].set(axis.polarity_switched) i += 1 class OutputConsole(Frame): # console to print stuff in, similar to standard python output def __init__(self, parent): Frame.__init__(self, parent, relief=SUNKEN, bd=1) self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(0, weight=1, minsize=60) scrollbar = Scrollbar(self) self.console = Text(self) self.console.bind("", lambda e: "break") # prevent user input into the console scrollbar.grid(row=0, column=1, sticky="ns") self.console.grid(row=0, column=0, sticky="nesw") scrollbar.config(command=self.console.yview) self.console.config(yscrollcommand=scrollbar.set) def ui_print(*content): # prints text to built in console, use exactly like normal print() output = "" for text in 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 g.app.OutputConsole.console.see(END) # scroll to bottom else: # if window is not open, do normal print print(output)