Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ class MotorClassicFullBase(DesignerWidget):
"PyDMLabel_egu",
"PyDMByteIndicator_hls",
"PyDMLabel_name",
"PyDMPushButton_clear_error",
"PyDMLineEdit_setpoint",
"PyDMByteIndicator_mvn",
"PyDMPushButton_stop",
Expand Down Expand Up @@ -88,9 +87,6 @@ class MotorClassicFullBase(DesignerWidget):
"PyDMLineEdit_twVal": [
"MOTOR",
],
"PyDMPushButton_clear_error": [
"MOTOR",
],
"PyDMPushButton_stop": [
"MOTOR",
],
Expand Down Expand Up @@ -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"""),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
107 changes: 107 additions & 0 deletions pcdswidgets/motion/common/motor_classic_full.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +78 to +81

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these for?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's confusing, but without these defined qt explodes when trying to run the pydm screen.
I've removed them and ran again to paste the error here and to construct a more complete explanation/understanding:

Details
Traceback (most recent call last):
  File "/cds/home/z/zlentz/projects/widget_cleanup_ecs-10276/pcdswidgets/.pixi/envs/default/bin/pydm", line 10, in <module>
    sys.exit(main())
             ^^^^^^
  File "/cds/home/z/zlentz/projects/widget_cleanup_ecs-10276/pcdswidgets/.pixi/envs/default/lib/python3.12/site-packages/pydm_launcher/main.py", line 120, in main
    app = pydm.PyDMApplication(
          ^^^^^^^^^^^^^^^^^^^^^
  File "/cds/home/z/zlentz/projects/widget_cleanup_ecs-10276/pcdswidgets/.pixi/envs/default/lib/python3.12/site-packages/pydm/application.py", line 134, in __init__
    self.main_window.open(ui_file, macros, command_line_args)
  File "/cds/home/z/zlentz/projects/widget_cleanup_ecs-10276/pcdswidgets/.pixi/envs/default/lib/python3.12/site-packages/pydm/main_window.py", line 412, in open
    new_widget = load_file(filename, macros=macros, args=args, target=target)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/cds/home/z/zlentz/projects/widget_cleanup_ecs-10276/pcdswidgets/.pixi/envs/default/lib/python3.12/site-packages/pydm/display.py", line 73, in load_file
    loaded_display = loader(file, args=args, macros=macros)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/cds/home/z/zlentz/projects/widget_cleanup_ecs-10276/pcdswidgets/.pixi/envs/default/lib/python3.12/site-packages/pydm/display.py", line 211, in load_ui_file
    display.load_ui_from_file(uifile, macros)
  File "/cds/home/z/zlentz/projects/widget_cleanup_ecs-10276/pcdswidgets/.pixi/envs/default/lib/python3.12/site-packages/pydm/display.py", line 454, in load_ui_from_file
    _load_compiled_ui_into_display(code_string, class_name, self, macros)
  File "/cds/home/z/zlentz/projects/widget_cleanup_ecs-10276/pcdswidgets/.pixi/envs/default/lib/python3.12/site-packages/pydm/display.py", line 184, in _load_compiled_ui_into_display
    klass.setupUi(display, display)
  File "<string>", line 22, in setupUi
AttributeError: type object 'MotorClassicFull' has no attribute 'GENERIC'

So, in the moment I simply added them in to make the error go away (and because my examples in PyDM do the same), but going further this must be due to how the pyuic5 ui file compiler works. This is done at runtime for PyDM so we can't examine the source code unless we generate it ourselves:

pixi run pyuic5 -o ../output.py ../test_motor_err.ui

Scrolling to lines 20-25 as per the traceback where we error on line 22

        self.MotorClassicFull = MotorClassicFull(Form)
        self.MotorClassicFull.setToolTip("")
        self.MotorClassicFull.setProperty("motor_type", MotorClassicFull.GENERIC)
        self.MotorClassicFull.setObjectName("MotorClassicFull")
        self.verticalLayout.addWidget(self.MotorClassicFull)

It becomes clear that the pyuic5 compiler that generates the python code has a baked-in assumption that all enum members are also stored as class variables. So this must be the underlying reason why we have to include these splayed out in the class here.


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}")
5 changes: 3 additions & 2 deletions pcdswidgets/tests/builder/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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])
9 changes: 0 additions & 9 deletions pcdswidgets/ui/motion/common/motor_classic_full.ui
Original file line number Diff line number Diff line change
Expand Up @@ -634,9 +634,6 @@ background-color: rgb(250, 250, 250);</string>
<property name="monitorDisp">
<bool>false</bool>
</property>
<property name="channel">
<string>ca://${MOTOR}:SEQ_SELN</string>
</property>
<property name="PyDMIcon">
<string/>
</property>
Expand All @@ -655,12 +652,6 @@ background-color: rgb(250, 250, 250);</string>
<property name="confirmMessage">
<string>Are you sure you want to proceed?</string>
</property>
<property name="pressValue">
<string>48</string>
</property>
<property name="releaseValue">
<string>None</string>
</property>
<property name="relativeChange">
<bool>false</bool>
</property>
Expand Down
Loading