Partially completed front-end reintegration.

This commit is contained in:
2021-08-03 22:29:09 +02:00
parent 11b5ea4e36
commit 8f70f85c84
11 changed files with 486 additions and 677 deletions
+110 -68
View File
@@ -3,6 +3,8 @@
# ToDo: optimize layout for smaller screen (like on IRS clean room PC)
# import packages for user interface:
from queue import Queue, Empty
from tkinter import *
from tkinter import ttk
from tkinter import messagebox
@@ -18,11 +20,11 @@ from datetime import datetime
# import other project files:
import src.globals as g
import src.cage_func as func
import src.csv_threading as csv
import src.config_handling as config
import src.csv_logging as log
from src.utility import ui_print
import src.helmholtz_cage_device as helmholtz_cage_device
# define font styles:
HEADER_FONT = ("Arial", 13, "bold")
@@ -245,30 +247,28 @@ class ManualMode(Frame):
# update the labels showing the min/max achievable values
compensate = self.compensate.get() # read out if compensate field checkbox is checked (True or False)
i = 0
for var in self.max_value_vars: # go through the max value labels for each axis
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.AXES[i].max_field * 1e6 # get max values from the axis object
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.AXES[i].max_comp_field * 1e6
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
i += 1
def switch_to_current_mode(self): # called when switching to the input current mode
self.compensate_checkbox.config(state=DISABLED) # disable the compensate ambient field checkbox
# update the labels showing the min/max achievable values
i = 0
for var in self.max_value_vars: # go through the max value labels for each axis
var.set("(%0.2f to %0.2f A)" % (-g.AXES[i].max_amps, g.AXES[i].max_amps)) # update the label
i += 1
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
func.setup_all() # reinitialize all PSUs and the Arduino
# reinitialize all PSUs and the Arduino
g.CAGE_DEVICE.reconnect_hardware()
# 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
@@ -276,7 +276,7 @@ class ManualMode(Frame):
logger.log_datapoint() # log data
def power_down(self): # called on "power down" button press
func.power_down_all() # power down outputs on all PSUs and the Arduino
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
@@ -297,7 +297,6 @@ class ManualMode(Frame):
else: # no issues while reading entries (user entered correct format)
function_to_call = self.modes[self.input_mode.get()][0] # get function of appropriate mode
function_to_call(vector) # call function (self.execute_field() or self.execute_current())
self.controller.StatusDisplay.update_labels() # update status display after change
# log change to the log file if user has selected event logging in the Configure Logging window
logger = self.controller.pages[ConfigureLogging] # get object of logging configurator
@@ -305,20 +304,34 @@ class ManualMode(Frame):
logger.log_datapoint() # log data
def execute_field(self, vector): # convert magnetic field vector and send to test bench
ui_print("Field executing:", vector, "\u03BCT")
compensate = self.compensate.get() # read out if compensate ambient field checkbox is ticked
if compensate: # ambient field should be compensated
func.set_field(vector * 1e-6) # convert to Tesla and send to test bench
elif not compensate: # ambient field should not be compensated
func.set_field_simple(vector * 1e-6) # convert to Tesla and send to test bench
else: # this really should never happen
ui_print("Unexpected value encountered: compensate =", compensate)
messagebox.showerror("Unexpected Value!", ("Unexpected value encountered: compensate =", compensate))
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("Current executing:", vector, "A")
func.set_current_vec(vector) # command 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):
@@ -552,8 +565,8 @@ class HardwareConfiguration(Frame):
# text for the description labels:
entry_texts = ["XY PSU Serial Port:", "Z PSU Serial Port:"]
# create variables to store the port names and set to current names
self.XY_port = StringVar(value=g.XY_PORT)
self.Z_port = StringVar(value=g.Z_PORT)
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.XY_port, self.Z_port] # list to store both port variables
row = 0
for text in entry_texts: # do this for both ports
@@ -596,7 +609,7 @@ class HardwareConfiguration(Frame):
# Fill in header (axis names):
col = 1
for text in ["X-Axis", "Y-Axis", "Z-Axis"]:
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
@@ -649,16 +662,16 @@ class HardwareConfiguration(Frame):
def restore_defaults(self): # restore default settings
config.reset_config_to_default() # overwrite config file with default
ui_print("\nReinitializing devices...")
func.setup_all() # setup everything with the defaults
g.CAGE_DEVICE.reconnect_hardware() # 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.XY_PORT)
self.Z_port.set(g.Z_PORT)
self.XY_port.set(g.CAGE_DEVICE.com_port_psu1)
self.Z_port.set(g.CAGE_DEVICE.com_port_psu2)
for key in self.entries.keys(): # go through the main value table
for i in [0, 1, 2]: # go through all three axes
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
@@ -667,7 +680,7 @@ class HardwareConfiguration(Frame):
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 = func.value_in_limits(g.AXIS_NAMES[i], self.entries[key][3], value)
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
@@ -676,8 +689,8 @@ class HardwareConfiguration(Frame):
def write_values(self): # update config object with user inputs into entry fields and reinitialize devices
# set serial ports for PSUs:
config.edit_config("PORTS", "xy_port", self.XY_port.get())
config.edit_config("PORTS", "z_port", self.Z_port.get())
config.edit_config("Supplies", "xy_port", self.XY_port.get())
config.edit_config("Supplies", "z_port", self.Z_port.get())
# set numeric values for all axes
for key in self.entries.keys(): # go through rows of entry table
@@ -695,7 +708,7 @@ class HardwareConfiguration(Frame):
# 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 = func.value_in_limits(g.AXIS_NAMES[i], config_key, value) # perform value check
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
@@ -723,7 +736,7 @@ class HardwareConfiguration(Frame):
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 = 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
@@ -733,7 +746,7 @@ class HardwareConfiguration(Frame):
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...")
func.setup_all() # reinitialize devices and program with new values
g.CAGE_DEVICE.reconnect_hardware() # 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
@@ -747,7 +760,7 @@ class HardwareConfiguration(Frame):
config.check_config(config.CONFIG_OBJECT) # check and display warnings if values are out of bounds
ui_print("\nReinitializing devices...")
func.setup_all() # reinitialize devices and program with new values
g.CAGE_DEVICE.reconnect_hardware() # reinitialize devices and program with new values
self.update_fields() # update entry fields to show new values
elif filename == '': # this happens when file selection window is closed without selecting a file
ui_print("No file selected, could not load config.")
@@ -768,14 +781,14 @@ class HardwareConfiguration(Frame):
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...")
func.setup_all() # reinitialize devices and program with new values
g.CAGE_DEVICE.reconnect_hardware() # 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...")
func.setup_all() # reinitialize devices and program with new values
g.CAGE_DEVICE.reconnect_hardware() # reinitialize devices and program with new values
self.update_fields() # update entry fields to show values as they are in the config
@@ -1026,6 +1039,10 @@ class StatusDisplay(Frame):
# 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)
@@ -1075,38 +1092,63 @@ class StatusDisplay(Frame):
LabelCol[row].grid(row=row + rowCounter, column=col, sticky="nsew") # place label
col += 1
self.update_labels() # fill in all values
# 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 continuous_label_update(self, controller, interval): # update display values in regular intervals (ms)
if not g.exitFlag: # application ist still running
self.update_labels() # update the label values
# call function again after time interval:
controller.after(interval, lambda: self.continuous_label_update(controller, interval))
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"
def update_labels(self): # update all values in the status display
g.ARDUINO.update_status_info() # get latest status info from arduino
i = 0
for axis in g.AXES: # go through all three axes
if axis.device is not None: # there is a PSU for this axis connected
axis.update_status_info() # get latest status info from PSU (Takes very long...)
# update all label variables with current values:
# ToDo (optional): Use the central dictionary currently defined in csv_logging.py for this
self.label_dict["PSU Serial Port:"][i].set(g.PORTS[i])
self.label_dict["PSU Channel:"][i].set(axis.channel)
self.label_dict["PSU Status:"][i].set(axis.connected)
self.label_dict["Arduino Status:"][i].set(g.ARDUINO.connected) # ToDo (optional): make this multicolumn
self.label_dict["Output:"][i].set(axis.output_active)
self.label_dict["Remote Control:"][i].set(axis.remote_ctrl_active)
self.label_dict["Voltage Setpoint:"][i].set("%0.3f V" % axis.voltage_setpoint)
self.label_dict["Actual Voltage:"][i].set("%0.3f V" % axis.voltage)
self.label_dict["Current Setpoint:"][i].set("%0.3f A" % axis.current_setpoint)
self.label_dict["Actual Current:"][i].set("%0.3f A" % axis.current)
self.label_dict["Target Field:"][i].set("%0.3f \u03BCT" % (axis.target_field * 1e6))
self.label_dict["Trgt. Field Raw:"][i].set("%0.3f \u03BCT" % (axis.target_field_comp * 1e6))
self.label_dict["Target Current:"][i].set("%0.3f A" % axis.target_current)
self.label_dict["Inverted:"][i].set(axis.polarity_switched)
i += 1
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