From 8d1870956f46fb0b91eb8c78f9fccc1fad4d0f1d Mon Sep 17 00:00:00 2001 From: Martin Zietz Date: Thu, 4 Feb 2021 15:33:19 +0100 Subject: [PATCH] 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