Skip to content

Commit 5864862

Browse files
authored
Merge pull request #72 from ZLLentz/da-valve
ENH: Dual-acting valve
2 parents 7760cac + 2ba1c42 commit 5864862

14 files changed

Lines changed: 589 additions & 20 deletions

File tree

.pre-commit-config.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# See https://pre-commit.com for more information
2+
# See https://pre-commit.com/hooks.html for more hooks
3+
repos:
4+
- repo: https://github.com/pre-commit/pre-commit-hooks
5+
rev: v4.1.0
6+
hooks:
7+
- id: no-commit-to-branch
8+
- id: trailing-whitespace
9+
- id: end-of-file-fixer
10+
- id: check-ast
11+
- id: check-case-conflict
12+
- id: check-json
13+
- id: check-merge-conflict
14+
- id: check-symlinks
15+
- id: check-xml
16+
- id: check-yaml
17+
exclude: '^(conda-recipe/meta.yaml)$'
18+
- id: debug-statements
19+
20+
- repo: https://gitlab.com/pycqa/flake8.git
21+
rev: 3.9.2
22+
hooks:
23+
- id: flake8
24+
25+
- repo: https://github.com/timothycrosley/isort
26+
rev: 5.10.1
27+
hooks:
28+
- id: isort

docs/source/addwidget.rst

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
==========================
2+
Adding a New Vacuum Widget
3+
==========================
4+
5+
This page details the development process for adding a new vacuum widget.
6+
It was made during the process of creating the PneumaticValveDA widget.
7+
8+
9+
Requirements
10+
------------
11+
You should have the following things in mind before you begin:
12+
13+
- What should the widget look like?
14+
- Is there any existing widget that this is similar to?
15+
- What are my interlock, error, state readback, and control PVs?
16+
17+
18+
Implementation Overview
19+
-----------------------
20+
You'll need to do the following things:
21+
22+
- Add a new icon widget.
23+
- Add a new valve widget that uses the icon.
24+
- Add your new widget to the designer.
25+
- Update stylesheets to be consistent for your new widget.
26+
- Add a new device class for your widget's expert screen.
27+
28+
29+
Adding a New Icon Widget
30+
------------------------
31+
The icon widgets are stored in pcdswidgets/icons and are implemented by
32+
using the QtGui painter tools. I suggest you pick a class that almost
33+
does what you want as a starting point.
34+
With BaseSignalIcon as a parent class, the only method you need to override
35+
is "draw_icon". Check out the other icons for examples and feel free to
36+
browse the qt documentation.
37+
This process will take a lot of iterations
38+
(edit the file, check the ui, repeat).
39+
To make this process smoother, I added a script embedded in the icons module.
40+
Try this to open an application that simply displays a widget:
41+
42+
.. code-block bash
43+
python -m pcdswidgets.icons.demo ControlValve
44+
45+
Some tips:
46+
47+
- The coordinate system starts from the top left of the icon, so positive y is down
48+
- The expected size of the widget icon is from 0 to 1 in both x and y
49+
- If you want something to be modifyable via stylesheet, PV, etc., you can make it
50+
a property with qt's @Property flag. This is useful for alarm sensitive coloring,
51+
for example.
52+
- When drawing a shape, it's useful to parameterize it even if you only use it once.
53+
This is because you may later want to edit the specifics, but if your QPolygon
54+
is just a list of numbers this will be very hard. A list of variables like
55+
"arrow_length" are easier to modify later.
56+
- When designing your shape, note that the widget might need to look good at
57+
different sizes. Pay particular care in designing widget icons with small features,
58+
these can become indistiguishable as we shrink the shapes down.
59+
60+
61+
Adding a New Widget Class
62+
-------------------------
63+
Widget classes in pcdswidgets are constructed from a network of mix-ins and parent
64+
classes. A good place to start is simply copying the most similar existing
65+
widget and modifying the specifics to match yours. Barring something extremely
66+
similar existing, you'll need to delve into the specifics of the inner workings
67+
and I can't encapsulate that in a guide.
68+
69+
In some cases, you may be able to get away with simply copying a widget
70+
and changing the docstrings, attributes, and super calls as appropriate.
71+
72+
For deeper dives, I recommend looking at each mix-in class in isolation to
73+
understand how that particular feature is implemented.
74+
75+
You can test your new widget quickly by running the helper script:
76+
77+
.. code-block bash
78+
python -m pcdswidgets.vacuum.demo PneumaticValveDA CRIX:VGC:11
79+
80+
But you should make some screens with it to explore the finer details.
81+
82+
83+
Adding your Widget to the Designer
84+
----------------------------------
85+
This is probably the easiest step of the process. Simply import your new widget
86+
in designer.py and add an appriopriate entry using the qtplugin_factory.
87+
88+
To check that this worked, you can simply open designer. You should see
89+
your widget added to the list.
90+
91+
92+
Stylesheets
93+
-----------
94+
For the widget to properly display its state, it needs an entry in the stylesheet.
95+
For widgets that are exceedingly simple to existing widgets, this might just
96+
involve copying and pasting existing entries in the stylesheet, and editing the
97+
copy to refer to your new widget. This is appropriate for adding a new valve type
98+
for example.
99+
100+
In other cases, you'll need to do involved testing to figure out what stylesheet
101+
gives the look and feel you want for the widget, and make sure this ends up in
102+
the master stylesheet.
103+
104+
The master stylesheet is held in the vacuumscreens repo. If it has not moved,
105+
it can be viewed at
106+
https://github.com/pcdshub/vacuumscreens/blob/master/styleSheet/masterStyleSheet.qss
107+
108+
To activate this stylesheet for dev use, you need to set your
109+
PYDM_STYLESHEET environment variable appropriately, e.g.
110+
111+
.. code-block bash
112+
export PYDM_STYLESHEET=/some/path/to/my/dev/folder/vacuumscreens/styleSheet.masterStyleSheet.qss
113+
114+
Make sure to open a pull request with your updated stylesheet in that repo and make
115+
sure that your edits get deployed in dev/prod.
116+
117+
118+
The Expert Screen
119+
-----------------
120+
We typically build our expert screens out of ophyd objects using the typhos module.
121+
All the specifics of this are out of scope for this tutorial, but check out
122+
pcdsdevices for our main repository of device definitions.
123+
124+
125+
Documentation
126+
-------------
127+
It is important to document your new widget.
128+
See examples throughout the documentation here.
129+
130+
There are three places to update:
131+
132+
- In the vacuum subfolder, find the relevant file and add your widget
133+
to the most logical section
134+
- In icons.rst, extend the two reference areas with a line for the table
135+
and the class-matching icon png name. The icon pngs are created
136+
automatically, so make sure your icon is importable from the top-level
137+
pcdswidgets.icons and is included in the __all__ tuple there.
138+
- Write detailed docstrings in the new classes you have added
139+
(both the icon and widget).
140+
141+
142+
Testing
143+
-------
144+
Your widget will automatically be tested if imported in the __all__ tuple
145+
of the vacuum submodule. Make sure to import your new widget in __init__.py
146+
there and include it in the __all__ tuple.
147+
148+
This will catch basic issues only.
149+
150+
You should also test your widget on real devices to make sure the behavior is
151+
correct.

