Rewrite: added command execution. Not runnable.

This commit is contained in:
2021-07-28 11:18:08 +02:00
parent fa6d50d04c
commit 80f036060a
2 changed files with 232 additions and 56 deletions
+225 -56
View File
@@ -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)
+7
View File
@@ -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."""