diff --git a/Test cases.xlsx b/Test cases.xlsx index 88dda9c..da0c57a 100644 Binary files a/Test cases.xlsx and b/Test cases.xlsx differ diff --git a/User_Interface.py b/User_Interface.py index 36577f0..226ef4e 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -3,6 +3,8 @@ 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 @@ -29,8 +31,8 @@ class HelmholtzGUI(Tk): self.Menu = TopMenu(self) # displays menu bar at the top - mainArea = Frame(self, padx=20, pady=20) - mainArea.pack(side="top", fill="both", expand=False) + mainArea = Frame(self, padx=10, pady=10) + mainArea.pack(side="top", fill="both", expand=True) mainArea.grid_rowconfigure(0, weight=1) mainArea.grid_columnconfigure(0, weight=1) @@ -44,7 +46,6 @@ class HelmholtzGUI(Tk): status_frame = Frame(self) status_frame.pack(side="bottom", fill="x", expand=False) - status_frame.grid_rowconfigure(ALL, weight=1) status_frame.grid_columnconfigure(1, weight=1) self.StatusDisplay = StatusDisplay(status_frame, self) @@ -514,7 +515,6 @@ class Configuration(Frame): class ExecuteCSVMode(Frame): # generate configuration window to set program constants # ToDo (optional): Generate graph to show sequence to be executed - # ToDo: add button for reinit def __init__(self, parent, controller): Frame.__init__(self, parent) @@ -537,24 +537,57 @@ class ExecuteCSVMode(Frame): # Setup buttons # 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) + 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) # Create and place buttons - self.select_file_button = Button(self.file_select_frame, text="Select csv file...", command=self.load_csv, + 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.file_select_frame, text="Run Sequence", command=self.run_sequence, + 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.file_select_frame, text="Stop Run", command=self.stop_run, + 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=func.setup_all, + 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) + + 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 @@ -570,9 +603,11 @@ class ExecuteCSVMode(Frame): self.sequence_array = csv.read_csv_to_array(filename) except BaseException as e: ui_print("Error while opening file:", e) - # ToDo: make error a popup + messagebox.showerror("Error!", "Error while opening file: \n%s" % e) + + csv.check_array(self.sequence_array) + self.display_plot() - # ToDo: check for excessive values 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.") @@ -584,6 +619,7 @@ class ExecuteCSVMode(Frame): 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 separate thread to run sequence execution in: @@ -592,11 +628,18 @@ class ExecuteCSVMode(Frame): csv_thread.start() # start thread def stop_run(self): - g.running = False + g.running = False # 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" + + def display_plot(self): # ToDo: comments + figure = csv.plot_field_sequence(self.sequence_array) + plotCanvas = FigureCanvasTkAgg(figure, self.plotFrame) + plotCanvas.draw() + plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") class StatusDisplay(Frame): @@ -645,11 +688,12 @@ class StatusDisplay(Frame): col = col + 1 # rowCounter = rowCounter + self.rowNo # increase row counter to place future stuff below this - self.continuous_label_update(controller, 500) # initiate regular value updates (ms) + self.update_labels() def continuous_label_update(self, controller, interval): # update display values in regular intervals self.update_labels() - if g.app is not None: + if g.app is not None: # app ist still running + # ToDo (optional): prevent call after program close controller.after(interval, lambda: self.continuous_label_update(controller, interval)) def update_labels(self): @@ -697,9 +741,9 @@ def ui_print(*content): # prints text to built in console output = "" for text in content: output = " ".join((output, str(text))) # append content - if g.app is not None: + if g.app is not None: # if main window is still open 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) \ No newline at end of file + print(output) diff --git a/cage_func.py b/cage_func.py index 300be0d..0d07be8 100644 --- a/cage_func.py +++ b/cage_func.py @@ -70,7 +70,9 @@ class Axis: self.current = self.device.get_current(self.channel) self.current_setpoint = self.device.get_current_setpoint(self.channel) except (serial.serialutil.SerialException, IndexError): - # ui_print("Connection Error with %s PSU on %s" % (self.name, self.port)) + if self.connected == "Connected": + ui_print("Connection Error with %s PSU on %s" % (self.name, self.port)) + messagebox.showerror("PSU Error", "Connection Error with %s PSU on %s" % (self.name, self.port)) self.connected = "Connection Error" self.output_active = "Unknown" self.remote_ctrl_active = "Unknown" @@ -94,6 +96,7 @@ class Axis: g.ARDUINO.digitalWrite(self.ardPin, "LOW") except Exception as e: ui_print("Error while powering down %s: %s" % (self.name, e)) + messagebox.showerror("PSU Error!", "Error while powering down %s: \n%s" % (self.name, e)) def set_signed_current(self, value): # sets current with correct polarity on this axis device = self.device @@ -167,6 +170,7 @@ class ArduinoCtrl(Arduino): axis.polarity_switched = "False" except Exception as e: ui_print("Error with Arduino:", e) + messagebox.showerror("Error with Arduino!", "Connection Error with Arduino: \n%s" % e) for axis in g.AXES: axis.polarity_switched = "Unknown" self.connected = "Connection Error" @@ -280,7 +284,7 @@ def power_down_all(): # temporary, set all outputs to 0 but keep connections en def shut_down_all(): # shutdown at program end or on error, set outputs to 0 and disable connections # ToDo: remove checks if connected or make them only for printing ui_print("\nAttempting to safely shut down all devices. Check equipment to confirm.") - message = "Tried to safely shut down all devices. Check equipment to confirm." + message = "Tried to shut down all devices. Check equipment to confirm." if g.XY_DEVICE is not None: try: set_to_zero(g.XY_DEVICE) @@ -356,4 +360,33 @@ def set_current_vec(vector): # sets needed currents on each axis for given vect axis.set_signed_current(vector[i]) except ValueError as e: ui_print(e) - i += 1 \ No newline at end of file + i += 1 + + +def devices_ok(xy_off=False, z_off=False, arduino_off=False): + # ToDo: comments + try: + if not xy_off: + if g.XY_DEVICE is not None: + g.X_AXIS.update_status_info() + if g.X_AXIS.connected != "Connected": + return False + else: + return False + if not z_off: + if g.Z_DEVICE is not None: + g.Z_AXIS.update_status_info() + if g.Z_AXIS.connected != "Connected": + return False + else: + return False + + if not arduino_off: + g.ARDUINO.update_status_info() + if g.ARDUINO.connected != "Connected": + return False + except Exception as e: + messagebox.showerror("Error!", "Error while checking devices: \n%s" % e) + return False + else: + return True diff --git a/csv_threading.py b/csv_threading.py index 16fcb7f..f284d36 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -1,12 +1,17 @@ -import User_Interface as ui -import cage_func as func import time import pandas from threading import * +from tkinter import messagebox + +import matplotlib.pyplot as plt + +import User_Interface as ui +import cage_func as func import globals as g class ExecCSVThread(Thread): + # ToDo: handling for disconnected devices def __init__(self, threadID, array, parent, controller): Thread.__init__(self) @@ -19,28 +24,36 @@ class ExecCSVThread(Thread): ui.ui_print("Starting Sequence Execution...") # g.threadLock.acquire() # Get lock to synchronize threads # ToDo: add locking/synchronization? Works without so far but might be more robust - execute_sequence(self.array, 0.1, self.controller) # run sequence + execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence self.parent.running = False # sequence finished --> no longer running # reset buttons on UI: self.parent.select_file_button["state"] = "normal" self.parent.execute_button["state"] = "normal" self.parent.stop_button["state"] = "disabled" + self.parent.reinit_button["state"] = "normal" -def execute_sequence(array, delay, controller): # runs through array containing times and desired field vectors +def execute_sequence(array, delay, parent, controller): # runs through array containing times and desired field vectors # array format: [time (s), xField (T), yField (T), zField (T)] # decimal commas # all times in seconds func.power_down_all() # sets outputs to 0 before starting t_zero = time.time() # set reference time for start of run + + # Check if everything is properly connected: + all_connected = func.devices_ok(parent.xy_override, parent.z_override, parent.arduino_override) + # True or False depending on devices status, checks for some devices may be overridden by user + i = 0 - while i < len(array) and g.running: # while array is not finished and user has not cancelled + while i < len(array) and g.running and all_connected: + # while array is not finished, user has not cancelled and devices are connected + t = time.time() - t_zero # get relative time if t >= array[i, 0]: # time for this row has come field_vec = array[i, 1:4] # extract desired field vector ui.ui_print("%f s: t = %0.2f s, target field vector = " - % (time.time()-t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") - func.set_field_simple(field_vec) # send field vector to test stand # ToDo!: reset to set_field() + % (time.time() - t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") + func.set_field(field_vec) # send field vector to test stand ui.ui_print(time.time() - t_zero) controller.StatusDisplay.update_labels() # update status display after change i = i + 1 # next row @@ -49,10 +62,17 @@ def execute_sequence(array, delay, controller): # runs through array containing pass else: # sleep to give other threads time to run time.sleep(delay) - if g.running: # sequence ended without interruption + + # check again if everything is connected before starting next loop run: + all_connected = func.devices_ok(parent.xy_override, parent.z_override, parent.arduino_override) + + if g.running and all_connected: # sequence ended without interruption ui.ui_print("Sequence executed, powering down channels.") - else: # interrupted by user + elif all_connected: # interrupted by user ui.ui_print("Sequence cancelled, powering down channels.") + elif g.running: # interrupted by device error + ui.ui_print("Error with at least one device, sequence aborted.") + messagebox.showinfo("Device Error!", "Error with at least one device, sequence aborted.") func.power_down_all() # set currents and voltages to 0, set arduino pins to low @@ -63,3 +83,40 @@ def read_csv_to_array(filepath): file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file array = file.to_numpy() # convert csv to array return array + + +def check_array(array): + # ToDo: message formatting, pop up warning + # ToDo: comments + concerns = [] + for row in array: + i = 1 + for axis in g.AXES: + value = row[i] + if value > axis.max_comp_field[1]: + concerns.append(row) + elif value < axis.max_comp_field[0]: + concerns.append(row) + i += 1 + ui.ui_print("Checked csv, found %i concerns." % len(concerns)) + if len(concerns) > 0: + ui.ui_print(concerns) + + +def plot_field_sequence(array): # ToDo: comments + # ToDo: make pretty + figure = plt.Figure(figsize=(8, 10), dpi=100) + + # noinspection PyTypeChecker,SpellCheckingInspection + axes = figure.subplots(3, sharex=True, sharey=True, gridspec_kw={'hspace': 0.4}) + + figure.suptitle("Magnetic Field Sequence") + t = array[:, 0] + + for i in [0, 1, 2]: + data = array[:, i + 1] * 1e6 + plot = axes[i] + plot.plot(t, data) + plot.set_title(g.AXIS_NAMES[i]) + + return figure diff --git a/globals.py b/globals.py index f33f60a..ffe1ec8 100644 --- a/globals.py +++ b/globals.py @@ -30,7 +30,7 @@ running = False # ToDo: put this into a config file 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], [0, 0, 0]]) * 1e-6, # background magnetic field [T] + "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] diff --git a/main.py b/main.py index 7edfb79..30cea3b 100644 --- a/main.py +++ b/main.py @@ -26,6 +26,8 @@ try: # start normal operations print("\nOpening User Interface...") g.app = HelmholtzGUI() + g.app.state('zoomed') # open maximized + g.app.StatusDisplay.continuous_label_update(g.app, 500) # initiate regular Status Display updates (ms) ui_print("Program Initialized") config.check_config(config.CONFIG_OBJECT) # check config for values exceeding limits diff --git a/requirements.txt b/requirements.txt index 6409c7c..088bdfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ numpy==1.19.3 # bug in numpy 1.19.4, 1.19.3 used as workaround pyserial~=3.5 future~=0.18.2 -pandas \ No newline at end of file +pandas~=1.1.5 +matplotlib~=3.3.2 \ No newline at end of file