From d02bde96311e395045b531aa7b7170f5b053bb64 Mon Sep 17 00:00:00 2001 From: Leon Teichroeb Date: Tue, 28 Sep 2021 17:46:09 +0200 Subject: [PATCH] Fixed csv_threading.py --- src/csv_logging.py | 2 +- src/csv_threading.py | 162 +++++++++++++++++++++++------------------- src/user_interface.py | 110 +++++++++++++++------------- 3 files changed, 152 insertions(+), 122 deletions(-) diff --git a/src/csv_logging.py b/src/csv_logging.py index e6f3f8b..b9adc9a 100644 --- a/src/csv_logging.py +++ b/src/csv_logging.py @@ -88,7 +88,7 @@ def write_to_file(dataframe, filepath): except PermissionError: message = "No permission to write to: \n%s. \nFile may be open in another program." % filepath messagebox.showerror("Permission Error", message) - except BaseException as e: + except Exception as e: message = "Error while trying to write to file \n%s.\n%s" % (filepath, e) messagebox.showerror("Error!", message) else: # no exceptions occurred diff --git a/src/csv_threading.py b/src/csv_threading.py index 6486a06..841c438 100644 --- a/src/csv_threading.py +++ b/src/csv_threading.py @@ -1,16 +1,16 @@ -# tThis file contains code for executing a sequence of magnetic fields from a csv file. +# This file contains code for executing a sequence of magnetic fields from a csv file. # To do this without crashing the UI it has to run in a separate thread using the threading module. -# ToDo!: apparently max. 1 thread can access PSU --> continuous update + csv thread crashes program. Find solution -# import packages: import time +from io import StringIO + import pandas import numpy as np from threading import * from tkinter import messagebox import matplotlib.pyplot as plt -# import other project files: +from src.exceptions import DeviceBusy, DeviceAccessError from src.utility import ui_print import src.user_interface as ui import src.globals as g @@ -20,114 +20,133 @@ class ExecCSVThread(Thread): # main class for executing a CSV sequence # it inherits the threading.Thread class, enabling sequence execution in a separate thread - def __init__(self, array, parent, controller, logging_enabled): + def __init__(self, array, parent, controller): Thread.__init__(self) self.array = array # numpy array containing data from csv to be executed self.parent = parent # object from which this class is called, here the ExecuteCSVMode object of the UI self.controller = controller # object on which mainloop() is running, usually the main UI window - self.logging_enabled = logging_enabled - self.__stop_event = Event() # event which can be set to stop the thread execution if needed + # Acquire cage device. This resource will only be released after the thread is ended. + try: + self.cage_dev = g.CAGE_DEVICE.request_proxy() + except DeviceBusy: + raise DeviceAccessError("Failed to acquire coil control. Required for ambient field calibration.") + + self._stop_event = Event() # event which can be set to stop the thread execution if needed def run(self): # called to start the execution of the thread ui_print("\nStarting Sequence Execution...") - self.execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence - # when the sequence has ended, reset buttons on the UI: - if not g.exitFlag: # main window is open - 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" + try: + self.execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence + finally: + self.cage_dev.idle() # set currents and voltages to 0, set arduino pins to low + # Release the proxy so other components can use it + self.cage_dev.close() + + # when the sequence has ended, reset buttons on the UI: + if not g.exitFlag: # main window is open + 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" # setup ability to interrupt thread (https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread) def stop(self): # stop thread execution, can be called from another thread to kill this one - self.__stop_event.set() + self._stop_event.set() + @property def stopped(self): # returns true if the thread has been stopped, used to check if a run should continue - return self.__stop_event.is_set() + return self._stop_event.is_set() def execute_sequence(self, array, delay, parent, controller): # main execution method of the class # runs through array with times and desired fields and commands test bench accordingly # array format: [time (s), xField (T), yField (T), zField (T)] - func.power_down_all() # sets outputs on PSUs to 0 and Arduino pins to LOW before starting + self.cage_dev.idle() # sets outputs on PSUs to 0 and Arduino pins to LOW 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.get(), parent.z_override.get(), - parent.arduino_override.get()) + all_connected = (parent.xy_override.get() or g.CAGE_DEVICE.psu1 is not None) and\ + (parent.z_override.get() or g.CAGE_DEVICE.psu2 is not None) and\ + (parent.arduino_override.get() or g.CAGE_DEVICE.arduino is not None) # True or False depending on devices status, checks for some devices may be overridden by user + if not all_connected: + ui_print("Required devices are not present, sequence aborted.") + messagebox.showwarning("Device Error!", "Required devices are not present, sequence aborted.") + return i = 0 # index of the current array row - while i < len(array) and all_connected and not self.stopped() and not g.exitFlag: + while i < len(array): + if self.stopped or g.exitFlag: + # Interrupt sequence + ui_print("Sequence interrupted, powering down channels.") + # Channels powered down in run function + return + # while array is not finished, devices are connected, user has not cancelled and application is running t = time.time() - t_zero # get time relative to start of run - if t >= array[i, 0]: # time for this row has come - g.threadLock.acquire() # execute all lines until threadLock.release() before going back to main thread + target_t = array[i, 0] # Target execution time of data point + if t >= target_t: # time for this row has come + field_vec = array[i, 1:4] # extract desired field vector + ui_print("[{:5.3f}s] B=[{:.1f}, {:.1f}, {:.1f}]\u03BCT for t={:.2f}s".format(t, + field_vec[0] * 1e6, + field_vec[1] * 1e6, + field_vec[2] * 1e6, + target_t)) + self.cage_dev.set_field_compensated(field_vec) # send field vector to test bench - # check if everything is still connected before sending commands: - all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), - parent.arduino_override.get()) - if all_connected: - field_vec = array[i, 1:4] # extract desired field vector - ui_print("%0.5f s: t = %0.2f s, target field vector =" - % (time.time() - t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") - func.set_field(field_vec) # send field vector to test bench + # log change to the log file if user has selected event logging in the Configure Logging window + logger = controller.pages[ui.ConfigureLogging] # get object of logging configurator + if logger.event_logging: # data should be logged when test bench is commanded + logger.log_datapoint() # log data - # log change to the log file if user has selected event logging in the Configure Logging window - logger = controller.pages[ui.ConfigureLogging] # get object of logging configurator - if logger.event_logging: # data should be logged when test bench is commanded - logger.log_datapoint() # log data + i = i + 1 # next row - i = i + 1 # next row - - g.threadLock.release() # allow going back to main thread now - - elif t <= array[i, 0] - delay - 0.02: # is there enough time to sleep before the next row? + elif t <= target_t - delay - 0.02: # is there enough time to sleep before the next row? time.sleep(delay) # sleep to give other threads time to run - if not self.stopped() and not g.exitFlag and all_connected: # sequence ended without interruption - ui_print("Sequence executed, powering down channels.") - elif all_connected: # interrupted by user - ui_print("Sequence cancelled, powering down channels.") - elif not all_connected: # interrupted by device error - ui_print("Error with at least one device, sequence aborted.") - messagebox.showwarning("Device Error!", "Error with at least one device, sequence aborted.") - else: # if this happens there is a mistake in the logic above, it really should not - # tell the user something weird happened: - ui_print("Encountered unexpected sequence end state:" - "\nThread Stopped:", self.stopped(), ", Application Closed:", g.exitFlag, - ", Devices connected:", all_connected) - messagebox.showwarning("Unexpected state", - "Encountered unexpected sequence end state, see console output for details.") - - func.power_down_all() # set currents and voltages to 0, set arduino pins to low + ui_print("Sequence executed, powering down channels.") def read_csv_to_array(filepath): # convert a given csv file to a numpy array # csv format: time (s); xField (T); yField (T); zField (T) (german excel) - # decimal commas - file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file without column headers - array = file.to_numpy() # convert csv to array - return array + # decimal or period commas. Do not use these characters as a thousands seperator! + with open(filepath, 'r') as csv_file: + # Normalize seperators + csv_string = csv_file.read().replace(',', '.') + # read csv file without column headers + file = pandas.read_csv(StringIO(csv_string), sep=';', decimal='.', header=0) + array = file.to_numpy() # convert csv to array + return array def check_array_ok(array): - # check if any magnetic fields in an array exceed the test bench limits and if so display a warning message - values_ok = True + """Checks if values are within limits, and if not shows a warning message.""" + # check if any magnetic fields in an array exceed the test bench limits + warnings = [] for i in [0, 1, 2]: # go through axes/columns - max_val = g.AXES[i].max_comp_field[1] # get limits the test bench can do - min_val = g.AXES[i].max_comp_field[0] - data = array[:, i + 1] # extract data for this axis from array - # noinspection PyTypeChecker - if any(data > max_val) or any(data < min_val): # if any datapoint is out of bounds - values_ok = False - if not values_ok: # show warning pop-up if values are exceeding limits - messagebox.showwarning("Value Limits Warning!", "Found field values exceeding limits of test bench." - "\nSee plot and check values in csv.") + # get limits the test bench can do + min_val, max_val = g.CAGE_DEVICE.axes[i].max_comp_field + for row_idx in range(array.shape[0]): + data_point = array[row_idx, i + 1] # extract data for this axis from array + if data_point > max_val or data_point < min_val: + # Out of bounds + warnings.append({'row': row_idx+1, 'axis': g.AXIS_NAMES[i]}) + + # show warning pop-up if values are exceeding limits + nr_warnings = len(warnings) + if nr_warnings > 0: + warning_msg = "Found field values exceeding limits of test bench.\n" + # Only print the first three warnings + for i in range(min(nr_warnings, 3)): + warning_msg += "[Line {}] {} exceeds limits.\n".format(warnings[i]['row'], warnings[i]['axis']) + if nr_warnings > 3: + warning_msg += "And {} more...".format(nr_warnings - 3) + # Show all warnings collectively + messagebox.showwarning("Value Limits Warning!", warning_msg) def plot_field_sequence(array, width, height): # create plot of fixed size (pixels) from array @@ -156,8 +175,7 @@ def plot_field_sequence(array, width, height): # create plot of fixed size (pix t = new_array[:, 0] # extract time column for i in [0, 1, 2]: # go through all three axes data = new_array[:, i + 1] * 1e6 # extract field column of this axis and convert to microtesla - max_val = g.AXES[i].max_comp_field[1] * 1e6 # get limits of achievable field - min_val = g.AXES[i].max_comp_field[0] * 1e6 + min_val, max_val = g.CAGE_DEVICE.axes[i].max_comp_field * 1e6 # get limits of achievable field plot = axes[i] # get appropriate subplot plot.plot(t, data, linestyle='solid', marker='.') # plot data diff --git a/src/user_interface.py b/src/user_interface.py index 0278aa2..39cda8c 100644 --- a/src/user_interface.py +++ b/src/user_interface.py @@ -14,7 +14,7 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg # import general packages: import numpy as np import os -from os.path import exists +import os.path import threading from datetime import datetime from math import pi @@ -51,6 +51,19 @@ class HelmholtzGUI(Tk): self.Menu = TopMenu(self) # display dropdown menu bar at the top (see TopMenu class for details) + # 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") + main_area = Frame(self, padx=10, pady=10) # create main area Frame where controls of each mode are displayed main_area.pack(side="top", fill="both", expand=True) # pack main area at the top of the window @@ -72,19 +85,6 @@ class HelmholtzGUI(Tk): 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 @@ -361,16 +361,19 @@ class ExecuteCSVMode(Frame): # Functional init: self.csv_thread = None # the thread object for executing a csv sequence self.sequence_array = None # array containing the values from the csv file + self.sequence_array_ok = False # Is the data valid? - # Build UI: - self.grid_rowconfigure(ALL, weight=1) # configure rows and columns of the Tkinter grid to expand with window - self.grid_columnconfigure(ALL, weight=1) + # Tkinter variables for axis hardware checks. Controlled by checkboxes. + self.xy_override = BooleanVar(value=False) # True to disable connection check for XY PSU + self.z_override = BooleanVar(value=False) # True to disable connection check for Z PSU + self.arduino_override = BooleanVar(value=False) # True to disable connection check for arduino + # --- UI ELEMENTS --- row_counter = 0 # keep track of which grid row we are in self.row_elements = [] # make list of elements in rows to later calculate height available for plot - # setup headline + # setup header 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) # add to list of row elements @@ -380,8 +383,6 @@ class ExecuteCSVMode(Frame): # 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) # add frame to list of row elements @@ -419,10 +420,6 @@ class ExecuteCSVMode(Frame): checkbox_label = Label(self.checkbox_frame, text="Disable device connection checks:") checkbox_label.grid(row=0, column=0, sticky=W, padx=3) - # create variables for the checkboxes: - self.xy_override = BooleanVar(value=False) # True to disable connection check for XY PSU - self.z_override = BooleanVar(value=False) # True to disable connection check for Z PSU - self.arduino_override = BooleanVar(value=False) # True to disable connection check for arduino # create checkboxes: xy_checkbox = Checkbutton(self.checkbox_frame, text="XY PSU", variable=self.xy_override, onvalue=True, offvalue=False) @@ -437,10 +434,12 @@ class ExecuteCSVMode(Frame): row_counter += 1 # make frame for plot of csv values (plot is generated and placed in display_plot() method) - 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) + self.plot_frame = Frame(self) + self.plot_frame.grid_rowconfigure(0, weight=1) + self.plot_frame.grid_columnconfigure(0, weight=1) + self.plot_frame.grid(row=row_counter, column=0, sticky="nsw", padx=10, pady=10) + + self.plot_canvas = None # Is generated upon plotting 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 @@ -451,19 +450,22 @@ class ExecuteCSVMode(Frame): # open file selection dialogue and store 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) + if os.path.exists(filename): # does the file exist? + ui_print("CSV file selected:", filename) try: # try to read data to an array self.sequence_array = csv_threading.read_csv_to_array(filename) # read array from csv - except BaseException as e: # something went wrong, probably wrong format in csv + except Exception as e: # something went wrong, probably wrong format in csv + self.sequence_array_ok = False # display error messages: ui_print("Error while opening file:", e) messagebox.showerror("Error!", "Error while opening file: \n%s" % e) try: # try to check the values and display the plot csv_threading.check_array_ok(self.sequence_array) # check for values exceeding limits + self.sequence_array_ok = True # Has nothing to do with limits. Just means the data was parsed self.display_plot() # plot data and display - except BaseException as e: # something went wrong, probably wrong format in csv + except Exception as e: # something went wrong, probably wrong format in csv + self.sequence_array_ok = False # display error messages: ui_print("Error while processing data from file:", e) messagebox.showerror("Error!", "Error while processing data from file: \n%s" % e) @@ -477,19 +479,18 @@ class ExecuteCSVMode(Frame): messagebox.showerror("File not found", "Selected file %s does not exist, could not load." % filename) def run_sequence(self): # called on run button press, starts thread for executing the sequence - # (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" - - # setup thread for running the sequence - # More info: https://www.tutorialspoint.com/python/python_multithreading.htm - g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later # create thread object: + try: + self.csv_thread = csv_threading.ExecCSVThread(self.sequence_array, self, self.controller) + self.csv_thread.start() # start thread - self.csv_thread = csv_threading.ExecCSVThread(self.sequence_array, self, self.controller, logging_enabled) - self.csv_thread.start() # start thread + # (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" + except DeviceAccessError as e: + ui_print(e) def stop_run(self): # called on stop button press, interrupts sequence execution self.csv_thread.stop() # call stop method of thread object, this will cause the csv loop to end @@ -505,27 +506,38 @@ class ExecuteCSVMode(Frame): logger.log_datapoint() # log data def reinitialize(self): # called on "Reinitialize devices" button press - func.setup_all() # reinitialize all PSUs and the Arduino + # reinitialize all PSUs and the Arduino + g.CAGE_DEVICE.reconnect_hardware() # 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 bench is commanded logger.log_datapoint() # log data - def display_plot(self): # generate and display a plot of the data loaded from a csv file + def display_plot(self): + """Generate and display a plot of the data loaded from a csv file""" + if not self.sequence_array_ok: + return # calculate available height for plot (in pixels): height_others = 0 # initialize variable to calculate height of other widgets 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 + # calculate available plot height: height = self.parent.winfo_height() - height_others - 50 # height of parent frame - other widgets - margin - width = min(self.parent.winfo_width() - 100, 1100) # set width to available space but max. 1100 + # Create plot figure = csv_threading.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 the UI + # Clear previous plots first + try: + self.plot_canvas.get_tk_widget().destroy() + except: + pass + # Show new plot + self.plot_canvas = FigureCanvasTkAgg(figure, self.plot_frame) # create canvas to draw figure on + self.plot_canvas.draw() # equivalent to matplotlib.show() + self.plot_canvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in the UI class CalibrateAmbientField(Frame):