From 80f036060a0d08e92bdd635e6950334a3a0d03c7 Mon Sep 17 00:00:00 2001 From: Leon Teichroeb Date: Wed, 28 Jul 2021 11:18:08 +0200 Subject: [PATCH] Rewrite: added command execution. Not runnable. --- src/helmholtz_cage_device.py | 281 ++++++++++++++++++++++++++++------- src/psu_device.py | 7 + 2 files changed, 232 insertions(+), 56 deletions(-) diff --git a/src/helmholtz_cage_device.py b/src/helmholtz_cage_device.py index 9d0da32..ae74022 100644 --- a/src/helmholtz_cage_device.py +++ b/src/helmholtz_cage_device.py @@ -1,6 +1,7 @@ import traceback -from threading import RLock +from threading import RLock, Thread, Event from tkinter import messagebox +from copy import deepcopy from src.arduino_device import ArduinoDevice from src.psu_device import PSUDevicePS2000B, PSUDeviceQL355TP @@ -8,6 +9,11 @@ from src.user_interface import ui_print import src.config_handling as config import src.globals as g + +class ProxyNotOwnedException(Exception): + pass + + class HelmholtzCageDevice: """This is the central object for controlling all the test bench related HW. This way, access can be synchronized and exclusive to a single controller at once. This device always exists, irrespective of @@ -17,7 +23,19 @@ class HelmholtzCageDevice: # This class is thread safe + POLLING_INTERVAL = 1.5 # Seconds between polling the device state + def __init__(self): + # Indicates all the threads should be joined + self._stop_flag = Event() + + # --- COMMAND QUEUEING --- + self.command_lock = RLock() + # Indicates to the command executing thread that a new command has arrived for execution + self.new_command_flag = Event() + # Contains the next command to be executed + self.command = None + # --- PROXY MODEL SETUP --- # Locks all proxy requesting and releasing access self.proxy_lock = RLock() @@ -25,6 +43,9 @@ class HelmholtzCageDevice: self.proxy_id = None # --- DEVICE SETUP --- + # Must be acquired to access any hardware + self.hardware_lock = RLock() + # All devices are usable if the object exists. None indicates the device is not connected/not working properly # Arduino setup try: @@ -50,6 +71,7 @@ class HelmholtzCageDevice: raise Exception("Invalid psu model: {}".format(self.psu_type)) except Exception as e: self.psu1 = None + # psu2: controls z axis try: if self.psu_type == "ps2000b": @@ -61,6 +83,28 @@ class HelmholtzCageDevice: except Exception as e: self.psu2 = None + # Zero and activate channels. This is a sort of "armed" state so that we can send commands later + self.psu1.idle() + self.psu2.idle() + + # --- AXIS CONFIGURATION --- + # This is also hardware related, but in a separate section to keep it clean. + # Get all settings from the config file + self.axes = [] + # Loop over axes + for i in range(3): + axis_dict = {} + for key in ["coil_const", "ambient_field", "resistance", "max_volts", "max_amps"] + axis_dict[key] = float(config.read_from_config(g.AXIS_NAMES[i], key, config.CONFIG_OBJECT)) + self.axes.append(axis_dict) + + # --- HW COMMUNICATION THREAD --- + self._cmd_exec_thread = Thread(target=self._cmd_exec_thread_method) + self._cmd_exec_thread.start() + + self._hw_poll_thread = Thread(target=self._hw_poll_thread_method) + self._hw_poll_thread.start() + def request_proxy(self): """Returns a new HelmholtzCageProxy or None, depending on if access is available""" with self.proxy_lock: @@ -82,68 +126,184 @@ class HelmholtzCageDevice: # Otherwise do nothing, this case requires no behaviour def queue_command(self, proxy_obj, command): - """ Queues a dict for immediate execution containing the command for the cage as a whole. """ + """ Queues a dict for immediate execution containing the command for the cage as a whole. + Since the newest command should always be run, it is not a real queue (just a variable)""" + with self.proxy_lock: + if id(proxy_obj) != self.proxy_id: + raise ProxyNotOwnedException() + + with self.command_lock: + # Overwrite any command that was queued but not yet executed. We now only care about the newer command + self.command = command + self.new_command_flag.set() + + def _cmd_exec_thread_method(self): + """This method forms the main thread for hardware command execution.""" + while not self._stop_flag.is_set(): + self.new_command_flag.wait() + # Avoid blocking the buffer while we are executing. + with self.command_lock: + command_buffer = deepcopy(self.command) # Dicts are mutable so must be copied + self.command = None # Processed commands are removed from "buffer" + if command_buffer: + try: + # Unpack command into "action" and argument + command_string = command_buffer['command'] + command_arg = command_buffer['arg'] + # Check which command and delegate to responsible function + if command_string == "set_signed_currents": + self._set_signed_currents(command_arg) + elif command_string == "set_field_raw": + self._set_field_raw(command_arg) + elif command_string == "set_field_compensated": + self._set_field_compensated(command_arg) + else: + raise Exception("Command unknown!") + except Exception as e: + # No ui print, since this is really not something that should happen... (relevant for devs) + print("An error occurred while processing command: {}".format(self.command)) + traceback.print_exc() + + def _hw_poll_thread_method(self): + """This method forms the main thread for hardware command execution.""" + while True: + # We will have to check if we passed this statement due to a stop flag or due to the polling interval + stop_flag_set = self._stop_flag.wait(timeout=self.POLLING_INTERVAL) + if stop_flag_set: + return + + with self.hardware_lock: + pass + + def _set_field_raw(self, arg): + currents = [] + for i in range(3): + currents.append(arg[i] / self.axes[i]['coil_const']) + + def _set_field_compensated(self, arg): + currents = [] + for i in range(3): + target_field = arg[i] - self.axes[i]['ambient_field'] + currents.append(target_field / self.axes[i]['coil_const']) + + def _set_signed_currents(self, arg): + """Sets the currents in the array arg in the respective coils x->y->z. + This function imposes safety limits by clamping the current when beyond the maximum.""" + + if self.psu1 is None or self.psu2 is None or self.arduino is None: + ui_print("Can't set current, PSUs and Arduino are not connected.") + + # One pass for every axis + for i in range(3): + # Check current limits + if abs(arg[i]) <= self.axes[i]['max_amps']: + safe_current = arg[i] + else: + safe_current = self.axes[i]['max_amps'] + ui_print("Attempted to exceed current limit on {}".format(g.AXIS_NAMES[i])) + + # Talk to hardware + with self.hardware_lock: + # TODO: Check for exceptions + # Set polarity on Arduino + if safe_current < 0: + # Reverse polarity + self.arduino.set_axis_polarity(i, True) + else: + # Positive polarity (default case) + self.arduino.set_axis_polarity(i, False) + + # 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) + voltage_limit = min(max(1.3 * abs(safe_current) * self.axes[i]['resistance'], 8), + self.axes[i]['max_volts']) # limit voltage + # TODO: This kind of stuff belongs in the config and should not be hardcoded + # Determine which channel of which psu is required + if i == 0 or i == 1: + psu = self.psu1 + channel = psu.valid_channels[i] + else: + psu = self.psu2 + channel = psu.valid_channels[0] + + # Set voltages and currents. Outputs should already be active from initializer. + psu.set_current(channel, safe_current) + psu.set_voltage(channel, voltage_limit) def shutdown(self): - """ Shuts down the hardware. This special command overrides the currently active proxy.""" + """ Shuts down the hardware. This special command overrides the currently active proxy. + The object cannot be recovered from this state, but may be re-instantiated.""" - 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." + # Send signals to kill threads: + self._stop_flag.set() + # _cmd_exec_thread: + with self.command_lock: + self.command = None + self.new_command_flag.set() # Causes the thread to unblock + self._cmd_exec_thread.join(timeout=2) + # TODO: Handle timeout behaviour + #_hw_poll_thread: - # Shutdown XY PSU - if self.psu1 is not None: - try: - self.psu1.shutdown() - self.psu1.destroy() - self.psu1 = None - except Exception as e: - 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." # 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." - # Shutdown Z PSU - if self.psu2 is not None: - try: - self.psu2.shutdown() - self.psu2.destroy() - self.psu2 = None - except Exception as e: - ui_print("Error while deactivating Z PSU:", e) # print the problem in the console - message += "\nError while deactivating Z PSU: %s" % e # append status to the message to show later - else: # device was successfully deactivated - ui_print("Z PSU deactivated.") - message += "\nZ 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("Z PSU not connected, can't deactivate.") - message += "\nZ PSU not connected, can't deactivate." + # This waiting period is not easily removed without resulting in unexpected behaviour + with self.hardware_lock: + 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." - # Shut down arduino: - if self.arduino is not None: - try: - self.arduino.shutdown() - self.arduino.close() - self.arduino = None - except Exception as e: - ui_print("Error while deactivating Arduino:", e) # print the problem in the console - message += "\nError while deactivating Arduino: %s" % e # append status to the message to show later - else: # device was successfully deactivated - ui_print("Arduino deactivated.") - message += "\nArduino 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("Arduino not connected, can't deactivate.") - message += "\nArduino not connected, can't deactivate." + # Shutdown XY PSU + if self.psu1 is not None: + try: + self.psu1.shutdown() + self.psu1.destroy() + self.psu1 = None + except Exception as e: + 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." # 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." - messagebox.showinfo("Program ended", - message) # Show a unified pop-up with how the shutdown on each device went + # Shutdown Z PSU + if self.psu2 is not None: + try: + self.psu2.shutdown() + self.psu2.destroy() + self.psu2 = None + except Exception as e: + ui_print("Error while deactivating Z PSU:", e) # print the problem in the console + message += "\nError while deactivating Z PSU: %s" % e # append status to the message to show later + else: # device was successfully deactivated + ui_print("Z PSU deactivated.") + message += "\nZ 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("Z PSU not connected, can't deactivate.") + message += "\nZ PSU not connected, can't deactivate." + + # Shut down arduino: + if self.arduino is not None: + try: + self.arduino.shutdown() + self.arduino.close() + self.arduino = None + except Exception as e: + ui_print("Error while deactivating Arduino:", e) # print the problem in the console + message += "\nError while deactivating Arduino: %s" % e # append status to the message to show later + else: # device was successfully deactivated + ui_print("Arduino deactivated.") + message += "\nArduino 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("Arduino not connected, can't deactivate.") + message += "\nArduino not connected, can't deactivate." + + messagebox.showinfo("Program ended", + message) # Show a unified pop-up with how the shutdown on each device went class HelmholtzCageProxy: @@ -152,5 +312,14 @@ class HelmholtzCageProxy: def __init__(self, cage_device): self.cage_device = cage_device + def set_signed_currents(self, vector): + self.cage_device.queue_command(self, {'command': 'set_signed_currents', 'arg': vector}) + + def set_field_raw(self, vector): + self.cage_device.queue_command(self, {'command': 'set_field_raw', 'arg': vector}) + + def set_field_compensated(self, vector): + self.cage_device.queue_command(self, {'command': 'set_field_compensated', 'arg': vector}) + def __del__(self): self.cage_device.release_proxy(self) \ No newline at end of file diff --git a/src/psu_device.py b/src/psu_device.py index dca9204..c988d7f 100644 --- a/src/psu_device.py +++ b/src/psu_device.py @@ -55,6 +55,13 @@ class PSUDevice(ABC): # 'actual_voltage':0, 'limit_voltage':0, 'actual_current':0, 'limit_current':0} pass + def idle(self): + """Zero all outputs but activate channels so commands can be sent.""" + for ch in self.valid_channels: + self.set_current(ch, 0) + self.set_voltage(ch, 0) + self.enable_channel(ch) + @abstractmethod def shutdown(self): """Shuts the PSU down safely and makes sure ALL outputs are off."""