dls-dodal 1.44.0__py3-none-any.whl → 1.46.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/RECORD +56 -46
  3. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +2 -0
  6. dodal/beamlines/b07.py +27 -0
  7. dodal/beamlines/b07_1.py +25 -0
  8. dodal/beamlines/i03.py +4 -4
  9. dodal/beamlines/i04.py +1 -1
  10. dodal/beamlines/i09.py +25 -0
  11. dodal/beamlines/i09_1.py +25 -0
  12. dodal/beamlines/i10.py +19 -35
  13. dodal/beamlines/i18.py +7 -4
  14. dodal/beamlines/i19_1.py +2 -1
  15. dodal/beamlines/i19_2.py +2 -1
  16. dodal/beamlines/i20_1.py +2 -1
  17. dodal/beamlines/i22.py +3 -3
  18. dodal/beamlines/i23.py +67 -2
  19. dodal/beamlines/p38.py +3 -3
  20. dodal/beamlines/p60.py +21 -0
  21. dodal/common/beamlines/beamline_utils.py +5 -0
  22. dodal/common/visit.py +1 -41
  23. dodal/devices/common_dcm.py +77 -0
  24. dodal/devices/detector/det_dist_to_beam_converter.py +16 -23
  25. dodal/devices/detector/detector.py +2 -1
  26. dodal/devices/electron_analyser/abstract_analyser_io.py +47 -0
  27. dodal/devices/electron_analyser/abstract_region.py +112 -0
  28. dodal/devices/electron_analyser/specs_analyser_io.py +19 -0
  29. dodal/devices/electron_analyser/specs_region.py +26 -0
  30. dodal/devices/electron_analyser/vgscienta_analyser_io.py +26 -0
  31. dodal/devices/electron_analyser/vgscienta_region.py +90 -0
  32. dodal/devices/{dcm.py → i03/dcm.py} +8 -12
  33. dodal/devices/{undulator_dcm.py → i03/undulator_dcm.py} +6 -4
  34. dodal/devices/i10/diagnostics.py +239 -0
  35. dodal/devices/i10/slits.py +93 -6
  36. dodal/devices/i13_1/merlin.py +3 -4
  37. dodal/devices/i13_1/merlin_controller.py +1 -1
  38. dodal/devices/i19/blueapi_device.py +102 -0
  39. dodal/devices/i19/shutter.py +5 -43
  40. dodal/devices/i22/dcm.py +10 -12
  41. dodal/devices/i24/dcm.py +8 -17
  42. dodal/devices/motors.py +21 -0
  43. dodal/devices/tetramm.py +3 -4
  44. dodal/devices/turbo_slit.py +10 -4
  45. dodal/devices/undulator.py +9 -7
  46. dodal/devices/util/adjuster_plans.py +1 -2
  47. dodal/devices/util/lookup_tables.py +38 -0
  48. dodal/devices/util/test_utils.py +1 -0
  49. dodal/devices/zebra/zebra.py +4 -0
  50. dodal/plan_stubs/data_session.py +10 -1
  51. dodal/plan_stubs/electron_analyser/configure_controller.py +80 -0
  52. dodal/plans/verify_undulator_gap.py +2 -2
  53. dodal/devices/electron_analyser/base_region.py +0 -64
  54. dodal/devices/electron_analyser/specs/specs_region.py +0 -24
  55. dodal/devices/electron_analyser/vgscienta/__init__.py +0 -0
  56. dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +0 -77
  57. dodal/devices/util/motor_utils.py +0 -6
  58. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/entry_points.txt +0 -0
  59. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/licenses/LICENSE +0 -0
  60. {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/top_level.txt +0 -0
  61. /dodal/{devices/electron_analyser/specs → plan_stubs/electron_analyser}/__init__.py +0 -0
@@ -1,5 +1,5 @@
1
1
  import numpy as np
2
- from ophyd_async.core import Array1D, StandardReadable, soft_signal_r_and_setter
2
+ from ophyd_async.core import Array1D, soft_signal_r_and_setter
3
3
  from ophyd_async.epics.core import epics_signal_r
4
4
  from ophyd_async.epics.motor import Motor
5
5
 
@@ -8,9 +8,14 @@ from dodal.common.crystal_metadata import (
8
8
  MaterialsEnum,
9
9
  make_crystal_metadata_from_material,
10
10
  )
11
+ from dodal.devices.common_dcm import (
12
+ BaseDCM,
13
+ PitchAndRollCrystal,
14
+ StationaryCrystal,
15
+ )
11
16
 
12
17
 
13
- class DCM(StandardReadable):
18
+ class DCM(BaseDCM[PitchAndRollCrystal, StationaryCrystal]):
14
19
  """
15
20
  A double crystal monochromator (DCM), used to select the energy of the beam.
16
21
 
@@ -30,13 +35,7 @@ class DCM(StandardReadable):
30
35
  MaterialsEnum.Si, (1, 1, 1)
31
36
  )
32
37
  with self.add_children_as_readables():
33
- self.bragg_in_degrees = Motor(prefix + "BRAGG")
34
- self.roll_in_mrad = Motor(prefix + "ROLL")
35
- self.offset_in_mm = Motor(prefix + "OFFSET")
36
38
  self.perp_in_mm = Motor(prefix + "PERP")
37
- self.energy_in_kev = Motor(prefix + "ENERGY")
38
- self.pitch_in_mrad = Motor(prefix + "PITCH")
39
- self.wavelength = Motor(prefix + "WAVELENGTH")
40
39
 
41
40
  # temperatures
42
41
  self.xtal1_temp = epics_signal_r(float, prefix + "TEMP1")
@@ -58,7 +57,4 @@ class DCM(StandardReadable):
58
57
  Array1D[np.uint64],
59
58
  initial_value=reflection_array,
60
59
  )
61
- self.crystal_metadata_d_spacing = epics_signal_r(
62
- float, prefix + "DSPACING:RBV"
63
- )
64
- super().__init__(name)
60
+ super().__init__(prefix, PitchAndRollCrystal, StationaryCrystal, name)
@@ -4,10 +4,9 @@ from bluesky.protocols import Movable
4
4
  from ophyd_async.core import AsyncStatus, Reference, StandardReadable
5
5
 
6
6
  from dodal.common.beamlines.beamline_parameters import get_beamline_parameters
7
-
8
- from ..log import LOGGER
9
- from .dcm import DCM
10
- from .undulator import Undulator
7
+ from dodal.devices.i03.dcm import DCM
8
+ from dodal.devices.undulator import Undulator
9
+ from dodal.log import LOGGER
11
10
 
12
11
  ENERGY_TIMEOUT_S: float = 30.0
13
12
 
@@ -23,6 +22,9 @@ class UndulatorDCM(StandardReadable, Movable[float]):
23
22
  Calling unulator_dcm.set(energy) will move the DCM motor, perform a table lookup
24
23
  and move the Undulator gap motor if needed. So the set method can be thought of as
25
24
  a comprehensive way to set beam energy.
25
+
26
+ This class will be removed in the future. Use the separate Undulator and DCM devices
27
+ instead. See https://github.com/DiamondLightSource/dodal/issues/1092
26
28
  """
27
29
 
28
30
  def __init__(
@@ -0,0 +1,239 @@
1
+ from bluesky.protocols import Movable
2
+ from ophyd_async.core import (
3
+ AsyncStatus,
4
+ Device,
5
+ StandardReadable,
6
+ StrictEnum,
7
+ )
8
+ from ophyd_async.core import StandardReadableFormat as Format
9
+ from ophyd_async.core._device import DeviceConnector
10
+ from ophyd_async.epics.adaravis import AravisDriverIO
11
+ from ophyd_async.epics.adcore import SingleTriggerDetector
12
+ from ophyd_async.epics.core import (
13
+ epics_signal_r,
14
+ epics_signal_rw,
15
+ )
16
+ from ophyd_async.epics.motor import Motor
17
+
18
+ from dodal.devices.current_amplifiers import (
19
+ CurrentAmpDet,
20
+ Femto3xxGainTable,
21
+ Femto3xxGainToCurrentTable,
22
+ Femto3xxRaiseTime,
23
+ FemtoDDPCA,
24
+ StruckScaler,
25
+ )
26
+
27
+
28
+ class D3Position(StrictEnum):
29
+ NOTHING = "Nothing"
30
+ GRID = "Grid"
31
+
32
+
33
+ class D5Position(StrictEnum):
34
+ CELL_IN = "Cell In"
35
+ CELL_OUT = "Cell Out"
36
+
37
+
38
+ class D5APosition(StrictEnum):
39
+ OUT_OF_THE_BEAM = "Out of the beam"
40
+ DIODE = "Diode"
41
+ BLADE = "Blade"
42
+ LA = "La ref"
43
+ GD = "Gd ref"
44
+ YB = "Yb ref"
45
+ GRID = "Grid"
46
+
47
+
48
+ class D6Position(StrictEnum):
49
+ DIODE_OUT = "Diode Out"
50
+ DIODE_IN = "Diode In"
51
+ AU_MESH = "Au Mesh"
52
+
53
+
54
+ class D7Position(StrictEnum):
55
+ OUT = "Out"
56
+ SHUTTER = "Shutter"
57
+
58
+
59
+ class InOutTable(StrictEnum):
60
+ MOVE_IN = "Move In"
61
+ MOVE_OUT = "Move Out"
62
+ RESET = "Reset"
63
+
64
+
65
+ class InOutReadBackTable(StrictEnum):
66
+ MOVE_IN = "Moving In"
67
+ MOVE_OUT = "Moving Out"
68
+ IN_BEAM = "In Beam"
69
+ FAULT = "Fault"
70
+ OUT_OF_BEAM = "Out of Beam"
71
+
72
+
73
+ class Positioner(StandardReadable, Movable):
74
+ """1D stage with a enum table to select positions."""
75
+
76
+ def __init__(
77
+ self,
78
+ prefix: str,
79
+ positioner_enum: type[StrictEnum],
80
+ positioner_suffix: str = "",
81
+ Positioner_pv_suffix: str = ":MP:SELECT",
82
+ name: str = "",
83
+ ) -> None:
84
+ self._stage_motion = Motor(prefix=prefix + positioner_suffix)
85
+ with self.add_children_as_readables(Format.CONFIG_SIGNAL):
86
+ self.stage_position = epics_signal_rw(
87
+ positioner_enum,
88
+ read_pv=prefix + positioner_suffix + Positioner_pv_suffix,
89
+ )
90
+ super().__init__(name=name)
91
+ self.positioner_enum = positioner_enum
92
+
93
+ @AsyncStatus.wrap
94
+ async def set(self, value: StrictEnum) -> None:
95
+ if value in self.positioner_enum:
96
+ await self.stage_position.set(value=value)
97
+ else:
98
+ raise ValueError(
99
+ f"{value} is not an allow position. Position must be: {self.positioner_enum}"
100
+ )
101
+
102
+
103
+ class I10PneumaticStage(StandardReadable):
104
+ """Pneumatic stage only has two real positions in or out.
105
+ Use for fluorescent screen which can be insert into the x-ray beam.
106
+ Most often use in conjunction with a webcam to locate the x-ray beam."""
107
+
108
+ def __init__(
109
+ self,
110
+ prefix: str,
111
+ name: str = "",
112
+ ) -> None:
113
+ with self.add_children_as_readables(Format.HINTED_SIGNAL):
114
+ self.stage_position_set = epics_signal_rw(
115
+ InOutTable,
116
+ read_pv=prefix + "CON",
117
+ )
118
+ self.stage_position_readback = epics_signal_r(
119
+ InOutReadBackTable,
120
+ read_pv=prefix + "STA",
121
+ )
122
+ super().__init__(name=name)
123
+
124
+
125
+ class ScreenCam(Device):
126
+ """Compound device of pneumatic stage(fluorescent screen) and webcam"""
127
+
128
+ def __init__(
129
+ self,
130
+ prefix: str,
131
+ cam_infix="DCAM:",
132
+ name: str = "",
133
+ ) -> None:
134
+ self.screen_stage = I10PneumaticStage(
135
+ prefix=prefix,
136
+ )
137
+ cam_pv = prefix + cam_infix
138
+ self.centroid_x = epics_signal_r(float, read_pv=f"{cam_pv}STAT:CentroidX_RBV")
139
+ self.centroid_y = epics_signal_r(float, read_pv=f"{cam_pv}STAT:CentroidY_RBV")
140
+ self.single_trigger_centroid = SingleTriggerDetector(
141
+ drv=AravisDriverIO(prefix=cam_pv + "CAM:"),
142
+ read_uncached=[
143
+ self.centroid_x,
144
+ self.centroid_y,
145
+ ],
146
+ )
147
+ super().__init__(name=name)
148
+
149
+
150
+ class FullDiagnostic(Device):
151
+ """Compound device of a diagnostic with screen, webcam and Positioner stage."""
152
+
153
+ def __init__(
154
+ self,
155
+ prefix: str,
156
+ positioner_enum: type[StrictEnum],
157
+ positioner_suffix: str = "",
158
+ Positioner_pv_suffix: str = ":MP:SELECT",
159
+ cam_infix: str = "DCAM:",
160
+ name: str = "",
161
+ ) -> None:
162
+ self.positioner = Positioner(
163
+ prefix=prefix,
164
+ positioner_enum=positioner_enum,
165
+ positioner_suffix=positioner_suffix,
166
+ Positioner_pv_suffix=Positioner_pv_suffix,
167
+ )
168
+ self.screen = ScreenCam(
169
+ prefix,
170
+ cam_infix,
171
+ name,
172
+ )
173
+ super().__init__(name)
174
+
175
+
176
+ class I10Diagnostic(Device):
177
+ """Collection of all the diagnostic stage on i10."""
178
+
179
+ def __init__(self, prefix, name: str = "") -> None:
180
+ self.d1 = ScreenCam(prefix=prefix + "PHDGN-01:")
181
+ self.d2 = ScreenCam(prefix=prefix + "PHDGN-02:")
182
+ self.d3 = FullDiagnostic(
183
+ prefix=prefix + "PHDGN-03:",
184
+ positioner_enum=D3Position,
185
+ positioner_suffix="DET:X",
186
+ )
187
+ self.d4 = ScreenCam(prefix=prefix + "PHDGN-04:")
188
+ self.d5 = Positioner(
189
+ prefix=prefix + "IONC-01:",
190
+ positioner_enum=D5Position,
191
+ positioner_suffix="Y",
192
+ )
193
+
194
+ self.d5A = Positioner(
195
+ prefix=prefix + "PHDGN-06:",
196
+ positioner_enum=D5APosition,
197
+ positioner_suffix="DET:X",
198
+ )
199
+
200
+ self.d6 = FullDiagnostic(
201
+ prefix=prefix + "PHDGN-05:",
202
+ positioner_enum=D6Position,
203
+ positioner_suffix="DET:X",
204
+ )
205
+ self.d7 = Positioner(
206
+ prefix=prefix + "PHDGN-07:",
207
+ positioner_enum=D7Position,
208
+ positioner_suffix="Y",
209
+ )
210
+ super().__init__(name)
211
+
212
+
213
+ class I10Diagnostic5ADet(Device):
214
+ """Diagnostic 5a detection with drain current and photo diode"""
215
+
216
+ def __init__(
217
+ self, prefix: str, name: str = "", connector: DeviceConnector | None = None
218
+ ) -> None:
219
+ self.drain_current = CurrentAmpDet(
220
+ current_amp=FemtoDDPCA(
221
+ prefix=prefix + "IAMP-06:",
222
+ suffix="GAIN",
223
+ gain_table=Femto3xxGainTable,
224
+ gain_to_current_table=Femto3xxGainToCurrentTable,
225
+ raise_timetable=Femto3xxRaiseTime,
226
+ ),
227
+ counter=StruckScaler(prefix=prefix + "SCLR-02:SCALER2", suffix=".S17"),
228
+ )
229
+ self.diode = CurrentAmpDet(
230
+ FemtoDDPCA(
231
+ prefix=prefix + "IAMP-05:",
232
+ suffix="GAIN",
233
+ gain_table=Femto3xxGainTable,
234
+ gain_to_current_table=Femto3xxGainToCurrentTable,
235
+ raise_timetable=Femto3xxRaiseTime,
236
+ ),
237
+ counter=StruckScaler(prefix=prefix + "SCLR-02:SCALER2", suffix=".S18"),
238
+ )
239
+ super().__init__(name, connector)
@@ -1,15 +1,21 @@
1
+ from ophyd_async.core import Device
2
+ from ophyd_async.core._device import DeviceConnector
3
+ from ophyd_async.epics.core import epics_signal_r
1
4
  from ophyd_async.epics.motor import Motor
2
5
 
3
- from dodal.devices.slits import Slits
6
+ from dodal.devices.slits import MinimalSlits, Slits
4
7
 
5
8
 
6
- class I10Slits(Slits):
9
+ class I10SlitsBlades(Slits):
10
+ """Slits with extra control for each blade."""
11
+
7
12
  def __init__(self, prefix: str, name: str = "") -> None:
8
13
  with self.add_children_as_readables():
9
- self.x_ring_blade = Motor(prefix + "XRING")
10
- self.x_hall_blade = Motor(prefix + "XHALL")
11
- self.y_top_blade = Motor(prefix + "YPLUS")
12
- self.y_bot_blade = Motor(prefix + "YMINUS")
14
+ self.ring_blade = Motor(prefix + "XRING")
15
+ self.hall_blade = Motor(prefix + "XHALL")
16
+ self.top_blade = Motor(prefix + "YPLUS")
17
+ self.bot_blade = Motor(prefix + "YMINUS")
18
+
13
19
  super().__init__(
14
20
  prefix=prefix,
15
21
  x_gap="XSIZE",
@@ -20,7 +26,41 @@ class I10Slits(Slits):
20
26
  )
21
27
 
22
28
 
29
+ class BladeDrainCurrents(Device):
30
+ """ "The drain current measurements on each blade. The drain current are due to
31
+ photoelectric effect (https://en.wikipedia.org/wiki/Photoelectric_effect).
32
+ Note the readings are in voltage as it is the output of a current amplifier."""
33
+
34
+ def __init__(
35
+ self,
36
+ prefix: str,
37
+ suffix_ring_blade: str = "SIG1",
38
+ suffix_hall_blade: str = "SIG2",
39
+ suffix_top_blade: str = "SIG3",
40
+ suffix_bot_blade: str = "SIG4",
41
+ name: str = "",
42
+ connector: DeviceConnector | None = None,
43
+ ) -> None:
44
+ self.ring_blade_current = epics_signal_r(
45
+ float, read_pv=prefix + suffix_ring_blade
46
+ )
47
+ self.hall_blade_current = epics_signal_r(
48
+ float, read_pv=prefix + suffix_hall_blade
49
+ )
50
+ self.top_blade_current = epics_signal_r(
51
+ float, read_pv=prefix + suffix_top_blade
52
+ )
53
+ self.bot_blade_current = epics_signal_r(
54
+ float, read_pv=prefix + suffix_bot_blade
55
+ )
56
+
57
+ super().__init__(name, connector)
58
+
59
+
23
60
  class I10PrimarySlits(Slits):
61
+ """First slits of the beamline with very high power load, they are two square water
62
+ cooled blocks(aperture/aptr) that overlap to produce slit like behavior."""
63
+
24
64
  def __init__(self, prefix: str, name: str = "") -> None:
25
65
  with self.add_children_as_readables():
26
66
  self.x_aptr_1 = Motor(prefix + "APTR1:X")
@@ -35,3 +75,50 @@ class I10PrimarySlits(Slits):
35
75
  y_centre="YCENTRE",
36
76
  name=name,
37
77
  )
78
+
79
+
80
+ class I10Slits(Device):
81
+ """Collection of all the i10 slits before end station."""
82
+
83
+ def __init__(self, prefix: str, name: str = "") -> None:
84
+ self.s1 = I10PrimarySlits(
85
+ prefix=prefix + "01:",
86
+ )
87
+ self.s2 = I10SlitsBlades(
88
+ prefix=prefix + "02:",
89
+ )
90
+ self.s3 = I10SlitsBlades(
91
+ prefix=prefix + "03:",
92
+ )
93
+ self.s4 = MinimalSlits(
94
+ prefix=prefix + "04:",
95
+ x_gap="XSIZE",
96
+ y_gap="YSIZE",
97
+ )
98
+ self.s5 = I10SlitsBlades(
99
+ prefix=prefix + "05:",
100
+ )
101
+ self.s6 = I10SlitsBlades(
102
+ prefix=prefix + "06:",
103
+ )
104
+ super().__init__(name=name)
105
+
106
+
107
+ class I10SlitsDrainCurrent(Device):
108
+ """Collection of all the drain current from i10 slits."""
109
+
110
+ def __init__(
111
+ self, prefix: str, name: str = "", connector: DeviceConnector | None = None
112
+ ) -> None:
113
+ self.s2 = BladeDrainCurrents(
114
+ prefix=prefix + "AL-SLITS-02:",
115
+ suffix_ring_blade="XRING:I",
116
+ suffix_hall_blade="XHALL:I",
117
+ suffix_top_blade="YPLUS:I",
118
+ suffix_bot_blade="YMINUS:I",
119
+ )
120
+ self.s3 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-01:")
121
+ self.s4 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-02:")
122
+ self.s5 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-03:")
123
+ self.s6 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-04:")
124
+ super().__init__(name, connector)
@@ -23,10 +23,9 @@ class Merlin(StandardDetector):
23
23
  super().__init__(
24
24
  MerlinController(self.drv),
25
25
  adcore.ADHDFWriter(
26
- self.hdf,
27
- path_provider,
28
- lambda: self.name,
29
- adcore.ADBaseDatasetDescriber(self.drv),
26
+ fileio=self.hdf,
27
+ path_provider=path_provider,
28
+ dataset_describer=adcore.ADBaseDatasetDescriber(self.drv),
30
29
  ),
31
30
  config_sigs=(self.drv.acquire_period, self.drv.acquire_time),
32
31
  name=name,
@@ -37,7 +37,7 @@ class MerlinController(ADBaseController):
37
37
  DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
38
38
  )
39
39
  await asyncio.gather(
40
- self.driver.num_images.set(trigger_info.total_number_of_triggers),
40
+ self.driver.num_images.set(trigger_info.total_number_of_exposures),
41
41
  self.driver.image_mode.set(ADImageMode.MULTIPLE),
42
42
  )
43
43
 
@@ -0,0 +1,102 @@
1
+ import asyncio
2
+ import json
3
+ from enum import Enum
4
+ from typing import TypeVar
5
+
6
+ from aiohttp import ClientSession
7
+ from bluesky.protocols import Movable
8
+ from ophyd_async.core import AsyncStatus, StandardReadable
9
+
10
+ from dodal.log import LOGGER
11
+
12
+ OPTICS_BLUEAPI_URL = "https://i19-blueapi.diamond.ac.uk"
13
+ HEADERS = {"Accept": "application/json", "Content-Type": "application/json"}
14
+
15
+ D = TypeVar("D")
16
+
17
+
18
+ class HutchState(str, Enum):
19
+ EH1 = "EH1"
20
+ EH2 = "EH2"
21
+
22
+
23
+ class OpticsBlueAPIDevice(StandardReadable, Movable[D]):
24
+ """General device that a REST call to the blueapi instance controlling the optics \
25
+ hutch running on the I19 cluster, which will evaluate the current hutch in use vs \
26
+ the hutch sending the request and decide if the plan will be run or not.
27
+
28
+ For details see the architecture described in \
29
+ https://github.com/DiamondLightSource/i19-bluesky/issues/30.
30
+ """
31
+
32
+ def __init__(self, name: str = "") -> None:
33
+ self.url = OPTICS_BLUEAPI_URL
34
+ self.headers = HEADERS
35
+ super().__init__(name)
36
+
37
+ @AsyncStatus.wrap
38
+ async def set(self, value: D):
39
+ """ On set send a POST request to the optics blueapi with the name and \
40
+ parameters, gets the generated task_id and then sends a PUT request that runs \
41
+ the plan.
42
+
43
+ Args:
44
+ value (dict): The value passed here should be the parameters for the POST \
45
+ request, taking the form:
46
+ {
47
+ "name": "plan_name",
48
+ "params": {
49
+ "experiment_hutch": f"{hutch_name}",
50
+ "access_device": "access_control",
51
+ "other_params": "...",
52
+ ...
53
+ }
54
+ }
55
+ """
56
+ # Value here vould be request params dictionary.
57
+ request_params = json.dumps(value)
58
+
59
+ async with ClientSession(base_url=self.url, raise_for_status=True) as session:
60
+ # First submit the plan to the worker
61
+ async with session.post(
62
+ "/tasks", data=request_params, headers=HEADERS
63
+ ) as response:
64
+ LOGGER.info(
65
+ f"Task submitted to the worker, response status: {response.status}"
66
+ )
67
+
68
+ try:
69
+ data = await response.json()
70
+ task_id = data["task_id"]
71
+ except Exception as e:
72
+ LOGGER.error(
73
+ f"Failed to get task_id from {self.url}/tasks POST. ({e})"
74
+ )
75
+ raise
76
+ # Then set the task as active and run asap
77
+ async with session.put(
78
+ "/worker/task", data=json.dumps({"task_id": task_id}), headers=HEADERS
79
+ ) as response:
80
+ if not response.ok:
81
+ LOGGER.error(
82
+ f"""Session PUT responded with {response.status}: {response.reason}.
83
+ Unable to run plan {value["name"]}.""" # type: ignore
84
+ )
85
+ return
86
+ LOGGER.info(f"Running plan: {value['name']}, task_id: {task_id}") # type: ignore
87
+
88
+ # Poll server at 2Hz until plan complete or errored
89
+ interval = 0.5
90
+ plan_complete = False
91
+
92
+ while not plan_complete:
93
+ async with session.get(f"/tasks/{task_id}") as res:
94
+ plan_result = await res.json()
95
+ plan_complete = plan_result["is_complete"]
96
+ errors = plan_result["errors"]
97
+ if len(errors) > 0:
98
+ message = "\n".join(errors)
99
+ LOGGER.error(f"Plan {value['name']} failed: {message}") # type:ignore
100
+ raise RuntimeError(f"Plan failed with error: {message}")
101
+ await asyncio.sleep(interval)
102
+ LOGGER.info(f"Plan {value['name']} done.") # type: ignore
@@ -1,23 +1,12 @@
1
- from enum import Enum
2
-
3
- from aiohttp import ClientSession
4
- from bluesky.protocols import Movable
5
- from ophyd_async.core import AsyncStatus, StandardReadable, StandardReadableFormat
1
+ from ophyd_async.core import AsyncStatus, StandardReadableFormat
6
2
  from ophyd_async.epics.core import epics_signal_r
7
3
 
8
4
  from dodal.devices.hutch_shutter import ShutterDemand, ShutterState
5
+ from dodal.devices.i19.blueapi_device import HutchState, OpticsBlueAPIDevice
9
6
  from dodal.devices.i19.hutch_access import ACCESS_DEVICE_NAME
10
- from dodal.log import LOGGER
11
-
12
- OPTICS_BLUEAPI_URL = "https://i19-blueapi.diamond.ac.uk"
13
-
14
7
 
15
- class HutchState(str, Enum):
16
- EH1 = "EH1"
17
- EH2 = "EH2"
18
8
 
19
-
20
- class AccessControlledShutter(StandardReadable, Movable[ShutterDemand]):
9
+ class AccessControlledShutter(OpticsBlueAPIDevice):
21
10
  """ I19-specific device to operate the hutch shutter.
22
11
 
23
12
  This device will send a REST call to the blueapi instance controlling the optics \
@@ -37,7 +26,6 @@ class AccessControlledShutter(StandardReadable, Movable[ShutterDemand]):
37
26
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
38
27
  self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
39
28
  self.hutch_request = hutch
40
- self.url = OPTICS_BLUEAPI_URL
41
29
  super().__init__(name)
42
30
 
43
31
  @AsyncStatus.wrap
@@ -47,33 +35,7 @@ class AccessControlledShutter(StandardReadable, Movable[ShutterDemand]):
47
35
  "params": {
48
36
  "experiment_hutch": self.hutch_request.value,
49
37
  "access_device": ACCESS_DEVICE_NAME,
50
- "shutter_demand": value,
38
+ "shutter_demand": value.value,
51
39
  },
52
40
  }
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}")
41
+ await super().set(REQUEST_PARAMS)