From ca2f094ef84436b26de666303762014529043470 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Wed, 10 Feb 2021 11:34:33 +0100 Subject: [PATCH] restructured program end, tried to fix threading issue on window close (not yet fixed) --- User_Interface.py | 28 +++++++++++---- cage_func.py | 3 +- csv_threading.py | 91 ++++++++++++++++++++++++++--------------------- globals.py | 3 +- main.py | 25 ++++++++++--- 5 files changed, 95 insertions(+), 55 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 3967320..7226c5a 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -8,6 +8,7 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np import os from os.path import exists +import threading import globals as g import cage_func as func @@ -519,8 +520,8 @@ class ExecuteCSVMode(Frame): 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 - g.running = False # variable to turn thread execution on or off # Build UI: self.grid_rowconfigure(ALL, weight=1) @@ -625,14 +626,13 @@ class ExecuteCSVMode(Frame): self.stop_button["state"] = "normal" self.reinit_button["state"] = "disabled" - # g.threadLock = threading.Lock() + # 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: - g.running = True - csv_thread = csv.ExecCSVThread("CSV_Thread", self.sequence_array, self, self.controller) - csv_thread.start() # start thread + self.csv_thread = csv.ExecCSVThread("CSV_Thread", self.sequence_array, self, self.controller) + self.csv_thread.start() # start thread def stop_run(self): - g.running = False # this will cause the csv loop to end + 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" @@ -654,6 +654,20 @@ class ExecuteCSVMode(Frame): plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in UI +class ConfigureLogging(Frame): + # generate window to configure data logging + + 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.grid_rowconfigure(ALL, weight=1) + self.grid_columnconfigure(ALL, weight=1) + + row_counter = 0 + + class StatusDisplay(Frame): def __init__(self, parent, controller): @@ -753,7 +767,7 @@ 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 main window is still open + 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 diff --git a/cage_func.py b/cage_func.py index ab6355b..904d510 100644 --- a/cage_func.py +++ b/cage_func.py @@ -282,7 +282,6 @@ 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 shut down all devices. Check equipment to confirm." if g.XY_DEVICE is not None: @@ -332,7 +331,7 @@ def shut_down_all(): # shutdown at program end or on error, set outputs to 0 an ui_print("Serial connection to Arduino closed.") message += "\nSerial connection to Arduino closed." - messagebox.showinfo("Shutdown Status", message) + messagebox.showinfo("Program ended", message) def set_field_simple(vector): # forms magnetic field as specified by vector, w/o cancelling ambient field diff --git a/csv_threading.py b/csv_threading.py index e49c0dc..b43556c 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -19,60 +19,69 @@ class ExecCSVThread(Thread): self.parent = parent # object from which this is called self.controller = controller # object on which mainloop() is running, usually main window + self.__stop_event = Event() + def run(self): 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.parent, self.controller) # run sequence - self.parent.running = False # sequence finished --> no longer running + self.execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence # 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" + if not g.exitFlag: # program 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" + def stop(self): # stop thread execution + self.__stop_event.set() -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 + def stopped(self): + return self.__stop_event.is_set() - # 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 + def execute_sequence(self, 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 - i = 0 - while i < len(array) and g.running and all_connected: - # while array is not finished, user has not cancelled and devices are connected + # Check if everything is properly connected: + all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), parent.arduino_override.get()) + # True or False depending on devices status, checks for some devices may be overridden by user - 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(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 + i = 0 + while i < len(array) and all_connected and not self.stopped() and not g.exitFlag: + # while array is not finished, devices are connected, user has not cancelled and application is running - elif t >= array[i, 0] - delay - 0.02: # next change time is close, not enough time to sleep - pass - else: # sleep to give other threads time to run - time.sleep(delay) + t = time.time() - t_zero # get relative time + if t >= array[i, 0]: # time for this row has come + # g.threadLock.acquire(0) + 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(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 + # g.threadLock.release() - # 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) + elif t >= array[i, 0] - delay - 0.02: # next change time is close, not enough time to sleep + pass + else: # sleep to give other threads time to run + time.sleep(delay) - if g.running and all_connected: # sequence ended without interruption - ui.ui_print("Sequence executed, powering down channels.") - 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 + # check again if everything is connected before starting next loop run: + all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), parent.arduino_override.get()) + + if not self.stopped() and not g.exitFlag and all_connected: # sequence ended without interruption + ui.ui_print("Sequence executed, powering down channels.") + elif all_connected: # interrupted by user + ui.ui_print("Sequence cancelled, powering down channels.") + elif not self.stopped() and not g.exitFlag: # 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 def read_csv_to_array(filepath): diff --git a/globals.py b/globals.py index ffe1ec8..3339517 100644 --- a/globals.py +++ b/globals.py @@ -21,7 +21,8 @@ global Z_PORT global PORTS global threadLock -running = False + +exitFlag = True # False when main window is open, false otherwise # Default Constants and maximum/minimum values (warning messages will be generated if these are exceeded) # format: [[default values], [maximum values], [minimum values]] diff --git a/main.py b/main.py index 30cea3b..22b374e 100644 --- a/main.py +++ b/main.py @@ -5,10 +5,23 @@ from tkinter import messagebox import cage_func as func from User_Interface import HelmholtzGUI from User_Interface import ui_print +import User_Interface as ui import globals as g import config_handling as config +def program_end(): + g.exitFlag = True # tell everything else the application has been closed + if g.app is not None: + if g.app.pages[ui.ExecuteCSVMode].csv_thread is not None: # end possible csv execution thread + g.app.pages[ui.ExecuteCSVMode].csv_thread.stop() # stop thread + g.app.pages[ui.ExecuteCSVMode].csv_thread.join() # wait for thread to finish + # g.app = None # reset to None so nothing tries to print in the UI output + func.shut_down_all() # shut down devices + if g.app is not None: + g.app.destroy() # close application + + try: # start normal operations config.CONFIG_FILE = 'config.ini' @@ -26,6 +39,7 @@ try: # start normal operations print("\nOpening User Interface...") g.app = HelmholtzGUI() + g.exitFlag = False 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") @@ -33,19 +47,22 @@ try: # start normal operations ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once func.setup_all() # initiate communication, set handles + + g.app.protocol("WM_DELETE_WINDOW", program_end) # call program end function if user closes the application + g.app.mainloop() - g.app = None # reset to None so nothing tries to print in the UI output -except BaseException as e: # if there is an error, print what happened +except Exception as e: # if there is an error, print what happened print("\nAn error occurred, Shutting down.") # shop pup-up error message: message = "%s.\nSee python console traceback for more details. " \ "\nShutting down devices, check equipment to confirm." % e messagebox.showerror("Error!", message) print(traceback.print_exc()) + program_end() # safely close everything and shut down devices -finally: # safely shut everything down at the end - func.shut_down_all() +# ToDo: rework window closing code https://bytes.com/topic/python/answers/431323-detect-tkinter-window-being-closed +# https://stackoverflow.com/questions/14694408/runtimeerror-main-thread-is-not-in-main-loop # ToDo: logging