From 09de5b75a6b57a8722d0231c96f6c4438d159427 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 11 Jun 2026 11:14:17 -0700 Subject: [PATCH 1/2] ENH: add an enum property to MotorClassicFull to change which motor reset method we use --- .../motion/common/motor_classic_full_base.py | 7 -- .../motion/common/motor_classic_full_form.py | 3 - .../motion/common/motor_classic_full.py | 107 ++++++++++++++++++ .../ui/motion/common/motor_classic_full.ui | 9 -- 4 files changed, 107 insertions(+), 19 deletions(-) diff --git a/pcdswidgets/generated/motion/common/motor_classic_full_base.py b/pcdswidgets/generated/motion/common/motor_classic_full_base.py index e5cf4ae5..9ba18b4d 100644 --- a/pcdswidgets/generated/motion/common/motor_classic_full_base.py +++ b/pcdswidgets/generated/motion/common/motor_classic_full_base.py @@ -56,7 +56,6 @@ class MotorClassicFullBase(DesignerWidget): "PyDMLabel_egu", "PyDMByteIndicator_hls", "PyDMLabel_name", - "PyDMPushButton_clear_error", "PyDMLineEdit_setpoint", "PyDMByteIndicator_mvn", "PyDMPushButton_stop", @@ -88,9 +87,6 @@ class MotorClassicFullBase(DesignerWidget): "PyDMLineEdit_twVal": [ "MOTOR", ], - "PyDMPushButton_clear_error": [ - "MOTOR", - ], "PyDMPushButton_stop": [ "MOTOR", ], @@ -129,9 +125,6 @@ class MotorClassicFullBase(DesignerWidget): "PyDMLineEdit_twVal": [ ("channel", """ca://${MOTOR}.TWV"""), ], - "PyDMPushButton_clear_error": [ - ("channel", "ca://${MOTOR}:SEQ_SELN"), - ], "PyDMPushButton_stop": [ ("channel", """ca://${MOTOR}.STOP"""), ], diff --git a/pcdswidgets/generated/motion/common/motor_classic_full_form.py b/pcdswidgets/generated/motion/common/motor_classic_full_form.py index 11ec94a7..c0fa10db 100644 --- a/pcdswidgets/generated/motion/common/motor_classic_full_form.py +++ b/pcdswidgets/generated/motion/common/motor_classic_full_form.py @@ -344,10 +344,7 @@ def retranslateUi(self, Form): self.PyDMByteIndicator_hls.setChannel(_translate("Form", "ca://${MOTOR}.HLS")) self.PyDMLabel_name.setChannel(_translate("Form", "ca://${MOTOR}.DESC")) self.PyDMPushButton_clear_error.setText(_translate("Form", "Clear Error")) - self.PyDMPushButton_clear_error.setChannel(_translate("Form", "ca://${MOTOR}:SEQ_SELN")) self.PyDMPushButton_clear_error.setConfirmMessage(_translate("Form", "Are you sure you want to proceed?")) - self.PyDMPushButton_clear_error.setPressValue(_translate("Form", "48")) - self.PyDMPushButton_clear_error.setReleaseValue(_translate("Form", "None")) self.PyDMLineEdit_setpoint.setChannel(_translate("Form", "ca://${MOTOR}.VAL")) self.PyDMByteIndicator_mvn.setChannel(_translate("Form", "ca://${MOTOR}.MOVN")) self.PyDMPushButton_stop.setText(_translate("Form", "Stop")) diff --git a/pcdswidgets/motion/common/motor_classic_full.py b/pcdswidgets/motion/common/motor_classic_full.py index 8d15977b..488f7fd9 100644 --- a/pcdswidgets/motion/common/motor_classic_full.py +++ b/pcdswidgets/motion/common/motor_classic_full.py @@ -4,14 +4,121 @@ This file can be safely edited to change the runtime behavior of the widget. """ +from pydm.utilities import ACTIVE_QT_WRAPPER, QtWrapperTypes +from qtpy.QtWidgets import QWidget + from pcdswidgets.builder.designer_options import DesignerOptions from pcdswidgets.builder.icon_options import IconOptions from pcdswidgets.generated.motion.common.motor_classic_full_base import MotorClassicFullBase +try: + from qtpy.QtCore import pyqtProperty +except ImportError: + from qtpy.QtCore import Property as pyqtProperty # type: ignore + +# Note: for forward compat, setting up enum properties is completely different +# depending on if we use pyqt5 or pyside6. +# I'm following the examples in PyDM here. +if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYSIDE6: + from enum import Enum + + from PySide6.QtCore import QEnum # type: ignore + + @QEnum + class MotorTypes(Enum): # type: ignore + """Options for motor type to select error reset PV.""" + + GENERIC = 0 + IMS = 1 + BECKHOFF = 2 + BECKHOFF_LEGACY = 3 + +else: + # pyqt5 can't use real python enums for this, unfortunately + class MotorTypes: # type: ignore + """Options for motor type to select error reset PV.""" + + GENERIC = 0 + IMS = 1 + BECKHOFF = 2 + BECKHOFF_LEGACY = 3 + class MotorClassicFull(MotorClassicFullBase): + """ + Generic motor widget at full size. + + This class is extended to allow us to support multiple + types of motor IOCs with the same ui layout. + + The user can set the `motor_type` enum to change the + following behavior: + + - Error reset PV and value + + There are four options: + + - GENERIC: default, no error reset. + - IMS: standard IMS IOC (not motor record). + - BECKHOFF: Motor that uses the 2026+ version of our TwinCAT motion libraries. + - BECKHOFF_LEGACY: uses the pre-2026 version of the above. + """ + designer_options = DesignerOptions( group="ECS Motion Common", is_container=False, icon=IconOptions.NONE, ) + # Boilerplate to make the enum property work + if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5: + from PyQt5.QtCore import Q_ENUM + + Q_ENUM(MotorTypes) + MotorTypes = MotorTypes + GENERIC = MotorTypes.GENERIC + IMS = MotorTypes.IMS + BECKHOFF = MotorTypes.BECKHOFF + BECKHOFF_LEGACY = MotorTypes.BECKHOFF_LEGACY + + def __init__(self, parent: QWidget | None = None): + super().__init__(parent) + self._motor_type = MotorTypes.GENERIC + self._clear_error_suffix = "" + self.PyDMPushButton_clear_error.hide() + + def after_set_macro(self, macro_name: str, value: str): + """Puts motor prefix into error reset PV.""" + if macro_name == "MOTOR": + self.new_clear_error_motor(value) + + def get_motor_type(self) -> MotorTypes | int: + return self._motor_type + + def set_motor_type(self, value: MotorTypes | int) -> None: + self._motor_type = value + match value: + case MotorTypes.IMS: + self.new_clear_error_suffix(":SEQ_SELN") + self.PyDMPushButton_clear_error.setPressValue(48) + self.PyDMPushButton_clear_error.show() + case MotorTypes.BECKHOFF: + self.new_clear_error_suffix(":bReset") + self.PyDMPushButton_clear_error.setPressValue(1) + self.PyDMPushButton_clear_error.show() + case MotorTypes.BECKHOFF_LEGACY: + self.new_clear_error_suffix(":PLC:bReset") + self.PyDMPushButton_clear_error.setPressValue(1) + self.PyDMPushButton_clear_error.show() + case _: + self.PyDMPushButton_clear_error.hide() + + motor_type = pyqtProperty(MotorTypes, get_motor_type, set_motor_type) + + def new_clear_error_suffix(self, suffix: str): + self._clear_error_suffix = suffix + if motor := self.get_macro("MOTOR"): + self.new_clear_error_motor(motor) + + def new_clear_error_motor(self, motor: str): + if self._clear_error_suffix: + self.PyDMPushButton_clear_error.set_channel(f"ca://{motor}{self._clear_error_suffix}") diff --git a/pcdswidgets/ui/motion/common/motor_classic_full.ui b/pcdswidgets/ui/motion/common/motor_classic_full.ui index decbc048..517c8c2a 100644 --- a/pcdswidgets/ui/motion/common/motor_classic_full.ui +++ b/pcdswidgets/ui/motion/common/motor_classic_full.ui @@ -634,9 +634,6 @@ background-color: rgb(250, 250, 250); false - - ca://${MOTOR}:SEQ_SELN - @@ -655,12 +652,6 @@ background-color: rgb(250, 250, 250); Are you sure you want to proceed? - - 48 - - - None - false From ccf8179db1c771ce474ac57212ffa8c00ed456a0 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 11 Jun 2026 11:27:48 -0700 Subject: [PATCH 2/2] TST: fix incorrect test assumption and annotate --- pcdswidgets/tests/builder/test_builder.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pcdswidgets/tests/builder/test_builder.py b/pcdswidgets/tests/builder/test_builder.py index 4500d346..c7fbd9bc 100644 --- a/pcdswidgets/tests/builder/test_builder.py +++ b/pcdswidgets/tests/builder/test_builder.py @@ -7,7 +7,7 @@ import pcdswidgets from pcdswidgets.builder.designer_widget import DesignerWidget -MODULE_ROOT = Path(pcdswidgets.__file__).parent +MODULE_ROOT = Path(pcdswidgets.__file__).parent # type: ignore UI_SOURCES = sorted((MODULE_ROOT / "ui").rglob("*.ui")) TEST_UI = str(Path(__file__).parent / "pytest.ui") @@ -90,7 +90,8 @@ def test_built_is_importable(ui_source: Path): assert issubclass(base_classes[0], DesignerWidget) assert base_classes[0].ui_form is form_classes[0] - assert len(main_classes) == 1 + # User can add additional classes + assert len(main_classes) >= 1 assert hasattr(main_classes[0], "designer_options") assert hasattr(main_classes[0], "_qt_designer_") assert issubclass(main_classes[0], base_classes[0])