0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 11:41:49 +02:00

refactor: base class for motor_control.py widgets

This commit is contained in:
wyzula-jan
2024-01-29 14:23:34 +01:00
parent 6fe08e6b82
commit 8139e271de
4 changed files with 171 additions and 183 deletions

View File

@ -4,4 +4,9 @@ from .scan_control import ScanControl
from .toolbar import ModularToolBar
from .editor import BECEditor
from .monitor_scatter_2D import BECMonitor2DScatter
from .motor_control import MotorControlRelative, MotorControlAbsolute, MotorControlSelection
from .motor_control import (
MotorControlRelative,
MotorControlAbsolute,
MotorControlSelection,
MotorThread,
)

View File

@ -1 +1,6 @@
from .motor_control import MotorControlRelative, MotorControlAbsolute, MotorControlSelection
from .motor_control import (
MotorControlRelative,
MotorControlAbsolute,
MotorControlSelection,
MotorThread,
)

View File

@ -4,7 +4,6 @@ from enum import Enum
import qdarktheme
from pyqtgraph.Qt import uic
from qtpy import uic
from qtpy.QtCore import QThread, Slot as pyqtSlot
from qtpy.QtCore import Signal as pyqtSignal, Qt
@ -34,41 +33,24 @@ CONFIG_DEFAULT = {
}
}
# class MotorControlConnectAbsoltue(QWidget):
class MotorControlWidget(QWidget):
"""Base class for motor control widgets."""
# class MotorControlPanel(QWidget):
# def __init__(self,parent=None):
# super().__init__()
# self.init_ui()
# def init_ui(self):
# """Initialize the UI."""
class MotorControlSelection(QWidget):
update_signal = pyqtSignal()
selected_motors_signal = pyqtSignal(str, str)
def __init__(
self,
parent=None,
client=None,
motor_thread=None,
config: dict = None,
):
super().__init__(parent=parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.dev = self.client.device_manager.devices
def __init__(self, parent=None, client=None, motor_thread=None, config=None):
super().__init__(parent)
self.client = client
self.motor_thread = motor_thread
self.config = config
# Loading UI
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "motor_control_selection.ui"), self)
if not self.client:
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client
# Motor Control Thread
self.motor_thread = (
MotorThread(client=self.client) if motor_thread is None else motor_thread
)
if not self.motor_thread:
self.motor_thread = MotorThread(client=self.client)
self._load_ui()
if self.config is None:
print(f"No initial config found for {self.__class__.__name__}")
@ -76,8 +58,44 @@ class MotorControlSelection(QWidget):
else:
self.on_config_update(self.config)
def _load_ui(self):
"""Load the UI from the .ui file."""
def _init_ui(self):
"""Initialize the UI components specific to the widget."""
@pyqtSlot(dict)
def on_config_update(self, config):
"""Handle configuration updates."""
self.config = config
self._init_ui()
class MotorControlSelection(MotorControlWidget):
"""
Widget for selecting the motors to control.
Signals:
selected_motors_signal (pyqtSignal): Signal to emit the selected motors.
Slots:
get_available_motors (pyqtSlot): Slot to populate the available motors in the combo boxes and set the index based on the configuration.
select_motor (pyqtSlot): Slot to emit the selected motors.
enable_motor_controls (pyqtSlot): Slot to enable/disable the motor controls GUI.
"""
selected_motors_signal = pyqtSignal(str, str)
def _load_ui(self):
"""Load the UI from the .ui file."""
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "motor_control_selection.ui"), self)
def _init_ui(self):
"""Initialize the UI."""
# Lock GUI while motors are moving
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
# self.motor_thread.move_finished.connect(self.motorSelection.setEnabled(True))
self.pushButton_connecMotors.clicked.connect(self.select_motor)
self.get_available_motors()
@ -98,6 +116,15 @@ class MotorControlSelection(QWidget):
self._init_ui()
@pyqtSlot(bool)
def enable_motor_controls(self, enable: bool) -> None:
"""
Enable or disable the motor controls.
Args:
enable(bool): True to enable, False to disable.
"""
self.motorSelection.setEnabled(enable)
def get_available_motors(self):
"""
Slot to populate the available motors in the combo boxes and set the index based on the configuration.
@ -110,31 +137,11 @@ class MotorControlSelection(QWidget):
self.comboBox_motor_y.addItems(self.motor_list)
# Set the index based on the config if provided
if self.config is not None:
if self.config:
index_x = self.comboBox_motor_x.findText(self.motor_x)
index_y = self.comboBox_motor_y.findText(self.motor_y)
if index_x != -1:
self.comboBox_motor_x.setCurrentIndex(index_x)
else:
print(
f"Warning: Motor '{self.motor_x}' specified in the config file is not available."
)
self.comboBox_motor_x.setCurrentIndex(0)
if index_y != -1:
self.comboBox_motor_y.setCurrentIndex(index_y)
else:
print(
f"Warning: Motor '{self.motor_y}' specified in the config file is not available."
)
self.comboBox_motor_y.setCurrentIndex(0)
if index_x != -1 and index_y != -1:
self.selected_motors_signal.emit(self.motor_x, self.motor_y)
# setup default index 0, if there is no config
else:
self.comboBox_motor_x.setCurrentIndex(0)
self.comboBox_motor_y.setCurrentIndex(0)
self.comboBox_motor_x.setCurrentIndex(index_x if index_x != -1 else 0)
self.comboBox_motor_y.setCurrentIndex(index_y if index_y != -1 else 0)
def select_motor(self):
"""Emit the selected motors"""
@ -142,41 +149,53 @@ class MotorControlSelection(QWidget):
motor_y = self.comboBox_motor_y.currentText()
self.selected_motors_signal.emit(motor_x, motor_y)
print(f"emitted motors {motor_x} and {motor_y}")
class MotorControlAbsolute(QWidget):
update_signal = pyqtSignal()
class MotorControlAbsolute(MotorControlWidget):
"""
Widget for controlling the motors to absolute coordinates.
Signals:
coordinates_signal (pyqtSignal): Signal to emit the coordinates.
Slots:
change_motors (pyqtSlot): Slot to change the active motors.
enable_motor_controls (pyqtSlot): Slot to enable/disable the motor controls.
"""
coordinates_signal = pyqtSignal(tuple)
def __init__(
self,
parent=None,
client=None,
motor_thread=None,
config: dict = None,
):
super().__init__(parent=parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.dev = self.client.device_manager.devices
self.config = config
# Loading UI
def _load_ui(self):
"""Load the UI from the .ui file."""
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "motor_control_absolute.ui"), self)
# Motor Control Thread
self.motor_thread = (
MotorThread(client=self.client) if motor_thread is None else motor_thread
def _init_ui(self):
"""Initialize the UI."""
# Check if there are any motors connected
if self.motor_x is None or self.motor_y is None:
self.motorControl_absolute.setEnabled(False)
return
# Move to absolute coordinates
self.pushButton_go_absolute.clicked.connect(
lambda: self.move_motor_absolute(
self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value()
)
)
if self.config is None:
print(f"No initial config found for {self.__class__.__name__}")
self._init_ui()
else:
self.on_config_update(self.config)
self.pushButton_set.clicked.connect(self.save_absolute_coordinates)
self.pushButton_save.clicked.connect(self.save_current_coordinates)
self.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
# Enable/Disable GUI
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
# self.motor_thread.move_finished.connect(lambda: self._enable_motor_controls(True))
# Error messages
self.motor_thread.motor_error.connect(
lambda error: MotorControlErrors.display_error_message(error)
)
@pyqtSlot(dict)
def on_config_update(self, config: dict) -> None:
@ -194,51 +213,50 @@ class MotorControlAbsolute(QWidget):
self._init_ui()
def _init_ui(self):
"""Initialize the UI."""
@pyqtSlot(bool)
def enable_motor_controls(self, enable: bool) -> None:
"""
Enable or disable the motor controls.
Args:
enable(bool): True to enable, False to disable.
"""
# Check if there are any motors connected
if (
self.motor_x is None or self.motor_y is None
): # TODO change logic of checking -> jsut check names
self.motorControl_absolute.setEnabled(False)
return
# Move to absolute coordinates
self.pushButton_go_absolute.clicked.connect(
lambda: self.move_motor_absolute(
self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value()
)
)
self.pushButton_set.clicked.connect(self.save_absolute_coordinates)
self.pushButton_save.clicked.connect(self.save_current_coordinates)
self.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
# Enable/Disable GUI
self.motor_thread.move_finished.connect(lambda: self._enable_motor_controls(True))
# Error messages
self.motor_thread.motor_error.connect(
lambda error: MotorControlErrors.display_error_message(error)
)
def _enable_motor_controls(self, disable: bool) -> None:
# Disable or enable all controls within the motorControl_absolute group box
for widget in self.motorControl_absolute.findChildren(QWidget):
widget.setEnabled(disable)
widget.setEnabled(enable)
# Enable the pushButton_stop if the motor is moving
self.pushButton_stop.setEnabled(True)
@pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str):
"""
Change the active motors and update config.
Can be connected to the selected_motors_signal from MotorControlSelection.
Args:
motor_x(str): New motor X to be controlled.
motor_y(str): New motor Y to be controlled.
"""
self.motor_x = motor_x
self.motor_y = motor_y
self.config["motor_control"]["motor_x"] = motor_x
self.config["motor_control"]["motor_y"] = motor_y
def move_motor_absolute(self, x: float, y: float) -> None:
self._enable_motor_controls(False)
"""
Move the motor to the target coordinates.
Args:
x(float): Target x coordinate.
y(float): Target y coordinate.
"""
# self._enable_motor_controls(False)
target_coordinates = (x, y)
self.motor_thread.move_absolute(self.motor_x, self.motor_y, target_coordinates)
if self.checkBox_save_with_go.isChecked():
self.save_absolute_coordinates()
def _init_keyboard_shortcuts(self):
"""Initialize the keyboard shortcuts."""
# Go absolute button
self.pushButton_go_absolute.setShortcut("Ctrl+G")
self.pushButton_go_absolute.setToolTip("Ctrl+G")
@ -265,43 +283,27 @@ class MotorControlAbsolute(QWidget):
def save_current_coordinates(self):
"""Emit the current coordinates from the motor thread"""
x, y = self.motor_thread.retrieve_coordinates()
self.coordinates_signal.emit((x, y))
x, y = self.motor_thread.get_coordinates(self.motor_x, self.motor_y)
self.coordinates_signal.emit((round(x, self.precision), round(y, self.precision)))
class MotorControlRelative(QWidget):
update_signal = pyqtSignal()
class MotorControlRelative(MotorControlWidget):
"""
Widget for controlling the motors to relative coordinates.
def __init__(
self,
parent=None,
client=None,
motor_thread=None,
config: dict = None,
):
super().__init__(parent=parent)
# BECclient
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.dev = self.client.device_manager.devices
self.config = config
Signals:
coordinates_signal (pyqtSignal): Signal to emit the coordinates.
Slots:
change_motors (pyqtSlot): Slot to change the active motors.
enable_motor_controls (pyqtSlot): Slot to enable/disable the motor controls.
"""
def _load_ui(self):
"""Load the UI from the .ui file."""
# Loading UI
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "motor_control_relative.ui"), self)
# Motor Control Thread
self.motor_thread = (
MotorThread(client=self.client) if motor_thread is None else motor_thread
)
self._init_ui()
if self.config is None:
print(f"No initial config found for {self.__class__.__name__}")
else:
self.on_config_update(self.config)
def _init_ui(self):
"""Initialize the UI."""
self._init_ui_motor_control()
@ -330,6 +332,8 @@ class MotorControlRelative(QWidget):
self.checkBox_same_xy.setChecked(self.config["motor_control"]["step_x_y_same"])
self.checkBox_enableArrows.setChecked(self.config["motor_control"]["move_with_arrows"])
self._init_ui()
def _init_ui_motor_control(self) -> None:
"""Initialize the motor control elements"""
@ -354,7 +358,7 @@ class MotorControlRelative(QWidget):
self._update_arrow_key_shortcuts()
# Enable/Disable GUI
self.motor_thread.move_finished.connect(lambda: self.enable_motor_controls(True))
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
# Precision update
self.spinBox_precision.valueChanged.connect(lambda x: self._update_precision(x))
@ -440,15 +444,11 @@ class MotorControlRelative(QWidget):
value = self.spinBox_step_y.value()
self.spinBox_step_x.setValue(value)
@pyqtSlot()
def enable_motor_control(self):
"""Enable the motor control buttons."""
self.motorControl.setEnabled(True)
@pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str):
"""
Change the active motors and update config.
Can be connected to the selected_motors_signal from MotorControlSelection.
Args:
motor_x(str): New motor X to be controlled.
motor_y(str): New motor Y to be controlled.
@ -458,11 +458,12 @@ class MotorControlRelative(QWidget):
self.config["motor_control"]["motor_x"] = motor_x
self.config["motor_control"]["motor_y"] = motor_y
@pyqtSlot(bool)
def enable_motor_controls(self, disable: bool) -> None:
"""
Enable or disable the motor controls.
Args:
disable(bool): True to disable, False to enable.
enable(bool): True to disable, False to enable.
"""
# Disable or enable all controls within the motorControl_absolute group box
@ -480,7 +481,6 @@ class MotorControlRelative(QWidget):
axis(str): Axis to move.
direction(int): Direction to move. 1 for positive, -1 for negative.
"""
self.enable_motor_controls(False)
if axis == "x":
step = direction * self.spinBox_step_x.value()
elif axis == "y":
@ -489,6 +489,8 @@ class MotorControlRelative(QWidget):
class MotorControlErrors:
"""Class for displaying formatted error messages."""
@staticmethod
def display_error_message(error_message: str) -> None:
"""
@ -543,24 +545,15 @@ class MotorThread(QThread):
"""
QThread subclass for controlling motor actions asynchronously.
Attributes:
Signals:
coordinates_updated (pyqtSignal): Signal to emit current coordinates.
limits_retrieved (pyqtSignal): Signal to emit current limits.
move_finished (pyqtSignal): Signal to emit when the move is finished.
motors_loaded (pyqtSignal): Signal to emit when the motors are loaded.
motors_selected (pyqtSignal): Signal to emit when the motors are selected.
motor_error (pyqtSignal): Signal to emit when there is an error with the motors.
lock_gui (pyqtSignal): Signal to lock/unlock the GUI.
"""
coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates
limits_retrieved = pyqtSignal(list, list) # Signal to emit current limits #TODO remove?
move_finished = pyqtSignal() # Signal to emit when the move is finished
motors_loaded = pyqtSignal(
list, list
) # Signal to emit when the motors are loaded #todo remove?
motors_selected = pyqtSignal(
object, object
) # Signal to emit when the motors are selected #TODO remove?
motor_error = pyqtSignal(str) # Signal to emit when there is an error with the motors
lock_gui = pyqtSignal(bool) # Signal to lock/unlock the GUI
def __init__(self, parent=None, client=None):
super().__init__(parent)
@ -648,6 +641,7 @@ class MotorThread(QThread):
motor_y(str): Motor Y to move.
target_coordinates(tuple): Target coordinates.
"""
self.lock_gui.emit(False)
try:
status = self.scans.mv(
self.dev[motor_x],
@ -660,7 +654,7 @@ class MotorThread(QThread):
except AlarmBase as e:
self.motor_error.emit(str(e))
finally:
self.move_finished.emit()
self.lock_gui.emit(True)
def _move_motor_relative(self, motor, value: float) -> None:
"""
@ -669,31 +663,15 @@ class MotorThread(QThread):
motor(str): Motor to move.
value(float): Value to move.
"""
self.lock_gui.emit(False)
try:
status = self.scans.mv(self.dev[motor], value, relative=True)
status.wait()
except AlarmBase as e:
print(e)
self.motor_error.emit(str(e))
finally:
self.move_finished.emit()
self.lock_gui.emit(True)
def stop_movement(self):
self.queue.request_scan_abortion()
self.queue.request_queue_reset()
if __name__ == "__main__":
bec_dispatcher = BECDispatcher()
# BECclient global variables
client = bec_dispatcher.client
client.start()
app = QApplication([])
qdarktheme.setup_theme("auto")
# motor_control = MotorControlRelative(client=client, config=CONFIG_DEFAULT)
motor_control = MotorControlSelection(client=client, config=CONFIG_DEFAULT)
window = motor_control
window.show()
app.exec()

View File

@ -17,7 +17,7 @@
</size>
</property>
<property name="windowTitle">
<string>Form</string>
<string>Motor Control Selection</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>