cleanup and comments

user interface
This commit is contained in:
Martin Zietz
2021-02-17 11:26:25 +01:00
parent 3b62e41f5a
commit f5dc7f097e
5 changed files with 295 additions and 261 deletions
+285 -258
View File
@@ -1,3 +1,6 @@
# 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.
from tkinter import *
from tkinter import ttk
from tkinter import messagebox
@@ -17,7 +20,7 @@ import csv_threading as csv
import config_handling as config
import csv_logging as log
NORM_FONT = ()
# define font styles:
HEADER_FONT = ("Arial", 13, "bold")
SUB_HEADER_FONT = ("Arial", 9, "bold")
BIG_BUTTON_FONT = ("Arial", 11, "bold")
@@ -25,54 +28,62 @@ SMALL_BUTTON_FONT = ("Arial", 9)
class HelmholtzGUI(Tk):
# main application window, almost everything else here es 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")
Tk.wm_iconbitmap(self, "Helmholtz.ico")
Tk.wm_title(self, "Helmholtz Cage Control") # set title of the window
Tk.wm_iconbitmap(self, "Helmholtz.ico") # set application icon
self.Menu = TopMenu(self) # displays menu bar at the top
self.Menu = TopMenu(self) # display dropdown menu bar at the top (see TopMenu class for details)
mainArea = Frame(self, padx=10, pady=10)
mainArea.pack(side="top", fill="both", expand=True)
mainArea = Frame(self, padx=10, pady=10) # create main area Frame where controls of each mode are displayed
mainArea.pack(side="top", fill="both", expand=True) # pack main area at the top of the window
mainArea.grid_rowconfigure(0, weight=1)
mainArea.grid_rowconfigure(0, weight=1) # configure rows and columns of the Tkinter grid to expand with window
mainArea.grid_columnconfigure(0, weight=1)
self.pages = {} # dictionary for storing all pages
# 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
for P in [ManualMode, Configuration, ExecuteCSVMode, ConfigureLogging]:
page = P(mainArea, self)
self.pages[P] = page
page.grid(row=0, column=0, sticky="nsew")
self.pages = {} # dictionary for storing all pages (different modes, displayed in main area)
for P in [ManualMode, Configuration, ExecuteCSVMode, ConfigureLogging]: # do this for every mode page
page = P(mainArea, self) # initialize the page with the mainArea 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
status_frame = Frame(self)
status_frame.pack(side="bottom", fill="x", expand=False)
status_frame.grid_columnconfigure(1, weight=1)
# 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")
self.show_frame(ManualMode)
self.show_frame(ManualMode) # show manual mode to start with
def show_frame(self, key):
frame = self.pages[key] # gets correct page from the dictionary
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() # brings this frame to the front
frame.tkraise() # bring this frame to the front
class TopMenu:
# the menu bar at the top of the window
def __init__(self, window):
menu = Menu(window)
window.config(menu=menu)
menu = Menu(window) # initialize Menu object
window.config(menu=menu) # put menu at the top of the window
ModeSelector = Menu(menu)
menu.add_cascade(label="Mode", menu=ModeSelector)
ModeSelector = Menu(menu) # create a submenu object
menu.add_cascade(label="Mode", menu=ModeSelector) # add a dropdown with the submenu object
# create the different options in the dropdown:
ModeSelector.add_command(label="Static Manual Input", command=lambda: self.manual_mode(window))
ModeSelector.add_command(label="Execute CSV Sequence", command=lambda: self.execute_csv_mode(window))
ModeSelector.add_separator()
@@ -80,33 +91,34 @@ class TopMenu:
ModeSelector.add_command(label="Settings...", command=lambda: self.configuration(window))
@staticmethod
def manual_mode(window):
def manual_mode(window): # switch to the manual mode page
window.show_frame(ManualMode)
@staticmethod
def configuration(window):
def configuration(window): # switch to the settings page
window.show_frame(Configuration)
@staticmethod
def execute_csv_mode(window):
def execute_csv_mode(window): # switch to the CSV execution page
window.show_frame(ExecuteCSVMode)
@staticmethod
def logging(window):
def logging(window): # switch to the logging settings page
window.show_frame(ConfigureLogging)
class ManualMode(Frame):
# Mode for manually setting currents and fields on the test stand.
# 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)
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)
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
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)
@@ -115,48 +127,54 @@ class ManualMode(Frame):
row_counter += 1
# Setup Dropdown Menu for input mode
dropdown_frame = Frame(self)
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)
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]
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
# 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)
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:"]
self.entry_vars = [StringVar() for _ in range(3)]
self.max_value_vars = [StringVar() for _ in range(3)]
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:
field = ttk.Entry(self.entries_frame, textvariable=self.entry_vars[row])
self.entry_vars[row].set(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)
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)
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
@@ -164,12 +182,13 @@ class ManualMode(Frame):
row_counter += 1
# setup checkbox for compensating ambient field
checkbox_frame = Frame(self, padx=20)
checkbox_frame = Frame(self, padx=20) # create frame to house it
checkbox_frame.grid(row=row_counter, column=0, sticky=W)
self.compensate = IntVar(value=1)
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=1, offvalue=0)
variable=self.compensate, onvalue=True, offvalue=False)
self.compensate_checkbox.pack(side="left")
row_counter += 1
@@ -192,13 +211,12 @@ class ManualMode(Frame):
pady=5, padx=5, font=BIG_BUTTON_FONT)
power_down_button.grid(row=row_counter, column=1, padx=5)
# add button for reinitialization
# 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
# Add spacer to Frame below
Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer
@@ -210,31 +228,36 @@ class ManualMode(Frame):
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 they are necessary
def change_mode_callback(self, var, index, mode): # called input mode dropdown is changed
# 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 update_max_fields(self): # update labels with maximum allowable field values
self.compensate_checkbox.config(state=NORMAL)
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)
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:
for var in self.max_value_vars: # go through the max value labels for each axis
if not compensate: # ambient field should not be compensated
field = g.AXES[i].max_field * 1e6 # get max values from the axis object
elif compensate: # ambient field should be compensated
field = g.AXES[i].max_comp_field * 1e6
else:
else: # this really should never happen
field = [0, 0]
ui_print("Unexpected value encountered: compensate =", comp)
val.set("(%0.1f to %0.1f \u03BCT)" % (field[0], field[1]))
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
i += 1
def update_max_currents(self): # update labels with maximum allowable current values
self.compensate_checkbox.config(state=DISABLED)
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
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))
for var in self.max_value_vars: # go through the max value labels for each axis
var.set("(%0.2f to %0.2f A)" % (-g.AXES[i].max_amps, g.AXES[i].max_amps)) # update the label
i += 1
def reinitialize(self): # called on "Reinitialize!" button press
@@ -253,38 +276,202 @@ class ManualMode(Frame):
if logger.event_logging: # data should be logged when test stand is commanded
logger.log_datapoint() # log data
def execute(self):
function_to_call = self.modes[self.input_mode.get()][0] # get function of appropriate mode
vector = np.array([0, 0, 0], dtype=float)
def execute(self): # called on "Execute!" button press
# reads values from the entry fields and commands the test stand accordingly
vector = np.array([0, 0, 0], dtype=float) # initialize vector to later send to test stand
i = 0
for var in self.entry_vars:
vector[i] = float(var.get())
i = i + 1
function_to_call(vector) # call function
self.controller.StatusDisplay.update_labels() # update status display after change
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())
self.controller.StatusDisplay.update_labels() # update status display after change
# log change to the log file if user has selected event logging in the Configure Logging window
logger = self.controller.pages[ConfigureLogging] # get object of logging configurator
if logger.event_logging: # data should be logged when test stand is commanded
logger.log_datapoint() # log data
def execute_field(self, vector): # convert magnetic field vector and send to test stand
ui_print("Field executing:", vector, "\u03BCT")
compensate = self.compensate.get() # read out if compensate ambient field checkbox is ticked
if compensate: # ambient field should be compensated
func.set_field(vector * 1e-6) # convert to Tesla and send to test stand
elif not compensate: # ambient field should not be compensated
func.set_field_simple(vector * 1e-6) # convert to Tesla and send to test stand
else: # this really should never happen
ui_print("Unexpected value encountered: compensate =", compensate)
messagebox.showerror("Unexpected Value!", ("Unexpected value encountered: compensate =", compensate))
@staticmethod
def execute_current(vector): # send current vector to the test stand
ui_print("Current executing:", vector, "A")
func.set_current_vec(vector) # command test stand
class ExecuteCSVMode(Frame):
# generate configuration window to set program constants
def __init__(self, parent, controller):
Frame.__init__(self, parent)
self.parent = parent
self.controller = controller # object on which mainloop() is running, usually main window
# Functional init:
self.csv_thread = None # the thread object for executing csv
self.sequence_array = None # array containing the values from the csv file
# Build UI:
self.grid_rowconfigure(ALL, weight=1)
self.grid_columnconfigure(ALL, weight=1)
row_counter = 0
self.row_elements = [] # make list of elements in rows to calculate height available for plot
# setup heading
header = Label(self, text="Execute CSV Mode", font=HEADER_FONT, pady=3)
header.grid(row=row_counter, column=0, padx=100, sticky=W)
self.row_elements.append(header)
row_counter += 1
# Setup 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)
self.row_elements.append(self.top_buttons_frame)
# Create and place buttons
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)
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)
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 for reinitialization
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 testing 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)
checkbox_label = Label(self.checkbox_frame, text="Disable device connection checks:")
checkbox_label.grid(row=0, column=0, sticky=W, padx=3)
self.xy_override = BooleanVar(value=False)
self.z_override = BooleanVar(value=False)
self.arduino_override = BooleanVar(value=False)
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
self.plotFrame = Frame(self)
self.plotFrame.grid_rowconfigure(0, weight=1)
self.plotFrame.grid_columnconfigure(0, weight=1)
self.plotFrame.grid(row=row_counter, column=0, sticky="nsw", padx=10, pady=10)
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 save path of selected file
filename = filedialog.askopenfilename(initialdir=directory, title="Select CSV File",
filetypes=(("Comma Separated Values", "*.csv*"), ("All Files", "*.*")))
if exists(filename): # does the file exist?
ui_print("File selected:", filename)
try:
self.sequence_array = csv.read_csv_to_array(filename) # read array from csv
except BaseException as e:
ui_print("Error while opening file:", e)
messagebox.showerror("Error!", "Error while opening file: \n%s" % e)
try:
csv.check_array_ok(self.sequence_array) # check for values exceeding limits
self.display_plot() # plot data and display
except BaseException as e:
ui_print("Error while processing data from file:", e)
messagebox.showerror("Error!", "Error while processing data from file: \n%s" % e)
else:
self.execute_button["state"] = "normal" # activate run button
elif filename == '': # this happens when file selection window is closed without selecting a file
ui_print("No file selected, could not load.")
else:
ui_print("Selected file", filename, "does not exist, could not load.")
def run_sequence(self):
# (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"
g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later
# create separate thread to run sequence execution in:
self.csv_thread = csv.ExecCSVThread(self.sequence_array, self, self.controller)
self.csv_thread.start() # start thread
def stop_run(self):
self.csv_thread.stop() # this will cause the csv loop to end
# (de)activate buttons as needed:
self.select_file_button["state"] = "normal"
self.execute_button["state"] = "normal"
self.stop_button["state"] = "disabled"
self.reinit_button["state"] = "normal"
# log change to the log file if user has selected event logging in the Configure Logging window
logger = self.controller.pages[ConfigureLogging] # get object of logging configurator
if logger.event_logging: # data should be logged when test stand is commanded
logger.log_datapoint() # log data
def execute_field(self, vector):
ui_print("field executing", vector)
comp = self.compensate.get()
if comp == 1:
func.set_field(vector * 1e-6)
elif comp == 0:
func.set_field_simple(vector * 1e-6)
else:
ui_print("Unexpected value encountered: compensate =", comp)
def reinitialize(self): # called on "Reinitialize devices" button press
func.setup_all() # reinitialize all PSUs and the Arduino
@staticmethod
def execute_current(vector):
ui_print("current executing:", vector)
try:
func.set_current_vec(vector)
except ValueError as e:
ui_print(e)
# log change to the log file if user has selected event logging in the Configure Logging window
logger = self.controller.pages[ConfigureLogging] # get object of logging configurator
if logger.event_logging: # data should be logged when test stand is commanded
logger.log_datapoint() # log data
def display_plot(self):
# calculate available height for plot (in pixels):
height_others = 0
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
height = self.parent.winfo_height() - height_others - 50 # set to height of parent frame - other rows - margin
width = min(self.parent.winfo_width() - 100, 1100) # set width to available space but max. 1100
figure = csv.plot_field_sequence(self.sequence_array, width, height) # create figure to be displayed
plotCanvas = FigureCanvasTkAgg(figure, self.plotFrame) # create canvas to draw figure on
plotCanvas.draw() # equivalent to matplotlib.show()
plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in UI
class Configuration(Frame):
@@ -546,166 +733,6 @@ class Configuration(Frame):
self.update_fields() # update entry fields to show values as they are in the config
class ExecuteCSVMode(Frame):
# generate configuration window to set program constants
def __init__(self, parent, controller):
Frame.__init__(self, parent)
self.parent = parent
self.controller = controller # object on which mainloop() is running, usually main window
# Functional init:
self.csv_thread = None # the thread object for executing csv
self.sequence_array = None # array containing the values from the csv file
# Build UI:
self.grid_rowconfigure(ALL, weight=1)
self.grid_columnconfigure(ALL, weight=1)
row_counter = 0
self.row_elements = [] # make list of elements in rows to calculate height available for plot
# setup heading
header = Label(self, text="Execute CSV Mode", font=HEADER_FONT, pady=3)
header.grid(row=row_counter, column=0, padx=100, sticky=W)
self.row_elements.append(header)
row_counter += 1
# Setup 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)
self.row_elements.append(self.top_buttons_frame)
# Create and place buttons
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)
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)
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 for reinitialization
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 testing 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)
checkbox_label = Label(self.checkbox_frame, text="Disable device connection checks:")
checkbox_label.grid(row=0, column=0, sticky=W, padx=3)
self.xy_override = BooleanVar(value=False)
self.z_override = BooleanVar(value=False)
self.arduino_override = BooleanVar(value=False)
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
self.plotFrame = Frame(self)
self.plotFrame.grid_rowconfigure(0, weight=1)
self.plotFrame.grid_columnconfigure(0, weight=1)
self.plotFrame.grid(row=row_counter, column=0, sticky="nsw", padx=10, pady=10)
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 save path of selected file
filename = filedialog.askopenfilename(initialdir=directory, title="Select CSV File",
filetypes=(("Comma Separated Values", "*.csv*"), ("All Files", "*.*")))
if exists(filename): # does the file exist?
ui_print("File selected:", filename)
try:
self.sequence_array = csv.read_csv_to_array(filename) # read array from csv
except BaseException as e:
ui_print("Error while opening file:", e)
messagebox.showerror("Error!", "Error while opening file: \n%s" % e)
try:
csv.check_array_ok(self.sequence_array) # check for values exceeding limits
self.display_plot() # plot data and display
except BaseException as e:
ui_print("Error while processing data from file:", e)
messagebox.showerror("Error!", "Error while processing data from file: \n%s" % e)
else:
self.execute_button["state"] = "normal" # activate run button
elif filename == '': # this happens when file selection window is closed without selecting a file
ui_print("No file selected, could not load.")
else:
ui_print("Selected file", filename, "does not exist, could not load.")
def run_sequence(self):
# (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"
g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later
# create separate thread to run sequence execution in:
self.csv_thread = csv.ExecCSVThread(self.sequence_array, self, self.controller)
self.csv_thread.start() # start thread
def stop_run(self):
self.csv_thread.stop() # this will cause the csv loop to end
# (de)activate buttons as needed:
self.select_file_button["state"] = "normal"
self.execute_button["state"] = "normal"
self.stop_button["state"] = "disabled"
self.reinit_button["state"] = "normal"
# log change to the log file if user has selected event logging in the Configure Logging window
logger = self.controller.pages[ConfigureLogging] # get object of logging configurator
if logger.event_logging: # data should be logged when test stand is commanded
logger.log_datapoint() # log data
def reinitialize(self): # called on "Reinitialize devices" button press
func.setup_all() # reinitialize all PSUs and the Arduino
# log change to the log file if user has selected event logging in the Configure Logging window
logger = self.controller.pages[ConfigureLogging] # get object of logging configurator
if logger.event_logging: # data should be logged when test stand is commanded
logger.log_datapoint() # log data
def display_plot(self):
# calculate available height for plot (in pixels):
height_others = 0
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
height = self.parent.winfo_height() - height_others - 50 # set to height of parent frame - other rows - margin
width = min(self.parent.winfo_width() - 100, 1100) # set width to available space but max. 1100
figure = csv.plot_field_sequence(self.sequence_array, width, height) # create figure to be displayed
plotCanvas = FigureCanvasTkAgg(figure, self.plotFrame) # create canvas to draw figure on
plotCanvas.draw() # equivalent to matplotlib.show()
plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in UI
class ConfigureLogging(Frame):
# generate window to configure data logging to csv
# ToDo: support logging of axis-independent info like Arduino status
+5 -2
View File
@@ -1,11 +1,13 @@
# This file contains all classes and functions directly related to the operation of the helmholtz test stand.
# The two main classes are Axis and ArduinoCtrl, see their definitions for details.
# import packages:
import numpy as np
import serial
import traceback
from tkinter import messagebox
# import other project files
from User_Interface import ui_print
from pyps2000b import PS2000B
from Arduino import Arduino
@@ -122,7 +124,8 @@ class Axis:
if abs(value) > self.max_amps: # prevent excessive currents
self.power_down() # set output to 0 and deactivate
raise ValueError("Invalid current value. Tried %0.2fA, max. %0.2fA allowed" % (value, self.max_amps))
raise ValueError("Invalid current value on %s. Tried %0.2fA, max. %0.2fA allowed"
% (self.name, value, self.max_amps))
elif value >= 0: # switch the e-box relay to change polarity as needed
g.ARDUINO.digitalWrite(self.ardPin, "LOW") # command the output pin on the arduino in the electronics box
@@ -384,7 +387,7 @@ def set_current_vec(vector): # sets currents on each axis according to given ve
axis.set_signed_current(vector[i]) # command test stand to set the current
except ValueError as e: # current was too high
ui_print(e)
ui_print(e) # print out the error message
i += 1
+2
View File
@@ -1,9 +1,11 @@
# This file contains functions and variables related to reading and writing configuration files.
# The configparser module is used for processing. Config files are of type .ini
# import packages:
from configparser import ConfigParser
from tkinter import messagebox
# import other project files:
import globals as g
import cage_func as func
# noinspection PyPep8Naming
+3
View File
@@ -1,12 +1,15 @@
# This file contains functions related to logging data from the program to a CSV file.
# They are mainly but not only called by the ConfigureLogging class in User_Interface.py.
# import packages
import pandas as pd
import globals as g
from datetime import datetime
import os
from tkinter import filedialog
from tkinter import messagebox
# import other project files
import User_Interface as ui
log_data = pd.DataFrame() # pandas data frame containing the logged data, in-program representation of csv/excel data
-1
View File
@@ -33,7 +33,6 @@ default_arrays = {
"coil_const": np.array([[38.6, 38.45, 37.9], [50, 50, 50], [0, 0, 0]]) * 1e-6, # Coil constants [x,y,z] [T/A]
"ambient_field": np.array([[30, 30, 30], [200, 200, 200], [-200, -200, -200]]) * 1e-6, # background magnetic field [T]
"resistance": np.array([[1.7, 1.7, 1.7], [5, 5, 5], [1, 1, 1]], dtype=float), # resistance of circuits [Ohm]
"max_watts": np.array([[15, 15, 15], [50, 50, 50], [0, 0, 0]], dtype=float), # max. allowed power for circuits [W]
"max_volts": np.array([[14, 14, 14], [16, 16, 16], [0, 0, 0]], dtype=float), # max. allowed voltage, limited to 16V by used diodes! [V]
"max_amps": np.array([[4.5, 4.5, 4.5], [6, 6, 6], [0, 0, 0]], dtype=float), # max. allowed current (A)
"relay_pin": [[15, 16, 17], [15, 16, 17], [15, 16, 17]] # pins on the arduino for reversing [x,y,z] polarity