# 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. # ToDo: optimize layout for smaller screen (like on IRS clean room PC) # import packages for user interface: import queue from queue import Queue, Empty from tkinter import * from tkinter import ttk from tkinter import messagebox from tkinter import filedialog from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg # import general packages: import numpy as np import os import os.path import threading from datetime import datetime from math import pi import csv # import other project files: import src.globals as g import src.csv_threading as csv_threading import src.config_handling as config import src.csv_logging as log from src.calibration import AmbientFieldCalibration, CoilConstantCalibration, MagnetometerCalibration from src.exceptions import DeviceAccessError from src.utility import ui_print, save_dict_list_to_csv import src.helmholtz_cage_device as helmholtz_cage_device # define font styles: HEADER_FONT = ("Arial", 13, "bold") SUB_HEADER_FONT = ("Arial", 9, "bold") BIG_BUTTON_FONT = ("Arial", 11, "bold") SMALL_BUTTON_FONT = ("Arial", 9) class HelmholtzGUI(Tk): # main application window, almost everything else here is 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") # set title of the window try: Tk.wm_iconbitmap(self, "Helmholtz.ico") # set application icon except TclError: pass 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 main_area.grid_rowconfigure(0, weight=1) # configure rows and columns of the Tkinter grid to expand with window main_area.grid_columnconfigure(0, weight=1) # 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 self.pages = {} # dictionary for storing all pages (different modes, displayed in main area) for P in [ManualMode, HardwareConfiguration, CalibrateAmbientField, CalibrateMagnetometer, ExecuteCSVMode, ConfigureLogging]: # do this for every mode page page = P(main_area, self) # initialize the page with the main_area 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 self.show_frame(ManualMode) # show manual mode to start with 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() # bring this frame to the front class TopMenu: # the menu bar at the top of the window def __init__(self, window): self.window = window menu = Menu(window) # initialize Menu object window.config(menu=menu) # put menu at the top of the window mode_selector = Menu(menu, tearoff=0) # create a submenu object menu.add_cascade(label="Menu", menu=mode_selector) # add a dropdown with the submenu object # create the different options in the dropdown: mode_selector.add_command(label="Static Manual Input", command=self.manual_mode) mode_selector.add_command(label="Execute CSV Sequence", command=self.execute_csv_mode) mode_selector.add_command(label="Calibrate Ambient Field", command=self.calibrate_ambient) mode_selector.add_command(label="Calibrate Magnetometer", command=self.calibrate_magnetometer) mode_selector.add_separator() mode_selector.add_command(label="Configure Data Logging", command=self.logging) mode_selector.add_command(label="Settings...", command=self.configuration) def manual_mode(self): # switch to the manual mode page self.window.show_frame(ManualMode) def configuration(self): # switch to the settings page self.window.show_frame(HardwareConfiguration) def calibrate_ambient(self): self.window.show_frame(CalibrateAmbientField) def calibrate_magnetometer(self): self.window.show_frame(CalibrateMagnetometer) def execute_csv_mode(self): # switch to the CSV execution page self.window.show_frame(ExecuteCSVMode) def logging(self): # switch to the logging settings page self.window.show_frame(ConfigureLogging) class ManualMode(Frame): # Mode for manually setting currents and fields on the test bench. # 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) # initialize the frame object self.controller = controller # object on which mainloop() is running, usually main window 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 # 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) header.grid(row=row_counter, column=0) row_counter += 1 # Setup Dropdown Menu for input mode 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) # 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 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) # 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:"] # 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: # 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) # create label showing the axis axis_label.grid(row=row, column=0, sticky=W) 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 row_counter += 1 # setup checkbox for compensating ambient field checkbox_frame = Frame(self, padx=20) # create frame to house it checkbox_frame.grid(row=row_counter, column=0, sticky=W) 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=True, offvalue=False) self.compensate_checkbox.pack(side="left") row_counter += 1 # Setup buttons self.buttons_frame = Frame(self) self.buttons_frame.grid_rowconfigure(ALL, weight=1) self.buttons_frame.grid_columnconfigure(ALL, weight=1) self.buttons_frame.grid(row=row_counter, column=0) Label(self.buttons_frame, text="").grid(row=0, column=0) # add spacer # add button for executing the current entries execute_button = Button(self.buttons_frame, text="Execute", command=self.execute, pady=5, padx=5, font=BIG_BUTTON_FONT) 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=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 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 Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer self.input_mode.trace_add('write', self.change_mode_callback) # call mode change function on dropdown change self.input_mode.set(default_mode) # call up default mode at the start 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 and units, e.g. calls update_max_fields function # noinspection PyUnusedLocal # 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 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) for i, var in enumerate(self.max_value_vars): # go through the max value labels for each axis if not compensate: # ambient field should not be compensated field = g.CAGE_DEVICE.axes[i].max_field * 1e6 # get max values from the axis object elif compensate: # ambient field should be compensated field = g.CAGE_DEVICE.axes[i].max_comp_field * 1e6 else: # this really should never happen field = [0, 0] 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 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 for i, var in enumerate(self.max_value_vars): # go through the max value labels for each axis # update the label var.set("(%0.2f to %0.2f A)" % (-g.CAGE_DEVICE.axes[i].max_amps, g.CAGE_DEVICE.axes[i].max_amps)) def reinitialize(self): # called on "Reinitialize!" button press # reinitialize all PSUs and the Arduino g.CAGE_DEVICE.reconnect_hardware_async() # 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 power_down(self): # called on "power down" button press g.CAGE_DEVICE.shutdown() # 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 bench is commanded logger.log_datapoint() # log data def execute(self): # called on "Execute!" button press # reads values from the entry fields and commands the test bench accordingly vector = np.array([0, 0, 0], dtype=float) # initialize vector to later send to test bench i = 0 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()) # 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 execute_field(self, vector): # convert magnetic field vector and send to test bench ui_print("\nField executing:", vector, "\u03BCT") # Acquire a proxy to the helmholtz cage: # This can fail if already in use try: with g.CAGE_DEVICE as cage_dev: compensate = self.compensate.get() # read out if compensate ambient field checkbox is ticked if compensate: # ambient field should be compensated cage_dev.set_field_compensated(vector * 1e-6) # convert to Tesla and send to test bench pass elif not compensate: # ambient field should not be compensated cage_dev.set_field_raw(vector * 1e-6) # convert to Tesla and send to test bench pass else: # this really should never happen ui_print("Unexpected value encountered: compensate =", compensate) messagebox.showerror("Unexpected Value!", ("Unexpected value encountered: compensate =", compensate)) except helmholtz_cage_device.DeviceBusy: ui_print("Error: Could not acquire control. Is the HW already in use?") @staticmethod def execute_current(vector): # send current vector to the test bench ui_print("\nCurrent executing:", vector, "A") with g.CAGE_DEVICE as cage_dev: # This can fail if already in use if cage_dev is None: ui_print("Error: Could not acquire control. Is the HW already in use?") return cage_dev.set_signed_currents(vector) # command test bench class ExecuteCSVMode(Frame): # Mode for executing magnetic field sequences from csv files. # Inherits the Frame object from Tkinter and is placed in the mainArea of the application window. # Multithreading is used to execute the sequence without crashing the UI def __init__(self, parent, controller): Frame.__init__(self, parent) self.parent = parent # parent UI object, e.g. the mainArea frame self.controller = controller # object on which mainloop() is running, usually main window # 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? # 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 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 row_counter += 1 # Setup buttons # Setup frame to house buttons: self.top_buttons_frame = Frame(self) 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 # Create and place buttons: # add button for selecting a csv file to execute: 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) # add button to start running the sequence from the file: 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) # add button to stop/interrupt the sequence: 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 to reinitialize devices (e.g. after reconnecting a device) 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 checkboxes to disable checks if devices are connected (enables testing with some devices missing) # setup frame to house 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) # add frame to list of row elements # setup label to explain checkbox function: checkbox_label = Label(self.checkbox_frame, text="Disable device connection checks:") checkbox_label.grid(row=0, column=0, sticky=W, padx=3) # create checkboxes: 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 (plot is generated and placed in display_plot() method) 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 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 store path of selected file filename = filedialog.askopenfilename(initialdir=directory, title="Select CSV File", filetypes=(("Comma Separated Values", "*.csv*"), ("All Files", "*.*"))) 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 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 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) else: # nothing went wrong self.execute_button["state"] = "normal" # activate run button --> enable execution elif filename == '': # this happens when file selection window is closed without selecting a file ui_print("No file selected, could not load.") else: # file does not exist # display error messages: ui_print("Selected file", filename, "does not exist, could not load.") 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 # create thread object: try: self.csv_thread = csv_threading.ExecCSVThread(self.sequence_array, self, self.controller) 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 # (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 bench is commanded logger.log_datapoint() # log data def reinitialize(self): # called on "Reinitialize devices" button press # reinitialize all PSUs and the Arduino g.CAGE_DEVICE.reconnect_hardware_async() # 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""" 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 # 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): def __init__(self, parent, controller): Frame.__init__(self, parent) self.parent = parent self.controller = controller # To center window # self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.left_column = Frame(self) self.left_column.grid(row=0, column=0, sticky="nsew") self.right_column = Frame(self) self.right_column.grid(row=0, column=1, sticky="nsew") self.left_column.rowconfigure(3, weight=1) # Thread variables self.calibration_ambient_thread = None self.calibration_coil_constants_thread = None self.view_mpi_queue = Queue() # Receives status information from calibration procedure threads. # UI variables self.connected_state_var = StringVar(value="Not connected") self.field_value_vars = [StringVar(value="No data"), StringVar(value="No data"), StringVar(value="No data")] self.calibration_procedure_progress_var = IntVar(value=0) # Contains results for ambient field calibration self.ambient_field_result_vars = [StringVar(), StringVar(), StringVar()] self.ambient_field_ut_result_vars = [StringVar(), StringVar(), StringVar()] self.ambient_field_residual_vars = [StringVar(), StringVar(), StringVar()] self.ambient_field_raw_data = None # Used for export to csv self.ambient_field_result = None # Used for saving to config file self.ambient_field_clipboard = "" # Used for copying to clipboard # Contains results for coil constant calibration self.coil_constant_vars = [StringVar(), StringVar(), StringVar()] self.coil_constant_dev_vars = [StringVar(), StringVar(), StringVar()] self.coil_angle_vars = [StringVar(), StringVar(), StringVar()] self.coil_constant_raw_data = None # Used for export to csv self.coil_constant_results = None # Used for saving to config file self.coil_constant_clipboard = "" # Used for copying to clipboard row_counter = 0 # Create headline header = Label(self.left_column, text="Ambient Field Calibration", font=HEADER_FONT) header.grid(row=row_counter, column=0, columnspan=2, padx=100, pady=20, sticky="nw") row_counter += 1 # Magnetometer connected indicator connected_status_frame = Frame(self.left_column) connected_status_frame.grid(row=row_counter, column=0, sticky="nw") connected_label = Label(connected_status_frame, text="Magnetometer state:", font=SUB_HEADER_FONT) connected_label.grid(row=0, column=0, padx=10, pady=20, sticky="nw") self.connected_state_label = Label(connected_status_frame, textvariable=self.connected_state_var, fg="red") self.connected_state_label.grid(row=0, column=1, padx=10, pady=20, sticky="nw") row_counter += 1 # Magnetometer field data grid field_data_frame = Frame(self.left_column) field_data_frame.grid(row=row_counter, column=0, sticky="nw") field_data_label = Label(field_data_frame, text="Field data:", font=SUB_HEADER_FONT) field_data_label.grid(row=0, column=0, padx=10, pady=3, sticky="nw") axis_labels = ['X:', 'Y:', 'Z:'] for i in range(3): field_data_axis_label = Label(field_data_frame, text=axis_labels[i]) field_data_axis_label.grid(row=i, column=1, padx=10, pady=3) field_data_axis_data = Label(field_data_frame, textvariable=self.field_value_vars[i]) field_data_axis_data.grid(row=i, column=2, padx=(20, 0), pady=3) field_data_axis_units = Label(field_data_frame, text="\u03BCT") field_data_axis_units.grid(row=i, column=3, padx=5, pady=3) row_counter += 1 # Calibration start and save to csv buttons start_button_frame = Frame(self.left_column) start_button_frame.grid(row=row_counter, column=0, sticky="sw") self.start_ambient_calibration_button = Button(start_button_frame, text="Calibrate Ambient Field", command=self.calibration_procedure_ambient, pady=5, padx=5, font=SMALL_BUTTON_FONT) self.start_ambient_calibration_button.grid(row=0, column=0, padx=10, pady=10) self.start_k_calibration_button = Button(start_button_frame, text="Calibrate Coil Constants", command=self.calibration_procedure_coil_constants, pady=5, padx=5, font=SMALL_BUTTON_FONT) self.start_k_calibration_button.grid(row=0, column=1, padx=10, pady=10) row_counter += 1 # Calibration progress bar progress_bar_frame = Frame(self.left_column) progress_bar_frame.grid(row=row_counter, column=0, sticky="swe") calibration_procedure_progress_label = Label(progress_bar_frame, text="Progress:") calibration_procedure_progress_label.grid(row=0, column=0, padx=10, pady=10) calibration_procedure_progress = ttk.Progressbar(progress_bar_frame, length=240, variable=self.calibration_procedure_progress_var) calibration_procedure_progress.grid(row=0, column=1, padx=10, pady=10, sticky="we") row_counter += 1 # --- Ambient field calibration results --- row_counter = 0 ambient_field_results_frame = LabelFrame(self.right_column, text="Ambient Field Results") ambient_field_results_frame.grid(row=row_counter, column=1, padx=(100, 0), pady=5, sticky="nw") for i, label in enumerate(['X', 'Y', 'Z']): axis_label = Label(ambient_field_results_frame, text=label) axis_label.grid(row=0, column=i + 1, padx=5, pady=(5, 0), sticky="nw") # Ambient field value (A) ambient_field_results_label = Label(ambient_field_results_frame, text="Ambient Field:") ambient_field_results_label.grid(row=1, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(ambient_field_results_frame, textvariable=self.ambient_field_result_vars[i], width=15, state='readonly') axis_data.grid(row=1, column=i + 1, padx=5, pady=5, sticky="nw") ambient_field_results_unit = Label(ambient_field_results_frame, text="A") ambient_field_results_unit.grid(row=1, column=4, padx=5, pady=5, sticky="nw") # Ambient field value (microtesla) ambient_field_results_ut_label = Label(ambient_field_results_frame, text="Ambient Field:") ambient_field_results_ut_label.grid(row=2, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(ambient_field_results_frame, textvariable=self.ambient_field_ut_result_vars[i], width=15, state='readonly') axis_data.grid(row=2, column=i + 1, padx=5, pady=5, sticky="nw") ambient_field_results_ut_unit = Label(ambient_field_results_frame, text="\u03BCT") ambient_field_results_ut_unit.grid(row=2, column=4, padx=5, pady=5, sticky="nw") # Residuals ambient_field_residual_label = Label(ambient_field_results_frame, text="Residual Field:") ambient_field_residual_label.grid(row=3, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(ambient_field_results_frame, textvariable=self.ambient_field_residual_vars[i], width=15, state='readonly') axis_data.grid(row=3, column=i + 1, padx=5, pady=5, sticky="nw") ambient_field_residual_unit = Label(ambient_field_results_frame, text="\u03BCT") ambient_field_residual_unit.grid(row=3, column=4, padx=5, pady=5, sticky="nw") # Save calibration buttons save_ambient_field_results_frame = Frame(ambient_field_results_frame) save_ambient_field_results_frame.grid(row=4, column=0, columnspan=5) # Save and apply self.ambient_field_save_results_button = Button(save_ambient_field_results_frame, text="Save and apply", command=self.save_and_apply_ambient_calibration, state="disabled") self.ambient_field_save_results_button.grid(row=0, column=0, padx=5, pady=5) self.save_ambient_calibration_button = Button(save_ambient_field_results_frame, text="Export raw CSV", command=self.save_to_csv_ambient_field, state="disabled") self.save_ambient_calibration_button.grid(row=0, column=1, padx=5, pady=5) self.copy_ambient_calibration_button = Button(save_ambient_field_results_frame, text="Copy to clipboard", command=self.copy_to_clipboard_ambient_field, state="disabled") self.copy_ambient_calibration_button.grid(row=0, column=2, padx=5, pady=5) row_counter += 1 # --- Coil constant results --- coil_constants_results_frame = LabelFrame(self.right_column, text="Coil Constants") coil_constants_results_frame.grid(row=row_counter, column=1, padx=(100, 0), pady=5, sticky="nw") for i, label in enumerate(['X', 'Y', 'Z']): axis_label = Label(coil_constants_results_frame, text=label) axis_label.grid(row=0, column=i + 1, padx=5, pady=(5, 0), sticky="nw") # Coil constants coil_constants_results_label = Label(coil_constants_results_frame, text="K:") coil_constants_results_label.grid(row=1, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(coil_constants_results_frame, textvariable=self.coil_constant_vars[i], width=15, state='readonly') axis_data.grid(row=1, column=i + 1, padx=5, pady=5, sticky="nw") coil_constants_results_unit = Label(coil_constants_results_frame, text="\u03BCT/A") coil_constants_results_unit.grid(row=1, column=4, padx=5, pady=5, sticky="nw") # Standard deviation of coil constants coil_constants_dev_results_label = Label(coil_constants_results_frame, text="K Std. Dev.:") coil_constants_dev_results_label.grid(row=2, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(coil_constants_results_frame, textvariable=self.coil_constant_dev_vars[i], width=15, state='readonly') axis_data.grid(row=2, column=i + 1, padx=5, pady=5, sticky="nw") coil_constants_dev_results_unit = Label(coil_constants_results_frame, text="\u03BCT/A") coil_constants_dev_results_unit.grid(row=2, column=4, padx=5, pady=5, sticky="nw") # Inter-axis angle labels for i, label in enumerate(['X-Y', 'Y-Z', 'X-Z']): axis_label = Label(coil_constants_results_frame, text=label) axis_label.grid(row=3, column=i + 1, padx=5, pady=0, sticky="nw") # Inter-axis angles coil_angles_label = Label(coil_constants_results_frame, text="Angles:") coil_angles_label.grid(row=4, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(coil_constants_results_frame, textvariable=self.coil_angle_vars[i], width=15, state='readonly') axis_data.grid(row=4, column=i + 1, padx=5, pady=5, sticky="nw") coil_angles_unit = Label(coil_constants_results_frame, text="°") coil_angles_unit.grid(row=4, column=4, padx=5, pady=5, sticky="nw") # Save calibration buttons save_coil_constant_results_frame = Frame(coil_constants_results_frame) save_coil_constant_results_frame.grid(row=5, column=0, columnspan=5) # Save and apply self.coil_constant_save_results_button = Button(save_coil_constant_results_frame, text="Save and apply", command=self.save_and_apply_coil_constants, state="disabled") self.coil_constant_save_results_button.grid(row=0, column=0, padx=5, pady=5) self.save_k_calibration_button = Button(save_coil_constant_results_frame, text="Export raw CSV", command=self.save_to_csv_coil_constants, state="disabled") self.save_k_calibration_button.grid(row=0, column=1, padx=5, pady=5) self.copy_coil_constant_button = Button(save_coil_constant_results_frame, text="Copy to clipboard", command=self.copy_to_clipboard_coil_constants, state="disabled") self.copy_coil_constant_button.grid(row=0, column=2, padx=5, pady=5) row_counter += 1 # This starts an endless polling loop self.update_view() def page_switch(self): # every class in the UI needs this, even if it doesn't do anything pass def update_view(self): # Get new connected status if g.MAGNETOMETER.connected: self.connected_state_var.set("connected") self.connected_state_label.configure(fg="green") else: self.connected_state_var.set("Not connected") self.connected_state_label.configure(fg="red") # Get new field data new_field = g.MAGNETOMETER.field for i in range(3): # Display in uT self.field_value_vars[i].set("{:.3f}".format(new_field[i] * 1e6)) # Get mpi messages from calibration procedures try: while True: msg = self.view_mpi_queue.get(block=False) cmd = msg['cmd'] arg = msg['arg'] if cmd == 'finished': self.reactivate_buttons() elif cmd == 'failed': messagebox.showerror("Calibration error", "Error occured during calibration:\n{}".format(arg)) self.reactivate_buttons() elif cmd == 'progress': self.calibration_procedure_progress_var.set(min(int(arg * 100), 100)) elif cmd == 'ambient_data': self.update_ambient_calibration_results(arg) elif cmd == 'coil_constant_results': self.update_coil_constant_results(arg) else: ui_print("Error: Unexpected mpi command '{}' in CalibrationTool".format(cmd)) except queue.Empty: pass self.controller.after(500, self.update_view) def reactivate_buttons(self): self.start_ambient_calibration_button.configure(text="Calibrate Ambient Field", state=NORMAL) self.start_k_calibration_button.configure(text="Calibrate Coil Constants", state=NORMAL) self.calibration_procedure_progress_var.set(0) def deactivate_buttons(self): self.start_ambient_calibration_button.configure(state=DISABLED) self.start_k_calibration_button.configure(state=DISABLED) def update_ambient_calibration_results(self, results): # Update gui for i in range(3): self.ambient_field_result_vars[i].set("{:.3f}".format(results['ambient'][i])) self.ambient_field_ut_result_vars[i].set("{:.3f}".format(results['ambient_ut'][i])) self.ambient_field_residual_vars[i].set("{:.3f}".format(results['residual'][i] * 1e6)) # Populate clipboard string self.ambient_field_clipboard = "\tX\tY\tZ\n" self.ambient_field_clipboard += "Ambient Field [A]" for i in range(3): self.ambient_field_clipboard += "\t{:.3f}".format(results['ambient'][i]) self.ambient_field_clipboard += "\nAmbient Field [uT]" for i in range(3): self.ambient_field_clipboard += "\t{:.3f}".format(results['ambient_ut'][i]) self.ambient_field_clipboard += "\nResidual Field [uT]" for i in range(3): self.ambient_field_clipboard += "\t{:.3f}".format(results['residual'][i] * 1e6) # Cache other data that the user may want to save self.ambient_field_raw_data = results['raw_data'] self.ambient_field_result = results['ambient_ut'] * 1e-6 # Update save-button states self.save_ambient_calibration_button.configure(state='normal') self.ambient_field_save_results_button.configure(state='normal') self.copy_ambient_calibration_button.configure(state='normal') def update_coil_constant_results(self, results): # Update UI for i in range(3): # Remember to convert from T/A to microT/A self.coil_constant_vars[i].set("{:.3f}".format(results['k'][i] * 1e6)) self.coil_constant_dev_vars[i].set("{:.3f}".format(results['k_dev'][i] * 1e6)) # Coil angles use dict access self.coil_angle_vars[0].set("{:.2f}".format(results['angle']['xy'])) self.coil_angle_vars[1].set("{:.2f}".format(results['angle']['yz'])) self.coil_angle_vars[2].set("{:.2f}".format(results['angle']['xz'])) # Populate clipboard string self.coil_constant_clipboard = "\tX\tY\tZ\n" self.coil_constant_clipboard += "K [uT/A]" for i in range(3): self.coil_constant_clipboard += "\t{:.3f}".format(results['k'][i] * 1e6) self.coil_constant_clipboard += "\nK Std. Dev. [uT/A]" for i in range(3): self.coil_constant_clipboard += "\t{:.3f}".format(results['k_dev'][i] * 1e6) self.coil_constant_clipboard += "\n\tX-Y\tY-Z\tX-Z" self.coil_constant_clipboard += "\nAngles [deg]" self.coil_constant_clipboard += "\t{:.3f}".format(results['angle']['xy']) self.coil_constant_clipboard += "\t{:.3f}".format(results['angle']['yz']) self.coil_constant_clipboard += "\t{:.3f}".format(results['angle']['xz']) # Cache other data that the user may want to save self.coil_constant_raw_data = results['raw_data'] self.coil_constant_results = results['k'] # Update save-button states self.save_k_calibration_button.configure(state='normal') self.coil_constant_save_results_button.configure(state='normal') self.copy_coil_constant_button.configure(state='normal') def calibration_procedure_ambient(self): try: self.calibration_ambient_thread = AmbientFieldCalibration(self.view_mpi_queue) self.calibration_ambient_thread.start() self.start_ambient_calibration_button.configure(text="Running") self.deactivate_buttons() except DeviceAccessError as e: messagebox.showwarning("Calibration failed", "Failed to start calibration:\n{}".format(e)) def calibration_procedure_coil_constants(self): try: self.calibration_coil_constants_thread = CoilConstantCalibration(self.view_mpi_queue) self.calibration_coil_constants_thread.start() self.start_k_calibration_button.configure(text="Running") self.deactivate_buttons() except DeviceAccessError as e: messagebox.showwarning("Calibration failed", "Failed to start calibration:\n{}".format(e)) def save_to_csv_coil_constants(self): if self.coil_constant_raw_data is None: ui_print("Error: Failed to export non-existent calibration data.") return save_dict_list_to_csv('coil_constant_calibration.csv', self.coil_constant_raw_data, query_path=True) ui_print("Saved calibration results to coil_constant_calibration.csv.") def save_to_csv_ambient_field(self): if self.ambient_field_raw_data is None: ui_print("Error: Failed to export non-existent calibration data.") return save_dict_list_to_csv('ambient_field_calibration.csv', self.ambient_field_raw_data, query_path=True) ui_print("Saved calibration results to ambient_field_calibration.csv.") def save_and_apply_ambient_calibration(self): if self.ambient_field_result is not None: ui_print("Saving ambient field calibration data") config.edit_config("X-Axis", "ambient_field", self.ambient_field_result[0]) config.edit_config("Y-Axis", "ambient_field", self.ambient_field_result[1]) config.edit_config("Z-Axis", "ambient_field", self.ambient_field_result[2]) ui_print("Reinitializing devices...") g.CAGE_DEVICE.reconnect_hardware_async() # setup everything with the defaults def save_and_apply_coil_constants(self): if self.coil_constant_results is not None: ui_print("Saving coil constant calibration data") config.edit_config("X-Axis", "coil_const", self.coil_constant_results[0]) config.edit_config("Y-Axis", "coil_const", self.coil_constant_results[1]) config.edit_config("Z-Axis", "coil_const", self.coil_constant_results[2]) ui_print("Reinitializing devices...") g.CAGE_DEVICE.reconnect_hardware_async() # setup everything with the defaults def copy_to_clipboard_ambient_field(self): self.clipboard_clear() self.clipboard_append(self.ambient_field_clipboard) self.update() def copy_to_clipboard_coil_constants(self): self.clipboard_clear() self.clipboard_append(self.coil_constant_clipboard) self.update() class CalibrateMagnetometer(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) self.parent = parent self.controller = controller # To center window # self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.left_column = Frame(self) self.left_column.grid(row=0, column=0, sticky="nsew") self.right_column = Frame(self) self.right_column.grid(row=0, column=1, sticky="nsew") self.left_column.rowconfigure(3, weight=1) # Thread variables self.calibration_thread = None self.view_mpi_queue = Queue() # Receives status information from calibration procedure threads. # UI variables self.connected_state_var = StringVar(value="Not connected") self.field_value_vars = [StringVar(value="No data"), StringVar(value="No data"), StringVar(value="No data")] self.calibration_procedure_progress_var = IntVar(value=0) # Calibration parameters self.calibration_points_var = IntVar(value=8) self.calibration_interval_var = DoubleVar(value=5) # Calibration results self.sensitivity_result_vars = [StringVar(), StringVar(), StringVar()] self.offset_result_vars = [StringVar(), StringVar(), StringVar()] self.angle_to_plane_result_vars = [StringVar(), StringVar(), StringVar()] self.angle_in_plane_result_vars = [StringVar(), StringVar(), StringVar()] self.residual_result_vars = [StringVar(), StringVar(), StringVar()] self.calibration_raw_results = None # Cached raw experiment data to allow for saving to csv. self.clipboard = "" # Clipboard string containing results self.mgm_to_helmholtz_cos_trans = [[DoubleVar(value=1), DoubleVar(value=0), DoubleVar(value=0)], [DoubleVar(value=0), DoubleVar(value=1), DoubleVar(value=0)], [DoubleVar(value=0), DoubleVar(value=0), DoubleVar(value=1)]] # UI Elements row_counter = 0 # Create headline header = Label(self.left_column, text="Magnetometer Calibration", font=HEADER_FONT) header.grid(row=row_counter, column=0, columnspan=2, padx=100, pady=20, sticky="nw") row_counter += 1 # Magnetometer connected indicator connected_status_frame = Frame(self.left_column) connected_status_frame.grid(row=row_counter, column=0, sticky="nw") connected_label = Label(connected_status_frame, text="Magnetometer state:", font=SUB_HEADER_FONT) connected_label.grid(row=0, column=0, padx=10, pady=20, sticky="nw") self.connected_state_label = Label(connected_status_frame, textvariable=self.connected_state_var, fg="red") self.connected_state_label.grid(row=0, column=1, padx=10, pady=20, sticky="nw") row_counter += 1 # Magnetometer field data grid field_data_frame = Frame(self.left_column) field_data_frame.grid(row=row_counter, column=0, sticky="nw") field_data_label = Label(field_data_frame, text="Field data:", font=SUB_HEADER_FONT) field_data_label.grid(row=0, column=0, padx=10, pady=3, sticky="nw") axis_labels = ['X:', 'Y:', 'Z:'] for i in range(3): field_data_axis_label = Label(field_data_frame, text=axis_labels[i]) field_data_axis_label.grid(row=i, column=1, padx=10, pady=3) field_data_axis_data = Label(field_data_frame, textvariable=self.field_value_vars[i]) field_data_axis_data.grid(row=i, column=2, padx=(20, 0), pady=3) field_data_axis_units = Label(field_data_frame, text="\u03BCT") field_data_axis_units.grid(row=i, column=3, padx=5, pady=3) row_counter += 1 # Centered controls controls_frame = Frame(self.left_column) controls_frame.grid(row=row_counter, column=0, sticky="sw") # Number of calibration points calibration_point_nr_label = Label(controls_frame, text="# of calibration points") calibration_point_nr_label.grid(row=0, column=0, pady=5, sticky="w") calibration_point_nr_entry = Entry(controls_frame, textvariable=self.calibration_points_var) calibration_point_nr_entry.grid(row=0, column=1, pady=5, sticky="w") # Measurement interval calibration_point_nr_label = Label(controls_frame, text="Measurement interval [s]") calibration_point_nr_label.grid(row=1, column=0, pady=5, sticky="w") calibration_point_nr_entry = Entry(controls_frame, textvariable=self.calibration_interval_var) calibration_point_nr_entry.grid(row=1, column=1, pady=5, sticky="w") # Calibration start buttons start_button_frame = Frame(controls_frame) start_button_frame.grid(row=2, column=0, columnspan=2) self.start_calibration_button = Button(start_button_frame, text="Start Calibration", command=self.start_calibration_procedure, pady=5, padx=5, font=SMALL_BUTTON_FONT) self.start_calibration_button.grid(row=0, column=0, padx=10, pady=(30, 10)) # Calibration progress bar progress_bar_frame = Frame(controls_frame) progress_bar_frame.grid(row=3, column=0, columnspan=2) calibration_procedure_progress_label = Label(progress_bar_frame, text="Progress:") calibration_procedure_progress_label.grid(row=0, column=0, padx=10, pady=10) calibration_procedure_progress = ttk.Progressbar(progress_bar_frame, length=240, variable=self.calibration_procedure_progress_var) calibration_procedure_progress.grid(row=0, column=1, padx=10, pady=10, sticky="we") row_counter += 1 # RIGHT COLUMN # Magnetometer calibration results row_counter = 0 calibration_results_frame = LabelFrame(self.right_column, text="Magnetometer Results") calibration_results_frame.grid(row=row_counter, column=1, padx=(100, 0), pady=20, sticky="nw") for i, label in enumerate(['X', 'Y', 'Z']): axis_label = Label(calibration_results_frame, text=label) axis_label.grid(row=0, column=i + 1, padx=5, pady=5, sticky="nw") # Axis sensitivities sensitivity_results_label = Label(calibration_results_frame, text="Sensitivity:") sensitivity_results_label.grid(row=1, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(calibration_results_frame, textvariable=self.sensitivity_result_vars[i], width=15, state='readonly') axis_data.grid(row=1, column=i + 1, padx=5, pady=5, sticky="nw") sensitivity_results_unit = Label(calibration_results_frame, text="-") sensitivity_results_unit.grid(row=1, column=4, padx=5, pady=5, sticky="nw") # Axis offsets offset_results_label = Label(calibration_results_frame, text="Offset:") offset_results_label.grid(row=2, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(calibration_results_frame, textvariable=self.offset_result_vars[i], width=15, state='readonly') axis_data.grid(row=2, column=i + 1, padx=5, pady=5, sticky="nw") offset_results_unit = Label(calibration_results_frame, text="\u03BCT") offset_results_unit.grid(row=2, column=4, padx=5, pady=5, sticky="nw") # Angle to XY coil plane angle_to_plane_label = Label(calibration_results_frame, text="Angle to XY plane:") angle_to_plane_label.grid(row=3, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(calibration_results_frame, textvariable=self.angle_to_plane_result_vars[i], width=15, state='readonly') axis_data.grid(row=3, column=i + 1, padx=5, pady=5, sticky="nw") angle_to_plane_unit = Label(calibration_results_frame, text="°") angle_to_plane_unit.grid(row=3, column=4, padx=5, pady=5, sticky="nw") # Angle in XY coil plane angle_in_plane_label = Label(calibration_results_frame, text="Angle in XY plane:") angle_in_plane_label.grid(row=4, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(calibration_results_frame, textvariable=self.angle_in_plane_result_vars[i], width=15, state='readonly') axis_data.grid(row=4, column=i + 1, padx=5, pady=5, sticky="nw") angle_in_plane_unit = Label(calibration_results_frame, text="°") angle_in_plane_unit.grid(row=4, column=4, padx=5, pady=5, sticky="nw") # Residual in system of equations residual_label = Label(calibration_results_frame, text="Residual:") residual_label.grid(row=5, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(calibration_results_frame, textvariable=self.residual_result_vars[i], width=15, state='readonly') axis_data.grid(row=5, column=i + 1, padx=5, pady=5, sticky="nw") residual_unit = Label(calibration_results_frame, text="\u03BCT") residual_unit.grid(row=5, column=4, padx=5, pady=5, sticky="nw") # Save calibration buttons save_calibration_results_frame = Frame(calibration_results_frame) save_calibration_results_frame.grid(row=6, column=0, columnspan=5) # Save and apply self.export_calibration_button = Button(save_calibration_results_frame, text="Export raw to CSV", command=self.export_csv, state="disabled", pady=5, padx=5) self.export_calibration_button.grid(row=0, column=0, padx=5, pady=5) self.copy_calibration_button = Button(save_calibration_results_frame, text="Copy to clipboard", command=self.copy_to_clipboard, state="disabled", pady=5, padx=5) self.copy_calibration_button.grid(row=0, column=1, padx=5, pady=5) row_counter += 1 # RIGHT Bottom COLUMN # Input coordinate system conversion matrix row_counter = 0 input_cos_frame = LabelFrame(self.right_column, text="Input MGM to Helmholtz COS Transformation Matrix") input_cos_frame.grid(row=row_counter, column=2, padx=(100, 0), pady=20, sticky="nw") for i, label in enumerate(['X', 'Y', 'Z']): axis_label = Label(input_cos_frame, text=label) axis_label.grid(row=0, column=i + 1, padx=5, pady=5, sticky="nw") # Axis sensitivities sensitivity_results_label = Label(input_cos_frame, text="X") sensitivity_results_label.grid(row=1, column=0, padx=5, pady=5, sticky="nw") for i in range(3): axis_data = Entry(input_cos_frame, textvariable=self.mgm_to_helmholtz_cos_trans[0][i], width=15) axis_data.grid(row=1, column=i + 1, padx=5, pady=5, sticky="nw") sensitivity_results_unit = Label(input_cos_frame, text="-") sensitivity_results_unit.grid(row=1, column=4, padx=5, pady=5, sticky="nw") # Axis offsets offset_results_label = Label(input_cos_frame, text="Y") offset_results_label.grid(row=2, column=0, padx=5, pady=5, sticky="nw") for i in range(3): if i == 0: axis_data = Entry(input_cos_frame, textvariable=self.mgm_to_helmholtz_cos_trans[0][1], width=15, state='readonly') self.mgm_to_helmholtz_cos_trans[1][0] = self.mgm_to_helmholtz_cos_trans[0][1] if i == 1: axis_data = Entry(input_cos_frame, textvariable=self.mgm_to_helmholtz_cos_trans[1][i], width=15) if i == 2: axis_data = Entry(input_cos_frame, textvariable=self.mgm_to_helmholtz_cos_trans[1][i], width=15) axis_data.grid(row=2, column=i + 1, padx=5, pady=5, sticky="nw") offset_results_unit = Label(input_cos_frame, text="-") offset_results_unit.grid(row=2, column=4, padx=5, pady=5, sticky="nw") # Angle to XY coil plane angle_to_plane_label = Label(input_cos_frame, text="Z") angle_to_plane_label.grid(row=3, column=0, padx=5, pady=5, sticky="nw") for i in range(3): if i == 0: axis_data = Entry(input_cos_frame, textvariable=self.mgm_to_helmholtz_cos_trans[0][2], width=15, state='readonly') self.mgm_to_helmholtz_cos_trans[2][0] = self.mgm_to_helmholtz_cos_trans[0][2] if i == 1: axis_data = Entry(input_cos_frame, textvariable=self.mgm_to_helmholtz_cos_trans[1][2], width=15, state='readonly') self.mgm_to_helmholtz_cos_trans[2][1] = self.mgm_to_helmholtz_cos_trans[1][2] if i == 2: axis_data = Entry(input_cos_frame, textvariable=self.mgm_to_helmholtz_cos_trans[2][i], width=15) axis_data.grid(row=3, column=i + 1, padx=5, pady=5, sticky="nw") angle_to_plane_unit = Label(input_cos_frame, text="-") angle_to_plane_unit.grid(row=3, column=4, padx=5, pady=5, sticky="nw") # Note on input label = "Note: Input orthogonal, normalized transformation Matrix." axis_note = Label(input_cos_frame, text=label) axis_note.grid(row=4, column=0, padx=5, pady=5, columnspan=5, sticky="nw") # Save calibration buttons save_input_cos_frame = Frame(input_cos_frame) save_input_cos_frame.grid(row=6, column=0, columnspan=5) # Save and apply self.export_cos_trans_button = Button(save_input_cos_frame, text="Export raw to CSV", command=self.export_csv, state="normal", pady=5, padx=5) self.export_cos_trans_button.grid(row=0, column=0, padx=5, pady=5) self.copy_cos_trans_matrix_button = Button(save_input_cos_frame, text="Copy to clipboard", command=self.copy_to_clipboard, state="normal", pady=5, padx=5) self.copy_cos_trans_matrix_button.grid(row=0, column=1, padx=5, pady=5) self.normalize_matrix_button = Button(save_input_cos_frame, text="Normalize matrix", command=self.matrix_normalize, state="normal", pady=5, padx=5) self.normalize_matrix_button.grid(row=0, column=2, padx=5, pady=5) row_counter += 1 # This starts an endless polling loop self.update_view() def page_switch(self): # every class in the UI needs this, even if it doesn't do anything pass def update_view(self): # Get new connected status if g.MAGNETOMETER.connected: self.connected_state_var.set("connected") self.connected_state_label.configure(fg="green") else: self.connected_state_var.set("Not connected") self.connected_state_label.configure(fg="red") # Get new field data new_field = g.MAGNETOMETER.field for i in range(3): # Display in uT self.field_value_vars[i].set("{:.3f}".format(new_field[i] * 1e6)) # Get mpi messages from calibration procedures try: while True: msg = self.view_mpi_queue.get(block=False) cmd = msg['cmd'] arg = msg['arg'] if cmd == 'finished': self.reactivate_buttons() elif cmd == 'failed': messagebox.showerror("Calibration error", "Error occured during calibration:\n{}".format(arg)) self.reactivate_buttons() elif cmd == 'progress': self.calibration_procedure_progress_var.set(min(int(arg * 100), 100)) elif cmd == 'calibration_data': self.display_calibration_results(arg) else: ui_print("Error: Unexpected mpi command '{}' in CalibrationTool".format(cmd)) except queue.Empty: pass self.controller.after(500, self.update_view) def reactivate_buttons(self): self.start_calibration_button.configure(text="Start Calibration", state=NORMAL) self.calibration_procedure_progress_var.set(0) def deactivate_buttons(self): self.start_calibration_button.configure(text="Running...", state=DISABLED) def display_calibration_results(self, results): # Cache raw experiment data for saving later self.calibration_raw_results = results['raw_data'] # Unpack the dict results = results['results'] # Display calibration in GUI for i in range(3): self.sensitivity_result_vars[i].set("{:.3f}".format(results[i]['sensitivity'])) self.offset_result_vars[i].set("{:.3f}".format(results[i]['offset'] * 1e6)) self.angle_to_plane_result_vars[i].set("{:.3f}".format(results[i]['alpha'] * 180 / pi)) self.angle_in_plane_result_vars[i].set("{:.3f}".format(results[i]['beta'] * 180 / pi)) self.residual_result_vars[i].set("{:.3e}".format(results[i]['residual'] * 1e6)) # Populate clipboard string self.clipboard = "\tX\tY\tZ\n" self.clipboard += "Sensitivity [-]" for i in range(3): self.clipboard += "\t{:.3f}".format(results[i]['sensitivity']) self.clipboard += "\nOffset [uT]" for i in range(3): self.clipboard += "\t{:.3f}".format(results[i]['offset'] * 1e6) self.clipboard += "\nAngle to XY Plane [deg]" for i in range(3): self.clipboard += "\t{:.3f}".format(results[i]['alpha'] * 180 / pi) self.clipboard += "\nAngle in XY Plane [deg]" for i in range(3): self.clipboard += "\t{:.3f}".format(results[i]['beta'] * 180 / pi) self.clipboard += "\nResidual [uT]" for i in range(3): self.clipboard += "\t{:.3e}".format(results[i]['residual'] * 1e6) # Enable save buttons self.export_calibration_button.configure(state="normal") self.copy_calibration_button.configure(state="normal") self.export_mgm_button.configure(state="normal") def start_calibration_procedure(self): try: calibration_points = self.calibration_points_var.get() calibration_interval = self.calibration_interval_var.get() self.calibration_thread = MagnetometerCalibration(self.view_mpi_queue, calibration_points, calibration_interval) self.calibration_thread.start() self.deactivate_buttons() except (DeviceAccessError, TclError) as e: messagebox.showwarning("Calibration failed", "Failed to start calibration:\n{}".format(e)) def export_csv(self): if self.calibration_raw_results is None: ui_print("Error: Failed to export non-existent calibration data.") return save_dict_list_to_csv('magnetometer_calibration.csv', self.calibration_raw_results, query_path=True) ui_print("Saved calibration results to magnetometer_calibration.csv.") def copy_to_clipboard(self): self.clipboard_clear() self.clipboard_append(self.clipboard) self.update() def matrix_normalize(self): try: ui_print("Input matrix to be normalized:") # Normalize Matrix matrix = [[self.mgm_to_helmholtz_cos_trans[0][0].get(), self.mgm_to_helmholtz_cos_trans[0][1].get(), self.mgm_to_helmholtz_cos_trans[0][2].get()], [self.mgm_to_helmholtz_cos_trans[1][0].get(), self.mgm_to_helmholtz_cos_trans[1][1].get(), self.mgm_to_helmholtz_cos_trans[1][2].get()], [self.mgm_to_helmholtz_cos_trans[2][0].get(), self.mgm_to_helmholtz_cos_trans[2][1].get(), self.mgm_to_helmholtz_cos_trans[2][2].get()]] matrix = np.array(matrix) ui_print(matrix) matrix_max = matrix.max() matrix_min = matrix.min() matrix = (matrix - matrix_min) / (matrix_max - matrix_min) ui_print("Normalized matrix:") ui_print(matrix) for i in range(3): for j in range(3): self.mgm_to_helmholtz_cos_trans[i][j].set(matrix[i][j]) except: # Couldn't compute matrix -> use unity matrix ui_print("Could not normalize matrix, reverted to unity matrix!") self.mgm_to_helmholtz_cos_trans = [[DoubleVar(value=1), DoubleVar(value=0), DoubleVar(value=0)], [DoubleVar(value=0), DoubleVar(value=1), DoubleVar(value=0)], [DoubleVar(value=0), DoubleVar(value=0), DoubleVar(value=1)]] class HardwareConfiguration(Frame): """Settings window to set program constants""" # noinspection PyUnusedLocal def __init__(self, parent, controller): Frame.__init__(self, parent) self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(ALL, weight=1) row_counter = 0 # create headline: header = Label(self, text="Configuration Window", font=HEADER_FONT, pady=3) header.grid(row=row_counter, column=0, padx=100, sticky=W) row_counter += 1 # Setup buttons to select config file # 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) # Create and place buttons # button to load a config file: load_file_button = Button(self.file_select_frame, text="Load config file...", command=self.load_config, pady=5, padx=5, font=SMALL_BUTTON_FONT) load_file_button.grid(row=0, column=0, padx=5) # button to save the current settings to the currently selected file save_button = Button(self.file_select_frame, text="Save current config", command=self.save_config, pady=5, padx=5, font=SMALL_BUTTON_FONT) save_button.grid(row=0, column=1, padx=5) # button to save current settings to a file selected by the user save_as_button = Button(self.file_select_frame, text="Save current config as...", command=self.save_config_as, pady=5, padx=5, font=SMALL_BUTTON_FONT) save_as_button.grid(row=0, column=2, padx=5) row_counter += 1 # Setup entry fields for the PSU serial ports: # setup frame: port_frame = Frame(self) port_frame.grid_rowconfigure(ALL, weight=1) port_frame.grid_columnconfigure(ALL, weight=1) port_frame.grid(row=row_counter, column=0, sticky=W) # text for the description labels: entry_texts = ["Arduino Serial Port:", "XY PSU Serial Port:", "Z PSU Serial Port:"] # create variables to store the port names and set to current names self.arduino_port = StringVar(value=g.CAGE_DEVICE.com_port_arduino) self.XY_port = StringVar(value=g.CAGE_DEVICE.com_port_psu1) self.Z_port = StringVar(value=g.CAGE_DEVICE.com_port_psu2) port_vars = [self.arduino_port, self.XY_port, self.Z_port] # list to store both port variables row = 0 for text in entry_texts: # do this for both ports field = Entry(port_frame, textvariable=port_vars[row]) # create entry field field.grid(row=row, column=1, sticky=W) axis_label = Label(port_frame, text=text) # create description label axis_label.grid(row=row, column=0, padx=5, pady=5, sticky=W) info_label = Label(port_frame, text="e.g. COM10") # add label with example of right format info_label.grid(row=row, column=2, sticky=W) row += 1 row_counter += 1 # Label(self, text="", pady=0).grid(row=row_counter, column=0) # add spacer row_counter += 1 # setup main entry field for operational constants # setup frame: value_frame = Frame(self) value_frame.grid_rowconfigure(ALL, weight=1) value_frame.grid_columnconfigure(ALL, weight=1) value_frame.grid(row=row_counter, column=0) # Setup dictionary to generate entry table from # {Key: [[x-value,y-value,z-value], unit, description, config file key, unit conversion factor]} self.entries = { "Coil Constants:": [[DoubleVar() for _ in range(3)], "\u03BCT/A", "Field generated per applied current", "coil_const", 1e6], "Ambient Field:": [[DoubleVar() for _ in range(3)], "\u03BCT", "Field to be compensated", "ambient_field", 1e6], "Resistances:": [[DoubleVar() for _ in range(3)], "\u03A9", "Resistance of coils + equipment", "resistance", 1], "Max. Current:": [[DoubleVar() for _ in range(3)], "A", "Max. allowed current", "max_amps", 1], "Max. Voltage:": [[DoubleVar() for _ in range(3)], "V", "Max. allowed voltage, must not exceed 16V!", "max_volts", 1], "Arduino Pins:": [[IntVar() for _ in range(3)], "-", "Should be 15, 16, 17", "relay_pin", 1] } self.fields = {} # setup dictionary with all entry fields # Fill in header (axis names): col = 1 for text in g.AXIS_NAMES: label = Label(value_frame, text=text, font=SUB_HEADER_FONT) label.grid(row=0, column=col, sticky="ew") col += 1 # generate table with entries, unit labels and descriptions: row = 1 for key in self.entries.keys(): # go through the different values to be set self.fields[key] = [] # initialize empty list for fields in the entry field dictionary for axis in range(3): # go through all three axes field = Entry(value_frame, textvariable=self.entries[key][0][axis], width=10) # setup entry field field.grid(row=row, column=axis + 1, sticky=W, padx=2) self.fields[key].append(field) # safe access to field to entry field dictionary axis_label = Label(value_frame, text=key, padx=5, pady=5) # add label with variable name axis_label.grid(row=row, column=0, sticky=W) unit_label = Label(value_frame, text=self.entries[key][1]) # add label with unit unit_label.grid(row=row, column=4, sticky=W) description_label = Label(value_frame, text=self.entries[key][2]) # add label with description description_label.grid(row=row, column=5, sticky=W) row = row + 1 row_counter += 1 self.update_fields() # set current values from config file # Label(self, text="", pady=3).grid(row=row_counter, column=0) # add spacer row_counter += 1 # Setup buttons to implement and restore defaults # Setup frame to house buttons: self.buttons_frame = Frame(self) self.buttons_frame.grid_rowconfigure(ALL, weight=1) self.buttons_frame.grid_columnconfigure(ALL, weight=1) self.buttons_frame.grid(row=row_counter, column=0, sticky=W, padx=20) # Create and place buttons # button to read the values from all fields, update the config and reinitialize the test bench implement_button = Button(self.buttons_frame, text="Update and Reinitialize", command=self.implement, pady=5, padx=5, font=BIG_BUTTON_FONT) implement_button.grid(row=0, column=0, padx=5) # button to restore default settings restore_button = Button(self.buttons_frame, text="Restore Defaults", command=self.restore_defaults, pady=5, padx=5, font=BIG_BUTTON_FONT) restore_button.grid(row=0, column=1, padx=5) row_counter += 1 # Label(self, text="", pady=3).grid(row=row_counter, column=0) # add spacer def page_switch(self): # function that is called when switching to this window self.update_fields() # update values in the entry fields from config def restore_defaults(self): # restore default settings config.reset_config_to_default() # overwrite config file with default ui_print("\nReinitializing devices...") g.CAGE_DEVICE.reconnect_hardware_async() # setup everything with the defaults self.update_fields() # update fields in config window def update_fields(self): # set current values for all entry variables from config file # set values for PSU serial ports: self.XY_port.set(g.CAGE_DEVICE.com_port_psu1) self.Z_port.set(g.CAGE_DEVICE.com_port_psu2) self.arduino_port.set(g.CAGE_DEVICE.com_port_arduino) for key in self.entries.keys(): # go through the main value table for i in range(3): # go through all three axes # get value from config file: value = config.read_from_config(g.AXIS_NAMES[i], self.entries[key][3], config.CONFIG_OBJECT) self.entries[key][0][i].set(value) # set initial value on the entry field variable type_value = self.entries[key][0][i].get() # get value with correct type by reading out field variable factor = self.entries[key][4] # get unit conversion factor self.entries[key][0][i].set(round(type_value * factor, 3)) # set value with correct unit conversion # check if value is within safe limits: value_check = helmholtz_cage_device.value_in_limits(g.AXIS_NAMES[i], self.entries[key][3], value) if value_check == 'OK': # value is acceptable self.fields[key][i].config(background="White") # set colour of this entry to white else: # value exceeds limits self.fields[key][i].config(background="Red") # set colour of this entry to red to show problem def write_values(self): # update config object with user inputs into entry fields and reinitialize devices # set serial ports for PSUs: config.edit_config("Supplies", "xy_port", self.XY_port.get()) config.edit_config("Supplies", "z_port", self.Z_port.get()) config.edit_config("Supplies", "arduino_port", self.arduino_port.get()) # set numeric values for all axes for key in self.entries.keys(): # go through rows of entry table for i in [0, 1, 2]: # go through columns of entry table (axes) try: value = self.entries[key][0][i].get() # get value from field except TclError as e: # wrong format entered, e.g. text in number fields ui_print("Invalid entry for %s %s %s" % (g.AXIS_NAMES[i], key, e)) # print error message # field will go back to the last valid value else: # format is ok factor = self.entries[key][4] # get unit conversion factor if factor not in [0, 1]: # prevent div/0 and conversion of int variables to float value = value / factor # do unit conversion # Check if value is within safe limits config_key = self.entries[key][3] # get handle by which value is indexed in config file value_ok = helmholtz_cage_device.value_in_limits(g.AXIS_NAMES[i], config_key, value) # perform value check if value_ok == 'OK': # value is within safe limits config.edit_config(g.AXIS_NAMES[i], config_key, value) # write new value to config file else: # value is not within limits unit = self.entries[key][1] # get unit string for error messages axis = g.AXIS_NAMES[i] # get axis name for error messages if value_ok == 'HIGH': # value is too high max_value = g.default_arrays[config_key][1][i] # get max value # assemble error message: message = "Attempted to set too high value for {s} {k}\n" \ "{v} {unit}, max. {mv} {unit} allowed.\n" \ "Excessive values may damage equipment!\n" \ "Do you really want to use this value?" \ .format(s=axis, k=key, v=value * factor, mv=round(max_value * factor, 1), unit=unit) elif value_ok == 'LOW': # value is too low min_value = g.default_arrays[config_key][2][i] # get min value # assemble error message: message = "Attempted to set too low value for {s} {k}\n" \ "{v} {unit}, min. {mv} {unit} allowed.\n" \ "Excessive values may damage equipment!\n" \ "Do you really want to use this value?" \ .format(s=axis, k=key, v=value * factor, mv=round(min_value * factor, 1), unit=unit) else: # this should be impossible message = "Unknown case for value limits check, this should not happen." # display pop-up message to ask user if he really wants the value answer = messagebox.askquestion("Value out of bounds", message) # answer becomes 'yes' or 'no' depending on user choice if answer == 'yes': # user really wants the value # call function to write new value to config file with override=True config.edit_config(g.AXIS_NAMES[i], config_key, value, True) # if user chooses 'no' nothing happens, old value is kept def implement(self): # "Update and Reinitialize" button, update config with new values and reinitialize devices self.write_values() # write current values from entry fields to config object ui_print("\nReinitializing devices...") g.CAGE_DEVICE.reconnect_hardware_async() # reinitialize devices and program with new values self.update_fields() # update entry fields to show new values def load_config(self): # load configuration from some config file directory = os.path.dirname(os.path.abspath(config.CONFIG_FILE)) # get directory of current config file # open file selection dialogue at current path and save path of selected file filename = filedialog.askopenfilename(initialdir=directory, title="Select Config File", filetypes=(("Config File", "*.ini*"), ("All Files", "*.*"))) if filename == () or filename == '': # this happens when file selection window is closed without selecting a file ui_print("No file selected, could not load config.") elif os.path.exists(filename): # does the file exist? config.CONFIG_FILE = filename # set global config file to the new file config.CONFIG_OBJECT = config.get_config_from_file(filename) # load from config file to config object config.check_config(config.CONFIG_OBJECT) # check and display warnings if values are out of bounds ui_print("\nReinitializing devices...") g.CAGE_DEVICE.reconnect_hardware_async() # reinitialize devices and program with new values self.update_fields() # update entry fields to show new values else: # file does not exist ui_print("Selected file", filename, "does not exist, could not load config.") def save_config_as(self): # save current configuration to a new config file directory = os.path.dirname(os.path.abspath(config.CONFIG_FILE)) # get directory of current config file # open file selection dialogue and save path of selected file filename = filedialog.asksaveasfilename(initialdir=directory, title="Save config to file", filetypes=([("Config File", "*.ini*")]), defaultextension=[("Config File", "*.ini*")]) if filename == '': # this happens when file selection window is closed without selecting a file ui_print("No file selected, could not save config.") else: # a file name was entered config.CONFIG_FILE = filename # set global config file to the new file self.write_values() # write current entry field values to the config object config.write_config_to_file(config.CONFIG_OBJECT) # write contents of config object to file ui_print("\nReinitializing devices...") g.CAGE_DEVICE.reconnect_hardware_async() # reinitialize devices and program with new values self.update_fields() # update entry fields to show values as they are in the config def save_config(self): # same as save_config_as() but with the current config file self.write_values() # write current entry field values to the config object config.write_config_to_file(config.CONFIG_OBJECT) # write contents of config object to file ui_print("\nReinitializing devices...") g.CAGE_DEVICE.reconnect_hardware_async() # reinitialize devices and program with new values self.update_fields() # update entry fields to show values as they are in the config class ConfigureLogging(Frame): # generate window to configure data logging to csv def __init__(self, parent, controller): Frame.__init__(self, parent) self.parent = parent # parent object, here tha mainArea frame self.controller = controller # object on which mainloop() is running, usually main window 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 bench # log_datapoint() has to be called wherever a command is sent to the test bench and data should be logged # it does not happen automatically whenever something is sent to the test bench # 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 headline 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=5) # button to start data logging self.start_logging_button = Button(self.top_buttons_frame, text="Start Logging", command=self.start_logging, pady=5, padx=5, font=SMALL_BUTTON_FONT) self.start_logging_button.grid(row=0, column=0, padx=5) # button to stop data logging self.stop_logging_button = Button(self.top_buttons_frame, text="Stop Logging", command=self.stop_logging, pady=5, padx=5, font=SMALL_BUTTON_FONT, state="disabled") self.stop_logging_button.grid(row=0, column=1, padx=5) # button to write log data to a file self.write_to_file_button = Button(self.top_buttons_frame, text="Write data to file", font=SMALL_BUTTON_FONT, command=self.write_to_file, pady=5, padx=5, state="disabled") self.write_to_file_button.grid(row=0, column=2, padx=5) # button to clear all logged data self.clear_data_button = Button(self.top_buttons_frame, text="Clear logged data", font=SMALL_BUTTON_FONT, command=self.clear_data, pady=5, padx=5, state="disabled") self.clear_data_button.grid(row=0, column=3, padx=5) row_counter += 1 # Create label showing how many datapoints have been logged # setup frame: self.log_label_frame = Frame(self) self.log_label_frame.grid_rowconfigure(ALL, weight=1) self.log_label_frame.grid_columnconfigure(ALL, weight=1) self.log_label_frame.grid(row=row_counter, column=0, sticky=W, padx=20) self.logged_datapoints = IntVar() # create variable to display number of logged datapoints # Add description label: datapoints_description = Label(self.log_label_frame, text="Datapoints logged:") datapoints_description.grid(row=0, column=0, sticky=W) # Add updatable label to show how much data has been logged datapoints_label = Label(self.log_label_frame, textvariable=self.logged_datapoints) datapoints_label.grid(row=0, column=1, sticky=W) row_counter += 1 # create checkboxes and entries to set when and how often data should be logged # setup frame: 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) # create checkbox variable for logging in regular time intervals self.regular_logging_var = BooleanVar(value=True) # create checkbox variable for logging whenever test bench is commanded self.event_logging_var = BooleanVar(value=True) 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 bench 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 # setup frame: 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=10, pady=10) self.checkbox_vars = {} # dictionary containing the bool variables changed by the checkboxes self.checkboxes = [] # list containing all the checkbox objects, used to lock/unlock all of them self.active_keys = [] # list with all keys of the currently ticked checkboxes # add general description headline: checkbox_label = Label(self.checkbox_frame, text="Select which data to log:") checkbox_label.grid(row=0, column=0, columnspan=2, sticky='w') # generate and place all checkboxes: row = 0 column = 0 for key, name in log.logging_selection_options.items(): # go through all loggable values self.checkbox_vars[key] = BooleanVar(value=True) # create variable for checkbox and put it in dictionary checkbox = Checkbutton(self.checkbox_frame, text=name, # generate checkbox variable=self.checkbox_vars[key], onvalue=True, offvalue=False) checkbox.grid(row=row + 1, column=column, padx=(0, 20), sticky=W) # place checkbox in UI self.checkboxes.append(checkbox) # add created checkbox to list of all checkboxes row += 1 if row > 8: row = 0 column += 1 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 start_logging(self): # start logging data (called by button) ui_print("Started data logging.") self.update_choices() # update list with ticked checkboxes 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 # get settings for regular logging: error = False # initialize variable to store if an error occurred if self.regular_logging: # regular logging checkbox is ticked try: # try to get log interval from entry field interval_ms = int(self.log_interval.get() * 1000) # get value and convert to ms except TclError as e: # invalid entry for log interval # show error pop-up: messagebox.showwarning("Wrong entry format!", "Invalid entry for log interval:\n%s" % e) self.event_logging = False # don't start event logging if there is a problem error = True # save that an error was encountered else: # no problems while reading out the log interval 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.stop_logging_button["state"] = "normal" self.start_logging_button["state"] = "disabled" self.write_to_file_button["state"] = "disabled" self.clear_data_button["state"] = "normal" self.lock_checkboxes() def stop_logging(self): # stop the data logging, called by "Stop Logging" button ui_print("Stopped data logging. Remember to save data to file!") self.regular_logging = False # tell everything its time to stop periodic logging self.event_logging = False # tell everything its time to stop logging on test bench commands self.write_to_file_button["state"] = "normal" # enable write to file button self.stop_logging_button["state"] = "disabled" # disable stop logging button self.start_logging_button["state"] = "normal" # enable start logging button self.unlock_checkboxes() # enable checkboxes 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 if filepath is None: # no valid file was selected # ask user if he wants to try again: try_again = messagebox.askquestion("No file selected", "No valid file was selected. Try again?") if try_again == 'yes': # user wants to try again self.write_to_file() # call same function again so user can retry else: # a valid filename was selected log.write_to_file(filepath) # write logged data to the file def clear_data(self): # called on button press, asks user if he want to save logged data and then deletes it if log.unsaved_data: # there is logged data that has not been written to a file yet # open pop-up to ask user if he wants to save the data or cancel clearing it: save_log = messagebox.askyesnocancel("Save log data?", "There seems to be unsaved logging data. " "Do you wish to write it to a file before deleting?") if save_log: # user has chosen yes self.write_to_file() # run write to file function to save data if save_log is not None: # user has chosen yes or no (not cancel) 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.") else: # user has chosen to cancel ui_print("Log data not cleared.") else: # there is no unsaved data log.clear_logged_data() # delete the logged data 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): # updates the list storing which checkboxes are currently ticked # (this is passed to logging functions and determines which data is logged) self.active_keys = {} # initialize the dict for key in self.checkbox_vars.keys(): # go through all checkboxes self.active_keys[key] = self.checkbox_vars[key].get() # add corresponding item to the list def lock_checkboxes(self): # lock all checkboxes, so they can not be modified while logging # lock all checkboxes: for checkbox in [*self.checkboxes, self.event_logging_checkbox, self.regular_logging_checkbox]: checkbox.config(state=DISABLED) self.interval_entry.config(state=DISABLED) # lock logging interval entry field def unlock_checkboxes(self): # opposite of lock 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 still active self.log_datapoint() # add a datapoint to the log data frame self.controller.after(interval, lambda: self.periodic_log(interval)) # call same function again after time 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: # some error occurred messagebox.showerror("Error!", "Error while logging data: \n%s" % e) def update_datapoint_count(self): # update label with how many datapoints have been logged if self.regular_logging or self.event_logging: # logging is still 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 class StatusDisplay(Frame): # status display to show information on test bench status in real time # noinspection PyUnusedLocal def __init__(self, parent, controller): Frame.__init__(self, parent, relief=SUNKEN, bd=1) self.controller = controller # Queue to store status updates which arrive from another thread by callback self.update_label_queue = Queue() # configure Tkinter grid self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(ALL, weight=1) rowCounter = 0 # keep track of which row we are at in the grid layout x_pad = 10 # centrally set padding # create column headers (X-Axis etc.) col = 0 for header in ["", "X-Axis", "Y-Axis", "Z-Axis"]: # define Column headers # create label: headLabel = Label(self, text=header, font=SUB_HEADER_FONT, borderwidth=1, relief="flat", anchor="w", padx=x_pad) headLabel.grid(row=rowCounter, column=col, sticky="ew") col = col + 1 # move to next column rowCounter += 1 # increase row counter to place future stuff below header # define content of row entries: TextLabels = ["PSU Serial Port:", "PSU Channel:", "PSU Status:", "Arduino Status:", "", "Output:", "Remote Control:", "Voltage Setpoint:", "Actual Voltage:", "Current Setpoint:", "Actual Current:", "", "Target Field:", "Trgt. Field Raw:", "Target Current:", "Inverted:"] self.rowNo = len(TextLabels) # get number of label rows self.columnNo = 4 # number of label columns # prepare list of lists to contain all labels for row entries in all columns: self.Labels = [[] for _ in range(self.columnNo)] # create dictionary to associate (changing) label variables with their labels: self.label_dict = {} # initialize dictionary for name in TextLabels: # go through all rows self.label_dict[name] = [StringVar() for _ in range(self.columnNo - 1)] # create variables for labels # add static labels for row titles: self.Labels[0].append(Label(self, text=name, borderwidth=1, relief="flat", anchor="w", padx=x_pad)) for col in range(self.columnNo - 1): # go through columns # add changeable labels for values: self.Labels[col + 1].append(Label(self, textvariable=self.label_dict[name][col], borderwidth=1, relief="flat", anchor="w", padx=x_pad)) # place all labels in grid layout col = 0 for LabelCol in self.Labels: # go through table columns for row in range(self.rowNo): # go through table rows LabelCol[row].grid(row=row + rowCounter, column=col, sticky="nsew") # place label col += 1 # Register callback to populate new data: g.CAGE_DEVICE.subscribe_status_updates(self.enqueue_new_status) # Starts polling loop for status display self.update_label_poll_method() def update_labels(self, status): # update all values in the status display for i in range(3): # go through all three axes # update all label variables with the new values: axis = status['axes'][i] if axis['connected']: # Deal with variables that are dependent on the current hardware state active = "True" if axis['active'] else "False" remote_active = "True" if axis['active'] else "False" voltage_setpoint = "%0.3f V" % axis['voltage_setpoint'] voltage = "%0.3f V" % axis['voltage'] current_setpoint = "%0.3f A" % axis['current_setpoint'] current = "%0.3f A" % axis['current'] polarity = axis['polarity'] else: active = "N/A" remote_active = "N/A" voltage_setpoint = "N/A" voltage = "N/A" current_setpoint = "N/A" current = "N/A" polarity = "N/A" psu_connected = "Connected" if axis['connected'] else "Not Connected" arduino_connected = "Connected" if status['arduino_connected'] else "Not Connected" self.label_dict["PSU Serial Port:"][i].set(axis['port']) self.label_dict["PSU Channel:"][i].set(axis['channel']) self.label_dict["PSU Status:"][i].set(psu_connected) self.label_dict["Arduino Status:"][i].set(arduino_connected) # ToDo (optional): make this multicolumn self.label_dict["Output:"][i].set(active) self.label_dict["Remote Control:"][i].set(remote_active) self.label_dict["Voltage Setpoint:"][i].set(voltage_setpoint) self.label_dict["Actual Voltage:"][i].set(voltage) self.label_dict["Current Setpoint:"][i].set(current_setpoint) self.label_dict["Actual Current:"][i].set(current) self.label_dict["Target Field:"][i].set("%0.3f \u03BCT" % (axis['target_field'] * 1e6)) self.label_dict["Trgt. Field Raw:"][i].set("%0.3f \u03BCT" % (axis['target_field_raw'] * 1e6)) self.label_dict["Target Current:"][i].set("%0.3f A" % axis['target_current']) self.label_dict["Inverted:"][i].set(polarity) def enqueue_new_status(self, status): """Runs in caller thread and places status onto queue to display when polled in update_label_poll_method""" self.update_label_queue.put(status) def update_label_poll_method(self): """Infinite loop to poll for status updates to display""" try: new_status = self.update_label_queue.get(block=False) # Blocks until new data is available. self.update_labels(new_status) except Empty: pass self.controller.after(200, self.update_label_poll_method) class OutputConsole(Frame): # console to print information to user in, similar to standard python output def __init__(self, parent): Frame.__init__(self, parent, relief=SUNKEN, bd=1) # Queue for messages to print when this class currently has main thread control self.print_queue = Queue() # configure Tkinter grid: self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(0, weight=1, minsize=60) # console needs to have a minimum width scrollbar = Scrollbar(self) # setup scrollbar self.console = Text(self) # setup main console widget self.console.bind("", lambda e: "break") # prevent user from writing into the console scrollbar.grid(row=0, column=1, sticky="ns") # place the scrollbar and stretch vertically self.console.grid(row=0, column=0, sticky="nesw") # place the output console # link scrollbar to the console scrollbar.config(command=self.console.yview) self.console.config(yscrollcommand=scrollbar.set) # Start the main thread for printing queued messages self.print_thread() def print_thread(self): """ Continuous thread that checks if messages are present, and prints them if they are. """ try: while True: msg = self.print_queue.get(block=False) # print to console self.console.insert(END, msg + "\n") self.console.see(END) # scroll console to bottom except Empty: pass # Print messages every 100 ms in the main Tkinter loop self.after(100, self.print_thread) def put(self, message): self.print_queue.put(message)