diff --git a/User_Interface.py b/User_Interface.py index 10705d4..bfed20a 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -1,3 +1,6 @@ +# 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 @@ -17,7 +20,7 @@ import csv_threading as csv import config_handling as config import csv_logging as log -NORM_FONT = () +# define font styles: HEADER_FONT = ("Arial", 13, "bold") SUB_HEADER_FONT = ("Arial", 9, "bold") BIG_BUTTON_FONT = ("Arial", 11, "bold") @@ -25,54 +28,62 @@ 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") - Tk.wm_iconbitmap(self, "Helmholtz.ico") + 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) # displays menu bar at the top + self.Menu = TopMenu(self) # display dropdown menu bar at the top (see TopMenu class for details) - mainArea = Frame(self, padx=10, pady=10) - mainArea.pack(side="top", fill="both", expand=True) + 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) + mainArea.grid_rowconfigure(0, weight=1) # configure rows and columns of the Tkinter grid to expand with window mainArea.grid_columnconfigure(0, weight=1) - self.pages = {} # dictionary for storing all pages + # 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 - for P in [ManualMode, Configuration, ExecuteCSVMode, ConfigureLogging]: - page = P(mainArea, self) - self.pages[P] = page - page.grid(row=0, column=0, sticky="nsew") + 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 - status_frame = Frame(self) - status_frame.pack(side="bottom", fill="x", expand=False) - status_frame.grid_columnconfigure(1, weight=1) + # 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) + self.show_frame(ManualMode) # show manual mode to start with - def show_frame(self, key): - frame = self.pages[key] # gets correct page from the dictionary + 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() # brings this frame to the front + 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) - window.config(menu=menu) + menu = Menu(window) # initialize Menu object + window.config(menu=menu) # put menu at the top of the window - ModeSelector = Menu(menu) - menu.add_cascade(label="Mode", menu=ModeSelector) + 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() @@ -80,33 +91,34 @@ class TopMenu: ModeSelector.add_command(label="Settings...", command=lambda: self.configuration(window)) @staticmethod - def manual_mode(window): + def manual_mode(window): # switch to the manual mode page window.show_frame(ManualMode) @staticmethod - def configuration(window): + def configuration(window): # switch to the settings page window.show_frame(Configuration) @staticmethod - def execute_csv_mode(window): + def execute_csv_mode(window): # switch to the CSV execution page window.show_frame(ExecuteCSVMode) @staticmethod - def logging(window): + 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) + 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) + 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 + 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) @@ -115,48 +127,54 @@ class ManualMode(Frame): row_counter += 1 # Setup Dropdown Menu for input mode - dropdown_frame = Frame(self) + 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) - self.input_mode = StringVar() - # make dictionary with information on all modes. - # content: [function to call on button press, unit text to be displayed] - self.modes = {"Magnetic Field": [self.execute_field, "\u03BCT", self.update_max_fields], - "Current": [self.execute_current, "A", self.update_max_currents]} - self.unit = StringVar() - default_mode = list(self.modes.keys())[0] + 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 + # 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) + 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:"] - self.entry_vars = [StringVar() for _ in range(3)] - self.max_value_vars = [StringVar() for _ in range(3)] + 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: - field = ttk.Entry(self.entries_frame, textvariable=self.entry_vars[row]) - self.entry_vars[row].set(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) + 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) + 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 @@ -164,12 +182,13 @@ class ManualMode(Frame): row_counter += 1 # setup checkbox for compensating ambient field - checkbox_frame = Frame(self, padx=20) + checkbox_frame = Frame(self, padx=20) # create frame to house it checkbox_frame.grid(row=row_counter, column=0, sticky=W) - self.compensate = IntVar(value=1) + 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=1, offvalue=0) + variable=self.compensate, onvalue=True, offvalue=False) self.compensate_checkbox.pack(side="left") row_counter += 1 @@ -192,13 +211,12 @@ class ManualMode(Frame): pady=5, padx=5, font=BIG_BUTTON_FONT) power_down_button.grid(row=row_counter, column=1, padx=5) - # add button for reinitialization + # 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 - # Add spacer to Frame below Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer @@ -210,31 +228,36 @@ class ManualMode(Frame): 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 they are necessary - def change_mode_callback(self, var, index, mode): # called input mode dropdown is changed + # 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 update_max_fields(self): # update labels with maximum allowable field values - self.compensate_checkbox.config(state=NORMAL) + 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 val in self.max_value_vars: - comp = self.compensate.get() - if comp == 0: - field = g.AXES[i].max_field * 1e6 - elif comp == 1: + 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: + else: # this really should never happen field = [0, 0] - ui_print("Unexpected value encountered: compensate =", comp) - val.set("(%0.1f to %0.1f \u03BCT)" % (field[0], field[1])) + 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 update_max_currents(self): # update labels with maximum allowable current values - self.compensate_checkbox.config(state=DISABLED) + 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 val in self.max_value_vars: - val.set("(%0.2f to %0.2f A)" % (-g.AXES[i].max_amps, g.AXES[i].max_amps)) + 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 @@ -253,38 +276,202 @@ class ManualMode(Frame): if logger.event_logging: # data should be logged when test stand is commanded logger.log_datapoint() # log data - def execute(self): - function_to_call = self.modes[self.input_mode.get()][0] # get function of appropriate mode - vector = np.array([0, 0, 0], dtype=float) + 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 - for var in self.entry_vars: - vector[i] = float(var.get()) - i = i + 1 - function_to_call(vector) # call function - self.controller.StatusDisplay.update_labels() # update status display after change + 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 execute_field(self, vector): - ui_print("field executing", vector) - comp = self.compensate.get() - if comp == 1: - func.set_field(vector * 1e-6) - elif comp == 0: - func.set_field_simple(vector * 1e-6) - else: - ui_print("Unexpected value encountered: compensate =", comp) + def reinitialize(self): # called on "Reinitialize devices" button press + func.setup_all() # reinitialize all PSUs and the Arduino - @staticmethod - def execute_current(vector): - ui_print("current executing:", vector) - try: - func.set_current_vec(vector) - except ValueError as e: - ui_print(e) + # 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): @@ -546,166 +733,6 @@ class Configuration(Frame): self.update_fields() # update entry fields to show values as they are in the config -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 ConfigureLogging(Frame): # generate window to configure data logging to csv # ToDo: support logging of axis-independent info like Arduino status diff --git a/cage_func.py b/cage_func.py index b2ac41f..75dd911 100644 --- a/cage_func.py +++ b/cage_func.py @@ -1,11 +1,13 @@ # This file contains all classes and functions directly related to the operation of the helmholtz test stand. # The two main classes are Axis and ArduinoCtrl, see their definitions for details. +# import packages: import numpy as np import serial import traceback from tkinter import messagebox +# import other project files from User_Interface import ui_print from pyps2000b import PS2000B from Arduino import Arduino @@ -122,7 +124,8 @@ class Axis: if abs(value) > self.max_amps: # prevent excessive currents self.power_down() # set output to 0 and deactivate - raise ValueError("Invalid current value. Tried %0.2fA, max. %0.2fA allowed" % (value, self.max_amps)) + raise ValueError("Invalid current value on %s. Tried %0.2fA, max. %0.2fA allowed" + % (self.name, value, self.max_amps)) elif value >= 0: # switch the e-box relay to change polarity as needed g.ARDUINO.digitalWrite(self.ardPin, "LOW") # command the output pin on the arduino in the electronics box @@ -384,7 +387,7 @@ def set_current_vec(vector): # sets currents on each axis according to given ve axis.set_signed_current(vector[i]) # command test stand to set the current except ValueError as e: # current was too high - ui_print(e) + ui_print(e) # print out the error message i += 1 diff --git a/config_handling.py b/config_handling.py index 0fbfea9..66ef1fd 100644 --- a/config_handling.py +++ b/config_handling.py @@ -1,9 +1,11 @@ # This file contains functions and variables related to reading and writing configuration files. # The configparser module is used for processing. Config files are of type .ini +# import packages: from configparser import ConfigParser from tkinter import messagebox +# import other project files: import globals as g import cage_func as func # noinspection PyPep8Naming diff --git a/csv_logging.py b/csv_logging.py index 41c1cdc..bca8962 100644 --- a/csv_logging.py +++ b/csv_logging.py @@ -1,12 +1,15 @@ # 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 packages import pandas as pd import globals as g from datetime import datetime import os from tkinter import filedialog from tkinter import messagebox + +# import other project files import User_Interface as ui log_data = pd.DataFrame() # pandas data frame containing the logged data, in-program representation of csv/excel data diff --git a/globals.py b/globals.py index 3339517..116049d 100644 --- a/globals.py +++ b/globals.py @@ -33,7 +33,6 @@ default_arrays = { "coil_const": np.array([[38.6, 38.45, 37.9], [50, 50, 50], [0, 0, 0]]) * 1e-6, # Coil constants [x,y,z] [T/A] "ambient_field": np.array([[30, 30, 30], [200, 200, 200], [-200, -200, -200]]) * 1e-6, # background magnetic field [T] "resistance": np.array([[1.7, 1.7, 1.7], [5, 5, 5], [1, 1, 1]], dtype=float), # resistance of circuits [Ohm] - "max_watts": np.array([[15, 15, 15], [50, 50, 50], [0, 0, 0]], dtype=float), # max. allowed power for circuits [W] "max_volts": np.array([[14, 14, 14], [16, 16, 16], [0, 0, 0]], dtype=float), # max. allowed voltage, limited to 16V by used diodes! [V] "max_amps": np.array([[4.5, 4.5, 4.5], [6, 6, 6], [0, 0, 0]], dtype=float), # max. allowed current (A) "relay_pin": [[15, 16, 17], [15, 16, 17], [15, 16, 17]] # pins on the arduino for reversing [x,y,z] polarity