Files
Helmholtz_Test_Bench/User_Interface.py
T
Martin Zietz fc6ca7284d comments and code cleanup
Logging, and threading cleanup
2021-02-16 11:26:57 +01:00

1020 lines
51 KiB
Python

from tkinter import *
from tkinter import ttk
from tkinter import messagebox
from tkinter import filedialog
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
import os
from os.path import exists
import threading
from datetime import datetime
import globals as g
import cage_func as func
import csv_threading as csv
import config_handling as config
import csv_logging as log
NORM_FONT = ()
HEADER_FONT = ("Arial", 13, "bold")
SUB_HEADER_FONT = ("Arial", 9, "bold")
BIG_BUTTON_FONT = ("Arial", 11, "bold")
SMALL_BUTTON_FONT = ("Arial", 9)
class HelmholtzGUI(Tk):
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, padx=10, pady=10)
mainArea.pack(side="top", fill="both", expand=True)
mainArea.grid_rowconfigure(0, weight=1)
mainArea.grid_columnconfigure(0, weight=1)
self.pages = {} # dictionary for storing all pages
for P in [ManualMode, Configuration, ExecuteCSVMode, ConfigureLogging]:
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_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="Execute CSV Sequence", command=lambda: self.execute_csv_mode(window))
ModeSelector.add_separator()
ModeSelector.add_command(label="Configure Data Logging", command=lambda: self.logging(window))
ModeSelector.add_command(label="Settings...", command=lambda: self.configuration(window))
@staticmethod
def manual_mode(window):
window.show_frame(ManualMode)
@staticmethod
def configuration(window):
window.show_frame(Configuration)
@staticmethod
def execute_csv_mode(window):
window.show_frame(ExecuteCSVMode)
@staticmethod
def logging(window):
window.show_frame(ConfigureLogging)
class ManualMode(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
self.controller = controller # object on which mainloop() is running, usually main window
self.grid_rowconfigure(ALL, weight=1)
self.grid_columnconfigure(ALL, weight=1)
row_counter = 0
# setup title text
header = Label(self, text="Manual Input Mode", font=HEADER_FONT, pady=3)
header.grid(row=row_counter, column=0)
row_counter += 1
# Setup Dropdown Menu for input mode
dropdown_frame = Frame(self)
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 quick power_down
power_down_button = Button(self.buttons_frame, text="Power Down All", command=self.power_down,
pady=5, padx=5, font=BIG_BUTTON_FONT)
power_down_button.grid(row=row_counter, column=1, padx=5)
# add button for reinitialization
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
self.input_mode.trace_add('write', self.change_mode_callback) # call mode change function on dropdown change
self.input_mode.set(default_mode) # call up default mode at the start
self.compensate.trace_add('write', self.change_mode_callback) # call mode change function on checkbox change
def page_switch(self): # function that is called when switching to this page in the UI
self.modes[self.input_mode.get()][2]() # update max values and units, e.g. calls update_max_fields function
# noinspection PyUnusedLocal
# not sure what the parameters are for, but they are necessary
def change_mode_callback(self, var, index, mode): # called input mode dropdown 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)
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]
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 reinitialize(self): # called on "Reinitialize!" 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 power_down(self): # called on "power down" button press
func.power_down_all() # power down outputs on all PSUs and the Arduino
# log change to the log file if user has selected event logging in the Configure Logging window
logger = self.controller.pages[ConfigureLogging] # get object of logging configurator
if logger.event_logging: # data should be logged when test 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)
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
# 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)
@staticmethod
def execute_current(vector):
ui_print("current executing:", vector)
try:
func.set_current_vec(vector)
except ValueError as e:
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
# Setup buttons to select config file
# Setup frame to house buttons:
self.file_select_frame = Frame(self)
self.file_select_frame.grid_rowconfigure(ALL, weight=1)
self.file_select_frame.grid_columnconfigure(ALL, weight=1)
self.file_select_frame.grid(row=row_counter, column=0, sticky=W, padx=20)
# Create and place buttons
load_file_button = Button(self.file_select_frame, text="Load config file...", command=self.load_config,
pady=5, padx=5, font=SMALL_BUTTON_FONT)
load_file_button.grid(row=0, column=0, padx=5)
save_button = Button(self.file_select_frame, text="Save current config", command=self.save_config,
pady=5, padx=5, font=SMALL_BUTTON_FONT)
save_button.grid(row=0, column=1, padx=5)
save_as_button = Button(self.file_select_frame, text="Save current config as...", command=self.save_config_as,
pady=5, padx=5, font=SMALL_BUTTON_FONT)
save_as_button.grid(row=0, column=2, padx=5)
row_counter += 1
# 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) # create variables to store the port names and set to current names
self.Z_port = StringVar(value=g.Z_PORT)
port_vars = [self.XY_port, self.Z_port]
row = 0
for text in entry_texts:
field = Entry(port_frame, textvariable=port_vars[row]) # create entry field
field.grid(row=row, column=1, sticky=W)
axis_label = Label(port_frame, text=text, padx=5, pady=10)
axis_label.grid(row=row, column=0, sticky=W)
info_label = Label(port_frame, text="e.g. COM10")
info_label.grid(row=row, column=2, sticky=W)
row += 1
row_counter += 1
Label(self, text="", pady=0).grid(row=row_counter, column=0) # add spacer
row_counter += 1
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",
"Field to be compensated", "ambient_field", 1e6],
"Resistances:": [[DoubleVar() for _ in range(3)], "\u03A9",
"Resistance of coils + equipment", "resistance", 1],
"Max. Current:": [[DoubleVar() for _ in range(3)], "A", "Max. allowed current", "max_amps", 1],
"Max. Voltage:": [[DoubleVar() for _ in range(3)], "V",
"Max. allowed voltage, must not exceed 16V!", "max_volts", 1],
"Arduino Pins:": [[IntVar() for _ in range(3)], "-", "Should be 15, 16, 17", "relay_pin", 1]
}
self.fields = {}
# 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():
self.fields[key] = []
for axis in range(3): # generate entry fields
field = Entry(value_frame, textvariable=self.entries[key][0][axis], width=10)
field.grid(row=row, column=axis + 1, sticky=W, padx=2)
self.fields[key].append(field) # safe access to field for use elsewhere
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
self.update_fields() # set current values from config file
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 Reinitialize", 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): # restore all default settings
config.reset_config_to_default(config.CONFIG_FILE) # overwrite config file with default
func.setup_all() # setup everything with the defaults
self.update_fields() # update fields in config window
def update_fields(self):
# set current values for all entry variables from config file
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 = config.read_from_config(g.AXIS_NAMES[i], self.entries[key][3],
config.CONFIG_OBJECT) # 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
# check if values are within safe limits:
value_check = func.value_in_limits(g.AXIS_NAMES[i], self.entries[key][3], value)
if value_check == 'OK': # value is acceptable
self.fields[key][i].config(background="White") # set colour of this entry to white
else: # value exceeds limits
self.fields[key][i].config(background="Red") # set colour of this entry to red to show problem
def write_values(self): # update config file with user inputs into entry fields and reinitialize
# set serial ports for PSUs:
config.edit_config("PORTS", "xy_port", self.XY_port.get())
config.edit_config("PORTS", "z_port", self.Z_port.get())
# set numeric values for all axes
for key in self.entries.keys(): # go through rows of entry table
for i in [0, 1, 2]: # go through columns of entry table (axes)
try:
value = self.entries[key][0][i].get() # get value from field
except TclError as e: # wrong format entered, e.g. text in number fields
ui_print("Invalid entry for %s %s %s" % (g.AXIS_NAMES[i], key, e))
else: # format is ok
factor = self.entries[key][4] # get unit conversion factor
if factor not in [0, 1]: # prevent div/0 and conversion of int variables to float
value = value / factor # do unit conversion
# Check if value is within safe limits
config_key = self.entries[key][3] # handle by which value is indexed in config file
value_ok = func.value_in_limits(g.AXIS_NAMES[i], config_key, value)
unit = self.entries[key][1] # get unit string for error messages
axis = g.AXIS_NAMES[i] # get axis name for error messages
if value_ok == 'OK':
config.edit_config(g.AXIS_NAMES[i], config_key, value) # write new value to config file
else: # value is not within limits
if value_ok == 'HIGH':
max_value = g.default_arrays[config_key][1][i] # get max value
message = "Attempted to set too high value for {s} {k}\n" \
"{v} {unit}, max. {mv} {unit} allowed.\n" \
"Excessive values may damage equipment!\n" \
"Do you really want to use this value?" \
.format(s=axis, k=key, v=value * factor, mv=round(max_value * factor, 1), unit=unit)
elif value_ok == 'LOW':
min_value = g.default_arrays[config_key][2][i] # get min value
message = "Attempted to set too low value for {s} {k}\n" \
"{v} {unit}, min. {mv} {unit} allowed.\n" \
"Excessive values may damage equipment!\n" \
"Do you really want to use this value?" \
.format(s=axis, k=key, v=value * factor, mv=round(min_value * factor, 1), unit=unit)
else:
message = "Unknown case, this should not happen."
# display pop-up message to ask user if he really wants the value
answer = messagebox.askquestion("Value out of Bounds", message)
# becomes 'yes' or 'no' depending on user choice
if answer == 'yes': # user really wants the value
# call function to write new value to config file with override=True
config.edit_config(g.AXIS_NAMES[i], config_key, value, True)
# if user chooses 'no' nothing happens, old value is kept
def implement(self): # executed on button press
self.write_values() # write current values from entry fields to config object
func.setup_all() # reinitialize devices and program with new values
self.update_fields() # update entry fields to show new values
def load_config(self): # load configuration from some config file
directory = os.path.dirname(os.path.abspath(config.CONFIG_FILE)) # get directory of current config file
# open file selection dialogue and save path of selected file
filename = filedialog.askopenfilename(initialdir=directory, title="Select Config File",
filetypes=(("Config File", "*.ini*"), ("All Files", "*.*")))
if exists(filename): # does the file exist?
config.CONFIG_FILE = filename # set global config file to the new file
config.CONFIG_OBJECT = config.get_config_from_file(filename) # load from config file to config object
config.check_config(
config.CONFIG_OBJECT) # check the values and display warnings if values are out of bounds
func.setup_all() # reinitialize devices and program with new values
self.update_fields() # update entry fields to show new values
elif filename == '': # this happens when file selection window is closed without selecting a file
ui_print("No file selected, could not load config.")
else:
ui_print("Selected file", filename, "does not exist, could not load config.")
def save_config_as(self): # save current configuration to a new config file
directory = os.path.dirname(os.path.abspath(config.CONFIG_FILE)) # get directory of current config file
# open file selection dialogue and save path of selected file
filename = filedialog.asksaveasfilename(initialdir=directory, title="Save config to file",
filetypes=([("Config File", "*.ini*")]),
defaultextension=[("Config File", "*.ini*")])
if filename == '': # this happens when file selection window is closed without selecting a file
ui_print("No file selected, could not save config.")
else: # a file name was entered
config.CONFIG_FILE = filename # set global config file to the new file
self.write_values() # write current entry field values to the config object
config.write_config_to_file(config.CONFIG_OBJECT) # write contents of config object to file
func.setup_all() # reinitialize devices and program with new values
self.update_fields() # update entry fields to show values as they are in the config
def save_config(self): # same as save_config_as() but with the current config file
self.write_values()
config.write_config_to_file(config.CONFIG_OBJECT)
func.setup_all() # reinitialize devices and program with new values
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)
csv.check_array_ok(self.sequence_array) # check for values exceeding limits
self.display_plot() # plot data and display
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
def __init__(self, parent, controller):
Frame.__init__(self, parent)
self.parent = parent
self.controller = controller # object on which mainloop() is running, usually main window
self.log_file = None # string containing path of log file
self.regular_logging = False # True if data should be logged regularly
self.event_logging = False # True if data should be logged every time a command is sent to the test stand
# log_datapoint() has to be called wherever a command is sent to the test stand and data should be logged
# it does not happen automatically whenever something is sent to the test stand
# It is done mainly in the functions for UI buttons, but rather inconsistently ToDo(optional): make consistent
self.grid_rowconfigure(ALL, weight=1)
self.grid_columnconfigure(ALL, weight=1)
row_counter = 0
# setup heading
header = Label(self, text="Configure Data Logging", font=HEADER_FONT, pady=3)
header.grid(row=row_counter, column=0, padx=100, sticky=W)
row_counter += 1
# Create and place buttons
# Setup frame to house buttons:
self.top_buttons_frame = Frame(self)
self.top_buttons_frame.grid_rowconfigure(ALL, weight=1)
self.top_buttons_frame.grid_columnconfigure(ALL, weight=1)
self.top_buttons_frame.grid(row=row_counter, column=0, sticky=W, padx=20, pady=5)
self.stop_logging_button = Button(self.top_buttons_frame, text="Stop Logging", command=self.stop_logging,
pady=5, padx=5, font=SMALL_BUTTON_FONT)
self.stop_logging_button.grid(row=0, column=0, padx=5)
self.start_logging_button = Button(self.top_buttons_frame, text="Start Logging", command=self.start_logging,
pady=5, padx=5, font=SMALL_BUTTON_FONT)
self.start_logging_button.grid(row=0, column=0, padx=5)
self.write_to_file_button = Button(self.top_buttons_frame, text="Write data to file", font=SMALL_BUTTON_FONT,
command=self.write_to_file, pady=5, padx=5, state="disabled")
self.write_to_file_button.grid(row=0, column=1, padx=5)
self.clear_data_button = Button(self.top_buttons_frame, text="Clear logged data", font=SMALL_BUTTON_FONT,
command=self.clear_data, pady=5, padx=5, state="disabled")
self.clear_data_button.grid(row=0, column=2, padx=5)
row_counter += 1
# Create label showing how many datapoints have been logged
self.log_label_frame = Frame(self)
self.log_label_frame.grid_rowconfigure(ALL, weight=1)
self.log_label_frame.grid_columnconfigure(ALL, weight=1)
self.log_label_frame.grid(row=row_counter, column=0, sticky=W, padx=20)
self.logged_datapoints = IntVar() # create variable to store number of logged datapoints
# Add description label:
datapoints_description = Label(self.log_label_frame, text="Datapoints logged:")
datapoints_description.grid(row=0, column=0, sticky=W)
# Add updatable label to show how much data has been logged
datapoints_label = Label(self.log_label_frame, textvariable=self.logged_datapoints)
datapoints_label.grid(row=0, column=1, sticky=W)
row_counter += 1
# create checkboxes and entries to set how often data should be logged
self.settings_frame = Frame(self)
self.settings_frame.grid_rowconfigure(ALL, weight=1)
self.settings_frame.grid_columnconfigure(ALL, weight=1)
self.settings_frame.grid(row=row_counter, column=0, sticky=W, padx=20)
self.regular_logging_var = BooleanVar(value=True) # create variable for the regular logging checkbox
self.event_logging_var = BooleanVar(value=True) # create variable for the logging on command checkbox
self.log_interval = DoubleVar(value=1) # create variable for logging interval entry field
# create checkboxes for regular and event logging:
self.regular_logging_checkbox = Checkbutton(self.settings_frame, text="Log in regular intervals",
variable=self.regular_logging_var, onvalue=True, offvalue=False)
self.event_logging_checkbox = Checkbutton(self.settings_frame, text="Log whenever test stand is commanded",
variable=self.event_logging_var, onvalue=True, offvalue=False)
self.regular_logging_checkbox.grid(row=0, column=0, sticky=W)
self.event_logging_checkbox.grid(row=1, column=0, sticky=W, columnspan=3)
# Set up entry field for setting logging interval
# Add description label for logging interval entry:
interval_label = Label(self.settings_frame, text=" Interval (s):")
interval_label.grid(row=0, column=1, sticky=W)
# Add entry field to set interval
self.interval_entry = Entry(self.settings_frame, textvariable=self.log_interval)
self.interval_entry.grid(row=0, column=2, sticky=W)
row_counter += 1
# Create checkboxes to select what data to log
self.checkbox_frame = Frame(self)
self.checkbox_frame.grid_rowconfigure(ALL, weight=1)
self.checkbox_frame.grid_columnconfigure(ALL, weight=1)
self.checkbox_frame.grid(row=row_counter, column=0, sticky=W, padx=10, pady=10)
self.checkbox_vars = {} # dictionary containing the bool variables changed by the checkboxes
self.checkboxes = [] # list containing all the checkbox objects, used to enable/disable all of them
self.active_keys = [] # list with all the keys relating to the currently ticked checkboxes
# generate and place all the checkboxes:
checkbox_label = Label(self.checkbox_frame, text="Select which data to log:")
checkbox_label.grid(row=0, column=0, columnspan=2)
# ToDo (optional): Add option to select which axes to log data from
row = 1
for key in log.axis_data_dict.keys():
self.checkbox_vars[key] = BooleanVar(value=True) # create variable for checkbox and put it in dictionary
checkbox = Checkbutton(self.checkbox_frame, text=key, # generate checkbox
variable=self.checkbox_vars[key], onvalue=True, offvalue=False)
checkbox.grid(row=row, column=0, sticky=W) # place checkbox in UI
self.checkboxes.append(checkbox) # add created checkbox to list
row += 1
def page_switch(self): # function that is called when switching to this window
# every class in the UI needs this, even if it doesn't do anything
pass
def start_logging(self):
ui_print("Started data logging.")
self.update_choices() # update list with ticked checkboxes
self.regular_logging = self.regular_logging_var.get() # check if regular logging checkbox is ticked
self.event_logging = self.event_logging_var.get() # check if event logging checkbox is ticked
self.update_datapoint_count() # start regular update of label showing how many datapoints have been collected
if self.logged_datapoints.get() == 0: # no data has been logged so far
# (if condition is here to keep timestamps consistent when repeatedly starting/stopping)
log.zero_time = datetime.now() # set reference time for timestamps in log
error = False
if self.regular_logging:
try: # try to get log interval
interval_ms = int(self.log_interval.get() * 1000)
except TclError as e: # invalid entry for log interval
messagebox.showwarning("Wrong entry format!", "Invalid entry for log interval:\n%s" % e)
self.event_logging = False # don't start logging if there is a problem
error = True
else:
self.periodic_log(interval_ms) # start periodic logging
if (self.regular_logging or self.event_logging) and not error: # logging is active and no error during setup
# lock/unlock buttons and checkboxes:
self.write_to_file_button["state"] = "disabled"
self.clear_data_button["state"] = "normal"
self.lock_checkboxes()
self.stop_logging_button.tkraise() # switch button to stop
def stop_logging(self):
ui_print("Stopped data logging. Remember to save data to file!")
self.regular_logging = False # tell everything its time to stop logging
self.event_logging = False # tell everything its time to stop logging
self.write_to_file_button["state"] = "normal" # enable button
self.unlock_checkboxes() # enable checkboxes
self.start_logging_button.tkraise() # switch start/stop button to start
def write_to_file(self): # lets user select a file and writes logged data to it
filepath = log.select_file() # select a file to write to
if filepath is None: # no valid file was selected
# ask user if he wants to try again:
try_again = messagebox.askquestion("No file selected", "No valid file was selected. Try again?")
if try_again == 'yes': # user wants to try again
self.write_to_file() # call same function again so user can retry
else:
log.write_to_file(log.log_data, filepath) # write logged data to the file
def clear_data(self): # called on button press, asks user if he want to save logged data and then deletes it
if log.unsaved_data: # there is logged data that has not been written to a file yet
# open pop-up to ask user if he wants to save the data:
save_log = messagebox.askquestion("Save log data?", "There seems to be unsaved logging data. "
"Do you wish to write it to a file before deleting?")
if save_log == 'yes': # user has chosen yes
self.write_to_file() # run write to file function to save data
log.clear_logged_data() # delete the logged data
log.unsaved_data = False # tell everything that there is no unsaved data remaining
self.logged_datapoints.set(len(log.log_data)) # update the label showing how much data has been logged
ui_print("Log data cleared.")
def update_choices(self):
# updates the list storing which checkboxes are currently ticked
# (this is passed to logging functions and determines which data is logged)
self.active_keys = [] # initialize the list
for key in self.checkbox_vars.keys(): # go through all checkboxes
if self.checkbox_vars[key].get(): # box is ticked
self.active_keys.append(key) # add corresponding item to the list
def lock_checkboxes(self): # lock all checkboxes, so they can not be modified while logging
for checkbox in [*self.checkboxes, self.event_logging_checkbox, self.regular_logging_checkbox]:
checkbox.config(state=DISABLED)
self.interval_entry.config(state=DISABLED)
def unlock_checkboxes(self):
for checkbox in [*self.checkboxes, self.event_logging_checkbox, self.regular_logging_checkbox]:
checkbox.config(state=NORMAL)
self.interval_entry.config(state=NORMAL)
def periodic_log(self, interval): # logs data in regular intervals (ms)
if self.regular_logging: # logging in intervals is active
self.log_datapoint()
self.controller.after(interval, lambda: self.periodic_log(interval)) # call again after time interval
def log_datapoint(self): # log a single datapoint based on which checkboxes are ticked
try:
log.log_datapoint(self.active_keys) # add datapoint with active checkboxes to log data frame
except Exception as e:
messagebox.showerror("Error!", "Error while logging data: \n%s" % e)
def update_datapoint_count(self):
if self.regular_logging or self.event_logging: # logging is active
self.logged_datapoints.set(len(log.log_data)) # update label with number of rows in log_data
self.controller.after(1000, self.update_datapoint_count) # call function again after 1 second
class StatusDisplay(Frame):
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()
def continuous_label_update(self, controller, interval): # update display values in regular intervals (ms)
if not g.exitFlag: # app ist still running
self.update_labels()
controller.after(interval, lambda: self.continuous_label_update(controller, interval))
def update_labels(self): # update all values in the status display
g.ARDUINO.update_status_info()
i = 0
for axis in g.AXES:
if axis.device is not None:
axis.update_status_info()
# update all label variables with current values:
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 += 1
class OutputConsole(Frame): # console to print stuff in, similar to standard python output
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)
def ui_print(*content): # prints text to built in console, use exactly like normal print()
output = ""
for text in content:
output = " ".join((output, str(text))) # merge all contents into one string
if not g.exitFlag:
output = "".join(("\n", output)) # begin new line each time
g.app.OutputConsole.console.insert(END, output) # print to console
g.app.OutputConsole.console.see(END) # scroll to bottom
else: # if window is not open, do normal print
print(output)