docs/source/icons.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Name Icon Import
3434
================== =========== =================================================================
3535
Pneumatic |pneuvalve| ``from pcdswidgets.icons import PneumaticValveSymbolIcon``
3636
Pneumatic NO |pnnovalve| ``from pcdswidgets.icons import PneumaticValveNOSymbolIcon``
37+
Pneumatic DA |pndavalve| ``from pcdswidgets.icons import PneumaticValveNOSymbolIcon``
3738
Aperture |aperture| ``from pcdswidgets.icons import ApertureValveSymbolIcon``
3839
Fast Shutter |fshutter| ``from pcdswidgets.icons import FastShutterSymbolIcon``
3940
Right Angle Manual |ramvalve| ``from pcdswidgets.icons import RightAngleManualValveSymbolIcon``
@@ -45,6 +46,7 @@ ControlOnlyValve |cntrlonly| ``from pcdswidgets.icons import ControlOnlyValveS
4546

4647
.. |pneuvalve| image:: /_static/icons/PneumaticValveSymbolIcon.png
4748
.. |pnnovalve| image:: /_static/icons/PneumaticValveNOSymbolIcon.png
49+
.. |pndavalve| image:: /_static/icons/PneumaticValveDASymbolIcon.png
4850
.. |aperture| image:: /_static/icons/ApertureValveSymbolIcon.png
4951
.. |fshutter| image:: /_static/icons/FastShutterSymbolIcon.png
5052
.. |ramvalve| image:: /_static/icons/RightAngleManualValveSymbolIcon.png

