forked from zietzm/Helmholtz_Test_Bench
Rewrite: added command execution. Not runnable.
This commit is contained in:
+225
-56
@@ -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)
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user