from tkinter import * from tkinter import ttk import globals as g import cage_func as func import numpy as np NORM_FONT = () HEADER_FONT = ("Arial", 13, "bold") SUB_HEADER_FONT = ("Arial", 9, "bold") BIG_BUTTON_FONT = ("Arial", 11, "bold") class HelmholtzGUI(Tk): def __init__(self): Tk.__init__(self) Tk.wm_title(self, "Helmholtz Cage Control") Tk.wm_iconbitmap(self, "Helmholtz.ico") self.Menu = TopMenu(self) # displays menu bar at the top mainArea = Frame(self) mainArea.pack(side="top", fill="both", expand=False) mainArea.grid_rowconfigure(0, weight=1) mainArea.grid_columnconfigure(0, weight=1) self.pages = {} # dictionary for storing all pages for P in [ManualMode, Configuration]: page = P(mainArea, self) self.pages[P] = page page.grid(row=0, column=0, sticky="nsew") status_frame = Frame(self) status_frame.pack(side="bottom", fill="x", expand=False) status_frame.grid_rowconfigure(ALL, weight=1) status_frame.grid_columnconfigure(1, weight=1) self.StatusDisplay = StatusDisplay(status_frame, self) self.StatusDisplay.grid(row=0, column=0, sticky="nesw") self.OutputConsole = OutputConsole(status_frame) self.OutputConsole.grid(row=0, column=1, sticky="nesw") self.show_frame(ManualMode) def show_frame(self, key): frame = self.pages[key] # gets correct page from the dictionary frame.page_switch() # update displays in this page with window-specific update function frame.tkraise() # brings this frame to the front class TopMenu: def __init__(self, window): menu = Menu(window) window.config(menu=menu) ModeSelector = Menu(menu) menu.add_cascade(label="Mode", menu=ModeSelector) ModeSelector.add_command(label="Static Manual Input", command=lambda: self.manual_mode(window)) ModeSelector.add_command(label="Configuration...", command=lambda: self.configuration(window)) @staticmethod def manual_mode(window): window.show_frame(ManualMode) @staticmethod def configuration(window): window.show_frame(Configuration) class ManualMode(Frame): # ToDo: Add buttons to safe and set to 0 def __init__(self, parent, controller): Frame.__init__(self, parent) self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(ALL, weight=1) row_counter = 0 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) dropdown_frame.grid_rowconfigure(ALL, weight=1) dropdown_frame.grid_columnconfigure(ALL, weight=1) dropdown_frame.grid(row=row_counter, column=0) self.input_mode = StringVar() # make dictionary with information on all modes. # content: [function to call on button press, unit text to be displayed] self.modes = {"Magnetic Field": [self.execute_field, "\u03BCT", self.update_max_fields], "Current": [self.execute_current, "A", self.update_max_currents]} self.unit = StringVar() default_mode = list(self.modes.keys())[0] 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 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 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) 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:"] self.entry_vars = [StringVar() for _ in range(3)] self.max_value_vars = [StringVar() for _ in range(3)] row = 0 for text in entry_texts: field = ttk.Entry(self.entries_frame, textvariable=self.entry_vars[row]) self.entry_vars[row].set(0) field.grid(row=row, column=1, sticky=W) axis_label = Label(self.entries_frame, text=text, padx=5, pady=10) axis_label.grid(row=row, column=0, sticky=W) unit_label = Label(self.entries_frame, textvariable=self.unit) unit_label.grid(row=row, column=2, sticky=W) 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) checkbox_frame.grid(row=row_counter, column=0, sticky=W) self.compensate = IntVar(value=1) self.compensate_checkbox = Checkbutton(checkbox_frame, text="Compensate ambient field", variable=self.compensate, onvalue=1, offvalue=0) 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 reinitialization reinit_button = Button(self.buttons_frame, text="Reinitialize", command=func.setup_axes, pady=5, padx=5, font=BIG_BUTTON_FONT) reinit_button.grid(row=row_counter, column=1, padx=5) row_counter = row_counter + 1 # Add spacer to Frame below 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, e.g. calls update_max_fields function def change_mode_callback(self, var, index, mode): # not sure what the parameters are for, but they are necessary 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 update_max_fields(self): # update labels with maximum allowable field values self.compensate_checkbox.config(state=NORMAL) i = 0 for val in self.max_value_vars: comp = self.compensate.get() if comp == 0: field = g.AXES[i].max_field * 1e6 elif comp == 1: field = g.AXES[i].max_comp_field * 1e6 else: field = [0, 0] func.ui_print("Unexpected value encountered: compensate =", comp) val.set("(%0.1f to %0.1f \u03BCT)" % (field[0], field[1])) i += 1 def update_max_currents(self): # update labels with maximum allowable current values self.compensate_checkbox.config(state=DISABLED) i = 0 for val in self.max_value_vars: val.set("(%0.2f to %0.2f A)" % (-g.AXES[i].max_amps, g.AXES[i].max_amps)) i += 1 def execute(self): function_to_call = self.modes[self.input_mode.get()][0] # get function of appropriate mode vector = np.array([0, 0, 0], dtype=float) i = 0 for var in self.entry_vars: vector[i] = float(var.get()) i = i + 1 function_to_call(vector) # call function # ToDo: update status display here def execute_field(self, vector): func.ui_print("field executing", vector) try: comp = self.compensate.get() if comp == 0: func.set_field(vector * 1e-6) elif comp == 1: func.set_field_simple(vector * 1e-6) else: func.ui_print("Unexpected value encountered: compensate =", comp) except ValueError as e: func.ui_print(e) @staticmethod def execute_current(vector): func.ui_print("current executing:", vector) try: func.set_current_vec(vector) except ValueError as e: func.ui_print(e) class Configuration(Frame): # generate configuration window to set program constants def __init__(self, parent, controller): Frame.__init__(self, parent) self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(ALL, weight=1) row_counter = 0 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 # Serial port settings 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) entry_texts = ["XY PSU Serial Port:", "Z PSU Serial Port:"] self.XY_port = StringVar(value=g.XY_PORT) self.Z_port = StringVar(value=g.Z_PORT) port_vars = [self.XY_port, self.Z_port] row = 0 for text in entry_texts: field = ttk.Entry(port_frame, textvariable=port_vars[row]) field.grid(row=row, column=1, sticky=W) axis_label = Label(port_frame, text=text, padx=5, pady=10) axis_label.grid(row=row, column=0, sticky=W) unit_label = Label(port_frame, text="e.g. COM10") unit_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 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", "", "coil_const", 1e6], "Ambient Field:": [[DoubleVar() for _ in range(3)], "\u03BCT/A", "Field to be compensated", "ambient_field", 1e6], "Resistances:": [[DoubleVar() for _ in range(3)], "\u03A9", "Resistance of coils + equipment", "resistance", 1], "Max. Power:": [[DoubleVar() for _ in range(3)], "W", "Max. allowed power", "max_watts", 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.update_fields() # set current values from config file # Fill in header (axis names): col = 1 for text in ["X-Axis", "Y-Axis", "Z-Axis"]: 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(): for axis in range(3): # generate entry fields field = ttk.Entry(value_frame, textvariable=self.entries[key][0][axis], width=10) field.grid(row=row, column=axis+1, sticky=W, padx=2) axis_label = Label(value_frame, text=key, padx=5, pady=5) axis_label.grid(row=row, column=0, sticky=W) unit_label = Label(value_frame, text=self.entries[key][1]) unit_label.grid(row=row, column=4, sticky=W) description_label = Label(value_frame, text=self.entries[key][2]) description_label.grid(row=row, column=5, sticky=W) row = row + 1 row_counter += 1 Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer row_counter += 1 # Setup buttons # 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 implement_button = Button(self.buttons_frame, text="Update and Reconnect", command=self.implement, pady=5, padx=5, font=BIG_BUTTON_FONT) implement_button.grid(row=0, column=0, padx=5) 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=10).grid(row=row_counter, column=0) # add spacer def page_switch(self): # function that is called when switching to this window self.update_fields() def restore_defaults(self): func.create_default_config(g.CONFIG_FILE) # overwrite config file with default func.setup_axes() # setup everything with the defaults ToDo: take out? self.update_fields() # update fields in config window def update_fields(self): # set current values for all entry variables from config file self.XY_port.set(g.XY_PORT) self.Z_port.set(g.Z_PORT) for key in self.entries.keys(): for i in [0, 1, 2]: value = func.read_config(g.AXIS_NAMES[i], self.entries[key][3]) # get value from config file self.entries[key][0][i].set(value) # set initial value on variable type_value = self.entries[key][0][i].get() # get value with correct data type 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 def implement(self): # update config file with user inputs into entry fields and reinitialize # ToDo: Warning messages if too high values are entered func.edit_config("PORTS", "xy_port", self.XY_port.get()) func.edit_config("PORTS", "z_port", self.Z_port.get()) for key in self.entries.keys(): # go through rows of entry table for i in [0, 1, 2]: # go through columns of entry table try: value = self.entries[key][0][i].get() # get value from field factor = self.entries[key][4] # get unit conversion factor if factor not in [0, 1]: # prevent conversion of int variables to float and div/0 value = value / factor # do unit conversion func.edit_config(g.AXIS_NAMES[i], self.entries[key][3], value) # write new value to config file except TclError as e: func.ui_print("Invalid entry for %s %s %s" % (g.AXIS_NAMES[i], key, e)) func.setup_axes() # reinitialize devices and program with new values self.update_fields() # update entry fields to show new values class StatusDisplay(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent, relief=SUNKEN, bd=1) 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 col = 0 for header in ["", "X-Axis", "Y-Axis", "Z-Axis"]: # create Column headers 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 = 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)] self.label_dict = {} for name in TextLabels: self.label_dict[name] = [StringVar() for _ in range(self.columnNo - 1)] # add 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): # add labels vor values self.Labels[col + 1].append(Label(self, textvariable=self.label_dict[name][col], borderwidth=1, relief="flat", anchor="w", padx=x_pad)) col = 0 for LabelCol in self.Labels: # place row entries in grid layout for all columns for row in range(self.rowNo): # place row entries LabelCol[row].grid(row=row + rowCounter, column=col, sticky="nsew") col = col + 1 # rowCounter = rowCounter + self.rowNo # increase row counter to place future stuff below this self.update_labels(controller) def update_labels(self, controller): # ToDo (optional): do this with a dictionary g.ARDUINO.update_status_info() i = 0 for axis in g.AXES: if axis.device is not None: axis.update_status_info() self.label_dict["PSU Serial Port:"][i].set(g.PORTS[i]) self.label_dict["PSU Channel:"][i].set(axis.channel) self.label_dict["PSU Status:"][i].set(axis.connected) self.label_dict["Arduino Status:"][i].set(g.ARDUINO.connected) # ToDo (optional): make this multicolumn self.label_dict["Output:"][i].set(axis.output_active) self.label_dict["Remote Control:"][i].set(axis.remote_ctrl_active) self.label_dict["Voltage Setpoint:"][i].set("%0.3f V" % axis.voltage_setpoint) self.label_dict["Actual Voltage:"][i].set("%0.3f V" % axis.voltage) self.label_dict["Current Setpoint:"][i].set("%0.3f A" % axis.current_setpoint) self.label_dict["Actual Current:"][i].set("%0.3f A" % axis.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_comp * 1e6)) self.label_dict["Target Current:"][i].set("%0.3f A" % axis.target_current) self.label_dict["Inverted:"][i].set(axis.polarity_switched) i = i + 1 controller.after(500, lambda: self.update_labels(controller)) class OutputConsole(Frame): # console to print stuff in def __init__(self, parent): Frame.__init__(self, parent, relief=SUNKEN, bd=1) self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(0, weight=1, minsize=60) scrollbar = Scrollbar(self) self.console = Text(self) self.console.bind("", lambda e: "break") # prevent user input into the console scrollbar.grid(row=0, column=1, sticky="ns") self.console.grid(row=0, column=0, sticky="nesw") scrollbar.config(command=self.console.yview) self.console.config(yscrollcommand=scrollbar.set)