Implemented ambient field calibration tool.

This commit is contained in:
2021-08-04 13:17:00 +02:00
parent 8f70f85c84
commit 3596733843
9 changed files with 502 additions and 72 deletions
+240 -26
View File
@@ -3,7 +3,7 @@
# 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
@@ -23,6 +23,8 @@ import src.globals as g
import src.csv_threading as csv
import src.config_handling as config
import src.csv_logging as log
from src.calibration import AmbientFieldCalibration
from src.exceptions import DeviceAccessError
from src.utility import ui_print
import src.helmholtz_cage_device as helmholtz_cage_device
@@ -47,19 +49,23 @@ class HelmholtzGUI(Tk):
self.Menu = TopMenu(self) # display dropdown menu bar at the top (see TopMenu class for details)
mainArea = Frame(self, padx=10, pady=10) # create main area Frame where controls of each mode are displayed
mainArea.pack(side="top", fill="both", expand=True) # pack main area at the top of the window
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
mainArea.grid_rowconfigure(0, weight=1) # configure rows and columns of the Tkinter grid to expand with window
mainArea.grid_columnconfigure(0, weight=1)
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, ExecuteCSVMode, ConfigureLogging]: # do this for every mode page
page = P(mainArea, self) # initialize the page with the mainArea frame as the parent
for P in [ManualMode,
HardwareConfiguration,
CalibrateAmbientField,
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
@@ -87,33 +93,34 @@ class HelmholtzGUI(Tk):
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
ModeSelector = Menu(menu) # create a submenu object
menu.add_cascade(label="Menu", menu=ModeSelector) # add a dropdown with the submenu object
mode_selector = Menu(menu) # 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:
ModeSelector.add_command(label="Static Manual Input", command=lambda: self.manual_mode(window))
ModeSelector.add_command(label="Execute CSV Sequence", command=lambda: self.execute_csv_mode(window))
ModeSelector.add_separator()
ModeSelector.add_command(label="Configure Data Logging", command=lambda: self.logging(window))
ModeSelector.add_command(label="Settings...", command=lambda: self.configuration(window))
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_separator()
mode_selector.add_command(label="Configure Data Logging", command=self.logging)
mode_selector.add_command(label="Settings...", command=self.configuration)
@staticmethod
def manual_mode(window): # switch to the manual mode page
window.show_frame(ManualMode)
def manual_mode(self): # switch to the manual mode page
self.window.show_frame(ManualMode)
@staticmethod
def configuration(window): # switch to the settings page
window.show_frame(HardwareConfiguration)
def configuration(self): # switch to the settings page
self.window.show_frame(HardwareConfiguration)
@staticmethod
def execute_csv_mode(window): # switch to the CSV execution page
window.show_frame(ExecuteCSVMode)
def calibrate_ambient(self):
self.window.show_frame(CalibrateAmbientField)
@staticmethod
def logging(window): # switch to the logging settings page
window.show_frame(ConfigureLogging)
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):
@@ -514,6 +521,212 @@ class ExecuteCSVMode(Frame):
plotCanvas.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)
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()]
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 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=(30, 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=(30, 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=20, 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, 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")
# 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)
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):
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))
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:
print("Error starting calibration procedure: {}".format(e))
def calibration_procedure_coil_constants(self):
try:
self.calibration_coil_constants_thread = AmbientFieldCalibration(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:
print("Error starting calibration procedure: {}".format(e))
class HardwareConfiguration(Frame):
"""Settings window to set program constants"""
@@ -1150,6 +1363,7 @@ class StatusDisplay(Frame):
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