forked from zietzm/Helmholtz_Test_Bench
cleanup and comments
user interface
This commit is contained in:
+285
-258
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user