forked from zietzm/Helmholtz_Test_Bench
2034 lines
111 KiB
Python
2034 lines
111 KiB
Python
# 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.
|
|
|
|
# ToDo: optimize layout for smaller screen (like on IRS clean room PC)
|
|
# import packages for user interface:
|
|
import queue
|
|
from queue import Queue, Empty
|
|
from tkinter import *
|
|
from tkinter import ttk
|
|
from tkinter import messagebox
|
|
from tkinter import filedialog
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
|
|
# import general packages:
|
|
import numpy as np
|
|
import os
|
|
import os.path
|
|
import threading
|
|
from datetime import datetime
|
|
from math import pi
|
|
import csv
|
|
|
|
# import other project files:
|
|
import src.globals as g
|
|
import src.csv_threading as csv_threading
|
|
import src.config_handling as config
|
|
import src.csv_logging as log
|
|
from src.calibration import AmbientFieldCalibration, CoilConstantCalibration, MagnetometerCalibration
|
|
from src.exceptions import DeviceAccessError
|
|
from src.utility import ui_print, save_dict_list_to_csv
|
|
import src.helmholtz_cage_device as helmholtz_cage_device
|
|
|
|
# define font styles:
|
|
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):
|
|
# main application window, almost everything else here is 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") # set title of the window
|
|
try:
|
|
Tk.wm_iconbitmap(self, "Helmholtz.ico") # set application icon
|
|
except TclError:
|
|
pass
|
|
|
|
self.Menu = TopMenu(self) # display dropdown menu bar at the top (see TopMenu class for details)
|
|
|
|
# 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")
|
|
|
|
main_area = Frame(self, padx=10, pady=10) # create main area Frame where controls of each mode are displayed
|
|
main_area.pack(side="top", fill="both", expand=True) # pack main area at the top of the window
|
|
|
|
main_area.grid_rowconfigure(0, weight=1) # configure rows and columns of the Tkinter grid to expand with window
|
|
main_area.grid_columnconfigure(0, weight=1)
|
|
|
|
# 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
|
|
|
|
self.pages = {} # dictionary for storing all pages (different modes, displayed in main area)
|
|
for P in [ManualMode,
|
|
HardwareConfiguration,
|
|
CalibrateAmbientField,
|
|
CalibrateMagnetometer,
|
|
ExecuteCSVMode,
|
|
ConfigureLogging]: # do this for every mode page
|
|
page = P(main_area, self) # initialize the page with the main_area 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
|
|
|
|
self.show_frame(ManualMode) # show manual mode to start with
|
|
|
|
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() # bring this frame to the front
|
|
|
|
|
|
class TopMenu:
|
|
# the menu bar at the top of the window
|
|
def __init__(self, window):
|
|
self.window = window
|
|
menu = Menu(window) # initialize Menu object
|
|
window.config(menu=menu) # put menu at the top of the window
|
|
|
|
mode_selector = Menu(menu, tearoff=0) # create a submenu object
|
|
menu.add_cascade(label="Menu", menu=mode_selector) # add a dropdown with the submenu object
|
|
# create the different options in the dropdown:
|
|
mode_selector.add_command(label="Static Manual Input", command=self.manual_mode)
|
|
mode_selector.add_command(label="Execute CSV Sequence", command=self.execute_csv_mode)
|
|
mode_selector.add_command(label="Calibrate Ambient Field", command=self.calibrate_ambient)
|
|
mode_selector.add_command(label="Calibrate Magnetometer", command=self.calibrate_magnetometer)
|
|
mode_selector.add_separator()
|
|
mode_selector.add_command(label="Configure Data Logging", command=self.logging)
|
|
mode_selector.add_command(label="Settings...", command=self.configuration)
|
|
|
|
def manual_mode(self): # switch to the manual mode page
|
|
self.window.show_frame(ManualMode)
|
|
|
|
def configuration(self): # switch to the settings page
|
|
self.window.show_frame(HardwareConfiguration)
|
|
|
|
def calibrate_ambient(self):
|
|
self.window.show_frame(CalibrateAmbientField)
|
|
|
|
def calibrate_magnetometer(self):
|
|
self.window.show_frame(CalibrateMagnetometer)
|
|
|
|
def execute_csv_mode(self): # switch to the CSV execution page
|
|
self.window.show_frame(ExecuteCSVMode)
|
|
|
|
def logging(self): # switch to the logging settings page
|
|
self.window.show_frame(ConfigureLogging)
|
|
|
|
|
|
class ManualMode(Frame):
|
|
# Mode for manually setting currents and fields on the test bench.
|
|
# 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) # initialize the frame object
|
|
|
|
self.controller = controller # object on which mainloop() is running, usually main window
|
|
|
|
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 # 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)
|
|
header.grid(row=row_counter, column=0)
|
|
|
|
row_counter += 1
|
|
|
|
# Setup Dropdown Menu for input mode
|
|
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) # 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 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) # 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:"] # 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: # 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) # create label showing the axis
|
|
axis_label.grid(row=row, column=0, sticky=W)
|
|
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
|
|
|
|
row_counter += 1
|
|
|
|
# setup checkbox for compensating ambient field
|
|
checkbox_frame = Frame(self, padx=20) # create frame to house it
|
|
checkbox_frame.grid(row=row_counter, column=0, sticky=W)
|
|
|
|
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=True, offvalue=False)
|
|
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 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
|
|
|
|
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 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 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)
|
|
for i, var in enumerate(self.max_value_vars): # go through the max value labels for each axis
|
|
if not compensate: # ambient field should not be compensated
|
|
field = g.CAGE_DEVICE.axes[i].max_field * 1e6 # get max values from the axis object
|
|
elif compensate: # ambient field should be compensated
|
|
field = g.CAGE_DEVICE.axes[i].max_comp_field * 1e6
|
|
else: # this really should never happen
|
|
field = [0, 0]
|
|
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
|
|
|
|
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
|
|
for i, var in enumerate(self.max_value_vars): # go through the max value labels for each axis
|
|
# update the label
|
|
var.set("(%0.2f to %0.2f A)" % (-g.CAGE_DEVICE.axes[i].max_amps, g.CAGE_DEVICE.axes[i].max_amps))
|
|
|
|
def reinitialize(self): # called on "Reinitialize!" button press
|
|
# reinitialize all PSUs and the Arduino
|
|
g.CAGE_DEVICE.reconnect_hardware_async()
|
|
|
|
# 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 bench is commanded
|
|
logger.log_datapoint() # log data
|
|
|
|
def power_down(self): # called on "power down" button press
|
|
g.CAGE_DEVICE.shutdown() # 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 bench is commanded
|
|
logger.log_datapoint() # log data
|
|
|
|
def execute(self): # called on "Execute!" button press
|
|
# reads values from the entry fields and commands the test bench accordingly
|
|
|
|
vector = np.array([0, 0, 0], dtype=float) # initialize vector to later send to test bench
|
|
i = 0
|
|
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())
|
|
|
|
# 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 bench is commanded
|
|
logger.log_datapoint() # log data
|
|
|
|
def execute_field(self, vector): # convert magnetic field vector and send to test bench
|
|
ui_print("\nField executing:", vector, "\u03BCT")
|
|
# Acquire a proxy to the helmholtz cage:
|
|
# This can fail if already in use
|
|
try:
|
|
with g.CAGE_DEVICE as cage_dev:
|
|
compensate = self.compensate.get() # read out if compensate ambient field checkbox is ticked
|
|
if compensate: # ambient field should be compensated
|
|
cage_dev.set_field_compensated(vector * 1e-6) # convert to Tesla and send to test bench
|
|
pass
|
|
elif not compensate: # ambient field should not be compensated
|
|
cage_dev.set_field_raw(vector * 1e-6) # convert to Tesla and send to test bench
|
|
pass
|
|
else: # this really should never happen
|
|
ui_print("Unexpected value encountered: compensate =", compensate)
|
|
messagebox.showerror("Unexpected Value!",
|
|
("Unexpected value encountered: compensate =", compensate))
|
|
except helmholtz_cage_device.DeviceBusy:
|
|
ui_print("Error: Could not acquire control. Is the HW already in use?")
|
|
|
|
@staticmethod
|
|
def execute_current(vector): # send current vector to the test bench
|
|
ui_print("\nCurrent executing:", vector, "A")
|
|
with g.CAGE_DEVICE as cage_dev:
|
|
# This can fail if already in use
|
|
if cage_dev is None:
|
|
ui_print("Error: Could not acquire control. Is the HW already in use?")
|
|
return
|
|
|
|
cage_dev.set_signed_currents(vector) # command test bench
|
|
|
|
|
|
class ExecuteCSVMode(Frame):
|
|
# Mode for executing magnetic field sequences from csv files.
|
|
# Inherits the Frame object from Tkinter and is placed in the mainArea of the application window.
|
|
# Multithreading is used to execute the sequence without crashing the UI
|
|
|
|
def __init__(self, parent, controller):
|
|
Frame.__init__(self, parent)
|
|
self.parent = parent # parent UI object, e.g. the mainArea frame
|
|
self.controller = controller # object on which mainloop() is running, usually main window
|
|
|
|
# Functional init:
|
|
self.csv_thread = None # the thread object for executing a csv sequence
|
|
self.sequence_array = None # array containing the values from the csv file
|
|
self.sequence_array_ok = False # Is the data valid?
|
|
|
|
# Tkinter variables for axis hardware checks. Controlled by checkboxes.
|
|
self.xy_override = BooleanVar(value=False) # True to disable connection check for XY PSU
|
|
self.z_override = BooleanVar(value=False) # True to disable connection check for Z PSU
|
|
self.arduino_override = BooleanVar(value=False) # True to disable connection check for arduino
|
|
|
|
# --- UI ELEMENTS ---
|
|
row_counter = 0 # keep track of which grid row we are in
|
|
|
|
self.row_elements = [] # make list of elements in rows to later calculate height available for plot
|
|
|
|
# setup header
|
|
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) # add to list of row elements
|
|
|
|
row_counter += 1
|
|
|
|
# Setup buttons
|
|
# Setup frame to house buttons:
|
|
self.top_buttons_frame = Frame(self)
|
|
self.top_buttons_frame.grid(row=row_counter, column=0, sticky=W, padx=20)
|
|
|
|
self.row_elements.append(self.top_buttons_frame) # add frame to list of row elements
|
|
|
|
# Create and place buttons:
|
|
# add button for selecting a csv file to execute:
|
|
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)
|
|
# add button to start running the sequence from the file:
|
|
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)
|
|
# add button to stop/interrupt the sequence:
|
|
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 to reinitialize devices (e.g. after reconnecting a device)
|
|
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 checkboxes to disable checks if devices are connected (enables testing with some devices missing)
|
|
# setup frame to house 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) # add frame to list of row elements
|
|
|
|
# setup label to explain checkbox function:
|
|
checkbox_label = Label(self.checkbox_frame, text="Disable device connection checks:")
|
|
checkbox_label.grid(row=0, column=0, sticky=W, padx=3)
|
|
|
|
# create checkboxes:
|
|
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 (plot is generated and placed in display_plot() method)
|
|
self.plot_frame = Frame(self)
|
|
self.plot_frame.grid_rowconfigure(0, weight=1)
|
|
self.plot_frame.grid_columnconfigure(0, weight=1)
|
|
self.plot_frame.grid(row=row_counter, column=0, sticky="nsw", padx=10, pady=10)
|
|
|
|
self.plot_canvas = None # Is generated upon plotting
|
|
|
|
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 store path of selected file
|
|
filename = filedialog.askopenfilename(initialdir=directory, title="Select CSV File",
|
|
filetypes=(("Comma Separated Values", "*.csv*"), ("All Files", "*.*")))
|
|
if os.path.exists(filename): # does the file exist?
|
|
ui_print("CSV file selected:", filename)
|
|
try: # try to read data to an array
|
|
self.sequence_array = csv_threading.read_csv_to_array(filename) # read array from csv
|
|
except Exception as e: # something went wrong, probably wrong format in csv
|
|
self.sequence_array_ok = False
|
|
# display error messages:
|
|
ui_print("Error while opening file:", e)
|
|
messagebox.showerror("Error!", "Error while opening file: \n%s" % e)
|
|
|
|
try: # try to check the values and display the plot
|
|
csv_threading.check_array_ok(self.sequence_array) # check for values exceeding limits
|
|
self.sequence_array_ok = True # Has nothing to do with limits. Just means the data was parsed
|
|
self.display_plot() # plot data and display
|
|
except Exception as e: # something went wrong, probably wrong format in csv
|
|
self.sequence_array_ok = False
|
|
# display error messages:
|
|
ui_print("Error while processing data from file:", e)
|
|
messagebox.showerror("Error!", "Error while processing data from file: \n%s" % e)
|
|
else: # nothing went wrong
|
|
self.execute_button["state"] = "normal" # activate run button --> enable execution
|
|
elif filename == '': # this happens when file selection window is closed without selecting a file
|
|
ui_print("No file selected, could not load.")
|
|
else: # file does not exist
|
|
# display error messages:
|
|
ui_print("Selected file", filename, "does not exist, could not load.")
|
|
messagebox.showerror("File not found", "Selected file %s does not exist, could not load." % filename)
|
|
|
|
def run_sequence(self): # called on run button press, starts thread for executing the sequence
|
|
# create thread object:
|
|
try:
|
|
self.csv_thread = csv_threading.ExecCSVThread(self.sequence_array, self, self.controller)
|
|
self.csv_thread.start() # start thread
|
|
|
|
# (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"
|
|
except DeviceAccessError as e:
|
|
ui_print(e)
|
|
|
|
def stop_run(self): # called on stop button press, interrupts sequence execution
|
|
self.csv_thread.stop() # call stop method of thread object, 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 bench is commanded
|
|
logger.log_datapoint() # log data
|
|
|
|
def reinitialize(self): # called on "Reinitialize devices" button press
|
|
# reinitialize all PSUs and the Arduino
|
|
g.CAGE_DEVICE.reconnect_hardware_async()
|
|
|
|
# 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 bench is commanded
|
|
logger.log_datapoint() # log data
|
|
|
|
def display_plot(self):
|
|
"""Generate and display a plot of the data loaded from a csv file"""
|
|
if not self.sequence_array_ok:
|
|
return
|
|
# calculate available height for plot (in pixels):
|
|
height_others = 0 # initialize variable to calculate height of other widgets
|
|
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
|
|
|
|
# calculate available plot height:
|
|
height = self.parent.winfo_height() - height_others - 50 # height of parent frame - other widgets - margin
|
|
width = min(self.parent.winfo_width() - 100, 1100) # set width to available space but max. 1100
|
|
|
|
# Create plot
|
|
figure = csv_threading.plot_field_sequence(self.sequence_array, width, height) # create figure to be displayed
|
|
# Clear previous plots first
|
|
try:
|
|
self.plot_canvas.get_tk_widget().destroy()
|
|
except:
|
|
pass
|
|
# Show new plot
|
|
self.plot_canvas = FigureCanvasTkAgg(figure, self.plot_frame) # create canvas to draw figure on
|
|
self.plot_canvas.draw() # equivalent to matplotlib.show()
|
|
self.plot_canvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in the UI
|
|
|
|
|
|
class CalibrateAmbientField(Frame):
|
|
def __init__(self, parent, controller):
|
|
Frame.__init__(self, parent)
|
|
self.parent = parent
|
|
self.controller = controller
|
|
|
|
# To center window
|
|
# self.columnconfigure(0, weight=1)
|
|
self.rowconfigure(0, weight=1)
|
|
self.left_column = Frame(self)
|
|
self.left_column.grid(row=0, column=0, sticky="nsew")
|
|
self.right_column = Frame(self)
|
|
self.right_column.grid(row=0, column=1, sticky="nsew")
|
|
self.left_column.rowconfigure(3, weight=1)
|
|
|
|
# Thread variables
|
|
self.calibration_ambient_thread = None
|
|
self.calibration_coil_constants_thread = None
|
|
self.view_mpi_queue = Queue() # Receives status information from calibration procedure threads.
|
|
|
|
# UI variables
|
|
self.connected_state_var = StringVar(value="Not connected")
|
|
self.field_value_vars = [StringVar(value="No data"),
|
|
StringVar(value="No data"),
|
|
StringVar(value="No data")]
|
|
self.calibration_procedure_progress_var = IntVar(value=0)
|
|
# Contains results for ambient field calibration
|
|
self.ambient_field_result_vars = [StringVar(), StringVar(), StringVar()]
|
|
self.ambient_field_ut_result_vars = [StringVar(), StringVar(), StringVar()]
|
|
self.ambient_field_residual_vars = [StringVar(), StringVar(), StringVar()]
|
|
self.ambient_field_raw_data = None # Used for export to csv
|
|
self.ambient_field_result = None # Used for saving to config file
|
|
self.ambient_field_clipboard = "" # Used for copying to clipboard
|
|
# Contains results for coil constant calibration
|
|
self.coil_constant_vars = [StringVar(), StringVar(), StringVar()]
|
|
self.coil_constant_dev_vars = [StringVar(), StringVar(), StringVar()]
|
|
self.coil_angle_vars = [StringVar(), StringVar(), StringVar()]
|
|
self.coil_constant_raw_data = None # Used for export to csv
|
|
self.coil_constant_results = None # Used for saving to config file
|
|
self.coil_constant_clipboard = "" # Used for copying to clipboard
|
|
|
|
row_counter = 0
|
|
|
|
# Create headline
|
|
header = Label(self.left_column, text="Ambient Field Calibration", font=HEADER_FONT)
|
|
header.grid(row=row_counter, column=0, columnspan=2, padx=100, pady=20, sticky="nw")
|
|
row_counter += 1
|
|
|
|
# Magnetometer connected indicator
|
|
connected_status_frame = Frame(self.left_column)
|
|
connected_status_frame.grid(row=row_counter, column=0, sticky="nw")
|
|
connected_label = Label(connected_status_frame, text="Magnetometer state:", font=SUB_HEADER_FONT)
|
|
connected_label.grid(row=0, column=0, padx=10, pady=20, sticky="nw")
|
|
self.connected_state_label = Label(connected_status_frame, textvariable=self.connected_state_var, fg="red")
|
|
self.connected_state_label.grid(row=0, column=1, padx=10, pady=20, sticky="nw")
|
|
row_counter += 1
|
|
|
|
# Magnetometer field data grid
|
|
field_data_frame = Frame(self.left_column)
|
|
field_data_frame.grid(row=row_counter, column=0, sticky="nw")
|
|
field_data_label = Label(field_data_frame, text="Field data:", font=SUB_HEADER_FONT)
|
|
field_data_label.grid(row=0, column=0, padx=10, pady=3, sticky="nw")
|
|
axis_labels = ['X:', 'Y:', 'Z:']
|
|
for i in range(3):
|
|
field_data_axis_label = Label(field_data_frame, text=axis_labels[i])
|
|
field_data_axis_label.grid(row=i, column=1, padx=10, pady=3)
|
|
|
|
field_data_axis_data = Label(field_data_frame, textvariable=self.field_value_vars[i])
|
|
field_data_axis_data.grid(row=i, column=2, padx=(20, 0), pady=3)
|
|
|
|
field_data_axis_units = Label(field_data_frame, text="\u03BCT")
|
|
field_data_axis_units.grid(row=i, column=3, padx=5, pady=3)
|
|
row_counter += 1
|
|
|
|
# Calibration start and save to csv buttons
|
|
start_button_frame = Frame(self.left_column)
|
|
start_button_frame.grid(row=row_counter, column=0, sticky="sw")
|
|
self.start_ambient_calibration_button = Button(start_button_frame, text="Calibrate Ambient Field",
|
|
command=self.calibration_procedure_ambient,
|
|
pady=5, padx=5, font=SMALL_BUTTON_FONT)
|
|
self.start_ambient_calibration_button.grid(row=0, column=0, padx=10, pady=10)
|
|
self.start_k_calibration_button = Button(start_button_frame, text="Calibrate Coil Constants",
|
|
command=self.calibration_procedure_coil_constants,
|
|
pady=5, padx=5, font=SMALL_BUTTON_FONT)
|
|
self.start_k_calibration_button.grid(row=0, column=1, padx=10, pady=10)
|
|
row_counter += 1
|
|
|
|
# Calibration progress bar
|
|
progress_bar_frame = Frame(self.left_column)
|
|
progress_bar_frame.grid(row=row_counter, column=0, sticky="swe")
|
|
calibration_procedure_progress_label = Label(progress_bar_frame, text="Progress:")
|
|
calibration_procedure_progress_label.grid(row=0, column=0, padx=10, pady=10)
|
|
calibration_procedure_progress = ttk.Progressbar(progress_bar_frame,
|
|
length=240,
|
|
variable=self.calibration_procedure_progress_var)
|
|
calibration_procedure_progress.grid(row=0, column=1, padx=10, pady=10, sticky="we")
|
|
row_counter += 1
|
|
|
|
# --- Ambient field calibration results ---
|
|
row_counter = 0
|
|
ambient_field_results_frame = LabelFrame(self.right_column, text="Ambient Field Results")
|
|
ambient_field_results_frame.grid(row=row_counter, column=1, padx=(100, 0), pady=5, sticky="nw")
|
|
for i, label in enumerate(['X', 'Y', 'Z']):
|
|
axis_label = Label(ambient_field_results_frame, text=label)
|
|
axis_label.grid(row=0, column=i + 1, padx=5, pady=(5, 0), sticky="nw")
|
|
# Ambient field value (A)
|
|
ambient_field_results_label = Label(ambient_field_results_frame, text="Ambient Field:")
|
|
ambient_field_results_label.grid(row=1, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(ambient_field_results_frame,
|
|
textvariable=self.ambient_field_result_vars[i],
|
|
width=15,
|
|
state='readonly')
|
|
axis_data.grid(row=1, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
ambient_field_results_unit = Label(ambient_field_results_frame, text="A")
|
|
ambient_field_results_unit.grid(row=1, column=4, padx=5, pady=5, sticky="nw")
|
|
# Ambient field value (microtesla)
|
|
ambient_field_results_ut_label = Label(ambient_field_results_frame, text="Ambient Field:")
|
|
ambient_field_results_ut_label.grid(row=2, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(ambient_field_results_frame,
|
|
textvariable=self.ambient_field_ut_result_vars[i],
|
|
width=15,
|
|
state='readonly')
|
|
axis_data.grid(row=2, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
ambient_field_results_ut_unit = Label(ambient_field_results_frame, text="\u03BCT")
|
|
ambient_field_results_ut_unit.grid(row=2, column=4, padx=5, pady=5, sticky="nw")
|
|
# Residuals
|
|
ambient_field_residual_label = Label(ambient_field_results_frame, text="Residual Field:")
|
|
ambient_field_residual_label.grid(row=3, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(ambient_field_results_frame,
|
|
textvariable=self.ambient_field_residual_vars[i],
|
|
width=15,
|
|
state='readonly')
|
|
axis_data.grid(row=3, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
ambient_field_residual_unit = Label(ambient_field_results_frame, text="\u03BCT")
|
|
ambient_field_residual_unit.grid(row=3, column=4, padx=5, pady=5, sticky="nw")
|
|
# Save calibration buttons
|
|
save_ambient_field_results_frame = Frame(ambient_field_results_frame)
|
|
save_ambient_field_results_frame.grid(row=4, column=0, columnspan=5)
|
|
# Save and apply
|
|
self.ambient_field_save_results_button = Button(save_ambient_field_results_frame, text="Save and apply",
|
|
command=self.save_and_apply_ambient_calibration,
|
|
state="disabled")
|
|
self.ambient_field_save_results_button.grid(row=0, column=0, padx=5, pady=5)
|
|
self.save_ambient_calibration_button = Button(save_ambient_field_results_frame, text="Export raw CSV",
|
|
command=self.save_to_csv_ambient_field,
|
|
state="disabled")
|
|
self.save_ambient_calibration_button.grid(row=0, column=1, padx=5, pady=5)
|
|
self.copy_ambient_calibration_button = Button(save_ambient_field_results_frame, text="Copy to clipboard",
|
|
command=self.copy_to_clipboard_ambient_field,
|
|
state="disabled")
|
|
self.copy_ambient_calibration_button.grid(row=0, column=2, padx=5, pady=5)
|
|
row_counter += 1
|
|
|
|
# --- Coil constant results ---
|
|
coil_constants_results_frame = LabelFrame(self.right_column, text="Coil Constants")
|
|
coil_constants_results_frame.grid(row=row_counter, column=1, padx=(100, 0), pady=5, sticky="nw")
|
|
for i, label in enumerate(['X', 'Y', 'Z']):
|
|
axis_label = Label(coil_constants_results_frame, text=label)
|
|
axis_label.grid(row=0, column=i + 1, padx=5, pady=(5, 0), sticky="nw")
|
|
# Coil constants
|
|
coil_constants_results_label = Label(coil_constants_results_frame, text="K:")
|
|
coil_constants_results_label.grid(row=1, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(coil_constants_results_frame,
|
|
textvariable=self.coil_constant_vars[i],
|
|
width=15,
|
|
state='readonly')
|
|
axis_data.grid(row=1, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
coil_constants_results_unit = Label(coil_constants_results_frame, text="\u03BCT/A")
|
|
coil_constants_results_unit.grid(row=1, column=4, padx=5, pady=5, sticky="nw")
|
|
# Standard deviation of coil constants
|
|
coil_constants_dev_results_label = Label(coil_constants_results_frame, text="K Std. Dev.:")
|
|
coil_constants_dev_results_label.grid(row=2, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(coil_constants_results_frame,
|
|
textvariable=self.coil_constant_dev_vars[i],
|
|
width=15,
|
|
state='readonly')
|
|
axis_data.grid(row=2, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
coil_constants_dev_results_unit = Label(coil_constants_results_frame, text="\u03BCT/A")
|
|
coil_constants_dev_results_unit.grid(row=2, column=4, padx=5, pady=5, sticky="nw")
|
|
# Inter-axis angle labels
|
|
for i, label in enumerate(['X-Y', 'Y-Z', 'X-Z']):
|
|
axis_label = Label(coil_constants_results_frame, text=label)
|
|
axis_label.grid(row=3, column=i + 1, padx=5, pady=0, sticky="nw")
|
|
# Inter-axis angles
|
|
coil_angles_label = Label(coil_constants_results_frame, text="Angles:")
|
|
coil_angles_label.grid(row=4, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(coil_constants_results_frame,
|
|
textvariable=self.coil_angle_vars[i],
|
|
width=15,
|
|
state='readonly')
|
|
axis_data.grid(row=4, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
coil_angles_unit = Label(coil_constants_results_frame, text="°")
|
|
coil_angles_unit.grid(row=4, column=4, padx=5, pady=5, sticky="nw")
|
|
# Save calibration buttons
|
|
save_coil_constant_results_frame = Frame(coil_constants_results_frame)
|
|
save_coil_constant_results_frame.grid(row=5, column=0, columnspan=5)
|
|
# Save and apply
|
|
self.coil_constant_save_results_button = Button(save_coil_constant_results_frame, text="Save and apply",
|
|
command=self.save_and_apply_coil_constants,
|
|
state="disabled")
|
|
self.coil_constant_save_results_button.grid(row=0, column=0, padx=5, pady=5)
|
|
self.save_k_calibration_button = Button(save_coil_constant_results_frame, text="Export raw CSV",
|
|
command=self.save_to_csv_coil_constants,
|
|
state="disabled")
|
|
self.save_k_calibration_button.grid(row=0, column=1, padx=5, pady=5)
|
|
self.copy_coil_constant_button = Button(save_coil_constant_results_frame, text="Copy to clipboard",
|
|
command=self.copy_to_clipboard_coil_constants,
|
|
state="disabled")
|
|
self.copy_coil_constant_button.grid(row=0, column=2, padx=5, pady=5)
|
|
row_counter += 1
|
|
|
|
# This starts an endless polling loop
|
|
self.update_view()
|
|
|
|
def page_switch(self):
|
|
# every class in the UI needs this, even if it doesn't do anything
|
|
pass
|
|
|
|
def update_view(self):
|
|
# Get new connected status
|
|
if g.MAGNETOMETER.connected:
|
|
self.connected_state_var.set("connected")
|
|
self.connected_state_label.configure(fg="green")
|
|
else:
|
|
self.connected_state_var.set("Not connected")
|
|
self.connected_state_label.configure(fg="red")
|
|
|
|
# Get new field data
|
|
new_field = g.MAGNETOMETER.field
|
|
for i in range(3):
|
|
# Display in uT
|
|
self.field_value_vars[i].set("{:.3f}".format(new_field[i] * 1e6))
|
|
|
|
# Get mpi messages from calibration procedures
|
|
try:
|
|
while True:
|
|
msg = self.view_mpi_queue.get(block=False)
|
|
cmd = msg['cmd']
|
|
arg = msg['arg']
|
|
if cmd == 'finished':
|
|
self.reactivate_buttons()
|
|
elif cmd == 'failed':
|
|
messagebox.showerror("Calibration error", "Error occured during calibration:\n{}".format(arg))
|
|
self.reactivate_buttons()
|
|
elif cmd == 'progress':
|
|
self.calibration_procedure_progress_var.set(min(int(arg * 100), 100))
|
|
elif cmd == 'ambient_data':
|
|
self.update_ambient_calibration_results(arg)
|
|
elif cmd == 'coil_constant_results':
|
|
self.update_coil_constant_results(arg)
|
|
else:
|
|
ui_print("Error: Unexpected mpi command '{}' in CalibrationTool".format(cmd))
|
|
except queue.Empty:
|
|
pass
|
|
|
|
self.controller.after(500, self.update_view)
|
|
|
|
def reactivate_buttons(self):
|
|
self.start_ambient_calibration_button.configure(text="Calibrate Ambient Field", state=NORMAL)
|
|
self.start_k_calibration_button.configure(text="Calibrate Coil Constants", state=NORMAL)
|
|
self.calibration_procedure_progress_var.set(0)
|
|
|
|
def deactivate_buttons(self):
|
|
self.start_ambient_calibration_button.configure(state=DISABLED)
|
|
self.start_k_calibration_button.configure(state=DISABLED)
|
|
|
|
def update_ambient_calibration_results(self, results):
|
|
# Update gui
|
|
for i in range(3):
|
|
self.ambient_field_result_vars[i].set("{:.3f}".format(results['ambient'][i]))
|
|
self.ambient_field_ut_result_vars[i].set("{:.3f}".format(results['ambient_ut'][i]))
|
|
self.ambient_field_residual_vars[i].set("{:.3f}".format(results['residual'][i] * 1e6))
|
|
|
|
# Populate clipboard string
|
|
self.ambient_field_clipboard = "\tX\tY\tZ\n"
|
|
self.ambient_field_clipboard += "Ambient Field [A]"
|
|
for i in range(3):
|
|
self.ambient_field_clipboard += "\t{:.3f}".format(results['ambient'][i])
|
|
self.ambient_field_clipboard += "\nAmbient Field [uT]"
|
|
for i in range(3):
|
|
self.ambient_field_clipboard += "\t{:.3f}".format(results['ambient_ut'][i])
|
|
self.ambient_field_clipboard += "\nResidual Field [uT]"
|
|
for i in range(3):
|
|
self.ambient_field_clipboard += "\t{:.3f}".format(results['residual'][i] * 1e6)
|
|
|
|
# Cache other data that the user may want to save
|
|
self.ambient_field_raw_data = results['raw_data']
|
|
self.ambient_field_result = results['ambient_ut'] * 1e-6
|
|
|
|
# Update save-button states
|
|
self.save_ambient_calibration_button.configure(state='normal')
|
|
self.ambient_field_save_results_button.configure(state='normal')
|
|
self.copy_ambient_calibration_button.configure(state='normal')
|
|
|
|
def update_coil_constant_results(self, results):
|
|
# Update UI
|
|
for i in range(3):
|
|
# Remember to convert from T/A to microT/A
|
|
self.coil_constant_vars[i].set("{:.3f}".format(results['k'][i] * 1e6))
|
|
self.coil_constant_dev_vars[i].set("{:.3f}".format(results['k_dev'][i] * 1e6))
|
|
# Coil angles use dict access
|
|
self.coil_angle_vars[0].set("{:.2f}".format(results['angle']['xy']))
|
|
self.coil_angle_vars[1].set("{:.2f}".format(results['angle']['yz']))
|
|
self.coil_angle_vars[2].set("{:.2f}".format(results['angle']['xz']))
|
|
|
|
# Populate clipboard string
|
|
self.coil_constant_clipboard = "\tX\tY\tZ\n"
|
|
self.coil_constant_clipboard += "K [uT/A]"
|
|
for i in range(3):
|
|
self.coil_constant_clipboard += "\t{:.3f}".format(results['k'][i] * 1e6)
|
|
self.coil_constant_clipboard += "\nK Std. Dev. [uT/A]"
|
|
for i in range(3):
|
|
self.coil_constant_clipboard += "\t{:.3f}".format(results['k_dev'][i] * 1e6)
|
|
self.coil_constant_clipboard += "\n\tX-Y\tY-Z\tX-Z"
|
|
self.coil_constant_clipboard += "\nAngles [deg]"
|
|
self.coil_constant_clipboard += "\t{:.3f}".format(results['angle']['xy'])
|
|
self.coil_constant_clipboard += "\t{:.3f}".format(results['angle']['yz'])
|
|
self.coil_constant_clipboard += "\t{:.3f}".format(results['angle']['xz'])
|
|
|
|
# Cache other data that the user may want to save
|
|
self.coil_constant_raw_data = results['raw_data']
|
|
self.coil_constant_results = results['k']
|
|
|
|
# Update save-button states
|
|
self.save_k_calibration_button.configure(state='normal')
|
|
self.coil_constant_save_results_button.configure(state='normal')
|
|
self.copy_coil_constant_button.configure(state='normal')
|
|
|
|
def calibration_procedure_ambient(self):
|
|
try:
|
|
self.calibration_ambient_thread = AmbientFieldCalibration(self.view_mpi_queue)
|
|
self.calibration_ambient_thread.start()
|
|
self.start_ambient_calibration_button.configure(text="Running")
|
|
self.deactivate_buttons()
|
|
except DeviceAccessError as e:
|
|
messagebox.showwarning("Calibration failed", "Failed to start calibration:\n{}".format(e))
|
|
|
|
def calibration_procedure_coil_constants(self):
|
|
try:
|
|
self.calibration_coil_constants_thread = CoilConstantCalibration(self.view_mpi_queue)
|
|
self.calibration_coil_constants_thread.start()
|
|
self.start_k_calibration_button.configure(text="Running")
|
|
self.deactivate_buttons()
|
|
except DeviceAccessError as e:
|
|
messagebox.showwarning("Calibration failed", "Failed to start calibration:\n{}".format(e))
|
|
|
|
def save_to_csv_coil_constants(self):
|
|
if self.coil_constant_raw_data is None:
|
|
ui_print("Error: Failed to export non-existent calibration data.")
|
|
return
|
|
|
|
save_dict_list_to_csv('coil_constant_calibration.csv', self.coil_constant_raw_data, query_path=True)
|
|
ui_print("Saved calibration results to coil_constant_calibration.csv.")
|
|
|
|
def save_to_csv_ambient_field(self):
|
|
if self.ambient_field_raw_data is None:
|
|
ui_print("Error: Failed to export non-existent calibration data.")
|
|
return
|
|
|
|
save_dict_list_to_csv('ambient_field_calibration.csv', self.ambient_field_raw_data, query_path=True)
|
|
ui_print("Saved calibration results to ambient_field_calibration.csv.")
|
|
|
|
def save_and_apply_ambient_calibration(self):
|
|
if self.ambient_field_result is not None:
|
|
ui_print("Saving ambient field calibration data")
|
|
config.edit_config("X-Axis", "ambient_field", self.ambient_field_result[0])
|
|
config.edit_config("Y-Axis", "ambient_field", self.ambient_field_result[1])
|
|
config.edit_config("Z-Axis", "ambient_field", self.ambient_field_result[2])
|
|
|
|
ui_print("Reinitializing devices...")
|
|
g.CAGE_DEVICE.reconnect_hardware_async() # setup everything with the defaults
|
|
|
|
def save_and_apply_coil_constants(self):
|
|
if self.coil_constant_results is not None:
|
|
ui_print("Saving coil constant calibration data")
|
|
config.edit_config("X-Axis", "coil_const", self.coil_constant_results[0])
|
|
config.edit_config("Y-Axis", "coil_const", self.coil_constant_results[1])
|
|
config.edit_config("Z-Axis", "coil_const", self.coil_constant_results[2])
|
|
|
|
ui_print("Reinitializing devices...")
|
|
g.CAGE_DEVICE.reconnect_hardware_async() # setup everything with the defaults
|
|
|
|
def copy_to_clipboard_ambient_field(self):
|
|
self.clipboard_clear()
|
|
self.clipboard_append(self.ambient_field_clipboard)
|
|
self.update()
|
|
|
|
def copy_to_clipboard_coil_constants(self):
|
|
self.clipboard_clear()
|
|
self.clipboard_append(self.coil_constant_clipboard)
|
|
self.update()
|
|
|
|
|
|
class CalibrateMagnetometer(Frame):
|
|
def __init__(self, parent, controller):
|
|
Frame.__init__(self, parent)
|
|
self.parent = parent
|
|
self.controller = controller
|
|
|
|
# To center window
|
|
# self.columnconfigure(0, weight=1)
|
|
self.rowconfigure(0, weight=1)
|
|
self.left_column = Frame(self)
|
|
self.left_column.grid(row=0, column=0, sticky="nsew")
|
|
self.right_column = Frame(self)
|
|
self.right_column.grid(row=0, column=1, sticky="nsew")
|
|
self.left_column.rowconfigure(3, weight=1)
|
|
|
|
# Thread variables
|
|
self.calibration_thread = None
|
|
self.view_mpi_queue = Queue() # Receives status information from calibration procedure threads.
|
|
|
|
# UI variables
|
|
self.connected_state_var = StringVar(value="Not connected")
|
|
self.field_value_vars = [StringVar(value="No data"),
|
|
StringVar(value="No data"),
|
|
StringVar(value="No data")]
|
|
self.calibration_procedure_progress_var = IntVar(value=0)
|
|
# Calibration parameters
|
|
self.calibration_points_var = IntVar(value=8)
|
|
self.calibration_interval_var = DoubleVar(value=5)
|
|
# Calibration results
|
|
self.sensitivity_result_vars = [StringVar(), StringVar(), StringVar()]
|
|
self.offset_result_vars = [StringVar(), StringVar(), StringVar()]
|
|
self.angle_to_plane_result_vars = [StringVar(), StringVar(), StringVar()]
|
|
self.angle_in_plane_result_vars = [StringVar(), StringVar(), StringVar()]
|
|
self.residual_result_vars = [StringVar(), StringVar(), StringVar()]
|
|
self.calibration_raw_results = None # Cached raw experiment data to allow for saving to csv.
|
|
self.clipboard = "" # Clipboard string containing results
|
|
self.mgm_to_helmholtz_cos_trans = [[DoubleVar(value=1), DoubleVar(value=0), DoubleVar(value=0)],
|
|
[DoubleVar(value=0), DoubleVar(value=1), DoubleVar(value=0)],
|
|
[DoubleVar(value=0), DoubleVar(value=0), DoubleVar(value=1)]]
|
|
|
|
# UI Elements
|
|
row_counter = 0
|
|
|
|
# Create headline
|
|
header = Label(self.left_column, text="Magnetometer Calibration", font=HEADER_FONT)
|
|
header.grid(row=row_counter, column=0, columnspan=2, padx=100, pady=20, sticky="nw")
|
|
row_counter += 1
|
|
|
|
# Magnetometer connected indicator
|
|
connected_status_frame = Frame(self.left_column)
|
|
connected_status_frame.grid(row=row_counter, column=0, sticky="nw")
|
|
connected_label = Label(connected_status_frame, text="Magnetometer state:", font=SUB_HEADER_FONT)
|
|
connected_label.grid(row=0, column=0, padx=10, pady=20, sticky="nw")
|
|
self.connected_state_label = Label(connected_status_frame, textvariable=self.connected_state_var, fg="red")
|
|
self.connected_state_label.grid(row=0, column=1, padx=10, pady=20, sticky="nw")
|
|
row_counter += 1
|
|
|
|
# Magnetometer field data grid
|
|
field_data_frame = Frame(self.left_column)
|
|
field_data_frame.grid(row=row_counter, column=0, sticky="nw")
|
|
field_data_label = Label(field_data_frame, text="Field data:", font=SUB_HEADER_FONT)
|
|
field_data_label.grid(row=0, column=0, padx=10, pady=3, sticky="nw")
|
|
axis_labels = ['X:', 'Y:', 'Z:']
|
|
for i in range(3):
|
|
field_data_axis_label = Label(field_data_frame, text=axis_labels[i])
|
|
field_data_axis_label.grid(row=i, column=1, padx=10, pady=3)
|
|
|
|
field_data_axis_data = Label(field_data_frame, textvariable=self.field_value_vars[i])
|
|
field_data_axis_data.grid(row=i, column=2, padx=(20, 0), pady=3)
|
|
|
|
field_data_axis_units = Label(field_data_frame, text="\u03BCT")
|
|
field_data_axis_units.grid(row=i, column=3, padx=5, pady=3)
|
|
row_counter += 1
|
|
|
|
# Centered controls
|
|
controls_frame = Frame(self.left_column)
|
|
controls_frame.grid(row=row_counter, column=0, sticky="sw")
|
|
# Number of calibration points
|
|
calibration_point_nr_label = Label(controls_frame, text="# of calibration points")
|
|
calibration_point_nr_label.grid(row=0, column=0, pady=5, sticky="w")
|
|
calibration_point_nr_entry = Entry(controls_frame, textvariable=self.calibration_points_var)
|
|
calibration_point_nr_entry.grid(row=0, column=1, pady=5, sticky="w")
|
|
# Measurement interval
|
|
calibration_point_nr_label = Label(controls_frame, text="Measurement interval [s]")
|
|
calibration_point_nr_label.grid(row=1, column=0, pady=5, sticky="w")
|
|
calibration_point_nr_entry = Entry(controls_frame, textvariable=self.calibration_interval_var)
|
|
calibration_point_nr_entry.grid(row=1, column=1, pady=5, sticky="w")
|
|
# Calibration start buttons
|
|
start_button_frame = Frame(controls_frame)
|
|
start_button_frame.grid(row=2, column=0, columnspan=2)
|
|
self.start_calibration_button = Button(start_button_frame, text="Start Calibration",
|
|
command=self.start_calibration_procedure,
|
|
pady=5, padx=5, font=SMALL_BUTTON_FONT)
|
|
self.start_calibration_button.grid(row=0, column=0, padx=10, pady=(30, 10))
|
|
# Calibration progress bar
|
|
progress_bar_frame = Frame(controls_frame)
|
|
progress_bar_frame.grid(row=3, column=0, columnspan=2)
|
|
calibration_procedure_progress_label = Label(progress_bar_frame, text="Progress:")
|
|
calibration_procedure_progress_label.grid(row=0, column=0, padx=10, pady=10)
|
|
calibration_procedure_progress = ttk.Progressbar(progress_bar_frame,
|
|
length=240,
|
|
variable=self.calibration_procedure_progress_var)
|
|
calibration_procedure_progress.grid(row=0, column=1, padx=10, pady=10, sticky="we")
|
|
row_counter += 1
|
|
|
|
# RIGHT COLUMN
|
|
# Magnetometer calibration results
|
|
row_counter = 0
|
|
calibration_results_frame = LabelFrame(self.right_column, text="Magnetometer Results")
|
|
calibration_results_frame.grid(row=row_counter, column=1, padx=(100, 0), pady=20, sticky="nw")
|
|
for i, label in enumerate(['X', 'Y', 'Z']):
|
|
axis_label = Label(calibration_results_frame, text=label)
|
|
axis_label.grid(row=0, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
# Axis sensitivities
|
|
sensitivity_results_label = Label(calibration_results_frame, text="Sensitivity:")
|
|
sensitivity_results_label.grid(row=1, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(calibration_results_frame,
|
|
textvariable=self.sensitivity_result_vars[i],
|
|
width=15,
|
|
state='readonly')
|
|
axis_data.grid(row=1, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
sensitivity_results_unit = Label(calibration_results_frame, text="-")
|
|
sensitivity_results_unit.grid(row=1, column=4, padx=5, pady=5, sticky="nw")
|
|
# Axis offsets
|
|
offset_results_label = Label(calibration_results_frame, text="Offset:")
|
|
offset_results_label.grid(row=2, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(calibration_results_frame,
|
|
textvariable=self.offset_result_vars[i],
|
|
width=15,
|
|
state='readonly')
|
|
axis_data.grid(row=2, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
offset_results_unit = Label(calibration_results_frame, text="\u03BCT")
|
|
offset_results_unit.grid(row=2, column=4, padx=5, pady=5, sticky="nw")
|
|
# Angle to XY coil plane
|
|
angle_to_plane_label = Label(calibration_results_frame, text="Angle to XY plane:")
|
|
angle_to_plane_label.grid(row=3, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(calibration_results_frame,
|
|
textvariable=self.angle_to_plane_result_vars[i],
|
|
width=15,
|
|
state='readonly')
|
|
axis_data.grid(row=3, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
angle_to_plane_unit = Label(calibration_results_frame, text="°")
|
|
angle_to_plane_unit.grid(row=3, column=4, padx=5, pady=5, sticky="nw")
|
|
# Angle in XY coil plane
|
|
angle_in_plane_label = Label(calibration_results_frame, text="Angle in XY plane:")
|
|
angle_in_plane_label.grid(row=4, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(calibration_results_frame,
|
|
textvariable=self.angle_in_plane_result_vars[i],
|
|
width=15,
|
|
state='readonly')
|
|
axis_data.grid(row=4, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
angle_in_plane_unit = Label(calibration_results_frame, text="°")
|
|
angle_in_plane_unit.grid(row=4, column=4, padx=5, pady=5, sticky="nw")
|
|
# Residual in system of equations
|
|
residual_label = Label(calibration_results_frame, text="Residual:")
|
|
residual_label.grid(row=5, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(calibration_results_frame,
|
|
textvariable=self.residual_result_vars[i],
|
|
width=15,
|
|
state='readonly')
|
|
axis_data.grid(row=5, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
residual_unit = Label(calibration_results_frame, text="\u03BCT")
|
|
residual_unit.grid(row=5, column=4, padx=5, pady=5, sticky="nw")
|
|
# Save calibration buttons
|
|
save_calibration_results_frame = Frame(calibration_results_frame)
|
|
save_calibration_results_frame.grid(row=6, column=0, columnspan=5)
|
|
# Save and apply
|
|
self.export_calibration_button = Button(save_calibration_results_frame, text="Export raw to CSV",
|
|
command=self.export_csv,
|
|
state="disabled",
|
|
pady=5, padx=5)
|
|
self.export_calibration_button.grid(row=0, column=0, padx=5, pady=5)
|
|
self.copy_calibration_button = Button(save_calibration_results_frame, text="Copy to clipboard",
|
|
command=self.copy_to_clipboard,
|
|
state="disabled",
|
|
pady=5, padx=5)
|
|
self.copy_calibration_button.grid(row=0, column=1, padx=5, pady=5)
|
|
row_counter += 1
|
|
|
|
# RIGHT Bottom COLUMN
|
|
# Input coordinate system conversion matrix
|
|
row_counter = 0
|
|
input_cos_frame = LabelFrame(self.right_column, text="Input MGM to Helmholtz COS Transformation Matrix")
|
|
input_cos_frame.grid(row=row_counter, column=2, padx=(100, 0), pady=20, sticky="nw")
|
|
for i, label in enumerate(['X', 'Y', 'Z']):
|
|
axis_label = Label(input_cos_frame, text=label)
|
|
axis_label.grid(row=0, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
# Axis sensitivities
|
|
sensitivity_results_label = Label(input_cos_frame, text="X")
|
|
sensitivity_results_label.grid(row=1, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
axis_data = Entry(input_cos_frame,
|
|
textvariable=self.mgm_to_helmholtz_cos_trans[0][i],
|
|
width=15)
|
|
axis_data.grid(row=1, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
sensitivity_results_unit = Label(input_cos_frame, text="-")
|
|
sensitivity_results_unit.grid(row=1, column=4, padx=5, pady=5, sticky="nw")
|
|
# Axis offsets
|
|
offset_results_label = Label(input_cos_frame, text="Y")
|
|
offset_results_label.grid(row=2, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
if i == 0:
|
|
axis_data = Entry(input_cos_frame,
|
|
textvariable=self.mgm_to_helmholtz_cos_trans[0][1],
|
|
width=15,
|
|
state='readonly')
|
|
self.mgm_to_helmholtz_cos_trans[1][0] = self.mgm_to_helmholtz_cos_trans[0][1]
|
|
if i == 1:
|
|
axis_data = Entry(input_cos_frame,
|
|
textvariable=self.mgm_to_helmholtz_cos_trans[1][i],
|
|
width=15)
|
|
if i == 2:
|
|
axis_data = Entry(input_cos_frame,
|
|
textvariable=self.mgm_to_helmholtz_cos_trans[1][i],
|
|
width=15)
|
|
axis_data.grid(row=2, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
offset_results_unit = Label(input_cos_frame, text="-")
|
|
offset_results_unit.grid(row=2, column=4, padx=5, pady=5, sticky="nw")
|
|
# Angle to XY coil plane
|
|
angle_to_plane_label = Label(input_cos_frame, text="Z")
|
|
angle_to_plane_label.grid(row=3, column=0, padx=5, pady=5, sticky="nw")
|
|
for i in range(3):
|
|
if i == 0:
|
|
axis_data = Entry(input_cos_frame,
|
|
textvariable=self.mgm_to_helmholtz_cos_trans[0][2],
|
|
width=15,
|
|
state='readonly')
|
|
self.mgm_to_helmholtz_cos_trans[2][0] = self.mgm_to_helmholtz_cos_trans[0][2]
|
|
if i == 1:
|
|
axis_data = Entry(input_cos_frame,
|
|
textvariable=self.mgm_to_helmholtz_cos_trans[1][2],
|
|
width=15,
|
|
state='readonly')
|
|
self.mgm_to_helmholtz_cos_trans[2][1] = self.mgm_to_helmholtz_cos_trans[1][2]
|
|
if i == 2:
|
|
axis_data = Entry(input_cos_frame,
|
|
textvariable=self.mgm_to_helmholtz_cos_trans[2][i],
|
|
width=15)
|
|
axis_data.grid(row=3, column=i + 1, padx=5, pady=5, sticky="nw")
|
|
angle_to_plane_unit = Label(input_cos_frame, text="-")
|
|
angle_to_plane_unit.grid(row=3, column=4, padx=5, pady=5, sticky="nw")
|
|
# Note on input
|
|
label = "Note: Input orthogonal, normalized transformation Matrix."
|
|
axis_note = Label(input_cos_frame, text=label)
|
|
axis_note.grid(row=4, column=0, padx=5, pady=5, columnspan=5, sticky="nw")
|
|
# Save calibration buttons
|
|
save_input_cos_frame = Frame(input_cos_frame)
|
|
save_input_cos_frame.grid(row=6, column=0, columnspan=5)
|
|
# Save and apply
|
|
self.export_cos_trans_button = Button(save_input_cos_frame, text="Export raw to CSV",
|
|
command=self.export_csv,
|
|
state="normal",
|
|
pady=5, padx=5)
|
|
self.export_cos_trans_button.grid(row=0, column=0, padx=5, pady=5)
|
|
self.copy_cos_trans_matrix_button = Button(save_input_cos_frame, text="Copy to clipboard",
|
|
command=self.copy_to_clipboard,
|
|
state="normal",
|
|
pady=5, padx=5)
|
|
self.copy_cos_trans_matrix_button.grid(row=0, column=1, padx=5, pady=5)
|
|
self.normalize_matrix_button = Button(save_input_cos_frame, text="Normalize matrix",
|
|
command=self.matrix_normalize,
|
|
state="normal",
|
|
pady=5, padx=5)
|
|
self.normalize_matrix_button.grid(row=0, column=2, padx=5, pady=5)
|
|
row_counter += 1
|
|
|
|
# This starts an endless polling loop
|
|
self.update_view()
|
|
|
|
def page_switch(self):
|
|
# every class in the UI needs this, even if it doesn't do anything
|
|
pass
|
|
|
|
def update_view(self):
|
|
# Get new connected status
|
|
if g.MAGNETOMETER.connected:
|
|
self.connected_state_var.set("connected")
|
|
self.connected_state_label.configure(fg="green")
|
|
else:
|
|
self.connected_state_var.set("Not connected")
|
|
self.connected_state_label.configure(fg="red")
|
|
|
|
# Get new field data
|
|
new_field = g.MAGNETOMETER.field
|
|
for i in range(3):
|
|
# Display in uT
|
|
self.field_value_vars[i].set("{:.3f}".format(new_field[i] * 1e6))
|
|
|
|
# Get mpi messages from calibration procedures
|
|
try:
|
|
while True:
|
|
msg = self.view_mpi_queue.get(block=False)
|
|
cmd = msg['cmd']
|
|
arg = msg['arg']
|
|
if cmd == 'finished':
|
|
self.reactivate_buttons()
|
|
elif cmd == 'failed':
|
|
messagebox.showerror("Calibration error", "Error occured during calibration:\n{}".format(arg))
|
|
self.reactivate_buttons()
|
|
elif cmd == 'progress':
|
|
self.calibration_procedure_progress_var.set(min(int(arg * 100), 100))
|
|
elif cmd == 'calibration_data':
|
|
self.display_calibration_results(arg)
|
|
else:
|
|
ui_print("Error: Unexpected mpi command '{}' in CalibrationTool".format(cmd))
|
|
except queue.Empty:
|
|
pass
|
|
|
|
self.controller.after(500, self.update_view)
|
|
|
|
def reactivate_buttons(self):
|
|
self.start_calibration_button.configure(text="Start Calibration", state=NORMAL)
|
|
self.calibration_procedure_progress_var.set(0)
|
|
|
|
def deactivate_buttons(self):
|
|
self.start_calibration_button.configure(text="Running...", state=DISABLED)
|
|
|
|
def display_calibration_results(self, results):
|
|
# Cache raw experiment data for saving later
|
|
self.calibration_raw_results = results['raw_data']
|
|
|
|
# Unpack the dict
|
|
results = results['results']
|
|
|
|
# Display calibration in GUI
|
|
for i in range(3):
|
|
self.sensitivity_result_vars[i].set("{:.3f}".format(results[i]['sensitivity']))
|
|
self.offset_result_vars[i].set("{:.3f}".format(results[i]['offset'] * 1e6))
|
|
self.angle_to_plane_result_vars[i].set("{:.3f}".format(results[i]['alpha'] * 180 / pi))
|
|
self.angle_in_plane_result_vars[i].set("{:.3f}".format(results[i]['beta'] * 180 / pi))
|
|
self.residual_result_vars[i].set("{:.3e}".format(results[i]['residual'] * 1e6))
|
|
|
|
# Populate clipboard string
|
|
self.clipboard = "\tX\tY\tZ\n"
|
|
self.clipboard += "Sensitivity [-]"
|
|
for i in range(3):
|
|
self.clipboard += "\t{:.3f}".format(results[i]['sensitivity'])
|
|
self.clipboard += "\nOffset [uT]"
|
|
for i in range(3):
|
|
self.clipboard += "\t{:.3f}".format(results[i]['offset'] * 1e6)
|
|
self.clipboard += "\nAngle to XY Plane [deg]"
|
|
for i in range(3):
|
|
self.clipboard += "\t{:.3f}".format(results[i]['alpha'] * 180 / pi)
|
|
self.clipboard += "\nAngle in XY Plane [deg]"
|
|
for i in range(3):
|
|
self.clipboard += "\t{:.3f}".format(results[i]['beta'] * 180 / pi)
|
|
self.clipboard += "\nResidual [uT]"
|
|
for i in range(3):
|
|
self.clipboard += "\t{:.3e}".format(results[i]['residual'] * 1e6)
|
|
|
|
# Enable save buttons
|
|
self.export_calibration_button.configure(state="normal")
|
|
self.copy_calibration_button.configure(state="normal")
|
|
self.export_mgm_button.configure(state="normal")
|
|
|
|
def start_calibration_procedure(self):
|
|
try:
|
|
calibration_points = self.calibration_points_var.get()
|
|
calibration_interval = self.calibration_interval_var.get()
|
|
self.calibration_thread = MagnetometerCalibration(self.view_mpi_queue,
|
|
calibration_points,
|
|
calibration_interval)
|
|
self.calibration_thread.start()
|
|
self.deactivate_buttons()
|
|
except (DeviceAccessError, TclError) as e:
|
|
messagebox.showwarning("Calibration failed", "Failed to start calibration:\n{}".format(e))
|
|
|
|
def export_csv(self):
|
|
if self.calibration_raw_results is None:
|
|
ui_print("Error: Failed to export non-existent calibration data.")
|
|
return
|
|
|
|
save_dict_list_to_csv('magnetometer_calibration.csv', self.calibration_raw_results, query_path=True)
|
|
ui_print("Saved calibration results to magnetometer_calibration.csv.")
|
|
|
|
def copy_to_clipboard(self):
|
|
self.clipboard_clear()
|
|
self.clipboard_append(self.clipboard)
|
|
self.update()
|
|
|
|
def matrix_normalize(self):
|
|
try:
|
|
ui_print("Input matrix to be normalized:")
|
|
# Normalize Matrix
|
|
matrix = [[self.mgm_to_helmholtz_cos_trans[0][0].get(), self.mgm_to_helmholtz_cos_trans[0][1].get(),
|
|
self.mgm_to_helmholtz_cos_trans[0][2].get()],
|
|
[self.mgm_to_helmholtz_cos_trans[1][0].get(), self.mgm_to_helmholtz_cos_trans[1][1].get(),
|
|
self.mgm_to_helmholtz_cos_trans[1][2].get()],
|
|
[self.mgm_to_helmholtz_cos_trans[2][0].get(), self.mgm_to_helmholtz_cos_trans[2][1].get(),
|
|
self.mgm_to_helmholtz_cos_trans[2][2].get()]]
|
|
matrix = np.array(matrix)
|
|
ui_print(matrix)
|
|
matrix_max = matrix.max()
|
|
matrix_min = matrix.min()
|
|
matrix = (matrix - matrix_min) / (matrix_max - matrix_min)
|
|
ui_print("Normalized matrix:")
|
|
ui_print(matrix)
|
|
for i in range(3):
|
|
for j in range(3):
|
|
self.mgm_to_helmholtz_cos_trans[i][j].set(matrix[i][j])
|
|
except:
|
|
# Couldn't compute matrix -> use unity matrix
|
|
ui_print("Could not normalize matrix, reverted to unity matrix!")
|
|
self.mgm_to_helmholtz_cos_trans = [[DoubleVar(value=1), DoubleVar(value=0), DoubleVar(value=0)],
|
|
[DoubleVar(value=0), DoubleVar(value=1), DoubleVar(value=0)],
|
|
[DoubleVar(value=0), DoubleVar(value=0), DoubleVar(value=1)]]
|
|
|
|
|
|
class HardwareConfiguration(Frame):
|
|
"""Settings window to set program constants"""
|
|
|
|
# noinspection PyUnusedLocal
|
|
def __init__(self, parent, controller):
|
|
Frame.__init__(self, parent)
|
|
|
|
self.grid_rowconfigure(ALL, weight=1)
|
|
self.grid_columnconfigure(ALL, weight=1)
|
|
|
|
row_counter = 0
|
|
|
|
# create headline:
|
|
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
|
|
# button to load a config file:
|
|
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)
|
|
# button to save the current settings to the currently selected file
|
|
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)
|
|
# button to save current settings to a file selected by the user
|
|
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
|
|
|
|
# Setup entry fields for the PSU serial ports:
|
|
# setup 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)
|
|
|
|
# text for the description labels:
|
|
entry_texts = ["Arduino Serial Port:", "XY PSU Serial Port:", "Z PSU Serial Port:"]
|
|
# create variables to store the port names and set to current names
|
|
self.arduino_port = StringVar(value=g.CAGE_DEVICE.com_port_arduino)
|
|
self.XY_port = StringVar(value=g.CAGE_DEVICE.com_port_psu1)
|
|
self.Z_port = StringVar(value=g.CAGE_DEVICE.com_port_psu2)
|
|
port_vars = [self.arduino_port, self.XY_port, self.Z_port] # list to store both port variables
|
|
row = 0
|
|
for text in entry_texts: # do this for both ports
|
|
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) # create description label
|
|
axis_label.grid(row=row, column=0, padx=5, pady=5, sticky=W)
|
|
info_label = Label(port_frame, text="e.g. COM10") # add label with example of right format
|
|
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
|
|
|
|
# setup main entry field for operational constants
|
|
# setup frame:
|
|
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", "Field generated per applied current",
|
|
"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 = {} # setup dictionary with all entry fields
|
|
|
|
# Fill in header (axis names):
|
|
col = 1
|
|
for text in g.AXIS_NAMES:
|
|
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(): # go through the different values to be set
|
|
self.fields[key] = [] # initialize empty list for fields in the entry field dictionary
|
|
for axis in range(3): # go through all three axes
|
|
field = Entry(value_frame, textvariable=self.entries[key][0][axis], width=10) # setup entry field
|
|
field.grid(row=row, column=axis + 1, sticky=W, padx=2)
|
|
self.fields[key].append(field) # safe access to field to entry field dictionary
|
|
axis_label = Label(value_frame, text=key, padx=5, pady=5) # add label with variable name
|
|
axis_label.grid(row=row, column=0, sticky=W)
|
|
unit_label = Label(value_frame, text=self.entries[key][1]) # add label with unit
|
|
unit_label.grid(row=row, column=4, sticky=W)
|
|
description_label = Label(value_frame, text=self.entries[key][2]) # add label with description
|
|
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=3).grid(row=row_counter, column=0) # add spacer
|
|
row_counter += 1
|
|
|
|
# Setup buttons to implement and restore defaults
|
|
# 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
|
|
# button to read the values from all fields, update the config and reinitialize the test bench
|
|
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)
|
|
# button to restore default settings
|
|
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=3).grid(row=row_counter, column=0) # add spacer
|
|
|
|
def page_switch(self): # function that is called when switching to this window
|
|
self.update_fields() # update values in the entry fields from config
|
|
|
|
def restore_defaults(self): # restore default settings
|
|
config.reset_config_to_default() # overwrite config file with default
|
|
ui_print("\nReinitializing devices...")
|
|
g.CAGE_DEVICE.reconnect_hardware_async() # 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
|
|
# set values for PSU serial ports:
|
|
self.XY_port.set(g.CAGE_DEVICE.com_port_psu1)
|
|
self.Z_port.set(g.CAGE_DEVICE.com_port_psu2)
|
|
self.arduino_port.set(g.CAGE_DEVICE.com_port_arduino)
|
|
|
|
for key in self.entries.keys(): # go through the main value table
|
|
for i in range(3): # go through all three axes
|
|
# get value from config file:
|
|
value = config.read_from_config(g.AXIS_NAMES[i], self.entries[key][3], config.CONFIG_OBJECT)
|
|
self.entries[key][0][i].set(value) # set initial value on the entry field variable
|
|
type_value = self.entries[key][0][i].get() # get value with correct type by reading out field variable
|
|
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 value is within safe limits:
|
|
value_check = helmholtz_cage_device.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 object with user inputs into entry fields and reinitialize devices
|
|
|
|
# set serial ports for PSUs:
|
|
config.edit_config("Supplies", "xy_port", self.XY_port.get())
|
|
config.edit_config("Supplies", "z_port", self.Z_port.get())
|
|
config.edit_config("Supplies", "arduino_port", self.arduino_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)) # print error message
|
|
# field will go back to the last valid value
|
|
|
|
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] # get handle by which value is indexed in config file
|
|
value_ok = helmholtz_cage_device.value_in_limits(g.AXIS_NAMES[i], config_key,
|
|
value) # perform value check
|
|
|
|
if value_ok == 'OK': # value is within safe limits
|
|
config.edit_config(g.AXIS_NAMES[i], config_key, value) # write new value to config file
|
|
else: # value is not within limits
|
|
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 == 'HIGH': # value is too high
|
|
max_value = g.default_arrays[config_key][1][i] # get max value
|
|
# assemble error message:
|
|
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': # value is too low
|
|
min_value = g.default_arrays[config_key][2][i] # get min value
|
|
# assemble error message:
|
|
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: # this should be impossible
|
|
message = "Unknown case for value limits check, 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)
|
|
# answer 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): # "Update and Reinitialize" button, update config with new values and reinitialize devices
|
|
self.write_values() # write current values from entry fields to config object
|
|
ui_print("\nReinitializing devices...")
|
|
g.CAGE_DEVICE.reconnect_hardware_async() # 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 at current path and save path of selected file
|
|
filename = filedialog.askopenfilename(initialdir=directory, title="Select Config File",
|
|
filetypes=(("Config File", "*.ini*"), ("All Files", "*.*")))
|
|
if filename == () or filename == '':
|
|
# this happens when file selection window is closed without selecting a file
|
|
ui_print("No file selected, could not load config.")
|
|
elif os.path.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 and display warnings if values are out of bounds
|
|
|
|
ui_print("\nReinitializing devices...")
|
|
g.CAGE_DEVICE.reconnect_hardware_async() # reinitialize devices and program with new values
|
|
self.update_fields() # update entry fields to show new values
|
|
else: # file does not exist
|
|
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
|
|
ui_print("\nReinitializing devices...")
|
|
g.CAGE_DEVICE.reconnect_hardware_async() # 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() # write current entry field values to the config object
|
|
config.write_config_to_file(config.CONFIG_OBJECT) # write contents of config object to file
|
|
ui_print("\nReinitializing devices...")
|
|
g.CAGE_DEVICE.reconnect_hardware_async() # reinitialize devices and program with new values
|
|
self.update_fields() # update entry fields to show values as they are in the config
|
|
|
|
|
|
class ConfigureLogging(Frame):
|
|
# generate window to configure data logging to csv
|
|
|
|
def __init__(self, parent, controller):
|
|
Frame.__init__(self, parent)
|
|
self.parent = parent # parent object, here tha mainArea frame
|
|
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 bench
|
|
# log_datapoint() has to be called wherever a command is sent to the test bench and data should be logged
|
|
# it does not happen automatically whenever something is sent to the test bench
|
|
# 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 headline
|
|
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)
|
|
|
|
# button to start data logging
|
|
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)
|
|
# button to stop data logging
|
|
self.stop_logging_button = Button(self.top_buttons_frame, text="Stop Logging", command=self.stop_logging,
|
|
pady=5, padx=5, font=SMALL_BUTTON_FONT, state="disabled")
|
|
self.stop_logging_button.grid(row=0, column=1, padx=5)
|
|
# button to write log data to a file
|
|
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=2, padx=5)
|
|
# button to clear all logged data
|
|
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=3, padx=5)
|
|
|
|
row_counter += 1
|
|
|
|
# Create label showing how many datapoints have been logged
|
|
# setup frame:
|
|
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 display 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 when and how often data should be logged
|
|
# setup frame:
|
|
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)
|
|
|
|
# create checkbox variable for logging in regular time intervals
|
|
self.regular_logging_var = BooleanVar(value=True)
|
|
# create checkbox variable for logging whenever test bench is commanded
|
|
self.event_logging_var = BooleanVar(value=True)
|
|
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 bench 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
|
|
# setup frame:
|
|
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 lock/unlock all of them
|
|
self.active_keys = [] # list with all keys of the currently ticked checkboxes
|
|
|
|
# add general description headline:
|
|
checkbox_label = Label(self.checkbox_frame, text="Select which data to log:")
|
|
checkbox_label.grid(row=0, column=0, columnspan=2, sticky='w')
|
|
# generate and place all checkboxes:
|
|
row = 0
|
|
column = 0
|
|
for key, name in log.logging_selection_options.items(): # go through all loggable values
|
|
self.checkbox_vars[key] = BooleanVar(value=True) # create variable for checkbox and put it in dictionary
|
|
checkbox = Checkbutton(self.checkbox_frame, text=name, # generate checkbox
|
|
variable=self.checkbox_vars[key], onvalue=True, offvalue=False)
|
|
checkbox.grid(row=row + 1, column=column, padx=(0, 20), sticky=W) # place checkbox in UI
|
|
self.checkboxes.append(checkbox) # add created checkbox to list of all checkboxes
|
|
row += 1
|
|
if row > 8:
|
|
row = 0
|
|
column += 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): # start logging data (called by button)
|
|
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
|
|
|
|
# get settings for regular logging:
|
|
error = False # initialize variable to store if an error occurred
|
|
if self.regular_logging: # regular logging checkbox is ticked
|
|
try: # try to get log interval from entry field
|
|
interval_ms = int(self.log_interval.get() * 1000) # get value and convert to ms
|
|
except TclError as e: # invalid entry for log interval
|
|
# show error pop-up:
|
|
messagebox.showwarning("Wrong entry format!", "Invalid entry for log interval:\n%s" % e)
|
|
self.event_logging = False # don't start event logging if there is a problem
|
|
error = True # save that an error was encountered
|
|
else: # no problems while reading out the log interval
|
|
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.stop_logging_button["state"] = "normal"
|
|
self.start_logging_button["state"] = "disabled"
|
|
self.write_to_file_button["state"] = "disabled"
|
|
self.clear_data_button["state"] = "normal"
|
|
self.lock_checkboxes()
|
|
|
|
def stop_logging(self): # stop the data logging, called by "Stop Logging" button
|
|
ui_print("Stopped data logging. Remember to save data to file!")
|
|
self.regular_logging = False # tell everything its time to stop periodic logging
|
|
self.event_logging = False # tell everything its time to stop logging on test bench commands
|
|
self.write_to_file_button["state"] = "normal" # enable write to file button
|
|
self.stop_logging_button["state"] = "disabled" # disable stop logging button
|
|
self.start_logging_button["state"] = "normal" # enable start logging button
|
|
self.unlock_checkboxes() # enable checkboxes
|
|
|
|
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: # a valid filename was selected
|
|
log.write_to_file(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 or cancel clearing it:
|
|
save_log = messagebox.askyesnocancel("Save log data?", "There seems to be unsaved logging data. "
|
|
"Do you wish to write it to a file before deleting?")
|
|
if save_log: # user has chosen yes
|
|
self.write_to_file() # run write to file function to save data
|
|
if save_log is not None: # user has chosen yes or no (not cancel)
|
|
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.")
|
|
else: # user has chosen to cancel
|
|
ui_print("Log data not cleared.")
|
|
else: # there is no unsaved data
|
|
log.clear_logged_data() # delete the logged data
|
|
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 dict
|
|
for key in self.checkbox_vars.keys(): # go through all checkboxes
|
|
self.active_keys[key] = self.checkbox_vars[key].get() # add corresponding item to the list
|
|
|
|
def lock_checkboxes(self): # lock all checkboxes, so they can not be modified while logging
|
|
# lock all checkboxes:
|
|
for checkbox in [*self.checkboxes, self.event_logging_checkbox, self.regular_logging_checkbox]:
|
|
checkbox.config(state=DISABLED)
|
|
self.interval_entry.config(state=DISABLED) # lock logging interval entry field
|
|
|
|
def unlock_checkboxes(self): # opposite of lock checkboxes
|
|
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 still active
|
|
self.log_datapoint() # add a datapoint to the log data frame
|
|
self.controller.after(interval, lambda: self.periodic_log(interval)) # call same function again after time
|
|
|
|
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: # some error occurred
|
|
messagebox.showerror("Error!", "Error while logging data: \n%s" % e)
|
|
|
|
def update_datapoint_count(self): # update label with how many datapoints have been logged
|
|
if self.regular_logging or self.event_logging: # logging is still 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):
|
|
# status display to show information on test bench status in real time
|
|
|
|
# noinspection PyUnusedLocal
|
|
def __init__(self, parent, controller):
|
|
Frame.__init__(self, parent, relief=SUNKEN, bd=1)
|
|
self.controller = controller
|
|
|
|
# Queue to store status updates which arrive from another thread by callback
|
|
self.update_label_queue = Queue()
|
|
|
|
# configure Tkinter grid
|
|
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
|
|
|
|
# create column headers (X-Axis etc.)
|
|
col = 0
|
|
for header in ["", "X-Axis", "Y-Axis", "Z-Axis"]: # define Column headers
|
|
# create label:
|
|
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 += 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)]
|
|
|
|
# create dictionary to associate (changing) label variables with their labels:
|
|
self.label_dict = {} # initialize dictionary
|
|
for name in TextLabels: # go through all rows
|
|
self.label_dict[name] = [StringVar() for _ in range(self.columnNo - 1)] # create variables for labels
|
|
# add static 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): # go through columns
|
|
# add changeable labels for values:
|
|
self.Labels[col + 1].append(Label(self, textvariable=self.label_dict[name][col],
|
|
borderwidth=1, relief="flat", anchor="w", padx=x_pad))
|
|
|
|
# place all labels in grid layout
|
|
col = 0
|
|
for LabelCol in self.Labels: # go through table columns
|
|
for row in range(self.rowNo): # go through table rows
|
|
LabelCol[row].grid(row=row + rowCounter, column=col, sticky="nsew") # place label
|
|
col += 1
|
|
|
|
# Register callback to populate new data:
|
|
g.CAGE_DEVICE.subscribe_status_updates(self.enqueue_new_status)
|
|
# Starts polling loop for status display
|
|
self.update_label_poll_method()
|
|
|
|
def update_labels(self, status): # update all values in the status display
|
|
for i in range(3): # go through all three axes
|
|
# update all label variables with the new values:
|
|
axis = status['axes'][i]
|
|
if axis['connected']:
|
|
# Deal with variables that are dependent on the current hardware state
|
|
active = "True" if axis['active'] else "False"
|
|
remote_active = "True" if axis['active'] else "False"
|
|
voltage_setpoint = "%0.3f V" % axis['voltage_setpoint']
|
|
voltage = "%0.3f V" % axis['voltage']
|
|
current_setpoint = "%0.3f A" % axis['current_setpoint']
|
|
current = "%0.3f A" % axis['current']
|
|
polarity = axis['polarity']
|
|
else:
|
|
active = "N/A"
|
|
remote_active = "N/A"
|
|
voltage_setpoint = "N/A"
|
|
voltage = "N/A"
|
|
current_setpoint = "N/A"
|
|
current = "N/A"
|
|
polarity = "N/A"
|
|
psu_connected = "Connected" if axis['connected'] else "Not Connected"
|
|
arduino_connected = "Connected" if status['arduino_connected'] else "Not Connected"
|
|
|
|
self.label_dict["PSU Serial Port:"][i].set(axis['port'])
|
|
self.label_dict["PSU Channel:"][i].set(axis['channel'])
|
|
self.label_dict["PSU Status:"][i].set(psu_connected)
|
|
self.label_dict["Arduino Status:"][i].set(arduino_connected) # ToDo (optional): make this multicolumn
|
|
self.label_dict["Output:"][i].set(active)
|
|
self.label_dict["Remote Control:"][i].set(remote_active)
|
|
self.label_dict["Voltage Setpoint:"][i].set(voltage_setpoint)
|
|
self.label_dict["Actual Voltage:"][i].set(voltage)
|
|
self.label_dict["Current Setpoint:"][i].set(current_setpoint)
|
|
self.label_dict["Actual Current:"][i].set(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_raw'] * 1e6))
|
|
self.label_dict["Target Current:"][i].set("%0.3f A" % axis['target_current'])
|
|
self.label_dict["Inverted:"][i].set(polarity)
|
|
|
|
def enqueue_new_status(self, status):
|
|
"""Runs in caller thread and places status onto queue to display when polled in update_label_poll_method"""
|
|
self.update_label_queue.put(status)
|
|
|
|
def update_label_poll_method(self):
|
|
"""Infinite loop to poll for status updates to display"""
|
|
try:
|
|
new_status = self.update_label_queue.get(block=False) # Blocks until new data is available.
|
|
self.update_labels(new_status)
|
|
except Empty:
|
|
pass
|
|
self.controller.after(200, self.update_label_poll_method)
|
|
|
|
|
|
class OutputConsole(Frame):
|
|
# console to print information to user in, similar to standard python output
|
|
|
|
def __init__(self, parent):
|
|
Frame.__init__(self, parent, relief=SUNKEN, bd=1)
|
|
|
|
# Queue for messages to print when this class currently has main thread control
|
|
self.print_queue = Queue()
|
|
|
|
# configure Tkinter grid:
|
|
self.grid_rowconfigure(ALL, weight=1)
|
|
self.grid_columnconfigure(0, weight=1, minsize=60) # console needs to have a minimum width
|
|
|
|
scrollbar = Scrollbar(self) # setup scrollbar
|
|
self.console = Text(self) # setup main console widget
|
|
self.console.bind("<Key>", lambda e: "break") # prevent user from writing into the console
|
|
|
|
scrollbar.grid(row=0, column=1, sticky="ns") # place the scrollbar and stretch vertically
|
|
self.console.grid(row=0, column=0, sticky="nesw") # place the output console
|
|
# link scrollbar to the console
|
|
scrollbar.config(command=self.console.yview)
|
|
self.console.config(yscrollcommand=scrollbar.set)
|
|
|
|
# Start the main thread for printing queued messages
|
|
self.print_thread()
|
|
|
|
def print_thread(self):
|
|
""" Continuous thread that checks if messages are present, and prints them if they are. """
|
|
try:
|
|
while True:
|
|
msg = self.print_queue.get(block=False)
|
|
# print to console
|
|
self.console.insert(END, msg + "\n")
|
|
self.console.see(END) # scroll console to bottom
|
|
except Empty:
|
|
pass
|
|
|
|
# Print messages every 100 ms in the main Tkinter loop
|
|
self.after(100, self.print_thread)
|
|
|
|
def put(self, message):
|
|
self.print_queue.put(message)
|