dls-dodal 1.39.0__py3-none-any.whl → 1.40.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 (48) hide show
  1. {dls_dodal-1.39.0.dist-info → dls_dodal-1.40.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.39.0.dist-info → dls_dodal-1.40.0.dist-info}/RECORD +48 -39
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/__init__.py +2 -0
  5. dodal/beamlines/adsim.py +3 -2
  6. dodal/beamlines/b01_1.py +3 -3
  7. dodal/beamlines/i03.py +141 -292
  8. dodal/beamlines/i04.py +112 -198
  9. dodal/beamlines/i13_1.py +5 -4
  10. dodal/beamlines/i18.py +124 -0
  11. dodal/beamlines/i19_1.py +74 -0
  12. dodal/beamlines/i19_2.py +61 -0
  13. dodal/beamlines/i20_1.py +37 -22
  14. dodal/beamlines/i22.py +7 -7
  15. dodal/beamlines/i24.py +100 -145
  16. dodal/beamlines/p38.py +12 -8
  17. dodal/beamlines/p45.py +5 -4
  18. dodal/beamlines/training_rig.py +4 -4
  19. dodal/common/beamlines/beamline_utils.py +2 -3
  20. dodal/common/beamlines/device_helpers.py +3 -1
  21. dodal/devices/aperturescatterguard.py +150 -64
  22. dodal/devices/apple2_undulator.py +86 -113
  23. dodal/devices/eiger.py +3 -2
  24. dodal/devices/fast_grid_scan.py +16 -18
  25. dodal/devices/hutch_shutter.py +25 -12
  26. dodal/devices/i10/rasor/rasor_scaler_cards.py +4 -4
  27. dodal/devices/i13_1/merlin.py +4 -3
  28. dodal/devices/i13_1/merlin_controller.py +2 -7
  29. dodal/devices/i18/KBMirror.py +19 -0
  30. dodal/devices/i18/diode.py +17 -0
  31. dodal/devices/i18/table.py +14 -0
  32. dodal/devices/i18/thor_labs_stage.py +12 -0
  33. dodal/devices/i19/__init__.py +0 -0
  34. dodal/devices/i19/shutter.py +57 -0
  35. dodal/devices/i22/nxsas.py +4 -4
  36. dodal/devices/motors.py +2 -2
  37. dodal/devices/oav/oav_detector.py +10 -19
  38. dodal/devices/pressure_jump_cell.py +33 -16
  39. dodal/devices/robot.py +30 -11
  40. dodal/devices/tetramm.py +8 -3
  41. dodal/devices/turbo_slit.py +7 -6
  42. dodal/devices/zocalo/zocalo_results.py +21 -4
  43. dodal/plans/save_panda.py +30 -14
  44. dodal/utils.py +54 -15
  45. {dls_dodal-1.39.0.dist-info → dls_dodal-1.40.0.dist-info}/LICENSE +0 -0
  46. {dls_dodal-1.39.0.dist-info → dls_dodal-1.40.0.dist-info}/WHEEL +0 -0
  47. {dls_dodal-1.39.0.dist-info → dls_dodal-1.40.0.dist-info}/entry_points.txt +0 -0
  48. {dls_dodal-1.39.0.dist-info → dls_dodal-1.40.0.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,8 @@ from bluesky.protocols import Movable
8
8
  from ophyd_async.core import (
9
9
  AsyncStatus,
10
10
  Reference,
11
+ SignalR,
12
+ SignalW,
11
13
  StandardReadable,
12
14
  StandardReadableFormat,
13
15
  StrictEnum,
@@ -88,7 +90,56 @@ MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
88
90
  MAXIMUM_GAP_MOTOR_POSITION = 100
89
91
 
90
92
 
91
- class UndulatorGap(StandardReadable, Movable):
93
+ async def estimate_motor_timeout(
94
+ setpoint: SignalR, curr_pos: SignalR, velocity: SignalR
95
+ ):
96
+ vel = await velocity.get_value()
97
+ cur_pos = await curr_pos.get_value()
98
+ target_pos = float(await setpoint.get_value())
99
+ return abs((target_pos - cur_pos) * 2.0 / vel) + 1
100
+
101
+
102
+ class SafeUndulatorMover(StandardReadable, Movable):
103
+ """A device that will check it's safe to move the undulator before moving it and
104
+ wait for the undulator to be safe again before calling the move complete.
105
+ """
106
+
107
+ def __init__(self, set_move: SignalW, prefix: str, name: str = ""):
108
+ # Gate keeper open when move is requested, closed when move is completed
109
+ self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
110
+
111
+ split_pv = prefix.split("-")
112
+ fault_pv = f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT"
113
+ self.fault = epics_signal_r(float, fault_pv)
114
+ self.set_move = set_move
115
+ super().__init__(name)
116
+
117
+ @AsyncStatus.wrap
118
+ async def set(self, value) -> None:
119
+ LOGGER.info(f"Setting {self.name} to {value}")
120
+ await self.raise_if_cannot_move()
121
+ await self._set_demand_positions(value)
122
+ timeout = await self.get_timeout()
123
+ LOGGER.info(f"Moving {self.name} to {value} with timeout = {timeout}")
124
+ await self.set_move.set(value=1, timeout=timeout)
125
+ await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
126
+
127
+ @abc.abstractmethod
128
+ async def _set_demand_positions(self, value) -> None:
129
+ """Set the demand positions on the device without actually hitting move."""
130
+
131
+ @abc.abstractmethod
132
+ async def get_timeout(self) -> float:
133
+ """Get the timeout for the move based on an estimate of how long it will take."""
134
+
135
+ async def raise_if_cannot_move(self) -> None:
136
+ if await self.fault.get_value() != 0:
137
+ raise RuntimeError(f"{self.name} is in fault state")
138
+ if await self.gate.get_value() == UndulatorGateStatus.OPEN:
139
+ raise RuntimeError(f"{self.name} is already in motion.")
140
+
141
+
142
+ class UndulatorGap(SafeUndulatorMover):
92
143
  """A device with a collection of epics signals to set Apple 2 undulator gap motion.
93
144
  Only PV used by beamline are added the full list is here:
94
145
  /dls_sw/work/R3.14.12.7/support/insertionDevice/db/IDGapVelocityControl.template
@@ -113,21 +164,17 @@ class UndulatorGap(StandardReadable, Movable):
113
164
  )
114
165
  # Nothing move until this is set to 1 and it will return to 0 when done
115
166
  self.set_move = epics_signal_rw(int, prefix + "BLGSETP")
116
- # Gate keeper open when move is requested, closed when move is completed
117
- self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
167
+
118
168
  # These are gap velocity limit.
119
169
  self.max_velocity = epics_signal_r(float, prefix + "BLGSETVEL.HOPR")
120
170
  self.min_velocity = epics_signal_r(float, prefix + "BLGSETVEL.LOPR")
121
171
  # These are gap limit.
122
172
  self.high_limit_travel = epics_signal_r(float, prefix + "BLGAPMTR.HLM")
123
173
  self.low_limit_travel = epics_signal_r(float, prefix + "BLGAPMTR.LLM")
124
- split_pv = prefix.split("-")
125
- self.fault = epics_signal_r(
126
- float,
127
- f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT",
128
- )
174
+
129
175
  # This is calculated acceleration from speed
130
176
  self.acceleration_time = epics_signal_r(float, prefix + "IDGSETACC")
177
+
131
178
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
132
179
  # Unit
133
180
  self.motor_egu = epics_signal_r(str, prefix + "BLGAPMTR.EGU")
@@ -136,32 +183,15 @@ class UndulatorGap(StandardReadable, Movable):
136
183
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
137
184
  # Gap readback value
138
185
  self.user_readback = epics_signal_r(float, prefix + "CURRGAPD")
139
- super().__init__(name)
140
-
141
- @AsyncStatus.wrap
142
- async def set(self, value) -> None:
143
- LOGGER.info(f"Setting {self.name} to {value}")
144
- await self.check_id_status()
145
- await self.user_setpoint.set(value=str(value))
146
- timeout = await self._cal_timeout()
147
- LOGGER.info(f"Moving {self.name} to {value} with timeout = {timeout}")
148
- await self.set_move.set(value=1, timeout=timeout)
149
- await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
150
-
151
- async def _cal_timeout(self) -> float:
152
- vel = await self.velocity.get_value()
153
- cur_pos = await self.user_readback.get_value()
154
- target_pos = float(await self.user_setpoint.get_value())
155
- return abs((target_pos - cur_pos) * 2.0 / vel) + 1
186
+ super().__init__(self.set_move, prefix, name)
156
187
 
157
- async def check_id_status(self) -> None:
158
- if await self.fault.get_value() != 0:
159
- raise RuntimeError(f"{self.name} is in fault state")
160
- if await self.gate.get_value() == UndulatorGateStatus.OPEN:
161
- raise RuntimeError(f"{self.name} is already in motion.")
188
+ async def _set_demand_positions(self, value) -> None:
189
+ await self.user_setpoint.set(str(value))
162
190
 
163
191
  async def get_timeout(self) -> float:
164
- return await self._cal_timeout()
192
+ return await estimate_motor_timeout(
193
+ self.user_setpoint, self.user_readback, self.velocity
194
+ )
165
195
 
166
196
 
167
197
  class UndulatorPhaseMotor(StandardReadable):
@@ -204,7 +234,7 @@ class UndulatorPhaseMotor(StandardReadable):
204
234
  super().__init__(name=name)
205
235
 
206
236
 
207
- class UndulatorPhaseAxes(StandardReadable, Movable):
237
+ class UndulatorPhaseAxes(SafeUndulatorMover):
208
238
  """
209
239
  A collection of 4 phase Motor to make up the full id phase motion. We are using the diamond pv convention.
210
240
  e.g. top_outer == Q1
@@ -231,66 +261,36 @@ class UndulatorPhaseAxes(StandardReadable, Movable):
231
261
  self.btm_outer = UndulatorPhaseMotor(prefix=prefix, infix=btm_outer)
232
262
  # Nothing move until this is set to 1 and it will return to 0 when done.
233
263
  self.set_move = epics_signal_rw(int, f"{prefix}BL{top_outer}" + "MOVE")
234
- self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
235
- split_pv = prefix.split("-")
236
- temp_pv = f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT"
237
- self.fault = epics_signal_r(float, temp_pv)
238
- super().__init__(name=name)
239
-
240
- @AsyncStatus.wrap
241
- async def set(self, value: Apple2PhasesVal) -> None:
242
- LOGGER.info(f"Setting {self.name} to {value}")
243
264
 
244
- await self.check_id_status()
265
+ super().__init__(self.set_move, prefix, name)
245
266
 
267
+ async def _set_demand_positions(self, value: Apple2PhasesVal) -> None:
246
268
  await asyncio.gather(
247
269
  self.top_outer.user_setpoint.set(value=value.top_outer),
248
270
  self.top_inner.user_setpoint.set(value=value.top_inner),
249
271
  self.btm_inner.user_setpoint.set(value=value.btm_inner),
250
272
  self.btm_outer.user_setpoint.set(value=value.btm_outer),
251
273
  )
252
- timeout = await self._cal_timeout()
253
- await self.set_move.set(value=1, timeout=timeout)
254
- await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
255
274
 
256
- async def _cal_timeout(self) -> float:
275
+ async def get_timeout(self) -> float:
257
276
  """
258
277
  Get all four motor speed, current positions and target positions to calculate required timeout.
259
278
  """
260
- velos = await asyncio.gather(
261
- self.top_outer.velocity.get_value(),
262
- self.top_inner.velocity.get_value(),
263
- self.btm_inner.velocity.get_value(),
264
- self.btm_outer.velocity.get_value(),
265
- )
266
- target_pos = await asyncio.gather(
267
- self.top_outer.user_setpoint_demand_readback.get_value(),
268
- self.top_inner.user_setpoint_demand_readback.get_value(),
269
- self.btm_inner.user_setpoint_demand_readback.get_value(),
270
- self.btm_outer.user_setpoint_demand_readback.get_value(),
271
- )
272
- cur_pos = await asyncio.gather(
273
- self.top_outer.user_setpoint_readback.get_value(),
274
- self.top_inner.user_setpoint_readback.get_value(),
275
- self.btm_inner.user_setpoint_readback.get_value(),
276
- self.btm_outer.user_setpoint_readback.get_value(),
279
+ axes = [self.top_outer, self.top_inner, self.btm_inner, self.btm_outer]
280
+ timeouts = await asyncio.gather(
281
+ *[
282
+ estimate_motor_timeout(
283
+ axis.user_setpoint_demand_readback,
284
+ axis.user_setpoint_readback,
285
+ axis.velocity,
286
+ )
287
+ for axis in axes
288
+ ]
277
289
  )
278
- move_distances = tuple(np.subtract(target_pos, cur_pos))
279
- move_times = np.abs(np.divide(move_distances, velos))
280
- longest_move_time = np.max(move_times)
281
- return longest_move_time * 2 + 1
282
-
283
- async def check_id_status(self) -> None:
284
- if await self.fault.get_value() != 0:
285
- raise RuntimeError(f"{self.name} is in fault state")
286
- if await self.gate.get_value() == UndulatorGateStatus.OPEN:
287
- raise RuntimeError(f"{self.name} is already in motion.")
288
-
289
- async def get_timeout(self) -> float:
290
- return await self._cal_timeout()
290
+ return np.max(timeouts)
291
291
 
292
292
 
293
- class UndulatorJawPhase(StandardReadable, Movable):
293
+ class UndulatorJawPhase(SafeUndulatorMover):
294
294
  """
295
295
  A JawPhase movable, this is use for moving the jaw phase which is use to control the
296
296
  linear arbitrary polarisation but only one some of the beamline.
@@ -308,49 +308,22 @@ class UndulatorJawPhase(StandardReadable, Movable):
308
308
  self.jaw_phase = UndulatorPhaseMotor(prefix=prefix, infix=jaw_phase)
309
309
  # Nothing move until this is set to 1 and it will return to 0 when done
310
310
  self.set_move = epics_signal_rw(int, f"{prefix}BL{move_pv}" + "MOVE")
311
- self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
312
- split_pv = prefix.split("-")
313
- temp_pv = f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT"
314
- self.fault = epics_signal_r(float, temp_pv)
315
- super().__init__(name=name)
316
311
 
317
- @AsyncStatus.wrap
318
- async def set(self, value: float) -> None:
319
- LOGGER.info(f"Setting {self.name} to {value}")
320
-
321
- await self.check_id_status()
312
+ super().__init__(self.set_move, prefix, name)
322
313
 
323
- await asyncio.gather(
324
- self.jaw_phase.user_setpoint.set(value=str(value)),
325
- )
326
- timeout = await self._cal_timeout()
327
- await self.set_move.set(value=1, timeout=timeout)
328
- await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
314
+ async def _set_demand_positions(self, value: float) -> None:
315
+ await self.jaw_phase.user_setpoint.set(value=str(value))
329
316
 
330
- async def _cal_timeout(self) -> float:
317
+ async def get_timeout(self) -> float:
331
318
  """
332
319
  Get motor speed, current position and target position to calculate required timeout.
333
320
  """
334
- velo, target_pos, cur_pos = await asyncio.gather(
335
- self.jaw_phase.velocity.get_value(),
336
- self.jaw_phase.user_setpoint_demand_readback.get_value(),
337
- self.jaw_phase.user_setpoint_readback.get_value(),
321
+ return await estimate_motor_timeout(
322
+ self.jaw_phase.user_setpoint_demand_readback,
323
+ self.jaw_phase.user_setpoint_readback,
324
+ self.jaw_phase.velocity,
338
325
  )
339
326
 
340
- move_distances = target_pos - cur_pos
341
- move_times = np.abs(move_distances / velo)
342
-
343
- return move_times * 2 + 1
344
-
345
- async def check_id_status(self) -> None:
346
- if await self.fault.get_value() != 0:
347
- raise RuntimeError(f"{self.name} is in fault state")
348
- if await self.gate.get_value() == UndulatorGateStatus.OPEN:
349
- raise RuntimeError(f"{self.name} is already in motion.")
350
-
351
- async def get_timeout(self) -> float:
352
- return await self._cal_timeout()
353
-
354
327
 
355
328
  class Apple2(StandardReadable, Movable):
356
329
  """
@@ -437,7 +410,7 @@ class Apple2(StandardReadable, Movable):
437
410
  """
438
411
 
439
412
  # Only need to check gap as the phase motors share both fault and gate with gap.
440
- await self.gap().check_id_status()
413
+ await self.gap().raise_if_cannot_move()
441
414
  await asyncio.gather(
442
415
  self.phase().top_outer.user_setpoint.set(value=value.top_outer),
443
416
  self.phase().top_inner.user_setpoint.set(value=value.top_inner),
dodal/devices/eiger.py CHANGED
@@ -2,6 +2,7 @@
2
2
  from dataclasses import dataclass
3
3
  from enum import Enum
4
4
 
5
+ from bluesky.protocols import Stageable
5
6
  from ophyd import Component, Device, EpicsSignalRO, Signal
6
7
  from ophyd.areadetector.cam import EigerDetectorCam
7
8
  from ophyd.status import AndStatus, Status, StatusBase
@@ -42,7 +43,7 @@ AVAILABLE_TIMEOUTS = {
42
43
  }
43
44
 
44
45
 
45
- class EigerDetector(Device):
46
+ class EigerDetector(Device, Stageable):
46
47
  class ArmingSignal(Signal):
47
48
  def set(self, value, *, timeout=None, settle_time=None, **kwargs):
48
49
  assert isinstance(self.parent, EigerDetector)
@@ -161,7 +162,7 @@ class EigerDetector(Device):
161
162
  status_ok = self.odin.check_and_wait_for_odin_state(
162
163
  self.timeouts.general_status_timeout
163
164
  )
164
- self.disable_roi_mode()
165
+ self.disable_roi_mode().wait(self.timeouts.general_status_timeout)
165
166
  self.disarming_status.set_finished()
166
167
  return status_ok
167
168
 
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from abc import ABC, abstractmethod
2
3
  from typing import Generic, TypeVar
3
4
 
@@ -9,9 +10,7 @@ from ophyd_async.core import (
9
10
  AsyncStatus,
10
11
  Device,
11
12
  Signal,
12
- SignalR,
13
13
  SignalRW,
14
- SoftSignalBackend,
15
14
  StandardReadable,
16
15
  wait_for_value,
17
16
  )
@@ -24,6 +23,7 @@ from ophyd_async.epics.core import (
24
23
  from pydantic import field_validator
25
24
  from pydantic.dataclasses import dataclass
26
25
 
26
+ from dodal.common.signal_utils import create_hardware_backed_soft_signal
27
27
  from dodal.log import LOGGER
28
28
  from dodal.parameters.experiment_parameter_base import AbstractExperimentWithBeamParams
29
29
 
@@ -170,21 +170,6 @@ class MotionProgram(Device):
170
170
  self.program_number = epics_signal_r(float, prefix + "CS1:PROG_NUM")
171
171
 
172
172
 
173
- class ExpectedImages(SignalR[int]):
174
- def __init__(self, parent: "FastGridScanCommon") -> None:
175
- super().__init__(SoftSignalBackend(int))
176
- self.parent = parent
177
-
178
- async def get_value(self, cached: bool | None = None):
179
- assert isinstance(self.parent, FastGridScanCommon)
180
- x = await self.parent.x_steps.get_value()
181
- y = await self.parent.y_steps.get_value()
182
- z = await self.parent.z_steps.get_value()
183
- first_grid = x * y
184
- second_grid = x * z
185
- return first_grid + second_grid
186
-
187
-
188
173
  class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
189
174
  """Device for a general fast grid scan
190
175
 
@@ -217,7 +202,9 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
217
202
  self.run_cmd = epics_signal_x(f"{prefix}RUN.PROC")
218
203
  self.status = epics_signal_r(int, f"{prefix}SCAN_STATUS")
219
204
 
220
- self.expected_images = ExpectedImages(parent=self)
205
+ self.expected_images = create_hardware_backed_soft_signal(
206
+ float, self._calculate_expected_images
207
+ )
221
208
 
222
209
  self.motion_program = MotionProgram(smargon_prefix)
223
210
 
@@ -243,6 +230,17 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
243
230
  }
244
231
  super().__init__(name)
245
232
 
233
+ async def _calculate_expected_images(self):
234
+ x, y, z = await asyncio.gather(
235
+ self.x_steps.get_value(),
236
+ self.y_steps.get_value(),
237
+ self.z_steps.get_value(),
238
+ )
239
+ LOGGER.info(f"Reading num of images found {x, y, z} images in each axis")
240
+ first_grid = x * y
241
+ second_grid = x * z
242
+ return first_grid + second_grid
243
+
246
244
  @AsyncStatus.wrap
247
245
  async def kickoff(self):
248
246
  curr_prog = await self.motion_program.program_number.get_value()
@@ -8,9 +8,16 @@ from ophyd_async.core import (
8
8
  )
9
9
  from ophyd_async.epics.core import epics_signal_r, epics_signal_w
10
10
 
11
+ from dodal.log import LOGGER
12
+
11
13
  HUTCH_SAFE_FOR_OPERATIONS = 0 # Hutch is locked and can't be entered
12
14
 
13
15
 
16
+ # Enable to allow testing when the beamline is down, do not change in production!
17
+ TEST_MODE = False
18
+ # will be made more generic in https://github.com/DiamondLightSource/dodal/issues/754
19
+
20
+
14
21
  class ShutterNotSafeToOperateError(Exception):
15
22
  pass
16
23
 
@@ -64,8 +71,8 @@ class HutchShutter(StandardReadable, Movable):
64
71
  """
65
72
 
66
73
  def __init__(self, prefix: str, name: str = "") -> None:
67
- self.control = epics_signal_w(ShutterDemand, prefix + "CON")
68
- self.status = epics_signal_r(ShutterState, prefix + "STA")
74
+ self.control = epics_signal_w(ShutterDemand, f"{prefix}CON")
75
+ self.status = epics_signal_r(ShutterState, f"{prefix}STA")
69
76
 
70
77
  bl_prefix = prefix.split("-")[0]
71
78
  self.interlock = HutchInterlock(bl_prefix)
@@ -75,18 +82,24 @@ class HutchShutter(StandardReadable, Movable):
75
82
  @AsyncStatus.wrap
76
83
  async def set(self, value: ShutterDemand):
77
84
  interlock_state = await self.interlock.shutter_safe_to_operate()
78
- if not interlock_state:
85
+ if not interlock_state and not TEST_MODE:
86
+ # If not in test mode, fail. If in test mode, the optics hutch may be open.
79
87
  raise ShutterNotSafeToOperateError(
80
88
  "The hutch has not been locked, not operating shutter."
81
89
  )
82
- if value == ShutterDemand.OPEN:
83
- await self.control.set(ShutterDemand.RESET, wait=True)
84
- await self.control.set(value, wait=True)
85
- return await wait_for_value(
86
- self.status, match=ShutterState.OPEN, timeout=DEFAULT_TIMEOUT
87
- )
90
+ if not TEST_MODE:
91
+ if value == ShutterDemand.OPEN:
92
+ await self.control.set(ShutterDemand.RESET, wait=True)
93
+ await self.control.set(value, wait=True)
94
+ return await wait_for_value(
95
+ self.status, match=ShutterState.OPEN, timeout=DEFAULT_TIMEOUT
96
+ )
97
+ else:
98
+ await self.control.set(value, wait=True)
99
+ return await wait_for_value(
100
+ self.status, match=ShutterState.CLOSED, timeout=DEFAULT_TIMEOUT
101
+ )
88
102
  else:
89
- await self.control.set(value, wait=True)
90
- return await wait_for_value(
91
- self.status, match=ShutterState.CLOSED, timeout=DEFAULT_TIMEOUT
103
+ LOGGER.warning(
104
+ "Running in test mode, will not operate the experiment shutter."
92
105
  )
@@ -5,8 +5,8 @@ from dodal.devices.current_amplifiers import StruckScaler
5
5
 
6
6
  class RasorScalerCard1(Device):
7
7
  def __init__(self, prefix, name: str = "") -> None:
8
- self.mon = StruckScaler(prefix=prefix, suffix=".16")
9
- self.det = StruckScaler(prefix=prefix, suffix=".17")
10
- self.fluo = StruckScaler(prefix=prefix, suffix=".18")
11
- self.drain = StruckScaler(prefix=prefix, suffix=".19")
8
+ self.mon = StruckScaler(prefix=prefix, suffix=".S17")
9
+ self.det = StruckScaler(prefix=prefix, suffix=".S18")
10
+ self.fluo = StruckScaler(prefix=prefix, suffix=".S19")
11
+ self.drain = StruckScaler(prefix=prefix, suffix=".S20")
12
12
  super().__init__(name)
@@ -1,6 +1,7 @@
1
1
  from ophyd_async.core import PathProvider, StandardDetector
2
2
  from ophyd_async.epics import adcore
3
3
 
4
+ from dodal.common.beamlines.device_helpers import CAM_SUFFIX, HDF5_SUFFIX
4
5
  from dodal.devices.i13_1.merlin_controller import MerlinController
5
6
  from dodal.devices.i13_1.merlin_io import MerlinDriverIO
6
7
 
@@ -13,12 +14,12 @@ class Merlin(StandardDetector):
13
14
  self,
14
15
  prefix: str,
15
16
  path_provider: PathProvider,
16
- drv_suffix="CAM:",
17
- hdf_suffix="HDF:",
17
+ drv_suffix=CAM_SUFFIX,
18
+ fileio_suffix=HDF5_SUFFIX,
18
19
  name: str = "",
19
20
  ):
20
21
  self.drv = MerlinDriverIO(prefix + drv_suffix)
21
- self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix)
22
+ self.hdf = adcore.NDFileHDFIO(prefix + fileio_suffix)
22
23
 
23
24
  super().__init__(
24
25
  MerlinController(self.drv),
@@ -4,15 +4,15 @@ import logging
4
4
  from ophyd_async.core import (
5
5
  DEFAULT_TIMEOUT,
6
6
  AsyncStatus,
7
- DetectorController,
8
7
  TriggerInfo,
9
8
  )
10
9
  from ophyd_async.epics import adcore
10
+ from ophyd_async.epics.adcore import ADBaseController
11
11
 
12
12
  from dodal.devices.i13_1.merlin_io import MerlinDriverIO, MerlinImageMode
13
13
 
14
14
 
15
- class MerlinController(DetectorController):
15
+ class MerlinController(ADBaseController):
16
16
  def __init__(
17
17
  self,
18
18
  driver: MerlinDriverIO,
@@ -37,11 +37,6 @@ class MerlinController(DetectorController):
37
37
  self.driver.image_mode.set(MerlinImageMode.MULTIPLE),
38
38
  )
39
39
 
40
- async def arm(self):
41
- self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
42
- self.driver, good_states=self.good_states, timeout=self.frame_timeout
43
- )
44
-
45
40
  async def wait_for_idle(self):
46
41
  if self._arm_status:
47
42
  await self._arm_status
@@ -0,0 +1,19 @@
1
+ from ophyd_async.core import StandardReadable
2
+ from ophyd_async.epics.core import epics_signal_rw
3
+
4
+
5
+ class KBMirror(StandardReadable):
6
+ def __init__(
7
+ self,
8
+ prefix: str,
9
+ name: str = "",
10
+ ):
11
+ self._prefix = prefix
12
+ with self.add_children_as_readables():
13
+ self.x = epics_signal_rw(float, prefix + "X")
14
+ self.y = epics_signal_rw(float, prefix + "Y")
15
+ self.bend1 = epics_signal_rw(float, prefix + "BEND1")
16
+ self.bend2 = epics_signal_rw(float, prefix + "BEND2")
17
+ self.curve = epics_signal_rw(float, prefix + "CURVE")
18
+ self.ellip = epics_signal_rw(float, prefix + "ELLIP")
19
+ super().__init__(name=name)
@@ -0,0 +1,17 @@
1
+ from ophyd_async.core import (
2
+ StandardReadable,
3
+ )
4
+ from ophyd_async.epics.core import epics_signal_r
5
+
6
+
7
+ class Diode(StandardReadable):
8
+ def __init__(
9
+ self,
10
+ prefix: str,
11
+ name: str = "",
12
+ ):
13
+ self._prefix = prefix
14
+ with self.add_children_as_readables():
15
+ self.signal = epics_signal_r(float, prefix + "B:DIODE:I")
16
+
17
+ super().__init__(name=name)
@@ -0,0 +1,14 @@
1
+ from ophyd_async.core import (
2
+ StandardReadable,
3
+ )
4
+ from ophyd_async.epics.motor import Motor
5
+
6
+
7
+ class Table(StandardReadable):
8
+ def __init__(self, prefix: str, name: str = "") -> None:
9
+ with self.add_children_as_readables():
10
+ self.x = Motor(prefix + "X")
11
+ self.y = Motor(prefix + "Y")
12
+ self.z = Motor(prefix + "Z")
13
+ self.theta = Motor(prefix + "THETA")
14
+ super().__init__(name=name)
@@ -0,0 +1,12 @@
1
+ from ophyd_async.core import (
2
+ StandardReadable,
3
+ )
4
+ from ophyd_async.epics.motor import Motor
5
+
6
+
7
+ class ThorLabsStage(StandardReadable):
8
+ def __init__(self, prefix: str, name: str = "") -> None:
9
+ with self.add_children_as_readables():
10
+ self.x = Motor(prefix + "X")
11
+ self.y = Motor(prefix + "Y")
12
+ super().__init__(name=name)
File without changes
@@ -0,0 +1,57 @@
1
+ from enum import Enum
2
+
3
+ from bluesky.protocols import Movable
4
+ from ophyd_async.core import AsyncStatus, StandardReadable
5
+ from ophyd_async.epics.core import epics_signal_r
6
+
7
+ from dodal.devices.hutch_shutter import HutchShutter, ShutterDemand
8
+ from dodal.log import LOGGER
9
+
10
+
11
+ class HutchInvalidError(Exception):
12
+ pass
13
+
14
+
15
+ class HutchState(str, Enum):
16
+ EH1 = "EH1"
17
+ EH2 = "EH2"
18
+ INVALID = "INVALID"
19
+
20
+
21
+ class HutchConditionalShutter(StandardReadable, Movable):
22
+ """ I19-specific device to operate the hutch shutter.
23
+
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.
27
+ As the two hutches are located in series, checking the hutch in use is necessary to \
28
+ avoid accidentally operating the shutter from one hutch while the other has beamtime.
29
+
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".
34
+ """
35
+
36
+ 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
+ self.hutch_request = hutch
41
+ super().__init__(name)
42
+
43
+ @AsyncStatus.wrap
44
+ async def set(self, value: ShutterDemand):
45
+ hutch_in_use = await self.hutch_state.get_value()
46
+ LOGGER.info(f"Current hutch in use: {hutch_in_use}")
47
+ if hutch_in_use == HutchState.INVALID:
48
+ raise HutchInvalidError(
49
+ "The hutch state is invalid. Contact the beamline staff."
50
+ )
51
+ if hutch_in_use != self.hutch_request:
52
+ # NOTE Warn but don't fail
53
+ LOGGER.warning(
54
+ f"{self.hutch_request} is not the hutch in use. Shutter will not be operated."
55
+ )
56
+ else:
57
+ await self.shutter.set(value)