Skip to content

Commit b481ffa

Browse files
Implement a new access controlled shutter for i19 (#1135)
* A start * Start adding REST call * Small note on order of things * Fill in some blanks * Run pre-commits * This should be working * Run pre-commits * Tidy up, add docstring and instantiate in beamlines * Make pre commits happy * Remove old shutter and tests * Forgot an async * Add read method and test * Maybe add the file * Fix dumb mistake * Run pre-commits, again * Maybe it can be simplified * Add a test for exception * Make ruff happy * Add comment * Comment out failing test * Add test * Add a couple of tests * Remove tests for old shutter and rename file * Update commet * Update device to use new wrapper * Remove hutch invalid option as logic now moved to i19-bluesky * Get dodal connect to work for i19-1 and link issue * Fix docstrings * Update src/dodal/devices/i19/shutter.py Co-authored-by: Dominic Oram <dominic.oram@diamond.ac.uk> * Remove read * Reword comment * Pull device name for hutch control into constant * Change log level --------- Co-authored-by: Dominic Oram <dominic.oram@diamond.ac.uk>
1 parent 50a69ae commit b481ffa

File tree

7 files changed

+214
-193
lines changed

7 files changed

+214
-193
lines changed

src/dodal/beamlines/i19_1.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
set_beamline as set_utils_beamline,
66
)
77
from dodal.devices.i19.beamstop import BeamStop
8-
from dodal.devices.i19.shutter import HutchConditionalShutter, HutchState
8+
from dodal.devices.i19.shutter import AccessControlledShutter, HutchState
99
from dodal.devices.oav.oav_detector import OAV
1010
from dodal.devices.oav.oav_parameters import OAVConfig
1111
from dodal.devices.synchrotron import Synchrotron
@@ -67,11 +67,11 @@ def zebra() -> Zebra:
6767

6868

