dls-dodal 1.39.0__py3-none-any.whl → 1.41.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.39.0.dist-info → dls_dodal-1.41.0.dist-info}/METADATA +5 -3
  2. {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/RECORD +61 -52
  3. {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +9 -4
  5. dodal/beamlines/__init__.py +2 -0
  6. dodal/beamlines/adsim.py +3 -2
  7. dodal/beamlines/b01_1.py +3 -3
  8. dodal/beamlines/i03.py +141 -292
  9. dodal/beamlines/i04.py +112 -198
  10. dodal/beamlines/i13_1.py +5 -4
  11. dodal/beamlines/i18.py +124 -0
  12. dodal/beamlines/i19_1.py +74 -0
  13. dodal/beamlines/i19_2.py +61 -0
  14. dodal/beamlines/i20_1.py +37 -22
  15. dodal/beamlines/i22.py +7 -7
  16. dodal/beamlines/i23.py +8 -11
  17. dodal/beamlines/i24.py +100 -145
  18. dodal/beamlines/p38.py +84 -220
  19. dodal/beamlines/p45.py +5 -4
  20. dodal/beamlines/training_rig.py +4 -4
  21. dodal/common/beamlines/beamline_utils.py +2 -3
  22. dodal/common/beamlines/device_helpers.py +3 -1
  23. dodal/devices/aperturescatterguard.py +150 -64
  24. dodal/devices/apple2_undulator.py +89 -114
  25. dodal/devices/attenuator/attenuator.py +1 -1
  26. dodal/devices/backlight.py +1 -1
  27. dodal/devices/bimorph_mirror.py +2 -2
  28. dodal/devices/eiger.py +3 -2
  29. dodal/devices/fast_grid_scan.py +26 -19
  30. dodal/devices/hutch_shutter.py +26 -13
  31. dodal/devices/i10/i10_apple2.py +3 -3
  32. dodal/devices/i10/rasor/rasor_scaler_cards.py +4 -4
  33. dodal/devices/i13_1/merlin.py +4 -3
  34. dodal/devices/i13_1/merlin_controller.py +2 -7
  35. dodal/devices/i18/KBMirror.py +19 -0
  36. dodal/devices/i18/diode.py +17 -0
  37. dodal/devices/i18/table.py +14 -0
  38. dodal/devices/i18/thor_labs_stage.py +12 -0
  39. dodal/devices/i19/__init__.py +0 -0
  40. dodal/devices/i19/shutter.py +57 -0
  41. dodal/devices/i22/nxsas.py +4 -4
  42. dodal/devices/i24/pmac.py +2 -2
  43. dodal/devices/motors.py +2 -2
  44. dodal/devices/oav/oav_detector.py +10 -19
  45. dodal/devices/pressure_jump_cell.py +43 -19
  46. dodal/devices/robot.py +31 -12
  47. dodal/devices/tetramm.py +8 -3
  48. dodal/devices/thawer.py +4 -4
  49. dodal/devices/turbo_slit.py +7 -6
  50. dodal/devices/undulator.py +1 -1
  51. dodal/devices/undulator_dcm.py +1 -1
  52. dodal/devices/util/epics_util.py +1 -1
  53. dodal/devices/zebra/zebra.py +4 -3
  54. dodal/devices/zebra/zebra_controlled_shutter.py +1 -1
  55. dodal/devices/zocalo/zocalo_results.py +21 -4
  56. dodal/plan_stubs/wrapped.py +10 -12
  57. dodal/plans/save_panda.py +30 -14
  58. dodal/utils.py +55 -21
  59. {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/LICENSE +0 -0
  60. {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/entry_points.txt +0 -0
  61. {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -215,9 +200,12 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
215
200
  self.scan_invalid = epics_signal_r(float, f"{prefix}SCAN_INVALID")
216
201
 
217
202
  self.run_cmd = epics_signal_x(f"{prefix}RUN.PROC")
203
+ self.stop_cmd = epics_signal_x(f"{prefix}STOP.PROC")
218
204
  self.status = epics_signal_r(int, f"{prefix}SCAN_STATUS")
219
205
 
220
- self.expected_images = ExpectedImages(parent=self)
206
+ self.expected_images = create_hardware_backed_soft_signal(
207
+ float, self._calculate_expected_images
208
+ )
221
209
 
222
210
  self.motion_program = MotionProgram(smargon_prefix)
223
211
 
@@ -243,6 +231,17 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
243
231
  }
244
232
  super().__init__(name)
245
233
 
234
+ async def _calculate_expected_images(self):
235
+ x, y, z = await asyncio.gather(
236
+ self.x_steps.get_value(),
237
+ self.y_steps.get_value(),
238
+ self.z_steps.get_value(),
239
+ )
240
+ LOGGER.info(f"Reading num of images found {x, y, z} images in each axis")
241
+ first_grid = x * y
242
+ second_grid = x * z
243
+ return first_grid + second_grid
244
+
246
245
  @AsyncStatus.wrap
247
246
  async def kickoff(self):
248
247
  curr_prog = await self.motion_program.program_number.get_value()
@@ -259,7 +258,15 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
259
258
 
260
259
  @AsyncStatus.wrap
261
260
  async def complete(self):
262
- await wait_for_value(self.status, 0, self.COMPLETE_STATUS)
261
+ try:
262
+ await wait_for_value(self.status, 0, self.COMPLETE_STATUS)
263
+ except asyncio.TimeoutError:
264
+ LOGGER.error(
265
+ "Hyperion timed out waiting for FGS motion to complete. This may have been caused by a goniometer stage getting stuck.\n\
266
+ Forcibly stopping the FGS motion program..."
267
+ )
268
+ await self.stop_cmd.trigger()
269
+ raise
263
270
 
264
271
  @abstractmethod
265
272
  def _create_position_counter(self, prefix: str) -> SignalRW[int]:
@@ -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
 
@@ -48,7 +55,7 @@ class HutchInterlock(StandardReadable):
48
55
  return interlock_state == HUTCH_SAFE_FOR_OPERATIONS
49
56
 
50
57
 
51
- class HutchShutter(StandardReadable, Movable):
58
+ class HutchShutter(StandardReadable, Movable[ShutterDemand]):
52
59
  """Device to operate the hutch shutter.
53
60
 
54
61
  When a demand is sent, the device should first check the hutch status \
@@ -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
  )
@@ -175,7 +175,7 @@ class I10Apple2(Apple2):
175
175
  self._available_pol = list(self.lookup_tables["Gap"].keys())
176
176
 
177
177
 
178
- class I10Apple2PGM(StandardReadable, Movable):
178
+ class I10Apple2PGM(StandardReadable, Movable[float]):
179
179
  """
180
180
  Compound device to set both ID and PGM energy at the sample time,poly_deg
181
181
 
@@ -211,7 +211,7 @@ class I10Apple2PGM(StandardReadable, Movable):
211
211
  )
212
212
 
213
213
 
214
- class I10Apple2Pol(StandardReadable, Movable):
214
+ class I10Apple2Pol(StandardReadable, Movable[str]):
215
215
  """
216
216
  Compound device to set polorisation of ID.
217
217
  """
@@ -240,7 +240,7 @@ class I10Apple2Pol(StandardReadable, Movable):
240
240
  ) # Move id to new polarisation
241
241
 
242
242
 
243
- class LinearArbitraryAngle(StandardReadable, Movable):
243
+ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
244
244
  """
245
245
  Device to set polorisation angle of the ID. Linear Arbitrary Angle (laa)
246
246
  is the direction of the magnetic field which can be change by varying the jaw_phase
@@ -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[ShutterDemand]):
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)
@@ -103,7 +103,7 @@ class NXSasPilatus(PilatusDetector):
103
103
  prefix: str,
104
104
  path_provider: PathProvider,
105
105
  drv_suffix: str,
106
- hdf_suffix: str,
106
+ fileio_suffix: str,
107
107
  metadata_holder: NXSasMetadataHolder,
108
108
  name: str = "",
109
109
  ):
@@ -116,7 +116,7 @@ class NXSasPilatus(PilatusDetector):
116
116
  prefix,
117
117
  path_provider,
118
118
  drv_suffix=drv_suffix,
119
- hdf_suffix=hdf_suffix,
119
+ fileio_suffix=fileio_suffix,
120
120
  name=name,
121
121
  )
122
122
  self._metadata_holder = metadata_holder
@@ -146,7 +146,7 @@ class NXSasOAV(AravisDetector):
146
146
  prefix: str,
147
147
  path_provider: PathProvider,
148
148
  drv_suffix: str,
149
- hdf_suffix: str,
149
+ fileio_suffix: str,
150
150
  metadata_holder: NXSasMetadataHolder,
151
151
  name: str = "",
152
152
  gpio_number: AravisController.GPIO_NUMBER = 1,
@@ -160,7 +160,7 @@ class NXSasOAV(AravisDetector):
160
160
  prefix,
161
161
  path_provider,
162
162
  drv_suffix=drv_suffix,
163
- hdf_suffix=hdf_suffix,
163
+ fileio_suffix=fileio_suffix,
164
164
  name=name,
165
165
  gpio_number=gpio_number,
166
166
  )
dodal/devices/i24/pmac.py CHANGED
@@ -71,7 +71,7 @@ class PMACStringMove(Triggerable):
71
71
  await self.signal_ref().set(self.cmd_string, wait=True)
72
72
 
73
73
 
74
- class PMACStringLaser(Device, Movable):
74
+ class PMACStringLaser(Device, Movable[LaserSettings]):
75
75
  """Set the pmac_string to control the laser."""
76
76
 
77
77
  def __init__(
@@ -90,7 +90,7 @@ class PMACStringLaser(Device, Movable):
90
90
  await self._signal_ref().set(value.value)
91
91
 
92
92
 
93
- class PMACStringEncReset(Device, Movable):
93
+ class PMACStringEncReset(Device, Movable[EncReset]):
94
94
  """Set a pmac_string to control the encoder channels in the controller."""
95
95
 
96
96
  def __init__(
dodal/devices/motors.py CHANGED
@@ -19,10 +19,10 @@ class XYZPositioner(StandardReadable):
19
19
  Notes
20
20
  -----
21
21
  Example usage::
22
- async with DeviceCollector():
22
+ async with init_devices():
23
23
  xyz_stage = XYZPositioner("BLXX-MO-STAGE-XX:")
24
24
  Or::
25
- with DeviceCollector():
25
+ with init_devices():
26
26
  xyz_stage = XYZPositioner("BLXX-MO-STAGE-XX:", infix = ("A", "B", "C"))
27
27
 
28
28
  """
@@ -1,6 +1,12 @@
1
1
  from enum import IntEnum
2
2
 
3
- from ophyd_async.core import DEFAULT_TIMEOUT, AsyncStatus, LazyMock, StandardReadable
3
+ from bluesky.protocols import Movable
4
+ from ophyd_async.core import (
5
+ DEFAULT_TIMEOUT,
6
+ AsyncStatus,
7
+ LazyMock,
8
+ StandardReadable,
9
+ )
4
10
  from ophyd_async.epics.core import epics_signal_rw
5
11
 
6
12
  from dodal.common.signal_utils import create_hardware_backed_soft_signal
@@ -8,12 +14,6 @@ from dodal.devices.areadetector.plugins.CAM import Cam
8
14
  from dodal.devices.oav.oav_parameters import DEFAULT_OAV_WINDOW, OAVConfig
9
15
  from dodal.devices.oav.snapshots.snapshot_with_beam_centre import SnapshotWithBeamCentre
10
16
  from dodal.devices.oav.snapshots.snapshot_with_grid import SnapshotWithGrid
11
- from dodal.log import LOGGER
12
-
13
-
14
- class ZoomLevelNotFoundError(Exception):
15
- def __init__(self, errmsg):
16
- LOGGER.error(errmsg)
17
17
 
18
18
 
19
19
  class Coords(IntEnum):
@@ -29,7 +29,7 @@ def _get_correct_zoom_string(zoom: str) -> str:
29
29
  return zoom
30
30
 
31
31
 
32
- class ZoomController(StandardReadable):
32
+ class ZoomController(StandardReadable, Movable[str]):
33
33
  """
34
34
  Device to control the zoom level. This should be set like
35
35
  o = OAV(name="oav")
@@ -46,18 +46,9 @@ class ZoomController(StandardReadable):
46
46
  self.level = epics_signal_rw(str, f"{prefix}MP:SELECT")
47
47
  super().__init__(name=name)
48
48
 
49
- async def _get_allowed_zoom_levels(self) -> list:
50
- zoom_levels = await self.level.describe()
51
- return zoom_levels[self.level.name]["choices"] # type: ignore
52
-
53
49
  @AsyncStatus.wrap
54
- async def set(self, level_to_set: str):
55
- allowed_zoom_levels = await self._get_allowed_zoom_levels()
56
- if level_to_set not in allowed_zoom_levels:
57
- raise ZoomLevelNotFoundError(
58
- f"{level_to_set} not found, expected one of {allowed_zoom_levels}"
59
- )
60
- await self.level.set(level_to_set, wait=True)
50
+ async def set(self, value: str):
51
+ await self.level.set(value, wait=True)
61
52
 
62
53
 
63
54
  class OAV(StandardReadable):
@@ -41,8 +41,8 @@ class ValveControlRequest(StrictEnum):
41
41
 
42
42
 
43
43
  class ValveOpenSeqRequest(StrictEnum):
44
- INACTIVE = 0
45
- OPEN_SEQ = 1
44
+ INACTIVE = "0"
45
+ OPEN_SEQ = "1"
46
46
 
47
47
 
48
48
  class PumpMotorDirectionState(StrictEnum):
@@ -76,7 +76,7 @@ class AllValvesControlState:
76
76
  valve_6: FastValveControlRequest | None = None
77
77
 
78
78
 
79
- class AllValvesControl(StandardReadable, Movable):
79
+ class AllValvesControl(StandardReadable, Movable[AllValvesControlState]):
80
80
  """
81
81
  valves 2, 4, 7, 8 are not controlled by the IOC,
82
82
  as they are under manual control.
@@ -151,11 +151,13 @@ class AllValvesControl(StandardReadable, Movable):
151
151
  )
152
152
 
153
153
 
154
- class ValveControl(StandardReadable):
154
+ class ValveControl(
155
+ StandardReadable, Movable[ValveControlRequest | ValveOpenSeqRequest]
156
+ ):
155
157
  def __init__(self, prefix: str, name: str = "") -> None:
156
158
  with self.add_children_as_readables():
157
159
  self.close = epics_signal_rw(ValveControlRequest, prefix + ":CON")
158
- self.open = epics_signal_rw(ValveOpenSeqRequest, prefix + ":OPENSEQ")
160
+ self.open = epics_signal_rw(int, prefix + ":OPENSEQ")
159
161
 
160
162
  super().__init__(name)
161
163
 
@@ -165,16 +167,18 @@ class ValveControl(StandardReadable):
165
167
  if isinstance(value, ValveControlRequest):
166
168
  set_status = self.close.set(value)
167
169
  elif isinstance(value, ValveOpenSeqRequest):
168
- set_status = self.open.set(value)
170
+ set_status = self.open.set(value.value)
169
171
 
170
172
  return set_status
171
173
 
172
174
 
173
- class FastValveControl(StandardReadable):
175
+ class FastValveControl(
176
+ StandardReadable, Movable[FastValveControlRequest | ValveOpenSeqRequest]
177
+ ):
174
178
  def __init__(self, prefix: str, name: str = "") -> None:
175
179
  with self.add_children_as_readables():
176
180
  self.close = epics_signal_rw(FastValveControlRequest, prefix + ":CON")
177
- self.open = epics_signal_rw(ValveOpenSeqRequest, prefix + ":OPENSEQ")
181
+ self.open = epics_signal_rw(int, prefix + ":OPENSEQ")
178
182
 
179
183
  super().__init__(name)
180
184
 
@@ -184,7 +188,7 @@ class FastValveControl(StandardReadable):
184
188
  if isinstance(value, FastValveControlRequest):
185
189
  set_status = self.close.set(value)
186
190
  elif isinstance(value, ValveOpenSeqRequest):
187
- set_status = self.open.set(value)
191
+ set_status = self.open.set(value.value)
188
192
 
189
193
  return set_status
190
194
 
@@ -219,21 +223,27 @@ class PressureTransducer(StandardReadable):
219
223
  self,
220
224
  prefix: str,
221
225
  cell_prefix: str,
222
- number: int,
226
+ transducer_number: int,
227
+ ethercat_channel_number: int,
223
228
  name: str = "",
224
229
  full_different_prefix_adc: str = "",
225
230
  ) -> None:
