From 7a4946f52ef4ecfad843d6a5ef8044d51df09e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Tue, 17 Mar 2026 08:41:48 -0300 Subject: [PATCH 01/17] feat(shutters): add `shutters.py` --- src/sophys/common/devices/shutters.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/sophys/common/devices/shutters.py diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py new file mode 100644 index 00000000..e69de29b From 4e5c8a3a078306c82c92770f5f140bca9c3edbce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Tue, 17 Mar 2026 08:42:35 -0300 Subject: [PATCH 02/17] feat(shutters): add firs idea for shutters devices. --- src/sophys/common/devices/shutters.py | 110 ++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index e69de29b..5d497e96 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -0,0 +1,110 @@ +from ophyd import EpicsSignal, EpicsSignalRO, FormattedComponent, Device +from ophyd.pv_positioner import PVPositionerComparator +from ..utils.status import PremadeStatus +from ophyd.status import AndStatus, SubscriptionStatus + + +class ShutterOpenClose(PVPositionerComparator): + real_setpoint = None + setpoint = FormattedComponent(EpicsSignal, "{prefix}{setpoint_suffix}") + readback = FormattedComponent( + EpicsSignalRO, "{prefix}{readback_suffix}", kind="hinted" + ) + permission = FormattedComponent( + EpicsSignalRO, "{prefix}{permission_suffix}", string=True + ) + + def __init__( + self, *args, setpoint_suffix, readback_suffix, permission_suffix, **kwargs + ): + self.setpoint_suffix = setpoint_suffix + self.readback_suffix = readback_suffix + self.permission_suffix = permission_suffix + super().__init__(*args, **kwargs) + + def set(self, value, *args, **kwargs): + if self.permission.connected: + if not self.permission.get(): + raise PremadeStatus( + success=False, + exception=PermissionError( + f"Shutter open permission is denied: {self.permission.pvname} {self.permission.get()}" + ), + ) + + if value == self.readback.get(): + self.real_setpoint = 1 if value == 0 else 0 + return super().set(1, *args, **kwargs) + else: + return PremadeStatus(success=True) + + def done_comparator(self, readback, setpoint): + return self.real_setpoint == readback + + +class ShutterToggle(Device): + setpoint = None + phton_status = FormattedComponent( + EpicsSignalRO, "{prefix}{ps_suffix}", kind="hinted" + ) + gamma_status = FormattedComponent( + EpicsSignalRO, "{prefix}{gs_suffix}", kind="hinted" + ) + open = FormattedComponent(EpicsSignal, "{prefix}{open_suffix}") + close = FormattedComponent(EpicsSignal, "{prefix}{close_suffix}") + permission = FormattedComponent( + EpicsSignalRO, "{prefix}{permission_suffix}", string=True + ) + + def __init__( + self, + *args, + open_suffix, + close_suffix, + ps_suffix, + gs_suffix, + permission_suffix, + **kwargs, + ): + self.open_suffix = open_suffix + self.close_suffix = close_suffix + self.ps_suffix = ps_suffix + self.gs_suffix = gs_suffix + self.permission_suffix = permission_suffix + super().__init__(*args, **kwargs) + + def set(self, value, *args, **kwargs): + if self.permission.connected: + if not self.permission.get(): + raise PremadeStatus( + success=False, + exception=PermissionError( + f"Shutter open permission is denied: {self.permission.pvname} {self.permission.get()}" + ), + ) + + if value == 0: + self.close.set(1, *args, **kwargs).wait() + + elif value == 1: + self.open.set(1, *args, **kwargs).wait() + + else: + raise PremadeStatus( + success=False, + exception=Exception(f"The value {value} is not a valid option!"), + ) + + self.setpoint = value + + return AndStatus( + SubscriptionStatus(self.phton_status, self.done_comparator, settle_time=3), + SubscriptionStatus(self.gamma_status, self.done_comparator, settle_time=3), + timeout=15, + ) + + def done_comparator(self, value, **kwargs): + is_closed = ( + self.phton_status.get() == 1 and self.gamma_status.get() == 1 + ) # NOTE: if one of the status is equal to zero, the shutter can be partially open + return is_closed if self.setpoint == 0 else not is_closed From 867f1971a11748493af044d15ce6f7a819092ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Tue, 17 Mar 2026 10:11:20 -0300 Subject: [PATCH 03/17] feat(shutters): add docstrings for the shutter devices. --- src/sophys/common/devices/shutters.py | 75 ++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 5d497e96..03803b71 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -5,6 +5,36 @@ class ShutterOpenClose(PVPositionerComparator): + """ + Abstraction layer for shutters with one actuation PV (OPENCLOSE) and one readback PV (PG_STATUS). There's an optional parameter for a permission PV. + + Parameters + ---------- + prefix: str + Prefix for the shutter's PVs. + + setpoint_suffix: str + Suffix for the actuation PV, e.g. OPENCLOSE + + readback_suffix: str + Suffix for the readback PV, e.g. PG_STATUS + + permission_suffix: str, optional + Suffix for a open permssion PV, if it exists. + + NOTE + ---- + This implemantation considers that the value of the `readback` signal is 0 for an open shutter and 1 for a closed shutter. + This is not so intuitive, so the `set` method considers that 1 is for opennig and 0 for closing the shutter. + + Usage Example + ------------- + >>> shutter = ShutterOpenClose(prefix="prefix", setpoint_suffix="setpoint_suffix", readback="readback_suffix", name="shutter") + >>> from bluesky.plans_stubs import mv + >>> RE(mv(shutter, 0)) # for closing + >>> RE(mv(shutter, 1)) # for opennig + """ + real_setpoint = None setpoint = FormattedComponent(EpicsSignal, "{prefix}{setpoint_suffix}") readback = FormattedComponent( @@ -32,7 +62,9 @@ def set(self, value, *args, **kwargs): ), ) - if value == self.readback.get(): + if ( + value == self.readback.get() + ): # Since we're swapping the readback values (o for closing and 1 for opennig), we actuate when value == readback self.real_setpoint = 1 if value == 0 else 0 return super().set(1, *args, **kwargs) else: @@ -43,6 +75,47 @@ def done_comparator(self, readback, setpoint): class ShutterToggle(Device): + """ + Abstraction layer for shutters with two actuation PV (OPEN and CLOSE) and two readback PV (PS_STATUS and GS_STATUS). There's an optional parameter for a permission PV. + + Parameters + ---------- + prefix: str + Prefix for the shutter's PVs. + + open_suffix: str + Suffix for the open actuation PV, e.g. OPEN + + close_suffix: str + Suffix for the close actuation PV, e.g. CLOSE + + ps_suffix: str + Suffix for one readback PVs, e.g. PS_STATUS + + close_suffix: str + Suffix for the second readback PV, e.g. GS_STATUS + + permission_suffix: str, optional + Suffix for a open permssion PV, if it exists. + + NOTES + ----- + This implemantation considers that the value of the `readback` signal is 0 for an open shutter and 1 for a closed shutter. + This is not so intuitive, so the `set` method considers that 1 is for opennig and 0 for closing the shutter. + + The `return` of the `set` method is an `AndStatus` with both `readback` signals. + + There's a `done_comparator` method that returns the state of the shutter, based in the two `readback` PVs. This method is + used as the `callback` for both `readback` signals. + + Usage Example + ------------- + >>> shutter = ShutterToggle(prefix="prefix", open_suffix="open_suffix", close_suffix="close_suffix", ps_suffix="ps_suffix", gs_suffix="gs_suffix", name="shutter") + >>> from bluesky.plans_stubs import mv + >>> RE(mv(shutter, 0)) # for closing + >>> RE(mv(shutter, 1)) # for opennig + """ + setpoint = None phton_status = FormattedComponent( EpicsSignalRO, "{prefix}{ps_suffix}", kind="hinted" From 9cf44ce2cdccd3f8c76f071eafa2f0283cfc3598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Tue, 17 Mar 2026 10:53:25 -0300 Subject: [PATCH 04/17] fix(shutters): set correct `ophyd.Kind` for components. --- src/sophys/common/devices/shutters.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 03803b71..81eb2bf5 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -36,12 +36,14 @@ class ShutterOpenClose(PVPositionerComparator): """ real_setpoint = None - setpoint = FormattedComponent(EpicsSignal, "{prefix}{setpoint_suffix}") + setpoint = FormattedComponent( + EpicsSignal, "{prefix}{setpoint_suffix}", kind="config" + ) readback = FormattedComponent( EpicsSignalRO, "{prefix}{readback_suffix}", kind="hinted" ) permission = FormattedComponent( - EpicsSignalRO, "{prefix}{permission_suffix}", string=True + EpicsSignalRO, "{permission_pv}", string=True, kind="config" ) def __init__( @@ -117,16 +119,16 @@ class ShutterToggle(Device): """ setpoint = None - phton_status = FormattedComponent( + photon_status = FormattedComponent( EpicsSignalRO, "{prefix}{ps_suffix}", kind="hinted" ) gamma_status = FormattedComponent( EpicsSignalRO, "{prefix}{gs_suffix}", kind="hinted" ) - open = FormattedComponent(EpicsSignal, "{prefix}{open_suffix}") - close = FormattedComponent(EpicsSignal, "{prefix}{close_suffix}") + open = FormattedComponent(EpicsSignal, "{prefix}{open_suffix}", kind="config") + close = FormattedComponent(EpicsSignal, "{prefix}{close_suffix}", kind="config") permission = FormattedComponent( - EpicsSignalRO, "{prefix}{permission_suffix}", string=True + EpicsSignalRO, "{permission_pv}", string=True, kind="config" ) def __init__( From 7de535ae8847ad1650bc7cedcd9163067eb691ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Tue, 17 Mar 2026 10:54:54 -0300 Subject: [PATCH 05/17] chore(shutters): set parameter `type` for the `__init__` method of shutter devices. --- src/sophys/common/devices/shutters.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 81eb2bf5..55db69a1 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -20,7 +20,7 @@ class ShutterOpenClose(PVPositionerComparator): Suffix for the readback PV, e.g. PG_STATUS permission_suffix: str, optional - Suffix for a open permssion PV, if it exists. + Permssion PV string, if it exists. NOTE ---- @@ -47,11 +47,16 @@ class ShutterOpenClose(PVPositionerComparator): ) def __init__( - self, *args, setpoint_suffix, readback_suffix, permission_suffix, **kwargs + self, + *args, + setpoint_suffix: str, + readback_suffix: str, + permission_pv: str = None, + **kwargs, ): self.setpoint_suffix = setpoint_suffix self.readback_suffix = readback_suffix - self.permission_suffix = permission_suffix + self.permission_pv = permission_pv super().__init__(*args, **kwargs) def set(self, value, *args, **kwargs): @@ -97,8 +102,8 @@ class ShutterToggle(Device): close_suffix: str Suffix for the second readback PV, e.g. GS_STATUS - permission_suffix: str, optional - Suffix for a open permssion PV, if it exists. + permission_pv: str, optional + Permssion PV string, if it exists. NOTES ----- @@ -134,18 +139,18 @@ class ShutterToggle(Device): def __init__( self, *args, - open_suffix, - close_suffix, - ps_suffix, - gs_suffix, - permission_suffix, + open_suffix: str, + close_suffix: str, + ps_suffix: str, + gs_suffix: str, + permission_pv: str = None, **kwargs, ): self.open_suffix = open_suffix self.close_suffix = close_suffix self.ps_suffix = ps_suffix self.gs_suffix = gs_suffix - self.permission_suffix = permission_suffix + self.permission_pv = permission_pv super().__init__(*args, **kwargs) def set(self, value, *args, **kwargs): From b0135b4849179b46695a2a03046d2b77e72d963b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Tue, 17 Mar 2026 10:56:46 -0300 Subject: [PATCH 06/17] feat(shutters): overwrite `read_configuration` method for dealing with disconnected `permission` signal. --- src/sophys/common/devices/shutters.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 55db69a1..7cebafae 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -1,7 +1,8 @@ -from ophyd import EpicsSignal, EpicsSignalRO, FormattedComponent, Device +from ophyd import EpicsSignal, EpicsSignalRO, FormattedComponent, Device, OrderedDict from ophyd.pv_positioner import PVPositionerComparator from ..utils.status import PremadeStatus from ophyd.status import AndStatus, SubscriptionStatus +from time import time class ShutterOpenClose(PVPositionerComparator): @@ -80,6 +81,15 @@ def set(self, value, *args, **kwargs): def done_comparator(self, readback, setpoint): return self.real_setpoint == readback + def read_configuration(self, *args, **kwargs): + if self.permission.connected: + return super().read_configuration(*args, **kwargs) + else: + return { + f"{self.setpoint.name}": self.setpoint.get(*args, **kwargs), + "timestamp": time(), + } + class ShutterToggle(Device): """ @@ -188,3 +198,12 @@ def done_comparator(self, value, **kwargs): self.phton_status.get() == 1 and self.gamma_status.get() == 1 ) # NOTE: if one of the status is equal to zero, the shutter can be partially open return is_closed if self.setpoint == 0 else not is_closed + + def read_configuration(self): + if self.permission.connected: + return super().read_configuration() + else: + res = OrderedDict() + for component in (self.open, self.close): + res.update({f"{component.name}": component.get(), "timestamp": time()}) + return res From e49b4a5a7fab20261a82fe32bc25c8ddf3d831b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Mon, 23 Mar 2026 17:28:15 -0300 Subject: [PATCH 07/17] fix(shutters): swap names from the two shutter classes. --- src/sophys/common/devices/shutters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 7cebafae..e22dd1f6 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -5,7 +5,7 @@ from time import time -class ShutterOpenClose(PVPositionerComparator): +class ShutterToggle(PVPositionerComparator): """ Abstraction layer for shutters with one actuation PV (OPENCLOSE) and one readback PV (PG_STATUS). There's an optional parameter for a permission PV. @@ -91,7 +91,7 @@ def read_configuration(self, *args, **kwargs): } -class ShutterToggle(Device): +class ShutterOpenClose(Device): """ Abstraction layer for shutters with two actuation PV (OPEN and CLOSE) and two readback PV (PS_STATUS and GS_STATUS). There's an optional parameter for a permission PV. From 88df15c69695da51ff45d0ff5d1c003536507a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Mon, 23 Mar 2026 17:28:47 -0300 Subject: [PATCH 08/17] chore(shutters): make docstring example of usage simpler. --- src/sophys/common/devices/shutters.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index e22dd1f6..421dc97b 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -31,9 +31,8 @@ class ShutterToggle(PVPositionerComparator): Usage Example ------------- >>> shutter = ShutterOpenClose(prefix="prefix", setpoint_suffix="setpoint_suffix", readback="readback_suffix", name="shutter") - >>> from bluesky.plans_stubs import mv - >>> RE(mv(shutter, 0)) # for closing - >>> RE(mv(shutter, 1)) # for opennig + >>> shutter.set(0).wait() # for closing + >>> shutter.set(1).wait() # for opening """ real_setpoint = None @@ -128,9 +127,8 @@ class ShutterOpenClose(Device): Usage Example ------------- >>> shutter = ShutterToggle(prefix="prefix", open_suffix="open_suffix", close_suffix="close_suffix", ps_suffix="ps_suffix", gs_suffix="gs_suffix", name="shutter") - >>> from bluesky.plans_stubs import mv - >>> RE(mv(shutter, 0)) # for closing - >>> RE(mv(shutter, 1)) # for opennig + >>> shutter.set(0).wait() # for closing + >>> shutter.set(1).wait() # for opening """ setpoint = None From f9aa1c1b58365de55a88848ea9f4f64f392f5754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Mon, 30 Mar 2026 17:49:44 -0300 Subject: [PATCH 09/17] fix(shutters): fix typos in shutters class. --- src/sophys/common/devices/shutters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 421dc97b..dc116521 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -108,7 +108,7 @@ class ShutterOpenClose(Device): ps_suffix: str Suffix for one readback PVs, e.g. PS_STATUS - close_suffix: str + gs_suffix: str Suffix for the second readback PV, e.g. GS_STATUS permission_pv: str, optional @@ -186,14 +186,14 @@ def set(self, value, *args, **kwargs): self.setpoint = value return AndStatus( - SubscriptionStatus(self.phton_status, self.done_comparator, settle_time=3), + SubscriptionStatus(self.photon_status, self.done_comparator, settle_time=3), SubscriptionStatus(self.gamma_status, self.done_comparator, settle_time=3), timeout=15, ) def done_comparator(self, value, **kwargs): is_closed = ( - self.phton_status.get() == 1 and self.gamma_status.get() == 1 + self.photon_status.get() == 1 and self.gamma_status.get() == 1 ) # NOTE: if one of the status is equal to zero, the shutter can be partially open return is_closed if self.setpoint == 0 else not is_closed From b338fe3bb4b21c9ba5af759213c064841c9868cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Mon, 30 Mar 2026 18:03:51 -0300 Subject: [PATCH 10/17] refactor(shutters): change `ophyd.Kind` of permission signal to "omitted" and remove `read_configuration` method. --- src/sophys/common/devices/shutters.py | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index dc116521..50148485 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -1,8 +1,7 @@ -from ophyd import EpicsSignal, EpicsSignalRO, FormattedComponent, Device, OrderedDict +from ophyd import EpicsSignal, EpicsSignalRO, FormattedComponent, Device from ophyd.pv_positioner import PVPositionerComparator from ..utils.status import PremadeStatus from ophyd.status import AndStatus, SubscriptionStatus -from time import time class ShutterToggle(PVPositionerComparator): @@ -43,7 +42,7 @@ class ShutterToggle(PVPositionerComparator): EpicsSignalRO, "{prefix}{readback_suffix}", kind="hinted" ) permission = FormattedComponent( - EpicsSignalRO, "{permission_pv}", string=True, kind="config" + EpicsSignalRO, "{permission_pv}", string=True, kind="omitted" ) def __init__( @@ -80,15 +79,6 @@ def set(self, value, *args, **kwargs): def done_comparator(self, readback, setpoint): return self.real_setpoint == readback - def read_configuration(self, *args, **kwargs): - if self.permission.connected: - return super().read_configuration(*args, **kwargs) - else: - return { - f"{self.setpoint.name}": self.setpoint.get(*args, **kwargs), - "timestamp": time(), - } - class ShutterOpenClose(Device): """ @@ -141,7 +131,7 @@ class ShutterOpenClose(Device): open = FormattedComponent(EpicsSignal, "{prefix}{open_suffix}", kind="config") close = FormattedComponent(EpicsSignal, "{prefix}{close_suffix}", kind="config") permission = FormattedComponent( - EpicsSignalRO, "{permission_pv}", string=True, kind="config" + EpicsSignalRO, "{permission_pv}", string=True, kind="omitted" ) def __init__( @@ -196,12 +186,3 @@ def done_comparator(self, value, **kwargs): self.photon_status.get() == 1 and self.gamma_status.get() == 1 ) # NOTE: if one of the status is equal to zero, the shutter can be partially open return is_closed if self.setpoint == 0 else not is_closed - - def read_configuration(self): - if self.permission.connected: - return super().read_configuration() - else: - res = OrderedDict() - for component in (self.open, self.close): - res.update({f"{component.name}": component.get(), "timestamp": time()}) - return res From 28726b18297366c158f50331286cbf5c882d3df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Wed, 22 Apr 2026 15:09:30 -0300 Subject: [PATCH 11/17] refactor(shutters): fix the actuation PVs shuffixes `OPENCLOSE`, `OPEN` and `CLOSE`. These suffixes were fixed as, after research, they were found to be repeated in the beamline's PVs. --- src/sophys/common/devices/shutters.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 50148485..58cbae92 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -14,7 +14,8 @@ class ShutterToggle(PVPositionerComparator): Prefix for the shutter's PVs. setpoint_suffix: str - Suffix for the actuation PV, e.g. OPENCLOSE + Suffix for the actuation PV. NOTE: This should be place/location of the shutter, e.g. OEA/FOE. + The PV will be formatted as "{prefix}{setpoint_suffix}OPENCLOSE". readback_suffix: str Suffix for the readback PV, e.g. PG_STATUS @@ -36,7 +37,7 @@ class ShutterToggle(PVPositionerComparator): real_setpoint = None setpoint = FormattedComponent( - EpicsSignal, "{prefix}{setpoint_suffix}", kind="config" + EpicsSignal, "{prefix}{setpoint_suffix}OPENCLOSE", kind="config" ) readback = FormattedComponent( EpicsSignalRO, "{prefix}{readback_suffix}", kind="hinted" @@ -89,11 +90,9 @@ class ShutterOpenClose(Device): prefix: str Prefix for the shutter's PVs. - open_suffix: str - Suffix for the open actuation PV, e.g. OPEN - - close_suffix: str - Suffix for the close actuation PV, e.g. CLOSE + shutter_suffix: str + Suffix for the OPEN and CLOSE PVs. NOTE: This should be place/location of the shutter, e.g. OEA/FOE. + The PVs will be formatted as "{prefix}{shutter_suffix}OPEN" and "{prefix}{shutter_suffix}CLOSE". ps_suffix: str Suffix for one readback PVs, e.g. PS_STATUS @@ -128,8 +127,12 @@ class ShutterOpenClose(Device): gamma_status = FormattedComponent( EpicsSignalRO, "{prefix}{gs_suffix}", kind="hinted" ) - open = FormattedComponent(EpicsSignal, "{prefix}{open_suffix}", kind="config") - close = FormattedComponent(EpicsSignal, "{prefix}{close_suffix}", kind="config") + open = FormattedComponent( + EpicsSignal, "{prefix}{shutter_suffix}OPEN", kind="config" + ) + close = FormattedComponent( + EpicsSignal, "{prefix}{shutter_suffix}CLOSE", kind="config" + ) permission = FormattedComponent( EpicsSignalRO, "{permission_pv}", string=True, kind="omitted" ) @@ -137,15 +140,13 @@ class ShutterOpenClose(Device): def __init__( self, *args, - open_suffix: str, - close_suffix: str, + shutter_suffix: str, ps_suffix: str, gs_suffix: str, permission_pv: str = None, **kwargs, ): - self.open_suffix = open_suffix - self.close_suffix = close_suffix + self.shutter_suffix = shutter_suffix self.ps_suffix = ps_suffix self.gs_suffix = gs_suffix self.permission_pv = permission_pv From 5c75b25c87ac37e3e20bf40ed144a7859e69558f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Wed, 22 Apr 2026 17:05:35 -0300 Subject: [PATCH 12/17] fix(shutters): fix typo in docstring. --- src/sophys/common/devices/shutters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 58cbae92..25117fdd 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -26,7 +26,7 @@ class ShutterToggle(PVPositionerComparator): NOTE ---- This implemantation considers that the value of the `readback` signal is 0 for an open shutter and 1 for a closed shutter. - This is not so intuitive, so the `set` method considers that 1 is for opennig and 0 for closing the shutter. + This is not so intuitive, so the `set` method considers that 1 is for opening and 0 for closing the shutter. Usage Example ------------- @@ -71,7 +71,7 @@ def set(self, value, *args, **kwargs): if ( value == self.readback.get() - ): # Since we're swapping the readback values (o for closing and 1 for opennig), we actuate when value == readback + ): # Since we're swapping the readback values (o for closing and 1 for opening), we actuate when value == readback self.real_setpoint = 1 if value == 0 else 0 return super().set(1, *args, **kwargs) else: @@ -106,7 +106,7 @@ class ShutterOpenClose(Device): NOTES ----- This implemantation considers that the value of the `readback` signal is 0 for an open shutter and 1 for a closed shutter. - This is not so intuitive, so the `set` method considers that 1 is for opennig and 0 for closing the shutter. + This is not so intuitive, so the `set` method considers that 1 is for openig and 0 for closing the shutter. The `return` of the `set` method is an `AndStatus` with both `readback` signals. From e52a7d8ece65488d4f33684ef96dc1c8f7bce306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Thu, 23 Apr 2026 08:50:01 -0300 Subject: [PATCH 13/17] refactor(shutters): instantiate `permission` as attribute if `permission_pv` is defined. --- src/sophys/common/devices/shutters.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 25117fdd..b9343a2f 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -21,7 +21,7 @@ class ShutterToggle(PVPositionerComparator): Suffix for the readback PV, e.g. PG_STATUS permission_suffix: str, optional - Permssion PV string, if it exists. + Permission PV string, if it exists. NOTE ---- @@ -42,9 +42,6 @@ class ShutterToggle(PVPositionerComparator): readback = FormattedComponent( EpicsSignalRO, "{prefix}{readback_suffix}", kind="hinted" ) - permission = FormattedComponent( - EpicsSignalRO, "{permission_pv}", string=True, kind="omitted" - ) def __init__( self, @@ -58,6 +55,11 @@ def __init__( self.readback_suffix = readback_suffix self.permission_pv = permission_pv super().__init__(*args, **kwargs) + if self.permission_pv is not None: + self.permission = EpicsSignalRO(f"{self.permission_pv}", name="permission") + self.permission_flag = True + else: + self.permission_flag = False def set(self, value, *args, **kwargs): if self.permission.connected: @@ -71,7 +73,7 @@ def set(self, value, *args, **kwargs): if ( value == self.readback.get() - ): # Since we're swapping the readback values (o for closing and 1 for opening), we actuate when value == readback + ): # Since we're swapping the readback values (0 for closing and 1 for opening), we actuate when value == readback self.real_setpoint = 1 if value == 0 else 0 return super().set(1, *args, **kwargs) else: @@ -101,7 +103,7 @@ class ShutterOpenClose(Device): Suffix for the second readback PV, e.g. GS_STATUS permission_pv: str, optional - Permssion PV string, if it exists. + Permission PV string, if it exists. NOTES ----- @@ -133,9 +135,6 @@ class ShutterOpenClose(Device): close = FormattedComponent( EpicsSignal, "{prefix}{shutter_suffix}CLOSE", kind="config" ) - permission = FormattedComponent( - EpicsSignalRO, "{permission_pv}", string=True, kind="omitted" - ) def __init__( self, @@ -151,6 +150,11 @@ def __init__( self.gs_suffix = gs_suffix self.permission_pv = permission_pv super().__init__(*args, **kwargs) + if self.permission_pv is not None: + self.permission = EpicsSignalRO(f"{self.permission_pv}", name="permission") + self.permission_flag = True + else: + self.permission_flag = False def set(self, value, *args, **kwargs): if self.permission.connected: From 3ac7d598cbf559941c88d04081ccd410982ba16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Thu, 23 Apr 2026 08:51:23 -0300 Subject: [PATCH 14/17] refactor(shutters): add `permission_flag` inside `set` method and `connection_timeout` param to ensure connection attempt. --- src/sophys/common/devices/shutters.py | 39 +++++++++++++++------------ 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index b9343a2f..58a9777a 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -62,15 +62,17 @@ def __init__( self.permission_flag = False def set(self, value, *args, **kwargs): - if self.permission.connected: - if not self.permission.get(): - raise PremadeStatus( - success=False, - exception=PermissionError( - f"Shutter open permission is denied: {self.permission.pvname} {self.permission.get()}" - ), - ) - + if self.permission_flag: + try: + if not self.permission.get(connection_timeout=2, **kwargs): + raise PremadeStatus( + success=False, + exception=PermissionError( + f"Shutter open permission is denied: {self.permission.pvname} {self.permission.get()}" + ), + ) + except TimeoutError: + raise if ( value == self.readback.get() ): # Since we're swapping the readback values (0 for closing and 1 for opening), we actuate when value == readback @@ -157,14 +159,17 @@ def __init__( self.permission_flag = False def set(self, value, *args, **kwargs): - if self.permission.connected: - if not self.permission.get(): - raise PremadeStatus( - success=False, - exception=PermissionError( - f"Shutter open permission is denied: {self.permission.pvname} {self.permission.get()}" - ), - ) + if self.permission_flag: + try: + if not self.permission.get(connection_timeout=2, **kwargs): + raise PremadeStatus( + success=False, + exception=PermissionError( + f"Shutter open permission is denied: {self.permission.pvname} {self.permission.get()}" + ), + ) + except TimeoutError: + raise if value == 0: self.close.set(1, *args, **kwargs).wait() From f769d1532ad0d963d10897a14d8768ac4fecad8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Fri, 24 Apr 2026 10:56:15 -0300 Subject: [PATCH 15/17] feat(shutters): add `_is_closed` method to check the status of photon and gamma shutters. This method was added so that a check to the current status can be made before the attempt of open/close. --- src/sophys/common/devices/shutters.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 58a9777a..4faadb33 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -171,17 +171,14 @@ def set(self, value, *args, **kwargs): except TimeoutError: raise - if value == 0: + if value == 0 and not self._is_closed(): self.close.set(1, *args, **kwargs).wait() - elif value == 1: + elif value == 1 and self._is_closed(): self.open.set(1, *args, **kwargs).wait() else: - raise PremadeStatus( - success=False, - exception=Exception(f"The value {value} is not a valid option!"), - ) + return PremadeStatus(success=True) self.setpoint = value @@ -191,8 +188,12 @@ def set(self, value, *args, **kwargs): timeout=15, ) - def done_comparator(self, value, **kwargs): - is_closed = ( - self.photon_status.get() == 1 and self.gamma_status.get() == 1 + def _is_closed(self): + """Check wheter the shutter is open or closed given the photon and gamma status PVs.""" + return (self.photon_status.get() == 1) and ( + self.gamma_status.get() == 1 ) # NOTE: if one of the status is equal to zero, the shutter can be partially open + + def done_comparator(self, value, **kwargs): + is_closed = self._is_closed() return is_closed if self.setpoint == 0 else not is_closed From b227be87745f457b4324a431079456805feb1ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Fri, 24 Apr 2026 12:48:55 -0300 Subject: [PATCH 16/17] chore: fix typo in docstring. --- src/sophys/common/devices/shutters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 4faadb33..508817e0 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -110,7 +110,7 @@ class ShutterOpenClose(Device): NOTES ----- This implemantation considers that the value of the `readback` signal is 0 for an open shutter and 1 for a closed shutter. - This is not so intuitive, so the `set` method considers that 1 is for openig and 0 for closing the shutter. + This is not so intuitive, so the `set` method considers that 1 is for opening and 0 for closing the shutter. The `return` of the `set` method is an `AndStatus` with both `readback` signals. From 9f659d801ce3c195464c504fa7f8bbad3cceb5af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Luis=20Biondo=20Neto?= Date: Mon, 27 Apr 2026 13:58:21 -0300 Subject: [PATCH 17/17] refactor(shutters): make the permission check for the `set` method simpler. The names of the attributes related to the permission PV are changed to make more sense with their functions. The check is made simpler by verifying the existence of the attribute `permission_signal` and the `try ... except` wrap was removed since it was unnecessary. --- src/sophys/common/devices/shutters.py | 67 +++++++++++++-------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/src/sophys/common/devices/shutters.py b/src/sophys/common/devices/shutters.py index 508817e0..a77586c2 100644 --- a/src/sophys/common/devices/shutters.py +++ b/src/sophys/common/devices/shutters.py @@ -53,26 +53,26 @@ def __init__( ): self.setpoint_suffix = setpoint_suffix self.readback_suffix = readback_suffix - self.permission_pv = permission_pv + self._permission_pv_name = permission_pv super().__init__(*args, **kwargs) - if self.permission_pv is not None: - self.permission = EpicsSignalRO(f"{self.permission_pv}", name="permission") - self.permission_flag = True - else: - self.permission_flag = False + if self._permission_pv_name is not None: + self.permission_signal = EpicsSignalRO( + f"{self._permission_pv_name}", name="permission" + ) def set(self, value, *args, **kwargs): - if self.permission_flag: - try: - if not self.permission.get(connection_timeout=2, **kwargs): - raise PremadeStatus( - success=False, - exception=PermissionError( - f"Shutter open permission is denied: {self.permission.pvname} {self.permission.get()}" - ), - ) - except TimeoutError: - raise + if hasattr(self, "permission_signal"): + permission_status = self.permission_signal.get( + connection_timeout=2, **kwargs + ) + if not permission_status: + raise PremadeStatus( + success=False, + exception=PermissionError( + f"Shutter open permission is denied: {self.permission_signal.pvname} {permission_status}." + ), + ) + if ( value == self.readback.get() ): # Since we're swapping the readback values (0 for closing and 1 for opening), we actuate when value == readback @@ -150,26 +150,25 @@ def __init__( self.shutter_suffix = shutter_suffix self.ps_suffix = ps_suffix self.gs_suffix = gs_suffix - self.permission_pv = permission_pv + self._permission_pv_name = permission_pv super().__init__(*args, **kwargs) - if self.permission_pv is not None: - self.permission = EpicsSignalRO(f"{self.permission_pv}", name="permission") - self.permission_flag = True - else: - self.permission_flag = False + if self._permission_pv_name is not None: + self.permission_signal = EpicsSignalRO( + f"{self._permission_pv_name}", name="permission" + ) def set(self, value, *args, **kwargs): - if self.permission_flag: - try: - if not self.permission.get(connection_timeout=2, **kwargs): - raise PremadeStatus( - success=False, - exception=PermissionError( - f"Shutter open permission is denied: {self.permission.pvname} {self.permission.get()}" - ), - ) - except TimeoutError: - raise + if hasattr(self, "permission_signal"): + permission_status = self.permission_signal.get( + connection_timeout=2, **kwargs + ) + if not permission_status: + raise PremadeStatus( + success=False, + exception=PermissionError( + f"Shutter open permission is denied: {self.permission_signal.pvname} {permission_status}" + ), + ) if value == 0 and not self._is_closed(): self.close.set(1, *args, **kwargs).wait()