From 27c804904b2a12d6272f43439bc4658e7fbed7ea Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Tue, 2 Feb 2021 13:11:27 +0100 Subject: [PATCH] Implemented configuration window --- User_Interface.py | 183 +++++++++++++++++++++++++++++++++++++++++----- cage_func.py | 19 ++--- globals.py | 7 +- 3 files changed, 177 insertions(+), 32 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index cbc57e6..f85b305 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -25,12 +25,12 @@ class HelmholtzGUI(Tk): mainArea.grid_rowconfigure(0, weight=1) mainArea.grid_columnconfigure(0, weight=1) - self.frames = {} # dictionary for storing all pages + self.pages = {} # dictionary for storing all pages - for F in [ManualMode]: - frame = F(mainArea, self) - self.frames[F] = frame - frame.grid(row=0, column=0, sticky="nsew") + for P in [ManualMode, Configuration]: + page = P(mainArea, self) + self.pages[P] = page + page.grid(row=0, column=0, sticky="nsew") status_frame = Frame(self) status_frame.pack(side="bottom", fill="x", expand=False) @@ -46,7 +46,8 @@ class HelmholtzGUI(Tk): self.show_frame(ManualMode) def show_frame(self, key): - frame = self.frames[key] # gets correct page from the dictionary + frame = self.pages[key] # gets correct page from the dictionary + frame.page_switch() # update displays in this page with window-specific update function frame.tkraise() # brings this frame to the front @@ -59,15 +60,18 @@ class TopMenu: ModeSelector = Menu(menu) menu.add_cascade(label="Mode", menu=ModeSelector) ModeSelector.add_command(label="Static Manual Input", command=lambda: self.manual_mode(window)) + ModeSelector.add_command(label="Configuration...", command=lambda: self.configuration(window)) @staticmethod def manual_mode(window): window.show_frame(ManualMode) + @staticmethod + def configuration(window): + window.show_frame(Configuration) + class ManualMode(Frame): - # ToDo: Display maximum values - # ToDo: Add option to cancel ambient field # ToDo: Add buttons to safe and set to 0 def __init__(self, parent, controller): @@ -88,7 +92,6 @@ class ManualMode(Frame): # content: [function to call on button press, unit text to be displayed] self.modes = {"Magnetic Field": [self.execute_field, "\u03BCT", self.update_max_fields], "Current": [self.execute_current, "A", self.update_max_currents]} - # "Raw Current": [self.input_raw_current, "A"]} ToDo (optional): make functions for this self.unit = StringVar() default_mode = list(self.modes.keys())[0] @@ -143,7 +146,6 @@ class ManualMode(Frame): 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_columnconfigure(2, weight=1, minsize=20) self.buttons_frame.grid(row=row_counter, column=0) Label(self.buttons_frame, text="").grid(row=0, column=0) # add spacer @@ -151,25 +153,28 @@ class ManualMode(Frame): # 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) + execute_button.grid(row=row_counter, column=0, padx=5) # add button for reinitialization reinit_button = Button(self.buttons_frame, text="Reinitialize", command=func.setup_axes, pady=5, padx=5, font=BIG_BUTTON_FONT) - reinit_button.grid(row=row_counter, column=1) + reinit_button.grid(row=row_counter, column=1, padx=5) row_counter = row_counter + 1 # Add spacer to Frame below - Label(self, text="", pady=10).grid(row=row_counter, column=0) + 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 dropdown change + 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, e.g. calls update_max_fields function def change_mode_callback(self, var, index, mode): # not sure what the parameters are for, but they are necessary self.unit.set(self.modes[self.input_mode.get()][1]) # change unit text - self.modes[self.input_mode.get()][2]() # update max values + self.modes[self.input_mode.get()][2]() # update max values, e.g. calls update_max_fields function def update_max_fields(self): # update labels with maximum allowable field values self.compensate_checkbox.config(state=NORMAL) @@ -203,11 +208,16 @@ class ManualMode(Frame): function_to_call(vector) # call function # ToDo: update status display here - @staticmethod - def execute_field(vector): + def execute_field(self, vector): func.ui_print("field executing", vector) try: - func.set_field_simple(vector * 1e-6) # ToDo: change to set_field + comp = self.compensate.get() + if comp == 0: + func.set_field(vector * 1e-6) + elif comp == 1: + func.set_field_simple(vector * 1e-6) + else: + func.ui_print("Unexpected value encountered: compensate =", comp) except ValueError as e: func.ui_print(e) @@ -220,6 +230,141 @@ class ManualMode(Frame): func.ui_print(e) +class Configuration(Frame): + # generate configuration window to set program constants + + def __init__(self, parent, controller): + Frame.__init__(self, parent) + + self.grid_rowconfigure(ALL, weight=1) + self.grid_columnconfigure(ALL, weight=1) + + row_counter = 0 + + # Serial port settings frame: + port_frame = Frame(self) + port_frame.grid_rowconfigure(ALL, weight=1) + port_frame.grid_columnconfigure(ALL, weight=1) + port_frame.grid(row=row_counter, column=0, sticky=W) + + entry_texts = ["XY PSU Serial Port:", "Z PSU Serial Port:"] + self.XY_port = StringVar(value=g.XY_PORT) + self.Z_port = StringVar(value=g.Z_PORT) + port_vars = [self.XY_port, self.Z_port] + row = 0 + for text in entry_texts: + field = ttk.Entry(port_frame, textvariable=port_vars[row]) + field.grid(row=row, column=1, sticky=W) + axis_label = Label(port_frame, text=text, padx=5, pady=10) + axis_label.grid(row=row, column=0, sticky=W) + unit_label = Label(port_frame, text="e.g. COM10") + unit_label.grid(row=row, column=2, sticky=W) + row += 1 + + row_counter += 1 + + Label(self, text="", pady=0).grid(row=row_counter, column=0) # add spacer + row_counter += 1 + + value_frame = Frame(self) + value_frame.grid_rowconfigure(ALL, weight=1) + value_frame.grid_columnconfigure(ALL, weight=1) + value_frame.grid(row=row_counter, column=0) + + # Setup dictionary to generate entry table from + # {Key: [[x-value,y-value,z-value], unit, description, config file key, unit conversion factor]} + self.entries = { + "Coil Constants:": [[DoubleVar() for _ in range(3)], "\u03BCT/A", "", "coil_const", 1e6], + "Ambient Field:": [[DoubleVar() for _ in range(3)], "\u03BCT/A", "Field to be compensated", "ambient_field", 1e6], + "Resistances:": [[DoubleVar() for _ in range(3)], "\u03A9", "Resistance of coils + equipment", "resistance", 1], + "Max. Power:": [[DoubleVar() for _ in range(3)], "W", "Max. allowed power", "max_watts", 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.update_fields() # set current values from config file + + # Fill in header (axis names): + col = 1 + for text in ["X-Axis", "Y-Axis", "Z-Axis"]: + label = Label(value_frame, text=text, font=SUB_HEADER_FONT) + label.grid(row=0, column=col, sticky="ew") + col += 1 + # generate table with entries, unit labels and descriptions: + row = 1 + for key in self.entries.keys(): + for axis in range(3): # generate entry fields + field = ttk.Entry(value_frame, textvariable=self.entries[key][0][axis], width=10) + field.grid(row=row, column=axis+1, sticky=W, padx=2) + axis_label = Label(value_frame, text=key, padx=5, pady=5) + axis_label.grid(row=row, column=0, sticky=W) + unit_label = Label(value_frame, text=self.entries[key][1]) + unit_label.grid(row=row, column=4, sticky=W) + description_label = Label(value_frame, text=self.entries[key][2]) + description_label.grid(row=row, column=5, sticky=W) + row = row + 1 + + row_counter += 1 + + Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer + row_counter += 1 + + # Setup buttons + # Setup frame to house buttons: + self.buttons_frame = Frame(self) + self.buttons_frame.grid_rowconfigure(ALL, weight=1) + self.buttons_frame.grid_columnconfigure(ALL, weight=1) + self.buttons_frame.grid(row=row_counter, column=0, sticky=W, padx=20) + + # Create and place buttons + implement_button = Button(self.buttons_frame, text="Update and Reconnect", command=self.implement, + pady=5, padx=5, font=BIG_BUTTON_FONT) + implement_button.grid(row=0, column=0, padx=5) + restore_button = Button(self.buttons_frame, text="Restore Defaults", command=self.restore_defaults, + pady=5, padx=5, font=BIG_BUTTON_FONT) + restore_button.grid(row=0, column=1, padx=5) + + row_counter += 1 + Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer + + def page_switch(self): # function that is called when switching to this window + self.update_fields() + + def restore_defaults(self): + func.create_default_config(g.CONFIG_FILE) # overwrite config file with default + func.setup_axes() # setup everything with the defaults ToDo: take out? + self.update_fields() # update fields in config window + + def update_fields(self): + # set current values for all entry variables from config file + self.XY_port.set(g.XY_PORT) + self.Z_port.set(g.Z_PORT) + + for key in self.entries.keys(): + for i in [0, 1, 2]: + value = func.read_config(g.AXIS_NAMES[i], self.entries[key][3]) # get value from config file + self.entries[key][0][i].set(value) # set initial value on variable + type_value = self.entries[key][0][i].get() # get value with correct data type + factor = self.entries[key][4] # get unit conversion factor + self.entries[key][0][i].set(round(type_value * factor, 3)) # set value with correct unit conversion + + def implement(self): # update config file with user inputs into entry fields and reinitialize + # ToDo: Error handling with warning messages to user if wrong data format or high/low value is entered + func.edit_config("PORTS", "xy_port", self.XY_port.get()) + func.edit_config("PORTS", "z_port", self.Z_port.get()) + + for key in self.entries.keys(): + for i in [0, 1, 2]: + value = self.entries[key][0][i].get() + factor = self.entries[key][4] # get unit conversion factor + if factor not in [0, 1]: # prevent conversion of int variables to float representation + value = value / factor # implement unit conversion + print(key, value) + func.edit_config(g.AXIS_NAMES[i], self.entries[key][3], value) + + func.setup_axes() + + class StatusDisplay(Frame): def __init__(self, parent, controller): @@ -269,7 +414,7 @@ class StatusDisplay(Frame): self.update_labels(controller) def update_labels(self, controller): - # ToDo: do this with a dictionary + # ToDo (optional): do this with a dictionary g.ARDUINO.update_status_info() i = 0 for axis in g.AXES: diff --git a/cage_func.py b/cage_func.py index 140ca09..19c930d 100644 --- a/cage_func.py +++ b/cage_func.py @@ -5,7 +5,7 @@ import pandas import time import numpy as np import serial -import traceback # ToDo: remove +import traceback from tkinter import * from configparser import ConfigParser @@ -135,9 +135,11 @@ class Axis: class ArduinoCtrl(Arduino): - def __init__(self, pins): + def __init__(self): self.connected = "Unknown" - self.pins = pins + self.pins = [0, 0, 0] + for i in range(3): + self.pins[i] = int(read_config(g.AXIS_NAMES[i], "relay_pin")) ui_print("\nConnecting to Arduino...") try: Arduino.__init__(self) # search for connected arduino and connect @@ -155,7 +157,7 @@ class ArduinoCtrl(Arduino): if self.connected == "Connected": try: for axis in g.AXES: - if g.ARDUINO.digitalRead(axis.ardPin): # ToDo: Test if this actually works + if g.ARDUINO.digitalRead(axis.ardPin): axis.polarity_switched = "True" else: axis.polarity_switched = "False" @@ -209,8 +211,8 @@ def create_default_config(file): # create config file from default values (stor i += 1 config.add_section("PORTS") - for key in g.defaults.keys(): - config.set("PORTS", key, str(g.defaults[key])) + for key in g.default_ports.keys(): + config.set("PORTS", key, str(g.default_ports[key])) with open(file, 'w') as conf: config.write(conf) @@ -230,7 +232,6 @@ def ui_print(*content): # prints text to built in console def setup_axes(): # creates device objects for all PSUs and sets their values # Connect to Arduino: - arduino_pins = read_config("PORTS", "relay_pins") try: if g.ARDUINO is not None: # ui_print("\nClosing arduino link") @@ -244,7 +245,7 @@ def setup_axes(): # creates device objects for all PSUs and sets their values pass # when no Arduino is connected but g.ARDUINO has been initialized then there is nothing to close # this throws exception, which can be ignored - g.ARDUINO = ArduinoCtrl(arduino_pins) + g.ARDUINO = ArduinoCtrl() except Exception as e: ui_print("Arduino setup failed:", e) ui_print(traceback.print_exc()) @@ -270,7 +271,7 @@ def setup_axes(): # creates device objects for all PSUs and sets their values g.Y_AXIS = Axis(1, None, 1, g.ARDUINO.pins[1]) ui_print("XY Device not connected or incorrect port set.") - ui_print("Connecting to Z Device on %s..." % g.XY_PORT) + ui_print("Connecting to Z Device on %s..." % g.Z_PORT) try: g.Z_DEVICE = PS2000B.PS2000B(g.Z_PORT) ui_print("Connection established.") diff --git a/globals.py b/globals.py index bf6bf24..af65349 100644 --- a/globals.py +++ b/globals.py @@ -19,7 +19,6 @@ global CONFIG_FILE global XY_PORT global Z_PORT -global RELAY_PINS global PORTS @@ -27,12 +26,12 @@ global PORTS default_arrays = { "coil_const": np.array([38.6, 38.45, 37.9]) * 1e-6, # Coil constants [x,y,z] [T/A] "ambient_field": np.array([30, 30, 30]) * 1e-6, # ambient magnetic field in measurement area [T] - "resistance": np.array([1.7, 1.7, 1.7]), # resistance of [x,y,z] circuits [Ohm] + "resistance": np.array([1.7, 1.7, 1.7], dtype=float), # resistance of [x,y,z] circuits [Ohm] "max_watts": np.array([15, 15, 15], dtype=float), # max. allowed power for [x,y,z] circuits [W] "max_volts": np.array([16, 16, 16], dtype=float), # max. allowed voltage, limited to 16V by used diodes! [V] + "relay_pin": [15, 16, 17] # pin on the arduino for reversing [x,y,z] polarity } -defaults = { +default_ports = { "xy_port": "COM1", # Serial port where PSU for X- and Y-Axes is connected "z_port": "COM2", # Serial port where PSU for Z-Axis is connected - "relay_pins": [15, 16, 17] # pin on the arduino for reversing [x,y,z] polarity }