226
231
  final_prefix = f"{prefix}{cell_prefix}"
227
232
  with self.add_children_as_readables():
228
233
  self.omron_pressure = epics_signal_r(
229
- float, f"{final_prefix}PP{number}:PRES"
234
+ float, f"{final_prefix}PP{transducer_number}:PRES"
235
+ )
236
+ self.omron_voltage = epics_signal_r(
237
+ float, f"{final_prefix}PP{transducer_number}:RAW"
230
238
  )
231
- self.omron_voltage = epics_signal_r(float, f"{final_prefix}PP{number}:RAW")
232
239
  self.beckhoff_pressure = epics_signal_r(
233
- float, f"{final_prefix}STATP{number}:MeanValue_RBV"
240
+ float, f"{final_prefix}STATP{transducer_number}:MeanValue_RBV"
234
241
  )
242
+ # P1 beckhoff voltage = BL38P-EA-ADC-02:CH1
243
+ # P2 beckhoff voltage = BL38P-EA-ADC-01:CH2
244
+ # P3 beckhoff voltage = BL38P-EA-ADC-01:CH1
235
245
  self.slow_beckhoff_voltage_readout = epics_signal_r(
236
- float, f"{full_different_prefix_adc}CH1"
246
+ float, f"{full_different_prefix_adc}CH{ethercat_channel_number}"
237
247
  )
