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.
- {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/METADATA +2 -2
- {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/RECORD +56 -46
- {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +2 -0
- dodal/beamlines/b07.py +27 -0
- dodal/beamlines/b07_1.py +25 -0
- dodal/beamlines/i03.py +4 -4
- dodal/beamlines/i04.py +1 -1
- dodal/beamlines/i09.py +25 -0
- dodal/beamlines/i09_1.py +25 -0
- dodal/beamlines/i10.py +19 -35
- dodal/beamlines/i18.py +7 -4
- dodal/beamlines/i19_1.py +2 -1
- dodal/beamlines/i19_2.py +2 -1
- dodal/beamlines/i20_1.py +2 -1
- dodal/beamlines/i22.py +3 -3
- dodal/beamlines/i23.py +67 -2
- dodal/beamlines/p38.py +3 -3
- dodal/beamlines/p60.py +21 -0
- dodal/common/beamlines/beamline_utils.py +5 -0
- dodal/common/visit.py +1 -41
- dodal/devices/common_dcm.py +77 -0
- dodal/devices/detector/det_dist_to_beam_converter.py +16 -23
- dodal/devices/detector/detector.py +2 -1
- dodal/devices/electron_analyser/abstract_analyser_io.py +47 -0
- dodal/devices/electron_analyser/abstract_region.py +112 -0
- dodal/devices/electron_analyser/specs_analyser_io.py +19 -0
- dodal/devices/electron_analyser/specs_region.py +26 -0
- dodal/devices/electron_analyser/vgscienta_analyser_io.py +26 -0
- dodal/devices/electron_analyser/vgscienta_region.py +90 -0
- dodal/devices/{dcm.py → i03/dcm.py} +8 -12
- dodal/devices/{undulator_dcm.py → i03/undulator_dcm.py} +6 -4
- dodal/devices/i10/diagnostics.py +239 -0
- dodal/devices/i10/slits.py +93 -6
- dodal/devices/i13_1/merlin.py +3 -4
- dodal/devices/i13_1/merlin_controller.py +1 -1
- dodal/devices/i19/blueapi_device.py +102 -0
- dodal/devices/i19/shutter.py +5 -43
- dodal/devices/i22/dcm.py +10 -12
- dodal/devices/i24/dcm.py +8 -17
- dodal/devices/motors.py +21 -0
- dodal/devices/tetramm.py +3 -4
- dodal/devices/turbo_slit.py +10 -4
- dodal/devices/undulator.py +9 -7
- dodal/devices/util/adjuster_plans.py +1 -2
- dodal/devices/util/lookup_tables.py +38 -0
- dodal/devices/util/test_utils.py +1 -0
- dodal/devices/zebra/zebra.py +4 -0
- dodal/plan_stubs/data_session.py +10 -1
- dodal/plan_stubs/electron_analyser/configure_controller.py +80 -0
- dodal/plans/verify_undulator_gap.py +2 -2
- dodal/devices/electron_analyser/base_region.py +0 -64
- dodal/devices/electron_analyser/specs/specs_region.py +0 -24
- dodal/devices/electron_analyser/vgscienta/__init__.py +0 -0
- dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +0 -77
- dodal/devices/util/motor_utils.py +0 -6
- {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/top_level.txt +0 -0
- /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,
|
|
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(
|
|
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
|
-
|
|
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
|
|
9
|
-
from .
|
|
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)
|
dodal/devices/i10/slits.py
CHANGED
|
@@ -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
|
|
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.
|
|
10
|
-
self.
|
|
11
|
-
self.
|
|
12
|
-
self.
|
|
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)
|
dodal/devices/i13_1/merlin.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
dodal/devices/i19/shutter.py
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
from
|
|
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
|
-
|
|
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)
|