From 239b685feef129dba90d0945a756a4d4c6a57db4 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Wed, 27 Jan 2021 11:21:03 +0100 Subject: [PATCH 01/36] debug and testing with arduino --- .gitignore | 2 + .idea/Python-PS2000B.iml | 2 +- .idea/misc.xml | 2 +- User_Interface.py | 1 + cage_func.py | 92 ++++++++++++++++++++++++++-------------- main.py | 9 ++-- settings.py | 13 ++---- 7 files changed, 71 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 37b8976..9c4f096 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,5 @@ ENV/ .idea/misc.xml .idea/Python-PS2000B.iml .idea/Python-PS2000B.iml +.idea/Python-PS2000B.iml +.idea/misc.xml diff --git a/.idea/Python-PS2000B.iml b/.idea/Python-PS2000B.iml index 98105af..653cec9 100644 --- a/.idea/Python-PS2000B.iml +++ b/.idea/Python-PS2000B.iml @@ -2,7 +2,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 6724169..5ea737b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/User_Interface.py b/User_Interface.py index 8c259f3..dbcbe75 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -228,6 +228,7 @@ class StatusDisplay(Frame): def update_labels(self, controller): # ToDo: do this with a dictionary + g.ARDUINO.update_status_info() i = 0 for axis in g.AXES: if axis.device is not None: diff --git a/cage_func.py b/cage_func.py index d246ccb..ab5651f 100644 --- a/cage_func.py +++ b/cage_func.py @@ -5,6 +5,7 @@ import pandas import time import numpy as np import serial +import traceback # ToDo: remove class Axis: @@ -69,31 +70,23 @@ class Axis: else: self.connected = "Connected" - if g.ARDUINO.connected == "Connected": - try: - if g.ARDUINO.digitalRead(self.ardPin): # ToDo: Test if this actually works - self.polarity_switched = "True" - else: self.polarity_switched = "False" - except Exception: - print("Error with Arduino") - self.polarity_switched = "Unknown" - g.ARDUINO.connected = "Connection Error" - else: - g.ARDUINO.connected = "Connected" - def print_status(self): # axis = axis control variable, stored in settings.py print("%s, %0.2f V, %0.2f A" % (self.device.get_device_status_information(self.channel), self.device.get_voltage(self.channel), self.device.get_current(self.channel))) def power_down(self): # temporary powerdown, set outputs to 0 but keep connections enabled - self.target_current = 0 - self.target_field = 0 - self.target_field_comp = 0 - self.device.set_voltage(0, self.channel) - self.device.set_current(0, self.channel) - self.device.disable_output(self.channel) - g.ARDUINO.digitalWrite(self.ardPin, "LOW") + try: + self.target_current = 0 + self.target_field = 0 + self.target_field_comp = 0 + if self.device is not None: + self.device.set_voltage(0, self.channel) + self.device.set_current(0, self.channel) + self.device.disable_output(self.channel) + g.ARDUINO.digitalWrite(self.ardPin, "LOW") + except Exception as e: + print(e) # ToDo: more error handling here def set_signed_current(self, value): # sets current with correct polarity on this axis device = self.device @@ -101,21 +94,22 @@ class Axis: ardPin = self.ardPin # print("Attempting to set current", value, "A") self.target_current = value - if self.connected == "Connected": + if self.connected == "Connected" or True: # ToDo!: remove True, only for arduino testing! if abs(value) > self.max_amps: # prevent excessive currents self.power_down() # set output to 0 and deactivate raise ValueError("Invalid current value. Tried %0.2fA, max. %0.2fA allowed" % (value, self.max_amps)) elif value >= 0: # switch polarity as needed - pass # g.ARDUINO.digitalWrite(ardPin, "LOW") ToDo: reactivate and tie to arduino + g.ARDUINO.digitalWrite(ardPin, "LOW") # ToDo: reactivate and tie to arduino elif value < 0: - pass # g.ARDUINO.digitalWrite(ardPin, "HIGH") ToDo: reactivate + g.ARDUINO.digitalWrite(ardPin, "HIGH") # ToDo: tie to arduino else: raise Exception("This should be impossible.") maxVoltage = min(max(1.1 * self.max_amps * self.resistance, 8), self.max_volts) # limit voltage# # print("sending values to device: U =", maxVoltage, "I =", abs(value)) - device.set_current(abs(value), channel) - device.set_voltage(maxVoltage, channel) - device.enable_output(channel) + if self.connected == "Connected": # ToDo!: remove if, only for arduino testing! + device.set_current(abs(value), channel) + device.set_voltage(maxVoltage, channel) + device.enable_output(channel) else: print(self.name, "not connected, can't set current.") @@ -138,28 +132,60 @@ class ArduinoCtrl(Arduino): def __init__(self, pins): self.connected = "Unknown" self.pins = pins - print("Connecting to Arduino...") + print("\nConnecting to Arduino...") try: Arduino.__init__(self) # search for connected arduino and connect for pin in self.pins: - g.ARDUINO.pinMode(pin, "Output") - g.ARDUINO.digitalWrite(pin, "LOW") - except Exception: + self.pinMode(pin, "Output") + self.digitalWrite(pin, "LOW") + except Exception as e: print("Connection to Arduino failed.") + print(e) self.connected = "Not Connected" else: - g.arduino_connected = "Connected" + self.connected = "Connected" print("Arduino ready.") + def update_status_info(self): + if self.connected == "Connected": + try: + for axis in g.AXES: + if g.ARDUINO.digitalRead(axis.ardPin): # ToDo: Test if this actually works + axis.polarity_switched = "True" + else: + axis.polarity_switched = "False" + except Exception as e: + print("Error with Arduino:", e) + for axis in g.AXES: + axis.polarity_switched = "Unknown" + self.connected = "Connection Error" + else: + g.ARDUINO.connected = "Connected" + def safe(self): # sets output pins to low and closes serial connection for pin in self.pins: - g.ARDUINO.digitalWrite(pin, "LOW") + self.digitalWrite(pin, "LOW") def setup_axes(): # creates device objects for all PSUs and sets their values + # Connect to Arduino: + try: + if g.ARDUINO is not None: + # print("\nClosing arduino link") + try: + g.ARDUINO.close() # close serial link before attempting reconnection + except serial.serialutil.SerialException: + pass + # serial.flush() in Arduino.close() fails when reconnecting + # this ignores it and allows serial.close() to execute (I think) + g.ARDUINO = ArduinoCtrl(g.RELAY_PINS) + except Exception as e: + print("Arduino setup failed:", e) + print(traceback.print_exc()) + g.AXES = [] - print("Connecting to XY Device on %s..." % g.XY_PORT) + print("\nConnecting to XY Device on %s..." % g.XY_PORT) try: if g.XY_DEVICE is not None: print("closing serial connection on XY device") @@ -187,6 +213,7 @@ def setup_axes(): # creates device objects for all PSUs and sets their values g.AXES.append(g.Y_AXIS) g.AXES.append(g.Z_AXIS) + print("") # new line i = 0 for axis in g.AXES: # ToDo: move to axis init axis.resistance = g.RESISTANCES[i] @@ -199,7 +226,6 @@ def setup_axes(): # creates device objects for all PSUs and sets their values axis.ambient_field = g.AMBIENT_FIELD[i] i = i+1 - def activate_all(): # enables remote control and output on all PSUs and channels g.XY_DEVICE.enable_all() g.Z_DEVICE.enable_all() diff --git a/main.py b/main.py index f75a627..5b4e90f 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,10 @@ -import User_Interface as ui +from User_Interface import HelmholtzGUI import cage_func as func import traceback -import settings as g try: # start normal operations - # Connect to Arduino: - g.ARDUINO = func.ArduinoCtrl(g.RELAY_PINS) - print("Connecting to PSUs...") + print("Starting setup...") func.setup_axes() # initiate communication, set handles print("\nOpening User Interface...") @@ -16,7 +13,7 @@ try: # start normal operations #g.TestValuesZ = ui.TestValues() g.TestValues = [g.TestValuesX, g.TestValuesY]#, g.TestValuesZ]''' - application = ui.HelmholtzGUI() + application = HelmholtzGUI() application.mainloop() except BaseException as e: # if there is an error, print what happened diff --git a/settings.py b/settings.py index 4ef4052..5bf5932 100644 --- a/settings.py +++ b/settings.py @@ -12,24 +12,19 @@ AXES = None # list containing [X_AXIS, Y_AXIS, Z_AXIS] # Constants: COIL_CONST = np.array([38.6, 38.45, 37.9]) * 1e-6 # Coil constants [x,y,z] in T/A AMBIENT_FIELD = np.array([80, 80, 80]) * 1e-6 # ambient magnetic field in measurement area, to be cancelled out -RESISTANCES = np.array([4.5, 8, 1]) # resistance of [x,y,z] circuits -MAX_WATTS = np.array([8, 25, 0]) # max. allowed power for [x,y,z] circuits +RESISTANCES = np.array([4.5, 8, 4]) # resistance of [x,y,z] circuits +MAX_WATTS = np.array([8, 25, 8]) # max. allowed power for [x,y,z] circuits MAX_VOLTS = [16, 16, 16] # max. allowed voltage, limited to 16V by used diodes! # COM-Ports for power supply units: -XY_PORT = "COM7" # placeholders +XY_PORT = "COM10" # placeholders Z_PORT = "COM11" AXIS_NAMES = ["X-Axis", "Y-Axis", "Z-Axis"] ports = [XY_PORT, XY_PORT, Z_PORT] -global ARDUINO +ARDUINO = None RELAY_PINS = [15, 16, 17] # pin on the Arduino for switching relay of each axis [x,y,z] # ToDo: make proper settings file to read from and write to - -global TestValuesX -global TestValuesY -global TestValuesZ -global TestValues From 4c7b9edd94c6d2646162d23b6ece2b37789cd60c Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Wed, 27 Jan 2021 13:45:15 +0100 Subject: [PATCH 02/36] added output console to ui --- User_Interface.py | 67 ++++++++++++++++++++++++++++++++++------------- cage_func.py | 11 ++++++++ main.py | 11 ++++---- settings.py | 2 ++ 4 files changed, 67 insertions(+), 24 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index dbcbe75..f3c8415 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -32,8 +32,16 @@ class HelmholtzGUI(Tk): self.frames[F] = frame frame.grid(row=0, column=0, sticky="nsew") - self.StatusDisplay = StatusDisplay(self, self) - self.StatusDisplay.pack(side="bottom", fill="x", expand=False) + status_frame = Frame(self) + status_frame.pack(side="bottom", fill="x", expand=False) + status_frame.grid_rowconfigure(ALL, weight=1) + status_frame.grid_columnconfigure(1, weight=1) + + self.StatusDisplay = StatusDisplay(status_frame, self) + self.StatusDisplay.grid(row=0, column=0, sticky="nesw") + + self.OutputConsole = OutputConsole(status_frame) + self.OutputConsole.grid(row=0, column=1, sticky="nesw") self.show_frame(ManualMode) @@ -66,7 +74,7 @@ class TestFrame(Frame): # ToDo: remove one.pack(fill=X) two = Label(self, text="Two", bg="blue") two.pack() - button = ttk.Button(self, text="Print stuff", command=lambda: print("Hello")) + button = ttk.Button(self, text="Print stuff", command=lambda: func.ui_print("Hello")) button.pack() @@ -130,14 +138,26 @@ class ManualMode(Frame): row_counter = row_counter + 1 # Setup execute button - Label(self, text="").grid(row=row_counter, column=0) # add spacer - row_counter = row_counter + 1 - execute_button = Button(self, text="Execute!", command=self.execute, - pady=5, padx=5, font=BIG_BUTTON_FONT) - execute_button.grid(row=row_counter, column=0, columnspan=2) + 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=row_counter, column=0) # add spacer + + 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) + + # 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) - # Add spacer to Frame below row_counter = row_counter + 1 + # Add spacer to Frame below + Label(self, text="", pady=10).grid(row=row_counter, column=0) self.input_mode.trace_add('write', self.change_mode_callback) # call mode change function on dropdown change @@ -158,19 +178,19 @@ class ManualMode(Frame): @staticmethod def execute_field(vector): - print("field executing", vector) + func.ui_print("field executing", vector) try: func.set_field_simple(vector*1e-6) # ToDo: change to set_field except ValueError as e: - print(e) + func.ui_print(e) @staticmethod def execute_current(vector): - print("current executing:", vector) + func.ui_print("current executing:", vector) try: func.set_current_vec(vector) except ValueError as e: - print(e) + func.ui_print(e) class StatusDisplay(Frame): @@ -219,11 +239,6 @@ class StatusDisplay(Frame): col = col + 1 # rowCounter = rowCounter + self.rowNo # increase row counter to place future stuff below this - # add button for reinitialization - reinit_button = Button(self, text="Reinitialize", command=func.setup_axes, - pady=5, padx=5, font=BIG_BUTTON_FONT) - reinit_button.grid(row=0, column=self.columnNo+1, rowspan=3) - self.update_labels(controller) def update_labels(self, controller): @@ -249,3 +264,19 @@ class StatusDisplay(Frame): self.label_dict["Inverted:"][i].set(axis.polarity_switched) i = i + 1 controller.after(500, lambda: self.update_labels(controller)) + + +class OutputConsole(Frame): + + def __init__(self, parent): + Frame.__init__(self, parent, relief=SUNKEN, bd=1) + + self.grid_rowconfigure(ALL, weight=1) + self.grid_columnconfigure(0, weight=1, minsize=60) + + scrollbar = Scrollbar(self) + self.console = Text(self) + scrollbar.grid(row=0, column=1, sticky="ns") + self.console.grid(row=0, column=0, sticky="nesw") + scrollbar.config(command=self.console.yview) + self.console.config(yscrollcommand=scrollbar.set) diff --git a/cage_func.py b/cage_func.py index ab5651f..53b7a42 100644 --- a/cage_func.py +++ b/cage_func.py @@ -6,6 +6,7 @@ import time import numpy as np import serial import traceback # ToDo: remove +from tkinter import * class Axis: @@ -167,6 +168,16 @@ class ArduinoCtrl(Arduino): self.digitalWrite(pin, "LOW") +def ui_print(*content): # prints text to built in console + output = "" + for text in content: + output = " ".join((output, str(text))) + if g.app is not None: + g.app.OutputConsole.console.insert(END, output) + else: + print(output) + + def setup_axes(): # creates device objects for all PSUs and sets their values # Connect to Arduino: try: diff --git a/main.py b/main.py index 5b4e90f..d39d5e3 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ from User_Interface import HelmholtzGUI import cage_func as func import traceback +import settings as g try: # start normal operations @@ -8,13 +9,11 @@ try: # start normal operations func.setup_axes() # initiate communication, set handles print("\nOpening User Interface...") - '''g.TestValuesX = ui.TestValues() - g.TestValuesY = ui.TestValues() - #g.TestValuesZ = ui.TestValues() - g.TestValues = [g.TestValuesX, g.TestValuesY]#, g.TestValuesZ]''' - application = HelmholtzGUI() - application.mainloop() + g.app = HelmholtzGUI() + g.app.mainloop() + g.app = None # reset to None so nothing tries to print in the UI output + except BaseException as e: # if there is an error, print what happened print("\nAn error occurred, Shutting down.") diff --git a/settings.py b/settings.py index 5bf5932..10b3d61 100644 --- a/settings.py +++ b/settings.py @@ -27,4 +27,6 @@ ARDUINO = None RELAY_PINS = [15, 16, 17] # pin on the Arduino for switching relay of each axis [x,y,z] +app = None + # ToDo: make proper settings file to read from and write to From 89a8a593777a4e13175a5d2fa70f8478f4c8c8e4 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Wed, 27 Jan 2021 14:11:30 +0100 Subject: [PATCH 03/36] changed all print() to ui_print() --- User_Interface.py | 15 +----- cage_func.py | 123 +++++++++++++++++++++++++--------------------- main.py | 6 +-- 3 files changed, 72 insertions(+), 72 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index f3c8415..19a8dec 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -27,7 +27,7 @@ class HelmholtzGUI(Tk): self.frames = {} # dictionary for storing all pages - for F in [TestFrame, ManualMode]: + for F in [ManualMode]: frame = F(mainArea, self) self.frames[F] = frame frame.grid(row=0, column=0, sticky="nsew") @@ -65,19 +65,6 @@ class TopMenu: window.show_frame(ManualMode) -class TestFrame(Frame): # ToDo: remove - - def __init__(self, parent, controller): - Frame.__init__(self, parent) - - one = Label(self, text="One", bg="red") - one.pack(fill=X) - two = Label(self, text="Two", bg="blue") - two.pack() - button = ttk.Button(self, text="Print stuff", command=lambda: func.ui_print("Hello")) - button.pack() - - class ManualMode(Frame): # ToDo: Display maximum values # ToDo: Add option to cancel ambient field diff --git a/cage_func.py b/cage_func.py index 53b7a42..39083b8 100644 --- a/cage_func.py +++ b/cage_func.py @@ -54,17 +54,19 @@ class Axis: device_status = self.device.get_device_status_information(self.channel) if device_status.output_active: self.output_active = "Active" - else: self.output_active = "Inactive" + else: + self.output_active = "Inactive" if device_status.remote_control_active: self.remote_ctrl_active = "Active" - else: self.remote_ctrl_active = "Inactive" + else: + self.remote_ctrl_active = "Inactive" self.voltage = self.device.get_voltage(self.channel) self.voltage_setpoint = self.device.get_voltage_setpoint(self.channel) self.current = self.device.get_current(self.channel) self.current_setpoint = self.device.get_current_setpoint(self.channel) except (serial.serialutil.SerialException, IndexError): - # print("Connection Error with %s PSU on %s" % (self.name, self.port)) + # ui_print("Connection Error with %s PSU on %s" % (self.name, self.port)) self.connected = "Connection Error" self.output_active = "Unknown" self.remote_ctrl_active = "Unknown" @@ -72,9 +74,9 @@ class Axis: self.connected = "Connected" def print_status(self): # axis = axis control variable, stored in settings.py - print("%s, %0.2f V, %0.2f A" - % (self.device.get_device_status_information(self.channel), - self.device.get_voltage(self.channel), self.device.get_current(self.channel))) + ui_print("%s, %0.2f V, %0.2f A" + % (self.device.get_device_status_information(self.channel), + self.device.get_voltage(self.channel), self.device.get_current(self.channel))) def power_down(self): # temporary powerdown, set outputs to 0 but keep connections enabled try: @@ -87,13 +89,13 @@ class Axis: self.device.disable_output(self.channel) g.ARDUINO.digitalWrite(self.ardPin, "LOW") except Exception as e: - print(e) # ToDo: more error handling here + ui_print(e) # ToDo: more error handling here def set_signed_current(self, value): # sets current with correct polarity on this axis device = self.device channel = self.channel ardPin = self.ardPin - # print("Attempting to set current", value, "A") + # ui_print("Attempting to set current", value, "A") self.target_current = value if self.connected == "Connected" or True: # ToDo!: remove True, only for arduino testing! if abs(value) > self.max_amps: # prevent excessive currents @@ -106,13 +108,13 @@ class Axis: else: raise Exception("This should be impossible.") maxVoltage = min(max(1.1 * self.max_amps * self.resistance, 8), self.max_volts) # limit voltage# - # print("sending values to device: U =", maxVoltage, "I =", abs(value)) + # ui_print("sending values to device: U =", maxVoltage, "I =", abs(value)) if self.connected == "Connected": # ToDo!: remove if, only for arduino testing! device.set_current(abs(value), channel) device.set_voltage(maxVoltage, channel) device.enable_output(channel) else: - print(self.name, "not connected, can't set current.") + ui_print(self.name, "not connected, can't set current.") def set_field_simple(self, value): # forms magnetic field as specified by value, w/o cancelling ambient field self.target_field = value @@ -133,19 +135,18 @@ class ArduinoCtrl(Arduino): def __init__(self, pins): self.connected = "Unknown" self.pins = pins - print("\nConnecting to Arduino...") + ui_print("\nConnecting to Arduino...") try: Arduino.__init__(self) # search for connected arduino and connect for pin in self.pins: self.pinMode(pin, "Output") self.digitalWrite(pin, "LOW") except Exception as e: - print("Connection to Arduino failed.") - print(e) + ui_print("Connection to Arduino failed:", e) self.connected = "Not Connected" else: self.connected = "Connected" - print("Arduino ready.") + ui_print("Arduino ready.") def update_status_info(self): if self.connected == "Connected": @@ -156,7 +157,7 @@ class ArduinoCtrl(Arduino): else: axis.polarity_switched = "False" except Exception as e: - print("Error with Arduino:", e) + ui_print("Error with Arduino:", e) for axis in g.AXES: axis.polarity_switched = "Unknown" self.connected = "Connection Error" @@ -173,8 +174,10 @@ def ui_print(*content): # prints text to built in console for text in content: output = " ".join((output, str(text))) if g.app is not None: - g.app.OutputConsole.console.insert(END, output) - else: + output = "".join(("\n", output)) # begin new line each time + g.app.OutputConsole.console.insert(END, output) # print to console + g.app.OutputConsole.console.see(END) # scroll to bottom + else: # if window is not open, do normal print print(output) @@ -182,60 +185,66 @@ def setup_axes(): # creates device objects for all PSUs and sets their values # Connect to Arduino: try: if g.ARDUINO is not None: - # print("\nClosing arduino link") + # ui_print("\nClosing arduino link") try: g.ARDUINO.close() # close serial link before attempting reconnection except serial.serialutil.SerialException: - pass + pass # serial.flush() in Arduino.close() fails when reconnecting # this ignores it and allows serial.close() to execute (I think) + except AttributeError: + 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(g.RELAY_PINS) except Exception as e: - print("Arduino setup failed:", e) - print(traceback.print_exc()) + ui_print("Arduino setup failed:", e) + ui_print(traceback.print_exc()) g.AXES = [] - print("\nConnecting to XY Device on %s..." % g.XY_PORT) + ui_print("\nConnecting to XY Device on %s..." % g.XY_PORT) try: if g.XY_DEVICE is not None: - print("closing serial connection on XY device") + ui_print("closing serial connection on XY device") g.XY_DEVICE.serial.close() g.XY_DEVICE = None g.XY_DEVICE = PS2000B.PS2000B(g.XY_PORT) # setup PSU - print("Connection established.") + ui_print("Connection established.") g.X_AXIS = Axis(0, g.XY_DEVICE, 0, g.ARDUINO.pins[0]) # create axis objects g.Y_AXIS = Axis(1, g.XY_DEVICE, 1, g.ARDUINO.pins[1]) except serial.serialutil.SerialException: g.X_AXIS = Axis(0, None, 0, g.ARDUINO.pins[0]) # create axis objects g.Y_AXIS = Axis(1, None, 1, g.ARDUINO.pins[1]) - print("XY Device not connected or incorrect port set.") + ui_print("XY Device not connected or incorrect port set.") - print("Connecting to Z Device on %s..." % g.XY_PORT) + ui_print("Connecting to Z Device on %s..." % g.XY_PORT) try: g.Z_DEVICE = PS2000B.PS2000B(g.Z_PORT) - print("Connection established.") + ui_print("Connection established.") g.Z_AXIS = Axis(2, g.Z_DEVICE, 0, g.ARDUINO.pins[2]) except serial.serialutil.SerialException: g.Z_AXIS = Axis(2, None, 0, g.ARDUINO.pins[2]) - print("Z Device not connected or incorrect port set.") + ui_print("Z Device not connected or incorrect port set.") g.AXES.append(g.X_AXIS) g.AXES.append(g.Y_AXIS) g.AXES.append(g.Z_AXIS) - print("") # new line + ui_print("") # new line i = 0 for axis in g.AXES: # ToDo: move to axis init axis.resistance = g.RESISTANCES[i] axis.max_watts = g.MAX_WATTS[i] axis.max_amps = np.sqrt(axis.max_watts / axis.resistance) - print(axis.name, "max Current:", axis.max_amps) + ui_print(axis.name, "max Current:", axis.max_amps) axis.max_volts = g.MAX_VOLTS[i] axis.coil_constant = g.COIL_CONST[i] axis.ambient_field = g.AMBIENT_FIELD[i] - i = i+1 + i = i + 1 + ui_print("") + def activate_all(): # enables remote control and output on all PSUs and channels g.XY_DEVICE.enable_all() @@ -247,23 +256,23 @@ def deactivate_all(): # disables remote control and output on all PSUs and chan try: g.XY_DEVICE.disable_all() except BaseException: - print("XY PSU deactivation unsuccessful.") + ui_print("XY PSU deactivation unsuccessful.") else: - print("XY PSU deactivated.") + ui_print("XY PSU deactivated.") try: g.Z_DEVICE.disable_all() except BaseException: - print("Z PSU deactivation unsuccessful.") + ui_print("Z PSU deactivation unsuccessful.") else: - print("Z PSU deactivated.") + ui_print("Z PSU deactivated.") def print_status_3(): - print("X-Axis:") + ui_print("X-Axis:") g.X_AXIS.print_status() - print("Y-Axis:") + ui_print("Y-Axis:") g.Y_AXIS.print_status() - print("Z-Axis:") + ui_print("Z-Axis:") g.Z_AXIS.print_status() @@ -282,28 +291,32 @@ def power_down_all(): # temporary, set all outputs to 0 but keep connections en def shut_down_all(): # shutdown at program end or on error, set outputs to 0 and disable connections # ToDo: better messages, check if things are connected first - print("\nAttempting to safely shut down all devices. Check equipment to confirm.") - try: set_to_zero(g.XY_DEVICE) + ui_print("\nAttempting to safely shut down all devices. Check equipment to confirm.") + try: + set_to_zero(g.XY_DEVICE) except: - print("XY PSU set to 0 unsuccessful.") + ui_print("XY PSU set to 0 unsuccessful.") else: - print("XY PSU currents and voltages set to 0.") - try: set_to_zero(g.Z_DEVICE) + ui_print("XY PSU currents and voltages set to 0.") + try: + set_to_zero(g.Z_DEVICE) except: - print("Z PSU set to 0 unsuccessful.") + ui_print("Z PSU set to 0 unsuccessful.") else: - print("Z PSU currents and voltages set to 0.") + ui_print("Z PSU currents and voltages set to 0.") deactivate_all() - try: g.ARDUINO.safe() + try: + g.ARDUINO.safe() except: - print("Arduino safing unsuccessful.") + ui_print("Arduino safing unsuccessful.") # else: # commented out bc this throws no exception, even when arduino is not connected - # print("Arduino pins set to LOW.") # ToDo: figure out error handling for this - try: g.ARDUINO.close() + # ui_print("Arduino pins set to LOW.") # ToDo: figure out error handling for this + try: + g.ARDUINO.close() except: - print("Closing Arduino connection failed.") + ui_print("Closing Arduino connection failed.") else: - print("Serial connection to Arduino closed.") + ui_print("Serial connection to Arduino closed.") def set_field_simple(vector): # forms magnetic field as specified by vector, w/o cancelling ambient field @@ -326,22 +339,22 @@ def set_current_vec(vector): # sets needed currents on each axis for given vect def execute_csv(filepath, printing=0): # runs through csv file containing times and desired field vectors # csv format: time (s); xField (T); yField (T); zField (T) # decimal commas - print("Reading File:", filepath) + ui_print("Reading File:", filepath) file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file array = file.to_numpy() # convert csv to array t_zero = time.time() t_ref = t_zero i = 0 - print("Starting Execution...") + ui_print("Starting Execution...") while i < len(array): t = time.time() - t_zero if t >= array[i, 0]: field_vec = array[i, 1:4] - print("t = %0.2f s, target field vector = " % (array[i, 0]), field_vec) + ui_print("t = %0.2f s, target field vector = " % (array[i, 0]), field_vec) set_field(field_vec) i = i + 1 if t - t_ref >= 1 and printing == 1: # print status every second print_status_3() t_ref = t - print("File executed, powering down channels.") + ui_print("File executed, powering down channels.") power_down_all() # set currents and voltages to 0, set arduino pins to low diff --git a/main.py b/main.py index d39d5e3..9d23a46 100644 --- a/main.py +++ b/main.py @@ -16,9 +16,9 @@ try: # start normal operations except BaseException as e: # if there is an error, print what happened - print("\nAn error occurred, Shutting down.") - print(e) - print(traceback.print_exc()) + func.ui_print("\nAn error occurred, Shutting down.") + func.ui_print(e) + func.ui_print(traceback.print_exc()) finally: # safely shut everything down at the end func.shut_down_all() From 5f23c71bfffac38dad06d90f2c406ea588cf68d8 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Thu, 28 Jan 2021 12:23:56 +0100 Subject: [PATCH 04/36] various changes - added checkbox for ambient field compensation - moved part of axis setup to axis.__init__ --- User_Interface.py | 58 ++++++++++++++++++++++++++++++++++++++++------- cage_func.py | 34 ++++++++++----------------- main.py | 3 +++ settings.py | 6 ++--- 4 files changed, 68 insertions(+), 33 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 19a8dec..d48af3e 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -86,8 +86,8 @@ class ManualMode(Frame): self.input_mode = StringVar() # make dictionary with information on all modes. # content: [function to call on button press, unit text to be displayed] - self.modes = {"Magnetic Field": [self.execute_field, "\u03BCT"], - "Current": [self.execute_current, "A"]} + 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] @@ -107,10 +107,12 @@ class ManualMode(Frame): 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) + 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:"] - self.entry_vars = [StringVar() for _ in range(len(entry_texts))] + self.entry_vars = [StringVar() for _ in range(3)] + self.max_value_vars = [StringVar() for _ in range(3)] row = 0 for text in entry_texts: field = ttk.Entry(self.entries_frame, textvariable=self.entry_vars[row]) @@ -120,19 +122,33 @@ class ManualMode(Frame): axis_label.grid(row=row, column=0, sticky=W) unit_label = Label(self.entries_frame, textvariable=self.unit) unit_label.grid(row=row, column=2, sticky=W) + 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 = row_counter + 1 + row_counter += 1 - # Setup execute button + # setup checkbox for compensating ambient field + checkbox_frame = Frame(self, padx=20) + checkbox_frame.grid(row=row_counter, column=0, sticky=W) + + self.compensate = IntVar(value=1) + self.compensate_checkbox = Checkbutton(checkbox_frame, text="Compensate ambient field", + variable=self.compensate, onvalue=1, offvalue=0) + 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_columnconfigure(2, weight=1, minsize=20) self.buttons_frame.grid(row=row_counter, column=0) - Label(self.buttons_frame, text="").grid(row=row_counter, column=0) # add spacer + 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) @@ -149,9 +165,33 @@ class ManualMode(Frame): 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 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 + + def update_max_fields(self): # update labels with maximum allowable field values + self.compensate_checkbox.config(state=NORMAL) + i = 0 + for val in self.max_value_vars: + comp = self.compensate.get() + if comp == 0: + field = g.AXES[i].max_field * 1e6 + elif comp == 1: + field = g.AXES[i].max_comp_field * 1e6 + else: + field = [0, 0] + func.ui_print("Unexpected value encountered: compensate =", comp) + val.set("(%0.1f to %0.1f \u03BCT)" % (field[0], field[1])) + i += 1 + + def update_max_currents(self): # update labels with maximum allowable current values + self.compensate_checkbox.config(state=DISABLED) + i = 0 + for val in self.max_value_vars: + val.set("(%0.2f to %0.2f A)" % (-g.AXES[i].max_amps, g.AXES[i].max_amps)) + i += 1 def execute(self): function_to_call = self.modes[self.input_mode.get()][0] # get function of appropriate mode @@ -167,7 +207,7 @@ class ManualMode(Frame): def execute_field(vector): func.ui_print("field executing", vector) try: - func.set_field_simple(vector*1e-6) # ToDo: change to set_field + func.set_field_simple(vector * 1e-6) # ToDo: change to set_field except ValueError as e: func.ui_print(e) @@ -253,7 +293,7 @@ class StatusDisplay(Frame): controller.after(500, lambda: self.update_labels(controller)) -class OutputConsole(Frame): +class OutputConsole(Frame): # console to print stuff in def __init__(self, parent): Frame.__init__(self, parent, relief=SUNKEN, bd=1) @@ -263,6 +303,8 @@ class OutputConsole(Frame): scrollbar = Scrollbar(self) self.console = Text(self) + self.console.bind("", lambda e: "break") # prevent user input into the console + scrollbar.grid(row=0, column=1, sticky="ns") self.console.grid(row=0, column=0, sticky="nesw") scrollbar.config(command=self.console.yview) diff --git a/cage_func.py b/cage_func.py index 39083b8..51846f2 100644 --- a/cage_func.py +++ b/cage_func.py @@ -20,15 +20,17 @@ class Axis: self.name = g.AXIS_NAMES[index] self.port = g.ports[index] - self.resistance = 0 # [Ohm] - # maximum allowable values to pass through this circuit - self.max_watts = 0 # [W] - self.max_amps = 0 # [A] - self.max_volts = 0 # [V] + self.resistance = g.RESISTANCES[index] + self.max_watts = g.MAX_WATTS[index] + self.max_amps = np.sqrt(self.max_watts / self.resistance) + self.max_volts = g.MAX_VOLTS[index] - self.coil_constant = 0 # coil constant of this axis [T/A] - self.ambient_field = 0 # ambient field in this axis [T] - # ToDo: get this info from settings file + self.coil_constant = g.COIL_CONST[index] + self.ambient_field = g.AMBIENT_FIELD[index] + + max_field = self.max_amps * self.coil_constant # max field reachable in this axis + self.max_field = np.array([-max_field, max_field]) + self.max_comp_field = np.array([self.ambient_field - max_field, self.ambient_field + max_field]) # dynamic information self.connected = "Not Connected" @@ -142,7 +144,7 @@ class ArduinoCtrl(Arduino): self.pinMode(pin, "Output") self.digitalWrite(pin, "LOW") except Exception as e: - ui_print("Connection to Arduino failed:", e) + ui_print("Connection to Arduino failed.", e) self.connected = "Not Connected" else: self.connected = "Connected" @@ -172,7 +174,7 @@ class ArduinoCtrl(Arduino): def ui_print(*content): # prints text to built in console output = "" for text in content: - output = " ".join((output, str(text))) + output = " ".join((output, str(text))) # append content if g.app is not None: output = "".join(("\n", output)) # begin new line each time g.app.OutputConsole.console.insert(END, output) # print to console @@ -232,18 +234,6 @@ def setup_axes(): # creates device objects for all PSUs and sets their values g.AXES.append(g.Z_AXIS) ui_print("") # new line - i = 0 - for axis in g.AXES: # ToDo: move to axis init - axis.resistance = g.RESISTANCES[i] - axis.max_watts = g.MAX_WATTS[i] - axis.max_amps = np.sqrt(axis.max_watts / axis.resistance) - ui_print(axis.name, "max Current:", axis.max_amps) - axis.max_volts = g.MAX_VOLTS[i] - - axis.coil_constant = g.COIL_CONST[i] - axis.ambient_field = g.AMBIENT_FIELD[i] - i = i + 1 - ui_print("") def activate_all(): # enables remote control and output on all PSUs and channels diff --git a/main.py b/main.py index 9d23a46..14b8d50 100644 --- a/main.py +++ b/main.py @@ -11,6 +11,9 @@ try: # start normal operations print("\nOpening User Interface...") g.app = HelmholtzGUI() + func.ui_print("Program Initialized") + func.ui_print("Starting setup...") + func.setup_axes() # initiate communication, set handles g.app.mainloop() g.app = None # reset to None so nothing tries to print in the UI output diff --git a/settings.py b/settings.py index 10b3d61..fd6ae92 100644 --- a/settings.py +++ b/settings.py @@ -11,9 +11,9 @@ AXES = None # list containing [X_AXIS, Y_AXIS, Z_AXIS] # Constants: COIL_CONST = np.array([38.6, 38.45, 37.9]) * 1e-6 # Coil constants [x,y,z] in T/A -AMBIENT_FIELD = np.array([80, 80, 80]) * 1e-6 # ambient magnetic field in measurement area, to be cancelled out -RESISTANCES = np.array([4.5, 8, 4]) # resistance of [x,y,z] circuits -MAX_WATTS = np.array([8, 25, 8]) # max. allowed power for [x,y,z] circuits +AMBIENT_FIELD = np.array([30, 30, 30]) * 1e-6 # ambient magnetic field in measurement area, to be cancelled out +RESISTANCES = np.array([1.7, 1.7, 1.7]) # resistance of [x,y,z] circuits +MAX_WATTS = np.array([15, 15, 15]) # max. allowed power for [x,y,z] circuits MAX_VOLTS = [16, 16, 16] # max. allowed voltage, limited to 16V by used diodes! # COM-Ports for power supply units: From c0fb71f73f2c1e600d648cd0884081c7a86535d1 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Thu, 28 Jan 2021 12:50:13 +0100 Subject: [PATCH 05/36] started on config file renamed settings.py to globals.py --- One_Unit_Test.py | 2 +- User_Interface.py | 2 +- cage_func.py | 4 ++-- settings.py => globals.py | 29 +++++++++++++++++++++++------ main.py | 10 +++++++++- main_old.py | 2 +- 6 files changed, 37 insertions(+), 12 deletions(-) rename settings.py => globals.py (68%) diff --git a/One_Unit_Test.py b/One_Unit_Test.py index 99d492a..a9420bd 100644 --- a/One_Unit_Test.py +++ b/One_Unit_Test.py @@ -1,7 +1,7 @@ # import platform import time as t import numpy as np -import settings as g +import globals as g import cage_func as func from pyps2000b import PS2000B diff --git a/User_Interface.py b/User_Interface.py index d48af3e..0f68de0 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -1,6 +1,6 @@ from tkinter import * from tkinter import ttk -import settings as g +import globals as g import cage_func as func import numpy as np diff --git a/cage_func.py b/cage_func.py index 51846f2..4e88973 100644 --- a/cage_func.py +++ b/cage_func.py @@ -1,6 +1,6 @@ from pyps2000b import PS2000B from Arduino import Arduino -import settings as g +import globals as g import pandas import time import numpy as np @@ -75,7 +75,7 @@ class Axis: else: self.connected = "Connected" - def print_status(self): # axis = axis control variable, stored in settings.py + def print_status(self): # axis = axis control variable, stored in globals.py ui_print("%s, %0.2f V, %0.2f A" % (self.device.get_device_status_information(self.channel), self.device.get_voltage(self.channel), self.device.get_current(self.channel))) diff --git a/settings.py b/globals.py similarity index 68% rename from settings.py rename to globals.py index fd6ae92..bec9393 100644 --- a/settings.py +++ b/globals.py @@ -1,7 +1,10 @@ import numpy as np +from configparser import ConfigParser +# global variables set in other files XY_DEVICE = None Z_DEVICE = None +ARDUINO = None X_AXIS = None # object structure: (device, channel, arduino pin, axis index) Y_AXIS = None @@ -9,6 +12,26 @@ Z_AXIS = None AXES = None # list containing [X_AXIS, Y_AXIS, Z_AXIS] +app = None + + +def read_config(file): # attempt to read config file + config_object = ConfigParser() + config_object.read(file) + print(config_object["X-Axis"]["coil_consthhh"]) + + +def create_config(file): + config = ConfigParser() + + config["X-Axis"] = { + "coil_const": str(38.6 * 1e-6) + } + + with open(file, 'w') as conf: + config.write(conf) + + # Constants: COIL_CONST = np.array([38.6, 38.45, 37.9]) * 1e-6 # Coil constants [x,y,z] in T/A AMBIENT_FIELD = np.array([30, 30, 30]) * 1e-6 # ambient magnetic field in measurement area, to be cancelled out @@ -23,10 +46,4 @@ Z_PORT = "COM11" AXIS_NAMES = ["X-Axis", "Y-Axis", "Z-Axis"] ports = [XY_PORT, XY_PORT, Z_PORT] -ARDUINO = None - RELAY_PINS = [15, 16, 17] # pin on the Arduino for switching relay of each axis [x,y,z] - -app = None - -# ToDo: make proper settings file to read from and write to diff --git a/main.py b/main.py index 14b8d50..8547c2f 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,18 @@ from User_Interface import HelmholtzGUI import cage_func as func import traceback -import settings as g +import globals as g try: # start normal operations + print("Reading config file...") + config_file = 'config.ini' + try: + g.read_config(config_file) + except KeyError: + print("Error when reading config file, creating new.") + g.create_config(config_file) + print("Starting setup...") func.setup_axes() # initiate communication, set handles diff --git a/main_old.py b/main_old.py index 1f5f244..df82b87 100644 --- a/main_old.py +++ b/main_old.py @@ -1,5 +1,5 @@ import numpy as np -import settings as g +import globals as g import cage_func as func # from pyps2000b import PS2000B from Arduino import Arduino From 8cf4961d99fc79e7b5c6c20ea6159455b712d610 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Thu, 28 Jan 2021 17:28:38 +0100 Subject: [PATCH 06/36] implemented config file read and default write --- .gitignore | 1 + User_Interface.py | 2 +- cage_func.py | 51 ++++++++++++++++++++++++++++++++++------ globals.py | 59 ++++++++++++++++++++++------------------------- main.py | 13 +++++------ main_old.py | 36 ----------------------------- 6 files changed, 79 insertions(+), 83 deletions(-) delete mode 100644 main_old.py diff --git a/.gitignore b/.gitignore index 9c4f096..7395fe1 100644 --- a/.gitignore +++ b/.gitignore @@ -100,3 +100,4 @@ ENV/ .idea/Python-PS2000B.iml .idea/Python-PS2000B.iml .idea/misc.xml +config.ini diff --git a/User_Interface.py b/User_Interface.py index 0f68de0..cbc57e6 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -275,7 +275,7 @@ class StatusDisplay(Frame): for axis in g.AXES: if axis.device is not None: axis.update_status_info() - self.label_dict["PSU Serial Port:"][i].set(g.ports[i]) + 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 diff --git a/cage_func.py b/cage_func.py index 4e88973..f23105b 100644 --- a/cage_func.py +++ b/cage_func.py @@ -7,6 +7,7 @@ import numpy as np import serial import traceback # ToDo: remove from tkinter import * +from configparser import ConfigParser class Axis: @@ -18,15 +19,15 @@ class Axis: self.ardPin = arduino_pin # output pin on the arduino for switching polarity on this axis self.name = g.AXIS_NAMES[index] - self.port = g.ports[index] + self.port = g.PORTS[index] - self.resistance = g.RESISTANCES[index] - self.max_watts = g.MAX_WATTS[index] + self.resistance = float(read_config(self.name, "resistance")) + self.max_watts = float(read_config(self.name, "max_watts")) self.max_amps = np.sqrt(self.max_watts / self.resistance) - self.max_volts = g.MAX_VOLTS[index] + self.max_volts = float(read_config(self.name, "max_volts")) - self.coil_constant = g.COIL_CONST[index] - self.ambient_field = g.AMBIENT_FIELD[index] + self.coil_constant = float(read_config(self.name, "coil_const")) + self.ambient_field = float(read_config(self.name, "ambient_field")) max_field = self.max_amps * self.coil_constant # max field reachable in this axis self.max_field = np.array([-max_field, max_field]) @@ -171,6 +172,37 @@ class ArduinoCtrl(Arduino): self.digitalWrite(pin, "LOW") +def read_config(section, key): # attempt to read config file + # ToDo (optional) better error handling + file = g.CONFIG_FILE + config_object = ConfigParser() + try: + config_object.read(file) + section_obj = config_object[section] + return section_obj[key] + except KeyError as e: + ui_print("Error while reading config file: e") + raise KeyError("Could not find key", key, "in config file.") + + +def create_default_config(file): + config = ConfigParser() + + i = 0 + for axis_name in g.AXIS_NAMES: + config.add_section(axis_name) + for key in g.default_arrays.keys(): + config.set(axis_name, key, str(g.default_arrays[key][i])) + i += 1 + + config.add_section("PORTS") + for key in g.defaults.keys(): + config.set("PORTS", key, str(g.defaults[key])) + + with open(file, 'w') as conf: + config.write(conf) + + def ui_print(*content): # prints text to built in console output = "" for text in content: @@ -185,6 +217,7 @@ 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") @@ -198,13 +231,17 @@ 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(g.RELAY_PINS) + g.ARDUINO = ArduinoCtrl(arduino_pins) except Exception as e: ui_print("Arduino setup failed:", e) ui_print(traceback.print_exc()) g.AXES = [] + g.XY_PORT = read_config("PORTS", "xy_port") + g.Z_PORT = read_config("PORTS", "z_port") + g.PORTS = [g.XY_PORT, g.XY_PORT, g.Z_PORT] + ui_print("\nConnecting to XY Device on %s..." % g.XY_PORT) try: if g.XY_DEVICE is not None: diff --git a/globals.py b/globals.py index bec9393..173f7c0 100644 --- a/globals.py +++ b/globals.py @@ -1,5 +1,4 @@ import numpy as np -from configparser import ConfigParser # global variables set in other files XY_DEVICE = None @@ -14,36 +13,32 @@ AXES = None # list containing [X_AXIS, Y_AXIS, Z_AXIS] app = None - -def read_config(file): # attempt to read config file - config_object = ConfigParser() - config_object.read(file) - print(config_object["X-Axis"]["coil_consthhh"]) - - -def create_config(file): - config = ConfigParser() - - config["X-Axis"] = { - "coil_const": str(38.6 * 1e-6) - } - - with open(file, 'w') as conf: - config.write(conf) - - -# Constants: -COIL_CONST = np.array([38.6, 38.45, 37.9]) * 1e-6 # Coil constants [x,y,z] in T/A -AMBIENT_FIELD = np.array([30, 30, 30]) * 1e-6 # ambient magnetic field in measurement area, to be cancelled out -RESISTANCES = np.array([1.7, 1.7, 1.7]) # resistance of [x,y,z] circuits -MAX_WATTS = np.array([15, 15, 15]) # max. allowed power for [x,y,z] circuits -MAX_VOLTS = [16, 16, 16] # max. allowed voltage, limited to 16V by used diodes! - -# COM-Ports for power supply units: -XY_PORT = "COM10" # placeholders -Z_PORT = "COM11" - AXIS_NAMES = ["X-Axis", "Y-Axis", "Z-Axis"] -ports = [XY_PORT, XY_PORT, Z_PORT] -RELAY_PINS = [15, 16, 17] # pin on the Arduino for switching relay of each axis [x,y,z] +global CONFIG_FILE + +'''global COIL_CONST +global RESISTANCES +global MAX_WATTS +global MAX_VOLTS +global AMBIENT_FIELD''' + +global XY_PORT +global Z_PORT +global RELAY_PINS + +global PORTS + +# Default Constants: +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] + "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] +} +defaults = { + "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 +} diff --git a/main.py b/main.py index 8547c2f..f4a8002 100644 --- a/main.py +++ b/main.py @@ -2,16 +2,15 @@ from User_Interface import HelmholtzGUI import cage_func as func import traceback import globals as g +from os.path import exists try: # start normal operations - print("Reading config file...") - config_file = 'config.ini' - try: - g.read_config(config_file) - except KeyError: - print("Error when reading config file, creating new.") - g.create_config(config_file) + g.CONFIG_FILE = 'config.ini' + + if not exists(g.CONFIG_FILE): + print("Config file not found, creating new from defaults.") + func.create_default_config(g.CONFIG_FILE) print("Starting setup...") func.setup_axes() # initiate communication, set handles diff --git a/main_old.py b/main_old.py deleted file mode 100644 index df82b87..0000000 --- a/main_old.py +++ /dev/null @@ -1,36 +0,0 @@ -import numpy as np -import globals as g -import cage_func as func -# from pyps2000b import PS2000B -from Arduino import Arduino - -# User Inputs/Configuration---------------------------------- -# Desired output: -mag_vec1 = np.array([10, 10, 5])*1e-6 - -# Constants: -g.COIL_CONST = np.array([38.6, 38.45, 37.9]) * 1e-9 # Coil constants [x,y,z] in T/A -g.AMBIENT_FIELD = np.array([80]) * 1e-6 # ambient magnetic field in measurement area, to be cancelled out -g.RESISTANCES = np.array([3.9, 1, 1]) # resistance of [x,y,z] circuits -g.MAX_WATTS = np.array([8, 0, 0]) # max. allowed power for [x,y,z] circuits -g.MAX_VOLTS = 16 # max. allowed voltage, limited to 16V by used diodes! - -# COM-Ports for power supply units: -g.XY_PORT = "COM1" # placeholders -g.Z_PORT = "COM2" - -# Code starts here------------------------------------------ -g.MAX_AMPS = np.sqrt(g.MAX_WATTS / g.RESISTANCES) # calculate maximum currents in each axis - -print("Connecting to PSUs...") -func.setup_axes() # initiate communication, set handles -print("Connecting to Arduino...") -g.ARDUINO = Arduino() # search for connected arduino and set handle -print("Arduino found, configuring pins.") -func.setup_arduino() -print("Activating PSU outputs...") -func.activate_all() # activate remote control and outputs on PSUs - -func.set_field_simple(mag_vec1) - -func.deactivate_all() From 4dc296a6f0eaf285f08e7d48453a9c445f36b607 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Thu, 28 Jan 2021 17:38:15 +0100 Subject: [PATCH 07/36] added edit_config function --- cage_func.py | 23 ++++++++++++++++++----- globals.py | 6 ------ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/cage_func.py b/cage_func.py index f23105b..140ca09 100644 --- a/cage_func.py +++ b/cage_func.py @@ -172,20 +172,33 @@ class ArduinoCtrl(Arduino): self.digitalWrite(pin, "LOW") -def read_config(section, key): # attempt to read config file +def read_config(section, key): # read specific value from config file # ToDo (optional) better error handling - file = g.CONFIG_FILE config_object = ConfigParser() try: - config_object.read(file) + config_object.read(g.CONFIG_FILE) section_obj = config_object[section] return section_obj[key] except KeyError as e: - ui_print("Error while reading config file: e") + ui_print("Error while reading config file:", e) raise KeyError("Could not find key", key, "in config file.") -def create_default_config(file): +def edit_config(section, key, value): # edit specific value in config file + config_object = ConfigParser() + try: + config_object.read(g.CONFIG_FILE) + section_obj = config_object[section] + section_obj[key] = str(value) + + with open(g.CONFIG_FILE, 'w') as conf: # Write changes back to file + config_object.write(conf) + except KeyError as e: + ui_print("Error while editing config file:", e) + raise KeyError("Could not find key", key, "in config file.") + + +def create_default_config(file): # create config file from default values (stored in globals.py) config = ConfigParser() i = 0 diff --git a/globals.py b/globals.py index 173f7c0..bf6bf24 100644 --- a/globals.py +++ b/globals.py @@ -17,12 +17,6 @@ AXIS_NAMES = ["X-Axis", "Y-Axis", "Z-Axis"] global CONFIG_FILE -'''global COIL_CONST -global RESISTANCES -global MAX_WATTS -global MAX_VOLTS -global AMBIENT_FIELD''' - global XY_PORT global Z_PORT global RELAY_PINS From 27c804904b2a12d6272f43439bc4658e7fbed7ea Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Tue, 2 Feb 2021 13:11:27 +0100 Subject: [PATCH 08/36] 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 } From 2a2a538fd80a23baa088540720be16e9ecdb177b Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Tue, 2 Feb 2021 17:56:43 +0100 Subject: [PATCH 09/36] added protection against excessive values --- User_Interface.py | 35 ++++++++++++++++++++-------- cage_func.py | 58 +++++++++++++++++++++++++++++++++++------------ globals.py | 20 ++++++++++------ main.py | 2 +- 4 files changed, 83 insertions(+), 32 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index f85b305..1651e80 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -5,6 +5,7 @@ import cage_func as func import numpy as np NORM_FONT = () +HEADER_FONT = ("Arial", 13, "bold") SUB_HEADER_FONT = ("Arial", 9, "bold") BIG_BUTTON_FONT = ("Arial", 11, "bold") @@ -82,6 +83,11 @@ class ManualMode(Frame): row_counter = 0 + 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) dropdown_frame.grid_rowconfigure(ALL, weight=1) @@ -241,6 +247,11 @@ class Configuration(Frame): row_counter = 0 + 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 + # Serial port settings frame: port_frame = Frame(self) port_frame.grid_rowconfigure(ALL, weight=1) @@ -349,20 +360,24 @@ class Configuration(Frame): 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 + # ToDo: Warning messages if too high values are 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) + for key in self.entries.keys(): # go through rows of entry table + for i in [0, 1, 2]: # go through columns of entry table + try: + value = self.entries[key][0][i].get() # get value from field + factor = self.entries[key][4] # get unit conversion factor + if factor not in [0, 1]: # prevent conversion of int variables to float and div/0 + value = value / factor # do unit conversion + func.edit_config(g.AXIS_NAMES[i], self.entries[key][3], value) # write new value to config file + except TclError as e: + func.ui_print("Invalid entry for %s %s %s" % (g.AXIS_NAMES[i], key, e)) - func.setup_axes() + func.setup_axes() # reinitialize devices and program with new values + self.update_fields() # update entry fields to show new values class StatusDisplay(Frame): diff --git a/cage_func.py b/cage_func.py index 19c930d..69a5a81 100644 --- a/cage_func.py +++ b/cage_func.py @@ -175,29 +175,59 @@ class ArduinoCtrl(Arduino): def read_config(section, key): # read specific value from config file - # ToDo (optional) better error handling - config_object = ConfigParser() + # ToDo (optional): better error handling + # ToDo: make pop-up error message for excessive values that can be waived + config_object = ConfigParser() # initialize config parser try: - config_object.read(g.CONFIG_FILE) - section_obj = config_object[section] - return section_obj[key] + config_object.read(g.CONFIG_FILE) # open config file + section_obj = config_object[section] # get relevant section + value = section_obj[key] # get relevant value in the section + # Value checking: + if section in g.AXIS_NAMES: # only check numerical values + max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value + min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] + if float(value) > float(max_value): + ui_print("WARNING: Too high value for", section, key, "read from config file:", + value, "max.", max_value, "allowed. Excessive values may damage equipment!") + elif float(value) < float(min_value): + ui_print("WARNING: Too low value for", section, key, "read from config file:", + value, "max.", max_value, "allowed. Excessive values may damage equipment!") + return value except KeyError as e: ui_print("Error while reading config file:", e) raise KeyError("Could not find key", key, "in config file.") def edit_config(section, key, value): # edit specific value in config file - config_object = ConfigParser() + config_object = ConfigParser() # initialize config parser + # Value checking: + # ToDo: make pop-up warning messages that can be waived try: - config_object.read(g.CONFIG_FILE) - section_obj = config_object[section] - section_obj[key] = str(value) - - with open(g.CONFIG_FILE, 'w') as conf: # Write changes back to file - config_object.write(conf) + if section in g.AXIS_NAMES: # only check numerical values + max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value + min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] + if value > max_value: + raise ValueError("Attempted to write too high value for", section, key, "to config file:", + value, ", max.", max_value, "allowed. Excessive values may damage equipment!") + elif value < min_value: + raise ValueError("Attempted to write too low value for", section, key, "to config file:", + value, ", max.", max_value, "allowed. Excessive values may damage equipment!") except KeyError as e: ui_print("Error while editing config file:", e) - raise KeyError("Could not find key", key, "in config file.") + raise KeyError("Could not find section", section, "in config file.") + except ValueError as e: # value too high/low + ui_print(e) + else: # no errors so far + try: + config_object.read(g.CONFIG_FILE) # open config file + section_obj = config_object[section] # get relevant section + section_obj[key] = str(value) # get relevant value in the section + + with open(g.CONFIG_FILE, 'w') as conf: # Write changes back to file + config_object.write(conf) + except KeyError as e: + ui_print("Error while editing config file:", e) + raise KeyError("Could not find key", key, "in config file.") def create_default_config(file): # create config file from default values (stored in globals.py) @@ -207,7 +237,7 @@ def create_default_config(file): # create config file from default values (stor for axis_name in g.AXIS_NAMES: config.add_section(axis_name) for key in g.default_arrays.keys(): - config.set(axis_name, key, str(g.default_arrays[key][i])) + config.set(axis_name, key, str(g.default_arrays[key][0][i])) i += 1 config.add_section("PORTS") diff --git a/globals.py b/globals.py index af65349..26033da 100644 --- a/globals.py +++ b/globals.py @@ -22,16 +22,22 @@ global Z_PORT global PORTS -# Default Constants: +# Default Constants and maximum/minimum values (warning messages will be generated if these are exceeded) +# format: [[default values], [maximum values], [minimum values]] +# ToDo: check actual maximum ratings +# ToDo: Add maximum current: 5A (BA Blessing page 30), remove max_watts (there for testing with resistors) 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], 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 + "coil_const": np.array([[38.6, 38.45, 37.9], [50, 50, 50], [0, 0, 0]]) * 1e-6, # Coil constants [x,y,z] [T/A] + "ambient_field": np.array([[30, 30, 30], [200, 200, 200], [0, 0, 0]]) * 1e-6, # background magnetic field [T] + "resistance": np.array([[1.7, 1.7, 1.7], [5, 5, 5], [1, 1, 1]], dtype=float), # resistance of circuits [Ohm] + "max_watts": np.array([[15, 15, 15], [50, 50, 50], [0, 0, 0]], dtype=float), # max. allowed power for circuits [W] + "max_volts": np.array([[14, 14, 14], [16, 16, 16], [0, 0, 0]], dtype=float), # max. allowed voltage, limited to 16V by used diodes! [V] + "relay_pin": [[15, 16, 17], [15, 16, 17], [15, 16, 17]] # pins on the arduino for reversing [x,y,z] polarity } 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 } +maximums = { + +} diff --git a/main.py b/main.py index f4a8002..b9fa5fc 100644 --- a/main.py +++ b/main.py @@ -19,7 +19,7 @@ try: # start normal operations g.app = HelmholtzGUI() func.ui_print("Program Initialized") - func.ui_print("Starting setup...") + func.ui_print("Starting setup...") # do it again, so it is printed in the UI console func.setup_axes() # initiate communication, set handles g.app.mainloop() g.app = None # reset to None so nothing tries to print in the UI output From 5cbd5bb69f0f0547ff23594d867612bb9d8d66d1 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Wed, 3 Feb 2021 13:04:56 +0100 Subject: [PATCH 10/36] first attempt at error messages --- cage_func.py | 30 ++++++++++++++++++++++-------- globals.py | 3 --- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/cage_func.py b/cage_func.py index 69a5a81..0b07836 100644 --- a/cage_func.py +++ b/cage_func.py @@ -7,6 +7,7 @@ import numpy as np import serial import traceback from tkinter import * +from tkinter import messagebox from configparser import ConfigParser @@ -198,26 +199,39 @@ def read_config(section, key): # read specific value from config file raise KeyError("Could not find key", key, "in config file.") -def edit_config(section, key, value): # edit specific value in config file +def edit_config(section, key, value, override=False): # edit specific value in config file config_object = ConfigParser() # initialize config parser + value_ok = False # Value checking: # ToDo: make pop-up warning messages that can be waived try: if section in g.AXIS_NAMES: # only check numerical values max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value - min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] + min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] # get min value if value > max_value: - raise ValueError("Attempted to write too high value for", section, key, "to config file:", - value, ", max.", max_value, "allowed. Excessive values may damage equipment!") + message = "Attempted to write too high value for {s} {k} to config file:\n" \ + "{v}, max. {mv} allowed. Excessive values may damage equipment!\n" \ + "Do you really want to use this value?".format(s=section, k=key, v=value, mv=max_value) + raise ValueError(message) elif value < min_value: - raise ValueError("Attempted to write too low value for", section, key, "to config file:", - value, ", max.", max_value, "allowed. Excessive values may damage equipment!") + message = "Attempted to write too low value for {s} {k} to config file:\n" \ + "{v}, max. {mv} allowed. Excessive values may damage equipment!\n" \ + "Do you really want to use this value?".format(s=section, k=key, v=value, mv=min_value) + raise ValueError(message) + else: + value_ok = True except KeyError as e: ui_print("Error while editing config file:", e) raise KeyError("Could not find section", section, "in config file.") except ValueError as e: # value too high/low - ui_print(e) - else: # no errors so far + value_ok = False + # display pop-up message to ask user if he really wants the value + answer = messagebox.askquestion("Value out of bounds", e) # becomes 'yes' or 'no' depending on user choice + if answer == 'yes': override = True + else: override = False + else: # no errors + value_ok = True + if value_ok or override: # value is ok or user has chosen to use it anyway try: config_object.read(g.CONFIG_FILE) # open config file section_obj = config_object[section] # get relevant section diff --git a/globals.py b/globals.py index 26033da..95dfcbc 100644 --- a/globals.py +++ b/globals.py @@ -38,6 +38,3 @@ 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 } -maximums = { - -} From 1847df859c98d9e67bfd898b4e884f531be8ae99 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Wed, 3 Feb 2021 16:40:45 +0100 Subject: [PATCH 11/36] more safe value checking --- User_Interface.py | 67 ++++++++++++++++++++++----- cage_func.py | 113 ++++++++++++++++++++++++++++++---------------- main.py | 4 +- 3 files changed, 132 insertions(+), 52 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 1651e80..b039cd0 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -1,5 +1,6 @@ from tkinter import * from tkinter import ttk +from tkinter import messagebox import globals as g import cage_func as func import numpy as np @@ -264,7 +265,7 @@ class Configuration(Frame): 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 = 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) @@ -293,7 +294,7 @@ class Configuration(Frame): "Arduino Pins:": [[IntVar() for _ in range(3)], "-", "Should be 15, 16, 17", "relay_pin", 1] } - self.update_fields() # set current values from config file + self.fields = {} # Fill in header (axis names): col = 1 @@ -304,9 +305,11 @@ class Configuration(Frame): # generate table with entries, unit labels and descriptions: row = 1 for key in self.entries.keys(): + self.fields[key] = [] for axis in range(3): # generate entry fields - field = ttk.Entry(value_frame, textvariable=self.entries[key][0][axis], width=10) + field = Entry(value_frame, textvariable=self.entries[key][0][axis], width=10) field.grid(row=row, column=axis+1, sticky=W, padx=2) + self.fields[key].append(field) # safe access to field for use elsewhere 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]) @@ -317,6 +320,10 @@ class Configuration(Frame): row_counter += 1 + print(self.fields) + + self.update_fields() # set current values from config file + Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer row_counter += 1 @@ -359,23 +366,61 @@ class Configuration(Frame): 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: Warning messages if too high values are entered + value_check = func.value_in_limits(g.AXIS_NAMES[i], self.entries[key][3], value) + if value_check == 'OK': + self.fields[key][i].config(background="White") + else: + self.fields[key][i].config(background="Red") + def implement(self): # update config file with user inputs into entry fields and reinitialize + + # set serial ports for PSUs: func.edit_config("PORTS", "xy_port", self.XY_port.get()) func.edit_config("PORTS", "z_port", self.Z_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 + 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 - factor = self.entries[key][4] # get unit conversion factor - if factor not in [0, 1]: # prevent conversion of int variables to float and div/0 - value = value / factor # do unit conversion - func.edit_config(g.AXIS_NAMES[i], self.entries[key][3], value) # write new value to config file - except TclError as e: + except TclError as e: # wrong format entered, e.g. text in number fields func.ui_print("Invalid entry for %s %s %s" % (g.AXIS_NAMES[i], key, e)) + 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] # handle by which value is indexed in config file + value_ok = func.value_in_limits(g.AXIS_NAMES[i], config_key, value) + unit = self.entries[key][1] # get unit string for error messages + + if value_ok == 'OK': + func.edit_config(g.AXIS_NAMES[i], config_key, value) # write new value to config file + else: # value is not within limits + if value_ok == 'HIGH': + max_value = g.default_arrays[config_key][1][i] # get max value + message = "Attempted to set too high value for {s} {k}\n" \ + "{v} {unit}, max. {mv} {unit} allowed. Excessive values may damage equipment!\n" \ + "Do you really want to use this value?"\ + .format(s=g.AXIS_NAMES[i], k=key, v=value*factor, mv=round(max_value*factor, 1), unit=unit) + elif value_ok == 'LOW': + min_value = g.default_arrays[config_key][2][i] # get min value + message = "Attempted to set too low value for {s} {k}\n" \ + "{v} {unit}, min. {mv} {unit} allowed. Excessive values may damage equipment!\n" \ + "Do you really want to use this value?"\ + .format(s=g.AXIS_NAMES[i], k=key, v=value*factor, mv=round(min_value*factor, 1), unit=unit) + else: message = "Unknown case, 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) + # 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 + func.edit_config(g.AXIS_NAMES[i], config_key, value, True) + # if user chooses 'no' nothing happens, old value is kept + func.setup_axes() # reinitialize devices and program with new values self.update_fields() # update entry fields to show new values diff --git a/cage_func.py b/cage_func.py index 0b07836..28b9a1a 100644 --- a/cage_func.py +++ b/cage_func.py @@ -6,6 +6,7 @@ import time import numpy as np import serial import traceback +import User_Interface as ui from tkinter import * from tkinter import messagebox from configparser import ConfigParser @@ -177,22 +178,23 @@ class ArduinoCtrl(Arduino): def read_config(section, key): # read specific value from config file # ToDo (optional): better error handling - # ToDo: make pop-up error message for excessive values that can be waived + config_object = ConfigParser() # initialize config parser try: config_object.read(g.CONFIG_FILE) # open config file section_obj = config_object[section] # get relevant section value = section_obj[key] # get relevant value in the section - # Value checking: - if section in g.AXIS_NAMES: # only check numerical values + + # Value checking: ToDo: decide if we want this + '''if section in g.AXIS_NAMES: # only check numerical values max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] if float(value) > float(max_value): - ui_print("WARNING: Too high value for", section, key, "read from config file:", - value, "max.", max_value, "allowed. Excessive values may damage equipment!") + ui_print("\nWARNING: Too high value for", section, key, "read from config file:", + value, "max.", max_value, "allowed. Excessive values may damage equipment!\n") elif float(value) < float(min_value): - ui_print("WARNING: Too low value for", section, key, "read from config file:", - value, "max.", max_value, "allowed. Excessive values may damage equipment!") + ui_print("\nWARNING: Too low value for", section, key, "read from config file:", + value, "max.", max_value, "allowed. Excessive values may damage equipment!\n")''' return value except KeyError as e: ui_print("Error while reading config file:", e) @@ -201,64 +203,82 @@ def read_config(section, key): # read specific value from config file def edit_config(section, key, value, override=False): # edit specific value in config file config_object = ConfigParser() # initialize config parser - value_ok = False - # Value checking: - # ToDo: make pop-up warning messages that can be waived + + # Check if value is within acceptable limits (set in globals.py) try: - if section in g.AXIS_NAMES: # only check numerical values + value_ok = True + if section in g.AXIS_NAMES and not override: # only check numerical values and not if overridden by user + value_ok = False max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] # get min value if value > max_value: - message = "Attempted to write too high value for {s} {k} to config file:\n" \ - "{v}, max. {mv} allowed. Excessive values may damage equipment!\n" \ - "Do you really want to use this value?".format(s=section, k=key, v=value, mv=max_value) + message = "Prevented writing too high value for {s} {k} to config file:\n" \ + "{v}, max. {mv} allowed. Erroneous values may damage equipment!" \ + .format(s=section, k=key, v=value, mv=max_value) raise ValueError(message) elif value < min_value: - message = "Attempted to write too low value for {s} {k} to config file:\n" \ - "{v}, max. {mv} allowed. Excessive values may damage equipment!\n" \ - "Do you really want to use this value?".format(s=section, k=key, v=value, mv=min_value) + message = "Prevented writing too low value for {s} {k} to config file:\n" \ + "{v}, max. {mv} allowed. Erroneous values may damage equipment!" \ + .format(s=section, k=key, v=value, mv=min_value) raise ValueError(message) else: value_ok = True - except KeyError as e: - ui_print("Error while editing config file:", e) - raise KeyError("Could not find section", section, "in config file.") - except ValueError as e: # value too high/low - value_ok = False - # display pop-up message to ask user if he really wants the value - answer = messagebox.askquestion("Value out of bounds", e) # becomes 'yes' or 'no' depending on user choice - if answer == 'yes': override = True - else: override = False - else: # no errors - value_ok = True - if value_ok or override: # value is ok or user has chosen to use it anyway - try: + + if value_ok or override: config_object.read(g.CONFIG_FILE) # open config file section_obj = config_object[section] # get relevant section section_obj[key] = str(value) # get relevant value in the section with open(g.CONFIG_FILE, 'w') as conf: # Write changes back to file config_object.write(conf) - except KeyError as e: - ui_print("Error while editing config file:", e) - raise KeyError("Could not find key", key, "in config file.") + + except KeyError as e: + ui_print("Error while editing config file:", e) + raise KeyError("Could not find key", key, "in config file.") + + +def check_config(): + ui_print("Checking config file for values exceeding limits:") + i = 0 + concerns = {} + problem_counter = 0 + for axis in g.AXIS_NAMES: + concerns[axis] = [] + for key in g.default_arrays.keys(): + value = float(read_config(axis, key)) + max_value = g.default_arrays[key][1][i] # get max value + min_value = g.default_arrays[key][2][i] # get min value + + if not min_value <= value <= max_value: + concerns[axis].append(key) + problem_counter += 1 + + if len(concerns[axis]) == 0: + concerns[axis].append("No problems detected.") + + ui_print(axis, ":", *concerns[axis]) + i += 1 + if problem_counter > 0: + messagebox.showwarning("Warning!", "Found values exceeding limits in config file. Check values in " + "configuration page to avoid equipment damage!") + g.app.show_frame(ui.Configuration) def create_default_config(file): # create config file from default values (stored in globals.py) - config = ConfigParser() + config = ConfigParser() # initialize config object i = 0 - for axis_name in g.AXIS_NAMES: - config.add_section(axis_name) - for key in g.default_arrays.keys(): - config.set(axis_name, key, str(g.default_arrays[key][0][i])) + for axis_name in g.AXIS_NAMES: # go through axes + config.add_section(axis_name) # add section for this axis + for key in g.default_arrays.keys(): # go through dictionary with default values + config.set(axis_name, key, str(g.default_arrays[key][0][i])) # set value i += 1 - config.add_section("PORTS") + config.add_section("PORTS") # add section for PSU serial ports for key in g.default_ports.keys(): config.set("PORTS", key, str(g.default_ports[key])) - with open(file, 'w') as conf: + with open(file, 'w') as conf: # write all we just did to the file config.write(conf) @@ -274,6 +294,19 @@ def ui_print(*content): # prints text to built in console print(output) +def value_in_limits(axis, key, value): # Check if value is within safe limits (set in globals.py) + # ToDo: replace checks everywhere with this + max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(axis)] # get max value + min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(axis)] # get min value + + if float(value) > float(max_value): + return 'HIGH' + elif float(value) < float(min_value): + return 'LOW' + else: + return 'OK' + + def setup_axes(): # creates device objects for all PSUs and sets their values # Connect to Arduino: try: diff --git a/main.py b/main.py index b9fa5fc..ad10971 100644 --- a/main.py +++ b/main.py @@ -19,7 +19,9 @@ try: # start normal operations g.app = HelmholtzGUI() func.ui_print("Program Initialized") - func.ui_print("Starting setup...") # do it again, so it is printed in the UI console + func.check_config() # check config file for values exceeding limits + + func.ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once func.setup_axes() # initiate communication, set handles g.app.mainloop() g.app = None # reset to None so nothing tries to print in the UI output From b0c5beb444bce553756212a0c21e28c1fa8c7ea4 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Wed, 3 Feb 2021 18:26:44 +0100 Subject: [PATCH 12/36] code cleanup --- ArduinoTest.py | 29 ---------------- One_Unit_Test.py | 72 --------------------------------------- User_Interface.py | 46 ++++++++++++++----------- cage_func.py | 85 ++++++++++++++++++++--------------------------- main.py | 4 +-- 5 files changed, 64 insertions(+), 172 deletions(-) delete mode 100644 ArduinoTest.py delete mode 100644 One_Unit_Test.py diff --git a/ArduinoTest.py b/ArduinoTest.py deleted file mode 100644 index 2436f73..0000000 --- a/ArduinoTest.py +++ /dev/null @@ -1,29 +0,0 @@ -import time - -from Arduino import Arduino - -print("Searching for Arduino...") -board = Arduino() -print("Arduino found.") -board.pinMode(15, "Output") -board.pinMode(16, "Output") -board.pinMode(17, "Output") -board.digitalWrite(15, "LOW") -board.digitalWrite(16, "LOW") -board.digitalWrite(17, "LOW") - - -i = 0 -while i <= 1: - print("running: ", i) - for var in [15,16,17]: - board.digitalWrite(var, "HIGH") - time.sleep(0.5) - time.sleep(5) - for var in [15,16,17]: - board.digitalWrite(var, "LOW") - time.sleep(0.5) - time.sleep(2) - i = i + 1 - -board.close() diff --git a/One_Unit_Test.py b/One_Unit_Test.py deleted file mode 100644 index a9420bd..0000000 --- a/One_Unit_Test.py +++ /dev/null @@ -1,72 +0,0 @@ -# import platform -import time as t -import numpy as np -import globals as g -import cage_func as func -from pyps2000b import PS2000B - -# User Inputs/Configuration---------------------------------- -Test1 = 0 -Test2 = 1 -Test3 = 0 -Test4 = 0 - -# Constants: -g.COIL_CONST = np.array([38.6, 38.45, 37.9]) * 1e-9 # Coil constants [x,y,z] in T/A -g.AMBIENT_FIELD = np.array([80]) * 1e-6 # ambient magnetic field in measurement area, to be cancelled out -g.RESISTANCES = np.array([3.9, 3.9, 1]) # resistance of [x,y,z] circuits -g.MAX_WATTS = np.array([8, 8, 0]) # max. allowed power for [x,y,z] circuits - -# COM-Ports for power supply units: -XY_PORT = "COM7" -g.XY_DEVICE = PS2000B.PS2000B(XY_PORT) - -g.MAX_AMPS = np.sqrt(g.MAX_WATTS / g.RESISTANCES) -#print(g.MAX_AMPS) - - -'''def print_status(): - print("Output 1:") - func.print_status(g.X_AXIS) - print("Output 2:") - func.print_status(g.Y_AXIS)''' - - -func.set_to_zero(g.XY_DEVICE) -#print_status() -t.sleep(3) - -if Test1 == 1: - print("setting") - g.XY_DEVICE.voltage1 = 5 - g.XY_DEVICE.current1 = 1 - g.XY_DEVICE.enable_all() - t.sleep(1) - #print_status() - t.sleep(5) - print("setting to zero") - func.set_to_zero(g.XY_DEVICE) - -if Test2 == 1: - print("setting current") - g.XY_DEVICE.set_current(0.2, 0) - g.XY_DEVICE.set_voltage(5, 0) - g.XY_DEVICE.enable_all() - t.sleep(1) - #print_status() - t.sleep(5) - func.set_to_zero(g.XY_DEVICE) - -if Test4 == 1: - func.set_axis_current(g.XY_DEVICE, 0.2) - t.sleep(1) - print_status() - t.sleep(10) - func.set_to_zero(g.XY_DEVICE) - t.sleep(1) - -#print_status() - -g.XY_DEVICE.disable_all() - -#print_status() diff --git a/User_Interface.py b/User_Interface.py index b039cd0..c89986c 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -21,7 +21,7 @@ class HelmholtzGUI(Tk): self.Menu = TopMenu(self) # displays menu bar at the top - mainArea = Frame(self) + mainArea = Frame(self, padx=20, pady=20) mainArea.pack(side="top", fill="both", expand=False) mainArea.grid_rowconfigure(0, weight=1) @@ -163,7 +163,7 @@ class ManualMode(Frame): 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, + reinit_button = Button(self.buttons_frame, text="Reinitialize", command=func.setup_all, pady=5, padx=5, font=BIG_BUTTON_FONT) reinit_button.grid(row=row_counter, column=1, padx=5) @@ -179,6 +179,7 @@ class ManualMode(Frame): 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 + # noinspection PyUnusedLocal 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, e.g. calls update_max_fields function @@ -287,10 +288,13 @@ class Configuration(Frame): # {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], + "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], + "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] } @@ -320,8 +324,6 @@ class Configuration(Frame): row_counter += 1 - print(self.fields) - self.update_fields() # set current values from config file Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer @@ -335,7 +337,7 @@ class Configuration(Frame): 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, + 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) restore_button = Button(self.buttons_frame, text="Restore Defaults", command=self.restore_defaults, @@ -348,9 +350,9 @@ class Configuration(Frame): def page_switch(self): # function that is called when switching to this window self.update_fields() - def restore_defaults(self): + def restore_defaults(self): # restore all default settings func.create_default_config(g.CONFIG_FILE) # overwrite config file with default - func.setup_axes() # setup everything with the defaults ToDo: take out? + func.setup_all() # setup everything with the defaults self.update_fields() # update fields in config window def update_fields(self): @@ -366,11 +368,12 @@ class Configuration(Frame): 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 values are within safe limits: value_check = func.value_in_limits(g.AXIS_NAMES[i], self.entries[key][3], value) - if value_check == 'OK': - self.fields[key][i].config(background="White") - else: - self.fields[key][i].config(background="Red") + 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 implement(self): # update config file with user inputs into entry fields and reinitialize @@ -395,6 +398,7 @@ class Configuration(Frame): config_key = self.entries[key][3] # handle by which value is indexed in config file value_ok = func.value_in_limits(g.AXIS_NAMES[i], config_key, value) 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 == 'OK': func.edit_config(g.AXIS_NAMES[i], config_key, value) # write new value to config file @@ -402,15 +406,17 @@ class Configuration(Frame): if value_ok == 'HIGH': max_value = g.default_arrays[config_key][1][i] # get max value message = "Attempted to set too high value for {s} {k}\n" \ - "{v} {unit}, max. {mv} {unit} allowed. Excessive values may damage equipment!\n" \ + "{v} {unit}, max. {mv} {unit} allowed.\n" \ + "Excessive values may damage equipment!\n" \ "Do you really want to use this value?"\ - .format(s=g.AXIS_NAMES[i], k=key, v=value*factor, mv=round(max_value*factor, 1), unit=unit) + .format(s=axis, k=key, v=value*factor, mv=round(max_value*factor, 1), unit=unit) elif value_ok == 'LOW': min_value = g.default_arrays[config_key][2][i] # get min value message = "Attempted to set too low value for {s} {k}\n" \ - "{v} {unit}, min. {mv} {unit} allowed. Excessive values may damage equipment!\n" \ + "{v} {unit}, min. {mv} {unit} allowed.\n" \ + "Excessive values may damage equipment!\n" \ "Do you really want to use this value?"\ - .format(s=g.AXIS_NAMES[i], k=key, v=value*factor, mv=round(min_value*factor, 1), unit=unit) + .format(s=axis, k=key, v=value*factor, mv=round(min_value*factor, 1), unit=unit) else: message = "Unknown case, this should not happen." # display pop-up message to ask user if he really wants the value @@ -421,7 +427,7 @@ class Configuration(Frame): func.edit_config(g.AXIS_NAMES[i], config_key, value, True) # if user chooses 'no' nothing happens, old value is kept - func.setup_axes() # reinitialize devices and program with new values + func.setup_all() # reinitialize devices and program with new values self.update_fields() # update entry fields to show new values @@ -498,7 +504,7 @@ class StatusDisplay(Frame): controller.after(500, lambda: self.update_labels(controller)) -class OutputConsole(Frame): # console to print stuff in +class OutputConsole(Frame): # console to print stuff in, similar to standard python output def __init__(self, parent): Frame.__init__(self, parent, relief=SUNKEN, bd=1) diff --git a/cage_func.py b/cage_func.py index 28b9a1a..b47510a 100644 --- a/cage_func.py +++ b/cage_func.py @@ -6,6 +6,7 @@ import time import numpy as np import serial import traceback +# noinspection PyPep8Naming import User_Interface as ui from tkinter import * from tkinter import messagebox @@ -140,7 +141,7 @@ class ArduinoCtrl(Arduino): def __init__(self): self.connected = "Unknown" self.pins = [0, 0, 0] - for i in range(3): + for i in range(3): # get correct pins from config file self.pins[i] = int(read_config(g.AXIS_NAMES[i], "relay_pin")) ui_print("\nConnecting to Arduino...") try: @@ -177,24 +178,11 @@ class ArduinoCtrl(Arduino): def read_config(section, key): # read specific value from config file - # ToDo (optional): better error handling - config_object = ConfigParser() # initialize config parser try: config_object.read(g.CONFIG_FILE) # open config file section_obj = config_object[section] # get relevant section value = section_obj[key] # get relevant value in the section - - # Value checking: ToDo: decide if we want this - '''if section in g.AXIS_NAMES: # only check numerical values - max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value - min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] - if float(value) > float(max_value): - ui_print("\nWARNING: Too high value for", section, key, "read from config file:", - value, "max.", max_value, "allowed. Excessive values may damage equipment!\n") - elif float(value) < float(min_value): - ui_print("\nWARNING: Too low value for", section, key, "read from config file:", - value, "max.", max_value, "allowed. Excessive values may damage equipment!\n")''' return value except KeyError as e: ui_print("Error while reading config file:", e) @@ -204,32 +192,30 @@ def read_config(section, key): # read specific value from config file def edit_config(section, key, value, override=False): # edit specific value in config file config_object = ConfigParser() # initialize config parser - # Check if value is within acceptable limits (set in globals.py) + # Check if value to write is within acceptable limits (set in globals.py) try: - value_ok = True + value_ok = 'OK' if section in g.AXIS_NAMES and not override: # only check numerical values and not if overridden by user - value_ok = False + value_ok = value_in_limits(section, key, value) # check if value is ok, too high or too low max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] # get min value - if value > max_value: + if value_ok == 'HIGH': message = "Prevented writing too high value for {s} {k} to config file:\n" \ "{v}, max. {mv} allowed. Erroneous values may damage equipment!" \ .format(s=section, k=key, v=value, mv=max_value) raise ValueError(message) - elif value < min_value: + elif value_ok == 'LOW': message = "Prevented writing too low value for {s} {k} to config file:\n" \ "{v}, max. {mv} allowed. Erroneous values may damage equipment!" \ .format(s=section, k=key, v=value, mv=min_value) raise ValueError(message) - else: - value_ok = True - if value_ok or override: + if value_ok == 'OK' or override: # value is within limits or user has overridden config_object.read(g.CONFIG_FILE) # open config file section_obj = config_object[section] # get relevant section section_obj[key] = str(value) # get relevant value in the section - with open(g.CONFIG_FILE, 'w') as conf: # Write changes back to file + with open(g.CONFIG_FILE, 'w') as conf: # Write changes to config file config_object.write(conf) except KeyError as e: @@ -237,31 +223,32 @@ def edit_config(section, key, value, override=False): # edit specific value in raise KeyError("Could not find key", key, "in config file.") -def check_config(): +def check_config(): # check all numeric values in the config file and see if they are within safe limits ui_print("Checking config file for values exceeding limits:") i = 0 - concerns = {} + concerns = {} # initialize dictionary for found problems problem_counter = 0 for axis in g.AXIS_NAMES: - concerns[axis] = [] - for key in g.default_arrays.keys(): - value = float(read_config(axis, key)) + concerns[axis] = [] # create dictionary entry for this axis + for key in g.default_arrays.keys(): # go over entries in this axis + value = float(read_config(axis, key)) # read value to check from config file max_value = g.default_arrays[key][1][i] # get max value min_value = g.default_arrays[key][2][i] # get min value - if not min_value <= value <= max_value: - concerns[axis].append(key) + if not min_value <= value <= max_value: # value is not in safe limits + concerns[axis].append(key) # add this entry to the problem dictionary problem_counter += 1 if len(concerns[axis]) == 0: concerns[axis].append("No problems detected.") - ui_print(axis, ":", *concerns[axis]) + ui_print(axis, ":", *concerns[axis]) # print out results for this axis i += 1 - if problem_counter > 0: - messagebox.showwarning("Warning!", "Found values exceeding limits in config file. Check values in " - "configuration page to avoid equipment damage!") - g.app.show_frame(ui.Configuration) + if problem_counter > 0: # some values are not ok + # shop pup-up warning message: + messagebox.showwarning("Warning!", "Found values exceeding limits in config file. Check values " + "to ensure correct operation and avoid equipment damage!") + g.app.show_frame(ui.Configuration) # open configuration window so user can check values def create_default_config(file): # create config file from default values (stored in globals.py) @@ -307,7 +294,7 @@ def value_in_limits(axis, key, value): # Check if value is within safe limits ( return 'OK' -def setup_axes(): # creates device objects for all PSUs and sets their values +def setup_all(): # main initialization function, creates device objects for all PSUs and Arduino and sets their values # Connect to Arduino: try: if g.ARDUINO is not None: @@ -336,7 +323,7 @@ def setup_axes(): # creates device objects for all PSUs and sets their values ui_print("\nConnecting to XY Device on %s..." % g.XY_PORT) try: if g.XY_DEVICE is not None: - ui_print("closing serial connection on XY device") + ui_print("Closing serial connection on XY device") g.XY_DEVICE.serial.close() g.XY_DEVICE = None g.XY_DEVICE = PS2000B.PS2000B(g.XY_PORT) # setup PSU @@ -350,6 +337,10 @@ def setup_axes(): # creates device objects for all PSUs and sets their values ui_print("Connecting to Z Device on %s..." % g.Z_PORT) try: + if g.Z_DEVICE is not None: + ui_print("Closing serial connection on Z device") + g.Z_DEVICE.serial.close() + g.Z_DEVICE = None g.Z_DEVICE = PS2000B.PS2000B(g.Z_PORT) ui_print("Connection established.") g.Z_AXIS = Axis(2, g.Z_DEVICE, 0, g.ARDUINO.pins[2]) @@ -454,25 +445,21 @@ def set_current_vec(vector): # sets needed currents on each axis for given vect i = i + 1 -def execute_csv(filepath, printing=0): # runs through csv file containing times and desired field vectors +def execute_csv(filepath): # runs through csv file containing times and desired field vectors # csv format: time (s); xField (T); yField (T); zField (T) # decimal commas ui_print("Reading File:", filepath) file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file array = file.to_numpy() # convert csv to array - t_zero = time.time() - t_ref = t_zero + t_zero = time.time() # set reference time for start of run i = 0 - ui_print("Starting Execution...") + ui_print("Starting File Execution...") while i < len(array): t = time.time() - t_zero - if t >= array[i, 0]: - field_vec = array[i, 1:4] - ui_print("t = %0.2f s, target field vector = " % (array[i, 0]), field_vec) - set_field(field_vec) - i = i + 1 - if t - t_ref >= 1 and printing == 1: # print status every second - print_status_3() - t_ref = t + if t >= array[i, 0]: # time for this row has come + field_vec = array[i, 1:4] # extract desired field vector + # ui_print("t = %0.2f s, target field vector = " % (array[i, 0]), field_vec) + set_field(field_vec) # send field vector to test stand + i = i + 1 # next row ui_print("File executed, powering down channels.") power_down_all() # set currents and voltages to 0, set arduino pins to low diff --git a/main.py b/main.py index ad10971..ab8f0cf 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,7 @@ try: # start normal operations func.create_default_config(g.CONFIG_FILE) print("Starting setup...") - func.setup_axes() # initiate communication, set handles + func.setup_all() # initiate communication, set handles print("\nOpening User Interface...") @@ -22,7 +22,7 @@ try: # start normal operations func.check_config() # check config file for values exceeding limits func.ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once - func.setup_axes() # initiate communication, set handles + func.setup_all() # initiate communication, set handles g.app.mainloop() g.app = None # reset to None so nothing tries to print in the UI output From ef914ff6fc6f8dceaf1fe1785a1b642dd8c5738a Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Thu, 4 Feb 2021 11:00:33 +0100 Subject: [PATCH 13/36] small stuff --- User_Interface.py | 23 ++++------ cage_func.py | 105 +++++++++++++++++++++++++--------------------- globals.py | 3 +- main.py | 6 +-- 4 files changed, 71 insertions(+), 66 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index c89986c..3ccb434 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -62,7 +62,7 @@ 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)) + ModeSelector.add_command(label="Settings...", command=lambda: self.configuration(window)) @staticmethod def manual_mode(window): @@ -214,20 +214,16 @@ class ManualMode(Frame): vector[i] = float(var.get()) i = i + 1 function_to_call(vector) # call function - # ToDo: update status display here def execute_field(self, vector): func.ui_print("field executing", vector) - try: - 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) + comp = self.compensate.get() + if comp == 1: + func.set_field(vector * 1e-6) + elif comp == 0: + func.set_field_simple(vector * 1e-6) + else: + func.ui_print("Unexpected value encountered: compensate =", comp) @staticmethod def execute_current(vector): @@ -288,7 +284,7 @@ class Configuration(Frame): # {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", + "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], @@ -480,7 +476,6 @@ class StatusDisplay(Frame): self.update_labels(controller) def update_labels(self, controller): - # 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 b47510a..e1f3499 100644 --- a/cage_func.py +++ b/cage_func.py @@ -95,7 +95,7 @@ class Axis: self.device.disable_output(self.channel) g.ARDUINO.digitalWrite(self.ardPin, "LOW") except Exception as e: - ui_print(e) # ToDo: more error handling here + ui_print("Error while powering down %s: %s" % (self.name, e)) def set_signed_current(self, value): # sets current with correct polarity on this axis device = self.device @@ -104,18 +104,21 @@ class Axis: # ui_print("Attempting to set current", value, "A") self.target_current = value if self.connected == "Connected" or True: # ToDo!: remove True, only for arduino testing! + if abs(value) > self.max_amps: # prevent excessive currents self.power_down() # set output to 0 and deactivate raise ValueError("Invalid current value. Tried %0.2fA, max. %0.2fA allowed" % (value, self.max_amps)) + elif value >= 0: # switch polarity as needed - g.ARDUINO.digitalWrite(ardPin, "LOW") # ToDo: reactivate and tie to arduino + g.ARDUINO.digitalWrite(ardPin, "LOW") # ToDo: tie to arduino? elif value < 0: - g.ARDUINO.digitalWrite(ardPin, "HIGH") # ToDo: tie to arduino + g.ARDUINO.digitalWrite(ardPin, "HIGH") # ToDo: tie to arduino? else: raise Exception("This should be impossible.") - maxVoltage = min(max(1.1 * self.max_amps * self.resistance, 8), self.max_volts) # limit voltage# + + maxVoltage = min(max(1.1 * self.max_amps * self.resistance, 8), self.max_volts) # limit voltage # ui_print("sending values to device: U =", maxVoltage, "I =", abs(value)) - if self.connected == "Connected": # ToDo!: remove if, only for arduino testing! + if self.connected == "Connected": # ToDo!: remove if clause, only for arduino testing! device.set_current(abs(value), channel) device.set_voltage(maxVoltage, channel) device.enable_output(channel) @@ -170,7 +173,7 @@ class ArduinoCtrl(Arduino): axis.polarity_switched = "Unknown" self.connected = "Connection Error" else: - g.ARDUINO.connected = "Connected" + self.connected = "Connected" def safe(self): # sets output pins to low and closes serial connection for pin in self.pins: @@ -282,7 +285,6 @@ def ui_print(*content): # prints text to built in console def value_in_limits(axis, key, value): # Check if value is within safe limits (set in globals.py) - # ToDo: replace checks everywhere with this max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(axis)] # get max value min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(axis)] # get min value @@ -360,22 +362,6 @@ def activate_all(): # enables remote control and output on all PSUs and channel g.Z_DEVICE.enable_all() -def deactivate_all(): # disables remote control and output on all PSUs and channels - # ToDo: add check if device is connected - try: - g.XY_DEVICE.disable_all() - except BaseException: - ui_print("XY PSU deactivation unsuccessful.") - else: - ui_print("XY PSU deactivated.") - try: - g.Z_DEVICE.disable_all() - except BaseException: - ui_print("Z PSU deactivation unsuccessful.") - else: - ui_print("Z PSU deactivated.") - - def print_status_3(): ui_print("X-Axis:") g.X_AXIS.print_status() @@ -399,55 +385,78 @@ def power_down_all(): # temporary, set all outputs to 0 but keep connections en def shut_down_all(): # shutdown at program end or on error, set outputs to 0 and disable connections - # ToDo: better messages, check if things are connected first + # ToDo: remove checks if connected or make them only for printing ui_print("\nAttempting to safely shut down all devices. Check equipment to confirm.") - try: - set_to_zero(g.XY_DEVICE) - except: - ui_print("XY PSU set to 0 unsuccessful.") + if g.XY_DEVICE is not None: + try: + set_to_zero(g.XY_DEVICE) + g.XY_DEVICE.disable_all() + except BaseException as e: + ui_print("Error while deactivating XY PSU:", e) + else: + ui_print("XY PSU deactivated.") else: - ui_print("XY PSU currents and voltages set to 0.") - try: - set_to_zero(g.Z_DEVICE) - except: - ui_print("Z PSU set to 0 unsuccessful.") + ui_print("XY PSU not connected, can't deactivate.") + if g.Z_DEVICE is not None: + try: + set_to_zero(g.Z_DEVICE) + g.Z_DEVICE.disable_all() + except BaseException as e: + ui_print("Error while deactivating Z PSU:", e) + else: + ui_print("Z PSU deactivated.") else: - ui_print("Z PSU currents and voltages set to 0.") - deactivate_all() + ui_print("Z PSU not connected, can't deactivate.") + try: g.ARDUINO.safe() - except: - ui_print("Arduino safing unsuccessful.") - # else: # commented out bc this throws no exception, even when arduino is not connected - # ui_print("Arduino pins set to LOW.") # ToDo: figure out error handling for this - try: - g.ARDUINO.close() - except: - ui_print("Closing Arduino connection failed.") + except BaseException as e: + ui_print("Arduino safing unsuccessful:", e) + # this throws no exception, even when arduino is not connected + # ToDo (optional): figure out error handling for this + if g.ARDUINO.connected == "Connected": + try: + g.ARDUINO.close() + except BaseException as e: + ui_print("Closing Arduino connection failed:", e) + else: + ui_print("Serial connection to Arduino closed.") else: - ui_print("Serial connection to Arduino closed.") + ui_print("Arduino not connected, can't close connection.") def set_field_simple(vector): # forms magnetic field as specified by vector, w/o cancelling ambient field for i in [0, 1, 2]: - g.AXES[i].set_field_simple(vector[i]) + try: + g.AXES[i].set_field_simple(vector[i]) + except ValueError as e: + ui_print(e) def set_field(vector): # forms magnetic field as specified by vector, corrected for ambient field for i in [0, 1, 2]: - g.AXES[i].set_field(vector[i]) + try: + g.AXES[i].set_field(vector[i]) + except ValueError as e: + ui_print(e) def set_current_vec(vector): # sets needed currents on each axis for given vector i = 0 for axis in g.AXES: - axis.set_signed_current(vector[i]) - i = i + 1 + try: + axis.target_field = 0 + axis.target_field_comp = 0 + axis.set_signed_current(vector[i]) + except ValueError as e: + ui_print(e) + i += 1 def execute_csv(filepath): # runs through csv file containing times and desired field vectors # csv format: time (s); xField (T); yField (T); zField (T) # decimal commas + # ToDo: set to zero before start ui_print("Reading File:", filepath) file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file array = file.to_numpy() # convert csv to array diff --git a/globals.py b/globals.py index 95dfcbc..7530983 100644 --- a/globals.py +++ b/globals.py @@ -5,7 +5,7 @@ XY_DEVICE = None Z_DEVICE = None ARDUINO = None -X_AXIS = None # object structure: (device, channel, arduino pin, axis index) +X_AXIS = None Y_AXIS = None Z_AXIS = None @@ -26,6 +26,7 @@ global PORTS # format: [[default values], [maximum values], [minimum values]] # ToDo: check actual maximum ratings # ToDo: Add maximum current: 5A (BA Blessing page 30), remove max_watts (there for testing with resistors) +# ToDo: put this into a config file default_arrays = { "coil_const": np.array([[38.6, 38.45, 37.9], [50, 50, 50], [0, 0, 0]]) * 1e-6, # Coil constants [x,y,z] [T/A] "ambient_field": np.array([[30, 30, 30], [200, 200, 200], [0, 0, 0]]) * 1e-6, # background magnetic field [T] diff --git a/main.py b/main.py index ab8f0cf..268a471 100644 --- a/main.py +++ b/main.py @@ -28,9 +28,9 @@ try: # start normal operations except BaseException as e: # if there is an error, print what happened - func.ui_print("\nAn error occurred, Shutting down.") - func.ui_print(e) - func.ui_print(traceback.print_exc()) + print("\nAn error occurred, Shutting down.") + print(e) + print(traceback.print_exc()) finally: # safely shut everything down at the end func.shut_down_all() From 8d1870956f46fb0b91eb8c78f9fccc1fad4d0f1d Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Thu, 4 Feb 2021 15:33:19 +0100 Subject: [PATCH 14/36] reworked config handling file is now only written when explicitly wanted, e.g. on button press. global config stored instead as config object --- .gitignore | 1 + User_Interface.py | 90 +++++++++++++++++++++++++++++++++++++++++------ cage_func.py | 66 ++++++++++++++++++++-------------- globals.py | 3 +- main.py | 10 ++++-- 5 files changed, 128 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index 7395fe1..f9dee33 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,4 @@ ENV/ .idea/Python-PS2000B.iml .idea/misc.xml config.ini +*.ini diff --git a/User_Interface.py b/User_Interface.py index 3ccb434..2ad47a6 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -1,14 +1,18 @@ from tkinter import * from tkinter import ttk from tkinter import messagebox +from tkinter import filedialog import globals as g import cage_func as func import numpy as np +import os +from os.path import exists NORM_FONT = () 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): @@ -74,7 +78,6 @@ class TopMenu: class ManualMode(Frame): - # ToDo: Add buttons to safe and set to 0 def __init__(self, parent, controller): Frame.__init__(self, parent) @@ -162,10 +165,15 @@ class ManualMode(Frame): 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=func.power_down_all, + pady=5, padx=5, font=BIG_BUTTON_FONT) + power_down_button.grid(row=row_counter, column=1, padx=5) + # add button for reinitialization reinit_button = Button(self.buttons_frame, text="Reinitialize", command=func.setup_all, pady=5, padx=5, font=BIG_BUTTON_FONT) - reinit_button.grid(row=row_counter, column=1, padx=5) + reinit_button.grid(row=row_counter, column=2, padx=5) row_counter = row_counter + 1 # Add spacer to Frame below @@ -250,6 +258,27 @@ class Configuration(Frame): 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 + # ToDo: comments + 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) + 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) + 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 + # Serial port settings frame: port_frame = Frame(self) port_frame.grid_rowconfigure(ALL, weight=1) @@ -308,7 +337,7 @@ class Configuration(Frame): self.fields[key] = [] for axis in range(3): # generate entry fields field = Entry(value_frame, textvariable=self.entries[key][0][axis], width=10) - field.grid(row=row, column=axis+1, sticky=W, padx=2) + field.grid(row=row, column=axis + 1, sticky=W, padx=2) self.fields[key].append(field) # safe access to field for use elsewhere axis_label = Label(value_frame, text=key, padx=5, pady=5) axis_label.grid(row=row, column=0, sticky=W) @@ -347,7 +376,7 @@ class Configuration(Frame): self.update_fields() def restore_defaults(self): # restore all default settings - func.create_default_config(g.CONFIG_FILE) # overwrite config file with default + func.reset_config_to_default(g.CONFIG_FILE) # overwrite config file with default func.setup_all() # setup everything with the defaults self.update_fields() # update fields in config window @@ -358,7 +387,8 @@ class Configuration(Frame): 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 + value = func.read_from_config(g.AXIS_NAMES[i], self.entries[key][3], + g.CONFIG_OBJECT) # 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 @@ -371,7 +401,7 @@ class Configuration(Frame): else: # value exceeds limits self.fields[key][i].config(background="Red") # set colour of this entry to red to show problem - def implement(self): # update config file with user inputs into entry fields and reinitialize + def write_values(self): # update config file with user inputs into entry fields and reinitialize # set serial ports for PSUs: func.edit_config("PORTS", "xy_port", self.XY_port.get()) @@ -404,16 +434,17 @@ class Configuration(Frame): 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) + "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': min_value = g.default_arrays[config_key][2][i] # get min value 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: message = "Unknown case, this should not happen." + "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: + message = "Unknown case, 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) @@ -423,9 +454,46 @@ class Configuration(Frame): func.edit_config(g.AXIS_NAMES[i], config_key, value, True) # if user chooses 'no' nothing happens, old value is kept + def implement(self): + self.write_values() func.setup_all() # reinitialize devices and program with new values self.update_fields() # update entry fields to show new values + def load_config(self): # ToDo: comments + directory = os.path.dirname(os.path.abspath(g.CONFIG_FILE)) + filename = filedialog.askopenfilename(initialdir=directory, title="Select Config File", + filetypes=(("Config File", "*.ini*"), ("All Files", "*.*"))) + if exists(filename): + g.CONFIG_FILE = filename + g.CONFIG_OBJECT = func.get_config_from_file(filename) + func.check_config(g.CONFIG_OBJECT) + func.setup_all() + self.update_fields() + elif filename == '': + func.ui_print("No file selected, could not load config.") + else: + func.ui_print("Selected file", filename, "does not seem to exist, could not load config.") + + def save_config_as(self): # ToDo: comments + directory = os.path.dirname(os.path.abspath(g.CONFIG_FILE)) + filename = filedialog.asksaveasfilename(initialdir=directory, title="Select Config File", + filetypes=([("Config File", "*.ini*")]), + defaultextension=[("Config File", "*.ini*")]) + if exists(filename): + g.CONFIG_FILE = filename + self.write_values() + func.write_config_to_file(g.CONFIG_OBJECT) + self.update_fields() + elif filename == '': + func.ui_print("No file selected, could not save config.") + else: + func.ui_print("Selected file", filename, "does not seem to exist, could not save config.") + + def save_config(self): # ToDo: comments + self.write_values() + func.write_config_to_file(g.CONFIG_OBJECT) + self.update_fields() + class StatusDisplay(Frame): diff --git a/cage_func.py b/cage_func.py index e1f3499..d3007ce 100644 --- a/cage_func.py +++ b/cage_func.py @@ -24,13 +24,13 @@ class Axis: self.name = g.AXIS_NAMES[index] self.port = g.PORTS[index] - self.resistance = float(read_config(self.name, "resistance")) - self.max_watts = float(read_config(self.name, "max_watts")) + self.resistance = float(read_from_config(self.name, "resistance", g.CONFIG_OBJECT)) + self.max_watts = float(read_from_config(self.name, "max_watts", g.CONFIG_OBJECT)) self.max_amps = np.sqrt(self.max_watts / self.resistance) - self.max_volts = float(read_config(self.name, "max_volts")) + self.max_volts = float(read_from_config(self.name, "max_volts", g.CONFIG_OBJECT)) - self.coil_constant = float(read_config(self.name, "coil_const")) - self.ambient_field = float(read_config(self.name, "ambient_field")) + self.coil_constant = float(read_from_config(self.name, "coil_const", g.CONFIG_OBJECT)) + self.ambient_field = float(read_from_config(self.name, "ambient_field", g.CONFIG_OBJECT)) max_field = self.max_amps * self.coil_constant # max field reachable in this axis self.max_field = np.array([-max_field, max_field]) @@ -145,7 +145,7 @@ class ArduinoCtrl(Arduino): self.connected = "Unknown" self.pins = [0, 0, 0] for i in range(3): # get correct pins from config file - self.pins[i] = int(read_config(g.AXIS_NAMES[i], "relay_pin")) + self.pins[i] = int(read_from_config(g.AXIS_NAMES[i], "relay_pin", g.CONFIG_OBJECT)) ui_print("\nConnecting to Arduino...") try: Arduino.__init__(self) # search for connected arduino and connect @@ -180,10 +180,19 @@ class ArduinoCtrl(Arduino): self.digitalWrite(pin, "LOW") -def read_config(section, key): # read specific value from config file +def get_config_from_file(file=g.CONFIG_FILE): config_object = ConfigParser() # initialize config parser + config_object.read(file) # open config file + return config_object # return config object, that contains all info from the file + + +def write_config_to_file(config_object): # ToDo: comments + with open(g.CONFIG_FILE, 'w') as conf: # Write changes to config file + config_object.write(conf) + + +def read_from_config(section, key, config_object): # read specific value from config object try: - config_object.read(g.CONFIG_FILE) # open config file section_obj = config_object[section] # get relevant section value = section_obj[key] # get relevant value in the section return value @@ -193,7 +202,8 @@ def read_config(section, key): # read specific value from config file def edit_config(section, key, value, override=False): # edit specific value in config file - config_object = ConfigParser() # initialize config parser + config_object = g.CONFIG_OBJECT + # ToDo: add check for data types, e.g. int for arduino ports # Check if value to write is within acceptable limits (set in globals.py) try: @@ -214,19 +224,24 @@ def edit_config(section, key, value, override=False): # edit specific value in raise ValueError(message) if value_ok == 'OK' or override: # value is within limits or user has overridden - config_object.read(g.CONFIG_FILE) # open config file - section_obj = config_object[section] # get relevant section - section_obj[key] = str(value) # get relevant value in the section - - with open(g.CONFIG_FILE, 'w') as conf: # Write changes to config file - config_object.write(conf) + try: + section_obj = config_object[section] # get relevant section + except KeyError: + ui_print("Could not find section", section, "in config file, creating new.") + config_object.add_section(section) + section_obj = config_object[section] + try: + section_obj[key] = str(value) # set relevant value in the section + except KeyError: + ui_print("Could not find key", key, "in config file, creating new.") + config_object.set(section, key, str(value)) except KeyError as e: ui_print("Error while editing config file:", e) raise KeyError("Could not find key", key, "in config file.") -def check_config(): # check all numeric values in the config file and see if they are within safe limits +def check_config(config_object): # check all numeric values in the config and see if they are within safe limits ui_print("Checking config file for values exceeding limits:") i = 0 concerns = {} # initialize dictionary for found problems @@ -234,7 +249,7 @@ def check_config(): # check all numeric values in the config file and see if th for axis in g.AXIS_NAMES: concerns[axis] = [] # create dictionary entry for this axis for key in g.default_arrays.keys(): # go over entries in this axis - value = float(read_config(axis, key)) # read value to check from config file + value = float(read_from_config(axis, key, config_object)) # read value to check from config file max_value = g.default_arrays[key][1][i] # get max value min_value = g.default_arrays[key][2][i] # get min value @@ -254,8 +269,9 @@ def check_config(): # check all numeric values in the config file and see if th g.app.show_frame(ui.Configuration) # open configuration window so user can check values -def create_default_config(file): # create config file from default values (stored in globals.py) - config = ConfigParser() # initialize config object +def reset_config_to_default(file): # reset values in config object to defaults (stored in globals.py) + config = ConfigParser() # initialize global config object + g.CONFIG_OBJECT = config i = 0 for axis_name in g.AXIS_NAMES: # go through axes @@ -268,9 +284,6 @@ def create_default_config(file): # create config file from default values (stor for key in g.default_ports.keys(): config.set("PORTS", key, str(g.default_ports[key])) - with open(file, 'w') as conf: # write all we just did to the file - config.write(conf) - def ui_print(*content): # prints text to built in console output = "" @@ -318,8 +331,8 @@ def setup_all(): # main initialization function, creates device objects for all g.AXES = [] - g.XY_PORT = read_config("PORTS", "xy_port") - g.Z_PORT = read_config("PORTS", "z_port") + g.XY_PORT = read_from_config("PORTS", "xy_port", g.CONFIG_OBJECT) + g.Z_PORT = read_from_config("PORTS", "z_port", g.CONFIG_OBJECT) g.PORTS = [g.XY_PORT, g.XY_PORT, g.Z_PORT] ui_print("\nConnecting to XY Device on %s..." % g.XY_PORT) @@ -379,9 +392,8 @@ def set_to_zero(device): # sets voltages and currents to 0 def power_down_all(): # temporary, set all outputs to 0 but keep connections enabled - set_to_zero(g.XY_DEVICE) - set_to_zero(g.Z_DEVICE) - g.ARDUINO.safe() + for axis in g.AXES: + axis.power_down() # set outputs to 0 and pin to low on this axis def shut_down_all(): # shutdown at program end or on error, set outputs to 0 and disable connections diff --git a/globals.py b/globals.py index 7530983..2515a40 100644 --- a/globals.py +++ b/globals.py @@ -15,7 +15,8 @@ app = None AXIS_NAMES = ["X-Axis", "Y-Axis", "Z-Axis"] -global CONFIG_FILE +CONFIG_FILE = None +CONFIG_OBJECT = None global XY_PORT global Z_PORT diff --git a/main.py b/main.py index 268a471..1751403 100644 --- a/main.py +++ b/main.py @@ -7,10 +7,14 @@ from os.path import exists try: # start normal operations g.CONFIG_FILE = 'config.ini' - + # ToDo: remember what the last config file was if not exists(g.CONFIG_FILE): print("Config file not found, creating new from defaults.") - func.create_default_config(g.CONFIG_FILE) + func.reset_config_to_default(g.CONFIG_FILE) + func.write_config_to_file(g.CONFIG_OBJECT) + + g.CONFIG_OBJECT = func.get_config_from_file(g.CONFIG_FILE) + print(g.CONFIG_OBJECT) print("Starting setup...") func.setup_all() # initiate communication, set handles @@ -19,7 +23,7 @@ try: # start normal operations g.app = HelmholtzGUI() func.ui_print("Program Initialized") - func.check_config() # check config file for values exceeding limits + func.check_config(g.CONFIG_OBJECT) # check config file for values exceeding limits func.ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once func.setup_all() # initiate communication, set handles From 8447a2a1560a68f02dcbe6ecfc0b58335b306752 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Thu, 4 Feb 2021 18:10:12 +0100 Subject: [PATCH 15/36] comments --- User_Interface.py | 46 ++++++++++++++++++++++++---------------------- cage_func.py | 2 +- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 2ad47a6..451c36d 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -454,42 +454,44 @@ class Configuration(Frame): func.edit_config(g.AXIS_NAMES[i], config_key, value, True) # if user chooses 'no' nothing happens, old value is kept - def implement(self): - self.write_values() + def implement(self): # executed on button press + self.write_values() # write current values from entry fields to config object func.setup_all() # reinitialize devices and program with new values self.update_fields() # update entry fields to show new values - def load_config(self): # ToDo: comments - directory = os.path.dirname(os.path.abspath(g.CONFIG_FILE)) + def load_config(self): # load configuration from some config file + directory = os.path.dirname(os.path.abspath(g.CONFIG_FILE)) # get directory of current config file + # open file selection dialogue and save path of selected file filename = filedialog.askopenfilename(initialdir=directory, title="Select Config File", filetypes=(("Config File", "*.ini*"), ("All Files", "*.*"))) - if exists(filename): - g.CONFIG_FILE = filename - g.CONFIG_OBJECT = func.get_config_from_file(filename) - func.check_config(g.CONFIG_OBJECT) - func.setup_all() - self.update_fields() - elif filename == '': + if exists(filename): # does the file exist? + g.CONFIG_FILE = filename # set global config file to the new file + g.CONFIG_OBJECT = func.get_config_from_file(filename) # load values from config file to config object + func.check_config(g.CONFIG_OBJECT) # check the values and display warnings if values are out of bounds + func.setup_all() # 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 func.ui_print("No file selected, could not load config.") else: - func.ui_print("Selected file", filename, "does not seem to exist, could not load config.") + func.ui_print("Selected file", filename, "does not exist, could not load config.") - def save_config_as(self): # ToDo: comments - directory = os.path.dirname(os.path.abspath(g.CONFIG_FILE)) + def save_config_as(self): # save current configuration to a new config file + directory = os.path.dirname(os.path.abspath(g.CONFIG_FILE)) # get directory of current config file + # open file selection dialogue and save path of selected file filename = filedialog.asksaveasfilename(initialdir=directory, title="Select Config File", filetypes=([("Config File", "*.ini*")]), defaultextension=[("Config File", "*.ini*")]) - if exists(filename): - g.CONFIG_FILE = filename - self.write_values() - func.write_config_to_file(g.CONFIG_OBJECT) - self.update_fields() - elif filename == '': + if exists(filename): # does the file exist? + g.CONFIG_FILE = filename # set global config file to the new file + self.write_values() # write current entry field values to the config object + func.write_config_to_file(g.CONFIG_OBJECT) # write contents of config object to file + self.update_fields() # update entry fields to show values as they are in the config + elif filename == '': # this happens when file selection window is closed without selecting a file func.ui_print("No file selected, could not save config.") else: - func.ui_print("Selected file", filename, "does not seem to exist, could not save config.") + func.ui_print("Selected file", filename, "does not exist, could not save config.") - def save_config(self): # ToDo: comments + def save_config(self): # same as save_config_as() but with the current config file self.write_values() func.write_config_to_file(g.CONFIG_OBJECT) self.update_fields() diff --git a/cage_func.py b/cage_func.py index d3007ce..d62592c 100644 --- a/cage_func.py +++ b/cage_func.py @@ -186,7 +186,7 @@ def get_config_from_file(file=g.CONFIG_FILE): return config_object # return config object, that contains all info from the file -def write_config_to_file(config_object): # ToDo: comments +def write_config_to_file(config_object): # write contents of config object to a config file with open(g.CONFIG_FILE, 'w') as conf: # Write changes to config file config_object.write(conf) From 8440a752960a21f1e83a132c3723fb87699398c3 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Sun, 7 Feb 2021 13:08:24 +0100 Subject: [PATCH 16/36] added csv execution and associated multithreading --- Test cases.xlsx | Bin 0 -> 12889 bytes Test2.csv | 15 +++++++ Test2_slow.csv | 15 +++++++ User_Interface.py | 104 +++++++++++++++++++++++++++++++++++++++++++--- cage_func.py | 23 +--------- csv_threading.py | 62 +++++++++++++++++++++++++++ globals.py | 3 ++ main.py | 4 +- 8 files changed, 198 insertions(+), 28 deletions(-) create mode 100644 Test cases.xlsx create mode 100644 Test2.csv create mode 100644 Test2_slow.csv create mode 100644 csv_threading.py diff --git a/Test cases.xlsx b/Test cases.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..88dda9cf58ce409302f45180b78cc44aadcc8315 GIT binary patch literal 12889 zcmeHu1zQ};w)Wr>G`I$LcXtQ`5AN>n?hxGF3GNQTJ-7sScMVSPZ?g9}H)m(>`~87? zdY)&xtGd?PQ`O6Atu=CzVBn|#NB}ed03ZbD#<_R+f&c(;kN^M*02)+7*v8t?$l6gy z(aqM#L5t4S%90=#9F!sl016!c-{b%A3iK(CeE?%b>Uh)98&`HxcuPRE>VCdoEOZQ( zKjg^Hk{&p!xctS1ZHefkMi>OWTJ4Z0fBX*4Y9*ISo1hSmCoNr>A4S)(Zn=&51(z31 zm8f5Sn>?Ls@mFlQm8gjcv1uo8nx-WKTJ>C4V$vgYt{|n54L2kFk3}IxC9(2{LFNJo zPyy5>0p#b3AEOET^E^_@3i7t}zIO5_j+SG5UES}TfB?Flpqz2$f6D{7Tp9O@7?UT^ij?~6E#}vH^nu@XB!cqu^`>BA9&WWNGs6lKrA*7@x@T z`dsh~@n6@%;2gR*rj|gm9)2(%5bi0HGN5d!%w%YGxtxG;)!RQ*?v}Z4ky+Z^T{zUY zS5h zez0?bq9U_T60_c{*y_vmDegDCY!nr+~s;sH_v*ge^vDoy5NDa~ioeDM#N&)UC zbly}y^?qr!HT@rzAhSXWrxl;7zp&>V#ZRRB%qJBd!t;l4N}Wulq76Fen=Dp(4q6i4 z++!;#m~xs`>u1?>5xMCYT6SHDq<5k`d(z5e41OeHdHaKD;{5<=&W(?HJ{IqZXmKhMB zkpqtXzkRgD%S!e!AbtJrzfL&1T3FE%V%Y*Ex=>d6jv#-!&@w$?^heS9np};OUPytq3*3xr5rO1Kq=N{092II;ULLJgKZ3c+nF^O;T>*bBJGUCIeq{$w zk;X#_5sde+G!8!Wq*&pZg?)DiTZNaOr*Q}5MzquOD&_agT4Fc9K%tUn>FZEIKVD+@ z=^YqcuCH~JVh2R8wK*O9kP6%x44RY+##Za*@D5jdD=3&{GB|Je<9t&bJ=P>jf{4@l zk~HNcTMs>bu^h(0(E`8RdSzvlgqGF5l?+@BuiA4HiQd;Z2E`3#&sYf)f63K$I=H~@ zB{Z3wUDfj+IY|$R@B$vFYKg!B07M{*06FPDc_?3D#Cn+lsSV?RAE|AiHh}{gTtX0^ z_DDE9$0iMt8QBLp9Xi39$`UWQ*{ z<;TXM`@7kJI_hyySPhs{njKF>KOKzk>qZ*;>;(=uVgRJ8U&B!mvy|k* zwYS4WIJy+Jws<7s2;s+P(@rb!dmlP z3875E3lwi4Zb)cI>fw#T6T74+o9nZDH`~*u$3pFDd~M?LUIv+o530?;Ho1~s`jsIv zxna)X2}*IHXdGz=mrlgj=<0!s9PI7k6Vq@sr_3j4Lx{AmdY(xE4;lB0@*BxYJUML- z!U;L7a^sTT_6hv%gj@-6LXx%P)o@*T1-ZSh7!yFg_`43eAMNDv0Xo}xf&c*gpPb|1 z=w@l;@Kg7UYK~ZMv!Sj6`34%a#qqkaqU)BxIalBJP}esf(UE+lK2Ho$IJamB^*NsI z5vy(_`X(+;lqt!|JvSwY5k(Ri5%GI7kEi(X!Ky);X|}w4zZ=_J#T$irIrQCd4vom0 z?C<_-a`PW>jl&s+m|5nsvF9X}o&b0cK3`r_ znEMfljCz!7g*;|r{j^MO<(2?=^Sd)0caBH^4O#UShVxs6>e#$7Gzk;6HC$-c4iUxcqnHXs|(M>Itmg|q>*6)IqeK>F*Dh-&Y(-q&Lu zKacm+k92`Sb}(N*vLP|95>_0Q!{HJzoVR^8xo>>)S}fYh8&GL*bXcmii%Ib@(I7`$ ziK~}|%l4(E=sK}%m{BgDYCs^iG$0DkHAhn^%8y8i6EaM(pp_W!R%yZLBTuD09BRqW zVdf_B6H9(xyQP12xHgnJIzb#ayJ^$tGPf?~vFgz(Wu`~Gt?#S0ryi^%dFs2ze#zd) z?4A2=SRc}>lobS{wrnLSo)9=@w*d9-_y=DUeh!n~es!9Gic*^rd?a_1;oK>Lro)E2 zbEo=(qoPw7`UTmlm6n1_nKzQy+vRQeef<>H#2IRx##lY1XY+d!Nx71esW=D%@Yr9re9OJbGWJR0>KoF@9Kj~fJ-X=GY>nHlt_zQK8J^`q82(dcHO z2a`-%J7Y6s@3_JfPH;3~n%g#E#rNP0J0*3?`(e(NPZD4 zBm?srlP|atobR|_xvMmPbx6%@KR9PsY2d?p>4)=_IjZ&Q#K#kiG|!|nYdh~Zq2oPp zP&R0LUg%}R`W%a&i)xU%x(VTRXHJa=>u%jTYJF`B-{7i>bU}{CCuZymWm4frOTy}l zmQn)nNKrWmX+*cHzBOxqYB@l0QPCaD4`EI$nC~vpaSpZy`&bg^C z3#P9ogauVpMVAUP`caFjvnk*BRn~hKw!mvfR4I#a#G4qzjM_KpxrX z%DKyeg9N8yNV9{3Ypjxm0wb>K4k>WZ)vY3D4q#$Uq3|WA^j$6`%Y3nA1WPqafzg$c zW^%XJ{dN@Jos!n)$;n`PGX(6La^VEYN&4Am=(y8Ga4G|7G2Pkt=1!Uy|=xLRu zyfm0@s8I{EQ?AZMIur+~QOPn=PNEEJRVbtw8Ar5+XsYB=6mvV9;$A*}W_U%-J02Eu zQK;y;LXCraDYJnw@otr=2FK&J8^s$Z>aEV_p6Rc z#g@LG6M#FwF&a1S6yI2=8q)F}iwcvT2EU!k>J+js58tnn6w$^kk_5Nsj!fr@33b!h zSs;R#Lk@p`>j~x71{S5E{or5J)yH1#`M4CQM(xH`eah(>(Bl{?isVN}_Pus`|6`gH z;&ncR`lod8b=FSY{qnl+*SnSnVBd%sIuS{E)S>trq>-a< zI;{)AULVM+J%k;&PMQ%e-8}gl{d=0<3Mg(**P1@6sfNOWs zAN9}e*eo{JjN>U@m*5Me@1HGuT@BZbYHy?{Pn-t#u2p|`ZSSf(Iqu+YzW|NiKJ&tH z8DXI*dpL#&Y;y_SMkTdtoHZ{)@nEt~Ogi<~zSHb@ zsRVHGepGuoiSl~KW1ZJIE`B)?D8U({%a<$8IVgI1>8mez>~foB;e`NRXf--=bqn)P zz8VS(-6S{A!zV%p05Jcy0>9>-pt>BF$A;u3tMuZx;lcZVG>{&#_wncyKT^_Av53NgZQ&Bybl8hj_lx6u*{YC>rN+g5O#8 z%`z3l5OG{>hV_Z8`jSp#=Syt|(~EW%1sb}@;7Fv`+gw#n8LTW*u@8N)X#u3hJj9^* z_mh-wWF!>Z6AGgCTbRj-C9KP!srg7C!?#KDt7v#kSTLxdizty}eB3^S)rKYo3HKNU zc4mhc#TY>Za*&u2@s;H?CI*##F&O9I%?pr8kY}s=A_Ro z0E=xrrT~GoU!axQj(ql4@!)C5JZ-_w>3r)>`~8fTa}BD0tzeUOX@M?~!t8_a&BwjJ3Bfv_(&9^eA+9eecX!fHS_PBeSGZ#09%!DqL;GOJz(#{Y`gbzIzMnkc zNJo)_9H5E`BJ@)|corBzQwcB)KJwqF5DhwyEzW|9GUk6o;^aSvonq>2)(W1wgUBFM zg&Cz=9$IP^sMWK2bhk-AVtZX4{!}j5e<_Faey%+;CgTNs`40)DW1s&(2;!E(P z_F=ZHvw)Pe+B(Jzlm4LMZ*%##k*0EargLTGm)GAMs%j(2y^$2QY3{bW(>GQI7k@q@ zQUo|)4NW=K*U+Vdjf_W8LJbSfxPhUTDYpX|9yM%IbKYcCRJZ;> z!@6||t@!rH&B3ca#OY`o7S%EVmAc6*XU^8cjg3M5_O!BfX;wJ8QKjH=>}E2nLgAef z%4Q3TS+bGX0=owke8lxbp?-B)q`;gU=4wPVf_COwu@DSSS$3820(-9@JQ&#X)KNy- zNlaX^va@(*0g#?g&*9ndlNU4YOtpNQ)>V|m?epo_=5SPLdX65WwaOp3tUS7eXXYv^ zooz;)P(|uxF}w0a%Y1RAbJ7A|)*LJ##Q6i=C)nEG(tih+pG zJrH&6`zroo4h&^M-___H9ZX1$4nLOq^4UB8NG%3B4he9>x(~XVKQsc%yzg(F3?APB z)ydyChvEL#s`0?oOBD9c^vfS%1xHgODZ=F)Uqx%f4Em_0t<(dE)2Z(?$Pph!*Cf zj_%diGgqGU2jwT!0JP&K@4R={0g1d20(r=zEZe!u*R2)S6B9Mq6s`3OXRPiwrGE1# z^Ya`op7+e>unCKIbLRnq(Aq4(>rJn4S?q>yKf5;rSUvNm4&C6vssxZphc>ZWwxDio zIhWB4dk&(|)-Kxx^OA7rcS-H8z$#V=K7@A#hGa`;!Md}v1oYWn zua}D``@3}mCJQqPQtikMMLn9~ZNAd`_M&cG5ATJF2&vqh4=< zWxen39k`LmG+a0DlcRk+9)8@bWWT(u62EDS&jrKdBVpVdSMoer(PbQ!cI!5bLY(rM zGr%O&zZO7VXiPZ^9|UM?5t#Qn)f~2IP(nw*ySjM~Y_z;9a1f*+)(O=IYZ|~jt(`gY zrM9OT>+N-8YCy~ycUplDjIuNcreG9W&YloZ3mr4W)C<^i3VZ8U$kE*sJF!66pR~A2 z)^~o4k5v#+7iw#bQWPFCODb%yOY(8jEVvITIVh4KaOr$(jkai{*lLc~(~xL0NMJ_D zJEUZ@f5end&XSo^gmD(HBV5FgxVcmOJy+!fgot|>rNe{4XmiCx1L|HG#4~J2IPZyv zBXatnVvKP5m{wm()JaV*gV4q3a4Jb`)U)q9p)jI-E)=#O_{{4_ZisdQL;bevL<`V& zi$b<2j!H#}Bq5$mva~gA5vlilbO+#i1xy5wpV9_MI2D#;Lt9>DX6i~E)n~XVYdX}q zi}F2D&iE-dcv)mC4O~s;-YjrGdl#*ry=r%|w~y>sEd|L1e8MPmYH*Hbr&}ctobsy4 zaZN6p*v(Gf6oiE!)g& zfUEpcs%8~>LAh-H$`F0Gz_DDTTxRc*77Fz~*R<+Kne&2G2pk?unAXB@+tR$uthJkj z!0j_RNqx7}Tc)i_Od8QF0<$p)2>dO;yc_V~c0PsrHY zJlr>V@mCQ}WzZ#pvMu`eY^W^QW9-Nllte}Keq=2lzx8oiw+gNXPuC!)HLSv%JZ&;% z37*+DpNTrc4ww+f8q+@=Q;g8;<_5baaB7Ci6hbm0b2w3iE>~u}P~n*7vWVD~%?Ti_ zsoT@WBz}p+1z&|@%91)4KhT?3&o8RekozG;T`sG1HxD(EWr?u47TrHE4w6~Kj+CFW zUi<+n3MLpV$9!*$(GO)TF$V44rIcZ}w(K2cDemwRa;7Ln6-7&mi7ja^HP?~#F4!qu zHZzCgY7mryn(nL9EN`o#CghuZiZDYgx?X6j2;B2CV#g!)wvw9f4yR(=|# zH1E!x+4c`5hb~72QQzmwygJnnohL7QBjB;p zA-_wz2;W_>Qw_FMPxaD2EPvfH5nH=r?gBg0c>Q=~BAJolN{Y{3$JYa`>a_D8Z5Wp@ zSXdR%X+}o_dxe!?wq=lxQIM=!wZ*W0i7&a{oLgKvJU{53ec)+E)6~cvu~O=& zXn&f-Xcq>lwQX=&1%x${Ny~>yBD^}Ij?Xr;9h)=Fg5+wnHc;zeL7j}!Mc9v%trlNeSt@lOkwx)cTA5hpC?#p z1Ol83s3@r^HvVZ}bch(OrodO1fF@5s>pQ_0Z!FrkDkl?53#9PysuwyPHqi_uhW%N_ zLk^QKU$wo9?l%{Nj&U<=S&yGB#M4nLVWhs=2L}a8q+7DHdN6K0^t7cb^wb5Fo4gaX zW~Y4pPW*(W3txWR=EQc3MV8)ib8r7n{t>i!Lg`Vc*=Ff(k)ho?YGrQK!Erqk&t|M~ zJr3_j(M~e`S&`0A-}4MRPfMrONkmn+YTbSE5N-wF>wAv1@7r1yl>@;PeEk=GEFDFC z)MSMQQc^)Mj31-;}Bqg07dgK2281+McQfjbWfkusxW#NI92eP)-}fS`YKsrqtNj zH`ib!PB$p{SG1NIvpWsfP?3tx0|V%J=K-b<*v?(w4O$e=|Wj;1lB@LLVc#J zbBdNwJ1e1LOQeH z^l{*k>H3>;20qUyww?RiiZBiwPhU9)9doSFlo2903uh31R{qi$4=^qO5W^oJ3V0u{ zNKr{euoA&7>2w}zWKr2_vPuOCAimid3lqqNA^GU+_66SnM6fSJH&@o3i08tF*H{=X zf|VeG5~l4oUkM_LYF0v}4CPb_ROMKNdR{_@UO2H7eRL}d7F?qRHn#tq6p}8!rS&bd zek4fmU|QwMo6p)`CAd{v-cpvJiV%5kWA-G$Non) zib1k1Z?iYBb?T{K>Z!5^oto7pIw^Y-c_%-3gkD&eesaaH>4#2m)AZn%V~wOb-`o&r zd);~q9G+HBlzr`Vjn^vc>xVo!`~(6$Y@KwEiD@B#d1ari?PGKBI`HesB zAx0%q{+kqTp#ZIhOA&1p#JYy_)_aDA4Ybt!2RuEKI6($z1OBH3 z2IcKf8&E>edTX~(|ET0@c_iO=0Pk=DV5ATy1TT8 z0ZH`E{}su2S7SjDLpGL#4!kqRTflUGQGmH5v!vd;9SylVc*FzYu7~xJknJc8FIWU! zSq%ZUL`o^AE=a*n(^{~`wbU~nnl@sv+T|)$51rL1ly-*bRK|kNA|k79hDdsk-f40K zwY6@qQCP<=lA>nvnCEqN5(-qQFsrWzQc-quYI`#=b8{o2iAY58%?qP?#w6nHPA{Xb zLW$_bM1;=RX&DyZ{(!8l2NyjnpJw1x?>6*%fZzj2rr-y+)^S;^kK=(nzavvj3=u--cjthTxzPEbo6w(XhBwA+q9et+hQG}b|dV%~*u)M_*@VOce2-SPQm{)9D_n*K(|7x~}}cZCQ# zLf{TlP#2s_7u*l|0E8IWcI)(E*VA|dO8gB)3<=ZE^6ct$0$PvI(7}cvs3-T+iFUr6 z>P*a6I(8vdXf-?3z-px0Ef#1!HDYu>0iQ0*yMh;B9OCb5R{7>Rv=z8!G-v<-`Jal< z7O3$YjqDYS936kEKKkFpI;&ZK<4IM3?~MB}KGTVfpS86JFR&HH;V zO}KCP5ReE(qU}R4vp2upA(J?d7Kf!2>&|>5yDsUp-MgX3W*Oah3W&9yeh7J140Go% zaG>kKY<_eSw5paqL=`>sh&nBOJi)AMSsvYb8~$K#e6>7uC|M*;79KBA!TzK~c@qT< zw?50!={ZC)Z+=p|!uiw>qg>ssqWyW?fsQDcX--a;<~?K0IQ>8d=(dqM^)>v1+tJ&0 zP-T?w3t5`q;<8W@5r&q=)_Gqu3meprD zt*pJI2+Cxr?PjLi|2($i!)#RvVX&8^ zy3O5kqDkjce5FVpjulS`9_WjN1S%u|4Gls_@T=ix2h9Wcr}w`(I)o_*eE(X&;uqxq zGz11V_D27;1i&@>*AWpXFWJk08nh1aDm>(op6|dVwCp#Wg$pf%x-Mk@1tOh=WYYR| ztv<7X>7C~&@4ollie%WX;QSCdM-{dLQ5tfBFLI=!u}bd#7?oB|ACxso6%H6Fm;d{b zqsv^Rq|pBEJPVqpoG$WMLmreykw+w&-i2^p)3dq*2Kf~>nJc7h>tI@p@19T`Z{S|7(?l{kmYK3pcOSW@U+L8OnZw{&+twAu;_X7|6w zi({!(KbZ?XK`KJD46}7@ULZq|4Fz8)RF&t5#jGRzZZ@DLZSespMEPL`= array[i, 0]: # time for this row has come - field_vec = array[i, 1:4] # extract desired field vector - # ui_print("t = %0.2f s, target field vector = " % (array[i, 0]), field_vec) - set_field(field_vec) # send field vector to test stand - i = i + 1 # next row - ui_print("File executed, powering down channels.") - power_down_all() # set currents and voltages to 0, set arduino pins to low + i += 1 \ No newline at end of file diff --git a/csv_threading.py b/csv_threading.py new file mode 100644 index 0000000..c1d4b07 --- /dev/null +++ b/csv_threading.py @@ -0,0 +1,62 @@ +import cage_func as func +import time +import pandas +from threading import * +import globals as g + + +class ExecCSVThread(Thread): + def __init__(self, threadID, array, parent): + # ToDo: comments + Thread.__init__(self) + + self.threadID = threadID + self.array = array # numpy array containing data from csv to be executed + self.parent = parent + + def run(self): + func.ui_print("Starting Sequence Execution...") + # g.threadLock.acquire() # Get lock to synchronize threads + # ToDo: add locking/synchronization? Works without so far but might be more robust + execute_sequence(self.array, 0.1) + self.parent.running = False + # reset buttons: + self.parent.select_file_button["state"] = "normal" + self.parent.execute_button["state"] = "normal" + self.parent.stop_button["state"] = "disabled" + + +def execute_sequence(array, delay): # runs through array containing times and desired field vectors + # array format: [time (s), xField (T), yField (T), zField (T)] + # decimal commas + # all times in seconds + func.power_down_all() # sets outputs to 0 before starting + t_zero = time.time() # set reference time for start of run + i = 0 + while i < len(array) and g.running: # while array is not finished and user has not cancelled + t = time.time() - t_zero # get relative time + if t >= array[i, 0]: # time for this row has come + field_vec = array[i, 1:4] # extract desired field vector + func.ui_print("%f s: t = %0.2f s, target field vector = " + % (time.time()-t_zero, array[i, 0]), field_vec*1e6, "\u03BCT") + func.set_field_simple(field_vec) # send field vector to test stand # ToDo!: reset to set_field() + func.ui_print(time.time()-t_zero) + i = i + 1 # next row + elif t >= array[i, 0] - delay - 0.02: # not enough time to sleep + pass + else: # sleep to give other threads time to run + time.sleep(delay) + if g.running: # sequence ended without interruption + func.ui_print("Sequence executed, powering down channels.") + else: # interrupted by user + func.ui_print("Sequence cancelled, powering down channels.") + func.power_down_all() # set currents and voltages to 0, set arduino pins to low + + +def read_csv_to_array(filepath): + # csv format: time (s); xField (T); yField (T); zField (T) + # decimal commas + func.ui_print("Reading File:", filepath) + file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file + array = file.to_numpy() # convert csv to array + return array diff --git a/globals.py b/globals.py index 2515a40..631e521 100644 --- a/globals.py +++ b/globals.py @@ -23,6 +23,9 @@ global Z_PORT global PORTS +global threadLock +running = False + # Default Constants and maximum/minimum values (warning messages will be generated if these are exceeded) # format: [[default values], [maximum values], [minimum values]] # ToDo: check actual maximum ratings diff --git a/main.py b/main.py index 1751403..7067a1a 100644 --- a/main.py +++ b/main.py @@ -23,7 +23,7 @@ try: # start normal operations g.app = HelmholtzGUI() func.ui_print("Program Initialized") - func.check_config(g.CONFIG_OBJECT) # check config file for values exceeding limits + func.check_config(g.CONFIG_OBJECT) # check config for values exceeding limits func.ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once func.setup_all() # initiate communication, set handles @@ -38,3 +38,5 @@ except BaseException as e: # if there is an error, print what happened finally: # safely shut everything down at the end func.shut_down_all() + +# ToDo: logging From 47b4e447b05dc1a08bc8c5f7d5df94c27ae6d36c Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Sun, 7 Feb 2021 13:25:09 +0100 Subject: [PATCH 17/36] enabled and added callable status display updates --- User_Interface.py | 15 +++++++++++---- csv_threading.py | 11 +++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 688734b..9bbb263 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -90,6 +90,8 @@ class ManualMode(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) + self.controller = controller # object on which mainloop() is running, usually main window + self.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(ALL, weight=1) @@ -230,6 +232,7 @@ class ManualMode(Frame): vector[i] = float(var.get()) i = i + 1 function_to_call(vector) # call function + self.controller.StatusDisplay.update_labels() # update status display after change def execute_field(self, vector): func.ui_print("field executing", vector) @@ -512,6 +515,7 @@ class ExecuteCSVMode(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) + self.controller = controller # object on which mainloop() is running, usually main window # Functional init: self.sequence_array = None # array containing the values from the csv file @@ -580,7 +584,7 @@ class ExecuteCSVMode(Frame): # g.threadLock = threading.Lock() # create separate thread to run sequence execution in: g.running = True - csv_thread = csv.ExecCSVThread("CSV_Thread", self.sequence_array, self) + csv_thread = csv.ExecCSVThread("CSV_Thread", self.sequence_array, self, self.controller) csv_thread.start() # start thread def stop_run(self): @@ -637,9 +641,13 @@ class StatusDisplay(Frame): col = col + 1 # rowCounter = rowCounter + self.rowNo # increase row counter to place future stuff below this - self.update_labels(controller) + self.continuous_label_update(controller, 500) # initiate regular value updates (ms) - def update_labels(self, controller): + def continuous_label_update(self, controller, interval): # update display values in regular intervals + self.update_labels() + controller.after(interval, lambda: self.continuous_label_update(controller, interval)) + + def update_labels(self): g.ARDUINO.update_status_info() i = 0 for axis in g.AXES: @@ -660,7 +668,6 @@ class StatusDisplay(Frame): self.label_dict["Target Current:"][i].set("%0.3f A" % axis.target_current) self.label_dict["Inverted:"][i].set(axis.polarity_switched) i = i + 1 - controller.after(500, lambda: self.update_labels(controller)) class OutputConsole(Frame): # console to print stuff in, similar to standard python output diff --git a/csv_threading.py b/csv_threading.py index c1d4b07..fedd674 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -6,19 +6,20 @@ import globals as g class ExecCSVThread(Thread): - def __init__(self, threadID, array, parent): + def __init__(self, threadID, array, parent, controller): # ToDo: comments Thread.__init__(self) self.threadID = threadID self.array = array # numpy array containing data from csv to be executed self.parent = parent + self.controller = controller # object on which mainloop() is running, usually main window def run(self): func.ui_print("Starting Sequence Execution...") # g.threadLock.acquire() # Get lock to synchronize threads # ToDo: add locking/synchronization? Works without so far but might be more robust - execute_sequence(self.array, 0.1) + execute_sequence(self.array, 0.1, self.controller) self.parent.running = False # reset buttons: self.parent.select_file_button["state"] = "normal" @@ -26,7 +27,7 @@ class ExecCSVThread(Thread): self.parent.stop_button["state"] = "disabled" -def execute_sequence(array, delay): # runs through array containing times and desired field vectors +def execute_sequence(array, delay, controller): # runs through array containing times and desired field vectors # array format: [time (s), xField (T), yField (T), zField (T)] # decimal commas # all times in seconds @@ -41,8 +42,10 @@ def execute_sequence(array, delay): # runs through array containing times and d % (time.time()-t_zero, array[i, 0]), field_vec*1e6, "\u03BCT") func.set_field_simple(field_vec) # send field vector to test stand # ToDo!: reset to set_field() func.ui_print(time.time()-t_zero) + controller.StatusDisplay.update_labels() # update status display after change i = i + 1 # next row - elif t >= array[i, 0] - delay - 0.02: # not enough time to sleep + + elif t >= array[i, 0] - delay - 0.02: # next change time is close, not enough time to sleep pass else: # sleep to give other threads time to run time.sleep(delay) From 08d4ca463d30aa4950c54f7e078eb28be11fcbd0 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Sun, 7 Feb 2021 14:54:12 +0100 Subject: [PATCH 18/36] moved config handling to separate file --- User_Interface.py | 54 +++++++++--------- cage_func.py | 138 +++++---------------------------------------- config_handling.py | 115 +++++++++++++++++++++++++++++++++++++ globals.py | 4 +- main.py | 18 +++--- 5 files changed, 169 insertions(+), 160 deletions(-) create mode 100644 config_handling.py diff --git a/User_Interface.py b/User_Interface.py index 9bbb263..a056c59 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -10,6 +10,7 @@ from os.path import exists import globals as g import cage_func as func import csv_threading as csv +import config_handling as config NORM_FONT = () HEADER_FONT = ("Arial", 13, "bold") @@ -328,7 +329,7 @@ class Configuration(Frame): "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. 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] @@ -387,7 +388,7 @@ class Configuration(Frame): self.update_fields() def restore_defaults(self): # restore all default settings - func.reset_config_to_default(g.CONFIG_FILE) # overwrite config file with default + config.reset_config_to_default(config.CONFIG_FILE) # overwrite config file with default func.setup_all() # setup everything with the defaults self.update_fields() # update fields in config window @@ -398,8 +399,8 @@ class Configuration(Frame): for key in self.entries.keys(): for i in [0, 1, 2]: - value = func.read_from_config(g.AXIS_NAMES[i], self.entries[key][3], - g.CONFIG_OBJECT) # get value from config file + value = config.read_from_config(g.AXIS_NAMES[i], self.entries[key][3], + config.CONFIG_OBJECT) # 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 @@ -415,8 +416,8 @@ class Configuration(Frame): def write_values(self): # update config file with user inputs into entry fields and reinitialize # set serial ports for PSUs: - func.edit_config("PORTS", "xy_port", self.XY_port.get()) - func.edit_config("PORTS", "z_port", self.Z_port.get()) + config.edit_config("PORTS", "xy_port", self.XY_port.get()) + config.edit_config("PORTS", "z_port", self.Z_port.get()) # set numeric values for all axes for key in self.entries.keys(): # go through rows of entry table @@ -438,7 +439,7 @@ class Configuration(Frame): axis = g.AXIS_NAMES[i] # get axis name for error messages if value_ok == 'OK': - func.edit_config(g.AXIS_NAMES[i], config_key, value) # write new value to config file + config.edit_config(g.AXIS_NAMES[i], config_key, value) # write new value to config file else: # value is not within limits if value_ok == 'HIGH': max_value = g.default_arrays[config_key][1][i] # get max value @@ -462,7 +463,7 @@ class Configuration(Frame): # 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 - func.edit_config(g.AXIS_NAMES[i], config_key, value, 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): # executed on button press @@ -471,14 +472,15 @@ class Configuration(Frame): 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(g.CONFIG_FILE)) # get directory of current 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.askopenfilename(initialdir=directory, title="Select Config File", filetypes=(("Config File", "*.ini*"), ("All Files", "*.*"))) if exists(filename): # does the file exist? - g.CONFIG_FILE = filename # set global config file to the new file - g.CONFIG_OBJECT = func.get_config_from_file(filename) # load values from config file to config object - func.check_config(g.CONFIG_OBJECT) # check the values and display warnings if values are out of bounds + 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 the values and display warnings if values are out of bounds func.setup_all() # 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 @@ -487,24 +489,23 @@ class Configuration(Frame): func.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(g.CONFIG_FILE)) # get directory of current 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 exists(filename): # does the file exist? - g.CONFIG_FILE = filename # set global config file to the new file - self.write_values() # write current entry field values to the config object - func.write_config_to_file(g.CONFIG_OBJECT) # write contents of config object to file - self.update_fields() # update entry fields to show values as they are in the config - elif filename == '': # this happens when file selection window is closed without selecting a file + + if filename == '': # this happens when file selection window is closed without selecting a file func.ui_print("No file selected, could not save config.") - else: - func.ui_print("Selected file", filename, "does not exist, 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 + 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() - func.write_config_to_file(g.CONFIG_OBJECT) + config.write_config_to_file(config.CONFIG_OBJECT) self.update_fields() @@ -541,18 +542,19 @@ class ExecuteCSVMode(Frame): # Create and place buttons self.select_file_button = Button(self.file_select_frame, text="Select csv file...", command=self.load_csv, - pady=5, padx=5, font=SMALL_BUTTON_FONT) + pady=5, padx=5, font=SMALL_BUTTON_FONT) self.select_file_button.grid(row=0, column=0, padx=5) self.execute_button = Button(self.file_select_frame, text="Run Sequence", command=self.run_sequence, - pady=5, padx=5, font=SMALL_BUTTON_FONT, state="disabled") + pady=5, padx=5, font=SMALL_BUTTON_FONT, state="disabled") self.execute_button.grid(row=0, column=1, padx=5) self.stop_button = Button(self.file_select_frame, text="Stop Run", command=self.stop_run, - pady=5, padx=5, font=SMALL_BUTTON_FONT, state="disabled") + pady=5, padx=5, font=SMALL_BUTTON_FONT, state="disabled") self.stop_button.grid(row=0, column=2, padx=5) row_counter += 1 - def page_switch(self): # every class in the UI needs this, even if it doesn't do anything + 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 diff --git a/cage_func.py b/cage_func.py index 6af91b6..a892022 100644 --- a/cage_func.py +++ b/cage_func.py @@ -1,16 +1,13 @@ -from pyps2000b import PS2000B -from Arduino import Arduino -import globals as g -import pandas -import time import numpy as np import serial import traceback -# noinspection PyPep8Naming -import User_Interface as ui from tkinter import * -from tkinter import messagebox -from configparser import ConfigParser + +from pyps2000b import PS2000B +from Arduino import Arduino +# noinspection PyPep8Naming +import config_handling as config +import globals as g class Axis: @@ -24,13 +21,13 @@ class Axis: self.name = g.AXIS_NAMES[index] self.port = g.PORTS[index] - self.resistance = float(read_from_config(self.name, "resistance", g.CONFIG_OBJECT)) - self.max_watts = float(read_from_config(self.name, "max_watts", g.CONFIG_OBJECT)) - self.max_amps = np.sqrt(self.max_watts / self.resistance) - self.max_volts = float(read_from_config(self.name, "max_volts", g.CONFIG_OBJECT)) + self.resistance = float(config.read_from_config(self.name, "resistance", config.CONFIG_OBJECT)) + self.max_watts = float(config.read_from_config(self.name, "max_watts", config.CONFIG_OBJECT)) + self.max_amps = float(config.read_from_config(self.name, "max_amps", config.CONFIG_OBJECT)) + self.max_volts = float(config.read_from_config(self.name, "max_volts", config.CONFIG_OBJECT)) - self.coil_constant = float(read_from_config(self.name, "coil_const", g.CONFIG_OBJECT)) - self.ambient_field = float(read_from_config(self.name, "ambient_field", g.CONFIG_OBJECT)) + self.coil_constant = float(config.read_from_config(self.name, "coil_const", config.CONFIG_OBJECT)) + self.ambient_field = float(config.read_from_config(self.name, "ambient_field", config.CONFIG_OBJECT)) max_field = self.max_amps * self.coil_constant # max field reachable in this axis self.max_field = np.array([-max_field, max_field]) @@ -145,7 +142,7 @@ class ArduinoCtrl(Arduino): self.connected = "Unknown" self.pins = [0, 0, 0] for i in range(3): # get correct pins from config file - self.pins[i] = int(read_from_config(g.AXIS_NAMES[i], "relay_pin", g.CONFIG_OBJECT)) + self.pins[i] = int(config.read_from_config(g.AXIS_NAMES[i], "relay_pin", config.CONFIG_OBJECT)) ui_print("\nConnecting to Arduino...") try: Arduino.__init__(self) # search for connected arduino and connect @@ -180,111 +177,6 @@ class ArduinoCtrl(Arduino): self.digitalWrite(pin, "LOW") -def get_config_from_file(file=g.CONFIG_FILE): - config_object = ConfigParser() # initialize config parser - config_object.read(file) # open config file - return config_object # return config object, that contains all info from the file - - -def write_config_to_file(config_object): # write contents of config object to a config file - with open(g.CONFIG_FILE, 'w') as conf: # Write changes to config file - config_object.write(conf) - - -def read_from_config(section, key, config_object): # read specific value from config object - try: - section_obj = config_object[section] # get relevant section - value = section_obj[key] # get relevant value in the section - return value - except KeyError as e: - ui_print("Error while reading config file:", e) - raise KeyError("Could not find key", key, "in config file.") - - -def edit_config(section, key, value, override=False): # edit specific value in config file - config_object = g.CONFIG_OBJECT - # ToDo: add check for data types, e.g. int for arduino ports - - # Check if value to write is within acceptable limits (set in globals.py) - try: - value_ok = 'OK' - if section in g.AXIS_NAMES and not override: # only check numerical values and not if overridden by user - value_ok = value_in_limits(section, key, value) # check if value is ok, too high or too low - max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value - min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] # get min value - if value_ok == 'HIGH': - message = "Prevented writing too high value for {s} {k} to config file:\n" \ - "{v}, max. {mv} allowed. Erroneous values may damage equipment!" \ - .format(s=section, k=key, v=value, mv=max_value) - raise ValueError(message) - elif value_ok == 'LOW': - message = "Prevented writing too low value for {s} {k} to config file:\n" \ - "{v}, max. {mv} allowed. Erroneous values may damage equipment!" \ - .format(s=section, k=key, v=value, mv=min_value) - raise ValueError(message) - - if value_ok == 'OK' or override: # value is within limits or user has overridden - try: - section_obj = config_object[section] # get relevant section - except KeyError: - ui_print("Could not find section", section, "in config file, creating new.") - config_object.add_section(section) - section_obj = config_object[section] - try: - section_obj[key] = str(value) # set relevant value in the section - except KeyError: - ui_print("Could not find key", key, "in config file, creating new.") - config_object.set(section, key, str(value)) - - except KeyError as e: - ui_print("Error while editing config file:", e) - raise KeyError("Could not find key", key, "in config file.") - - -def check_config(config_object): # check all numeric values in the config and see if they are within safe limits - ui_print("Checking config file for values exceeding limits:") - i = 0 - concerns = {} # initialize dictionary for found problems - problem_counter = 0 - for axis in g.AXIS_NAMES: - concerns[axis] = [] # create dictionary entry for this axis - for key in g.default_arrays.keys(): # go over entries in this axis - value = float(read_from_config(axis, key, config_object)) # read value to check from config file - max_value = g.default_arrays[key][1][i] # get max value - min_value = g.default_arrays[key][2][i] # get min value - - if not min_value <= value <= max_value: # value is not in safe limits - concerns[axis].append(key) # add this entry to the problem dictionary - problem_counter += 1 - - if len(concerns[axis]) == 0: - concerns[axis].append("No problems detected.") - - ui_print(axis, ":", *concerns[axis]) # print out results for this axis - i += 1 - if problem_counter > 0: # some values are not ok - # shop pup-up warning message: - messagebox.showwarning("Warning!", "Found values exceeding limits in config file. Check values " - "to ensure correct operation and avoid equipment damage!") - g.app.show_frame(ui.Configuration) # open configuration window so user can check values - - -def reset_config_to_default(file): # reset values in config object to defaults (stored in globals.py) - config = ConfigParser() # initialize global config object - g.CONFIG_OBJECT = config - - i = 0 - for axis_name in g.AXIS_NAMES: # go through axes - config.add_section(axis_name) # add section for this axis - for key in g.default_arrays.keys(): # go through dictionary with default values - config.set(axis_name, key, str(g.default_arrays[key][0][i])) # set value - i += 1 - - config.add_section("PORTS") # add section for PSU serial ports - for key in g.default_ports.keys(): - config.set("PORTS", key, str(g.default_ports[key])) - - def ui_print(*content): # prints text to built in console output = "" for text in content: @@ -331,8 +223,8 @@ def setup_all(): # main initialization function, creates device objects for all g.AXES = [] - g.XY_PORT = read_from_config("PORTS", "xy_port", g.CONFIG_OBJECT) - g.Z_PORT = read_from_config("PORTS", "z_port", g.CONFIG_OBJECT) + g.XY_PORT = config.read_from_config("PORTS", "xy_port", config.CONFIG_OBJECT) + g.Z_PORT = config.read_from_config("PORTS", "z_port", config.CONFIG_OBJECT) g.PORTS = [g.XY_PORT, g.XY_PORT, g.Z_PORT] ui_print("\nConnecting to XY Device on %s..." % g.XY_PORT) diff --git a/config_handling.py b/config_handling.py new file mode 100644 index 0000000..13ff1ad --- /dev/null +++ b/config_handling.py @@ -0,0 +1,115 @@ +from configparser import ConfigParser +from tkinter import messagebox + +import globals as g +import cage_func as func +# noinspection PyPep8Naming +import User_Interface as ui + +global CONFIG_FILE +global CONFIG_OBJECT + + +def get_config_from_file(file): + config_object = ConfigParser() # initialize config parser + config_object.read(file) # open config file + return config_object # return config object, that contains all info from the file + + +def write_config_to_file(config_object): # write contents of config object to a config file + with open(CONFIG_FILE, 'w') as conf: # Write changes to config file + config_object.write(conf) + + +def read_from_config(section, key, config_object): # read specific value from config object + try: + section_obj = config_object[section] # get relevant section + value = section_obj[key] # get relevant value in the section + return value + except KeyError as e: + func.ui_print("Error while reading config file:", e) + raise KeyError("Could not find key", key, "in config file.") + + +def edit_config(section, key, value, override=False): # edit specific value in config file + config_object = CONFIG_OBJECT + # ToDo: add check for data types, e.g. int for arduino ports + + # Check if value to write is within acceptable limits (set in globals.py) + try: + value_ok = 'OK' + if section in g.AXIS_NAMES and not override: # only check numerical values and not if overridden by user + value_ok = func.value_in_limits(section, key, value) # check if value is ok, too high or too low + max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value + min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] # get min value + if value_ok == 'HIGH': + message = "Prevented writing too high value for {s} {k} to config file:\n" \ + "{v}, max. {mv} allowed. Erroneous values may damage equipment!" \ + .format(s=section, k=key, v=value, mv=max_value) + raise ValueError(message) + elif value_ok == 'LOW': + message = "Prevented writing too low value for {s} {k} to config file:\n" \ + "{v}, max. {mv} allowed. Erroneous values may damage equipment!" \ + .format(s=section, k=key, v=value, mv=min_value) + raise ValueError(message) + + if value_ok == 'OK' or override: # value is within limits or user has overridden + try: + section_obj = config_object[section] # get relevant section + except KeyError: + func.ui_print("Could not find section", section, "in config file, creating new.") + config_object.add_section(section) + section_obj = config_object[section] + try: + section_obj[key] = str(value) # set relevant value in the section + except KeyError: + func.ui_print("Could not find key", key, "in config file, creating new.") + config_object.set(section, key, str(value)) + + except KeyError as e: + func.ui_print("Error while editing config file:", e) + raise KeyError("Could not find key", key, "in config file.") + + +def check_config(config_object): # check all numeric values in the config and see if they are within safe limits + func.ui_print("Checking config file for values exceeding limits:") + i = 0 + concerns = {} # initialize dictionary for found problems + problem_counter = 0 + for axis in g.AXIS_NAMES: + concerns[axis] = [] # create dictionary entry for this axis + for key in g.default_arrays.keys(): # go over entries in this axis + value = float(read_from_config(axis, key, config_object)) # read value to check from config file + max_value = g.default_arrays[key][1][i] # get max value + min_value = g.default_arrays[key][2][i] # get min value + + if not min_value <= value <= max_value: # value is not in safe limits + concerns[axis].append(key) # add this entry to the problem dictionary + problem_counter += 1 + + if len(concerns[axis]) == 0: + concerns[axis].append("No problems detected.") + + func.ui_print(axis, ":", *concerns[axis]) # print out results for this axis + i += 1 + if problem_counter > 0: # some values are not ok + # shop pup-up warning message: + messagebox.showwarning("Warning!", "Found values exceeding limits in config file. Check values " + "to ensure correct operation and avoid equipment damage!") + g.app.show_frame(ui.Configuration) # open configuration window so user can check values + + +def reset_config_to_default(file): # reset values in config object to defaults (stored in globals.py) + config = ConfigParser() # initialize global config object + config.CONFIG_OBJECT = config + + i = 0 + for axis_name in g.AXIS_NAMES: # go through axes + config.add_section(axis_name) # add section for this axis + for key in g.default_arrays.keys(): # go through dictionary with default values + config.set(axis_name, key, str(g.default_arrays[key][0][i])) # set value + i += 1 + + config.add_section("PORTS") # add section for PSU serial ports + for key in g.default_ports.keys(): + config.set("PORTS", key, str(g.default_ports[key])) \ No newline at end of file diff --git a/globals.py b/globals.py index 631e521..f33f60a 100644 --- a/globals.py +++ b/globals.py @@ -15,9 +15,6 @@ app = None AXIS_NAMES = ["X-Axis", "Y-Axis", "Z-Axis"] -CONFIG_FILE = None -CONFIG_OBJECT = None - global XY_PORT global Z_PORT @@ -37,6 +34,7 @@ default_arrays = { "resistance": np.array([[1.7, 1.7, 1.7], [5, 5, 5], [1, 1, 1]], dtype=float), # resistance of circuits [Ohm] "max_watts": np.array([[15, 15, 15], [50, 50, 50], [0, 0, 0]], dtype=float), # max. allowed power for circuits [W] "max_volts": np.array([[14, 14, 14], [16, 16, 16], [0, 0, 0]], dtype=float), # max. allowed voltage, limited to 16V by used diodes! [V] + "max_amps": np.array([[4.5, 4.5, 4.5], [6, 6, 6], [0, 0, 0]], dtype=float), # max. allowed current (A) "relay_pin": [[15, 16, 17], [15, 16, 17], [15, 16, 17]] # pins on the arduino for reversing [x,y,z] polarity } default_ports = { diff --git a/main.py b/main.py index 7067a1a..d1dc877 100644 --- a/main.py +++ b/main.py @@ -1,20 +1,22 @@ +from os.path import exists + from User_Interface import HelmholtzGUI import cage_func as func import traceback import globals as g -from os.path import exists +import config_handling as config + try: # start normal operations - g.CONFIG_FILE = 'config.ini' + config.CONFIG_FILE = 'config.ini' # ToDo: remember what the last config file was - if not exists(g.CONFIG_FILE): + if not exists(config.CONFIG_FILE): print("Config file not found, creating new from defaults.") - func.reset_config_to_default(g.CONFIG_FILE) - func.write_config_to_file(g.CONFIG_OBJECT) + config.reset_config_to_default(config.CONFIG_FILE) + config.write_config_to_file(config.CONFIG_OBJECT) - g.CONFIG_OBJECT = func.get_config_from_file(g.CONFIG_FILE) - print(g.CONFIG_OBJECT) + config.CONFIG_OBJECT = config.get_config_from_file(config.CONFIG_FILE) print("Starting setup...") func.setup_all() # initiate communication, set handles @@ -23,7 +25,7 @@ try: # start normal operations g.app = HelmholtzGUI() func.ui_print("Program Initialized") - func.check_config(g.CONFIG_OBJECT) # check config for values exceeding limits + config.check_config(config.CONFIG_OBJECT) # check config for values exceeding limits func.ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once func.setup_all() # initiate communication, set handles From 063a7049d0f6ec5698b5a960427b562e0ebbfb84 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Sun, 7 Feb 2021 15:15:05 +0100 Subject: [PATCH 19/36] moved ui_print function to User_Interface.py --- User_Interface.py | 42 ++++++++++++++++++++++++++++-------------- cage_func.py | 14 +------------- config_handling.py | 19 ++++++++++--------- csv_threading.py | 15 ++++++++------- main.py | 7 ++++--- 5 files changed, 51 insertions(+), 46 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index a056c59..bb2db06 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -214,7 +214,7 @@ class ManualMode(Frame): field = g.AXES[i].max_comp_field * 1e6 else: field = [0, 0] - func.ui_print("Unexpected value encountered: compensate =", comp) + ui_print("Unexpected value encountered: compensate =", comp) val.set("(%0.1f to %0.1f \u03BCT)" % (field[0], field[1])) i += 1 @@ -236,22 +236,22 @@ class ManualMode(Frame): self.controller.StatusDisplay.update_labels() # update status display after change def execute_field(self, vector): - func.ui_print("field executing", vector) + ui_print("field executing", vector) comp = self.compensate.get() if comp == 1: func.set_field(vector * 1e-6) elif comp == 0: func.set_field_simple(vector * 1e-6) else: - func.ui_print("Unexpected value encountered: compensate =", comp) + ui_print("Unexpected value encountered: compensate =", comp) @staticmethod def execute_current(vector): - func.ui_print("current executing:", vector) + ui_print("current executing:", vector) try: func.set_current_vec(vector) except ValueError as e: - func.ui_print(e) + ui_print(e) class Configuration(Frame): @@ -425,7 +425,7 @@ class Configuration(Frame): 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 - func.ui_print("Invalid entry for %s %s %s" % (g.AXIS_NAMES[i], key, e)) + ui_print("Invalid entry for %s %s %s" % (g.AXIS_NAMES[i], key, e)) else: # format is ok factor = self.entries[key][4] # get unit conversion factor @@ -484,9 +484,9 @@ class Configuration(Frame): func.setup_all() # 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 - func.ui_print("No file selected, could not load config.") + ui_print("No file selected, could not load config.") else: - func.ui_print("Selected file", filename, "does not exist, could not load config.") + 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 @@ -496,17 +496,19 @@ class Configuration(Frame): defaultextension=[("Config File", "*.ini*")]) if filename == '': # this happens when file selection window is closed without selecting a file - func.ui_print("No file selected, could not save config.") + 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 + func.setup_all() # 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() config.write_config_to_file(config.CONFIG_OBJECT) - self.update_fields() + func.setup_all() # reinitialize devices and program with new values + self.update_fields() # update entry fields to show values as they are in the config class ExecuteCSVMode(Frame): @@ -563,19 +565,19 @@ class ExecuteCSVMode(Frame): filename = filedialog.askopenfilename(initialdir=directory, title="Select CSV File", filetypes=(("Comma Separated Values", "*.csv*"), ("All Files", "*.*"))) if exists(filename): # does the file exist? - func.ui_print("File selected:", filename) + ui_print("File selected:", filename) try: self.sequence_array = csv.read_csv_to_array(filename) except BaseException as e: - func.ui_print("Error while opening file:", e) + ui_print("Error while opening file:", e) # ToDo: make error a popup # ToDo: check for excessive values self.execute_button["state"] = "normal" # activate run button elif filename == '': # this happens when file selection window is closed without selecting a file - func.ui_print("No file selected, could not load.") + ui_print("No file selected, could not load.") else: - func.ui_print("Selected file", filename, "does not exist, could not load.") + ui_print("Selected file", filename, "does not exist, could not load.") def run_sequence(self): # (de)activate buttons as needed: @@ -688,3 +690,15 @@ class OutputConsole(Frame): # console to print stuff in, similar to standard py self.console.grid(row=0, column=0, sticky="nesw") scrollbar.config(command=self.console.yview) self.console.config(yscrollcommand=scrollbar.set) + + +def ui_print(*content): # prints text to built in console + output = "" + for text in content: + output = " ".join((output, str(text))) # append content + if g.app is not None: + output = "".join(("\n", output)) # begin new line each time + g.app.OutputConsole.console.insert(END, output) # print to console + g.app.OutputConsole.console.see(END) # scroll to bottom + else: # if window is not open, do normal print + print(output) \ No newline at end of file diff --git a/cage_func.py b/cage_func.py index a892022..e3f6c75 100644 --- a/cage_func.py +++ b/cage_func.py @@ -1,8 +1,8 @@ import numpy as np import serial import traceback -from tkinter import * +from User_Interface import ui_print from pyps2000b import PS2000B from Arduino import Arduino # noinspection PyPep8Naming @@ -177,18 +177,6 @@ class ArduinoCtrl(Arduino): self.digitalWrite(pin, "LOW") -def ui_print(*content): # prints text to built in console - output = "" - for text in content: - output = " ".join((output, str(text))) # append content - if g.app is not None: - output = "".join(("\n", output)) # begin new line each time - g.app.OutputConsole.console.insert(END, output) # print to console - g.app.OutputConsole.console.see(END) # scroll to bottom - else: # if window is not open, do normal print - print(output) - - def value_in_limits(axis, key, value): # Check if value is within safe limits (set in globals.py) max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(axis)] # get max value min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(axis)] # get min value diff --git a/config_handling.py b/config_handling.py index 13ff1ad..5651efe 100644 --- a/config_handling.py +++ b/config_handling.py @@ -6,8 +6,8 @@ import cage_func as func # noinspection PyPep8Naming import User_Interface as ui -global CONFIG_FILE -global CONFIG_OBJECT +global CONFIG_FILE # variable storing the path of the used config file +global CONFIG_OBJECT # object of type ConfigParser(), storing all configuration information def get_config_from_file(file): @@ -27,7 +27,7 @@ def read_from_config(section, key, config_object): # read specific value from c value = section_obj[key] # get relevant value in the section return value except KeyError as e: - func.ui_print("Error while reading config file:", e) + ui.ui_print("Error while reading config file:", e) raise KeyError("Could not find key", key, "in config file.") @@ -57,22 +57,22 @@ def edit_config(section, key, value, override=False): # edit specific value in try: section_obj = config_object[section] # get relevant section except KeyError: - func.ui_print("Could not find section", section, "in config file, creating new.") + ui.ui_print("Could not find section", section, "in config file, creating new.") config_object.add_section(section) section_obj = config_object[section] try: section_obj[key] = str(value) # set relevant value in the section except KeyError: - func.ui_print("Could not find key", key, "in config file, creating new.") + ui.ui_print("Could not find key", key, "in config file, creating new.") config_object.set(section, key, str(value)) except KeyError as e: - func.ui_print("Error while editing config file:", e) + ui.ui_print("Error while editing config file:", e) raise KeyError("Could not find key", key, "in config file.") def check_config(config_object): # check all numeric values in the config and see if they are within safe limits - func.ui_print("Checking config file for values exceeding limits:") + ui.ui_print("Checking config file for values exceeding limits:") i = 0 concerns = {} # initialize dictionary for found problems problem_counter = 0 @@ -90,7 +90,7 @@ def check_config(config_object): # check all numeric values in the config and s if len(concerns[axis]) == 0: concerns[axis].append("No problems detected.") - func.ui_print(axis, ":", *concerns[axis]) # print out results for this axis + ui.ui_print(axis, ":", *concerns[axis]) # print out results for this axis i += 1 if problem_counter > 0: # some values are not ok # shop pup-up warning message: @@ -101,7 +101,8 @@ def check_config(config_object): # check all numeric values in the config and s def reset_config_to_default(file): # reset values in config object to defaults (stored in globals.py) config = ConfigParser() # initialize global config object - config.CONFIG_OBJECT = config + global CONFIG_OBJECT + CONFIG_OBJECT = config i = 0 for axis_name in g.AXIS_NAMES: # go through axes diff --git a/csv_threading.py b/csv_threading.py index fedd674..0841dae 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -1,3 +1,4 @@ +import User_Interface as ui import cage_func as func import time import pandas @@ -16,7 +17,7 @@ class ExecCSVThread(Thread): self.controller = controller # object on which mainloop() is running, usually main window def run(self): - func.ui_print("Starting Sequence Execution...") + ui.ui_print("Starting Sequence Execution...") # g.threadLock.acquire() # Get lock to synchronize threads # ToDo: add locking/synchronization? Works without so far but might be more robust execute_sequence(self.array, 0.1, self.controller) @@ -38,10 +39,10 @@ def execute_sequence(array, delay, controller): # runs through array containing t = time.time() - t_zero # get relative time if t >= array[i, 0]: # time for this row has come field_vec = array[i, 1:4] # extract desired field vector - func.ui_print("%f s: t = %0.2f s, target field vector = " - % (time.time()-t_zero, array[i, 0]), field_vec*1e6, "\u03BCT") + ui.ui_print("%f s: t = %0.2f s, target field vector = " + % (time.time()-t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") func.set_field_simple(field_vec) # send field vector to test stand # ToDo!: reset to set_field() - func.ui_print(time.time()-t_zero) + ui.ui_print(time.time() - t_zero) controller.StatusDisplay.update_labels() # update status display after change i = i + 1 # next row @@ -50,16 +51,16 @@ def execute_sequence(array, delay, controller): # runs through array containing else: # sleep to give other threads time to run time.sleep(delay) if g.running: # sequence ended without interruption - func.ui_print("Sequence executed, powering down channels.") + ui.ui_print("Sequence executed, powering down channels.") else: # interrupted by user - func.ui_print("Sequence cancelled, powering down channels.") + ui.ui_print("Sequence cancelled, powering down channels.") func.power_down_all() # set currents and voltages to 0, set arduino pins to low def read_csv_to_array(filepath): # csv format: time (s); xField (T); yField (T); zField (T) # decimal commas - func.ui_print("Reading File:", filepath) + ui.ui_print("Reading File:", filepath) file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file array = file.to_numpy() # convert csv to array return array diff --git a/main.py b/main.py index d1dc877..2bd46c0 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,8 @@ from os.path import exists -from User_Interface import HelmholtzGUI import cage_func as func +from User_Interface import HelmholtzGUI +from User_Interface import ui_print import traceback import globals as g import config_handling as config @@ -24,10 +25,10 @@ try: # start normal operations print("\nOpening User Interface...") g.app = HelmholtzGUI() - func.ui_print("Program Initialized") + ui_print("Program Initialized") config.check_config(config.CONFIG_OBJECT) # check config for values exceeding limits - func.ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once + ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once func.setup_all() # initiate communication, set handles g.app.mainloop() g.app = None # reset to None so nothing tries to print in the UI output From f9d0f8c69b4034e6de3c95a81936d3b72cf40919 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Sun, 7 Feb 2021 15:47:26 +0100 Subject: [PATCH 20/36] moved error messages and program end message to messagebox --- User_Interface.py | 3 ++- cage_func.py | 26 ++++++++++++++++++++------ csv_threading.py | 11 +++++------ main.py | 8 ++++++-- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index bb2db06..36577f0 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -649,7 +649,8 @@ class StatusDisplay(Frame): def continuous_label_update(self, controller, interval): # update display values in regular intervals self.update_labels() - controller.after(interval, lambda: self.continuous_label_update(controller, interval)) + if g.app is not None: + controller.after(interval, lambda: self.continuous_label_update(controller, interval)) def update_labels(self): g.ARDUINO.update_status_info() diff --git a/cage_func.py b/cage_func.py index e3f6c75..300be0d 100644 --- a/cage_func.py +++ b/cage_func.py @@ -1,6 +1,7 @@ import numpy as np import serial import traceback +from tkinter import messagebox from User_Interface import ui_print from pyps2000b import PS2000B @@ -279,42 +280,55 @@ def power_down_all(): # temporary, set all outputs to 0 but keep connections en def shut_down_all(): # shutdown at program end or on error, set outputs to 0 and disable connections # ToDo: remove checks if connected or make them only for printing ui_print("\nAttempting to safely shut down all devices. Check equipment to confirm.") + message = "Tried to safely shut down all devices. Check equipment to confirm." if g.XY_DEVICE is not None: try: set_to_zero(g.XY_DEVICE) g.XY_DEVICE.disable_all() except BaseException as e: ui_print("Error while deactivating XY PSU:", e) + message += "\nError while deactivating XY PSU: %s" % e else: ui_print("XY PSU deactivated.") + message += "\nXY PSU deactivated." else: ui_print("XY PSU not connected, can't deactivate.") + message += "\nXY PSU not connected, can't deactivate." if g.Z_DEVICE is not None: try: set_to_zero(g.Z_DEVICE) g.Z_DEVICE.disable_all() except BaseException as e: ui_print("Error while deactivating Z PSU:", e) + message += "\nError while deactivating Z PSU: %s" % e else: ui_print("Z PSU deactivated.") + message += "\nZ PSU deactivated." else: ui_print("Z PSU not connected, can't deactivate.") + message += "\nZ PSU not connected, can't deactivate." try: g.ARDUINO.safe() except BaseException as e: ui_print("Arduino safing unsuccessful:", e) + message += "\nArduino safing unsuccessful: %s" % e # this throws no exception, even when arduino is not connected # ToDo (optional): figure out error handling for this - if g.ARDUINO.connected == "Connected": - try: - g.ARDUINO.close() - except BaseException as e: + try: + g.ARDUINO.close() + except BaseException as e: + if g.ARDUINO.connected == "Connected": ui_print("Closing Arduino connection failed:", e) + message += "\nClosing Arduino connection failed: %s" % e else: - ui_print("Serial connection to Arduino closed.") + ui_print("Arduino not connected, can't close connection.") + message += "\nArduino not connected, can't close connection." else: - ui_print("Arduino not connected, can't close connection.") + ui_print("Serial connection to Arduino closed.") + message += "\nSerial connection to Arduino closed." + + messagebox.showinfo("Shutdown Status", message) def set_field_simple(vector): # forms magnetic field as specified by vector, w/o cancelling ambient field diff --git a/csv_threading.py b/csv_threading.py index 0841dae..16fcb7f 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -8,21 +8,20 @@ import globals as g class ExecCSVThread(Thread): def __init__(self, threadID, array, parent, controller): - # ToDo: comments Thread.__init__(self) self.threadID = threadID self.array = array # numpy array containing data from csv to be executed - self.parent = parent + self.parent = parent # object from which this is called self.controller = controller # object on which mainloop() is running, usually main window def run(self): ui.ui_print("Starting Sequence Execution...") # g.threadLock.acquire() # Get lock to synchronize threads # ToDo: add locking/synchronization? Works without so far but might be more robust - execute_sequence(self.array, 0.1, self.controller) - self.parent.running = False - # reset buttons: + execute_sequence(self.array, 0.1, self.controller) # run sequence + self.parent.running = False # sequence finished --> no longer running + # reset buttons on UI: self.parent.select_file_button["state"] = "normal" self.parent.execute_button["state"] = "normal" self.parent.stop_button["state"] = "disabled" @@ -58,7 +57,7 @@ def execute_sequence(array, delay, controller): # runs through array containing def read_csv_to_array(filepath): - # csv format: time (s); xField (T); yField (T); zField (T) + # csv format: time (s); xField (T); yField (T); zField (T) (german excel) # decimal commas ui.ui_print("Reading File:", filepath) file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file diff --git a/main.py b/main.py index 2bd46c0..7edfb79 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,10 @@ from os.path import exists +import traceback +from tkinter import messagebox import cage_func as func from User_Interface import HelmholtzGUI from User_Interface import ui_print -import traceback import globals as g import config_handling as config @@ -36,7 +37,10 @@ try: # start normal operations except BaseException as e: # if there is an error, print what happened print("\nAn error occurred, Shutting down.") - print(e) + # shop pup-up error message: + message = "%s.\nSee python console traceback for more details. " \ + "\nShutting down devices, check equipment to confirm." % e + messagebox.showerror("Error!", message) print(traceback.print_exc()) finally: # safely shut everything down at the end From f0161d849dee24a2287c81be57f5e81ae62649b4 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Mon, 8 Feb 2021 20:10:20 +0100 Subject: [PATCH 21/36] many things --- Test cases.xlsx | Bin 12889 -> 18028 bytes User_Interface.py | 80 +++++++++++++++++++++++++++++++++++----------- cage_func.py | 39 ++++++++++++++++++++-- csv_threading.py | 75 +++++++++++++++++++++++++++++++++++++------ globals.py | 2 +- main.py | 2 ++ requirements.txt | 3 +- 7 files changed, 169 insertions(+), 32 deletions(-) diff --git a/Test cases.xlsx b/Test cases.xlsx index 88dda9cf58ce409302f45180b78cc44aadcc8315..da0c57af67d80b2605f18655ffaa09d840b61233 100644 GIT binary patch delta 8940 zcmaJ{bzD`;w?A}uhje#?lyplgDIwCG(wjqylyE31X`~c5bcb|ENH-`T-EpXg-uHXn zd*A!K{m+@%GqYx`b@pDfzTdTHU+qT2k!q?UAQFO*L8u@Qh!*6PLpan32LcVDRFcxc z14+Z`J3P2y7fQ{t-}j!acf!+tOGH{|YNmn1G26AZn5^Xd3fi0Jchw2iv^6A;e0sO( zzd1AF?bhO8e1wkXc$g%@-kS^h98wu*5?Ry>F9&;E9d* zD)9xiniKFnv0^&;h?ucN$A@#gYq{q1A5M_QVQlmP;#|VZYi9~XH&g@#$5dZc9fBoCfPR!CLzMS%y*Tp8Kha{V-GXE_0OCFR zX*RGA&0W++2*qXD;(8au*_7eC#qOi^CA2daK|hAXD*;l-yf*PdTqZL>?G^HZ-MDnK zQDPm51ph85$KAZ?9ZYKAXL_2E;Bb4CzO%>cBOm*E4ccRYaKs3TnQBemrQ#$}PM$AYGfWoiIBR>*w<<(r zh`p}b$sF4yxy;TO_7P8aM}FR@GRw5NtVjLzlL!Wj17kJx-H8NZ;EQG;#W8a^am0}n z&Y{mIYNkMo@D+wa==+`aapk>cJU~fxSuW>)vNf&JWWH%oW?S2@eXARa7!{w$ANLj)yeRK zrg;-v(HL&7gvW7jQ?$jspG$N4#;~(>!fAk`P8gKKl~lGB=)Ku-_cP?j59?s}rPF<_ z_K%)L1cZHPIhHCUeZ|fPLx2C4PaM_3;J|a_VB*YrRcrQ4c$a-L{j8j3(`I}=CEF#y z0zn3+wGK;Oh`0jz1+6V_qbm@gi{)y#4SbAv7s7+b%HI;0c3-X?k7WYrR$u|ARLKq( z`3^s79~X_xQV-XypF>J2hJC8u5b%vwU!dtu&?Papike2JP*?VzXN&d3&yEqZ4~Smj z^`U=@lD+1L*mmF0ktb*?TLT|&<7fp>N%3+Y^=sX1g>AVc5gI0VhO?R@a&Ohp^NOB& zA7O$(_xA{(f4=_>xzr^751~L0>VuJo0vLQ~cO6tUJtR!ML{4@#P3N&^)Ih0~h;Y^< zJ$|AK?!VqVHAt{3ho5be8gzlIG;O3WYiS>ib z16WpFMJ|gS-+Gtbd-6ZJoE8QzQlw3ofAxGfebVyN^Q*h{#LiB;u%r9A@P+6NJ75xC z#J$%wKH|nbh-mVjd%I`qyYp;A<;Wx5Q(H{1(AUMf>y0Gq_bu(buUXw2=#E{Jvd0eY${P`bKj9t*@m7TFyJ$% zV)BsCkwe_sTA@460*|v0vvo+39+Q@Cog^jG>cS;RFWncn;Y-)rk9;E5C6r}Dia$a4 zOv8DSKJUpAVA@U=oYcNsm06aF!9X!p`}v}_*y-fJ5t=>>CUo-gwm3d40WcSyBP)Cp z&IQZxrm2M#J+BqBJQ=OFpBFyh6in~=?t=;0e2eD9t7Q}Q!C5Xu6rK-Pi-~5Is=vz8W+-P#c{Wo<20wo0Y%UE5 zWnD_E#XuM0P@IF^>8M8~h8JbRNcvo|h5;Hr@o@3Cn({EFZN#&3Z zpTRg98Ip=nthJY01-|H%es{y%EO6~6*D}r73n)I9M@|^{o}>7qL>-t6STCM5wqVqN z;T2UoNN~-cAE8P!ESMx)iP)G`Zy7l?%@KV(pE}uq+2HMb>lI{~(bq0pIR_hd?fsZ- zx0$VL+PBlgk(e`imgM{??%W|0_Av`}x?H)5lfScdR?NmM|3a83J*j+6mkRKyLDG)Pfui~wZ;+?@~=FlP`<+;kb3YU!Hm!vEYyakNsN_dY=hnTst zmejqsh;YuyejICHCX7iJ!`PN5X?s=SrN zVK>;dLz#{y{TUW~Zd#js{rF(rXNvjzU*`-q(dQEJmWR}V{UK?f2SM(+DF9wBZ$}3% zH%kZi(70jrP9EIQgWv<)bEk1%-1tHhxkI)yxa3#YA&&S}M@7!gT*1LBj@Bu+R)up< zinJD3k5@9owTWr^$edB8ZR-VkCy5c6J%|+#l)BU~uYDUnjtIOQ&{)6$%NL4FjTRAl zsZ{>7l}VO-H<&Wji>8xYsUr&WZ4ufVec9V80nf@ze;EcPCxnLsu;osdn;)!W{B0cw zt&_Dox9Kkn|I__{ZEkrpTKWw|e|!(8R=H21(%Cti1gT@9LTp!9O#Up5 z`6$0V4@&O#9!z6zEH-D-Ro<{0OCyr&=;~&hEWq(aGim;*YXO>t=$iQ{2Yz=|IIRMp z{dQQ2zse)a!A+FJD%uva<=AF_1~O+oD;=hRoM@`1RYxwfxIvTFUG)9lVcIh<2g)D! z-r)u>XDEo4i6%TN7nyY$>vO{3DaXeuP17F(!oN%gf+!Zi;}6!xJy=Wq@cUq`g^jtJ z$A4`7-T7l_*}D#vFFd&6zy93~r-@x@IibZ6n%!s9D5yWHvC?04s^MOr7m`#)DaP@4 z-3L5z7Y``-C>hVFS!>ovMQEjA(FYBS8{Fw4U@7K`Bxa*dcSM#Dxg;?e+qvl-q=pX&C_BhYqLvQ?{j z(aQVK!Z{z6v_jU)7`vgYlH0ZVR!xS$&-Pyw+{GG=zckJ*3D`4`k&`JC)y_4D7?n6=nP86}OVAwteH9 z!O2fCifyt4c>^0tRDoOkKCT#oz23^2ZrrZI*45M6yz`LPrw|qe1dz!DyT5wyiG%x zgFyeebJ|38j##}$VaKgAncteJe<&@=yP8XQ#i@JK>_Chfv2CkKpf!UQ|MgFMO9x0A zGRHmlyr7~8mPc3YK4i1`s%Hck-*s^?^FXJnkNvSPnD2>x0jKWVaQnS0Us$N~wX7qx_VtdGS@4(W@4&Br%n5BTcUeGqZ@xkk} zPlA&>vbxmkFe%ZtqHpX>J+f-iKp2__x4$G?I;jQ0dQ>D{sFqvcBycpY~#|(nX*60hHd2voBCdER1Q^ zGuWXzbM``V^J+Hf+1&|o19MjT^JS*7R>DmfV{~dKVsm{pJrC{Gm(dunC>!|1y661g z)HGCt6PtgC8<^UdnQN{Snoh?^QmkJ%80bj28_hW;O+k6_SfA-ahycgs_5B@JXfl}{ zlO&tHff*-;a+L1{K+?!47sWGtV8a#q)@Vm8w>y=1L0QMJpJHu@U}U3@j5;aPXXZU~ zxzT~X%o*ONi)+}BPwl zH>3imKt6f^z8Pzn)lDU}mlLslegBEur=;E7HWd|2McfBO5P{_gl=AnFoo=I%5b@}P zBPD;nIgA2!tcAX`>mzhNW&}?M+CBq^Qj zDOp_NBlPB<7k`P?w`jM0^;9nl>Dfd!rN&aQxcc!+V_N~vL5SG;NjiVVUL^583Y@HX zmp%eA-{Bmv6TI!L*~#qqg;``h%C-AL@^NIw;3UIsAttk5uDoC5kyB~7H&ta1YNC&^ z-xEziEcD9?SDGcj^$W@RTPDu$5H7{#N*E$3DIVv>g_Ncj|AvKG8Qf|{%H}D|Pj=_? zVzBLnvU)%GE=f0{tSlyDzuOA^)ojV4qbtYdLInY^{PI2b20?_{X38_r5lj7uh>|qOY)BM!@%|j^r-)VkUvu8o( z!^j*H2=ox={ub%oJ$xK2f5-S;Lr3Q|5!^d=AQ_NC@c3E#Os9C-t^l&-SW`D1AQXwv z>baZYm^^L@SZszh*Sa90``v~!cN~fH2U>}*r%RclxaSUiuIXPFa6bp#V~n;SOJRYp9~Z?xhz%k1umMv2VW{Pgc4c(j z^D$7G64hQ$IyHosFPPNpBxdV}SG-5vbi0bE-uAy$PI^g;^(GMBEbJP7MBCD-?l`y@mRY;W;4)B*sfX*FC?KRx zmx*YaI&LLX?L&u7cY;fAMrTnBFvHWqul?ZYu;H3iePOj6qKU1+W5_nR2>XDfMM!_w z1o9GVBL!O9`|3Lm-8_6E z(9;{isW)?&VPsDnRQei2e_jtiVRh0B^SaBDK_%3d?{Z*6ePcz)arHBQ$Vbu^P;t;+ zJc|<2HNw8cZ4>yD>c!I9Ahr@kwH{m*q7l{rnt`;3j4zP}WBxIm4jf9!_-+`?JL3`A z+xwet!WhCxKljBO%__(aFo%1r;x(mIgT2!5ESn>6x&O5K>#N5Es41xkOV>{dC>bIH z7&DFLITBTmC~Rr70d^XB-jBo56j2VZs+7HKJh8N0$%nD$&RGRx`@WX-f3 zi>Z9j>+Kb@%3U}u(5yX*V{QF*DP|-@9gtK-?EBVfGtp0s?zj=lf!SW)BYu`u7bS1% z{$<7dk;r5-j#5E5PKH_~zZsc9p<=zSmP6-s)046CpU^vzvfPE@?@PDLwH#PiN!A|a zDiW!x$dpLJL*uSdhnmBgGPUsT=vuO4bu&MEs?`fHPb*jWL%0_V*r+p}Wauj9mnl9! z7}w=3(P&}6@X{zP1gu4Gva17G%EJqvhp@h_urK43Ynge+6>IrzxlF zEZljE#7z3s;bs3h&3u82UxucQEG>SA*!51^6~Mqfrl*v)z(iT2gsm`txne&RUQsG? zF@ugEDTw~aCxKR;FC$-<#e03l2$<#Mu4-PH3C55JyER*(4)f~l!Ar;hujshVV{~mP z^{Y5Z86=b9&84m4)tE)v*(F;$)P$C3&`VaUo@d)5zv&FmIEpaj%^g;#zIszjIyRsl z#l9G#gG)G@Zcm~Myw|WB!5ZMm8tJ-E3x8wu^}2f^Z%R8$%Qav^OC!2X;Sp8r2MyA; z)D@qm<-l|F?a1@#zOM7QG>`Ag2Q?4e1sW01*dUKs@{rJ9K6r@qFAx0>g!%8rZzpBw zbf~ZM;I`23J<)HKP1BqcunQhlpy6r<-+~5R$yF;K#XP0B%c@+j4)WDM!;i7%JGg2g zb3RawY#(bgl^U~egsu~MS3*bV{-oqQ-j4HjnTs3g!<8F4IhY%o<%QKa zn;Dm`9wx@)wnQK)R`#CnJ5C7T_pE`>DaK z(uC_~D&$z>*G10Vbz6AAY#UtsQ)PY?0k5R$WK8o!I@YJT(J7`asW@-HTEuc@AC&Pp zs`ezT1qZ;mg(_vDV-H#THywh~(sSYOoiXhl>Ofk0ku#LLg! zif0K?bB8#L)#)LRHu?th)>|v!bAMcPM z+TZ4tw?b(dk>CKVSoy=22gpK4@$cjN4@BEFSahBj!Ud=+?xhKiE6figD1kinco9!t zju#9gKT__Gw`Vt)gP(sV&VcVD{s_2eT0c78J`?@X!TBsr znHMe3N(}Sk8_UiwZur1mpoBeUmlOpZerG{8#6{Ng688Q2z@&TKTRyR2KuHZjlkQX` zE0o=$%C)p$o#<(#(?B$Os*zF>KEh|aSrx(tgdeQVo^yMFgKTIm@W+@)tme7;pR=k= zxQIClH~B_%*rK5_TI!fI*?Jrkt7r~fwBZVQUc&W^{`j?B#MpovU9uBRPRxn0lV-m$ zgTQ+gMo2xuG4WD-k<=&`bIQAv~nXDobA_`P!YF+ zaGS_4f5s4<=LCl+9*8VI@RG5{IF-r9;V~h)bM~CH{miyE&~BBtub+-p6F zv|Ph9gu3aDzYNd_;e@J=?TK?OAR*`(T&#zfhwDTfDQxs~a)MJb9N4%mP2gnu+E3dpAFrOFy{nqbt&oYBIj!Mv0oD+3%7IY!{0%TD%RX{ zS*=OS9kbEN?E4LZU1muzpNjq%=&+lLPg?bXsWUm1E!&5PT03X8nsO{0y7FS8&Vv*= zL3<-8JV1h+nGo3vDZA9rCq=m(F*=Iskm9_r#7AVoT(GhRoB0dUrF_%*B zJ^N+?sDpzv)F4&r(qmFeZcIKMtYO7AfD0VZ!%ZuBlGvuDUw!bJkDjm$y z7Q$07@FUnLS4gMdk`V`V#DR*N(g&UxH@Yj2~D z9f_q|$jF3aQbVi9FW}=3qkQF`FRIE%(7s8Duy(Z9@7u$o5lsCVkRhrgF2M8(18bGI zai7l<%h^XOW=4TzK-w>HQio9kVIN2`6b^piC6j%$ezS|cMEJCPNaKqtyXtN zQv&=%?%U@jvn?7r5>HkfKhX!qSUQ#FnG{{z+)f^#k1jgd-^NS5>amC~@>>k_{YFNe zNhRMhenn#-x|~_A^L`XkN2pHIS)MB4y0>MG_;cS{sYwMhKPeBF2{k{OaXI<15jd7) zi*np$7bzA8&7(!?;vL8r-m!vo7n*Z`1Op-jFq_*~-nEZnVB(-fH@9!mXr?-GYDZLZ zbcnyP(rqVu^o5@d6yt@9Wa8McYVy{GOTX49)X}<6s83)y)lmyN?>8Z`H3DC#@U4u1 zJzIuXJhS$RR?IQHgt1`B-M2C}5pu!3tIKK)qUM;i5b{YRAL$aKN^5=SM;d|MTy>x< zTTtnlXPlh}i7v`w9(viHsBvEKeSO(fQ<$IFaLKt>Q7l10ow-3dth0mWA{DTwD||zk zsbc#aN8L%BX)!Xyb&NEcli*TBmTdz?h^y<6zz0j#e zTs0gm-84zcf(&gUHI=&C)%`NvdJ5H=>Fte13?{?OUb0sXg!jR;VeH=)$lfQk<@=WE z2NxuVG-|(_(V3g1$M`6oBcKdL9B{1+Jx=G&5$aL7dHl?!x(%I5vK&3o(9;6g30lOG zLHBLh(%h<;wfqf{Z1jn}|?F!4xxxj=xuJ!?RAIyF6psU zIgA~e@gYxge&y^Cr$qd0uZkbA+h-w?$5y2=zovX%*oEM8)S=iKX!4!g2QNL+yT+$K zs#Qa0`JKF+y zehvXRpxVjW|jD+r~=NR-Mz=`jNwrlKA+Y@5>6B*&T_D5_eJAEXV_yAW}NeL zX6cDNkmgN6jKE&2t$cfRSS{v2dT%DyPDblq2DI~ch<82W5u(S-d>0(2bYiCFmWjb?yEZrGgkJ<49SFjVF^sSDy8!&x7m5Vn3-eR=~CpaODU)M`>XOKEnsp zLR{>+mJF&E3EkhlAo!w2qyFbhSa*}lVHD`w0(T$27^N+a`b*Mg!94?WOUj*LK{r{H z;Gj%QlkiY(wo^D#IC#znsQK4_w6ZXM^9sKS1@~Vi0Zr9^>hMrtixzzlONaMY zX8LEfJmCZH%0~Im+QOe<5U7UqSET`t7ec{91|cWJeW+#r=YohZx9KM4RlFY+M`A;7Cj|31XjQ$s^4B35PmjBJe z{F&y;^7oDYurmL4;=cjV9|=;Nf0yt#IP^z^@LwN&{2LSGiPhb5vV0h4-8(xzK z@`#to@{p+iz?6r|$^2+|=#NF&`K14xW?BO)o?3c}D` za`C%nQEz8zu7Fm<0oG#Dp_Kzv|6>_7a_Cer^(l&2LmY z;4h0 zHLr1=Hw)_)r9_y1#Ild=!{@~j#ij3ceup^%a>TfAxl7-&@9RB}XX?%O`%qq(ziQRf zCY$`Vf~sd>qcyvXl1tgXo|%$s33UN|{_bMlQ2@#>`L0TUO&iAqv}H1ER zzq_u!ye~e+52JwDSxwJIP&waFy$k&51+(xN@uwf4{Dj~&`-5(;2R09H7o!N>+r`IbF|seuA8#`K zGGG-TUxiI%nznnPF(vBUE~|c)`>wJ%zdk!~uR|4(z8~lAH~@?4jKWgi2Q$78b1_8w zzA;gbYn7FJCu2hN;laF+{lz&Z2y}akD3l;YVqn3s0V`6b10pa8#Eb<3k=)&sw==&N z+|B6)9PY&D?c!XZJLEbiK#HQ;lqE&=)h3DKVW~nN@@^?a=XxNbgG2HR?pw*|x#`aV zImvp(ngOb#(T$H_$oaGxw6DGJ6}AkAz5BzFUzx$lRnHp+F3u+Vklf$E1Sa@9JnQ}> z!4?43-jc1!hFGCHoia#t9i@aG;~=QTX6gLuVfnkra}lqg5}xa z6P@zWadh;f;wSEhWXsX9ez{lGO9NFJXVC&KV>fi@wRowFDutyq>mHAKScKA)Iee?b zJOB6`PyZYL%k75!t@g78mulgKUMi}A%%?rk5Rz#5;$>Yqhea+i}DCbz$R0ESw~lIZj8M3Ja34A zE43550Pg-L>F1}c9HvXtLEqo*xhSN?Vpl+6DyA{6rVOP_nuULGr9A|uj%pXgqT zJyR0(zR~Frya9Nn4b8W zzJpxawL!E)G=)djHJ%N2dXJTV%G*n>%nLo8YNE`h&`bz8gr8H?(>z3~IlRn6c z3gq7LPv=d{@5};iB&<@#xU%&cWbr3$u0@!)muxkD3holtFgRs7Cm~<>B1t=9mx-eq} z8q}!K)!gcTF#o!b?4SW-V3P~;jY=X6A%<9+n`6U>I>-?KMFM3dhY#f?K7Tkn)3`P( z9Nn9jNJzZoxsi!Phiab zp2IVfU?stV=cG?$_X);?+M3M5$Ih@bnGEs2^34s*Hp$gmxm@|eGqyx;7e!lU=v7uy z!o}D~st18~7h6lk02W#fNdADZx1()~t|*B{OGCiX;&bFn_3Sik01j-mQ15j73}$xag2LS|k*N4`}-MItUp#!cyJW ze*ZC5GgRxM>YM1lgWPfG2v6!-covr&Vx@c;1VpoU-+oB1MG9ux_lA{xpDH+wbyN!i5NbIG~?Zb_Mk6 zj+t)xChilcxHUb zQ?LAdLbV6c@fu~AV;T?gDmHUm&yMV{^6E!+0 zv!giG)0>^)mBF~Oj9e>0SJ*$qB2t!2yN0eg=Gl?6Sx^a&aZK zUl$68qii+;H6!Q0XPB`3Dv?e6lFnJZ)6DqvGIrlOG1WkAt;{SmDxy4aOz9QBAyY>G z#S59&2gjbB3YFfL@-=AoUUGlU+=RMl%gVl($X^)7<`>nnCE`>D(4d9*bqi`7@z!AB zx{ArOkXSPTK5ouI70J#Mnps%l7A_Y|{Y39ytp6#;# z9*t1W7+D(X6d!LMFgmfkKO{(GTNU2!Fu#`-&qH_+i+NXu!erlKBX??pcO13i;XRIp>0(?!tenGY1ryuDDF zVoJ+6^<=@i$rw*nzI3e{*o7N^uPgLx)GX?_*`xMb1+ED+h#Bb)aO2d+L}kZ=D6tdF z8bfcr-ZuJ!S{R^z0%>^f!_XtM)2u{9h6fg{@OXhoHoe()1MZ`LddvfhFIJ}Ix9Kxq ziEQ6EsboN^@YQ?V!oxyUGXQ5X5kJA@%T82=ZYMIV!k$6NRgCj?kL8-ilBi-E6f3uYJ06yHXl=$Ghcs-SWx7cE!`({E+ zs=3Vt9a9}`h`dN0psxh=JQZKuTQhU2>I>(P={*P*ZY}QSW-GE*2h_vx-|+e`a%FsY zK|UTGiT`O)FnE=Pe(&tSvQtM!@rah$IOb)rqOMgO_c-KPLf_fOkh-C~(eERJnK43> z^nsKKkF6m>Qoc@cpuK6z`4=a{0&QmxHPx~B4>y@_Y}1^(XTnt3?ETbs zO=wlK)Z!&OJSwY9Szx(+d>Ad?NWBK8*)UDZN;HoaoTZeXBXmhY=}$GZ&r)`I)-|S? zMNnF$FO9QRiF`>j!=th`WnAk|p5dHIv990^6RE?$Lg_a&bWf$ctuM?r<&f%J31lEn zInE#s>l2sS?CKC|5=-+I^g2Vju-5Tt3mq634G}mQsTxm|18rlxOva%v1GU^gmNeG4 zvD|svktNKUj1v4Nwc;F^d0;S;7M|rZ)_M}h3Sn6B1|r(B3183=k@595=JtJ@&_nHq zyiyeG+a5pi4BHl8n`Cpn zG8It|l_R+atPe-Y<>5bi?&W|04{B(Ko}#!7{iH=IxW!0j^mnC zg_V=<=!0R~irha`E%+6Z%P7CD#t~qdG!wk<-Dkz&Lo~PCfBYgAqpLr?YW`kXk&cIy z5BK_OR{q&!#c@X;H~Wk0UxYtS6}fkxVkk&;8DioH0sFJtAzN_0FsL;r$63ex}vMh(XUA=z?Pp@OnIrcby z(4rL-Og1`rje$4lnsPx+?IcHi%7`jlsA((PBm#bNEecE8TJdR}hXO{vaNx-e^szK|V8>xwVe%eeA4R*R>& z|HLtbxxha1x8x)kSuY`sfozpI#z2%x5Mr%-9iT=+#f33|0(S}dx!}QUdOS_YlDykz z>!30D~(c{A!RtCmVy z3oCXio1fLWRbt=GsyI8b=RX=8ANZ>P}+Z-eQ8ZpJ9M`LI0mTL=4L5U^F4O^4W`A)xrb zrvJN~yMHn3&ARrL+f6yy1B0jRvBF+>@5o3z9 zU=CKqt|A-awUJ{ayVRdOt_Y diff --git a/User_Interface.py b/User_Interface.py index 36577f0..226ef4e 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -3,6 +3,8 @@ from tkinter import ttk from tkinter import messagebox from tkinter import filedialog +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg + import numpy as np import os from os.path import exists @@ -29,8 +31,8 @@ class HelmholtzGUI(Tk): self.Menu = TopMenu(self) # displays menu bar at the top - mainArea = Frame(self, padx=20, pady=20) - mainArea.pack(side="top", fill="both", expand=False) + mainArea = Frame(self, padx=10, pady=10) + mainArea.pack(side="top", fill="both", expand=True) mainArea.grid_rowconfigure(0, weight=1) mainArea.grid_columnconfigure(0, weight=1) @@ -44,7 +46,6 @@ class HelmholtzGUI(Tk): status_frame = Frame(self) status_frame.pack(side="bottom", fill="x", expand=False) - status_frame.grid_rowconfigure(ALL, weight=1) status_frame.grid_columnconfigure(1, weight=1) self.StatusDisplay = StatusDisplay(status_frame, self) @@ -514,7 +515,6 @@ class Configuration(Frame): class ExecuteCSVMode(Frame): # generate configuration window to set program constants # ToDo (optional): Generate graph to show sequence to be executed - # ToDo: add button for reinit def __init__(self, parent, controller): Frame.__init__(self, parent) @@ -537,24 +537,57 @@ class ExecuteCSVMode(Frame): # Setup buttons # 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) + 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) # Create and place buttons - self.select_file_button = Button(self.file_select_frame, text="Select csv file...", command=self.load_csv, + 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) - self.execute_button = Button(self.file_select_frame, text="Run Sequence", command=self.run_sequence, + 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) - self.stop_button = Button(self.file_select_frame, text="Stop Run", command=self.stop_run, + 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 for reinitialization + self.reinit_button = Button(self.top_buttons_frame, text="Reinitialize Devices", command=func.setup_all, + pady=5, padx=5, font=SMALL_BUTTON_FONT) + self.reinit_button.grid(row=0, column=3, padx=5) row_counter += 1 + # setup testing 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) + + checkbox_label = Label(self.checkbox_frame, text="Disable device connection checks:") + checkbox_label.grid(row=0, column=0, sticky=W, padx=3) + self.xy_override = BooleanVar(value=False) + self.z_override = BooleanVar(value=False) + self.arduino_override = BooleanVar(value=False) + 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 + self.plotFrame = Frame(self) + self.plotFrame.grid_rowconfigure(0, weight=1) + self.plotFrame.grid_columnconfigure(0, weight=1) + self.plotFrame.grid(row=row_counter, column=0, sticky="nsw", padx=10, pady=10) + 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 @@ -570,9 +603,11 @@ class ExecuteCSVMode(Frame): self.sequence_array = csv.read_csv_to_array(filename) except BaseException as e: ui_print("Error while opening file:", e) - # ToDo: make error a popup + messagebox.showerror("Error!", "Error while opening file: \n%s" % e) + + csv.check_array(self.sequence_array) + self.display_plot() - # ToDo: check for excessive values self.execute_button["state"] = "normal" # activate run button elif filename == '': # this happens when file selection window is closed without selecting a file ui_print("No file selected, could not load.") @@ -584,6 +619,7 @@ class ExecuteCSVMode(Frame): self.select_file_button["state"] = "disabled" self.execute_button["state"] = "disabled" self.stop_button["state"] = "normal" + self.reinit_button["state"] = "disabled" # g.threadLock = threading.Lock() # create separate thread to run sequence execution in: @@ -592,11 +628,18 @@ class ExecuteCSVMode(Frame): csv_thread.start() # start thread def stop_run(self): - g.running = False + g.running = False # 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" + + def display_plot(self): # ToDo: comments + figure = csv.plot_field_sequence(self.sequence_array) + plotCanvas = FigureCanvasTkAgg(figure, self.plotFrame) + plotCanvas.draw() + plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") class StatusDisplay(Frame): @@ -645,11 +688,12 @@ class StatusDisplay(Frame): col = col + 1 # rowCounter = rowCounter + self.rowNo # increase row counter to place future stuff below this - self.continuous_label_update(controller, 500) # initiate regular value updates (ms) + self.update_labels() def continuous_label_update(self, controller, interval): # update display values in regular intervals self.update_labels() - if g.app is not None: + if g.app is not None: # app ist still running + # ToDo (optional): prevent call after program close controller.after(interval, lambda: self.continuous_label_update(controller, interval)) def update_labels(self): @@ -697,9 +741,9 @@ def ui_print(*content): # prints text to built in console output = "" for text in content: output = " ".join((output, str(text))) # append content - if g.app is not None: + if g.app is not None: # if main window is still open output = "".join(("\n", output)) # begin new line each time g.app.OutputConsole.console.insert(END, output) # print to console g.app.OutputConsole.console.see(END) # scroll to bottom else: # if window is not open, do normal print - print(output) \ No newline at end of file + print(output) diff --git a/cage_func.py b/cage_func.py index 300be0d..0d07be8 100644 --- a/cage_func.py +++ b/cage_func.py @@ -70,7 +70,9 @@ class Axis: self.current = self.device.get_current(self.channel) self.current_setpoint = self.device.get_current_setpoint(self.channel) except (serial.serialutil.SerialException, IndexError): - # ui_print("Connection Error with %s PSU on %s" % (self.name, self.port)) + if self.connected == "Connected": + ui_print("Connection Error with %s PSU on %s" % (self.name, self.port)) + messagebox.showerror("PSU Error", "Connection Error with %s PSU on %s" % (self.name, self.port)) self.connected = "Connection Error" self.output_active = "Unknown" self.remote_ctrl_active = "Unknown" @@ -94,6 +96,7 @@ class Axis: g.ARDUINO.digitalWrite(self.ardPin, "LOW") except Exception as e: ui_print("Error while powering down %s: %s" % (self.name, e)) + messagebox.showerror("PSU Error!", "Error while powering down %s: \n%s" % (self.name, e)) def set_signed_current(self, value): # sets current with correct polarity on this axis device = self.device @@ -167,6 +170,7 @@ class ArduinoCtrl(Arduino): axis.polarity_switched = "False" except Exception as e: ui_print("Error with Arduino:", e) + messagebox.showerror("Error with Arduino!", "Connection Error with Arduino: \n%s" % e) for axis in g.AXES: axis.polarity_switched = "Unknown" self.connected = "Connection Error" @@ -280,7 +284,7 @@ def power_down_all(): # temporary, set all outputs to 0 but keep connections en def shut_down_all(): # shutdown at program end or on error, set outputs to 0 and disable connections # ToDo: remove checks if connected or make them only for printing ui_print("\nAttempting to safely shut down all devices. Check equipment to confirm.") - message = "Tried to safely shut down all devices. Check equipment to confirm." + message = "Tried to shut down all devices. Check equipment to confirm." if g.XY_DEVICE is not None: try: set_to_zero(g.XY_DEVICE) @@ -356,4 +360,33 @@ def set_current_vec(vector): # sets needed currents on each axis for given vect axis.set_signed_current(vector[i]) except ValueError as e: ui_print(e) - i += 1 \ No newline at end of file + i += 1 + + +def devices_ok(xy_off=False, z_off=False, arduino_off=False): + # ToDo: comments + try: + if not xy_off: + if g.XY_DEVICE is not None: + g.X_AXIS.update_status_info() + if g.X_AXIS.connected != "Connected": + return False + else: + return False + if not z_off: + if g.Z_DEVICE is not None: + g.Z_AXIS.update_status_info() + if g.Z_AXIS.connected != "Connected": + return False + else: + return False + + if not arduino_off: + g.ARDUINO.update_status_info() + if g.ARDUINO.connected != "Connected": + return False + except Exception as e: + messagebox.showerror("Error!", "Error while checking devices: \n%s" % e) + return False + else: + return True diff --git a/csv_threading.py b/csv_threading.py index 16fcb7f..f284d36 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -1,12 +1,17 @@ -import User_Interface as ui -import cage_func as func import time import pandas from threading import * +from tkinter import messagebox + +import matplotlib.pyplot as plt + +import User_Interface as ui +import cage_func as func import globals as g class ExecCSVThread(Thread): + # ToDo: handling for disconnected devices def __init__(self, threadID, array, parent, controller): Thread.__init__(self) @@ -19,28 +24,36 @@ class ExecCSVThread(Thread): ui.ui_print("Starting Sequence Execution...") # g.threadLock.acquire() # Get lock to synchronize threads # ToDo: add locking/synchronization? Works without so far but might be more robust - execute_sequence(self.array, 0.1, self.controller) # run sequence + execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence self.parent.running = False # sequence finished --> no longer running # reset buttons on UI: self.parent.select_file_button["state"] = "normal" self.parent.execute_button["state"] = "normal" self.parent.stop_button["state"] = "disabled" + self.parent.reinit_button["state"] = "normal" -def execute_sequence(array, delay, controller): # runs through array containing times and desired field vectors +def execute_sequence(array, delay, parent, controller): # runs through array containing times and desired field vectors # array format: [time (s), xField (T), yField (T), zField (T)] # decimal commas # all times in seconds func.power_down_all() # sets outputs to 0 before starting t_zero = time.time() # set reference time for start of run + + # Check if everything is properly connected: + all_connected = func.devices_ok(parent.xy_override, parent.z_override, parent.arduino_override) + # True or False depending on devices status, checks for some devices may be overridden by user + i = 0 - while i < len(array) and g.running: # while array is not finished and user has not cancelled + while i < len(array) and g.running and all_connected: + # while array is not finished, user has not cancelled and devices are connected + t = time.time() - t_zero # get relative time if t >= array[i, 0]: # time for this row has come field_vec = array[i, 1:4] # extract desired field vector ui.ui_print("%f s: t = %0.2f s, target field vector = " - % (time.time()-t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") - func.set_field_simple(field_vec) # send field vector to test stand # ToDo!: reset to set_field() + % (time.time() - t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") + func.set_field(field_vec) # send field vector to test stand ui.ui_print(time.time() - t_zero) controller.StatusDisplay.update_labels() # update status display after change i = i + 1 # next row @@ -49,10 +62,17 @@ def execute_sequence(array, delay, controller): # runs through array containing pass else: # sleep to give other threads time to run time.sleep(delay) - if g.running: # sequence ended without interruption + + # check again if everything is connected before starting next loop run: + all_connected = func.devices_ok(parent.xy_override, parent.z_override, parent.arduino_override) + + if g.running and all_connected: # sequence ended without interruption ui.ui_print("Sequence executed, powering down channels.") - else: # interrupted by user + elif all_connected: # interrupted by user ui.ui_print("Sequence cancelled, powering down channels.") + elif g.running: # interrupted by device error + ui.ui_print("Error with at least one device, sequence aborted.") + messagebox.showinfo("Device Error!", "Error with at least one device, sequence aborted.") func.power_down_all() # set currents and voltages to 0, set arduino pins to low @@ -63,3 +83,40 @@ def read_csv_to_array(filepath): file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file array = file.to_numpy() # convert csv to array return array + + +def check_array(array): + # ToDo: message formatting, pop up warning + # ToDo: comments + concerns = [] + for row in array: + i = 1 + for axis in g.AXES: + value = row[i] + if value > axis.max_comp_field[1]: + concerns.append(row) + elif value < axis.max_comp_field[0]: + concerns.append(row) + i += 1 + ui.ui_print("Checked csv, found %i concerns." % len(concerns)) + if len(concerns) > 0: + ui.ui_print(concerns) + + +def plot_field_sequence(array): # ToDo: comments + # ToDo: make pretty + figure = plt.Figure(figsize=(8, 10), dpi=100) + + # noinspection PyTypeChecker,SpellCheckingInspection + axes = figure.subplots(3, sharex=True, sharey=True, gridspec_kw={'hspace': 0.4}) + + figure.suptitle("Magnetic Field Sequence") + t = array[:, 0] + + for i in [0, 1, 2]: + data = array[:, i + 1] * 1e6 + plot = axes[i] + plot.plot(t, data) + plot.set_title(g.AXIS_NAMES[i]) + + return figure diff --git a/globals.py b/globals.py index f33f60a..ffe1ec8 100644 --- a/globals.py +++ b/globals.py @@ -30,7 +30,7 @@ running = False # ToDo: put this into a config file default_arrays = { "coil_const": np.array([[38.6, 38.45, 37.9], [50, 50, 50], [0, 0, 0]]) * 1e-6, # Coil constants [x,y,z] [T/A] - "ambient_field": np.array([[30, 30, 30], [200, 200, 200], [0, 0, 0]]) * 1e-6, # background magnetic field [T] + "ambient_field": np.array([[30, 30, 30], [200, 200, 200], [-200, -200, -200]]) * 1e-6, # background magnetic field [T] "resistance": np.array([[1.7, 1.7, 1.7], [5, 5, 5], [1, 1, 1]], dtype=float), # resistance of circuits [Ohm] "max_watts": np.array([[15, 15, 15], [50, 50, 50], [0, 0, 0]], dtype=float), # max. allowed power for circuits [W] "max_volts": np.array([[14, 14, 14], [16, 16, 16], [0, 0, 0]], dtype=float), # max. allowed voltage, limited to 16V by used diodes! [V] diff --git a/main.py b/main.py index 7edfb79..30cea3b 100644 --- a/main.py +++ b/main.py @@ -26,6 +26,8 @@ try: # start normal operations print("\nOpening User Interface...") g.app = HelmholtzGUI() + g.app.state('zoomed') # open maximized + g.app.StatusDisplay.continuous_label_update(g.app, 500) # initiate regular Status Display updates (ms) ui_print("Program Initialized") config.check_config(config.CONFIG_OBJECT) # check config for values exceeding limits diff --git a/requirements.txt b/requirements.txt index 6409c7c..088bdfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ numpy==1.19.3 # bug in numpy 1.19.4, 1.19.3 used as workaround pyserial~=3.5 future~=0.18.2 -pandas \ No newline at end of file +pandas~=1.1.5 +matplotlib~=3.3.2 \ No newline at end of file From 792848dda23e28771caccac22d5456bd4843ade1 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Mon, 8 Feb 2021 20:35:58 +0100 Subject: [PATCH 22/36] fit csv plot into window --- User_Interface.py | 18 ++++++++++++++++-- csv_threading.py | 6 ++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 226ef4e..a7c6f1f 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -518,8 +518,8 @@ class ExecuteCSVMode(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) + self.parent = parent self.controller = controller # object on which mainloop() is running, usually main window - # Functional init: self.sequence_array = None # array containing the values from the csv file g.running = False # variable to turn thread execution on or off @@ -529,9 +529,11 @@ class ExecuteCSVMode(Frame): self.grid_columnconfigure(ALL, weight=1) row_counter = 0 + self.row_elements = [] # make list of elements in rows to calculate height available for plot 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) row_counter += 1 @@ -542,6 +544,8 @@ class ExecuteCSVMode(Frame): self.top_buttons_frame.grid_columnconfigure(ALL, weight=1) self.top_buttons_frame.grid(row=row_counter, column=0, sticky=W, padx=20) + self.row_elements.append(self.top_buttons_frame) + # Create and place buttons 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) @@ -565,6 +569,8 @@ class ExecuteCSVMode(Frame): 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) + checkbox_label = Label(self.checkbox_frame, text="Disable device connection checks:") checkbox_label.grid(row=0, column=0, sticky=W, padx=3) self.xy_override = BooleanVar(value=False) @@ -636,7 +642,15 @@ class ExecuteCSVMode(Frame): self.reinit_button["state"] = "normal" def display_plot(self): # ToDo: comments - figure = csv.plot_field_sequence(self.sequence_array) + # calculate available height for plot: + height_others = 0 + 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 + height = self.parent.winfo_height() - height_others - 50 # set to height of parent frame - other rows - margin + + width = min(self.parent.winfo_width() - 100, 1100) # set width to available space but max. 1100 + + figure = csv.plot_field_sequence(self.sequence_array, width, height) plotCanvas = FigureCanvasTkAgg(figure, self.plotFrame) plotCanvas.draw() plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") diff --git a/csv_threading.py b/csv_threading.py index f284d36..b814ef3 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -103,9 +103,11 @@ def check_array(array): ui.ui_print(concerns) -def plot_field_sequence(array): # ToDo: comments +def plot_field_sequence(array, width, height): # ToDo: comments # ToDo: make pretty - figure = plt.Figure(figsize=(8, 10), dpi=100) + fig_dpi = 100 + px = 1/fig_dpi + figure = plt.Figure(figsize=(width*px, height*px), dpi=fig_dpi) # noinspection PyTypeChecker,SpellCheckingInspection axes = figure.subplots(3, sharex=True, sharey=True, gridspec_kw={'hspace': 0.4}) From a0bab62ebcaadf32e2dd9706fa68bccddc0abc45 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Tue, 9 Feb 2021 12:21:07 +0100 Subject: [PATCH 23/36] Visual improvements for csv graphs --- User_Interface.py | 28 +++++++++--------- cage_func.py | 31 ++++++++++---------- csv_threading.py | 72 +++++++++++++++++++++++++++-------------------- out of bounds.csv | 12 ++++++++ 4 files changed, 82 insertions(+), 61 deletions(-) create mode 100644 out of bounds.csv diff --git a/User_Interface.py b/User_Interface.py index a7c6f1f..3967320 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -279,7 +279,6 @@ class Configuration(Frame): self.file_select_frame.grid(row=row_counter, column=0, sticky=W, padx=20) # Create and place buttons - # ToDo: comments 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) @@ -299,17 +298,17 @@ class Configuration(Frame): 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.XY_port = StringVar(value=g.XY_PORT) # create variables to store the port names and set to current names self.Z_port = StringVar(value=g.Z_PORT) port_vars = [self.XY_port, self.Z_port] row = 0 for text in entry_texts: - field = Entry(port_frame, textvariable=port_vars[row]) + 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, 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) + info_label = Label(port_frame, text="e.g. COM10") + info_label.grid(row=row, column=2, sticky=W) row += 1 row_counter += 1 @@ -514,7 +513,6 @@ class Configuration(Frame): class ExecuteCSVMode(Frame): # generate configuration window to set program constants - # ToDo (optional): Generate graph to show sequence to be executed def __init__(self, parent, controller): Frame.__init__(self, parent) @@ -606,13 +604,13 @@ class ExecuteCSVMode(Frame): if exists(filename): # does the file exist? ui_print("File selected:", filename) try: - self.sequence_array = csv.read_csv_to_array(filename) + self.sequence_array = csv.read_csv_to_array(filename) # read array from csv except BaseException as e: ui_print("Error while opening file:", e) messagebox.showerror("Error!", "Error while opening file: \n%s" % e) - csv.check_array(self.sequence_array) - self.display_plot() + csv.check_array_ok(self.sequence_array) # check for values exceeding limits + self.display_plot() # plot data and display self.execute_button["state"] = "normal" # activate run button elif filename == '': # this happens when file selection window is closed without selecting a file @@ -641,8 +639,8 @@ class ExecuteCSVMode(Frame): self.stop_button["state"] = "disabled" self.reinit_button["state"] = "normal" - def display_plot(self): # ToDo: comments - # calculate available height for plot: + def display_plot(self): + # calculate available height for plot (in pixels): height_others = 0 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 @@ -650,10 +648,10 @@ class ExecuteCSVMode(Frame): width = min(self.parent.winfo_width() - 100, 1100) # set width to available space but max. 1100 - figure = csv.plot_field_sequence(self.sequence_array, width, height) - plotCanvas = FigureCanvasTkAgg(figure, self.plotFrame) - plotCanvas.draw() - plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") + figure = csv.plot_field_sequence(self.sequence_array, width, height) # create figure to be displayed + plotCanvas = FigureCanvasTkAgg(figure, self.plotFrame) # create canvas to draw figure on + plotCanvas.draw() # equivalent to matplotlib.show() + plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in UI class StatusDisplay(Frame): diff --git a/cage_func.py b/cage_func.py index 0d07be8..ab6355b 100644 --- a/cage_func.py +++ b/cage_func.py @@ -364,16 +364,17 @@ def set_current_vec(vector): # sets needed currents on each axis for given vect def devices_ok(xy_off=False, z_off=False, arduino_off=False): - # ToDo: comments - try: - if not xy_off: - if g.XY_DEVICE is not None: - g.X_AXIS.update_status_info() - if g.X_AXIS.connected != "Connected": - return False - else: + # check if all devices are connected, return True if yes + # checks for individual devices can be disabled by parameters above (default not disabled) + try: # handle errors while checking connections + if not xy_off: # if check for this device is not disabled + if g.XY_DEVICE is not None: # has the handle for this device been set? + g.X_AXIS.update_status_info() # update info --> this actually communicates with the device + if g.X_AXIS.connected != "Connected": # if not connected + return False # return and exit function + else: # if handle has not been set the device is inactive --> not ok return False - if not z_off: + if not z_off: # same as above if g.Z_DEVICE is not None: g.Z_AXIS.update_status_info() if g.Z_AXIS.connected != "Connected": @@ -381,12 +382,12 @@ def devices_ok(xy_off=False, z_off=False, arduino_off=False): else: return False - if not arduino_off: - g.ARDUINO.update_status_info() + if not arduino_off: # check not disabled + g.ARDUINO.update_status_info() # update status info --> attempts communication if g.ARDUINO.connected != "Connected": return False - except Exception as e: - messagebox.showerror("Error!", "Error while checking devices: \n%s" % e) - return False - else: + except Exception as e: # if an error is encountered while checking the devices + messagebox.showerror("Error!", "Error while checking devices: \n%s" % e) # show error pop-up + return False # clearly something is not ok + else: # if nothing has triggered so far all devices are ok --> return True return True diff --git a/csv_threading.py b/csv_threading.py index b814ef3..e49c0dc 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -11,7 +11,6 @@ import globals as g class ExecCSVThread(Thread): - # ToDo: handling for disconnected devices def __init__(self, threadID, array, parent, controller): Thread.__init__(self) @@ -79,46 +78,57 @@ def execute_sequence(array, delay, parent, controller): # runs through array co def read_csv_to_array(filepath): # csv format: time (s); xField (T); yField (T); zField (T) (german excel) # decimal commas - ui.ui_print("Reading File:", filepath) file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file array = file.to_numpy() # convert csv to array return array -def check_array(array): - # ToDo: message formatting, pop up warning - # ToDo: comments - concerns = [] - for row in array: - i = 1 - for axis in g.AXES: - value = row[i] - if value > axis.max_comp_field[1]: - concerns.append(row) - elif value < axis.max_comp_field[0]: - concerns.append(row) - i += 1 - ui.ui_print("Checked csv, found %i concerns." % len(concerns)) - if len(concerns) > 0: - ui.ui_print(concerns) +def check_array_ok(array): # check if any magnetic fields in an array exceed the limits + values_ok = True + for i in [0, 1, 2]: # go through axes + max_val = g.AXES[i].max_comp_field[1] # get limits + min_val = g.AXES[i].max_comp_field[0] + data = array[:, i + 1] # extract data for this axis from array + # noinspection PyTypeChecker + if any(data > max_val) or any(data < min_val): # if any datapoint is out of bounds + values_ok = False + if not values_ok: # show warning pop-up if values are exceeding limits + messagebox.showwarning("Value Limits Warning!", "Found field values exceeding limits of test stand." + "\nSee plot and check values in csv.") -def plot_field_sequence(array, width, height): # ToDo: comments - # ToDo: make pretty - fig_dpi = 100 - px = 1/fig_dpi - figure = plt.Figure(figsize=(width*px, height*px), dpi=fig_dpi) +def plot_field_sequence(array, width, height): # create plot of fixed size from array + # ToDo (optional): polar plots, plots of angle... + # ToDo (optional): show graphs as steps (as performed by test stand) + fig_dpi = 100 # set figure resolution + px = 1/fig_dpi # get pixel to inch size conversion + figure = plt.Figure(figsize=(width*px, height*px), dpi=fig_dpi) # create figure with correct size # noinspection PyTypeChecker,SpellCheckingInspection - axes = figure.subplots(3, sharex=True, sharey=True, gridspec_kw={'hspace': 0.4}) + axes = figure.subplots(3, sharex=True, sharey=True, gridspec_kw={'hspace': 0.4}) # create subplots with shared axes figure.suptitle("Magnetic Field Sequence") - t = array[:, 0] - for i in [0, 1, 2]: - data = array[:, i + 1] * 1e6 - plot = axes[i] - plot.plot(t, data) - plot.set_title(g.AXIS_NAMES[i]) + t = array[:, 0] # extract time column + for i in [0, 1, 2]: # go through all three axes + data = array[:, i + 1] * 1e6 # extract field column of this axis + max_val = g.AXES[i].max_comp_field[1] * 1e6 # get limits of achievable field + min_val = g.AXES[i].max_comp_field[0] * 1e6 + plot = axes[i] # get appropriate subplot - return figure + plot.plot(t, data, linestyle='solid', marker='.') # plot data + + if any(data > max_val): # if any value is higher than the maximum + plot.axhline(y=max_val, linestyle='dashed', color='r') # plot horizontal line to show maximum + # add label to line: + plot.text(t[-1], max_val, "max", horizontalalignment='center', verticalalignment='top', color='r') + if any(data < min_val): # same as above + plot.axhline(y=min_val, linestyle='dashed', color='r') + plot.text(t[-1], min_val, "min", horizontalalignment='center', color='r') + plot.set_title(g.AXIS_NAMES[i], size=10) # set subplot title (e.g. "X-Axis") + + # set shared axis labels: + axes[2].set_xlabel("Time (s)") + axes[1].set_ylabel("Magnetic Field (\u03BCT)") + + return figure # return the created figure to be inserted somewhere else diff --git a/out of bounds.csv b/out of bounds.csv new file mode 100644 index 0000000..d3c377e --- /dev/null +++ b/out of bounds.csv @@ -0,0 +1,12 @@ +Time (s);xField (T);yField (T);zField (T); +0;0,00015;-0,00015;0,00002;150 +1;0,00017;-0,00017;0,00002;170 +2;0,00018;-0,00018;0,00002;180 +3;0,00019;-0,00019;0,00002;190 +4;0,0002;-0,0002;0,00002;200 +5;0,00021;-0,00021;0,00002;210 +6;0,00022;-0,00022;0,00002;220 +7;0,0002;-0,0002;0,00002;200 +8;0,00018;-0,00018;0,00002;180 +9;0,00005;-0,00005;0,00002;50 +10;-0,00004;0,00004;0,00002;-40 From ca2f094ef84436b26de666303762014529043470 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Wed, 10 Feb 2021 11:34:33 +0100 Subject: [PATCH 24/36] restructured program end, tried to fix threading issue on window close (not yet fixed) --- User_Interface.py | 28 +++++++++++---- cage_func.py | 3 +- csv_threading.py | 91 ++++++++++++++++++++++++++--------------------- globals.py | 3 +- main.py | 25 ++++++++++--- 5 files changed, 95 insertions(+), 55 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 3967320..7226c5a 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -8,6 +8,7 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np import os from os.path import exists +import threading import globals as g import cage_func as func @@ -519,8 +520,8 @@ class ExecuteCSVMode(Frame): self.parent = parent self.controller = controller # object on which mainloop() is running, usually main window # Functional init: + self.csv_thread = None # the thread object for executing csv self.sequence_array = None # array containing the values from the csv file - g.running = False # variable to turn thread execution on or off # Build UI: self.grid_rowconfigure(ALL, weight=1) @@ -625,14 +626,13 @@ class ExecuteCSVMode(Frame): self.stop_button["state"] = "normal" self.reinit_button["state"] = "disabled" - # g.threadLock = threading.Lock() + # g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later # create separate thread to run sequence execution in: - g.running = True - csv_thread = csv.ExecCSVThread("CSV_Thread", self.sequence_array, self, self.controller) - csv_thread.start() # start thread + self.csv_thread = csv.ExecCSVThread("CSV_Thread", self.sequence_array, self, self.controller) + self.csv_thread.start() # start thread def stop_run(self): - g.running = False # this will cause the csv loop to end + self.csv_thread.stop() # this will cause the csv loop to end # (de)activate buttons as needed: self.select_file_button["state"] = "normal" self.execute_button["state"] = "normal" @@ -654,6 +654,20 @@ class ExecuteCSVMode(Frame): plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in UI +class ConfigureLogging(Frame): + # generate window to configure data logging + + def __init__(self, parent, controller): + Frame.__init__(self, parent) + self.parent = parent + self.controller = controller # object on which mainloop() is running, usually main window + + self.grid_rowconfigure(ALL, weight=1) + self.grid_columnconfigure(ALL, weight=1) + + row_counter = 0 + + class StatusDisplay(Frame): def __init__(self, parent, controller): @@ -753,7 +767,7 @@ def ui_print(*content): # prints text to built in console output = "" for text in content: output = " ".join((output, str(text))) # append content - if g.app is not None: # if main window is still open + if not g.exitFlag: output = "".join(("\n", output)) # begin new line each time g.app.OutputConsole.console.insert(END, output) # print to console g.app.OutputConsole.console.see(END) # scroll to bottom diff --git a/cage_func.py b/cage_func.py index ab6355b..904d510 100644 --- a/cage_func.py +++ b/cage_func.py @@ -282,7 +282,6 @@ def power_down_all(): # temporary, set all outputs to 0 but keep connections en def shut_down_all(): # shutdown at program end or on error, set outputs to 0 and disable connections - # ToDo: remove checks if connected or make them only for printing ui_print("\nAttempting to safely shut down all devices. Check equipment to confirm.") message = "Tried to shut down all devices. Check equipment to confirm." if g.XY_DEVICE is not None: @@ -332,7 +331,7 @@ def shut_down_all(): # shutdown at program end or on error, set outputs to 0 an ui_print("Serial connection to Arduino closed.") message += "\nSerial connection to Arduino closed." - messagebox.showinfo("Shutdown Status", message) + messagebox.showinfo("Program ended", message) def set_field_simple(vector): # forms magnetic field as specified by vector, w/o cancelling ambient field diff --git a/csv_threading.py b/csv_threading.py index e49c0dc..b43556c 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -19,60 +19,69 @@ class ExecCSVThread(Thread): self.parent = parent # object from which this is called self.controller = controller # object on which mainloop() is running, usually main window + self.__stop_event = Event() + def run(self): ui.ui_print("Starting Sequence Execution...") # g.threadLock.acquire() # Get lock to synchronize threads # ToDo: add locking/synchronization? Works without so far but might be more robust - execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence - self.parent.running = False # sequence finished --> no longer running + self.execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence # reset buttons on UI: - self.parent.select_file_button["state"] = "normal" - self.parent.execute_button["state"] = "normal" - self.parent.stop_button["state"] = "disabled" - self.parent.reinit_button["state"] = "normal" + if not g.exitFlag: # program window is open + self.parent.select_file_button["state"] = "normal" + self.parent.execute_button["state"] = "normal" + self.parent.stop_button["state"] = "disabled" + self.parent.reinit_button["state"] = "normal" + def stop(self): # stop thread execution + self.__stop_event.set() -def execute_sequence(array, delay, parent, controller): # runs through array containing times and desired field vectors - # array format: [time (s), xField (T), yField (T), zField (T)] - # decimal commas - # all times in seconds - func.power_down_all() # sets outputs to 0 before starting - t_zero = time.time() # set reference time for start of run + def stopped(self): + return self.__stop_event.is_set() - # Check if everything is properly connected: - all_connected = func.devices_ok(parent.xy_override, parent.z_override, parent.arduino_override) - # True or False depending on devices status, checks for some devices may be overridden by user + def execute_sequence(self, array, delay, parent, controller): # runs through array containing times and desired field vectors + # array format: [time (s), xField (T), yField (T), zField (T)] + # decimal commas + # all times in seconds + func.power_down_all() # sets outputs to 0 before starting + t_zero = time.time() # set reference time for start of run - i = 0 - while i < len(array) and g.running and all_connected: - # while array is not finished, user has not cancelled and devices are connected + # Check if everything is properly connected: + all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), parent.arduino_override.get()) + # True or False depending on devices status, checks for some devices may be overridden by user - t = time.time() - t_zero # get relative time - if t >= array[i, 0]: # time for this row has come - field_vec = array[i, 1:4] # extract desired field vector - ui.ui_print("%f s: t = %0.2f s, target field vector = " - % (time.time() - t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") - func.set_field(field_vec) # send field vector to test stand - ui.ui_print(time.time() - t_zero) - controller.StatusDisplay.update_labels() # update status display after change - i = i + 1 # next row + i = 0 + while i < len(array) and all_connected and not self.stopped() and not g.exitFlag: + # while array is not finished, devices are connected, user has not cancelled and application is running - elif t >= array[i, 0] - delay - 0.02: # next change time is close, not enough time to sleep - pass - else: # sleep to give other threads time to run - time.sleep(delay) + t = time.time() - t_zero # get relative time + if t >= array[i, 0]: # time for this row has come + # g.threadLock.acquire(0) + field_vec = array[i, 1:4] # extract desired field vector + ui.ui_print("%f s: t = %0.2f s, target field vector = " + % (time.time() - t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") + func.set_field(field_vec) # send field vector to test stand + ui.ui_print(time.time() - t_zero) + controller.StatusDisplay.update_labels() # update status display after change + i = i + 1 # next row + # g.threadLock.release() - # check again if everything is connected before starting next loop run: - all_connected = func.devices_ok(parent.xy_override, parent.z_override, parent.arduino_override) + elif t >= array[i, 0] - delay - 0.02: # next change time is close, not enough time to sleep + pass + else: # sleep to give other threads time to run + time.sleep(delay) - if g.running and all_connected: # sequence ended without interruption - ui.ui_print("Sequence executed, powering down channels.") - elif all_connected: # interrupted by user - ui.ui_print("Sequence cancelled, powering down channels.") - elif g.running: # interrupted by device error - ui.ui_print("Error with at least one device, sequence aborted.") - messagebox.showinfo("Device Error!", "Error with at least one device, sequence aborted.") - func.power_down_all() # set currents and voltages to 0, set arduino pins to low + # check again if everything is connected before starting next loop run: + all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), parent.arduino_override.get()) + + if not self.stopped() and not g.exitFlag and all_connected: # sequence ended without interruption + ui.ui_print("Sequence executed, powering down channels.") + elif all_connected: # interrupted by user + ui.ui_print("Sequence cancelled, powering down channels.") + elif not self.stopped() and not g.exitFlag: # interrupted by device error + ui.ui_print("Error with at least one device, sequence aborted.") + messagebox.showinfo("Device Error!", "Error with at least one device, sequence aborted.") + func.power_down_all() # set currents and voltages to 0, set arduino pins to low def read_csv_to_array(filepath): diff --git a/globals.py b/globals.py index ffe1ec8..3339517 100644 --- a/globals.py +++ b/globals.py @@ -21,7 +21,8 @@ global Z_PORT global PORTS global threadLock -running = False + +exitFlag = True # False when main window is open, false otherwise # Default Constants and maximum/minimum values (warning messages will be generated if these are exceeded) # format: [[default values], [maximum values], [minimum values]] diff --git a/main.py b/main.py index 30cea3b..22b374e 100644 --- a/main.py +++ b/main.py @@ -5,10 +5,23 @@ from tkinter import messagebox import cage_func as func from User_Interface import HelmholtzGUI from User_Interface import ui_print +import User_Interface as ui import globals as g import config_handling as config +def program_end(): + g.exitFlag = True # tell everything else the application has been closed + if g.app is not None: + if g.app.pages[ui.ExecuteCSVMode].csv_thread is not None: # end possible csv execution thread + g.app.pages[ui.ExecuteCSVMode].csv_thread.stop() # stop thread + g.app.pages[ui.ExecuteCSVMode].csv_thread.join() # wait for thread to finish + # g.app = None # reset to None so nothing tries to print in the UI output + func.shut_down_all() # shut down devices + if g.app is not None: + g.app.destroy() # close application + + try: # start normal operations config.CONFIG_FILE = 'config.ini' @@ -26,6 +39,7 @@ try: # start normal operations print("\nOpening User Interface...") g.app = HelmholtzGUI() + g.exitFlag = False g.app.state('zoomed') # open maximized g.app.StatusDisplay.continuous_label_update(g.app, 500) # initiate regular Status Display updates (ms) ui_print("Program Initialized") @@ -33,19 +47,22 @@ try: # start normal operations ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once func.setup_all() # initiate communication, set handles + + g.app.protocol("WM_DELETE_WINDOW", program_end) # call program end function if user closes the application + g.app.mainloop() - g.app = None # reset to None so nothing tries to print in the UI output -except BaseException as e: # if there is an error, print what happened +except Exception as e: # if there is an error, print what happened print("\nAn error occurred, Shutting down.") # shop pup-up error message: message = "%s.\nSee python console traceback for more details. " \ "\nShutting down devices, check equipment to confirm." % e messagebox.showerror("Error!", message) print(traceback.print_exc()) + program_end() # safely close everything and shut down devices -finally: # safely shut everything down at the end - func.shut_down_all() +# ToDo: rework window closing code https://bytes.com/topic/python/answers/431323-detect-tkinter-window-being-closed +# https://stackoverflow.com/questions/14694408/runtimeerror-main-thread-is-not-in-main-loop # ToDo: logging From 95295e1f6198ba6b7ade053ece91fbb58a5493b4 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Wed, 10 Feb 2021 11:46:23 +0100 Subject: [PATCH 25/36] workaround for thread end problem on program close --- User_Interface.py | 2 +- csv_threading.py | 8 +++++--- main.py | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 7226c5a..5e7c224 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -626,7 +626,7 @@ class ExecuteCSVMode(Frame): self.stop_button["state"] = "normal" self.reinit_button["state"] = "disabled" - # g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later + g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later # create separate thread to run sequence execution in: self.csv_thread = csv.ExecCSVThread("CSV_Thread", self.sequence_array, self, self.controller) self.csv_thread.start() # start thread diff --git a/csv_threading.py b/csv_threading.py index b43556c..ad80021 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -56,15 +56,17 @@ class ExecCSVThread(Thread): t = time.time() - t_zero # get relative time if t >= array[i, 0]: # time for this row has come - # g.threadLock.acquire(0) + g.threadLock.acquire() # execute the next few lines before going back to the main thread + field_vec = array[i, 1:4] # extract desired field vector - ui.ui_print("%f s: t = %0.2f s, target field vector = " + ui.ui_print("%f s: t = %0.2f s, target field vector = " # ToDo: better printing % (time.time() - t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") func.set_field(field_vec) # send field vector to test stand ui.ui_print(time.time() - t_zero) controller.StatusDisplay.update_labels() # update status display after change i = i + 1 # next row - # g.threadLock.release() + + g.threadLock.release() # allow going back to main thread now elif t >= array[i, 0] - delay - 0.02: # next change time is close, not enough time to sleep pass diff --git a/main.py b/main.py index 22b374e..14f02e0 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ from os.path import exists import traceback from tkinter import messagebox +import time import cage_func as func from User_Interface import HelmholtzGUI @@ -15,7 +16,9 @@ def program_end(): if g.app is not None: if g.app.pages[ui.ExecuteCSVMode].csv_thread is not None: # end possible csv execution thread g.app.pages[ui.ExecuteCSVMode].csv_thread.stop() # stop thread - g.app.pages[ui.ExecuteCSVMode].csv_thread.join() # wait for thread to finish + # g.app.pages[ui.ExecuteCSVMode].csv_thread.join() # wait for thread to finish + # ToDo: figure out why this doesn't work with join() + time.sleep(0.2) # give the thread time to finish, workaround to avoid join() # g.app = None # reset to None so nothing tries to print in the UI output func.shut_down_all() # shut down devices if g.app is not None: From db81aff2db060e972015a14e6e1f1362f9913b63 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Sun, 14 Feb 2021 15:14:48 +0100 Subject: [PATCH 26/36] added data logging functionality --- .gitignore | 1 + User_Interface.py | 91 +++++++++++++++++++++++++++++++++++++++++-- csv_logging.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++ csv_threading.py | 3 +- main.py | 18 +++++---- 5 files changed, 200 insertions(+), 12 deletions(-) create mode 100644 csv_logging.py diff --git a/.gitignore b/.gitignore index f9dee33..0d0cc32 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,4 @@ ENV/ .idea/misc.xml config.ini *.ini +log.csv diff --git a/User_Interface.py b/User_Interface.py index 5e7c224..5d20e27 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -9,11 +9,13 @@ import numpy as np import os from os.path import exists import threading +from datetime import datetime import globals as g import cage_func as func import csv_threading as csv import config_handling as config +import csv_logging as log NORM_FONT = () HEADER_FONT = ("Arial", 13, "bold") @@ -40,7 +42,7 @@ class HelmholtzGUI(Tk): self.pages = {} # dictionary for storing all pages - for P in [ManualMode, Configuration, ExecuteCSVMode]: + for P in [ManualMode, Configuration, ExecuteCSVMode, ConfigureLogging]: page = P(mainArea, self) self.pages[P] = page page.grid(row=0, column=0, sticky="nsew") @@ -73,6 +75,8 @@ class TopMenu: menu.add_cascade(label="Mode", menu=ModeSelector) 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)) @staticmethod @@ -87,6 +91,10 @@ class TopMenu: def execute_csv_mode(window): window.show_frame(ExecuteCSVMode) + @staticmethod + def logging(window): + window.show_frame(ConfigureLogging) + class ManualMode(Frame): @@ -598,7 +606,7 @@ class ExecuteCSVMode(Frame): pass def load_csv(self): # load in csv file to be executed - directory = os.path.abspath(os.getcwd()) # get directory of current config file + directory = os.path.abspath(os.getcwd()) # get project directory # open file selection dialogue and save path of selected file filename = filedialog.askopenfilename(initialdir=directory, title="Select CSV File", filetypes=(("Comma Separated Values", "*.csv*"), ("All Files", "*.*"))) @@ -662,11 +670,86 @@ class ConfigureLogging(Frame): self.parent = parent 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.grid_rowconfigure(ALL, weight=1) self.grid_columnconfigure(ALL, weight=1) row_counter = 0 + # 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) + + self.stop_logging_button = Button(self.top_buttons_frame, text="Stop Logging", command=self.stop_logging, + pady=5, padx=5, font=SMALL_BUTTON_FONT) + self.stop_logging_button.grid(row=0, column=0, padx=5) + 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) + 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=1, padx=5) + + row_counter += 1 + + # Create checkboxes to select what data to log + 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.active_keys = [] # list with all the keys relating to the currently ticked checkboxes + + # generate and place all the checkboxes: + row = 0 + for key in log.axis_data_dict.keys(): + self.checkbox_vars[key] = BooleanVar(value=True) # create variable for checkbox and put it in dictionary + checkbox = Checkbutton(self.checkbox_frame, text=key, # generate checkbox + variable=self.checkbox_vars[key], onvalue=True, offvalue=False) + checkbox.grid(row=row, column=0, sticky=W) # place checkbox in UI + row += 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): + ui_print("Started data logging.") + self.update_choices() # update list with ticked checkboxes + self.regular_logging = True + log.zero_time = datetime.now() + self.periodic_log(1000) # ToDo: get interval from entry field + self.write_to_file_button["state"] = "disabled" + self.stop_logging_button.tkraise() # switch button to stop + + def stop_logging(self): + ui_print("Stopped data logging. Remember to save data to file!") + self.regular_logging = False + self.write_to_file_button["state"] = "normal" + self.start_logging_button.tkraise() # switch button to start + + @staticmethod + def write_to_file(): + filepath = log.select_file() # select a file to write to + log.write_to_file(log.log_data, filepath) # write logged data to the file + + def update_choices(self): + self.active_keys = [] + for key in self.checkbox_vars.keys(): + if self.checkbox_vars[key].get(): # box is ticked + self.active_keys.append(key) + + def periodic_log(self, interval): # logs data in regular intervals (ms) + if self.regular_logging: # logging in intervals is active + log.log_datapoint(self.active_keys) # add datapoint with active keys to log data frame + self.controller.after(interval, lambda: self.periodic_log(interval)) # call function again after interval + class StatusDisplay(Frame): @@ -716,7 +799,7 @@ class StatusDisplay(Frame): self.update_labels() - def continuous_label_update(self, controller, interval): # update display values in regular intervals + def continuous_label_update(self, controller, interval): # update display values in regular intervals (ms) self.update_labels() if g.app is not None: # app ist still running # ToDo (optional): prevent call after program close @@ -766,7 +849,7 @@ class OutputConsole(Frame): # console to print stuff in, similar to standard py def ui_print(*content): # prints text to built in console output = "" for text in content: - output = " ".join((output, str(text))) # append content + output = " ".join((output, str(text))) # merge all contents into one string if not g.exitFlag: output = "".join(("\n", output)) # begin new line each time g.app.OutputConsole.console.insert(END, output) # print to console diff --git a/csv_logging.py b/csv_logging.py new file mode 100644 index 0000000..00fed3d --- /dev/null +++ b/csv_logging.py @@ -0,0 +1,99 @@ +# This file contains functions related to logging data from the program to a CSV file +# They are mainly but not only called by the ConfigureLogging class in User_Interface.py + +import pandas as pd +import globals as g +from datetime import datetime +import os +from tkinter import filedialog +from tkinter import messagebox +import User_Interface as ui + +log_data = pd.DataFrame() # pandas data frame containing logged data +logging = False # Bool to indicate if data should be logged at the moment +unsaved_data = False # Bool to indicate if there is unsaved data, set to True each time a datapoint is logged +zero_time = datetime.now() + +# create dictionary with all value handles that could be logged +# Key: String that is displayed in UI and column headers. Also serves as handle to access dictionary elements. +# Keys are the same as the rows in the status display ToDo (optional): use this for the status display +# Content: name of the corresponding attribute in the Axis class (in cage_func.py). +# Important: attribute handle must match definition in Axis class exactly, used with axis.getattr() to get values. +axis_data_dict = { + 'PSU Status': 'connected', + 'Voltage Setpoint': 'voltage_setpoint', + 'Actual Voltage': 'voltage', + 'Current Setpoint': 'current_setpoint', + 'Actual Current': 'current', + 'Target Field': 'target_field_comp', + 'Trgt. Field Raw': 'target_field_comp', + 'Target Current': 'target_current', + 'Inverted': 'polarity_switched' +} + + +def triple_list(key_list): # creates list with each entry of key_list tripled with axis names before it + new_list = [] # initialize list + for key in key_list: # go through the given list + for axis_name in ['X', 'Y', 'Z']: # per given list entry create three, one for each axis + new_list.append(' '.join((axis_name, key))) # put axis_name before the given entry and append to new list + return new_list + + +def init_log_dataframe(key_list): # probably not needed, ToDo: remove + global log_data + columns = triple_list(key_list) + log_data = pd.DataFrame(columns=columns) + + +def log_datapoint(key_list): # ToDo: comments + global log_data + global unsaved_data + date = datetime.now().date() + time = datetime.now().strftime("%H:%M:%S,%f") + t = (datetime.now() - zero_time).total_seconds() + data = [[date, time, t]] + for key in key_list: + for axis in g.AXES: + data[0].append(getattr(axis, axis_data_dict[key])) # get value + column_names = ["Date", "Time", "t (s)", *triple_list(key_list)] + new_row = pd.DataFrame(data, columns=column_names) + log_data = log_data.append(new_row, ignore_index=True) + unsaved_data = True + + +def select_file(): # select a file to write logs to + directory = os.path.abspath(os.getcwd()) # get project directory + # open file selection dialogue and save path of selected file + filepath = filedialog.asksaveasfilename(initialdir=directory, title="Set log file", + filetypes=([("Comma Separated Values", "*.csv*")]), + defaultextension=[("Comma Separated Values", "*.csv*")]) + + if filepath == '': # this happens when file selection window is closed without selecting a file + ui.ui_print("No file selected, can not save logged data.") + return None + else: # a valid file name was entered + return filepath + + +def write_to_file(dataframe, filepath): + # get global variables for use in this function: + global unsaved_data + if filepath is not None: # user has selected a file and no errors occurred + ui.ui_print("Writing logged data to file", filepath) + try: + # write data collected in log_data DataFrame to csv file in german excel format: + dataframe.to_csv(filepath, index=False, sep=';', decimal=',') + except PermissionError: + message = "No permission to write to: \n%s. \nFile may be open in another program." % filepath + messagebox.showerror("Permission Error", message) + except BaseException as e: + message = "Error while trying to write to file \n%s.\n%s" % (filepath, e) + messagebox.showerror("Error!", message) + else: # no exceptions occurred + unsaved_data = False # data has been saved, so no unsaved data remains + + +def clear_logged_data(): # clears all logged data from data frame + global log_data # get global variable + log_data = pd.DataFrame() # reset to an empty data frame, i.e. clear all logged data diff --git a/csv_threading.py b/csv_threading.py index ad80021..3721dcc 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -74,7 +74,8 @@ class ExecCSVThread(Thread): time.sleep(delay) # check again if everything is connected before starting next loop run: - all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), parent.arduino_override.get()) + all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), + parent.arduino_override.get()) if not self.stopped() and not g.exitFlag and all_connected: # sequence ended without interruption ui.ui_print("Sequence executed, powering down channels.") diff --git a/main.py b/main.py index 14f02e0..58dd69a 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,6 @@ from os.path import exists import traceback from tkinter import messagebox -import time import cage_func as func from User_Interface import HelmholtzGUI @@ -9,18 +8,23 @@ from User_Interface import ui_print import User_Interface as ui import globals as g import config_handling as config +import csv_logging as log -def program_end(): +def program_end(): # called on exception or when user closes application g.exitFlag = True # tell everything else the application has been closed if g.app is not None: if g.app.pages[ui.ExecuteCSVMode].csv_thread is not None: # end possible csv execution thread g.app.pages[ui.ExecuteCSVMode].csv_thread.stop() # stop thread - # g.app.pages[ui.ExecuteCSVMode].csv_thread.join() # wait for thread to finish - # ToDo: figure out why this doesn't work with join() - time.sleep(0.2) # give the thread time to finish, workaround to avoid join() - # g.app = None # reset to None so nothing tries to print in the UI output func.shut_down_all() # shut down devices + + if log.unsaved_data: # There is logged data that has not been saved yet + # open pop-up to ask user if he wants to save the data: + save_log = messagebox.askquestion("Save log data?", "There seems to be unsaved logging data. " + "Do you wish to write it to a file now?") + if save_log == 'yes': # user has chosen yes + filepath = log.select_file() # let user select a file to write to + log.write_to_file(log.log_data, filepath) # write the data to the chosen file if g.app is not None: g.app.destroy() # close application @@ -51,7 +55,7 @@ try: # start normal operations ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once func.setup_all() # initiate communication, set handles - g.app.protocol("WM_DELETE_WINDOW", program_end) # call program end function if user closes the application + g.app.protocol("WM_DELETE_WINDOW", program_end) # call program_end function if user closes the application g.app.mainloop() From ab2619b18499fb100f39e3a9451935587098d525 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Sun, 14 Feb 2021 16:42:53 +0100 Subject: [PATCH 27/36] more work on logging --- User_Interface.py | 99 +++++++++++++++++++++++++++++++++++++++-------- csv_logging.py | 10 +---- 2 files changed, 85 insertions(+), 24 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 5d20e27..f33497b 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -663,7 +663,9 @@ class ExecuteCSVMode(Frame): class ConfigureLogging(Frame): - # generate window to configure data logging + # generate window to configure data logging to csv + # ToDo: support logging of axis-independent info like Arduino status + # ToDo (optional): implement logging of a datapoint every time new commands are sent to the test stand def __init__(self, parent, controller): Frame.__init__(self, parent) @@ -683,7 +685,7 @@ class ConfigureLogging(Frame): 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) + self.top_buttons_frame.grid(row=row_counter, column=0, sticky=W, padx=20, pady=10) self.stop_logging_button = Button(self.top_buttons_frame, text="Stop Logging", command=self.stop_logging, pady=5, padx=5, font=SMALL_BUTTON_FONT) @@ -694,6 +696,25 @@ class ConfigureLogging(Frame): 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=1, padx=5) + 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=2, padx=5) + + row_counter += 1 + + # Create label showing how many datapoints have been logged + 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 store 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 @@ -704,15 +725,20 @@ class ConfigureLogging(Frame): 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.active_keys = [] # list with all the keys relating to the currently ticked checkboxes + self.checkboxes = [] # list containing all the checkbox objects, used to enable/disable all of them + self.active_keys = [] # list with all the keys relating to the currently ticked checkboxes # generate and place all the checkboxes: - row = 0 + checkbox_label = Label(self.checkbox_frame, text="Select which data to log:") + checkbox_label.grid(row=0, column=0, columnspan=2) + # ToDo (optional): Add option to select which axes to log data from + row = 1 for key in log.axis_data_dict.keys(): self.checkbox_vars[key] = BooleanVar(value=True) # create variable for checkbox and put it in dictionary checkbox = Checkbutton(self.checkbox_frame, text=key, # generate checkbox variable=self.checkbox_vars[key], onvalue=True, offvalue=False) checkbox.grid(row=row, column=0, sticky=W) # place checkbox in UI + self.checkboxes.append(checkbox) # add created checkbox to list row += 1 def page_switch(self): # function that is called when switching to this window @@ -723,33 +749,73 @@ class ConfigureLogging(Frame): ui_print("Started data logging.") self.update_choices() # update list with ticked checkboxes self.regular_logging = True - log.zero_time = datetime.now() - self.periodic_log(1000) # ToDo: get interval from entry field + 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 + self.periodic_log(1000) # start periodic logging ToDo: get interval from entry field + + # lock/unlock buttons and checkboxes: self.write_to_file_button["state"] = "disabled" + self.clear_data_button["state"] = "normal" + self.lock_checkboxes() self.stop_logging_button.tkraise() # switch button to stop def stop_logging(self): ui_print("Stopped data logging. Remember to save data to file!") self.regular_logging = False - self.write_to_file_button["state"] = "normal" + self.write_to_file_button["state"] = "normal" # enable button + self.unlock_checkboxes() # enable checkboxes self.start_logging_button.tkraise() # switch button to start - @staticmethod - def write_to_file(): + 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 - log.write_to_file(log.log_data, filepath) # write logged data to the file + 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: + log.write_to_file(log.log_data, 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: + save_log = messagebox.askquestion("Save log data?", "There seems to be unsaved logging data. " + "Do you wish to write it to a file before deleting?") + if save_log == 'yes': # user has chosen yes + self.write_to_file() # run write to file function to save data + log.clear_logged_data() # delete the logged data + log.unsaved_data = False # tell everything that there is no unsaved data remaining + ui_print("Log data cleared.") def update_choices(self): - self.active_keys = [] - for key in self.checkbox_vars.keys(): + # 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 list + for key in self.checkbox_vars.keys(): # go through all checkboxes if self.checkbox_vars[key].get(): # box is ticked - self.active_keys.append(key) + self.active_keys.append(key) # add corresponding item to the list + + def lock_checkboxes(self): + for checkbox in self.checkboxes: + checkbox.config(state=DISABLED) + + def unlock_checkboxes(self): + for checkbox in self.checkboxes: + checkbox.config(state=NORMAL) def periodic_log(self, interval): # logs data in regular intervals (ms) if self.regular_logging: # logging in intervals is active log.log_datapoint(self.active_keys) # add datapoint with active keys to log data frame self.controller.after(interval, lambda: self.periodic_log(interval)) # call function again after interval + def update_datapoint_count(self): + if self.regular_logging: # logging is 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): @@ -805,12 +871,13 @@ class StatusDisplay(Frame): # ToDo (optional): prevent call after program close controller.after(interval, lambda: self.continuous_label_update(controller, interval)) - def update_labels(self): + def update_labels(self): # update all values in the status display g.ARDUINO.update_status_info() i = 0 for axis in g.AXES: if axis.device is not None: axis.update_status_info() + # update all label variables with current values: 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) @@ -825,7 +892,7 @@ class StatusDisplay(Frame): 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 = i + 1 + i += 1 class OutputConsole(Frame): # console to print stuff in, similar to standard python output @@ -846,7 +913,7 @@ class OutputConsole(Frame): # console to print stuff in, similar to standard py self.console.config(yscrollcommand=scrollbar.set) -def ui_print(*content): # prints text to built in console +def ui_print(*content): # prints text to built in console, use exactly like normal print() output = "" for text in content: output = " ".join((output, str(text))) # merge all contents into one string diff --git a/csv_logging.py b/csv_logging.py index 00fed3d..3bdf2d5 100644 --- a/csv_logging.py +++ b/csv_logging.py @@ -10,7 +10,6 @@ from tkinter import messagebox import User_Interface as ui log_data = pd.DataFrame() # pandas data frame containing logged data -logging = False # Bool to indicate if data should be logged at the moment unsaved_data = False # Bool to indicate if there is unsaved data, set to True each time a datapoint is logged zero_time = datetime.now() @@ -40,12 +39,6 @@ def triple_list(key_list): # creates list with each entry of key_list tripled w return new_list -def init_log_dataframe(key_list): # probably not needed, ToDo: remove - global log_data - columns = triple_list(key_list) - log_data = pd.DataFrame(columns=columns) - - def log_datapoint(key_list): # ToDo: comments global log_data global unsaved_data @@ -91,7 +84,8 @@ def write_to_file(dataframe, filepath): message = "Error while trying to write to file \n%s.\n%s" % (filepath, e) messagebox.showerror("Error!", message) else: # no exceptions occurred - unsaved_data = False # data has been saved, so no unsaved data remains + unsaved_data = False # tell everything that there is no unsaved data remaining + ui.ui_print("Log data saved to", filepath) def clear_logged_data(): # clears all logged data from data frame From 24281ad9f323ed791a3a6768a1c87cbe7e05fc69 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Mon, 15 Feb 2021 12:56:42 +0100 Subject: [PATCH 28/36] Added ability to log data when test stand is commanded --- User_Interface.py | 139 ++++++++++++++++++++++++++++++++++++++-------- cage_func.py | 11 +--- csv_threading.py | 8 ++- 3 files changed, 124 insertions(+), 34 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index f33497b..6b2aa7e 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -108,6 +108,7 @@ class ManualMode(Frame): row_counter = 0 + # setup title text header = Label(self, text="Manual Input Mode", font=HEADER_FONT, pady=3) header.grid(row=row_counter, column=0) @@ -187,12 +188,12 @@ class ManualMode(Frame): 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=func.power_down_all, + 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 - reinit_button = Button(self.buttons_frame, text="Reinitialize", command=func.setup_all, + 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) @@ -206,10 +207,11 @@ class ManualMode(Frame): 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 + self.modes[self.input_mode.get()][2]() # update max values and units, e.g. calls update_max_fields function # noinspection PyUnusedLocal - def change_mode_callback(self, var, index, mode): # not sure what the parameters are for, but they are necessary + # not sure what the parameters are for, but they are necessary + def change_mode_callback(self, var, index, mode): # called input mode dropdown 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 @@ -235,6 +237,22 @@ class ManualMode(Frame): val.set("(%0.2f to %0.2f A)" % (-g.AXES[i].max_amps, g.AXES[i].max_amps)) i += 1 + def reinitialize(self): # called on "Reinitialize!" button press + func.setup_all() # reinitialize 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 stand is commanded + 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 + + # 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 stand is commanded + logger.log_datapoint() # log data + def execute(self): function_to_call = self.modes[self.input_mode.get()][0] # get function of appropriate mode vector = np.array([0, 0, 0], dtype=float) @@ -245,6 +263,11 @@ class ManualMode(Frame): function_to_call(vector) # call function 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 + if logger.event_logging: # data should be logged when test stand is commanded + logger.log_datapoint() # log data + def execute_field(self, vector): ui_print("field executing", vector) comp = self.compensate.get() @@ -538,6 +561,7 @@ class ExecuteCSVMode(Frame): row_counter = 0 self.row_elements = [] # make list of elements in rows to calculate height available for plot + # setup heading 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) @@ -564,7 +588,7 @@ class ExecuteCSVMode(Frame): pady=5, padx=5, font=SMALL_BUTTON_FONT, state="disabled") self.stop_button.grid(row=0, column=2, padx=5) # add button for reinitialization - self.reinit_button = Button(self.top_buttons_frame, text="Reinitialize Devices", command=func.setup_all, + 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) @@ -647,6 +671,19 @@ class ExecuteCSVMode(Frame): 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 stand is commanded + logger.log_datapoint() # log data + + def reinitialize(self): # called on "Reinitialize devices" button press + func.setup_all() # reinitialize 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 stand is commanded + logger.log_datapoint() # log data + def display_plot(self): # calculate available height for plot (in pixels): height_others = 0 @@ -665,7 +702,6 @@ class ExecuteCSVMode(Frame): class ConfigureLogging(Frame): # generate window to configure data logging to csv # ToDo: support logging of axis-independent info like Arduino status - # ToDo (optional): implement logging of a datapoint every time new commands are sent to the test stand def __init__(self, parent, controller): Frame.__init__(self, parent) @@ -674,18 +710,28 @@ class ConfigureLogging(Frame): 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 stand + # log_datapoint() has to be called wherever a command is sent to the test stand and data should be logged + # it does not happen automatically whenever something is sent to the test stand + # 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 heading + 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=10) + self.top_buttons_frame.grid(row=row_counter, column=0, sticky=W, padx=20, pady=5) self.stop_logging_button = Button(self.top_buttons_frame, text="Stop Logging", command=self.stop_logging, pady=5, padx=5, font=SMALL_BUTTON_FONT) @@ -718,6 +764,34 @@ class ConfigureLogging(Frame): row_counter += 1 + # create checkboxes and entries to set how often data should be logged + 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) + + self.regular_logging_var = BooleanVar(value=True) # create variable for the regular logging checkbox + self.event_logging_var = BooleanVar(value=True) # create variable for the logging on command checkbox + 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 stand 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 self.checkbox_frame = Frame(self) self.checkbox_frame.grid_rowconfigure(ALL, weight=1) @@ -748,26 +822,38 @@ class ConfigureLogging(Frame): def start_logging(self): ui_print("Started data logging.") self.update_choices() # update list with ticked checkboxes - self.regular_logging = True + 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 - self.periodic_log(1000) # start periodic logging ToDo: get interval from entry field - # lock/unlock buttons and checkboxes: - self.write_to_file_button["state"] = "disabled" - self.clear_data_button["state"] = "normal" - self.lock_checkboxes() - self.stop_logging_button.tkraise() # switch button to stop + error = False + if self.regular_logging: + try: # try to get log interval + interval_ms = int(self.log_interval.get() * 1000) + except TclError as e: # invalid entry for log interval + messagebox.showwarning("Wrong entry format!", "Invalid entry for log interval:\n%s" % e) + self.event_logging = False # don't start logging if there is a problem + error = True + else: + 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.write_to_file_button["state"] = "disabled" + self.clear_data_button["state"] = "normal" + self.lock_checkboxes() + self.stop_logging_button.tkraise() # switch button to stop def stop_logging(self): ui_print("Stopped data logging. Remember to save data to file!") - self.regular_logging = False + self.regular_logging = False # tell everything its time to stop logging + self.event_logging = False # tell everything its time to stop logging self.write_to_file_button["state"] = "normal" # enable button self.unlock_checkboxes() # enable checkboxes - self.start_logging_button.tkraise() # switch button to start + self.start_logging_button.tkraise() # switch start/stop button to start 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 @@ -788,6 +874,7 @@ class ConfigureLogging(Frame): self.write_to_file() # run write to file function to save data 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.") def update_choices(self): @@ -798,21 +885,29 @@ class ConfigureLogging(Frame): if self.checkbox_vars[key].get(): # box is ticked self.active_keys.append(key) # add corresponding item to the list - def lock_checkboxes(self): - for checkbox in self.checkboxes: + def lock_checkboxes(self): # lock all checkboxes, so they can not be modified while logging + for checkbox in [*self.checkboxes, self.event_logging_checkbox, self.regular_logging_checkbox]: checkbox.config(state=DISABLED) + self.interval_entry.config(state=DISABLED) def unlock_checkboxes(self): - for checkbox in self.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 active - log.log_datapoint(self.active_keys) # add datapoint with active keys to log data frame - self.controller.after(interval, lambda: self.periodic_log(interval)) # call function again after interval + self.log_datapoint() + self.controller.after(interval, lambda: self.periodic_log(interval)) # call again after time interval + + 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: + messagebox.showerror("Error!", "Error while logging data: \n%s" % e) def update_datapoint_count(self): - if self.regular_logging: # logging is active + if self.regular_logging or self.event_logging: # logging is 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 diff --git a/cage_func.py b/cage_func.py index 904d510..188c4c4 100644 --- a/cage_func.py +++ b/cage_func.py @@ -260,16 +260,7 @@ def activate_all(): # enables remote control and output on all PSUs and channel g.Z_DEVICE.enable_all() -def print_status_3(): - ui_print("X-Axis:") - g.X_AXIS.print_status() - ui_print("Y-Axis:") - g.Y_AXIS.print_status() - ui_print("Z-Axis:") - g.Z_AXIS.print_status() - - -def set_to_zero(device): # sets voltages and currents to 0 +def set_to_zero(device): # sets voltages and currents to 0 on all channels of a specific PSU device.voltage1 = 0 device.current1 = 0 device.voltage2 = 0 diff --git a/csv_threading.py b/csv_threading.py index 3721dcc..0c9b25d 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -23,8 +23,6 @@ class ExecCSVThread(Thread): def run(self): ui.ui_print("Starting Sequence Execution...") - # g.threadLock.acquire() # Get lock to synchronize threads - # ToDo: add locking/synchronization? Works without so far but might be more robust self.execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence # reset buttons on UI: if not g.exitFlag: # program window is open @@ -64,6 +62,12 @@ class ExecCSVThread(Thread): func.set_field(field_vec) # send field vector to test stand ui.ui_print(time.time() - t_zero) 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 = controller.pages[ui.ConfigureLogging] # get object of logging configurator + if logger.event_logging: # data should be logged when test stand is commanded + logger.log_datapoint() # log data + i = i + 1 # next row g.threadLock.release() # allow going back to main thread now From 20f06fa837a7636769846ab7a4b74fdd9150d20f Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Mon, 15 Feb 2021 13:11:25 +0100 Subject: [PATCH 29/36] fixed issue Status Display update was attempted after window close, resulting in error messages from connected device --- User_Interface.py | 5 ++--- csv_threading.py | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 6b2aa7e..b60490f 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -961,9 +961,8 @@ class StatusDisplay(Frame): self.update_labels() def continuous_label_update(self, controller, interval): # update display values in regular intervals (ms) - self.update_labels() - if g.app is not None: # app ist still running - # ToDo (optional): prevent call after program close + if not g.exitFlag: # app ist still running + self.update_labels() controller.after(interval, lambda: self.continuous_label_update(controller, interval)) def update_labels(self): # update all values in the status display diff --git a/csv_threading.py b/csv_threading.py index 0c9b25d..cda0a42 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -37,7 +37,9 @@ class ExecCSVThread(Thread): def stopped(self): return self.__stop_event.is_set() - def execute_sequence(self, array, delay, parent, controller): # runs through array containing times and desired field vectors + def execute_sequence(self, array, delay, parent, controller): + # runs through array with times and desired fields and commands test stand accordingly + # array format: [time (s), xField (T), yField (T), zField (T)] # decimal commas # all times in seconds From fc6ca7284db9fc796c92f2014e645b17db00d5e1 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Tue, 16 Feb 2021 11:26:57 +0100 Subject: [PATCH 30/36] comments and code cleanup Logging, and threading cleanup --- User_Interface.py | 2 +- cage_func.py | 19 +++++--- csv_logging.py | 37 ++++++++-------- csv_threading.py | 108 +++++++++++++++++++++++++--------------------- 4 files changed, 93 insertions(+), 73 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index b60490f..07eddd1 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -660,7 +660,7 @@ class ExecuteCSVMode(Frame): g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later # create separate thread to run sequence execution in: - self.csv_thread = csv.ExecCSVThread("CSV_Thread", self.sequence_array, self, self.controller) + self.csv_thread = csv.ExecCSVThread(self.sequence_array, self, self.controller) self.csv_thread.start() # start thread def stop_run(self): diff --git a/cage_func.py b/cage_func.py index 188c4c4..224ed44 100644 --- a/cage_func.py +++ b/cage_func.py @@ -1,3 +1,6 @@ +# this file contains all classes and functions directly related to the operation of the helmholtz test stand +# the two main classes are Axis and ArduinoCtrl, see their definitions for details + import numpy as np import serial import traceback @@ -6,24 +9,26 @@ from tkinter import messagebox from User_Interface import ui_print from pyps2000b import PS2000B from Arduino import Arduino -# noinspection PyPep8Naming import config_handling as config import globals as g class Axis: + # Main class representing an axis (x,y,z) of the test stand + # contains static and dynamic status information about this axis and methods to control it + def __init__(self, index, device, PSU_channel, arduino_pin): # static information - self.index = index - self.device = device # power supply object (PS2000B class) - self.channel = PSU_channel # power supply unit channel (1 or 2) + self.index = index # index of this axis, 0->X, 1->Y, 2->Z + self.device = device # power supply object for this axis (PS2000B class) + self.channel = PSU_channel # power supply unit channel (0 or 1) self.ardPin = arduino_pin # output pin on the arduino for switching polarity on this axis - self.name = g.AXIS_NAMES[index] - self.port = g.PORTS[index] + self.name = g.AXIS_NAMES[index] # get name of this axis from list in globals.py (e.g. "X-Axis" + self.port = g.PORTS[index] # get serial port of this axis PSU + # read static information from the configuration object (which has read it from the config file or settings) self.resistance = float(config.read_from_config(self.name, "resistance", config.CONFIG_OBJECT)) - self.max_watts = float(config.read_from_config(self.name, "max_watts", config.CONFIG_OBJECT)) self.max_amps = float(config.read_from_config(self.name, "max_amps", config.CONFIG_OBJECT)) self.max_volts = float(config.read_from_config(self.name, "max_volts", config.CONFIG_OBJECT)) diff --git a/csv_logging.py b/csv_logging.py index 3bdf2d5..881b437 100644 --- a/csv_logging.py +++ b/csv_logging.py @@ -9,9 +9,9 @@ from tkinter import filedialog from tkinter import messagebox import User_Interface as ui -log_data = pd.DataFrame() # pandas data frame containing logged data +log_data = pd.DataFrame() # pandas data frame containing the logged data, in-program representation of csv/excel data unsaved_data = False # Bool to indicate if there is unsaved data, set to True each time a datapoint is logged -zero_time = datetime.now() +zero_time = datetime.now() # set reference for timestamps in log file, reset when log_data is cleared and restarted # create dictionary with all value handles that could be logged # Key: String that is displayed in UI and column headers. Also serves as handle to access dictionary elements. @@ -39,20 +39,23 @@ def triple_list(key_list): # creates list with each entry of key_list tripled w return new_list -def log_datapoint(key_list): # ToDo: comments - global log_data - global unsaved_data - date = datetime.now().date() - time = datetime.now().strftime("%H:%M:%S,%f") - t = (datetime.now() - zero_time).total_seconds() - data = [[date, time, t]] - for key in key_list: - for axis in g.AXES: - data[0].append(getattr(axis, axis_data_dict[key])) # get value - column_names = ["Date", "Time", "t (s)", *triple_list(key_list)] - new_row = pd.DataFrame(data, columns=column_names) - log_data = log_data.append(new_row, ignore_index=True) - unsaved_data = True +def log_datapoint(key_list): # logs a single row of data into the log_data DataFrame + # key_list determines what data is logged + global log_data # get global dataframe with logged data + global unsaved_data # get global variable that indicates if there is unsaved data + date = datetime.now().date() # get current date + time = datetime.now().strftime("%H:%M:%S,%f") # get string with current time in correct format + t = (datetime.now() - zero_time).total_seconds() # calculate timestamp relative to the start of the logging + data = [[date, time, t]] # initialize new data row with timestamps + for key in key_list: # go through the list telling us what data to log + for axis in g.AXES: # log this data for each axis + # get relevant value from the correct AXIS object and append to new data row: + data[0].append(getattr(axis, axis_data_dict[key])) + + column_names = ["Date", "Time", "t (s)", *triple_list(key_list)] # create list with the correct column headers + new_row = pd.DataFrame(data, columns=column_names) # create data frame containing the new row + log_data = log_data.append(new_row, ignore_index=True) # append the new data frame to the logged data + unsaved_data = True # tell other program parts that there is now unsaved data def select_file(): # select a file to write logs to @@ -90,4 +93,4 @@ def write_to_file(dataframe, filepath): def clear_logged_data(): # clears all logged data from data frame global log_data # get global variable - log_data = pd.DataFrame() # reset to an empty data frame, i.e. clear all logged data + log_data = pd.DataFrame() # reset to an empty data frame, i.e. clearing all logged data diff --git a/csv_threading.py b/csv_threading.py index cda0a42..cf57adb 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -1,3 +1,6 @@ +# this file contains code for executing a sequence of magnetic fields from a csv file +# to do this without crashing the UI it has to run in a separate thread using the threading module + import time import pandas from threading import * @@ -11,100 +14,108 @@ import globals as g class ExecCSVThread(Thread): - def __init__(self, threadID, array, parent, controller): + # main class for executing a CSV sequence + # it inherits the threading.Thread class, enabling sequence execution in a separate thread + + def __init__(self, array, parent, controller): Thread.__init__(self) - self.threadID = threadID self.array = array # numpy array containing data from csv to be executed - self.parent = parent # object from which this is called - self.controller = controller # object on which mainloop() is running, usually main window + self.parent = parent # object from which this class is called, here the ExecuteCSVMode object of the UI + self.controller = controller # object on which mainloop() is running, usually the main UI window - self.__stop_event = Event() + self.__stop_event = Event() # event which can be set to stop the thread execution if needed - def run(self): + def run(self): # called to start the execution of the thread ui.ui_print("Starting Sequence Execution...") self.execute_sequence(self.array, 0.1, self.parent, self.controller) # run sequence - # reset buttons on UI: - if not g.exitFlag: # program window is open + # when the sequence has ended, reset buttons on the UI: + if not g.exitFlag: # main window is open self.parent.select_file_button["state"] = "normal" self.parent.execute_button["state"] = "normal" self.parent.stop_button["state"] = "disabled" self.parent.reinit_button["state"] = "normal" - def stop(self): # stop thread execution + def stop(self): # stop thread execution, can be called from another thread to kill this one self.__stop_event.set() - def stopped(self): + def stopped(self): # returns true if the thread has been stopped, used to check if a run should continue return self.__stop_event.is_set() def execute_sequence(self, array, delay, parent, controller): + # main execution method of the class # runs through array with times and desired fields and commands test stand accordingly # array format: [time (s), xField (T), yField (T), zField (T)] - # decimal commas - # all times in seconds - func.power_down_all() # sets outputs to 0 before starting + func.power_down_all() # sets outputs on PSUs to 0 and Arduino pins to LOW before starting t_zero = time.time() # set reference time for start of run # Check if everything is properly connected: - all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), parent.arduino_override.get()) + all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), + parent.arduino_override.get()) # True or False depending on devices status, checks for some devices may be overridden by user - i = 0 + i = 0 # index of the current array row while i < len(array) and all_connected and not self.stopped() and not g.exitFlag: # while array is not finished, devices are connected, user has not cancelled and application is running - t = time.time() - t_zero # get relative time + t = time.time() - t_zero # get time relative to start of run if t >= array[i, 0]: # time for this row has come - g.threadLock.acquire() # execute the next few lines before going back to the main thread + g.threadLock.acquire() # execute all lines until threadLock.release() before going back to main thread - field_vec = array[i, 1:4] # extract desired field vector - ui.ui_print("%f s: t = %0.2f s, target field vector = " # ToDo: better printing - % (time.time() - t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") - func.set_field(field_vec) # send field vector to test stand - ui.ui_print(time.time() - t_zero) - controller.StatusDisplay.update_labels() # update status display after change + # check if everything is still connected before sending commands: + all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), + parent.arduino_override.get()) + if all_connected: + field_vec = array[i, 1:4] # extract desired field vector + ui.ui_print("%0.5f s: t = %0.2f s, target field vector =" + % (time.time() - t_zero, array[i, 0]), field_vec * 1e6, "\u03BCT") + func.set_field(field_vec) # send field vector to test stand + 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 = controller.pages[ui.ConfigureLogging] # get object of logging configurator - if logger.event_logging: # data should be logged when test stand is commanded - logger.log_datapoint() # log data + # log change to the log file if user has selected event logging in the Configure Logging window + logger = controller.pages[ui.ConfigureLogging] # get object of logging configurator + if logger.event_logging: # data should be logged when test stand is commanded + logger.log_datapoint() # log data - i = i + 1 # next row + i = i + 1 # next row g.threadLock.release() # allow going back to main thread now - elif t >= array[i, 0] - delay - 0.02: # next change time is close, not enough time to sleep - pass - else: # sleep to give other threads time to run - time.sleep(delay) - - # check again if everything is connected before starting next loop run: - all_connected = func.devices_ok(parent.xy_override.get(), parent.z_override.get(), - parent.arduino_override.get()) + elif t <= array[i, 0] - delay - 0.02: # is there enough time to sleep before the next row? + time.sleep(delay) # sleep to give other threads time to run if not self.stopped() and not g.exitFlag and all_connected: # sequence ended without interruption ui.ui_print("Sequence executed, powering down channels.") elif all_connected: # interrupted by user ui.ui_print("Sequence cancelled, powering down channels.") - elif not self.stopped() and not g.exitFlag: # interrupted by device error + elif not all_connected: # interrupted by device error ui.ui_print("Error with at least one device, sequence aborted.") - messagebox.showinfo("Device Error!", "Error with at least one device, sequence aborted.") + messagebox.showwarning("Device Error!", "Error with at least one device, sequence aborted.") + else: # if this happens there is a mistake in the logic above, it really should not + # tell the user something weird happened: + ui.ui_print("Encountered unexpected sequence end state:" + "\nThread Stopped:", self.stopped(), ", Application Closed:", g.exitFlag, + ", Devices connected:", all_connected) + messagebox.showwarning("Unexpected state", + "Encountered unexpected sequence end state, see console output for details.") + func.power_down_all() # set currents and voltages to 0, set arduino pins to low -def read_csv_to_array(filepath): +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 commas - file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file + file = pandas.read_csv(filepath, sep=';', decimal=',', header=0) # read csv file without column headers array = file.to_numpy() # convert csv to array return array -def check_array_ok(array): # check if any magnetic fields in an array exceed the limits +def check_array_ok(array): + # check if any magnetic fields in an array exceed the test stand limits and if so display a warning message values_ok = True - for i in [0, 1, 2]: # go through axes - max_val = g.AXES[i].max_comp_field[1] # get limits + for i in [0, 1, 2]: # go through axes/columns + max_val = g.AXES[i].max_comp_field[1] # get limits the test stand can do min_val = g.AXES[i].max_comp_field[0] data = array[:, i + 1] # extract data for this axis from array # noinspection PyTypeChecker @@ -115,21 +126,21 @@ def check_array_ok(array): # check if any magnetic fields in an array exceed th "\nSee plot and check values in csv.") -def plot_field_sequence(array, width, height): # create plot of fixed size from array +def plot_field_sequence(array, width, height): # create plot of fixed size (pixels) from array # ToDo (optional): polar plots, plots of angle... # ToDo (optional): show graphs as steps (as performed by test stand) - fig_dpi = 100 # set figure resolution + fig_dpi = 100 # set figure resolution (dots per inch) px = 1/fig_dpi # get pixel to inch size conversion figure = plt.Figure(figsize=(width*px, height*px), dpi=fig_dpi) # create figure with correct size # noinspection PyTypeChecker,SpellCheckingInspection axes = figure.subplots(3, sharex=True, sharey=True, gridspec_kw={'hspace': 0.4}) # create subplots with shared axes - figure.suptitle("Magnetic Field Sequence") + figure.suptitle("Magnetic Field Sequence") # set figure title t = array[:, 0] # extract time column for i in [0, 1, 2]: # go through all three axes - data = array[:, i + 1] * 1e6 # extract field column of this axis + data = array[:, i + 1] * 1e6 # extract field column of this axis and convert to microtesla max_val = g.AXES[i].max_comp_field[1] * 1e6 # get limits of achievable field min_val = g.AXES[i].max_comp_field[0] * 1e6 plot = axes[i] # get appropriate subplot @@ -143,10 +154,11 @@ def plot_field_sequence(array, width, height): # create plot of fixed size from if any(data < min_val): # same as above plot.axhline(y=min_val, linestyle='dashed', color='r') plot.text(t[-1], min_val, "min", horizontalalignment='center', color='r') + plot.set_title(g.AXIS_NAMES[i], size=10) # set subplot title (e.g. "X-Axis") # set shared axis labels: axes[2].set_xlabel("Time (s)") axes[1].set_ylabel("Magnetic Field (\u03BCT)") - return figure # return the created figure to be inserted somewhere else + return figure # return the created figure to be inserted somewhere From 59d0184dd6b561420b240d931e91c5c16ed69dbc Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Tue, 16 Feb 2021 13:43:55 +0100 Subject: [PATCH 31/36] comments and cleanup cage_func --- cage_func.py | 290 ++++++++++++++++++++++++++++----------------------- 1 file changed, 160 insertions(+), 130 deletions(-) diff --git a/cage_func.py b/cage_func.py index 224ed44..f0f64e3 100644 --- a/cage_func.py +++ b/cage_func.py @@ -27,7 +27,7 @@ class Axis: self.name = g.AXIS_NAMES[index] # get name of this axis from list in globals.py (e.g. "X-Axis" self.port = g.PORTS[index] # get serial port of this axis PSU - # read static information from the configuration object (which has read it from the config file or settings) + # read static information from the configuration object (which has read it from the config file or settings): self.resistance = float(config.read_from_config(self.name, "resistance", config.CONFIG_OBJECT)) self.max_amps = float(config.read_from_config(self.name, "max_amps", config.CONFIG_OBJECT)) self.max_volts = float(config.read_from_config(self.name, "max_volts", config.CONFIG_OBJECT)) @@ -35,11 +35,12 @@ class Axis: self.coil_constant = float(config.read_from_config(self.name, "coil_const", config.CONFIG_OBJECT)) self.ambient_field = float(config.read_from_config(self.name, "ambient_field", config.CONFIG_OBJECT)) - max_field = self.max_amps * self.coil_constant # max field reachable in this axis - self.max_field = np.array([-max_field, max_field]) - self.max_comp_field = np.array([self.ambient_field - max_field, self.ambient_field + max_field]) + max_field = self.max_amps * self.coil_constant # calculate max field reachable in this axis + self.max_field = np.array([-max_field, max_field]) # make array with min/max reachable field (w/o compensation) + # calculate max and min field that can be reached after compensating for the ambient field + self.max_comp_field = np.array([self.ambient_field - max_field, self.ambient_field + max_field]) # [min, max] - # dynamic information + # initialize dynamic information, this is updated by self.update_status_info() later self.connected = "Not Connected" self.output_active = "Unknown" # power output on the PSU enabled? self.remote_ctrl_active = "Unknown" # remote control on the PSU enabled? @@ -57,189 +58,211 @@ class Axis: if self.device is not None: self.update_status_info() - def update_status_info(self): # Read out the values of the parameters stored in this class and update them - try: - self.device.update_device_information(self.channel) - device_status = self.device.get_device_status_information(self.channel) - if device_status.output_active: + def update_status_info(self): # Read out the values of the dynamic parameters stored in this object and update them + try: # try to read out the data, this will fail on connection error to PSU + self.device.update_device_information(self.channel) # update the information in the device object + device_status = self.device.get_device_status_information(self.channel) # get object with new status info + + if device_status.output_active: # is the power output active? self.output_active = "Active" else: self.output_active = "Inactive" + + # is remote control active, allowing the device to be controlled by this program? if device_status.remote_control_active: self.remote_ctrl_active = "Active" else: self.remote_ctrl_active = "Inactive" + # get currents and voltages: self.voltage = self.device.get_voltage(self.channel) self.voltage_setpoint = self.device.get_voltage_setpoint(self.channel) self.current = self.device.get_current(self.channel) self.current_setpoint = self.device.get_current_setpoint(self.channel) - except (serial.serialutil.SerialException, IndexError): - if self.connected == "Connected": + + except (serial.serialutil.SerialException, IndexError): # Connection error, usually the PSU is unplugged + if self.connected == "Connected": # only show error messages if the device was connected before this error + # Show error as print-out in console and as pop-up: ui_print("Connection Error with %s PSU on %s" % (self.name, self.port)) messagebox.showerror("PSU Error", "Connection Error with %s PSU on %s" % (self.name, self.port)) + # set status attributes to connection error status: self.connected = "Connection Error" self.output_active = "Unknown" self.remote_ctrl_active = "Unknown" - else: - self.connected = "Connected" + else: # no communications error + self.connected = "Connected" # PSU is connected - def print_status(self): # axis = axis control variable, stored in globals.py + def print_status(self): # print out the current status of the device (not used at the moment) ui_print("%s, %0.2f V, %0.2f A" % (self.device.get_device_status_information(self.channel), self.device.get_voltage(self.channel), self.device.get_current(self.channel))) def power_down(self): # temporary powerdown, set outputs to 0 but keep connections enabled try: + # set class object attributes to 0 to reflect shutdown in status displays, log files etc. self.target_current = 0 self.target_field = 0 self.target_field_comp = 0 - if self.device is not None: - self.device.set_voltage(0, self.channel) - self.device.set_current(0, self.channel) - self.device.disable_output(self.channel) - g.ARDUINO.digitalWrite(self.ardPin, "LOW") - except Exception as e: + + if self.device is not None: # there is a PSU connected for this axis + self.device.set_voltage(0, self.channel) # set voltage on PSU channel to 0 + self.device.set_current(0, self.channel) # set current on PSU channel to 0 + self.device.disable_output(self.channel) # disable power output on PSU channel + g.ARDUINO.digitalWrite(self.ardPin, "LOW") # set arduino pin for polarity switch relay to unpowered state + except Exception as e: # some error was encountered + # show error message: ui_print("Error while powering down %s: %s" % (self.name, e)) messagebox.showerror("PSU Error!", "Error while powering down %s: \n%s" % (self.name, e)) - def set_signed_current(self, value): # sets current with correct polarity on this axis - device = self.device - channel = self.channel - ardPin = self.ardPin + def set_signed_current(self, value): + # sets current with correct polarity on this axis, this is the primary way to control the test stand + # ui_print("Attempting to set current", value, "A") - self.target_current = value - if self.connected == "Connected" or True: # ToDo!: remove True, only for arduino testing! + self.target_current = value # show target value in object attribute for status display, logging etc. - if abs(value) > self.max_amps: # prevent excessive currents - self.power_down() # set output to 0 and deactivate - raise ValueError("Invalid current value. Tried %0.2fA, max. %0.2fA allowed" % (value, self.max_amps)) + if abs(value) > self.max_amps: # prevent excessive currents + self.power_down() # set output to 0 and deactivate + raise ValueError("Invalid current value. Tried %0.2fA, max. %0.2fA allowed" % (value, self.max_amps)) - elif value >= 0: # switch polarity as needed - g.ARDUINO.digitalWrite(ardPin, "LOW") # ToDo: tie to arduino? - elif value < 0: - g.ARDUINO.digitalWrite(ardPin, "HIGH") # ToDo: tie to arduino? - else: - raise Exception("This should be impossible.") + elif value >= 0: # switch the e-box relay to change polarity as needed + g.ARDUINO.digitalWrite(self.ardPin, "LOW") # command the output pin on the arduino in the electronics box + elif value < 0: + g.ARDUINO.digitalWrite(self.ardPin, "HIGH") # command the output pin on the arduino in the electronics box - maxVoltage = min(max(1.1 * self.max_amps * self.resistance, 8), self.max_volts) # limit voltage - # ui_print("sending values to device: U =", maxVoltage, "I =", abs(value)) - if self.connected == "Connected": # ToDo!: remove if clause, only for arduino testing! - device.set_current(abs(value), channel) - device.set_voltage(maxVoltage, channel) - device.enable_output(channel) - else: + # determine voltage limit to be set on PSU, must be high enough to not limit the current: + # min. 8V, max. max_volts, in-between as needed with current value (+margin to not limit current) + maxVoltage = min(max(1.3 * value * self.resistance, 8), self.max_volts) # limit voltage + if self.connected == "Connected": # only try to command the PSU if its actually connected + self.device.set_current(abs(value), self.channel) # set desired current + self.device.set_voltage(maxVoltage, self.channel) # set voltage limit + self.device.enable_output(self.channel) # activate the power output + else: # the PSU is not connected ui_print(self.name, "not connected, can't set current.") def set_field_simple(self, value): # forms magnetic field as specified by value, w/o cancelling ambient field - self.target_field = value - self.target_field_comp = value - current = value / self.coil_constant - self.set_signed_current(current) + self.target_field = value # update object attribute for display + self.target_field_comp = value # same as above, bc no compensation + current = value / self.coil_constant # calculate needed current + self.set_signed_current(current) # command the test stand def set_field(self, value): # forms magnetic field as specified by value, corrected for ambient field - self.target_field = value - field = value - self.ambient_field - self.target_field_comp = field - current = field / self.coil_constant - self.set_signed_current(current) + self.target_field = value # update object attribute for display + field = value - self.ambient_field # calculate needed field after compensation + self.target_field_comp = field # update object attribute for display + current = field / self.coil_constant # calculate needed current + self.set_signed_current(current) # command the test stand class ArduinoCtrl(Arduino): + # main class to control the electronics box (which means commanding the arduino inside) + # inherits from the Arduino library def __init__(self): - self.connected = "Unknown" - self.pins = [0, 0, 0] - for i in range(3): # get correct pins from config file + self.connected = "Unknown" # connection status attribute, nominal "Connected" + self.pins = [0, 0, 0] # initialize list with pins to switch relay of each axis + for i in range(3): # get correct pins from the config self.pins[i] = int(config.read_from_config(g.AXIS_NAMES[i], "relay_pin", config.CONFIG_OBJECT)) + ui_print("\nConnecting to Arduino...") - try: - Arduino.__init__(self) # search for connected arduino and connect + try: # try to set up the arduino + Arduino.__init__(self) # search for connected arduino and connect by initializing arduino library class for pin in self.pins: self.pinMode(pin, "Output") self.digitalWrite(pin, "LOW") - except Exception as e: + except Exception as e: # some error occurred, usually the arduino is not connected ui_print("Connection to Arduino failed.", e) self.connected = "Not Connected" - else: + else: # connection was successfully established self.connected = "Connected" ui_print("Arduino ready.") - def update_status_info(self): - if self.connected == "Connected": - try: - for axis in g.AXES: - if g.ARDUINO.digitalRead(axis.ardPin): - axis.polarity_switched = "True" - else: - axis.polarity_switched = "False" - except Exception as e: + def update_status_info(self): # update the attributes stored in this class object + if self.connected == "Connected": # only do this if arduino is connected (initialize new instance to reconnect) + try: # try to read the status of the pins from the arduino + for axis in g.AXES: # go through all three axes + if g.ARDUINO.digitalRead(axis.ardPin): # pin is HIGH --> relay is switched + axis.polarity_switched = "True" # set attribute in axis object accordingly + else: # pin is LOW --> relay is not switched + axis.polarity_switched = "False" # set attribute in axis object accordingly + except Exception as e: # some error occurred while trying to read status, usually arduino is disconnected + # show warning messages to alert user ui_print("Error with Arduino:", e) messagebox.showerror("Error with Arduino!", "Connection Error with Arduino: \n%s" % e) - for axis in g.AXES: + for axis in g.AXES: # set polarity switch attributes in axis objects to "Unknown" axis.polarity_switched = "Unknown" - self.connected = "Connection Error" - else: - self.connected = "Connected" + self.connected = "Connection Error" # update own connection status + else: # no error occurred --> data was read successfully + self.connected = "Connected" # update own connection status - def safe(self): # sets output pins to low and closes serial connection + def safe(self): # sets relay switching pins to low to depower most of the electronics box for pin in self.pins: self.digitalWrite(pin, "LOW") def value_in_limits(axis, key, value): # Check if value is within safe limits (set in globals.py) - max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(axis)] # get max value - min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(axis)] # get min value + # axis is string with axis name, e.g. "X-Axis" + # key specifies which value to check, e.g. current + max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(axis)] # get max value from dictionary in globals.py + min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(axis)] # get min value from dictionary in globals.py - if float(value) > float(max_value): + if float(value) > float(max_value): # value is too high return 'HIGH' - elif float(value) < float(min_value): + elif float(value) < float(min_value): # value is too low return 'LOW' - else: + else: # value is within limits return 'OK' -def setup_all(): # main initialization function, creates device objects for all PSUs and Arduino and sets their values - # Connect to Arduino: - try: - if g.ARDUINO is not None: - # ui_print("\nClosing arduino link") +def setup_all(): # main test stand initialization function + # creates device objects for all PSUs and Arduino and sets their values + # initializes an object of class Axis for all three axes (x,y,z) + + # Setup Arduino: + try: # broad error handling for unforeseen errors, handling in ArduinoCtrl should catch most errors + if g.ARDUINO is not None: # the arduino has been initialized before, so we need to first close its connection try: - g.ARDUINO.close() # close serial link before attempting reconnection + g.ARDUINO.close() # close serial link except serial.serialutil.SerialException: pass # serial.flush() in Arduino.close() fails when reconnecting # this ignores it and allows serial.close() to execute (I think) except AttributeError: 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() - except Exception as e: + # when no Arduino is connected but g.ARDUINO has been initialized then there is nothing to close + # this throws an exception, which can be ignored + + g.ARDUINO = ArduinoCtrl() # initialize the arduino object from the control class, connects and sets up + except Exception as e: # some unforeseen error occurred + # show error messages to alert user ui_print("Arduino setup failed:", e) ui_print(traceback.print_exc()) + messagebox.showerror("Error!", "Arduino setup failed:\n%s \nCheck traceback in console." % e) - g.AXES = [] + # Setup PSUs and axis objects: + g.AXES = [] # initialize global list containing the three axis objects + # get serial ports for the PSUs from config g.XY_PORT = config.read_from_config("PORTS", "xy_port", config.CONFIG_OBJECT) g.Z_PORT = config.read_from_config("PORTS", "z_port", config.CONFIG_OBJECT) - g.PORTS = [g.XY_PORT, g.XY_PORT, g.Z_PORT] + g.PORTS = [g.XY_PORT, g.XY_PORT, g.Z_PORT] # write list with PSU port for each axis (X/Y share PSU) + # setup PSU and axis objects for X and Y axes: ui_print("\nConnecting to XY Device on %s..." % g.XY_PORT) - try: - if g.XY_DEVICE is not None: + try: # try to connect to the PSU + if g.XY_DEVICE is not None: # if PSU has previously been connected we need to close the serial link first ui_print("Closing serial connection on XY device") g.XY_DEVICE.serial.close() g.XY_DEVICE = None g.XY_DEVICE = PS2000B.PS2000B(g.XY_PORT) # setup PSU ui_print("Connection established.") - g.X_AXIS = Axis(0, g.XY_DEVICE, 0, g.ARDUINO.pins[0]) # create axis objects + g.X_AXIS = Axis(0, g.XY_DEVICE, 0, g.ARDUINO.pins[0]) # create axis objects (index, PSU, channel, relay pin) g.Y_AXIS = Axis(1, g.XY_DEVICE, 1, g.ARDUINO.pins[1]) - except serial.serialutil.SerialException: - g.X_AXIS = Axis(0, None, 0, g.ARDUINO.pins[0]) # create axis objects + except serial.serialutil.SerialException: # communications error, usually PSU is not connected or wrong port set + g.X_AXIS = Axis(0, None, 0, g.ARDUINO.pins[0]) # create axis objects without the PSU g.Y_AXIS = Axis(1, None, 1, g.ARDUINO.pins[1]) ui_print("XY Device not connected or incorrect port set.") + # same for the Z axis ui_print("Connecting to Z Device on %s..." % g.Z_PORT) try: if g.Z_DEVICE is not None: @@ -253,16 +276,12 @@ def setup_all(): # main initialization function, creates device objects for all g.Z_AXIS = Axis(2, None, 0, g.ARDUINO.pins[2]) ui_print("Z Device not connected or incorrect port set.") + # put newly created axis objects into a list for access later g.AXES.append(g.X_AXIS) g.AXES.append(g.Y_AXIS) g.AXES.append(g.Z_AXIS) - ui_print("") # new line - - -def activate_all(): # enables remote control and output on all PSUs and channels - g.XY_DEVICE.enable_all() - g.Z_DEVICE.enable_all() + ui_print("") # print new line def set_to_zero(device): # sets voltages and currents to 0 on all channels of a specific PSU @@ -272,27 +291,35 @@ def set_to_zero(device): # sets voltages and currents to 0 on all channels of a device.current2 = 0 -def power_down_all(): # temporary, set all outputs to 0 but keep connections enabled +def power_down_all(): # on all PSUs set all outputs to 0 but keep connections enabled for axis in g.AXES: axis.power_down() # set outputs to 0 and pin to low on this axis -def shut_down_all(): # shutdown at program end or on error, set outputs to 0 and disable connections +def shut_down_all(): # safe shutdown at program end or on error + # set outputs to 0 and disable connections on all devices + ui_print("\nAttempting to safely shut down all devices. Check equipment to confirm.") + # start writing string to later show how shutdown on all devices went in a single info pop-up: message = "Tried to shut down all devices. Check equipment to confirm." - if g.XY_DEVICE is not None: - try: - set_to_zero(g.XY_DEVICE) - g.XY_DEVICE.disable_all() - except BaseException as e: - ui_print("Error while deactivating XY PSU:", e) - message += "\nError while deactivating XY PSU: %s" % e - else: + + # Shut down PSUs: + if g.XY_DEVICE is not None: # the PSU has been setup before + try: # try to safe the PSU + set_to_zero(g.XY_DEVICE) # set currents and voltages to 0 for both channels + g.XY_DEVICE.disable_all() # disable power output on both channels + except BaseException as e: # some error occurred, usually device has been disconnected + ui_print("Error while deactivating XY PSU:", e) # print the problem in the console + message += "\nError while deactivating XY PSU: %s" % e # append status to the message to show later + else: # device was successfully deactivated ui_print("XY PSU deactivated.") - message += "\nXY PSU deactivated." - else: + message += "\nXY PSU deactivated." # append message to show later + else: # the device was not connected before + # tell user there was no need/no possibility to deactivate: ui_print("XY PSU not connected, can't deactivate.") message += "\nXY PSU not connected, can't deactivate." + + # same as above if g.Z_DEVICE is not None: try: set_to_zero(g.Z_DEVICE) @@ -307,38 +334,40 @@ def shut_down_all(): # shutdown at program end or on error, set outputs to 0 an ui_print("Z PSU not connected, can't deactivate.") message += "\nZ PSU not connected, can't deactivate." + # Shut down Arduino: try: - g.ARDUINO.safe() - except BaseException as e: + g.ARDUINO.safe() # call safe method in ArduinoCtrl class (all relay pins to LOW) + except BaseException as e: # some error occurred ui_print("Arduino safing unsuccessful:", e) - message += "\nArduino safing unsuccessful: %s" % e + message += "\nArduino safing unsuccessful: %s" % e # append to the message to show later # this throws no exception, even when arduino is not connected # ToDo (optional): figure out error handling for this try: - g.ARDUINO.close() - except BaseException as e: - if g.ARDUINO.connected == "Connected": + g.ARDUINO.close() # close the serial link + except BaseException as e: # something went wrong there + if g.ARDUINO.connected == "Connected": # Arduino was connected, some error occurred ui_print("Closing Arduino connection failed:", e) message += "\nClosing Arduino connection failed: %s" % e - else: + else: # Arduino was not connected, so error is expected ui_print("Arduino not connected, can't close connection.") message += "\nArduino not connected, can't close connection." - else: + else: # no problems, connection was successfully closed ui_print("Serial connection to Arduino closed.") message += "\nSerial connection to Arduino closed." - messagebox.showinfo("Program ended", message) + messagebox.showinfo("Program ended", message) # Show a unified pop-up with how the shutdown on each device went def set_field_simple(vector): # forms magnetic field as specified by vector, w/o cancelling ambient field for i in [0, 1, 2]: try: - g.AXES[i].set_field_simple(vector[i]) - except ValueError as e: - ui_print(e) + g.AXES[i].set_field_simple(vector[i]) # try to set the field on each axis + except ValueError as e: # a limit was violated, usually the needed current was too high + ui_print(e) # let the user know def set_field(vector): # forms magnetic field as specified by vector, corrected for ambient field + # same as set_field_simple(), but with compensation for i in [0, 1, 2]: try: g.AXES[i].set_field(vector[i]) @@ -346,14 +375,15 @@ def set_field(vector): # forms magnetic field as specified by vector, corrected ui_print(e) -def set_current_vec(vector): # sets needed currents on each axis for given vector +def set_current_vec(vector): # sets currents on each axis according to given vector i = 0 for axis in g.AXES: try: - axis.target_field = 0 - axis.target_field_comp = 0 - axis.set_signed_current(vector[i]) - except ValueError as e: + axis.target_field = 0 # set target field attribute to 0 to show that current, not field is controlled atm + axis.target_field_comp = 0 # as above + + axis.set_signed_current(vector[i]) # command test stand to set the current + except ValueError as e: # current was too high ui_print(e) i += 1 From 3b62e41f5a41c92e101637f80403a4dde5a9e008 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Tue, 16 Feb 2021 17:25:44 +0100 Subject: [PATCH 32/36] cleanup and comments - moved csv files and unused code files to separate folders - comments and cleanup in config_handling --- Test1.csv => Test cases/Test1.csv | 0 Test2.csv => Test cases/Test2.csv | 0 Test2_slow.csv => Test cases/Test2_slow.csv | 0 .../out of bounds.csv | 2 +- User_Interface.py | 21 +++-- cage_func.py | 4 +- config_handling.py | 89 +++++++++++-------- csv_logging.py | 4 +- csv_threading.py | 4 +- main.py | 2 +- .../example.py | 0 .../example2.py | 0 12 files changed, 73 insertions(+), 53 deletions(-) rename Test1.csv => Test cases/Test1.csv (100%) rename Test2.csv => Test cases/Test2.csv (100%) rename Test2_slow.csv => Test cases/Test2_slow.csv (100%) rename out of bounds.csv => Test cases/out of bounds.csv (91%) rename example.py => zz old test files etc/example.py (100%) rename example2.py => zz old test files etc/example2.py (100%) diff --git a/Test1.csv b/Test cases/Test1.csv similarity index 100% rename from Test1.csv rename to Test cases/Test1.csv diff --git a/Test2.csv b/Test cases/Test2.csv similarity index 100% rename from Test2.csv rename to Test cases/Test2.csv diff --git a/Test2_slow.csv b/Test cases/Test2_slow.csv similarity index 100% rename from Test2_slow.csv rename to Test cases/Test2_slow.csv diff --git a/out of bounds.csv b/Test cases/out of bounds.csv similarity index 91% rename from out of bounds.csv rename to Test cases/out of bounds.csv index d3c377e..3e10ebc 100644 --- a/out of bounds.csv +++ b/Test cases/out of bounds.csv @@ -4,7 +4,7 @@ Time (s);xField (T);yField (T);zField (T); 2;0,00018;-0,00018;0,00002;180 3;0,00019;-0,00019;0,00002;190 4;0,0002;-0,0002;0,00002;200 -5;0,00021;-0,00021;0,00002;210 +5;0,00021;0,00021;0,00002;210 6;0,00022;-0,00022;0,00002;220 7;0,0002;-0,0002;0,00002;200 8;0,00018;-0,00018;0,00002;180 diff --git a/User_Interface.py b/User_Interface.py index 07eddd1..10705d4 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -420,7 +420,8 @@ class Configuration(Frame): self.update_fields() def restore_defaults(self): # restore all default settings - config.reset_config_to_default(config.CONFIG_FILE) # overwrite config file with default + config.reset_config_to_default() # overwrite config file with default + ui_print("\nReinitializing devices...") func.setup_all() # setup everything with the defaults self.update_fields() # update fields in config window @@ -500,6 +501,7 @@ class Configuration(Frame): def implement(self): # executed on button press 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 self.update_fields() # update entry fields to show new values @@ -511,8 +513,9 @@ class Configuration(Frame): if 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 the values and display warnings if values are out of bounds + 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 self.update_fields() # update entry fields to show new values elif filename == '': # this happens when file selection window is closed without selecting a file @@ -642,10 +645,14 @@ class ExecuteCSVMode(Frame): ui_print("Error while opening file:", e) messagebox.showerror("Error!", "Error while opening file: \n%s" % e) - csv.check_array_ok(self.sequence_array) # check for values exceeding limits - self.display_plot() # plot data and display - - self.execute_button["state"] = "normal" # activate run button + try: + csv.check_array_ok(self.sequence_array) # check for values exceeding limits + self.display_plot() # plot data and display + except BaseException as e: + ui_print("Error while processing data from file:", e) + messagebox.showerror("Error!", "Error while processing data from file: \n%s" % e) + else: + self.execute_button["state"] = "normal" # activate run button elif filename == '': # this happens when file selection window is closed without selecting a file ui_print("No file selected, could not load.") else: diff --git a/cage_func.py b/cage_func.py index f0f64e3..b2ac41f 100644 --- a/cage_func.py +++ b/cage_func.py @@ -1,5 +1,5 @@ -# this file contains all classes and functions directly related to the operation of the helmholtz test stand -# the two main classes are Axis and ArduinoCtrl, see their definitions for details +# This file contains all classes and functions directly related to the operation of the helmholtz test stand. +# The two main classes are Axis and ArduinoCtrl, see their definitions for details. import numpy as np import serial diff --git a/config_handling.py b/config_handling.py index 5651efe..0fbfea9 100644 --- a/config_handling.py +++ b/config_handling.py @@ -1,3 +1,6 @@ +# This file contains functions and variables related to reading and writing configuration files. +# The configparser module is used for processing. Config files are of type .ini + from configparser import ConfigParser from tkinter import messagebox @@ -6,11 +9,13 @@ import cage_func as func # noinspection PyPep8Naming import User_Interface as ui -global CONFIG_FILE # variable storing the path of the used config file +global CONFIG_FILE # string storing the path of the used config file global CONFIG_OBJECT # object of type ConfigParser(), storing all configuration information +# CONFIG_OBJECT is what is mostly read/written by the program +# CONFIG_FILE is only used to export/import to/from a file -def get_config_from_file(file): +def get_config_from_file(file): # read a config file to a config_object config_object = ConfigParser() # initialize config parser config_object.read(file) # open config file return config_object # return config object, that contains all info from the file @@ -21,62 +26,70 @@ def write_config_to_file(config_object): # write contents of config object to a config_object.write(conf) -def read_from_config(section, key, config_object): # read specific value from config object +def read_from_config(section, key, config_object): # read a specific value from a config object try: section_obj = config_object[section] # get relevant section value = section_obj[key] # get relevant value in the section return value - except KeyError as e: + except KeyError as e: # a section or key was used, that does not exist ui.ui_print("Error while reading config file:", e) raise KeyError("Could not find key", key, "in config file.") -def edit_config(section, key, value, override=False): # edit specific value in config file - config_object = CONFIG_OBJECT - # ToDo: add check for data types, e.g. int for arduino ports +def edit_config(section, key, value, override=False): # edit a specific value in the CONFIG_OBJECT + # section: Section of the config, e.g. "X-Axis" or "PORTS" + # key: name of the value in the section, e.g. max_amps + # value: new value to be written into the config + # override: Bool to allow user to force writing a value into the config, even if it exceeds the safe limit - # Check if value to write is within acceptable limits (set in globals.py) + global CONFIG_OBJECT # get the global config object to edit it + + # ToDo (optional): add check for data types, e.g. int for arduino ports + # Check if value to write is within acceptable limits (set in dictionary in globals.py): try: value_ok = 'OK' - if section in g.AXIS_NAMES and not override: # only check numerical values and not if overridden by user + if section in g.AXIS_NAMES and not override: # only check values in axis sections and not if check overridden value_ok = func.value_in_limits(section, key, value) # check if value is ok, too high or too low - max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value - min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] # get min value - if value_ok == 'HIGH': + + if value_ok == 'HIGH': # value is too high + max_value = g.default_arrays[key][1][g.AXIS_NAMES.index(section)] # get max value for message printing message = "Prevented writing too high value for {s} {k} to config file:\n" \ "{v}, max. {mv} allowed. Erroneous values may damage equipment!" \ .format(s=section, k=key, v=value, mv=max_value) - raise ValueError(message) - elif value_ok == 'LOW': + raise ValueError(message) # return an error with the message attached + + elif value_ok == 'LOW': # value is too low + min_value = g.default_arrays[key][2][g.AXIS_NAMES.index(section)] # get min value for message printing message = "Prevented writing too low value for {s} {k} to config file:\n" \ "{v}, max. {mv} allowed. Erroneous values may damage equipment!" \ .format(s=section, k=key, v=value, mv=min_value) - raise ValueError(message) + raise ValueError(message) # return an error with the message attached - if value_ok == 'OK' or override: # value is within limits or user has overridden + if value_ok == 'OK' or override: # value is within limits or user has overridden the checks try: - section_obj = config_object[section] # get relevant section - except KeyError: + section_obj = CONFIG_OBJECT[section] # get relevant section in the config + except KeyError: # there is no such section ui.ui_print("Could not find section", section, "in config file, creating new.") - config_object.add_section(section) - section_obj = config_object[section] + CONFIG_OBJECT.add_section(section) # create the missing section + section_obj = CONFIG_OBJECT[section] # get the object of the section try: - section_obj[key] = str(value) # set relevant value in the section - except KeyError: + section_obj[key] = str(value) # set value for correct entry in the section + except KeyError: # there is no entry with this key ui.ui_print("Could not find key", key, "in config file, creating new.") - config_object.set(section, key, str(value)) + CONFIG_OBJECT.set(section, key, str(value)) # create the entry and set the value - except KeyError as e: + except KeyError as e: # key for section or specific value does not exist in the dictionary for max/min values ui.ui_print("Error while editing config file:", e) - raise KeyError("Could not find key", key, "in config file.") + raise KeyError("Could not find key", key, "in config file.") # return an error def check_config(config_object): # check all numeric values in the config and see if they are within safe limits ui.ui_print("Checking config file for values exceeding limits:") - i = 0 + concerns = {} # initialize dictionary for found problems - problem_counter = 0 - for axis in g.AXIS_NAMES: + problem_counter = 0 # count the number of values that exceed limits + i = 0 + for axis in g.AXIS_NAMES: # go through all 3 axes concerns[axis] = [] # create dictionary entry for this axis for key in g.default_arrays.keys(): # go over entries in this axis value = float(read_from_config(axis, key, config_object)) # read value to check from config file @@ -87,30 +100,30 @@ def check_config(config_object): # check all numeric values in the config and s concerns[axis].append(key) # add this entry to the problem dictionary problem_counter += 1 - if len(concerns[axis]) == 0: + if len(concerns[axis]) == 0: # no problems were found for this axis concerns[axis].append("No problems detected.") ui.ui_print(axis, ":", *concerns[axis]) # print out results for this axis i += 1 if problem_counter > 0: # some values are not ok # shop pup-up warning message: - messagebox.showwarning("Warning!", "Found values exceeding limits in config file. Check values " - "to ensure correct operation and avoid equipment damage!") + messagebox.showwarning("Warning!", "Found %i value(s) exceeding limits in config file. Check values " + "to ensure correct operation and avoid equipment damage!" % problem_counter) g.app.show_frame(ui.Configuration) # open configuration window so user can check values -def reset_config_to_default(file): # reset values in config object to defaults (stored in globals.py) - config = ConfigParser() # initialize global config object - global CONFIG_OBJECT - CONFIG_OBJECT = config +def reset_config_to_default(): # reset values in config object to defaults (set in globals.py) + config = ConfigParser() # reinitialize empty config object + global CONFIG_OBJECT # get the global config object + CONFIG_OBJECT = config # reset it to the empty object i = 0 for axis_name in g.AXIS_NAMES: # go through axes config.add_section(axis_name) # add section for this axis for key in g.default_arrays.keys(): # go through dictionary with default values - config.set(axis_name, key, str(g.default_arrays[key][0][i])) # set value + config.set(axis_name, key, str(g.default_arrays[key][0][i])) # set values i += 1 config.add_section("PORTS") # add section for PSU serial ports - for key in g.default_ports.keys(): - config.set("PORTS", key, str(g.default_ports[key])) \ No newline at end of file + for key in g.default_ports.keys(): # go through dictionary of default serial ports + config.set("PORTS", key, str(g.default_ports[key])) # set the value for each axis diff --git a/csv_logging.py b/csv_logging.py index 881b437..41c1cdc 100644 --- a/csv_logging.py +++ b/csv_logging.py @@ -1,5 +1,5 @@ -# This file contains functions related to logging data from the program to a CSV file -# They are mainly but not only called by the ConfigureLogging class in User_Interface.py +# This file contains functions related to logging data from the program to a CSV file. +# They are mainly but not only called by the ConfigureLogging class in User_Interface.py. import pandas as pd import globals as g diff --git a/csv_threading.py b/csv_threading.py index cf57adb..a6963c0 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -1,5 +1,5 @@ -# this file contains code for executing a sequence of magnetic fields from a csv file -# to do this without crashing the UI it has to run in a separate thread using the threading module +# tThis file contains code for executing a sequence of magnetic fields from a csv file. +# To do this without crashing the UI it has to run in a separate thread using the threading module. import time import pandas diff --git a/main.py b/main.py index 58dd69a..5fb229d 100644 --- a/main.py +++ b/main.py @@ -35,7 +35,7 @@ try: # start normal operations # ToDo: remember what the last config file was if not exists(config.CONFIG_FILE): print("Config file not found, creating new from defaults.") - config.reset_config_to_default(config.CONFIG_FILE) + config.reset_config_to_default() config.write_config_to_file(config.CONFIG_OBJECT) config.CONFIG_OBJECT = config.get_config_from_file(config.CONFIG_FILE) diff --git a/example.py b/zz old test files etc/example.py similarity index 100% rename from example.py rename to zz old test files etc/example.py diff --git a/example2.py b/zz old test files etc/example2.py similarity index 100% rename from example2.py rename to zz old test files etc/example2.py From f5dc7f097e7de57a6b20b95ceb1e2dbe5a7ae2ed Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Wed, 17 Feb 2021 11:26:25 +0100 Subject: [PATCH 33/36] cleanup and comments user interface --- User_Interface.py | 543 ++++++++++++++++++++++++--------------------- cage_func.py | 7 +- config_handling.py | 2 + csv_logging.py | 3 + globals.py | 1 - 5 files changed, 295 insertions(+), 261 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 10705d4..bfed20a 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -1,3 +1,6 @@ +# 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. + from tkinter import * from tkinter import ttk from tkinter import messagebox @@ -17,7 +20,7 @@ import csv_threading as csv import config_handling as config import csv_logging as log -NORM_FONT = () +# define font styles: HEADER_FONT = ("Arial", 13, "bold") SUB_HEADER_FONT = ("Arial", 9, "bold") BIG_BUTTON_FONT = ("Arial", 11, "bold") @@ -25,54 +28,62 @@ SMALL_BUTTON_FONT = ("Arial", 9) class HelmholtzGUI(Tk): - + # main application window, almost everything else here es 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") - Tk.wm_iconbitmap(self, "Helmholtz.ico") + Tk.wm_title(self, "Helmholtz Cage Control") # set title of the window + Tk.wm_iconbitmap(self, "Helmholtz.ico") # set application icon - self.Menu = TopMenu(self) # displays menu bar at the top + self.Menu = TopMenu(self) # display dropdown menu bar at the top (see TopMenu class for details) - mainArea = Frame(self, padx=10, pady=10) - mainArea.pack(side="top", fill="both", expand=True) + 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 - mainArea.grid_rowconfigure(0, weight=1) + mainArea.grid_rowconfigure(0, weight=1) # configure rows and columns of the Tkinter grid to expand with window mainArea.grid_columnconfigure(0, weight=1) - self.pages = {} # dictionary for storing all pages + # 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 - for P in [ManualMode, Configuration, ExecuteCSVMode, ConfigureLogging]: - page = P(mainArea, self) - self.pages[P] = page - page.grid(row=0, column=0, sticky="nsew") + self.pages = {} # dictionary for storing all pages (different modes, displayed in main area) + for P in [ManualMode, Configuration, ExecuteCSVMode, ConfigureLogging]: # do this for every mode page + page = P(mainArea, self) # initialize the page with the mainArea 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 - status_frame = Frame(self) - status_frame.pack(side="bottom", fill="x", expand=False) - status_frame.grid_columnconfigure(1, weight=1) + # 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") - self.show_frame(ManualMode) + self.show_frame(ManualMode) # show manual mode to start with - def show_frame(self, key): - frame = self.pages[key] # gets correct page from the dictionary + 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() # brings this frame to the front + frame.tkraise() # bring this frame to the front class TopMenu: - + # the menu bar at the top of the window def __init__(self, window): - menu = Menu(window) - window.config(menu=menu) + menu = Menu(window) # initialize Menu object + window.config(menu=menu) # put menu at the top of the window - ModeSelector = Menu(menu) - menu.add_cascade(label="Mode", menu=ModeSelector) + ModeSelector = Menu(menu) # create a submenu object + menu.add_cascade(label="Mode", menu=ModeSelector) # 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() @@ -80,33 +91,34 @@ class TopMenu: ModeSelector.add_command(label="Settings...", command=lambda: self.configuration(window)) @staticmethod - def manual_mode(window): + def manual_mode(window): # switch to the manual mode page window.show_frame(ManualMode) @staticmethod - def configuration(window): + def configuration(window): # switch to the settings page window.show_frame(Configuration) @staticmethod - def execute_csv_mode(window): + def execute_csv_mode(window): # switch to the CSV execution page window.show_frame(ExecuteCSVMode) @staticmethod - def logging(window): + def logging(window): # switch to the logging settings page window.show_frame(ConfigureLogging) class ManualMode(Frame): - + # Mode for manually setting currents and fields on the test stand. + # 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) + 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) + 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 + 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) @@ -115,48 +127,54 @@ class ManualMode(Frame): row_counter += 1 # Setup Dropdown Menu for input mode - dropdown_frame = Frame(self) + 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) - self.input_mode = StringVar() - # make dictionary with information on all modes. - # 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]} - self.unit = StringVar() - default_mode = list(self.modes.keys())[0] + 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 + # 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) + 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:"] - self.entry_vars = [StringVar() for _ in range(3)] - self.max_value_vars = [StringVar() for _ in range(3)] + 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: - field = ttk.Entry(self.entries_frame, textvariable=self.entry_vars[row]) - self.entry_vars[row].set(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) + 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) + 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 @@ -164,12 +182,13 @@ class ManualMode(Frame): row_counter += 1 # setup checkbox for compensating ambient field - checkbox_frame = Frame(self, padx=20) + checkbox_frame = Frame(self, padx=20) # create frame to house it checkbox_frame.grid(row=row_counter, column=0, sticky=W) - self.compensate = IntVar(value=1) + 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=1, offvalue=0) + variable=self.compensate, onvalue=True, offvalue=False) self.compensate_checkbox.pack(side="left") row_counter += 1 @@ -192,13 +211,12 @@ class ManualMode(Frame): pady=5, padx=5, font=BIG_BUTTON_FONT) power_down_button.grid(row=row_counter, column=1, padx=5) - # add button for reinitialization + # 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 - # Add spacer to Frame below Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer @@ -210,31 +228,36 @@ class ManualMode(Frame): 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 they are necessary - def change_mode_callback(self, var, index, mode): # called input mode dropdown is changed + # 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 update_max_fields(self): # update labels with maximum allowable field values - self.compensate_checkbox.config(state=NORMAL) + 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) i = 0 - for val in self.max_value_vars: - comp = self.compensate.get() - if comp == 0: - field = g.AXES[i].max_field * 1e6 - elif comp == 1: + for var in 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 + elif compensate: # ambient field should be compensated field = g.AXES[i].max_comp_field * 1e6 - else: + else: # this really should never happen field = [0, 0] - ui_print("Unexpected value encountered: compensate =", comp) - val.set("(%0.1f to %0.1f \u03BCT)" % (field[0], field[1])) + 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 update_max_currents(self): # update labels with maximum allowable current values - self.compensate_checkbox.config(state=DISABLED) + 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 val in self.max_value_vars: - val.set("(%0.2f to %0.2f A)" % (-g.AXES[i].max_amps, g.AXES[i].max_amps)) + 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 def reinitialize(self): # called on "Reinitialize!" button press @@ -253,38 +276,202 @@ class ManualMode(Frame): if logger.event_logging: # data should be logged when test stand is commanded logger.log_datapoint() # log data - def execute(self): - function_to_call = self.modes[self.input_mode.get()][0] # get function of appropriate mode - vector = np.array([0, 0, 0], dtype=float) + def execute(self): # called on "Execute!" button press + # reads values from the entry fields and commands the test stand accordingly + + vector = np.array([0, 0, 0], dtype=float) # initialize vector to later send to test stand i = 0 - for var in self.entry_vars: - vector[i] = float(var.get()) - i = i + 1 - function_to_call(vector) # call function - self.controller.StatusDisplay.update_labels() # update status display after change + 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()) + 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 + if logger.event_logging: # data should be logged when test stand is commanded + logger.log_datapoint() # log data + + def execute_field(self, vector): # convert magnetic field vector and send to test stand + 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 stand + elif not compensate: # ambient field should not be compensated + func.set_field_simple(vector * 1e-6) # convert to Tesla and send to test stand + else: # this really should never happen + ui_print("Unexpected value encountered: compensate =", compensate) + messagebox.showerror("Unexpected Value!", ("Unexpected value encountered: compensate =", compensate)) + + @staticmethod + def execute_current(vector): # send current vector to the test stand + ui_print("Current executing:", vector, "A") + func.set_current_vec(vector) # command test stand + + +class ExecuteCSVMode(Frame): + # generate configuration window to set program constants + + def __init__(self, parent, controller): + Frame.__init__(self, parent) + self.parent = parent + self.controller = controller # object on which mainloop() is running, usually main window + # Functional init: + self.csv_thread = None # the thread object for executing csv + self.sequence_array = None # array containing the values from the csv file + + # Build UI: + self.grid_rowconfigure(ALL, weight=1) + self.grid_columnconfigure(ALL, weight=1) + + row_counter = 0 + self.row_elements = [] # make list of elements in rows to calculate height available for plot + + # setup heading + 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) + + row_counter += 1 + + # Setup 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) + + self.row_elements.append(self.top_buttons_frame) + + # Create and place buttons + 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) + 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) + 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 for reinitialization + 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 testing 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) + + checkbox_label = Label(self.checkbox_frame, text="Disable device connection checks:") + checkbox_label.grid(row=0, column=0, sticky=W, padx=3) + self.xy_override = BooleanVar(value=False) + self.z_override = BooleanVar(value=False) + self.arduino_override = BooleanVar(value=False) + 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 + self.plotFrame = Frame(self) + self.plotFrame.grid_rowconfigure(0, weight=1) + self.plotFrame.grid_columnconfigure(0, weight=1) + self.plotFrame.grid(row=row_counter, column=0, sticky="nsw", padx=10, pady=10) + + 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 save path of selected file + filename = filedialog.askopenfilename(initialdir=directory, title="Select CSV File", + filetypes=(("Comma Separated Values", "*.csv*"), ("All Files", "*.*"))) + if exists(filename): # does the file exist? + ui_print("File selected:", filename) + try: + self.sequence_array = csv.read_csv_to_array(filename) # read array from csv + except BaseException as e: + ui_print("Error while opening file:", e) + messagebox.showerror("Error!", "Error while opening file: \n%s" % e) + + try: + csv.check_array_ok(self.sequence_array) # check for values exceeding limits + self.display_plot() # plot data and display + except BaseException as e: + ui_print("Error while processing data from file:", e) + messagebox.showerror("Error!", "Error while processing data from file: \n%s" % e) + else: + self.execute_button["state"] = "normal" # activate run button + elif filename == '': # this happens when file selection window is closed without selecting a file + ui_print("No file selected, could not load.") + else: + ui_print("Selected file", filename, "does not exist, could not load.") + + def run_sequence(self): + # (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" + + g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later + # create separate thread to run sequence execution in: + self.csv_thread = csv.ExecCSVThread(self.sequence_array, self, self.controller) + self.csv_thread.start() # start thread + + def stop_run(self): + self.csv_thread.stop() # 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 stand is commanded logger.log_datapoint() # log data - def execute_field(self, vector): - ui_print("field executing", vector) - comp = self.compensate.get() - if comp == 1: - func.set_field(vector * 1e-6) - elif comp == 0: - func.set_field_simple(vector * 1e-6) - else: - ui_print("Unexpected value encountered: compensate =", comp) + def reinitialize(self): # called on "Reinitialize devices" button press + func.setup_all() # reinitialize all PSUs and the Arduino - @staticmethod - def execute_current(vector): - ui_print("current executing:", vector) - try: - func.set_current_vec(vector) - except ValueError as e: - ui_print(e) + # 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 stand is commanded + logger.log_datapoint() # log data + + def display_plot(self): + # calculate available height for plot (in pixels): + height_others = 0 + 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 + height = self.parent.winfo_height() - height_others - 50 # set to height of parent frame - other rows - margin + + width = min(self.parent.winfo_width() - 100, 1100) # set width to available space but max. 1100 + + figure = csv.plot_field_sequence(self.sequence_array, width, height) # create figure to be displayed + plotCanvas = FigureCanvasTkAgg(figure, self.plotFrame) # create canvas to draw figure on + plotCanvas.draw() # equivalent to matplotlib.show() + plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in UI class Configuration(Frame): @@ -546,166 +733,6 @@ class Configuration(Frame): self.update_fields() # update entry fields to show values as they are in the config -class ExecuteCSVMode(Frame): - # generate configuration window to set program constants - - def __init__(self, parent, controller): - Frame.__init__(self, parent) - self.parent = parent - self.controller = controller # object on which mainloop() is running, usually main window - # Functional init: - self.csv_thread = None # the thread object for executing csv - self.sequence_array = None # array containing the values from the csv file - - # Build UI: - self.grid_rowconfigure(ALL, weight=1) - self.grid_columnconfigure(ALL, weight=1) - - row_counter = 0 - self.row_elements = [] # make list of elements in rows to calculate height available for plot - - # setup heading - 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) - - row_counter += 1 - - # Setup 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) - - self.row_elements.append(self.top_buttons_frame) - - # Create and place buttons - 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) - 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) - 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 for reinitialization - 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 testing 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) - - checkbox_label = Label(self.checkbox_frame, text="Disable device connection checks:") - checkbox_label.grid(row=0, column=0, sticky=W, padx=3) - self.xy_override = BooleanVar(value=False) - self.z_override = BooleanVar(value=False) - self.arduino_override = BooleanVar(value=False) - 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 - self.plotFrame = Frame(self) - self.plotFrame.grid_rowconfigure(0, weight=1) - self.plotFrame.grid_columnconfigure(0, weight=1) - self.plotFrame.grid(row=row_counter, column=0, sticky="nsw", padx=10, pady=10) - - 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 save path of selected file - filename = filedialog.askopenfilename(initialdir=directory, title="Select CSV File", - filetypes=(("Comma Separated Values", "*.csv*"), ("All Files", "*.*"))) - if exists(filename): # does the file exist? - ui_print("File selected:", filename) - try: - self.sequence_array = csv.read_csv_to_array(filename) # read array from csv - except BaseException as e: - ui_print("Error while opening file:", e) - messagebox.showerror("Error!", "Error while opening file: \n%s" % e) - - try: - csv.check_array_ok(self.sequence_array) # check for values exceeding limits - self.display_plot() # plot data and display - except BaseException as e: - ui_print("Error while processing data from file:", e) - messagebox.showerror("Error!", "Error while processing data from file: \n%s" % e) - else: - self.execute_button["state"] = "normal" # activate run button - elif filename == '': # this happens when file selection window is closed without selecting a file - ui_print("No file selected, could not load.") - else: - ui_print("Selected file", filename, "does not exist, could not load.") - - def run_sequence(self): - # (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" - - g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later - # create separate thread to run sequence execution in: - self.csv_thread = csv.ExecCSVThread(self.sequence_array, self, self.controller) - self.csv_thread.start() # start thread - - def stop_run(self): - self.csv_thread.stop() # 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 stand is commanded - logger.log_datapoint() # log data - - def reinitialize(self): # called on "Reinitialize devices" button press - func.setup_all() # reinitialize 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 stand is commanded - logger.log_datapoint() # log data - - def display_plot(self): - # calculate available height for plot (in pixels): - height_others = 0 - 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 - height = self.parent.winfo_height() - height_others - 50 # set to height of parent frame - other rows - margin - - width = min(self.parent.winfo_width() - 100, 1100) # set width to available space but max. 1100 - - figure = csv.plot_field_sequence(self.sequence_array, width, height) # create figure to be displayed - plotCanvas = FigureCanvasTkAgg(figure, self.plotFrame) # create canvas to draw figure on - plotCanvas.draw() # equivalent to matplotlib.show() - plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in UI - - class ConfigureLogging(Frame): # generate window to configure data logging to csv # ToDo: support logging of axis-independent info like Arduino status diff --git a/cage_func.py b/cage_func.py index b2ac41f..75dd911 100644 --- a/cage_func.py +++ b/cage_func.py @@ -1,11 +1,13 @@ # This file contains all classes and functions directly related to the operation of the helmholtz test stand. # The two main classes are Axis and ArduinoCtrl, see their definitions for details. +# import packages: import numpy as np import serial import traceback from tkinter import messagebox +# import other project files from User_Interface import ui_print from pyps2000b import PS2000B from Arduino import Arduino @@ -122,7 +124,8 @@ class Axis: if abs(value) > self.max_amps: # prevent excessive currents self.power_down() # set output to 0 and deactivate - raise ValueError("Invalid current value. Tried %0.2fA, max. %0.2fA allowed" % (value, self.max_amps)) + raise ValueError("Invalid current value on %s. Tried %0.2fA, max. %0.2fA allowed" + % (self.name, value, self.max_amps)) elif value >= 0: # switch the e-box relay to change polarity as needed g.ARDUINO.digitalWrite(self.ardPin, "LOW") # command the output pin on the arduino in the electronics box @@ -384,7 +387,7 @@ def set_current_vec(vector): # sets currents on each axis according to given ve axis.set_signed_current(vector[i]) # command test stand to set the current except ValueError as e: # current was too high - ui_print(e) + ui_print(e) # print out the error message i += 1 diff --git a/config_handling.py b/config_handling.py index 0fbfea9..66ef1fd 100644 --- a/config_handling.py +++ b/config_handling.py @@ -1,9 +1,11 @@ # This file contains functions and variables related to reading and writing configuration files. # The configparser module is used for processing. Config files are of type .ini +# import packages: from configparser import ConfigParser from tkinter import messagebox +# import other project files: import globals as g import cage_func as func # noinspection PyPep8Naming diff --git a/csv_logging.py b/csv_logging.py index 41c1cdc..bca8962 100644 --- a/csv_logging.py +++ b/csv_logging.py @@ -1,12 +1,15 @@ # This file contains functions related to logging data from the program to a CSV file. # They are mainly but not only called by the ConfigureLogging class in User_Interface.py. +# import packages import pandas as pd import globals as g from datetime import datetime import os from tkinter import filedialog from tkinter import messagebox + +# import other project files import User_Interface as ui log_data = pd.DataFrame() # pandas data frame containing the logged data, in-program representation of csv/excel data diff --git a/globals.py b/globals.py index 3339517..116049d 100644 --- a/globals.py +++ b/globals.py @@ -33,7 +33,6 @@ default_arrays = { "coil_const": np.array([[38.6, 38.45, 37.9], [50, 50, 50], [0, 0, 0]]) * 1e-6, # Coil constants [x,y,z] [T/A] "ambient_field": np.array([[30, 30, 30], [200, 200, 200], [-200, -200, -200]]) * 1e-6, # background magnetic field [T] "resistance": np.array([[1.7, 1.7, 1.7], [5, 5, 5], [1, 1, 1]], dtype=float), # resistance of circuits [Ohm] - "max_watts": np.array([[15, 15, 15], [50, 50, 50], [0, 0, 0]], dtype=float), # max. allowed power for circuits [W] "max_volts": np.array([[14, 14, 14], [16, 16, 16], [0, 0, 0]], dtype=float), # max. allowed voltage, limited to 16V by used diodes! [V] "max_amps": np.array([[4.5, 4.5, 4.5], [6, 6, 6], [0, 0, 0]], dtype=float), # max. allowed current (A) "relay_pin": [[15, 16, 17], [15, 16, 17], [15, 16, 17]] # pins on the arduino for reversing [x,y,z] polarity From 056fd3efdddd497ecffc27a03a3808d6cf8c8552 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Wed, 17 Feb 2021 11:51:20 +0100 Subject: [PATCH 34/36] code cleanup and comments UI: CSV mode --- User_Interface.py | 85 +++++++++++++++++++++++++++++------------------ csv_threading.py | 1 + 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index bfed20a..7bf1d7d 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -315,27 +315,31 @@ class ManualMode(Frame): class ExecuteCSVMode(Frame): - # generate configuration window to set program constants + # 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 + 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 csv + self.csv_thread = None # the thread object for executing a csv sequence self.sequence_array = None # array containing the values from the csv file # Build UI: - self.grid_rowconfigure(ALL, weight=1) + 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 - self.row_elements = [] # make list of elements in rows to calculate height available for plot + row_counter = 0 # keep track of which grid row we are in - # setup heading + self.row_elements = [] # make list of elements in rows to later calculate height available for plot + + # setup headline 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) + self.row_elements.append(header) # add to list of row elements row_counter += 1 @@ -346,38 +350,46 @@ class ExecuteCSVMode(Frame): self.top_buttons_frame.grid_columnconfigure(ALL, weight=1) self.top_buttons_frame.grid(row=row_counter, column=0, sticky=W, padx=20) - self.row_elements.append(self.top_buttons_frame) + self.row_elements.append(self.top_buttons_frame) # add frame to list of row elements - # Create and place buttons + # 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 for reinitialization + # 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 testing checkboxes + # 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) + 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) - self.xy_override = BooleanVar(value=False) - self.z_override = BooleanVar(value=False) - self.arduino_override = BooleanVar(value=False) + + # create variables for the 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 + # 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) @@ -390,7 +402,7 @@ class ExecuteCSVMode(Frame): row_counter += 1 - # make frame for plot of csv values + # make frame for plot of csv values (plot is generated and placed in display_plot() method) self.plotFrame = Frame(self) self.plotFrame.grid_rowconfigure(0, weight=1) self.plotFrame.grid_columnconfigure(0, weight=1) @@ -402,44 +414,50 @@ class ExecuteCSVMode(Frame): 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 save path of selected file + # 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 exists(filename): # does the file exist? ui_print("File selected:", filename) - try: + try: # try to read data to an array self.sequence_array = csv.read_csv_to_array(filename) # read array from csv - except BaseException as e: + except BaseException as e: # something went wrong, probably wrong format in csv + # display error messages: ui_print("Error while opening file:", e) messagebox.showerror("Error!", "Error while opening file: \n%s" % e) - try: + try: # try to check the values and display the plot csv.check_array_ok(self.sequence_array) # check for values exceeding limits self.display_plot() # plot data and display - except BaseException as e: + except BaseException as e: # something went wrong, probably wrong format in csv + # 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: - self.execute_button["state"] = "normal" # activate run button + 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: + 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): + def run_sequence(self): # called on run button press, starts thread for executing the sequence # (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" + # setup thread for running the sequence: + # More info: https://www.tutorialspoint.com/python/python_multithreading.htm g.threadLock = threading.Lock() # create thread locking object, used to ensure all devices switch at once later - # create separate thread to run sequence execution in: + # create thread object: self.csv_thread = csv.ExecCSVThread(self.sequence_array, self, self.controller) self.csv_thread.start() # start thread - def stop_run(self): - self.csv_thread.stop() # this will cause the csv loop to end + 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" @@ -459,19 +477,20 @@ class ExecuteCSVMode(Frame): if logger.event_logging: # data should be logged when test stand is commanded logger.log_datapoint() # log data - def display_plot(self): + def display_plot(self): # generate and display a plot of the data loaded from a csv file # calculate available height for plot (in pixels): - height_others = 0 + 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 - height = self.parent.winfo_height() - height_others - 50 # set to height of parent frame - other rows - margin + # 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 figure = csv.plot_field_sequence(self.sequence_array, width, height) # create figure to be displayed plotCanvas = FigureCanvasTkAgg(figure, self.plotFrame) # create canvas to draw figure on plotCanvas.draw() # equivalent to matplotlib.show() - plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in UI + plotCanvas.get_tk_widget().grid(row=0, column=0, sticky="nesw") # place canvas in the UI class Configuration(Frame): diff --git a/csv_threading.py b/csv_threading.py index a6963c0..3dbd1cb 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -36,6 +36,7 @@ class ExecCSVThread(Thread): self.parent.stop_button["state"] = "disabled" self.parent.reinit_button["state"] = "normal" + # setup ability to interrupt thread (https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread) def stop(self): # stop thread execution, can be called from another thread to kill this one self.__stop_event.set() From 689e0dd8126f517349ab713cf38a84d46e212816 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Sat, 20 Feb 2021 17:36:23 +0100 Subject: [PATCH 35/36] comments and cleanup user interface comments and cleanup --- User_Interface.py | 281 +++++++++++++++++++++++++++------------------- 1 file changed, 163 insertions(+), 118 deletions(-) diff --git a/User_Interface.py b/User_Interface.py index 7bf1d7d..29ffda6 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -494,8 +494,9 @@ class ExecuteCSVMode(Frame): class Configuration(Frame): - # generate configuration window to set program constants + # generate settings window to set program constants + # noinspection PyUnusedLocal def __init__(self, parent, controller): Frame.__init__(self, parent) @@ -504,6 +505,7 @@ class Configuration(Frame): 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) @@ -517,35 +519,41 @@ class Configuration(Frame): 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 - # Serial port settings frame: + # 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 = ["XY PSU Serial Port:", "Z PSU Serial Port:"] - self.XY_port = StringVar(value=g.XY_PORT) # create variables to store the port names and set to current names + # 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) - port_vars = [self.XY_port, self.Z_port] + port_vars = [self.XY_port, self.Z_port] # list to store both port variables row = 0 - for text in entry_texts: + 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, padx=5, pady=10) + axis_label = Label(port_frame, text=text, padx=5, pady=10) # create description label axis_label.grid(row=row, column=0, sticky=W) - info_label = Label(port_frame, text="e.g. COM10") + 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 @@ -554,6 +562,8 @@ class Configuration(Frame): 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) @@ -573,7 +583,7 @@ class Configuration(Frame): "Arduino Pins:": [[IntVar() for _ in range(3)], "-", "Should be 15, 16, 17", "relay_pin", 1] } - self.fields = {} + self.fields = {} # setup dictionary with all entry fields # Fill in header (axis names): col = 1 @@ -583,17 +593,17 @@ class Configuration(Frame): col += 1 # generate table with entries, unit labels and descriptions: row = 1 - for key in self.entries.keys(): - self.fields[key] = [] - for axis in range(3): # generate entry fields - field = Entry(value_frame, textvariable=self.entries[key][0][axis], width=10) + 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 for use elsewhere - axis_label = Label(value_frame, text=key, padx=5, pady=5) + 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]) + 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]) + 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 @@ -604,7 +614,7 @@ class Configuration(Frame): Label(self, text="", pady=10).grid(row=row_counter, column=0) # add spacer row_counter += 1 - # Setup buttons + # 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) @@ -612,9 +622,11 @@ class Configuration(Frame): 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 stand 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) @@ -623,36 +635,36 @@ class Configuration(Frame): 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() + self.update_fields() # update values in the entry fields from config - def restore_defaults(self): # restore all default settings + 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 self.update_fields() # update fields in config window - def update_fields(self): - # set current values for all entry variables from config file + 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) - for key in self.entries.keys(): - for i in [0, 1, 2]: - value = config.read_from_config(g.AXIS_NAMES[i], self.entries[key][3], - config.CONFIG_OBJECT) # 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 + for key in self.entries.keys(): # go through the main value table + for i in [0, 1, 2]: # 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 values are within safe limits: + # check if value is within safe limits: value_check = func.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 file with user inputs into entry fields and reinitialize + 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()) @@ -664,7 +676,8 @@ class Configuration(Frame): 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)) + 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 @@ -672,40 +685,43 @@ class Configuration(Frame): value = value / factor # do unit conversion # Check if value is within safe limits - config_key = self.entries[key][3] # handle by which value is indexed in config file - value_ok = func.value_in_limits(g.AXIS_NAMES[i], config_key, value) - unit = self.entries[key][1] # get unit string for error messages - axis = g.AXIS_NAMES[i] # get axis name for error messages + 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 - if value_ok == 'OK': + 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 - if value_ok == 'HIGH': + 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': + 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: - message = "Unknown case, this should not happen." + 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) - # becomes 'yes' or 'no' depending on user choice + # 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): # executed on button press + 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 @@ -713,7 +729,7 @@ class Configuration(Frame): 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 and save path of selected 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 exists(filename): # does the file exist? @@ -726,7 +742,7 @@ class Configuration(Frame): 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.") - else: + 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 @@ -746,8 +762,8 @@ class Configuration(Frame): 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() - config.write_config_to_file(config.CONFIG_OBJECT) + 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 func.setup_all() # reinitialize devices and program with new values self.update_fields() # update entry fields to show values as they are in the config @@ -755,10 +771,11 @@ class Configuration(Frame): class ConfigureLogging(Frame): # generate window to configure data logging to csv # ToDo: support logging of axis-independent info like Arduino status + # ToDo (optional): Add option to select which axes to log data from def __init__(self, parent, controller): Frame.__init__(self, parent) - self.parent = 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 @@ -773,7 +790,7 @@ class ConfigureLogging(Frame): row_counter = 0 - # setup heading + # 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) @@ -786,15 +803,19 @@ class ConfigureLogging(Frame): 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 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) self.stop_logging_button.grid(row=0, column=0, padx=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) + self.start_logging_button.grid(row=0, column=0, padx=5) # same place as stop logging button, replace each other + # 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=1, 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=2, padx=5) @@ -802,12 +823,13 @@ class ConfigureLogging(Frame): 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 store number of logged datapoints + 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) @@ -817,21 +839,24 @@ class ConfigureLogging(Frame): row_counter += 1 - # create checkboxes and entries to set how often data should be logged + # 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) - self.regular_logging_var = BooleanVar(value=True) # create variable for the regular logging checkbox - self.event_logging_var = BooleanVar(value=True) # create variable for the logging on command checkbox + # create checkbox variable for logging in regular time intervals + self.regular_logging_var = BooleanVar(value=True) + # create checkbox variable for logging whenever test stand 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) + variable=self.regular_logging_var, onvalue=True, offvalue=False) self.event_logging_checkbox = Checkbutton(self.settings_frame, text="Log whenever test stand is commanded", - variable=self.event_logging_var, onvalue=True, offvalue=False) + 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) @@ -846,33 +871,34 @@ class ConfigureLogging(Frame): 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 enable/disable all of them - self.active_keys = [] # list with all the keys relating to the currently ticked 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 - # generate and place all the 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) - # ToDo (optional): Add option to select which axes to log data from + # generate and place all checkboxes: row = 1 - for key in log.axis_data_dict.keys(): + for key in log.axis_data_dict.keys(): # 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=key, # generate checkbox variable=self.checkbox_vars[key], onvalue=True, offvalue=False) checkbox.grid(row=row, column=0, sticky=W) # place checkbox in UI - self.checkboxes.append(checkbox) # add created checkbox to list + self.checkboxes.append(checkbox) # add created checkbox to list of all checkboxes row += 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): + 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 @@ -883,30 +909,33 @@ class ConfigureLogging(Frame): # (if condition is here to keep timestamps consistent when repeatedly starting/stopping) log.zero_time = datetime.now() # set reference time for timestamps in log - error = False - if self.regular_logging: - try: # try to get log interval - interval_ms = int(self.log_interval.get() * 1000) + # 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 logging if there is a problem - error = True - else: + 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.write_to_file_button["state"] = "disabled" self.clear_data_button["state"] = "normal" self.lock_checkboxes() - self.stop_logging_button.tkraise() # switch button to stop + self.stop_logging_button.tkraise() # switch button to "stop" - def stop_logging(self): + 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 logging - self.event_logging = False # tell everything its time to stop logging - self.write_to_file_button["state"] = "normal" # enable button + 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 stand commands + self.write_to_file_button["state"] = "normal" # enable write to file button self.unlock_checkboxes() # enable checkboxes - self.start_logging_button.tkraise() # switch start/stop button to start + self.start_logging_button.tkraise() # switch start/stop button to "start" 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 @@ -915,7 +944,7 @@ class ConfigureLogging(Frame): 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: + else: # a valid filename was selected log.write_to_file(log.log_data, 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 @@ -930,61 +959,69 @@ class ConfigureLogging(Frame): 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 + 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 list for key in self.checkbox_vars.keys(): # go through all checkboxes if self.checkbox_vars[key].get(): # box is ticked self.active_keys.append(key) # 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) + self.interval_entry.config(state=DISABLED) # lock logging interval entry field - def unlock_checkboxes(self): + 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 active - self.log_datapoint() - self.controller.after(interval, lambda: self.periodic_log(interval)) # call again after time interval + 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: + except Exception as e: # some error occurred messagebox.showerror("Error!", "Error while logging data: \n%s" % e) - def update_datapoint_count(self): - if self.regular_logging or self.event_logging: # logging is active + 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 stand status in real time + # noinspection PyUnusedLocal def __init__(self, parent, controller): Frame.__init__(self, parent, relief=SUNKEN, bd=1) + # 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"]: # create Column headers + 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 = rowCounter + 1 # increase row counter to place future stuff below header - # define content of row entries + rowCounter += 1 # increase row counter to place future stuff below header + + # define content of row entries: + # ToDo (optional): Use the central dictionary, currently defined in csv_logging.py TextLabels = ["PSU Serial Port:", "PSU Channel:", "PSU Status:", "Arduino Status:", "", "Output:", "Remote Control:", "Voltage Setpoint:", "Actual Voltage:", "Current Setpoint:", "Actual Current:", "", @@ -995,36 +1032,40 @@ class StatusDisplay(Frame): # prepare list of lists to contain all labels for row entries in all columns: self.Labels = [[] for _ in range(self.columnNo)] - self.label_dict = {} - for name in TextLabels: - self.label_dict[name] = [StringVar() for _ in range(self.columnNo - 1)] - # add labels for row titles + # 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): # add labels vor values + 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: # place row entries in grid layout for all columns - for row in range(self.rowNo): # place row entries - LabelCol[row].grid(row=row + rowCounter, column=col, sticky="nsew") - col = col + 1 - # rowCounter = rowCounter + self.rowNo # increase row counter to place future stuff below this + 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 - self.update_labels() + self.update_labels() # fill in all values def continuous_label_update(self, controller, interval): # update display values in regular intervals (ms) - if not g.exitFlag: # app ist still running - self.update_labels() + 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): # update all values in the status display - g.ARDUINO.update_status_info() + g.ARDUINO.update_status_info() # get latest status info from arduino i = 0 - for axis in g.AXES: - if axis.device is not None: - axis.update_status_info() + 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 # 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) @@ -1042,31 +1083,35 @@ class StatusDisplay(Frame): i += 1 -class OutputConsole(Frame): # console to print stuff in, similar to standard python output +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) + # configure Tkinter grid: self.grid_rowconfigure(ALL, weight=1) - self.grid_columnconfigure(0, weight=1, minsize=60) + self.grid_columnconfigure(0, weight=1, minsize=60) # console needs to have a minimum width - scrollbar = Scrollbar(self) - self.console = Text(self) - self.console.bind("", lambda e: "break") # prevent user input into the console + scrollbar = Scrollbar(self) # setup scrollbar + self.console = Text(self) # setup main console widget + self.console.bind("", lambda e: "break") # prevent user from writing into the console - scrollbar.grid(row=0, column=1, sticky="ns") - self.console.grid(row=0, column=0, sticky="nesw") + 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) -def ui_print(*content): # prints text to built in console, use exactly like normal print() - output = "" - for text in content: - output = " ".join((output, str(text))) # merge all contents into one string - if not g.exitFlag: +def ui_print(*content): # prints text to built-in console, use exactly like normal print() + output = "" # initialize output as empty string + for text in content: # go through all elements to be printed + output = " ".join((output, str(text))) # add element to the output string + + if not g.exitFlag: # application is still running --> output window is visible output = "".join(("\n", output)) # begin new line each time g.app.OutputConsole.console.insert(END, output) # print to console - g.app.OutputConsole.console.see(END) # scroll to bottom + g.app.OutputConsole.console.see(END) # scroll console to bottom else: # if window is not open, do normal print print(output) From 177928d82ea555e048fb0b856872759ecb265559 Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Sat, 20 Feb 2021 18:08:41 +0100 Subject: [PATCH 36/36] code cleanup and comments --- Arduino/arduino.py | 1 + User_Interface.py | 4 ++- csv_threading.py | 3 ++- globals.py | 44 ++++++++++++++++++-------------- main.py | 49 +++++++++++++++++++----------------- LICENSE => pyps2000b/LICENSE | 0 pyps2000b/PS2000B.py | 2 ++ 7 files changed, 59 insertions(+), 44 deletions(-) rename LICENSE => pyps2000b/LICENSE (100%) diff --git a/Arduino/arduino.py b/Arduino/arduino.py index 597c58b..37f0cfd 100644 --- a/Arduino/arduino.py +++ b/Arduino/arduino.py @@ -1,3 +1,4 @@ +# This file enables control of a connected Arduino microcontroller. #!/usr/bin/env python import logging import itertools diff --git a/User_Interface.py b/User_Interface.py index 29ffda6..94cb111 100644 --- a/User_Interface.py +++ b/User_Interface.py @@ -1,19 +1,21 @@ # 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. +# import packages for user interface: 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 from os.path import exists import threading from datetime import datetime +# import other project files: import globals as g import cage_func as func import csv_threading as csv diff --git a/csv_threading.py b/csv_threading.py index 3dbd1cb..6ec1fc8 100644 --- a/csv_threading.py +++ b/csv_threading.py @@ -1,13 +1,14 @@ # tThis file contains code for executing a sequence of magnetic fields from a csv file. # To do this without crashing the UI it has to run in a separate thread using the threading module. +# import packages: import time import pandas from threading import * from tkinter import messagebox - import matplotlib.pyplot as plt +# import other project files: import User_Interface as ui import cage_func as func import globals as g diff --git a/globals.py b/globals.py index 116049d..281add2 100644 --- a/globals.py +++ b/globals.py @@ -1,43 +1,49 @@ +# This file is used to hold global variables that are used by more than one file of the program. +# Instead of always passing all variables to functions, this file can simply be imported to get them. + import numpy as np -# global variables set in other files -XY_DEVICE = None -Z_DEVICE = None -ARDUINO = None +XY_DEVICE = None # XY PSU object will be stored here (class PS2000B) +Z_DEVICE = None # Z PSU object will be stored here (class PS2000B) +ARDUINO = None # Arduino object will be stored here (class ArduinoCtrl) +# Axis objects will be stored here (class Axis) X_AXIS = None Y_AXIS = None Z_AXIS = None AXES = None # list containing [X_AXIS, Y_AXIS, Z_AXIS] -app = None +app = None # Main Tkinter application object will be stored here (class HelmholtzGUI) -AXIS_NAMES = ["X-Axis", "Y-Axis", "Z-Axis"] +AXIS_NAMES = ["X-Axis", "Y-Axis", "Z-Axis"] # list with the names of each axis, used mainly for printing functions -global XY_PORT -global Z_PORT +global XY_PORT # serial port for XY PSU will be stored here (string) +global Z_PORT # serial port for Z PSU will be stored here (string) -global PORTS +global PORTS # list containing [XY_PORT, XY_PORT, Z_PORT], used in loops where info on each axis is needed -global threadLock +global threadLock # thread locking object, used to force threads to perform actions in a certain order (threading.Lock) -exitFlag = True # False when main window is open, false otherwise +exitFlag = True # False when main window is open, True otherwise -# Default Constants and maximum/minimum values (warning messages will be generated if these are exceeded) -# format: [[default values], [maximum values], [minimum values]] -# ToDo: check actual maximum ratings -# ToDo: Add maximum current: 5A (BA Blessing page 30), remove max_watts (there for testing with resistors) +# Create dictionaries with default Constants and maximum/minimum values +# Used to create default configs and to check if user inputs are within safe limits +# ToDo: check actual maximum ratings (or refine after testing) # ToDo: put this into a config file + +# Dictionary for numerical values: +# format: key: [default values], [maximum values], [minimum values] default_arrays = { "coil_const": np.array([[38.6, 38.45, 37.9], [50, 50, 50], [0, 0, 0]]) * 1e-6, # Coil constants [x,y,z] [T/A] - "ambient_field": np.array([[30, 30, 30], [200, 200, 200], [-200, -200, -200]]) * 1e-6, # background magnetic field [T] + "ambient_field": np.array([[30, 30, 30], [200, 200, 200], [-200, -200, -200]]) * 1e-6, # ambient magnetic field [T] "resistance": np.array([[1.7, 1.7, 1.7], [5, 5, 5], [1, 1, 1]], dtype=float), # resistance of circuits [Ohm] - "max_volts": np.array([[14, 14, 14], [16, 16, 16], [0, 0, 0]], dtype=float), # max. allowed voltage, limited to 16V by used diodes! [V] + "max_volts": np.array([[14, 14, 14], [16, 16, 16], [0, 0, 0]], dtype=float), # max. voltage, limited to 16V by used diodes! [V] "max_amps": np.array([[4.5, 4.5, 4.5], [6, 6, 6], [0, 0, 0]], dtype=float), # max. allowed current (A) "relay_pin": [[15, 16, 17], [15, 16, 17], [15, 16, 17]] # pins on the arduino for reversing [x,y,z] polarity } +# Dictionary for PSU serial ports: 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 + "xy_port": "COM1", # Default serial port where PSU for X- and Y-Axes is connected + "z_port": "COM2", # Default serial port where PSU for Z-Axis is connected } diff --git a/main.py b/main.py index 5fb229d..3428d40 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,11 @@ +# Main file of the program. Run this file to start the application. + +# import packages: from os.path import exists import traceback from tkinter import messagebox +# import other project files: import cage_func as func from User_Interface import HelmholtzGUI from User_Interface import ui_print @@ -12,64 +16,63 @@ import csv_logging as log def program_end(): # called on exception or when user closes application + # safely shuts everything down and saves any unsaved data + g.exitFlag = True # tell everything else the application has been closed - if g.app is not None: - if g.app.pages[ui.ExecuteCSVMode].csv_thread is not None: # end possible csv execution thread - g.app.pages[ui.ExecuteCSVMode].csv_thread.stop() # stop thread + if g.app is not None: # the main Tkinter app object has been initialized before + if g.app.pages[ui.ExecuteCSVMode].csv_thread is not None: # check if a thread for executing CSVs exists + g.app.pages[ui.ExecuteCSVMode].csv_thread.stop() # stop the thread + func.shut_down_all() # shut down devices - if log.unsaved_data: # There is logged data that has not been saved yet + if log.unsaved_data: # Check if there is logged data that has not been saved yet # open pop-up to ask user if he wants to save the data: save_log = messagebox.askquestion("Save log data?", "There seems to be unsaved logging data. " "Do you wish to write it to a file now?") if save_log == 'yes': # user has chosen yes filepath = log.select_file() # let user select a file to write to log.write_to_file(log.log_data, filepath) # write the data to the chosen file + if g.app is not None: g.app.destroy() # close application try: # start normal operations - config.CONFIG_FILE = 'config.ini' + config.CONFIG_FILE = 'config.ini' # set the config file path # ToDo: remember what the last config file was - if not exists(config.CONFIG_FILE): + if not exists(config.CONFIG_FILE): # config file does not exist yet print("Config file not found, creating new from defaults.") - config.reset_config_to_default() - config.write_config_to_file(config.CONFIG_OBJECT) + config.reset_config_to_default() # create configuration object from defaults + config.write_config_to_file(config.CONFIG_OBJECT) # write the configuration object to a new file - config.CONFIG_OBJECT = config.get_config_from_file(config.CONFIG_FILE) + config.CONFIG_OBJECT = config.get_config_from_file(config.CONFIG_FILE) # read configuration data from config file print("Starting setup...") - func.setup_all() # initiate communication, set handles + func.setup_all() # initiate communication with devices and initialize all major program objects print("\nOpening User Interface...") - g.app = HelmholtzGUI() - g.exitFlag = False - g.app.state('zoomed') # open maximized + g.app = HelmholtzGUI() # initialize user interface + g.exitFlag = False # tell all functions that the user interface is now running + g.app.state('zoomed') # open UI in maximized window g.app.StatusDisplay.continuous_label_update(g.app, 500) # initiate regular Status Display updates (ms) ui_print("Program Initialized") config.check_config(config.CONFIG_OBJECT) # check config for values exceeding limits - ui_print("\nStarting setup...") # do it again, so it is printed in the UI console ToDo: do it only once - func.setup_all() # initiate communication, set handles + ui_print("\nStarting setup...") # do setup again, so it is printed in the UI console ToDo: do it only once + func.setup_all() # initiate communication with devices and initialize all major program objects g.app.protocol("WM_DELETE_WINDOW", program_end) # call program_end function if user closes the application - g.app.mainloop() + g.app.mainloop() # start main program loop -except Exception as e: # if there is an error, print what happened +except Exception as e: # An error has occurred somewhere in the program print("\nAn error occurred, Shutting down.") # shop pup-up error message: message = "%s.\nSee python console traceback for more details. " \ "\nShutting down devices, check equipment to confirm." % e messagebox.showerror("Error!", message) - print(traceback.print_exc()) + print(traceback.print_exc()) # print error traceback in the python console program_end() # safely close everything and shut down devices - -# ToDo: rework window closing code https://bytes.com/topic/python/answers/431323-detect-tkinter-window-being-closed -# https://stackoverflow.com/questions/14694408/runtimeerror-main-thread-is-not-in-main-loop - -# ToDo: logging diff --git a/LICENSE b/pyps2000b/LICENSE similarity index 100% rename from LICENSE rename to pyps2000b/LICENSE diff --git a/pyps2000b/PS2000B.py b/pyps2000b/PS2000B.py index be9d496..a1a3d7a 100644 --- a/pyps2000b/PS2000B.py +++ b/pyps2000b/PS2000B.py @@ -1,3 +1,5 @@ +# This file enables communication with PS2000B Power Supply Units. + #!/usr/bin/env python3 # coding=utf-8 # Python access to Elektro Automatik PS 2000 B devices via USB/serial