238
248
 
239
249
  super().__init__(name)
@@ -284,13 +294,27 @@ class PressureJumpCell(StandardReadable):
284
294
  with self.add_children_as_readables():
285
295
  self.pressure_transducers: DeviceVector[PressureTransducer] = DeviceVector(
286
296
  {
287
- i: PressureTransducer(
297
+ 1: PressureTransducer(
298
+ prefix=prefix,
299
+ cell_prefix=cell_prefix,
300
+ transducer_number=1,
301
+ full_different_prefix_adc=f"{prefix}{adc_prefix}-02:",
302
+ ethercat_channel_number=1,
303
+ ),
304
+ 2: PressureTransducer(
305
+ prefix=prefix,
306
+ cell_prefix=cell_prefix,
307
+ transducer_number=2,
308
+ full_different_prefix_adc=f"{prefix}{adc_prefix}-01:",
309
+ ethercat_channel_number=2,
310
+ ),
311
+ 3: PressureTransducer(
288
312
  prefix=prefix,
289
- number=i,
290
313
  cell_prefix=cell_prefix,
291
- full_different_prefix_adc=f"{prefix}{adc_prefix}-0{i}:",
292
- )
293
- for i in [1, 2, 3]
314
+ transducer_number=3,
315
+ full_different_prefix_adc=f"{prefix}{adc_prefix}-01:",
316
+ ethercat_channel_number=1,
317
+ ),
294
318
  }
295
319
  )
296
320