docs/source/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ LCLS PyDM Widget Library
2525
:caption: Developer Documentation
2626
:hidden:
2727

28-
dev.rst
28+
dev.rst
29+
addwidget.rst
2930

3031
.. toctree::
3132
:maxdepth: 1

docs/source/vacuum/valves.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ Pneumatic Valve - NO
1414
.. autoclass:: pcdswidgets.vacuum.valves.PneumaticValveNO
1515
:members:
1616

17+
Pneumatic Valve - DA
18+
--------------------
19+
20+
.. autoclass:: pcdswidgets.vacuum.valves.PneumaticValveDA
21+
:members:
22+
1723
Aperture Valve
1824
--------------
1925

pcdswidgets/designer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from .vacuum.valves import (PneumaticValve, FastShutter, NeedleValve,
88
ProportionalValve, RightAngleManualValve,
99
ApertureValve, ControlValve, ControlOnlyValveNC,
10-
ControlOnlyValveNO, PneumaticValveNO)
10+
ControlOnlyValveNO, PneumaticValveNO,
11+
PneumaticValveDA)
1112

1213
from .vacuum.base import PCDSSymbolBase
1314

@@ -22,6 +23,8 @@
2223
group="PCDS Valves")
2324
PCDSPneumaticValveNOPlugin = qtplugin_factory(PneumaticValveNO,
2425
group="PCDS Valves")
26+
PCDSPneumaticValveDAPlugin = qtplugin_factory(PneumaticValveDA,
27+
group="PCDS Valves")
2528
PCDSApertureValvePlugin = qtplugin_factory(ApertureValve, group='PCDS Valves')
2629
PCDSFastShutterPlugin = qtplugin_factory(FastShutter, group="PCDS Valves")
2730

pcdswidgets/icons/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
ApertureValveSymbolIcon, RightAngleManualValveSymbolIcon,
77
NeedleValveSymbolIcon, ProportionalValveSymbolIcon,
88
ControlValveSymbolIcon, ControlOnlyValveSymbolIcon,
9-
PneumaticValveNOSymbolIcon)
9+
PneumaticValveNOSymbolIcon, PneumaticValveDASymbolIcon)
1010
from .others import RGASymbolIcon
1111

1212
__all__ = ['RoughGaugeSymbolIcon', 'CathodeGaugeSymbolIcon',
@@ -17,4 +17,5 @@
1717
'ApertureValveSymbolIcon', 'RightAngleManualValveSymbolIcon',
1818
'NeedleValveSymbolIcon', 'ProportionalValveSymbolIcon',
1919
'RGASymbolIcon', 'ControlValveSymbolIcon',
20-
'ControlOnlyValveSymbolIcon', 'PneumaticValveNOSymbolIcon']
20+
'ControlOnlyValveSymbolIcon', 'PneumaticValveNOSymbolIcon',
21+
'PneumaticValveDASymbolIcon']

pcdswidgets/icons/demo/__init__.py

Whitespace-only changes.

