Skip to content

Commit e4914d5

Browse files
authored
Cleave hutch shutter into two classes ( Resolves #1965 ) (#1971)
* Cleave hutch shutter into two classes ( Resolve#1965 ) * Hutch shutter with interlock now becomes InterlockedHutchShutter * Hutch shutter falling back on the :ILKSTA default interlock does not specify the interlock nor use it internally * Base class created to abstract out the elements common to both types of HutchShutter * Exist steps within set method defragmented across parent class and child classes - keeping the bare minimal differences in the children * Some inevitable method renaming has resulted from repurposing the code for the above reasons / purposes * Apply Additional fixes to hutch shutter coupled elements * Further tweaks needed to satisfy type checks and to correct mistaken use of ShutterDemand.CLOSE where ShutterState.CLOSED was required * Correct snafus in super class constructor call chain * Calls from child implementations of the base class for shutter were passing in name before prefix instead of the other way around * Additional changes requested in code review
1 parent ad1213b commit e4914d5

5 files changed

Lines changed: 293 additions & 89 deletions

File tree

src/dodal/beamlines/i03.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from dodal.devices.fluorescence_detector_motion import FluorescenceDetector
3737
from dodal.devices.flux import Flux
3838
from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVoltages
39-
from dodal.devices.hutch_shutter import HutchInterlock, HutchShutter
39+
from dodal.devices.hutch_shutter import HutchInterlock, InterlockedHutchShutter
4040
from dodal.devices.ipin import IPin
4141
from dodal.devices.motors import XYZStage
4242
from dodal.devices.oav.oav_detector import OAVBeamCentreFile
@@ -269,8 +269,10 @@ def sample_shutter() -> ZebraShutter:
269269

270270

271271
@devices.factory()
272-
def hutch_shutter() -> HutchShutter:
273-
return HutchShutter(PREFIX.beamline_prefix, HutchInterlock(PREFIX.beamline_prefix))
272+
def hutch_shutter() -> InterlockedHutchShutter:
273+
return InterlockedHutchShutter(
274+
PREFIX.beamline_prefix, HutchInterlock(PREFIX.beamline_prefix)
275+
)
274276

275277

276278
@devices.factory()

src/dodal/beamlines/i19_optics.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
HutchAccessControl,
88
)
99
from dodal.devices.focusing_mirror import FocusingMirrorWithPiezo
10-
from dodal.devices.hutch_shutter import HutchInterlock, HutchShutter
10+
from dodal.devices.hutch_shutter import HutchInterlock, InterlockedHutchShutter
1111
from dodal.log import set_beamline as set_log_beamline
1212
from dodal.utils import BeamlinePrefix
1313

@@ -20,9 +20,11 @@
2020

2121

2222
@devices.factory()
23-
def shutter() -> HutchShutter:
23+
def shutter() -> InterlockedHutchShutter:
2424
"""Real experiment shutter device for I19."""
25-
return HutchShutter(PREFIX.beamline_prefix, HutchInterlock(PREFIX.beamline_prefix))
25+
return InterlockedHutchShutter(
26+
PREFIX.beamline_prefix, HutchInterlock(PREFIX.beamline_prefix)
27+
)
2628

2729

2830
@devices.factory()

src/dodal/beamlines/i24.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from dodal.devices.beamlines.i24.focus_mirrors import FocusMirrorsMode
2222
from dodal.devices.beamlines.i24.pmac import PMAC
2323
from dodal.devices.beamlines.i24.vgonio import VerticalGoniometer
24-
from dodal.devices.hutch_shutter import HutchInterlock, HutchShutter
24+
from dodal.devices.hutch_shutter import HutchInterlock, InterlockedHutchShutter
2525
from dodal.devices.motors import YZStage
2626
from dodal.devices.oav.oav_detector import OAVBeamCentreFile
2727
from dodal.devices.oav.oav_parameters import OAVConfigBeamCentre
@@ -129,8 +129,10 @@ def zebra() -> Zebra:
129129

130130

131131
@devices.factory()
132-
def shutter() -> HutchShutter:
133-
return HutchShutter(PREFIX.beamline_prefix, HutchInterlock(PREFIX.beamline_prefix))
132+
def shutter() -> InterlockedHutchShutter:
133+
return InterlockedHutchShutter(
134+
PREFIX.beamline_prefix, HutchInterlock(PREFIX.beamline_prefix)
135+
)
134136

135137

136138
@devices.factory()

src/dodal/devices/hutch_shutter.py

Lines changed: 96 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -95,61 +95,120 @@ async def shutter_safe_to_operate(self) -> bool:
9595
return isclose(float(interlock_state), HUTCH_SAFE_FOR_OPERATIONS, abs_tol=5e-2)
9696

9797

98-
class HutchShutter(StandardReadable, Movable[ShutterDemand]):
98+
class BaseHutchShutter(ABC, StandardReadable, Movable[ShutterDemand]):
9999
"""Device to operate the hutch shutter.
100100
101-
When a demand is sent and interlock given, the device should first check the hutch
102-
status and raise an error if it's not interlocked (searched and locked), meaning
103-
it's not safe to operate the shutter. When no interlock specified, the shutter
104-
can be operated without checking the hutch status relying on default shutter
105-
interlock (:ILKSTA).
101+
Base class for HutchShutters - extended by some that do not use an interlock
102+
and by others that do. See child classes for details
103+
104+
Attributes:
105+
control: An writeable EPICS signal to drive the shutter state changes
106+
status: A readable EPICS signal to read the present shutter state
107+
"""
108+
109+
def __init__(
110+
self,
111+
bl_shutter_prefix: str,
112+
name: str = "",
113+
) -> None:
114+
self.control = epics_signal_w(ShutterDemand, f"{bl_shutter_prefix}:CON")
115+
with self.add_children_as_readables():
116+
self.status = epics_signal_r(ShutterState, f"{bl_shutter_prefix}:STA")
117+
super().__init__(name)
118+
119+
@AsyncStatus.wrap
120+
async def set(self, value: ShutterDemand):
121+
if TEST_MODE:
122+
self._test_mode_set()
123+
else:
124+
if value == ShutterDemand.OPEN:
125+
await self._pre_open_shutter_actions()
126+
required_match: ShutterState = ShutterState.OPEN
127+
else:
128+
required_match: ShutterState = ShutterState.CLOSED
129+
await self._shutter_action(value=value, required_match=required_match)
130+
131+
@abstractmethod
132+
async def _pre_open_shutter_actions(self):
133+
"""Provides internal implementation of pre-requisite steps that support opening of the shutter."""
134+
135+
async def _shutter_action(self, value: ShutterDemand, required_match: ShutterState):
136+
await self.control.set(value)
137+
await wait_for_value(self.status, match=required_match, timeout=DEFAULT_TIMEOUT)
138+
139+
def _test_mode_set(self):
140+
LOGGER.warning("Running in test mode, will not operate the experiment shutter.")
141+
142+
143+
class HutchShutter(BaseHutchShutter):
144+
"""Device to operate the hutch shutter.
145+
146+
When a demand is sent, the shutter can be operated without checking
147+
the hutch status, instead relying on default shutter interlock (:ILKSTA).
148+
149+
If the requested shutter position is "Open", the shutter control PV should first
150+
go to "Reset" and then move to "Open". This is because before opening the hutch
151+
shutter, the interlock status will show as `failed` until the hutch shutter is
152+
reset. The reset will set the interlock status to `OK`, allowing for shutter operations.
153+
Until this step is done, the hutch shutter can't be opened. The reset is not needed
154+
for closing the shutter.
155+
"""
156+
157+
def __init__(
158+
self,
159+
bl_prefix: str,
160+
shtr_infix: str = EXP_SHUTTER_1_INFIX,
161+
name: str = "",
162+
) -> None:
163+
super().__init__(f"{bl_prefix}{shtr_infix}", name)
164+
165+
async def _pre_open_shutter_actions(self):
166+
"""Required by parent class API - resets the shutter prior to opening."""
167+
await self.control.set(ShutterDemand.RESET)
168+
169+
170+
class InterlockedHutchShutter(BaseHutchShutter):
171+
"""Device to operate the hutch shutter. With an interlock.
172+
173+
When a demand is sent the device should first check the hutch status and
174+
raise an error if it's not interlocked (searched and locked), as not interlocked
175+
means it's not safe to operate the shutter.
106176
107177
If the requested shutter position is "Open", the shutter control PV should first
108178
go to "Reset" and then move to "Open". This is because before opening the hutch
109179
shutter, the interlock status will show as `failed` until the hutch shutter is
110-
reset. This will set the interlock status to `OK`, allowing for shutter operations.
180+
reset. The reset will set the interlock status to `OK`, allowing for shutter operations.
111181
Until this step is done, the hutch shutter can't be opened. The reset is not needed
112182
for closing the shutter.
183+
184+
Attributes:
185+
interlock : Hutch PSS based interlock status checker
113186
"""
114187

115188
def __init__(
116189
self,
117190
bl_prefix: str,
118-
interlock: BaseHutchInterlock | None = None,
191+
interlock: BaseHutchInterlock,
119192
shtr_infix: str = EXP_SHUTTER_1_INFIX,
120193
name: str = "",
121194
) -> None:
122-
self.control = epics_signal_w(ShutterDemand, f"{bl_prefix}{shtr_infix}:CON")
123195
with self.add_children_as_readables():
124-
self.status = epics_signal_r(ShutterState, f"{bl_prefix}{shtr_infix}:STA")
125196
self.interlock = interlock
126-
super().__init__(name)
197+
super().__init__(f"{bl_prefix}{shtr_infix}", name)
127198

128-
@AsyncStatus.wrap
129-
async def set(self, value: ShutterDemand):
130-
if not TEST_MODE:
131-
if value == ShutterDemand.OPEN:
132-
await self._check_interlock()
133-
await self.control.set(ShutterDemand.RESET)
134-
await self.control.set(value)
135-
return await wait_for_value(
136-
self.status, match=ShutterState.OPEN, timeout=DEFAULT_TIMEOUT
137-
)
138-
else:
139-
await self.control.set(value)
140-
return await wait_for_value(
141-
self.status, match=ShutterState.CLOSED, timeout=DEFAULT_TIMEOUT
142-
)
143-
else:
144-
LOGGER.warning(
145-
"Running in test mode, will not operate the experiment shutter."
146-
)
199+
async def _pre_open_shutter_actions(self):
200+
"""Required by parent class API - checks interlock, then resets the shutter prior to opening."""
201+
await self._check_interlock()
202+
await self.control.set(ShutterDemand.RESET)
147203

148204
async def _check_interlock(self):
149-
if self.interlock is not None:
150-
interlock_state = await self.interlock.shutter_safe_to_operate()
151-
if not interlock_state:
152-
# If not in test mode, fail. If in test mode, the optics hutch may be open.
153-
raise ShutterNotSafeToOperateError(
154-
"The hutch has not been locked, not operating shutter."
155-
)
205+
"""Disrupts shutter opening if the interlock is not in a safe to operate state.
206+
207+
Raises:
208+
ShutterNotSafeToOperateError - whereby an unhappy interlock will veto any attempt to open the shutter.
209+
"""
210+
interlock_state = await self.interlock.shutter_safe_to_operate()
211+
if not interlock_state:
212+
raise ShutterNotSafeToOperateError(
213+
"The hutch has not been locked, not operating shutter."
214+
)

0 commit comments

Comments
 (0)