forked from zietzm/Helmholtz_Test_Bench
312 lines
13 KiB
Python
312 lines
13 KiB
Python
from tkinter import *
|
|
from tkinter import ttk
|
|
import globals as g
|
|
import cage_func as func
|
|
import numpy as np
|
|
|
|
NORM_FONT = ()
|
|
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.frames = {} # dictionary for storing all pages
|
|
|
|
for F in [ManualMode]:
|
|
frame = F(mainArea, self)
|
|
self.frames[F] = frame
|
|
frame.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.frames[key] # gets correct page from the dictionary
|
|
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))
|
|
|
|
@staticmethod
|
|
def manual_mode(window):
|
|
window.show_frame(ManualMode)
|
|
|
|
|
|
class ManualMode(Frame):
|
|
# ToDo: Display maximum values
|
|
# ToDo: Add option to cancel ambient field
|
|
# 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
|
|
|
|
# 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]}
|
|
# "Raw Current": [self.input_raw_current, "A"]} ToDo (optional): make functions for this
|
|
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_columnconfigure(2, weight=1, minsize=20)
|
|
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)
|
|
|
|
# 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)
|
|
|
|
row_counter = row_counter + 1
|
|
# Add spacer to Frame below
|
|
|
|
Label(self, text="", pady=10).grid(row=row_counter, column=0)
|
|
|
|
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 dropdown change
|
|
|
|
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
|
|
|
|
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
|
|
|
|
@staticmethod
|
|
def execute_field(vector):
|
|
func.ui_print("field executing", vector)
|
|
try:
|
|
func.set_field_simple(vector * 1e-6) # ToDo: change to set_field
|
|
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 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: 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("<Key>", 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)
|