diff --git a/User_Interface.py b/User_Interface.py index f33497b..6b2aa7e 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -108,6 +108,7 @@ class ManualMode(Frame): row_counter = 0 + # setup title text header = Label(self, text="Manual Input Mode", font=HEADER_FONT, pady=3) header.grid(row=row_counter, column=0) @@ -187,12 +188,12 @@ class ManualMode(Frame): 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=func.power_down_all, + 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 - reinit_button = Button(self.buttons_frame, text="Reinitialize", command=func.setup_all, + 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) @@ -206,10 +207,11 @@ class ManualMode(Frame): 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, e.g. calls update_max_fields function + self.modes[self.input_mode.get()][2]() # update max values and units, e.g. calls update_max_fields function # noinspection PyUnusedLocal - def change_mode_callback(self, var, index, mode): # not sure what the parameters are for, but they are necessary + # 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 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 @@ -235,6 +237,22 @@ class ManualMode(Frame): val.set("(%0.2f to %0.2f A)" % (-g.AXES[i].max_amps, g.AXES[i].max_amps)) 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): function_to_call = self.modes[self.input_mode.get()][0] # get function of appropriate mode vector = np.array([0, 0, 0], dtype=float) @@ -245,6 +263,11 @@ class ManualMode(Frame): function_to_call(vector) # call function 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): ui_print("field executing", vector) comp = self.compensate.get() @@ -538,6 +561,7 @@ class ExecuteCSVMode(Frame): 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) @@ -564,7 +588,7 @@ class ExecuteCSVMode(Frame): 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, + 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) @@ -647,6 +671,19 @@ class ExecuteCSVMode(Frame): 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 @@ -665,7 +702,6 @@ class ExecuteCSVMode(Frame): class ConfigureLogging(Frame): # generate window to configure data logging to csv # ToDo: support logging of axis-independent info like Arduino status - # ToDo (optional): implement logging of a datapoint every time new commands are sent to the test stand def __init__(self, parent, controller): Frame.__init__(self, parent) @@ -674,18 +710,28 @@ class ConfigureLogging(Frame): 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=10) + 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) @@ -718,6 +764,34 @@ class ConfigureLogging(Frame): 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) @@ -748,26 +822,38 @@ class ConfigureLogging(Frame): def start_logging(self): ui_print("Started data logging.") self.update_choices() # update list with ticked checkboxes - self.regular_logging = True + 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 - self.periodic_log(1000) # start periodic logging ToDo: get interval from entry field - # 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 + 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 + 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 button to start + 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 @@ -788,6 +874,7 @@ class ConfigureLogging(Frame): 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): @@ -798,21 +885,29 @@ class ConfigureLogging(Frame): if self.checkbox_vars[key].get(): # box is ticked self.active_keys.append(key) # add corresponding item to the list - def lock_checkboxes(self): - for checkbox in self.checkboxes: + 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: + 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 - log.log_datapoint(self.active_keys) # add datapoint with active keys to log data frame - self.controller.after(interval, lambda: self.periodic_log(interval)) # call function again after interval + 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: # logging is active + 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 diff --git a/cage_func.py b/cage_func.py index 904d510..188c4c4 100644 --- a/cage_func.py +++ b/cage_func.py @@ -260,16 +260,7 @@ def activate_all(): # enables remote control and output on all PSUs and channel g.Z_DEVICE.enable_all() -def print_status_3(): - ui_print("X-Axis:") - g.X_AXIS.print_status() - ui_print("Y-Axis:") - g.Y_AXIS.print_status() - ui_print("Z-Axis:") - g.Z_AXIS.print_status() - - -def set_to_zero(device): # sets voltages and currents to 0 +def set_to_zero(device): # sets voltages and currents to 0 on all channels of a specific PSU device.voltage1 = 0 device.current1 = 0 device.voltage2 = 0 diff --git a/csv_threading.py b/csv_threading.py index 3721dcc..0c9b25d 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -23,8 +23,6 @@ class ExecCSVThread(Thread): 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 self.execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence # reset buttons on UI: if not g.exitFlag: # program window is open @@ -64,6 +62,12 @@ class ExecCSVThread(Thread): 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 + + # 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 stand is commanded + logger.log_datapoint() # log data + i = i + 1 # next row g.threadLock.release() # allow going back to main thread now