6969
@device_factory()
70-
def shutter() -> HutchConditionalShutter:
71-
"""Get the i19-2 hutch shutter device, instantiate it if it hasn't already been.
70+
def shutter() -> AccessControlledShutter:
71+
"""Get the i19-1 hutch shutter device, instantiate it if it hasn't already been.
7272
If this is called when already instantiated, it will return the existing object.
7373
"""
74-
return HutchConditionalShutter(
74+
return AccessControlledShutter(
7575
prefix=f"{PREFIX.beamline_prefix}-PS-SHTR-01:",
7676
hutch=HutchState.EH1,
7777
)

src/dodal/beamlines/i19_2.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
set_beamline as set_utils_beamline,
66
)
77
from dodal.devices.i19.beamstop import BeamStop
8-
from dodal.devices.i19.shutter import HutchConditionalShutter, HutchState
8+
from dodal.devices.i19.shutter import AccessControlledShutter, HutchState
99
from dodal.devices.synchrotron import Synchrotron
1010
from dodal.devices.zebra.zebra import Zebra
1111
from dodal.devices.zebra.zebra_constants_mapping import (
@@ -52,11 +52,11 @@ def zebra() -> Zebra:
5252

5353

5454
@device_factory()
55-
def shutter() -> HutchConditionalShutter:
55+
def shutter() -> AccessControlledShutter:
5656
"""Get the i19-2 hutch shutter device, instantiate it if it hasn't already been.
5757
If this is called when already instantiated, it will return the existing object.
5858
"""
59-
return HutchConditionalShutter(
59+
return AccessControlledShutter(
6060
prefix=f"{PREFIX.beamline_prefix}-PS-SHTR-01:",
6161
hutch=HutchState.EH2,
6262
)

src/dodal/beamlines/i19_optics.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
set_beamline as set_utils_beamline,
66
)
77
from dodal.devices.hutch_shutter import HutchShutter
8-
from dodal.devices.i19.hutch_access import HutchAccessControl
8+
from dodal.devices.i19.hutch_access import ACCESS_DEVICE_NAME, HutchAccessControl
99
from dodal.log import set_beamline as set_log_beamline
1010
from dodal.utils import BeamlinePrefix
1111

@@ -31,4 +31,6 @@ def access_control() -> HutchAccessControl:
3131
"""Get a device that checks the active hutch for i19, instantiate it if it hasn't already been.
3232
If this is called when already instantiated, it will return the existing object.
3333
"""
34-
return HutchAccessControl(f"{PREFIX.beamline_prefix}-OP-STAT-01:", "access_control")
34+
return HutchAccessControl(
35+
f"{PREFIX.beamline_prefix}-OP-STAT-01:", ACCESS_DEVICE_NAME
36+
)

src/dodal/devices/i19/hutch_access.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from ophyd_async.core import StandardReadable
22
from ophyd_async.epics.core import epics_signal_r
33

4+
ACCESS_DEVICE_NAME = "access_control" # Device name in i19-blueapi
5+
46

57
class HutchAccessControl(StandardReadable):
68
def __init__(self, prefix: str, name: str = "") -> None:

src/dodal/devices/i19/shutter.py

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,79 @@
11
from enum import Enum
22

3+
from aiohttp import ClientSession
34
from bluesky.protocols import Movable
4-
from ophyd_async.core import AsyncStatus, StandardReadable
5+
from ophyd_async.core import AsyncStatus, StandardReadable, StandardReadableFormat
56
from ophyd_async.epics.core import epics_signal_r
67

7-
from dodal.devices.hutch_shutter import HutchShutter, ShutterDemand
8+
from dodal.devices.hutch_shutter import ShutterDemand, ShutterState
9+
from dodal.devices.i19.hutch_access import ACCESS_DEVICE_NAME
810
from dodal.log import LOGGER
911

10-
11-
class HutchInvalidError(Exception):
12-
pass
12+
OPTICS_BLUEAPI_URL = "https://i19-blueapi.diamond.ac.uk"
1313

1414

1515
class HutchState(str, Enum):
1616
EH1 = "EH1"
1717
EH2 = "EH2"
18-
INVALID = "INVALID"
1918

2019

21-
class HutchConditionalShutter(StandardReadable, Movable[ShutterDemand]):
20+
class AccessControlledShutter(StandardReadable, Movable[ShutterDemand]):
2221
""" I19-specific device to operate the hutch shutter.
2322
24-
This device evaluates the hutch state value to work out which of the two I19 \
25-
hutches is in use and then implements the HutchShutter device to operate the \
26-
experimental shutter.
23+
This device will send a REST call to the blueapi instance controlling the optics \
24+
hutch running on the I19 cluster, which will evaluate the current hutch in use vs \
25+
the hutch sending the request and decide if the plan will be run or not.
2726
As the two hutches are located in series, checking the hutch in use is necessary to \
2827
avoid accidentally operating the shutter from one hutch while the other has beamtime.
2928
30-
The hutch name should be passed to the device upon instantiation. If this does not \
31-
coincide with the current hutch in use, a warning will be logged and the shutter \
32-
will not be operated. This is to allow for testing of plans.
33-
An error will instead be raised if the hutch state reads as "INVALID".
29+
The name of the hutch that wants to operate the shutter should be passed to the \
30+
device upon instantiation.
31+
32+
For details see the architecture described in \
33+
https://github.com/DiamondLightSource/i19-bluesky/issues/30.
3434
"""
3535

3636
def __init__(self, prefix: str, hutch: HutchState, name: str = "") -> None:
37-
self.shutter = HutchShutter(prefix=prefix, name=name)
38-
bl_prefix = prefix.split("-")[0]
39-
self.hutch_state = epics_signal_r(str, f"{bl_prefix}-OP-STAT-01:EHStatus.VALA")
40-
if hutch == HutchState.INVALID:
41-
raise HutchInvalidError(
42-
"Cannot define experimental shutter for invalid hutch"
43-
)
37+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
38+
self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
4439
self.hutch_request = hutch
40+
self.url = OPTICS_BLUEAPI_URL
4541
super().__init__(name)
4642

4743
@AsyncStatus.wrap
4844
async def set(self, value: ShutterDemand):
49-
hutch_in_use = await self.hutch_state.get_value()
50-
LOGGER.info(f"Current hutch in use: {hutch_in_use}")
51-
if hutch_in_use == HutchState.INVALID:
52-
raise HutchInvalidError(
53-
"The hutch state is invalid. Contact the beamline staff."
54-
)
55-
if hutch_in_use != self.hutch_request:
56-
# NOTE Warn but don't fail
57-
LOGGER.warning(
58-
f"{self.hutch_request} is not the hutch in use. Shutter will not be operated."
59-
)
60-
else:
61-
await self.shutter.set(value)
45+
REQUEST_PARAMS = {
46+
"name": "operate_shutter_plan",
47+
"params": {
48+
"experiment_hutch": self.hutch_request.value,
49+
"access_device": ACCESS_DEVICE_NAME,
50+
"shutter_demand": value,
51+
},
52+
}
53+
async with ClientSession(base_url=self.url, raise_for_status=True) as session:
54+
# First submit the plan to the worker
55+
async with session.post("/tasks", data=REQUEST_PARAMS) as response:
56+
LOGGER.debug(
57+
f"Task submitted to the worker, response status: {response.status}"
58+
)
59+
60+
try:
61+
data = await response.json()
62+
task_id = data["task_id"]
63+
except Exception as e:
64+
LOGGER.error(
65+
f"Failed to get task_id from {self.url}/tasks POST. ({e})"
66+
)
67+
raise
68+
# Then set the task as active and run asap
69+
async with session.put(
70+
"/worker/tasks", data={"task_id": task_id}
71+
) as response:
72+
if not response.ok:
73+
LOGGER.error(
74+
f"""Unable to operate the shutter.
75+
Session PUT responded with {response.status}: {response.reason}.
76+
"""
77+
)
78+
return
79+
LOGGER.debug(f"Run operate shutter plan, task_id: {task_id}")

tests/devices/i19/test_hutch_conditional_shutter.py

Lines changed: 0 additions & 150 deletions
This file was deleted.

0 commit comments

Comments
 (0)