pcdswidgets/icons/demo/__main__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
Show an icon. Useful for development.
3+
4+
Invoke as e.g. "python -m pcdswidgets.icons.demo ControlValve"
5+
"""
6+
import sys
7+
8+
from qtpy.QtWidgets import QApplication
9+
10+
from .. import * # noqa
11+
12+
13+
cls = sys.argv[1] + 'SymbolIcon'
14+
app = QApplication([])
15+
icon = globals()[cls]()
16+
icon.show()
17+
app.exec()

pcdswidgets/icons/valves.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import math
22

3-
from qtpy.QtCore import (QPointF, QRectF, Qt, Property, QLineF)
4-
from qtpy.QtGui import (QPainterPath, QBrush, QColor, QPolygonF, QTransform)
3+
from qtpy.QtCore import Property, QLineF, QPointF, QRectF, Qt
4+
from qtpy.QtGui import (QBrush, QColor, QPainterPath, QPen, QPolygonF,
5+
QTransform)
56

67
from .base import BaseSymbolIcon
78

@@ -356,3 +357,77 @@ def draw_icon(self, painter):
356357

357358
# Draw the O
358359
painter.drawEllipse(QPointF(0.65, 0.15), 0.1, 0.1)
360+
361+
362+
class PneumaticValveDASymbolIcon(BaseSymbolIcon):
363+
"""
364+
A widget with a dual-acting pneumatic valve symbol drawn in it.
365+
366+
Parameters
367+
----------
368+
parent : QWidget
369+
The parent widget for the icon
370+
"""
371+
def __init__(self, parent=None, **kwargs):
372+
super().__init__(parent, **kwargs)
373+
self._interlock_brush = QBrush(QColor(0, 255, 0), Qt.SolidPattern)
374+
375+
@Property(QBrush)
376+
def interlockBrush(self):
377+
return self._interlock_brush
378+
379+
@interlockBrush.setter
380+
def interlockBrush(self, new_brush):
381+
if new_brush != self._interlock_brush:
382+
self._interlock_brush = new_brush
383+
self.update()
384+
385+
def draw_icon(self, painter):
386+
path = QPainterPath(QPointF(0, 0.3))
387+
path.lineTo(0, 0.9)
388+
path.lineTo(1, 0.3)
389+
path.lineTo(1, 0.9)
390+
path.closeSubpath()
391+
painter.drawPath(path)
392+
painter.drawLine(QPointF(0.5, 0.6), QPointF(0.5, 0.3))
393+
painter.setBrush(self._interlock_brush)
394+
painter.drawRect(QRectF(0.2, 0, 0.6, 0.3))
395+
396+
# Set fill color to black, line width to minimum for arrows
397+
black_brush = QBrush(QColor(0, 0, 0))
398+
painter.setBrush(black_brush)
399+
painter.setPen(QPen(black_brush, 0))
400+
401+
# Draw an arrow around 0, 0 pointing right
402+
# This polygon starts from the tip and works its way around clockwise
403+
tip_length = 0.2
404+
tip_width = 0.1
405+
length = 0.2
406+
width = 0.02
407+
rightward_arrow = QPolygonF(
408+
[QPointF(tip_length, 0.0),
409+
QPointF(0.0, -tip_width),
410+
QPointF(0.0, -width),
411+
QPointF(-length, -width),
412+
QPointF(-length, width),
413+
QPointF(0.0, width),
414+
QPointF(0.0, tip_width)]
415+
)
416+
# Second arrow looks left
417+
point_left = QTransform()
418+
point_left.rotate(180)
419+
leftward_arrow = point_left.map(rightward_arrow)
420+
421+
# Establish start positions for the arrows
422+
# This is where the origin goes (0, 0)
423+
# This is where the triangle meets the line
424+
right_start = QPointF(0.59, 0.15)
425+
left_start = QPointF(0.41, 0.15)
426+
427+
# Assign arrows to positions
428+
right_translated = rightward_arrow.translated(right_start)
429+
left_translated = leftward_arrow.translated(left_start)
430+
431+
# Call draw
432+
painter.drawPolygon(right_translated)
433+
painter.drawPolygon(left_translated)

0 commit comments

Comments
 (0)