@@ -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