Introduced red status indicator line, minor warning fixes

This commit is contained in:
2023-02-23 18:17:01 +01:00
parent d0649a1af5
commit 70fa24f905
2 changed files with 84 additions and 52 deletions
+50 -7
View File
@@ -9,6 +9,7 @@ import numpy as np
from threading import *
from tkinter import messagebox
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from src.exceptions import DeviceBusy, DeviceAccessError
from src.utility import ui_print
@@ -79,6 +80,9 @@ class ExecCSVThread(Thread):
messagebox.showwarning("Device Error!", "Required devices are not present, sequence aborted.")
return
# Initialize plot
figure, avx_lines = display_plot(parent)
i = 0 # index of the current array row
while i < len(array):
if self.stopped or g.exit_flag:
@@ -88,9 +92,17 @@ class ExecCSVThread(Thread):
return
# while array is not finished, devices are connected, user has not cancelled and application is running
t = time.time() - t_zero # get time relative to start of run
target_t = array[i, 0] # Target execution time of data point
# Update figure
try:
for j in range(4):
avx_lines[j].set_data([t, t], [0, 1])
parent.plot_canvas.draw() # equivalent to matplotlib.show()
except DeviceAccessError as e:
ui_print("Failed to update figure: ", e)
if t >= target_t: # time for this row has come
field_vec = array[i, 1:4] # extract desired field vector
ui_print("[{:5.3f}s] B=[{:.1f}, {:.1f}, {:.1f}]\u03BCT for t={:.2f}s".format(t,
@@ -106,7 +118,6 @@ class ExecCSVThread(Thread):
logger = controller.pages[ui.ConfigureLogging] # get object of logging configurator
if logger.event_logging: # data should be logged when test bench is commanded
logger.log_datapoint() # log data
i = i + 1 # next row
elif t <= target_t - delay - 0.02: # is there enough time to sleep before the next row?
@@ -117,7 +128,7 @@ class ExecCSVThread(Thread):
def read_csv_to_array(filepath): # convert a given csv file to a numpy array
# csv format: time [s], xField [T], yField [T], zField [T] (german excel)
# decimal or period commas. Do not use these characters as a thousands separator!
# decimal or period commas. Do not use these characters as a thousand separator!
with open(filepath, 'r') as csv_file:
# Normalize separators
csv_string = csv_file.read()
@@ -153,6 +164,38 @@ def check_array_ok(array):
messagebox.showwarning("Value Limits Warning!", warning_msg)
def display_plot(parent): # create plot of fixed size (pixels) from array
# calculate available height for plot (in pixels):
height_others = 0 # initialize variable to calculate height of other widgets
for element in parent.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 = parent.parent.winfo_height() - height_others - 50 # height of parent frame - other widgets - margin
width = min(parent.parent.winfo_width() - 100, 1100) # set width to available space but max. 1100
# Create plot
figure = plot_field_sequence(parent.sequence_array, width, height) # create figure to be displayed
# Clear previous plots first
try:
if parent.plot_canvas is not None:
parent.plot_canvas.get_tk_widget().destroy()
except Exception as e:
ui_print("Something went wrong while plotting csv data!", e)
messagebox.showerror("Error!", "Something went wrong while plotting csv data: \n%s" % e)
pass
axes = figure.axes
avx_lines = [axes[0].axvline(x=0, color="r"), axes[1].axvline(x=0, color="r"),
axes[2].axvline(x=0, color="r"), axes[3].axvline(x=0, color="r")]
# Show new plot
parent.plot_canvas = FigureCanvasTkAgg(figure, parent.plot_frame) # create canvas to draw figure on
parent.plot_canvas.draw() # equivalent to matplotlib.show()
parent.plot_canvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in the UI
return figure, avx_lines
def plot_field_sequence(array, width, height): # create plot of fixed size (pixels) from array
# ToDo (optional): polar plots, plots of angle...
fig_dpi = 100 # set figure resolution (dots per inch)
@@ -160,12 +203,12 @@ def plot_field_sequence(array, width, height): # create plot of fixed size (pix
figure = plt.Figure(figsize=(width*px, height*px), dpi=fig_dpi) # create figure with correct size
# noinspection PyTypeChecker,SpellCheckingInspection
axes = figure.subplots(4, sharex=True, sharey=False, gridspec_kw={'hspace': 0.4}) # create subplots with shared axes
axes = figure.subplots(4, sharex=True, sharey=False, gridspec_kw={'hspace': 0.4}) # make subplots with shared axes
figure.suptitle("Magnetic Field Sequence") # set figure title
# modify data to show instantaneous jumps in field to reflect test bench operation
new_array = np.array([[0, 0, 0, 0, 0]], dtype=float) # initialize modified array, zeros to show start from no fields
new_array = np.array([[0, 0, 0, 0, 0]], dtype=float) # initialize modified array, zeros to show start from no field
last_values = [0, 0, 0, 0] # [x,y,z, rr] field values / rot rate from last data point (zero here)
for row in array[:, 0:5]: # go through each row in the original array
@@ -200,8 +243,8 @@ def plot_field_sequence(array, width, height): # create plot of fixed size (pix
ylim_mag = ([min(axes[0].get_ylim()), max(axes[0].get_ylim()),
min(axes[1].get_ylim()), max(axes[1].get_ylim()),
min(axes[2].get_ylim()), max(axes[2].get_ylim())])
for i in range(3): axes[0].set_ylim(min(ylim_mag),max(ylim_mag))
for i in range(3):
axes[0].set_ylim(min(ylim_mag), max(ylim_mag))
# set shared axis labels:
axes[2].set_xlabel("Time [s])")
+34 -45
View File
@@ -20,7 +20,6 @@ import os.path
from datetime import datetime
from math import pi
import warnings
warnings.filterwarnings("error")
# import other project files:
import src.globals as g
@@ -33,6 +32,9 @@ from src.exceptions import DeviceAccessError, MagFieldOutOfBounds
from src.utility import ui_print, save_dict_list_to_csv, save_dict_list_to_csv2, load_dict_list_from_csv
import src.helmholtz_cage_device as helmholtz_cage_device
# Filter warning messages
warnings.filterwarnings("error")
# Define global font styles:
screen_height_limit1 = 1100 # Limit after which font size gets reduced
screen_height_limit2 = 900 # Limit after which font size gets reduced
@@ -311,11 +313,11 @@ class ManualMode(Frame):
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))
messagebox.showerror("Unexpected Value! Unexpected value encountered: compensate = ", str(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
self.compensate_checkbox.config(state=DISABLED) # disable the compensation of the 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
@@ -374,8 +376,8 @@ class ManualMode(Frame):
pass
else: # this really should never happen
ui_print("Unexpected value encountered: compensate =", compensate)
messagebox.showerror("Unexpected Value!",
("Unexpected value encountered: compensate =", compensate))
messagebox.showerror("Unexpected Value! Unexpected value encountered: compensate = ",
str(compensate))
except helmholtz_cage_device.DeviceBusy:
ui_print("Error: Could not acquire control. Is the HW already in use?")
@@ -522,7 +524,7 @@ class ExecuteCSVMode(Frame):
self.generate_load_csv_button.grid(row=0, column=1, padx=5, pady=5)
row_counter += 1
info_text = Label(self,
text="Generate data of a circular, counter clockwise motion around an arbitrary axis and strength.",
text="Generate data of a circular, counter clockwise motion around an arbitrary axis.",
padx=100, pady=3)
info_text.grid(row=row_counter, column=col_counter, padx=0, sticky=W)
row_counter += 1
@@ -692,7 +694,6 @@ class ExecuteCSVMode(Frame):
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
def export_csv_sequence(self): # Generate and export csv sequence
# Generate rotation data
@@ -701,7 +702,7 @@ class ExecuteCSVMode(Frame):
rot_sequence = dict(enumerate(t))
for i in range(len(t)):
rot_sequence[i] = {'Time [s]]': t[i], 'xField [T]': x[i], 'yField [T]': y[i], 'zField [T]': z[i],
'Rotation Rate [deg/s]': rr[i] }
'Rotation Rate [deg/s]': rr[i]}
# Save dictionary to disk
save_dict_list_to_csv2('test_sequence_rotation.csv', rot_sequence, query_path=True)
ui_print("Saved test sequence to disc.")
@@ -798,11 +799,11 @@ class ExecuteCSVMode(Frame):
self.rot_cycle_number_vars.set("{:d}".format(rot_cycle_number))
# Variable rotation rate
# Solve integral of r(t) = r_0 + r_1 * t + r_2 * t ^ 2 + r3 * sin(r_4 * t + r_5) from 0 to tau
R = 360 * rot_cycle_number # [deg] Cycle number multiples of full circle
angle = 360 * rot_cycle_number # [deg] Cycle number multiples of full circle
func = lambda tau2: r[0] + r[1] * tau2 + r[2] * tau2 ** 2 + r[3] * (np.sin(r[4] * tau2 + r[5]))
func_integral = lambda tau: R - (r[0] * tau + r[1] / 2 * tau ** 2 + r[2] / 3 * tau ** 3
- r[3] * (np.cos(r[4] * tau + r[5]) + np.cos(r[5]))
+ r[3] * r[4] * (np.sin(r[4] * tau + r[5]) - np.sin(r[5])))
func_integral = lambda tau: angle - (r[0] * tau + r[1] / 2 * tau ** 2 + r[2] / 3 * tau ** 3
- r[3] * (np.cos(r[4] * tau + r[5]) + np.cos(r[5]))
+ r[3] * r[4] * (np.sin(r[4] * tau + r[5]) - np.sin(r[5])))
tau_solution = -1
i = 0 # iterator and guess variable [s]
while tau_solution < 0 or abs(func_integral(tau_solution)) > 0.5:
@@ -1129,7 +1130,7 @@ class CalibrateAmbientField(Frame):
if cmd == 'finished':
self.reactivate_buttons()
elif cmd == 'failed':
messagebox.showerror("Calibration error", "Error occured during calibration:\n{}".format(arg))
messagebox.showerror("Calibration error", "Error occurred during calibration:\n{}".format(arg))
self.reactivate_buttons()
elif cmd == 'progress':
self.calibration_procedure_progress_var.set(min(int(arg * 100), 100))
@@ -1518,7 +1519,8 @@ class CalibrateMagnetometerSimple(Frame):
# Notes on the calibration method
calibration_method_notes_frame = LabelFrame(self.right_column, text="Calibration method notes:")
calibration_method_notes_frame.grid(row=row_counter, column=1, padx=(100, 0), pady=20, sticky="nw")
label = "-Implementation according to Zikmund et al. [DOI: 10.1109/I2MTC.2014.6860790]\n-Points created by Fibonacci sphere\n-Only accounts for hard-iron offset and MGM scaling errors!"
label = "-Implementation according to Zikmund et al. [DOI: 10.1109/I2MTC.2014.6860790]\n" \
"-Points created by Fibonacci sphere\n-Only accounts for hard-iron offset and MGM scaling errors!"
calibration_method_notes = Label(calibration_method_notes_frame, anchor='w', justify='left', text=label)
calibration_method_notes.grid(row=3, column=0, padx=5, pady=5, sticky="nw")
@@ -1624,7 +1626,7 @@ class CalibrateMagnetometerSimple(Frame):
if cmd == 'finished':
self.reactivate_buttons()
elif cmd == 'failed':
messagebox.showerror("Calibration error", "Error occured during calibration:\n{}".format(arg))
messagebox.showerror("Calibration error", "Error occurred during calibration:\n{}".format(arg))
self.reactivate_buttons()
elif cmd == 'progress':
self.calibration_procedure_progress_var.set(min(int(arg * 100), 100))
@@ -1686,7 +1688,7 @@ class CalibrateMagnetometerSimple(Frame):
try:
calibration_points = self.calibration_points_var.get()
calibration_interval = self.calibration_interval_var.get()
compensated_field = self.self.compensated_field_var.get()
compensated_field = self.compensated_field_var.get()
calibration_mag_field = self.mag_field_magnitude_var.get() * 1e-6 # converted to micro Tesla
if calibration_mag_field <= 0 or calibration_mag_field > g.MAG_MAG_FIELD:
raise MagFieldOutOfBounds
@@ -1918,7 +1920,7 @@ class CalibrateMagnetometerComplete(Frame):
calibration_point_nr_unit = Label(controls_frame, text="s (> 2 s)")
calibration_point_nr_unit.grid(row=2, column=2, pady=5, sticky="nw")
# Oversampling
calibration_point_nr_label = Label(controls_frame, text="Oversampling (samples/setpoint)")
calibration_point_nr_label = Label(controls_frame, text="Oversampling (samples/set point)")
calibration_point_nr_label.grid(row=3, column=0, pady=5, sticky="nw")
calibration_point_nr_entry = Entry(controls_frame, textvariable=self.calibration_oversampling_var)
calibration_point_nr_entry.grid(row=3, column=1, pady=5, sticky="nw")
@@ -2040,19 +2042,7 @@ class CalibrateMagnetometerComplete(Frame):
results_label_unit = Label(calibration_results_frame, text="-")
results_label_unit.grid(row=row_counter + row, column=1 + 3, padx=5, pady=5, sticky="nw")
row_counter += 3
"""
# A_mat
results_label_a_mat_inv = Label(calibration_results_frame, text="A =")
results_label_a_mat_inv.grid(row=row_counter+1, column=0, padx=5, pady=5, sticky="nw")
for row in range(3):
for column in range(3):
axis_data = Entry(calibration_results_frame,
textvariable=self.a_mat_result_vars[row][column],
width=15,
state='readonly')
axis_data.grid(row=row_counter+row, column=1 + column, padx=5, pady=5, sticky="nw")
row_counter += 3
"""
# b
results_label_a_mat_inv = Label(calibration_results_frame, text="b^T =")
results_label_a_mat_inv.grid(row=row_counter, column=0, padx=5, pady=5, sticky="nw")
@@ -2090,11 +2080,15 @@ class CalibrateMagnetometerComplete(Frame):
# Notes on the calibration method
calibration_method_notes_frame = LabelFrame(self.right_column, text="Calibration method notes:")
calibration_method_notes_frame.grid(row=0, column=0, padx=(100, 0), pady=20, sticky="nw")
label = "-Implementation of calibration according to Kok et al. [ISBN: 978-0-9824438-5-9]\n-Implementation of ellipsoid fit according to Li et al. [DOI: 10.1109/GMAP.2004.1290055]\n-Points created by Fibonacci sphere\n-Accounts for soft-iron (A matrix) and hard-iron (b offset vector) effects!\n-Measured to calibrated field function: h=A^-1 (h_m-b)"
label = "-Implementation of calibration according to Kok et al. [ISBN: 978-0-9824438-5-9]\n" \
"-Implementation of ellipsoid fit according to Li et al. [DOI: 10.1109/GMAP.2004.1290055]\n" \
"-Points created by Fibonacci sphere\n" \
"-Accounts for soft-iron (A matrix) and hard-iron (b offset vector) effects!\n" \
"-Measured to calibrated field function: h=A^-1 (h_m-b)"
calibration_method_notes = Label(calibration_method_notes_frame, anchor='w', justify='left', text=label)
calibration_method_notes.grid(row=0, column=0, padx=5, pady=5, sticky="nw")
# Plot frame (overwrite plotframe)
# Plot frame
plot_frame = LabelFrame(self.right_column, text="Result plots:")
plot_frame.grid(row=1, column=0, padx=(100, 0), pady=20, sticky="nw")
fig1 = plt.figure('MGM_cal_complete_left', figsize=(2.5, 3), dpi=100)
@@ -2152,7 +2146,7 @@ class CalibrateMagnetometerComplete(Frame):
if cmd == 'finished':
self.reactivate_buttons()
elif cmd == 'failed':
messagebox.showerror("Calibration error", "Error occured during calibration:\n{}".format(arg))
messagebox.showerror("Calibration error", "Error occurred during calibration:\n{}".format(arg))
self.reactivate_buttons()
elif cmd == 'progress':
self.calibration_procedure_progress_var.set(min(int(arg * 100), 100))
@@ -2411,7 +2405,7 @@ class HardwareConfiguration(Frame):
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
# Create and place buttons in frame
# 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)
@@ -2451,10 +2445,7 @@ class HardwareConfiguration(Frame):
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
row_counter += 2
# setup main entry field for operational constants
# setup frame:
@@ -2506,7 +2497,6 @@ class HardwareConfiguration(Frame):
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
@@ -2516,7 +2506,7 @@ class HardwareConfiguration(Frame):
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
# Create and place buttons in frame
# 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)
@@ -2527,7 +2517,6 @@ class HardwareConfiguration(Frame):
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
@@ -2649,7 +2638,7 @@ class HardwareConfiguration(Frame):
# 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*")])
defaultextension="*.ini*")
if filename == '': # this happens when file selection window is closed without selecting a file
ui_print("No file selected, could not save config.")
@@ -2835,8 +2824,8 @@ class ConfigureLogging(Frame):
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.regular_logging = False # tell everything it is time to stop periodic logging
self.event_logging = False # tell everything it is 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
@@ -2852,7 +2841,7 @@ class ConfigureLogging(Frame):
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
def clear_data(self): # called on button press, asks user if logged data should be saved 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. "